LaravelからS3互換のMinIOを使えるように、docker-compose環境を整える

こんにちは。小國です。最近は Laravel を触っています。

AWS で Laravel アプリケーションを運用する際、ステートレスにするために画像などのファイルを S3 に保管することがあるかと思います。

一方、弊社では Docker を使ってローカルの開発環境を整えており、そこでは S3 の代わりに S3 互換の MinIO を使用しています(使用していこうと思います)。

本記事では、Docker で MinIO の設定、および Laravel から MinIO へファイルの作成・削除・ダウロードをご紹介します。

なお、前提として、すでに Docker(docker-compose)で Laravel アプリケーションが動いているものとします。

環境

  • Laravel 6.1.0

Docker で MinIO を立ち上げる

まずは、docker-compose を使って MinIO を起動します。

  • .env
+# Minio config
+MINIO_PORT=60007
+
+# AWS config
+AWS_URL=http://minio:9000
+AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXXXXXXX
+AWS_SECRET_ACCESS_KEY=YYYYYYYYYYYYYYYYYYYY
+AWS_DEFAULT_REGION=us-east-1
+AWS_BUCKET=test
+AWS_PATH_STYLE_ENDPOINT=true
  • docker-compose.yml
+  minio:
+    image: minio/minio 
+    ports:
+      - "${MINIO_PORT}:9000"
+    volumes:
+      - ./.docker/minio/data:/export
+    environment:
+      MINIO_ACCESS_KEY: ${AWS_ACCESS_KEY_ID}
+      MINIO_SECRET_KEY: ${AWS_SECRET_ACCESS_KEY}
+    command: server /export

ホストマシンから MinIO が動いているか確認

docker-compose up -d 後、ホストマシンから http://localhost:60007 につながることを確認します。正しく起動できると以下のような画面が表示されると思います。

f:id:seeds-std:20191108194045p:plain

設定した $AWS_ACCESS_KEY_ID$AWS_SECRET_ACCESS_KEY でログインします。

f:id:seeds-std:20191108192902p:plain

右下の「+」ボタンより、test バケットを作成し(のちほどこのバケットを使用します)、ファイルがアップロードができるか確認しましょう。

Laravel から s3ドライバーで MinIO を使うように変更

s3ドライバー で MinIO を使うよう変更し、Tinker を使って Laravel から保存できることを確認します。

  • flysystem-aws-s3-v3 インストール
$ composer require league/flysystem-aws-s3-v3 ~1.0
  • config/filesystems.php
         's3' => [
             'driver' => 's3',
+            'endpoint' => env('AWS_URL'),
+            'use_path_style_endpoint' => env('AWS_PATH_STYLE_ENDPOINT', false),
             'key' => env('AWS_ACCESS_KEY_ID'),
             'secret' => env('AWS_SECRET_ACCESS_KEY'),
             'region' => env('AWS_DEFAULT_REGION'),
$ php artisan tinker
>>> Storage::disk('s3')->put('hello.json', '{"hello": "world"}')
=> true

MinIO にファイルが作成されているかと思います。

ファイルの作成・削除・ダウロード

Laravel から MinIO へファイルの作成・削除・ダウロードをやってみます。

  • 2019_11_11_020835_create_assets_table.php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateAssetsTable extends Migration
{
    public function up()
    {
        Schema::create('assets', function (Blueprint $table) {
            $table->increments('id');
            $table->string('model')->nullable();
            $table->integer('foreign_key')->nullable();
            $table->string('name');
            $table->string('type');
            $table->integer('size');
            $table->string('disk');
            $table->string('path');
            $table->timestamps();
            $table->index(['model', 'foreign_key']);
        });
    }

    public function down()
    {
        Schema::dropIfExists('assets');
    }
}
  • app/Asset.php
<?php

namespace App;
use Illuminate\Database\Eloquent\Model;
class Asset extends Model
{
    protected $fillable = [
        'foreign_key',
        'model',
        'name',
        'type',
        'size',
        'disk',
        'path',
    ];
}
  • routes/web.php
+    // Asset Routes...
+    Route::get('assets/{asset}/download', 'AssetController@download')->name('assets.download');
+    Route::resource('assets', 'AssetController')->only(['index', 'create', 'store', 'destroy']);
  • app/Http/Controllers/AssetController.php
<?php

namespace App\Http\Controllers;
use App\Asset;
use App\Http\Requests\StoreAsset;
use Illuminate\Support\Facades\Storage;
class AssetController extends Controller
{
    public function index()
    {
        $assets = Asset::query()->paginate();
        return view('asset.index', compact('assets'));
    }

    public function create()
    {
        return view('asset.create');
    }

    public function store(StoreAsset $request)
    {
        $file = $request->file('file');
        $path = $file->store('assets', 's3');
        if (!$path) {
            abort(500);
        }

        $asset = new Asset([
            'model' => Asset:class,
            'name' => $file->getClientOriginalName(),
            'size' => $file->getSize(),
            'type' => $file->getMimeType(),
            'path' => $path
            'disk' => 's3'
        ]);
        if ($asset->save()) {
            return redirect()->route('assets.index')->with('success', __('messages.saved'));
        }

        return redirect()->route('assets.index')->with('error', __('messages.could_not_be_saved'));
    }

    public function destroy(Asset $asset)
    {
        if (Storage::disk($asset->disk)->exists($asset->path) && !Storage::disk($asset->disk)->delete($asset->path)) {
            abort(500);
        }

        if ($asset->delete()) {
            return back()->with('success', __('messages.deleted'));
        }

        return back()->with('error', __('messages.could_not_be_deleted'));
    }

    public function download(Asset $asset)
    {
        return Storage::disk($asset->disk)->download($asset->path);
    }
}
  • app/Http/Requests/StoreAsset.php
<?php

namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreAsset extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'file' => 'required'
        ];
    }
}
  • resources/views/asset/create.blade.php
