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 につながることを確認します。正しく起動できると以下のような画面が表示されると思います。
設定した $AWS_ACCESS_KEY_ID
と $AWS_SECRET_ACCESS_KEY
でログインします。
右下の「+」ボタンより、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 にそのまま保存する環境を作りました。