こんにちは。小國です。最近は 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
1+# Minio config
2+MINIO_PORT=60007
3+
4+# AWS config
5+AWS_URL=http://minio:9000
6+AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXXXXXXX
7+AWS_SECRET_ACCESS_KEY=YYYYYYYYYYYYYYYYYYYY
8+AWS_DEFAULT_REGION=us-east-1
9+AWS_BUCKET=test
10+AWS_PATH_STYLE_ENDPOINT=true
11
- docker-compose.yml
1+ minio:
2+ image: minio/minio
3+ ports:
4+ - "${MINIO_PORT}:9000"
5+ volumes:
6+ - ./.docker/minio/data:/export
7+ environment:
8+ MINIO_ACCESS_KEY: ${AWS_ACCESS_KEY_ID}
9+ MINIO_SECRET_KEY: ${AWS_SECRET_ACCESS_KEY}
10+ command: server /export
11
ホストマシンから 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 インストール
1$ composer require league/flysystem-aws-s3-v3 ~1.0
- config/filesystems.php
1 's3' => [
2 'driver' => 's3',
3+ 'endpoint' => env('AWS_URL'),
4+ 'use_path_style_endpoint' => env('AWS_PATH_STYLE_ENDPOINT', false),
5 'key' => env('AWS_ACCESS_KEY_ID'),
6 'secret' => env('AWS_SECRET_ACCESS_KEY'),
7 'region' => env('AWS_DEFAULT_REGION'),
8
1$ php artisan tinker
2>>> Storage::disk('s3')->put('hello.json', '{"hello": "world"}')
3=> true
MinIO にファイルが作成されているかと思います。
ファイルの作成・削除・ダウロード
Laravel から MinIO へファイルの作成・削除・ダウロードをやってみます。
- 2019_11_11_020835_create_assets_table.php
1<?php
2
3use Illuminate\Support\Facades\Schema;
4use Illuminate\Database\Schema\Blueprint;
5use Illuminate\Database\Migrations\Migration;
6class CreateAssetsTable extends Migration
7{
8 public function up()
9 {
10 Schema::create('assets', function (Blueprint $table) {
11 $table->increments('id');
12 $table->string('model')->nullable();
13 $table->integer('foreign_key')->nullable();
14 $table->string('name');
15 $table->string('type');
16 $table->integer('size');
17 $table->string('disk');
18 $table->string('path');
19 $table->timestamps();
20 $table->index(['model', 'foreign_key']);
21 });
22 }
23
24 public function down()
25 {
26 Schema::dropIfExists('assets');
27 }
28}
29
- app/Asset.php
1<?php
2
3namespace App;
4use Illuminate\Database\Eloquent\Model;
5class Asset extends Model
6{
7 protected $fillable = [
8 'foreign_key',
9 'model',
10 'name',
11 'type',
12 'size',
13 'disk',
14 'path',
15 ];
16}
17
- routes/web.php
1+ // Asset Routes...
2+ Route::get('assets/{asset}/download', 'AssetController@download')->name('assets.download');
3+ Route::resource('assets', 'AssetController')->only(['index', 'create', 'store', 'destroy']);
4
- app/Http/Controllers/AssetController.php
1<?php
2
3namespace App\Http\Controllers;
4use App\Asset;
5use App\Http\Requests\StoreAsset;
6use Illuminate\Support\Facades\Storage;
7class AssetController extends Controller
8{
9 public function index()
10 {
11 $assets = Asset::query()->paginate();
12 return view('asset.index', compact('assets'));
13 }
14
15 public function create()
16 {
17 return view('asset.create');
18 }
19
20 public function store(StoreAsset $request)
21 {
22 $file = $request->file('file');
23 $path = $file->store('assets', 's3');
24 if (!$path) {
25 abort(500);
26 }
27
28 $asset = new Asset([
29 'model' => Asset:class,
30 'name' => $file->getClientOriginalName(),
31 'size' => $file->getSize(),
32 'type' => $file->getMimeType(),
33 'path' => $path
34 'disk' => 's3'
35 ]);
36 if ($asset->save()) {
37 return redirect()->route('assets.index')->with('success', __('messages.saved'));
38 }
39
40 return redirect()->route('assets.index')->with('error', __('messages.could_not_be_saved'));
41 }
42
43 public function destroy(Asset $asset)
44 {
45 if (Storage::disk($asset->disk)->exists($asset->path) && !Storage::disk($asset->disk)->delete($asset->path)) {
46 abort(500);
47 }
48
49 if ($asset->delete()) {
50 return back()->with('success', __('messages.deleted'));
51 }
52
53 return back()->with('error', __('messages.could_not_be_deleted'));
54 }
55
56 public function download(Asset $asset)
57 {
58 return Storage::disk($asset->disk)->download($asset->path);
59 }
60}
61
- app/Http/Requests/StoreAsset.php
1<?php
2
3namespace App\Http\Requests;
4use Illuminate\Foundation\Http\FormRequest;
5class StoreAsset extends FormRequest
6{
7 public function authorize()
8 {
9 return true;
10 }
11
12 public function rules()
13 {
14 return [
15 'file' => 'required'
16 ];
17 }
18}
19
- resources/views/asset/create.blade.php
1@extends('layouts.app')
2@section('content')
3<div class="card">
4 <div class="card-header">
5 {{ __('Create New') }}
6 </div>
7 <div class="card-body">
8 <form method="post" action="{{ route('assets.store') }}" enctype="multipart/form-data">
9 @csrf
10 <div class="form-group">
11 <label for="name">{{ __('File') }}</label>
12 <input type="file" class="form-control" name="file"/>
13 </div>
14 <button type="submit" class="btn btn-primary" dusk="upload">{{ __('Upload') }}</button>
15 </form>
16 </div>
17</div>
18@endsection
19
- resources/views/asset/index.blade.php
1@extends('layouts.app')
2@section('content')
3<div class="card">
4 <div class="card-header">
5 {{ __('Assets') }}
6 </div>
7 <div class="card-body">
8 <table class="table">
9 <thead>
10 <th>{{ __('ID') }}</th>
11 <th>{{ __('Image') }}</th>
12 <th>{{ __('File Name') }}</th>
13 <th>{{ __('File Size') }}</th>
14 <th>{{ __('Created At') }}</th>
15 <th>{{ __('Actions') }}</th>
16 </thead>
17 <tbody>
18 @foreach($assets as $asset)
19 <tr>
20 <td>{{ $asset->id }}</td>
21 <td><img src="{{ route('assets.download', $asset->id) }}" width="150"></td>
22 <td>{{ $asset->name }}</td>
23 <td>{{ number_format($asset->size) }} Bytes</td>
24 <td>{{ $asset->created_at }}</td>
25 <td>
26 <form action="{{ route('assets.destroy', $asset->id)}}" method="post" class="d-inline">
27 @csrf
28 @method('DELETE')
29 <button class="btn btn-danger" type="submit" dusk="delete">{{ __('Delete') }}</button>
30 </form>
31 </td>
32 </tr>
33 @endforeach
34 </tbody>
35 </table>
36 {{ $assets->->appends(request()->query())->links() }}
37 </div>
38</div>
39@endsection
まとめ
ローカルの開発環境では s3ドライバーを使って MinIO に保存し、AWS で運用時には S3 にそのまま保存する環境を作りました。