@extends('layouts.app')
@section('content')
<div class="card">
    <div class="card-header">
      {{ __('Create New') }}
    </div>
    <div class="card-body">
        <form method="post" action="{{ route('assets.store') }}" enctype="multipart/form-data">
        @csrf
        <div class="form-group">
            <label for="name">{{ __('File') }}</label>
            <input type="file" class="form-control" name="file"/>
        </div>
        <button type="submit" class="btn btn-primary" dusk="upload">{{ __('Upload') }}</button>
        </form>
    </div>
</div>
@endsection
  • resources/views/asset/index.blade.php
@extends('layouts.app')
@section('content')
<div class="card">
    <div class="card-header">
        {{ __('Assets') }}
    </div>
    <div class="card-body">
        <table class="table">
        <thead>
            <th>{{ __('ID') }}</th>
            <th>{{ __('Image') }}</th>
            <th>{{ __('File Name') }}</th>
            <th>{{ __('File Size') }}</th>
            <th>{{ __('Created At') }}</th>
            <th>{{ __('Actions') }}</th>
        </thead>
        <tbody>
        @foreach($assets as $asset)
            <tr>
                <td>{{ $asset->id }}</td>
                <td><img src="{{ route('assets.download', $asset->id) }}" width="150"></td>
                <td>{{ $asset->name }}</td>
                <td>{{ number_format($asset->size) }} Bytes</td>
                <td>{{ $asset->created_at }}</td>
                <td>
                    <form action="{{ route('assets.destroy', $asset->id)}}" method="post" class="d-inline">
                    @csrf
                    @method('DELETE')
                    <button class="btn btn-danger" type="submit" dusk="delete">{{ __('Delete') }}</button>
                    </form>
                </td>
            </tr>
        @endforeach
        </tbody>
     </table>
    {{ $assets->->appends(request()->query())->links() }}
    </div>
</div>
@endsection

まとめ

ローカルの開発環境では s3ドライバーを使って MinIO に保存し、AWS で運用時には S3 にそのまま保存する環境を作りました。

参考サイト

WSL と VSCode を使って Vue の開発環境を整えてみる

こんにちは, Web事業部の西村です
今回はタイトルにもある通り WSL(Windows Subsystem for Linux) と VSCode(Visual Studio Code) を用いてVueの環境を整えてみたいと思います

目次

なぜWSLを使うの?

まず, なぜWSLを使うかというお話です
cross-env というnpmパッケージがあります
npmスクリプトを実行する際に, 任意の環境変数に変更できるというものです
しかし, このパッケージはWindowsでは正常に動作しません
そこで, WSL内のLinux環境を使って開発を進め, 快適に開発しようということです

この記事で取り扱わないこと

  • WSLに関する解説
  • Linuxの解説
  • cross-envの解説
  • Vueに関する解説

WSLのインストール

1: 設定を開き アプリ を選択します
2: 画面右側にある 関連設定プログラムと機能 を選択します
3: 新たに出たウィンドウにある Windowsの機能の有効化または無効化 を選択します
4: さらにウィンドウが出るので Linux用Windowsサブシステム にチェックを入れます
5: 再起動が促されるので再起動します

Linuxディストリビューションのインストール

下記のリンクからMicrosoft Storeに移動し好きなディストリビューションをインストールします
WindowsでLinuxを実行する

この記事では Ubuntu をインストールしたものとして解説します

VSCodeのインストール + 拡張機能のインストール

1: 下記のサイトからインストーラーをダウンロードしインストールします

⚠注意
インストール中 PATHへの追加 という項目がありますが, 必ずチェックを入れておいてください

2: インストール完了後起動させ, 左側のタブから Extensions を選択します
3: Remote と検索し Remote Development を追加します
4: VSCodeを閉じます

Linuxの準備

1: 先ほどインストールしたディストリビューションを起動させます (起動させるとターミナルが開き初期化が始まります)
2: ユーザ名 / パスワードの入力が促されるので設定します

Installing, this may take a few minutes...
Please create a default UNIX user account. The username does not need to match your Windows username.
For more information visit: https://aka.ms/wslusers
Enter new UNIX username: <ユーザ名>
Enter new UNIX password: <パスワード (表示されません)>
Retype new UNIX password: <パスワード確認 (表示されません)>
passwd: password updated successfully
Installation successful!
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

ユーザ名@PC名:~$

3: 下記のコマンドを順番に実行しアップデートを行います

:~$ sudo apt update
:~$ sudo apt upgrade -y

⚠注意
アップデート中に YES/NO を聞かれる部分があると思いますが, YESを選択してください

4: 下記のコマンドを実行し Node.js をインストールします

:~$ curl -sL https://deb.nodesource.com/setup_13.x | sudo -E bash -
:~$ sudo apt install -y nodejs

5: 下記のコマンドを実行し Vue CLI をインストールします

:~$ sudo npm install -g @vue/cli

6: サンプルのプロジェクトを作成するために下記のコマンドを実行します

:~$ vue create sample_project

すると次のような表示が出ますのでそのまま Enter します

Vue CLI v4.0.5
? Please pick a preset: (Use arrow keys)
❯ default (babel, eslint)
  Manually select features

しばらくすると初期パッケージのインストールが完了します

7: インストールが完了したらVSCodeで先ほど作成したWSL内のプロジェクトを開きます

:~$ code sample_project/

⚠注意
ここでVSCodeが起動しない場合,パスが設定できていませんので再度インストールしてみてください

ここまでの手順がうまくいった場合, WSL内のプロジェクトがWindowsのVSCodeで見ることができるようになっているはずです
また, 先ほどまで利用していたターミナルは閉じても問題ないです

:~$ exit

追加の拡張機能と動作確認

1: 起動したVSCodeの ExtensionsVue と検索し, Vetur という拡張機能をインストールします
2: Ctrl + Shift + @ を押しターミナルを起動させ下記のコマンドを実行します

:~/sample_project$ npm run serve

するとビルドが始まり, 開発用のサーバが起動します

3: http://localhost:8080/ にアクセスし, 画面が表示されるかどうかを確認します

最後に

