hero_picture
Cover Image for LaravelからS3互換のMinIOを使えるように、docker-compose環境を整える

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

参考サイト