月別: 2019年11月

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デザインだとその重要性がより顕著に現れると思います。
ユーザーはサイトをくまなくチェックするわけではなく、
「このサイトに自分が求めている情報はあるか?」と、情報を流し読みしています。

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

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

●まとめ


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

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

© SEEDS Co.,Ltd.