いかがでしょうか, Windowsのみを使い続けている人にはきつい内容かもしれませんが, 1度整えてしまうと使いやすい環境なのではないでしょうか?
Windowsだけれども, UbuntuやMacみたいに開発できるといった点ではメリットなのかもしれません
しかし, WSLでは動作が遅いため, 来年に一般公開される予定の WSL2 が待ち遠しい限りです(WSL2では速度面が大幅に解消されています)
ここまで読んでいただきありがとうございました

蛇足?

(A ・ω・)Dockerでもいいんじゃね?  うん(・ω・私)

AWS Chatbot(beta版)を用いたSlack通知

クラウド事業部 インフラエンジニアの吉岡です。

AWS Chatbot(beta版)を用いたSlack通知を試してみましたので、ご紹介したいと思います。

AWS Chatbotとはどういった機能なのか

Slack チャンネルや Amazon Chime チャットルームに対して通知を行う機能です。
サポートしているサービスの通知をSNSトピックに転送し、
SNSトピックからChatbotを経由して各チャットサービスに表示することができます。
現在対応しているサービスは下記の通りです。

AWS Billing and Cost Managemen
AWS CloudFormation
AWS CodeBuild等の開発者用ツール
Amazon CloudWatch Alarms
Amazon CloudWatch Events
AWS Config
AWS Health
AWS Security Hub
Amazon GuardDuty
AWS Systems Manager

今回はAWS System ManagerのRunCommandを実行し、Slackに通知します。設定する通知フローは下記の図の通りです。

Slackへの通知フロー

手順

・SNSトピックの作成
Amazon SNS→トピック→トピックの作成と進み、適当な名前を付けたトピックを作成します。(サブスクリプションはまだ設定しません。)

f:id:seeds-std:20191114181702p:plain

・CloudWatchEventの設定
CloudWatch→ルールの作成、と進みルールを作成します。
今回はchatbot-testという名前で作成しました。

サービス名:EC2 Simple Systems Manager(SSM)
f:id:seeds-std:20191114182349p:plain

イベントタイプ:すべてのイベント
f:id:seeds-std:20191114182353p:plain

ターゲット:SNSトピック(先ほど作成したトピック)
f:id:seeds-std:20191114182357p:plain

・Chatbotのインストール
AWS Chatbotを選択し、
Chatbotの設定を始めていきます。
通知を送る先となるChatツールを選択します。

f:id:seeds-std:20191114182950p:plain

SlackにChatbotをインストールします。

f:id:seeds-std:20191114183017p:plain

今回はpublicのslacktestというチャンネルに通知するように設定します。
IAMロールは自動作成、ロール名はchatbot-slackとしました。
SNS regionはSNSトピックの設定されているリージョン(今回は東京リージョン)
SNS topicsは先ほど作成したchatbot-testを指定します。

f:id:seeds-std:20191114183152p:plain
f:id:seeds-std:20191114183155p:plain
f:id:seeds-std:20191114183157p:plain

設定が完了すると、SNSトピックにサブスクリプションが追加されていることが確認できます。

動作確認

RunCommandを実行し、Slackに通知されることを確認します。
今回はAWS-RunShellScriptでechoコマンドを実行します。
インスタンス指定
SNS通知なし
の条件で下記の内容で実行します。

#!/bin/bash
echo "Hello chatbot"

実行後に下記の様な通知がSlackに届いていることが確認できました。

f:id:seeds-std:20191114183614p:plain

あとがき

今回はChatbotを用いてSlack通知を行うために必要な作業がどういったものか、勉強を兼ねて試してみましたが、非常に簡単に設定ができました。
現在はbeta版ですので、一部の環境のみですが、シーズではこの機能を利用してAWS Health Dashboardの情報をslack通知する設定を行っています。

CloudTrailが有効化されていることが前提ですが、セッションマネージャーを使ったログイン時に通知が届くような設定もできるようです。
正式なリリースが楽しみですね!

デザインにおける余白の重要性

どうもこんにちは!Webデザイナーの衣笠です。

シーズに入社して、はや4ヶ月。
「仕事終わりのビールが美味しい」と思える日が来るなんて、大人になった…なんて思っています。

この4ヶ月間、実際の案件を通し先輩にフィードバックをもらうことで、デザインの考え方やノウハウなど、多くのことを学んできました。

タイポグラフィや配色など、デザインを構成する重要ポイントは様々ですが、今回はそのなかでも特に学びが多かった「余白の重要性」について書いてみたいと思います。

●余白とは?


「余白」というとその字から、「余った空白の部分」という認識をもたれるかも知れません。
しかしデザインにおける余白とは、デザインをする上で最終的に「余ったもの」ではなく、
デザインを行う最初の段階で設計し作り出すもの
といえます。

余白を上手く使うことで

  • ユーザビリティの向上
  • デザインの印象をつくる
    など、様々なメリットがあります。

制作物に適した余白を設計するためには、まずは余白の役割を知ることが大事です。

●余白の役割


・情報をグループ化する

f:id:seeds-std:20191111201906p:plain

余白を使うことで、コンテンツごとにまとまりを作り、情報を整理することができます。
コンテンツの内容を読む前でも、一目で同じ情報ということが、視覚的に分かります。

・文字の可読性を上げる

f:id:seeds-std:20191111201920p:plain

ユーザーにストレスを与えず、文章を読んでもらうために、文字周りの余白は大切です。
各要素間の余白(見出し、本文、テキストが入るボックス)のみならず、
文字間や行間なども、文字の可読性を上げる重要ポイントです。

・視線の誘導を行う

f:id:seeds-std:20191111201932p:plain

要素間の余白を調整することで、ユーザーの視線の動きをコントロールすることも可能です。
上の図においては、ユーザーの視線は左上から右下へと「Z」の字のように流れるように、余白の近接間を調整し、視線の流れを設計しています。

・ユーザーの行動を補助する(手順を示す)

f:id:seeds-std:20191111201946p:plain

重要なボタンの周囲には、余白を十分にとり他の要素を配置しないなど、目立たせたい要素を明確にすることで、ユーザーの行動を補助することも可能です。

