Laravel + MinIO temporaryUrlメソッドについて


MinIOは、AWSのS3と互換性のあるオブジェクトストレージサーバーです。
開発環境でS3の代替として使用されることが多く、ローカル環境でクラウドストレージのシミュレーションを行うのに適しています。

LaravelのファイルストレージでMinIOを使用する際、公式ドキュメントでは`temporaryUrl`メソッドがサポートされていないと記述されています。


> MinIOを使用する場合、temporaryUrlメソッドによる一時ストレージURLの生成はサポートされていません。

https://laravel.com/docs/11.x/filesystem#minio


サポートされていないと書かれていますが、`temporaryUrl`メソッド自体は動作します。
ただ、設定によっては、生成されたURLがブラウザからアクセスできない場合があります。
その問題点と解決策について説明します。

## 問題点

開発環境をDockerで構築した場合、MinIOを使用する際の環境変数は以下のようになると思います。

```ini:.env
AWS_ACCESS_KEY_ID=minio_username
AWS_SECRET_ACCESS_KEY=minio_password
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=local
AWS_ENDPOINT=http://minio:9001
AWS_USE_PATH_STYLE_ENDPOINT=true
```

`AWS_ENDPOINT`にはPHPアプリケーション側からアクセスするためのURLを指定します。
ただ、この設定のみだと、`url`メソッドを使いURLを生成した場合、`http://minio:9001/local/xxx` のようなURLが生成されます。
このURLはPHPアプリケーション側からみたDockerネットワーク内部のURLなので、Docker外部（つまり、ホストマシンのブラウザなど）から直接アクセスすることはできません。
そのため、ドキュメントには次のように書かれています。


> MinIOを使用するときに Laravelの統合が適切なURLを生成するには、AWS_URLアプリケーションのローカル URLと一致し、URLパスにバケット名が含まれるように環境変数を定義する必要があります。

https://laravel.com/docs/11.x/filesystem#minio

なので、環境変数に`AWS_URL`を追加します。

```ini:.env
AWS_URL=http://localhost:9001/local
```

この設定を行うことで、`url`メソッドでURLを生成した場合、`http://localhost:9001/local/xxx` のようなURLが生成されDocker外部からアクセスできるようになります。

ただ、`temporaryUrl`メソッドによるURLの生成は`AWS_URL`ではなく`AWS_ENDPOINT`を元に生成されるため、Docker外部からアクセスできません。
`temporaryUrl`メソッド自体は動作するので、`AWS_ENDPOINT`のURLをDocker外部からアクセスできる状態になれば問題は解決します。


## 解決策

上記の状態で一番簡単な解決策は、`AWS_URL`の環境変数は使わず、`AWS_ENDPOINT`のホスト名をホストマシンの`/etc/hosts`に追加することです。

```:hosts
127.0.0.1 minio
```

少し強引ですが、`http://minio:9001/local/xxx` はDocker外部からアクセスできるようになります。


## その他の解決策

その他解決策は色々あるのですが、MinIOのURLをhttpsにするパターンでLaravelとMinIOが動く環境をDockerで構築してみます。

### Dockerの設定

Docker Composeを使用して環境を構築します。以下は`docker-compose.yml`の例です。

`minio.laravel.test`というホスト名でMinIOにアクセスできるようにします。
`minio-create-buckets`はMinIOのバケットを作成するためのコンテナです。

```yml:docker-compose.yml
services:
    app:
        build:
            context: ./docker/php
        volumes:
            - .:/var/www
        working_dir: /var/www
        user: 1000:1000
    web:
        build:
            context: ./docker/nginx
        ports:
            - 80:80
            - 443:443
        volumes:
            - ./docker/nginx/config:/etc/nginx/conf.d
            - ./:/var/www
        networks:
            default:
                aliases:
                    - minio.laravel.test
    minio:
        image: minio/minio
        environment:
            MINIO_ROOT_USER: ${MINIO_USERNAME-minio}
            MINIO_ROOT_PASSWORD: ${MINIO_PASSWORD-password}
        command: server /data --address :9001 --console-address :8900
        volumes:
            - minio:/data
        ports:
            - 9001:9001
            - 8900:8900
    minio-create-buckets:
        image: minio/mc
        depends_on:
            - minio
        entrypoint: >
            /bin/sh -c " /usr/bin/mc config host add local http://minio:9001 ${MINIO_USERNAME-minio} ${MINIO_PASSWORD-password}; /usr/bin/mc mb -p local/${MINIO_BUCKET-local}; /usr/bin/mc anonymous set download local/${MINIO_BUCKET-local}; exit 0;"

volumes:
    minio:
```

nginxのDockerfileでは、自己署名証明書を作成しています。

