カテゴリー: Laravel

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 にそのまま保存する環境を作りました。

参考サイト

Laravelを学ぶ為、DockerでLaravelを動かせる環境を構築した

こんにちは、西山です。

今回からは麻雀に負けずともブログ記事を書こうと思います。

エンジニア35歳定年説を気にせず
PHPフレームワークとして有名なLaravelを勉強し始めましたので
学んだ事や経験した事を記載していきます。

会社ではDockerを立ち上げるだけで完全に自動化され
ファイルの修正が必要なく、Laravelが動作するDockerリポジトリが存在します。

それとは別に、自分で一からDockerの開発環境を作成してみました。

便利な物を効率よく使うのも重要ですが
学習の為、自分で作成したDocker環境にLaravelをインストールし
シンプルな内容のリポジトリを作成しました。

github.com

この作成過程や実際に作成したリポジトリの使い方を元に話を進めます。

尚、開発環境はMacで、Dockerは 「Docker Desktop for Mac」 を使用しました。

docs.docker.com

※Windowsでは、「Docker Toolbox」を使用しDockerの環境を用意しました。

■1. Laravelの学習

リポジトリを作成するにあたり、まずは下記の2つを取り組みました。

ドットインストール 「Laravel 5.5入門」
書籍 「PHPフレームワーク Laravel入門」

ドットインストールは短い動画が複数あり取り組みやすく分かりやすいですが
細かく気になった点が省かれていると感じました。
その後で、書籍で丁寧に一通り説明されているのを読み、理解が進みました。

全体の流れをドットインストールで学んでからだったので、書籍の細かい点も分かりやすく感じました。

Laravelはインストールも簡単ですし、ベースのファイルを作成する為の便利なコマンドも多く用意されています。

■2. リポジトリの作成「DockerへのLaravelのインストール」

元々PHPが動作するシンプルなDockerを作成していまして
そこにLaravelのインストールとプロジェクト作成を行い、今回のリポジトリを作成しました。

作業の流れを記載します。

Dockerのコンテナを作成・起動

docker-compose up -d

「web」という名前のコンテナを私は用意しています。

Webサーバーのコンテナに入る

docker-compose exec web bash

Webサーバーに入って、Laravelをインストールします。
まずはComposerのインストールが必要です

https://getcomposer.org/download/

下記の記載があるので、それぞれ1行ずつコマンドを叩き実行します

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === 'a5c698ffe4b8e849a443b120cd5ba38043260d5c4023dbf93e1558871f1f07f58274fc6f4c93bcfd858c6bd0775cd8d1') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"

ここまで実行すると下記のファイルが作成されています。
composer.phar

ここで、ComposerからLaravelをインストールします。
laravel_appという名前でプロジェクトを作ります。

プロジェクト作成

php composer.phar create-project --prefer-dist laravel/laravel laravel_app

インストールが完了しましたら、プロジェクトのディレクトリに移動しLaravelのバージョンを確認します。

ディレクトリ移動

cd laravel_app

Laravelのバージョン確認

php artisan --version

この artisan のコマンドはモデル、コントローラー、マイグレーション等の様々なベースファイルの作成ができたりなど
Laravel開発に置いて、非常に便利なコマンドです。
artisan は職人という意味なので、指示を出して使いこなすエンジニアは親方ですね。

下記のようにバージョンが表示されればOKです。

Laravel Framework 5.8.34

laravel

親方デビューです。

追加されたLaravelのファイルを git add して今回のリポジトリが作成できました。

■3. リポジトリの使い方「Laravelのリポジトリを git clone して動かす時の注意点」

しかし、別PCに git clone してdockerを立ち上げて動かそうとした所、動きませんでした。
別の現場では、すんなり親方になれませんでした。

エラーメッセージ

Warning:  require(/app/laravel_app/public/../vendor/autoload.php): failed to open stream: No such file or directory in /app/laravel_app/public/index.php on line 24
Fatal error:  require(): Failed opening required '/app/laravel_app/public/../vendor/autoload.php' (include_path='.:/usr/local/lib/php') in /app/laravel_app/public/index.php on line 24

先ほど自分で名前を決めて作成したLaravelのプロジェクトのディレクトリ「laravel_app」
下記にvendorディレクトリが存在していませんでした。

/laravel_app/vendor

artisanコマンドでプロジェクトを作った時には存在しましたが
vendorディレクトリが無いのでインストール

■【現在はDockerのwebサーバーのディレクトリ「laravel_app」にいる状態】

※ /composer.phar に「composer.phar」は存在します。

インストール

php ../composer.phar install

vendorディレクトリが作成されましたが、500エラーと表示されます。

500 Server Error

【Laravel】の環境変数設定ファイル「.env」が作成されていなかったので作成します。
Gitで管理されない状態になっていました。

/laravel_app/.envを作成

cp .env.example .env

Dockerで開発していますので、Dockerのデータベースの設定を確認します。

/docker-compose.yml の設定を確認

version: '3'
services:
web:
build: ./docker/web/
depends_on:
- db
volumes:
- ./:/app
- ./docker/web/php.ini:/usr/local/etc/php/php.ini
- ./docker/web/000-default.conf:/etc/apache2/sites-enabled/000-default.conf
working_dir: /app
ports:
- ${WEB_PORT}:80
db:
build: ./docker/db/
volumes:
- ./docker/db/mysql:/var/lib/mysql
- ./docker/db/init:/docker-entrypoint-initdb.d
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
ports:
- ${DB_PORT}:3306

上記の {MYSQL_ROOT_PASSWORD} の記載は
【Docker】の環境変数の設定ファイル「.env」で管理しています。
※Laravelの環境変数の設定ファイルとは別ファイルで、リポジトリ直下に存在します。

Dockerの .env 場所

/.env

/.env の設定を確認

WEB_PORT=50012
DB_PORT=50013
MYSQL_ROOT_PASSWORD=rootpassword
MYSQL_DATABASE=testdb
MYSQL_USER=test
MYSQL_PASSWORD=testpassword

Dockerのデータベースに接続する必要があるので
Laravelの.envのデータベース接続の箇所を修正

Laravelの .env 場所

/laravel_app/.env

Dockerからの接続になるので、/docker-compose.yml を確認し
「DB_HOST」は「db」とし「DB_PORT」は「3306」であることに注意してください。

/laravel_app/.env 修正内容

DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=testdb
DB_USERNAME=root
DB_PASSWORD=rootpassword

まだエラーが続きます。しぶといですね。

No application encryption key has been specified.

No application encryption key has been specified.

下記のコマンドを実行

キー作成

php artisan key:generate

キャッシュクリア

php artisan config:clear

これで、ようやくトップページが表示されました。
git cloneして、すぐに使えると思っていましたが、何点か注意が必要な状況でした。

私が作ったリポジトリに限らず、Laravelの .env はGit管理されない設定になっている為
他のLaravelのリポジトリを git clone して使えなかった場合には
上記の手順を参考にしていただければ幸いです。

あとがき

いくつになっても、新しい事を知ることは重要なのでめげずに進んでいきます。

健康診断もあり、30半ばになるとより一層健康が気になるところです。
では、そろそろ自分探し(再検査)に行ってまいります。

以上、西山でした。

© SEEDS Co.,Ltd.