余白を使用し、ユーザーが次に取るべき行動を分かりやすく提示することで、ユーザーの迷いをなくす・負荷のかからない設計を行えます。

・デザインの印象をつくる

余白の役割は、ユーザビリティの面だけでなく、デザインのビジュアルにも大きな影響を与えます。
まだ余白の感覚が掴めていない新人デザイナーは、余白を恐れて詰め込みがちなデザインをする傾向があるといいますが、余白を上手く取り入れることで、ゆったりとした高級感のあるデザインを作成することも可能です。

●余白は大事!


余白において一番大切な役割は、「ユーザビリティの向上」です。

  • 情報整理がされておらず、文字も読みづらい
  • 伝えたいことがひと目で明確に分からない

このようなデザインでは、どんなに良いコンテンツが掲載されていても、ユーザーには届きません。

「余白」が重要なのは、デザイン全般的に言えることですが、Webデザインだとその重要性がより顕著に現れると思います。
ユーザーはサイトをくまなくチェックするわけではなく、
「このサイトに自分が求めている情報はあるか?」と、情報を流し読みしています。

ユーザーの求める情報を掲載していたとしても、それが目に留まらず、
「自分の求めている情報が載っているか分からない」と判断されてしまうと、
サイトから離脱(→他のサイトに移る)となってしまいます。

明確に情報を提示し、ユーザーの視線を導くために、余白は大きな役割を果たします

●まとめ


以上、今回は「デザインにおける余白の重要性」というテーマでお送りいたしました。

デザイナーとしてレベルアップしていくためには、余白の使い方をマスターする必要がありそうですね!
より良いデザインができるように、日々精進していきたいと思います!

Kotlin/JSのAWS Lambda関数でJsonを返してみる

f:id:seeds-std:20191010160615p:plain
こんにちは, Web事業部の西村です
私の前回の記事 Promiseを利用して非同期に処理するようにしてみました
今回はこのLambda関数で kotlinx.serialization を用いてJsonを返すようにしてみたいと思います

目次

過去の記事

なぜ kotlinx.serialization?

まず kotlinx.serialization を用いる理由です
ここまでの記事を読んでくださっている方々であれば “わざわざないでしょ” と思っている方もいると思います
その通りで, json()JSON.stringifiy() 関数を用いればJsonを返すことができます
しかし, kotlinx.serialization を用いることにより, Client側でも同じクラスを利用することができ, 開発する際に便利であるといえます
またマルチプラットフォームであるため様々な環境で利用できるというのもメリットです

注意事項

この記事では kotlinx.serialization に関する詳しい解説は行いません
詳しく知りたい方は下記のリポジトリをご覧ください
github.com

開発環境

この記事では下記の環境で開発を行っています

  • AdoptOpenJDK 1.8.0_222-b10
  • IntelliJ IDEA Community 2019.2.2
  • Kotlin 1.3.50

プロジェクト

前回の記事 まで作成したプロジェクトを利用します
プロジェクトの構成は下記のようになっています

KotlinLambda
├─.gradle/
├─.idea/
├─build/
├─gradle/
├─src/
│ └─main/
│   └─kotlin/
│     └─jp.co.seeds_std.lambda.kotlin/
│       └─handler.kt
├─build.gradle.kts
├─compress.zip
├─gradlew
├─gradlew.bat
└─settings.gradle.kts

また, handler.ktは下記のようになっています

package jp.co.seeds_std.lambda.kotlin
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.promise
import kotlin.js.Json
import kotlin.js.json
@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
val tasks = (1..10).map {
async {
delay(1000)
it
}
}
val results = tasks.awaitAll()
return@promise Response(body = "Result: ${results.sum()} - Coroutines")
}
data class Response(
val statusCode: Int = 200,
val body: String = "{}",
val isBase64Encoded: Boolean = false,
val headers: Json = json(),
val multiValueHeaders: Json = json()
)

Jsonを返すようにする

目標

今回は下記のようなJsonを返すようにしてみます

{
"id": 0, // ID
"text": "", // 文字列
"random_numbers": [5, 38, 24] // 適当な数値
}

依存関係を追加する

kotlinx.serialization は外部ライブラリですので依存関係を追加する必要があります
また, 専用のプラグインも必要であるためプラグインの追加も行います

build.gradle.kts / pluginsブロック

plugins {
kotlin("js") version "1.3.50"
kotlin("plugin.serialization") version "1.3.50"
}

依存関係の追加

build.gradle.kts / dependenciesブロック

sourceSets["main"].dependencies {
implementation(kotlin("stdlib-js"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.3.2")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:0.13.0") // 追加
}

リポジトリに jcenter を追加する必要があるので追加します

build.gradle.kts / repositories

repositories {
mavenCentral()
jcenter() // 追加
}

クライアント側に返すクラスを作成する

作成するクラスが今回のJsonに変換するクラスとなるので各種アノテーションも付与します
今回は Responseクラス の下に作成します(本当は別ファイルにするほうがいいです)

handler.kt

@Serializable
data class ResponseBody(
@SerialName("id")
val id: Int = 0,
@SerialName("text")
val text: String = "",
@SerialName("random_numbers")
val randomNumbers: List<Int> = emptyList()
)
  • @Serializable これがついているクラスがシリアライズ/デシリアライズすることができます
  • @SerialName(String) 引数で決められた文字列がkeyとなるようになります, ない場合は変数名が利用されるようになるのでなくても問題ないです

Jsonを返す準備をする

まずは前回までに書いていたコードの一部を削除しスッキリさせます

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
return@promise Response(body = "")
}

続いて, 先ほど作ったクラスを文字列に変換するために kotlinxのJsonオブジェクト を作成します

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
val json = kotlinx.serialization.json.Json(JsonConfiguration.Stable) // 追加
return@promise Response(body = "")
}

⚠注意1
今回の記事ではすでにkotlinが実装している Jsonクラス をインポートしているため同じ名前であるkotlinxの Jsonクラス をインポートできません
kotlinのJsonkotlinxのJson とを間違わないよう注意してください