```Dockerfile:docker/nginx/Dockerfile
FROM nginx:1.25.4

RUN apt-get update && apt-get -y install openssl \
    && openssl req -newkey rsa:2048 -x509 -nodes -set_serial 1 -days 3650 \
    -subj "/C=JP/ST=Tokyo/L=Chiyoda-ku" \
    -keyout "/etc/ssl/private/server.key" -out "/etc/ssl/private/server.crt" \
    && chmod 400 /etc/ssl/private/server.*
```

nginxの設定で`minio.*`のホスト名でMinIOにアクセスできるようにします。

```conf:docker/nginx/config/default.conf
server {
    listen 80 default_server;
    listen 443 ssl default_server;

    server_name _;

    client_max_body_size 0;

    root /var/www/public;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass app:9000;
        fastcgi_index index.php;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;

        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        include fastcgi_params;

        fastcgi_buffers 16 16k;
        fastcgi_buffer_size 32k;
        fastcgi_read_timeout 600;
    }

    ssl_certificate /etc/ssl/private/server.crt;
    ssl_certificate_key /etc/ssl/private/server.key;
}

server {
    listen 80;
    listen 443 ssl;

    server_name minio.*;

    client_max_body_size 0;

    location / {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;

        proxy_pass http://minio:9001/;
    }

    ssl_certificate /etc/ssl/private/server.crt;
    ssl_certificate_key /etc/ssl/private/server.key;
}
```

PHPのDockerfileを作成します。

```Dockerfile:docker/php/Dockerfile
FROM php:8.2-fpm

RUN apt-get update && apt-get install -y \
    git \
    curl \
    libpng-dev \
    libonig-dev \
    libxml2-dev \
    zip \
    unzip

RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd

COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
```


### ホスト名の設定

ローカルマシンの`/etc/hosts`ファイルに以下の行を追加します。

```
127.0.0.1 laravel.test minio.laravel.test
```

これにより、ブラウザから`https://minio.laravel.test`でMinIOにアクセスできるようになります。

### 環境変数の設定

`.env`ファイルに以下の設定をします。

```ini:.env
AWS_ACCESS_KEY_ID="minio"
AWS_SECRET_ACCESS_KEY="password"
AWS_DEFAULT_REGION=ap-northeast-1
AWS_BUCKET="local"
AWS_ENDPOINT="https://minio.laravel.test"
AWS_USE_PATH_STYLE_ENDPOINT=true
AWS_SSL_VERIFY=false
```

### Laravelの設定

`config/filesystems.php`の`s3`を以下のように設定します。
自己署名証明書の場合、curlでエラーが発生するので、`http=>vaerify`を`false`に設定します。
そのための`.env` に`AWS_SSL_VERIFY=false`を設定してあります。

```php
's3' => [
    'driver' => 's3',
    'key' => env('AWS_ACCESS_KEY_ID'),
    'secret' => env('AWS_SECRET_ACCESS_KEY'),
    'region' => env('AWS_DEFAULT_REGION'),
    'bucket' => env('AWS_BUCKET'),
    'url' => env('AWS_URL'),
    'endpoint' => env('AWS_ENDPOINT'),
    'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
    'throw' => false,
    'http' => [
        'verify' => env('AWS_SSL_VERIFY', true)
    ],
],
```


### 検証

設定が正しく行われたかを確認するために、以下のようなコードを作成し実行します。

```php:app/Console/Commands/TestMinioUrl.php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;

class TestMinioUrl extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'test-minio-url';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'test minio url';

    /**
     * Execute the console command.
     */
    public function handle()
    {
        $disk = Storage::disk('s3');

        // Upload a file
        $disk->put('test.txt', 'Hello MinIO!!');

        // Get the URL of a file
        $url = $disk->url('test.txt');
        print "Normal URL: " . $url . PHP_EOL;

        // Generate a temporary URL
        $tempUrl = $disk->temporaryUrl('test.txt', now()->addMinutes(5));
        print "Temporary URL: " . $tempUrl . PHP_EOL;
    }
}
```

```bash
docker-compose exec app php artisan test-minio-url
```

正しく設定されていれば、生成されたURLがブラウザからアクセス可能であることを確認できます。


## リポジトリ

https://github.com/horatjp/example-laravel-minio


## 参考

- [Laravel公式ドキュメント - ファイルストレージ](https://laravel.com/docs/11.x/filesystem#minio)
- [MinIO公式ドキュメント](https://min.io/docs/minio/linux/index.html)
- [Laravel Temporary URLs & MinIO](https://tkacz.pro/laravel-temporary-urls-minio/)
- [Laravel9 + S3 (MinIO) で署名付き URL を発行してファイルをアップロードする](https://qiita.com/tomyf/items/7b0d880e0fadfc997992)