こんにちは。小國です。最近は 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 にそのまま保存する環境を作りました。

参考サイト