⚠注意2
JsonConfiguration には Stable 以外の設定もありますが, 実験的機能となっていますので利用はお勧めしません

Jsonを返す

まずは先ほど作成した ResponseBodyクラス のオブジェクトを作成します

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
val json = kotlinx.serialization.json.Json(JsonConfiguration.Stable)
val responseBody = ResponseBody(1, "Lambda Json!!!", List(3) { Random.nextInt(100) }) // 追加
return@promise Response(body = "")
}

最後に stringify関数 を用いてクラスを文字列に変換し レスポンスのbody に設定します

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
val json = kotlinx.serialization.json.Json(JsonConfiguration.Stable)
val responseBody = ResponseBody(1, "Lambda Json!!!", List(3) { Random.nextInt(100) })
return@promise Response(body = json.stringify(ResponseBody.serializer(), responseBody)) // 変更
}
  • ResponseBody.serializer() この関数は @Serializable アノテーションがついているクラスの場合コンパイラが自動的に作成してくれます

動作確認

関数をデプロイし, API GatewayのURLにアクセスし {"id":1,"text":"Lambda Json!!!","random_numbers":[ランダムな数字が3つ]} 表示されれば成功です

最後に

いかがでしたでしょうか
LambdaでJsonを返すのはありがちです
これを利用することによりKotlinで作成するマルチプラットフォームなアプリケーションではかなり強力になりうると考えます
また, ここまで4つの記事に分け Kotlin/JS をもちいて AWS Lambda関数 を作って便利にしてみました
これがベストプラクティスかどうかはプロジェクトによると思いますが、1つの参考になるとうれしい限りです
ここまで読んでいただきありがとうございました


蛇足

JSON.stringify() を使ってJsonを返してみる

JavaScriptにもとから存在する, JSON.stringify を用いてもJsonを返すことができます

handler.js

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
val responseBody = ResponseBody(1, "Lambda Json!!!", listOf(0, 1, 2))
return@promise Response(body = JSON.stringify(responseBody))
}

実行結果

{"id":1,"text":"Lambda Json!!!","randomNumbers":[0,1,2]}

しかしこれではJsonのKeyは指定できないので変数をすべて private に変更すると結果が変わるので注意が必要です

privateに変更したときの実行結果

{"id_0":1,"text_0":"Lambda Json!!!","randomNumbers_0":[0,1,2]}

こういうケースもあるため kotlinx.serialization を用いることをお勧めします

json() と JSON.stringify() を使ってJsonを返してみる

一番オーソドックスでわかりやすい返し方だと私は思います

handler.js

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
val responseBody = json(
"id" to 0,
"text" to "Lambda Json!!!",
"random_numbers" to listOf(0, 1, 2)
)
return@promise Response(body = JSON.stringify(responseBody))
}

実行結果

{"id":1,"text":"Lambda Json!!!","random_numbers":[0,1,2]}

蛇足まで読んでいただきありがとうございました

open-wcでWeb Componentsをつくる

f:id:seeds-std:20191024163712p:plain

こんにちは、プログラマーの衣笠です。
Web Componentsの各ブラウザでの実装がそろってきたのでそろそろ触ってみようと調べていたら、open-wcというものを見つけました。ツールを用意してくれていたので今回はこれを使って簡単なコンポーネントをひとつ作ってみたいと思います。

open-wc とは

Web Componentsを作成、共有する上での推奨事項をまとめてそれをツールとして提供することを目的とした集まりです。

open-wc.org

推奨事項は

  • Developing
  • Linting
  • Testing
  • Building
  • Demoing
  • Publishing
  • Automating

と章をわけてガイドを提供してくれています。
そこで推奨しているパッケージや設定を用意してくれていてツールで生成できるようになっています。

open-wcのツールで開発環境構築

ツールを使うと簡単に開発環境を用意してくれます。
npmが使えてプロジェクトのディレクトリを置きたい場所で

npm init @open-wc

とすると勝手にツールをダウンロードしてきて実行されます。
ツールが実行されると色々質問されます。作成されるものはその回答によって変わります。
今回は簡単は単一のコンポーネントのプロジェクトを作成したいと思うので以下のように回答すると

What would you like to do today? › Scaffold a new project
What would you like to scaffold? › Web Component
What would you like to add? › (選択なし)
What is the tag name of your application/web component? … password-input
Do you want to write this file structure to disk? › Yes
Do you want to install dependencies? › Yes, with npm  

password-input ディレクトリを以下のような構成で作成してくれます。

.
├── LICENSE
├── README.md
├── demo
│   └── index.html
├── index.js
├── package-lock.json
├── package.json
├── password-input.js
└── src
└── PasswordInput.js

テストやビルドの環境などをあとで追加したい場合、もう一度ツールを使って追加できます。

開発サーバー

ツールで生成されたディレクトリで npm run start をすると開発サーバーが立ち上がってブラウザで 自動でdemo/index.html を開きます。
開発サーバーは es-dev-server というopen-wc お手製のパッケージが使われています。
open-wcはできるだけWeb標準に近づくことで長期投資をする方針で、開発も最新のブラウザでES Modulesを活用して進めることを推しています。なので es-dev-server はビルドツールを使わない前提の仕様になっています。
ビルドの待ち時間がないっていうのはすごい魅力的ですね。
また、node_modules のパッケージもいい感じに読み込んでくれます。

WebComponentの実装

今回は最近よくある入力したパスワードの内容を見えるように切り替えられるinput要素 password-input を作ってみました。
作成したものをGlitchで埋め込んでおきます。View Appボタンでデモページに切り替えられます。

実装は src/PasswordInput.js にしていきます。demo/index.html はデモページです。
open-wcはLitElementとlit-htmlを使った実装を推奨していて、プロジェクト生成時点で使えるようになっています。
ここからはLitElementでの実装方法を説明していきます。

カスタム要素の定義

カスタム要素は通常HTMLElementクラスを継承したクラスを定義しますが、LitElementではLitElementクラスを継承して定義します。
タグ名とクラスの紐付けは password-input.js で既にされているので触る必要がありません。

要素のプロパティ、属性

LitElementではゲッター properties で返すオブジェクトで要素のプロパティを定義します。

  ...
static get properties() {
return {
value: {type: String},
visibility: {type: Boolean}
};
}
constructor() {
super();
this.value = "";
this.visibility = false;
}
...

オブジェクトのプロパティに定義したいプロパティの名前、その値のオブジェクトに設定を指定します。
基本的に定義したプロパティと同じ名前の属性も一緒に定義されます。
typeBoolean にすると disabled 属性みたいな扱いになります。
初期値はコンストラクタで設定できます。

要素のコンテンツ

Web Componentsはカスタム要素のコンテンツをShadowDOMによって定義します。
LitElementでは renderメソッドに定義していきます。

  ...
_toggleVisibility() {
this.visibility = !this.visibility
}
render() {
return html`
<input type=${this.visibility ? 'text' : 'password'}
value=${this.value}
>
<span
class="toggle"
@click=${this._toggleVisibility}
>
....
</span>
`;
}
...

render メソッドはlit-elementが用意しているタグ付きテンプレートリテラルの html を使ってコンテンツを定義します。
イベント名に接頭辞 @ を付けた属性にイベントハンドラをバインドできます。

要素のスタイル

要素のスタイルの定義は styles ゲッターに定義します。LitElementが用意している css タグ付きテンプレートリテラルを使います。
コンテンツのスタイルはShadowDOMの中に定義されてカプセル化されるので外の要素に影響しません。Vue.jsのscoped CSSと同じです。
CSSの管理が楽になるのでありがたいです。

  ...
static get styles() {
return css`
:host {
--icon-size: 24px;
display: inline-flex;
...
}
...
input {
line-height: 1em;
...
}
...
.icon {
width: var(--icon-size);
height: var(--icon-size);
}
...
`;
}
...

:host セレクタでカスタム要素のスタイルを定義します。カスタム要素のスタイルは外から変更可能です。
対してそれ以外のShadowDOMのスタイルは外から直接変更できません。
外から変更する方法のひとつにCSSカスタムプロパティを使った方法があります。
ここでは --icon-size を定義して外からパスワードの変更を切り替えるボタンのアイコンのサイズを変更できるようにしています。
他にも Shadow Parts という part 属性と ::part() セレクタを使った方法があります。

::part() – CSS: Cascading Style Sheets | MDN

まとめ

以上がopen-wcで簡単なWeb Componentをつくってみた内容になります。

今回はDevelopingのみを扱いましたが、簡単なものだったので紹介できていないこともあります。興味があればぜひ公式のガイドを読んでみてください。
文章では伝わらなかったと思いますがビルドの時間がないというのはすごく快適です。ES Modulesが普及してきたからこそだと思いますが、Web Componentsの開発環境だけではなく他の開発環境でもこのスタイルが広まればいいなと思いました。
open-wcも今年できたばかりで日本語の情報がほとんどありませんがこの記事がみなさんの触れるきっかけになれば幸いです。

ブラウザ上でJavaScriptを利用したクリップボードへのコピー機能の実装

エンジニアの中氏です。

ブラウザ上でクリップボードへのコピー機能の実装は便利なJSライブラリのzclipを従来利用していましたが、
Flash の機能を利用している為、将来的に Flash が完全廃止になった際に利用出来なくなることが予想されます。

よって、今回はピュアなJavaScriptのみでコピー機能を実装する方法をご紹介します。
具体的には document.execCommand を利用してコピーを行う方法です。
テキストの範囲選択して、document.execCommand('copy') を実行してコピーを行います。

ここでは、画面に表示されているテキストをコピーするという例で下記に関数の例を記載します。

/**
 * 指定した要素のテキストノードのテキストをコピー
 *
 * @param {string|HTMLElement} subject
 */
function copyByTextNode(subject) {
let textNode = $(subject).get(0).childNodes[0];
let range = document.createRange();
range.selectNode(textNode);
// 選択範囲解除
getSelection().removeAllRanges();
// 範囲選択 
getSelection().addRange(range);
// コピー実行
document.execCommand('copy');
// 選択範囲解除
getSelection().removeAllRanges();
}

copyByTextNode([セレクタ])
で使用します。

セレクタを指定するだけで、そのDOMのテキストノードをコピー出来ます。

AWSソリューションアーキテクトアソシエイト(SAA-C01)に合格しました

f:id:seeds-std:20191021181435p:plain

こんにちは。小國です。

社員の AWS 認定の取得が相次いでいるシーズです。私も 1ヶ月ほど前になりますが、AWSソリューションアーキテクトアソシエイト(SAA-C01)に無事に合格できました。

AWSソリューションアーキテクトアソシエイトを目指す方の参考になればと思い、その時の勉強方法などを紹介します。

まずは自己紹介

  • ソフトウェアエンジニア 12年(主に LAMP 環境で CakePHP、Laravel、他に Android もちょっとできます)
  • 応用情報技術者試験(AP)
  • LAMP や LEMP 程度なら構築できます(ネットワークは要勉強)
  • 過去に LPIC-1(LPIC-2 は前半だけ合格。。。)
  • とりあえず、なんでもやってみる人

勉強前の AWS の知識

AWS は EC2、VPC、RDS のチュートリアル*1を触ったことがある程度で、その他の各種サービスについては以下のような認識でした。

  • S3、聞いたことある
  • ACM、???
  • CloudFront、聞いたことある
  • ELB、聞いたことある
  • リージョン・AZ・エッジロケーション、微妙。。。
  • Lambda、ランブラ?(後にラムダと読むことを知る)
  • DynamoDB、???

勉強方法

勉強期間は約 2週間で、以下の 2冊の本をやりきりました。

AWS認定資格試験テキスト AWS認定 ソリューションアーキテクト-アソシエイト

AWS認定資格試験テキスト AWS認定 ソリューションアーキテクト-アソシエイト

AWS認定資格試験テキスト AWS認定 ソリューションアーキテクト-アソシエイト

  • 作者: NRIネットコム株式会社,佐々木拓郎,林晋一郎,金澤圭
  • 出版社/メーカー: SBクリエイティブ
  • 発売日: 2019/04/20
  • メディア: 単行本
  • この商品を含むブログを見る

AWS の主要なサービスについて要点をがしっかりとまとまっていて、わかりやすくとてもためになりました。章末や模擬試験の解答の解説も丁寧で良かったと思います。

勉強方法はというと、一通り目を通し、章末や模擬試験をすべて正解するまで(間違っている箇所はどこが間違っているか分かるまで)やりました。

おそらく 4、5回はやったかと思います。

徹底攻略 AWS認定 ソリューションアーキテクト – アソシエイト教科書

徹底攻略 AWS認定 ソリューションアーキテクト ? アソシエイト教科書

徹底攻略 AWS認定 ソリューションアーキテクト ? アソシエイト教科書

  • 作者: ??部昭寛,宮?光平,菖蒲淳司,株式会社ソキウス・ジャパン
  • 出版社/メーカー: インプレス
  • 発売日: 2019/01/18
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る

1冊では不安だったので、こちらも購入しました。

こちらは、問題を解いて間違えた箇所について、本で調べました。こちらの本も同様に、すべて正解するまでやりました。

先に購入した対策本で理解ができていなかった箇所がカバーできたのでよかったと思いますが、解答の解説が少なかったように思います。

模擬試験

本試験の 2日前ほどに受けました。思っていたより簡単で、結果も 84%ほどの正答率でした。

いよいよ本試験!

本試験ですが、模擬試験に比べ文章も長く・問題も単純なものが少なく、難しかったです。

40分ほど残して一通り解答し、残りの時間いっぱいを使って見直しをしました。終わったあとはすごく頭が痛かったです。頭がしっかり働く午前中に受験したのが幸いでした。

問題文は、VPC や ELB のネットワークの複合的な問題や、DynanoDB、暗号化についての問題が多かった印象です。

感想

とりあえずは、AWSソリューションアーキテクトアソシエイト(SAA-C01)を取得できよかったです。

ただし、実際に AWS を使いこなせるかは甚だ疑問で、実際に手を動かしてみて知識・技術の定着をして、業務に役に立てるようやっていきたいです。

また、相当の難易度だと聞く、プロフェッショナル取得を目指せるよう勉強したいなと。。。

余談ではありますが、弊社 CTO の、原口さんこと cs_sonar さんが、AWSソリューションアーキテクトプロフェッショナル(SAP-C01)に合格されてます。

プロフェッショナルを目指す方は、ご参考にしてはいかがでしょうか。

blog.seeds-std.co.jp

Slackの新機能!「ワークフロービルダー」やってみた

f:id:seeds-std:20191019041007p:plain
クラウド事業部の川勝です。

先日(2019年10月15日)弊社でも使用しているチャットツールSlackに新機能「ワークフロービルダー」がリリースされました!
https://slack.com/intl/ja-jp/features/workflow-automation

早速試してみましたのでご紹介したいと思います。

導入

ワークフローの自動化で作業をシンプルに
重要な仕事の妨げになるルーティン作業からチームを解放。Slack のワークフロービルダーを活用しましょう。

とキャッチフレーズがありまして簡単にいうとチャンネルに対してアクションを設定し、メッセージまたはフォーム表示できたりします。

アクションとは以下のタイミングで実行することができます。

  • ワークフローボタンから手動実行
  • チャンネルに新しい人がジョインしたとき
  • 特定の絵文字リアクションがついたとき

新しくプロジェクトにアサインした人に自動で必要情報を表示とか、毎朝のMTG内容をフォーム入力で投稿、、といったようなことができそうですね。

弊社ではスマートフォン等の検証機があり、その使用時に特定のチャンネルで通知する、というフローがあります。

f:id:seeds-std:20191019023204p:plain
端末使用時の様子

いままでは手動でこのように微妙に統一感がなく各自が投稿していましたので、今回はこちらの投稿フォームを作ってみたいと思います!

ワークフロー作成

f:id:seeds-std:20191019023537p:plain

左上のワークスペースのメニューに「Workflow Builder (NEW)」と使ってくれと言わんばかりの項目が増えています。
こちらをクリックします。

f:id:seeds-std:20191019023853p:plain

自分が作成したワークフローの管理ができます。(後述していますが、チームで作成されているすべてが無条件に表示されるわけではない)

新規作成してみましょう。

f:id:seeds-std:20191019024430p:plain

命名

f:id:seeds-std:20191019024623p:plain

次にどのチャンネルにワークフローを設定するか選択します。
ここで選択したチャンネルからのみアクションが実行されます。
short nameはワークフロー起動時に表示されている名称になります。

「Save」するとこんな感じに表示されます。

f:id:seeds-std:20191019024940p:plain

「Add Step」をクリックして、メッセージやフォームを作成してつなげていきます。
すると、、、

f:id:seeds-std:20191019024907p:plain

ちょっと飛ばしてこのようになりました。

今回だと

  • ワークフロー実行
  • フォーム入力項目設定
  • フォーム入力内容にもとづくメッセージ設定
  • メッセージについているボタンをクリックしたときのメッセージ設定

という順番になっています。

では項目設定の解説していきます。

まず「Add Step」をクリック。

f:id:seeds-std:20191019025603p:plain

すると「Send a message」 or 「Create a form」の選択がでてきます。

今回の場合だと最初は フォームがほしいので「Create a form」を選択します。

f:id:seeds-std:20191019030110p:plain

こちらはselect boxで選択肢の設定をしています。

他にも以下選択肢が設定可能です。

  • Short answer (改行NGの自由入力)
  • Long answer (改行OKの自由入力)
  • Select form list (画像のselect box)
  • Select a person (チームないのユーザを選択できるselect box)
  • Select a channel or DM (チームないのチャンネル or DMを選択できるselect box)

また確定したときDMで内容を送信したりもできます。

続いて通常のメッセージ

f:id:seeds-std:20191019030801p:plain

メッセージはどのチャンネルに送信するか選択できます。(ワークフローを起動したチャンネルと異なってもOK)
またフォームに続いてのメッセージの場合は Insert a variable でフォーム入力値の挿入ができます。
他にもアクションを起動した人、アクションを実行したチャンネルなどが挿入可能です。

あと面白いのがボタンをつけることができます。
ボタンをつけていると、次のStepでボタンをクリックしたら起動、ということが実現できます!

f:id:seeds-std:20191019031248p:plain

そうすると先に送信したメッセージのスレッドに投稿ということも可能になっています。

ここまででできたら右上の「Publish」で公開するとチャンネルからワークフローが実行可能です!

f:id:seeds-std:20191019031538p:plain

公開できたら実行してみましょう!

実行

f:id:seeds-std:20191019040321g:plain

こんな感じで実行できました!

感想

簡単に登録できてこれからどんどん活用したいなと思える機能でした。
ただ新機能ということもあってちょっとかゆいところに手が届かないなあという感じもあります。

以下は個人の感想です。

  • ワークフロービルダーで作成したものを他人が編集できるようにするにはCollaboratorsに追加しないといけないため、
    無条件でチームない共有されない。
  • 絵文字リアクションなんかは複数のチャンネルで同じワークフローを発火できたら面白そう、とおもったがそれはできない。
    ただし、アクション後のメッセージ投稿は任意のチャンネルに投稿できます。
  • アクションはチャンネルにだけ設定できる。自分用にDMに登録はできない。

今後もっと使いやすくなることに期待したいですね!

また面白い使い方が見つけたらブログにしたいと思います。

ちなみに以下にサンプルがあるので、最初はここからダウンロードしたものをインポートして作成すると作りやすいと思います。(現在英語だけみたいです…)
https://slack.com/intl/ja-jp/slack-tips/workflow-builder-examples

以上川勝でした。

Amazon QLDB 楽しいかもというお話

f:id:seeds-std:20191016131408p:plain

はじめに

みなさま、こんにちは。

WEB事業部の李です。 どうぞよろしくお願いいたします。
最近、タピオカにハマっています。
美味しいタピオカを探しております。

さて、Amazon QLDB(以降、単に「QLDB」)が東京リージョンに対応しました。
面白そうなので、ドキュメントを読みつつ、触ってみました。
本日は、QLDBの、この機能いいなと思った感想を書きたいと思います。
※機能面の詳細については、別の機会に書きたいと思います。

QLDBの紹介

まずは、QLDBってなんだ!というところですが、
ざっくり言うと、中央集権的な台帳サービスです。

例えば、データベースに履歴情報を格納して、アプリ側でそれを検索したり、
新たに登録や更新があれば、過去の改竄が無いようにデータを挿入していくといったことをしたい場合があるかと思います。

そんな時に、DB側でその保証を約束してくれるような構造になっていれば楽なのになと、毎回思ったりしますが、
QLDBは、まさにそれを実現してくれるソリューションとして登場したように感じます。

Q: Amazon Quantum Ledger Database とは何ですか?

Amazon Quantum Ledger Database (QLDB) は、台帳管理専用データベースです。
アプリケーションのデータに生じたすべての変更に関する、完全で暗号的に検証可能な履歴を提供します。

よくある質問 – Amazon QLDB | AWS

また、開発者ドキュメントのチュートリアルでは、
自動車とその所有者の所有履歴を管理する台帳を例で挙げられていました。
世の中履歴データだらけなので、自動車以外にも対象はかなり多そうです。

docs.aws.amazon.com

ユースケースとして、金融や小売での利用が紹介されてました。

・金融

銀行では多くの場合、顧客の銀行口座間のクレジットカードおよびデビットカードによる取引などの重要データの追跡に一元管理台帳的なアプリケーションが必要になります。
複雑な監査機能を持つカスタム台帳を構築する代わりに、QLDB ですべての金融トランザクションの正確かつ完全な記録を簡単に保存できます。

・小売 & サプライチェーン

小売業では、製品原産地や出荷製品の品目数、出荷先、出荷担当者など、製品サプライチェーンのあらゆる段階に関する情報にアクセスしなければならないことが多々あります。
QLDB を使用すると、製品がどの物流局面にあっても在庫および物流に関する完全な履歴を確認し追跡できます。

↑どちらも有用そうです。

特徴

さて、QLDBは下記のような、特徴があります。
・フルマネージドであること。かつ、サーバーレス(->自動スケーリング)。
・すべての変更が透過的で、イミュータブルであること。
・トランザクションログが、暗号的に検証可能であること。

aws.amazon.com

アプリ開発の時には、JAVAのドライバーが用意されていますので、下記のチュートリアルをご参考ください。

docs.aws.amazon.com

この機能いいなと思った感想

さて、色々便利そうで、素敵な機能があるのですが、
その中で、個人的にこりゃ便利だなと思った機能を二点あげたいなと思います。

改竄出来ないDBであること

新規だろうが、更新だろうが、削除だろうが、全て履歴に残してくれます。
また、その時々のメタ情報を残してくれますので、アプリ側で云々する必要が無いです。
ちなみにミスった操作も、全て保存され、かつ検証可能です。
※誤操作や誤情報を登録しないようなフローや制限などはアプリ側で考慮は必要です。それらも全て記録されますので、その辺は仕様によります。

PartiQLでクエリ可能であること

sqlチックな言語で、保存されたデータをクエリできちゃいます。
ネストされたデータをクエリできるのは、素敵だなと思います。
※2019/10/17現在、すべての PartiQL オペレーションをサポートしているわけでは無いらしいです。

おわりに

クロスリージョンレプリケーションはサポートされていないなど、
出始めのサービスなので、まだまだ成長段階かと思いますが、
ポテンシャルはめちゃくちゃ大きいような気がします。(=楽できる。 )

Page 1 of 17

© SEEDS Co.,Ltd.