はじめに
ローカル開発環境に、S3互換のストレージとしてRustFSを導入してバケット作成を行います。また、RustFSが利用できるコンテナを常駐起動した状態で、Laravel12を利用して簡単な動作確認まで行います。Laravelの環境準備に関しては本記事で解説を行いませんのでご留意ください。
ディレクトリ構成
私の環境では、Laravelのリソースはsrcの中に入れており、infra配下にはdocker系のリソースを配置しています。
今回修正が必要だったファイルは以下の3つになります。
- docker-compose.yml
- src/config/filesystems.php
- .env
また、新規に以下を追加して、バケット作成を行うOne-Shotコンテナを準備します。
- infra/docker/rustfs-setup/Dockerfile
- infra/docker/rustfs-setup/rustfs-setup.sh
加えて、RustFSを利用したストレージ操作を簡易的に確認するため以下を追加します。
- src/app/Console/Commands/RustFsDebugCommand.php
/
├── infra/
│ └── docker/
│ ├── ...
│ └── rustfs-setup/
│ ├── Dockerfile # バケット作成用のOne-Shotコンテナ
│ └── rustfs-setup.sh # バケット作成用のスクリプト
├── src/
│ ├── Laravelのリソースファイル群色々
│ ├── .env
│ ├── config/
│ │ ├── filesystems.php
│ │ └── ...
│ └── app/
│ └── Console/
│ └── Commands/
│ └── RustFsDebugCommand.php # RustFSに対する動作確認用
└── docker-compose.yml # rustfs, rustfs-setup サービスを追加定義
docker-compose.ymlの修正
- rustfsサービスにより、S3互換ストレージを提供します。
- rustfs-setupサービスは、rustfsサービス起動後、バケット作成と、必要に応じて初期オブジェクトの投入を行います。
- appサービスは、phpが動作するコンテナです。rustfsサービスの起動と、rustfs-setupサービスの正常終了を確認するまで起動しないようにしています。
services:
db:
...
redis:
...
app:
...
depends_on:
redis:
condition: service_healthy
db:
condition: service_healthy
mailcatcher:
condition: service_healthy
+ rustfs:
+ condition: service_started
+ rustfs-setup:
+ # 依存コンテナが正常終了(exit code 0)するまで待つ
+ condition: service_completed_successfully
mailcatcher:
...
+ # ====================================================
+ # RustFS container (storage server for local)
+ # ====================================================
+ rustfs:
+ container_name: sample-rustfs
+ image: rustfs/rustfs:1.0.0-alpha.91
+ environment:
+ RUSTFS_ACCESS_KEY: local
+ RUSTFS_SECRET_KEY: local
+ RUSTFS_CONSOLE_ENABLE: "true"
+ TZ: "Asia/Tokyo"
+ command: ["/data"]
+ ports:
+ # S3 API
+ - "9000:9000"
+ # Web Console
+ - "9001:9001"
+ volumes:
+ - rustfs_data:/data
+
+ # ====================================================
+ # RustFS setup one-shot container
+ # ====================================================
+ rustfs-setup:
+ container_name: sample-rustfs-setup
+ build:
+ context: .
+ dockerfile: ./infra/docker/rustfs-setup/Dockerfile
+ environment:
+ ENDPOINT_URL: http://rustfs:9000
+ S3_BUCKET_NAME: sample-bucket
+ AWS_ACCESS_KEY_ID: local
+ AWS_SECRET_ACCESS_KEY: local
+ AWS_DEFAULT_REGION: ap-northeast-1
+ SHARE_DIR: /share
+ volumes:
+ - ./infra/docker/rustfs-setup/share:/share:ro
+ depends_on:
+ rustfs:
+ condition: service_started
+ restart: "no"
volumes:
...
+ rustfs_data:
+ driver: local
...
rustfs-setupサービスのDockerfile
rustfs-setupサービスのDockerfileはシンプルで、起動時に初期化用のスクリプトrustfs-setup.shを実行するのみです
FROM amazon/aws-cli:2.34.7
COPY infra/docker/rustfs-setup/rustfs-setup.sh /usr/local/bin/rustfs-setup.sh
ENTRYPOINT ["sh", "/usr/local/bin/rustfs-setup.sh"]
rustfs-setup.shの追加
#!/bin/sh
set -eu
set -x
echo "[rustfs-setup] start"
ENDPOINT_URL="${ENDPOINT_URL:-http://rustfs:9000}"
S3_BUCKET_NAME="${S3_BUCKET_NAME:-sample-bucket}"
SHARE_DIR="${SHARE_DIR:-/share}"
export AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-local}"
export AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-local}"
export AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-ap-northeast-1}"
# wait for RustFS (max 30s)
i=0
while ! aws s3api list-buckets --endpoint-url "${ENDPOINT_URL}" >/dev/null 2>&1; do
i=$((i+1))
if [ "$i" -ge 30 ]; then
echo "[rustfs-setup] ERROR: RustFS did not become ready within 30 seconds: ${ENDPOINT_URL}" >&2
exit 1
fi
sleep 1
done
aws --version
if aws s3api head-bucket --bucket "${S3_BUCKET_NAME}" --endpoint-url "${ENDPOINT_URL}" >/dev/null 2>&1; then
echo "[rustfs-setup] bucket already exists: ${S3_BUCKET_NAME}"
else
aws s3api create-bucket \
--bucket "${S3_BUCKET_NAME}" \
--endpoint-url "${ENDPOINT_URL}"
fi
if [ -d "${SHARE_DIR}" ] && [ -n "$(find "${SHARE_DIR}" -type f -print -quit 2>/dev/null)" ]; then
echo "[rustfs-setup] sync share to bucket: ${SHARE_DIR} -> s3://${S3_BUCKET_NAME}/"
aws s3 sync \
"${SHARE_DIR}/" \
"s3://${S3_BUCKET_NAME}/" \
--endpoint-url "${ENDPOINT_URL}" \
--no-progress
else
echo "[rustfs-setup] skip share sync: no files under ${SHARE_DIR}"
fi
# list for sanity
aws s3 ls --endpoint-url "${ENDPOINT_URL}"
aws s3 ls "s3://${S3_BUCKET_NAME}" --endpoint-url "${ENDPOINT_URL}"
echo "[rustfs-setup] done"
コード解説1
以下部分にて、rustfsサービスのS3互換APIが、実際に応答できる状態か最大30秒待機させています。
# wait for RustFS (max 30s)
i=0
while ! aws s3api list-buckets --endpoint-url "${ENDPOINT_URL}" >/dev/null 2>&1; do
i=$((i+1))
if [ "$i" -ge 30 ]; then
echo "[rustfs-setup] ERROR: RustFS did not become ready within 30 seconds: ${ENDPOINT_URL}" >&2
exit 1
fi
sleep 1
done
コード解説2
次のブロックとなる以下部分では、head-bucketを利用してバケットが既に作成済みかを確認しています。バケットが未作成の場合にのみ、create-bucketによるバケット作成が行われます。
if aws s3api head-bucket --bucket "${S3_BUCKET_NAME}" --endpoint-url "${ENDPOINT_URL}" >/dev/null 2>&1; then
echo "[rustfs-setup] bucket already exists: ${S3_BUCKET_NAME}"
else
aws s3api create-bucket \
--bucket "${S3_BUCKET_NAME}" \
--endpoint-url "${ENDPOINT_URL}"
fi
コード解説3
-d "${SHARE_DIR}"により、/shareディレクトリが存在するか確認します。そして、find "${SHARE_DIR}" -type f -print -quitにより/share配下にファイルがあるかを確認します。どちらも存在する場合、aws s3 syncにより、share配下の初期ファイル群を、まとめてバケットへ投入しています。今回は用途として不要なため、--deleteオプションは使用していないことに留意してください。
if [ -d "${SHARE_DIR}" ] && [ -n "$(find "${SHARE_DIR}" -type f -print -quit 2>/dev/null)" ]; then
echo "[rustfs-setup] sync share to bucket: ${SHARE_DIR} -> s3://${S3_BUCKET_NAME}/"
aws s3 sync \
"${SHARE_DIR}/" \
"s3://${S3_BUCKET_NAME}/" \
--endpoint-url "${ENDPOINT_URL}" \
--no-progress
else
echo "[rustfs-setup] skip share sync: no files under ${SHARE_DIR}"
fi
filesystems.phpの修正
Laravel側でプログラムを書いて、バケットに対して簡単な操作を行うために以下のようにs3の設定を整備しました。
return [
...省略
'disks' => [
...省略
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'ap-northeast-1'),
'bucket' => env('S3_BUCKET_NAME'),
'url' => env('AWS_S3_PUBLIC_URL'),
'endpoint' => env('AWS_S3_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_S3_USE_PATH_STYLE_ENDPOINT', false),
'throw' => true,
'report' => false,
],
],
...省略
];
.envの修正
Laravel側でプログラムを書いて、バケットに対して簡単な操作を行うために以下のように.envの設定を整備しました。
# ----------------------------------------------------------------
# AWS Auth Settings
# ----------------------------------------------------------------
AWS_DEFAULT_REGION=ap-northeast-1
AWS_ACCESS_KEY_ID=local
AWS_SECRET_ACCESS_KEY=local
...
# ----------------------------------------------------------------
# RustFS (Storage Server) Settings
# ----------------------------------------------------------------
FILESYSTEM_DISK=s3
# RustFS のエンドポイント(コンテナ間アクセス)
AWS_S3_ENDPOINT=http://rustfs:9000
# S3互換ストレージをローカル利用する場合はtrueを指定
AWS_S3_USE_PATH_STYLE_ENDPOINT=true
# バケット名(rustfs-setupで作成したバケット名)
S3_BUCKET_NAME=sample-bucket
# 公開用URL
AWS_S3_PUBLIC_URL=http://localhost:9000/sample-bucket
簡易的なバケット操作を試す
ホスト側にて、以下を配置します。sample.pngは適当に撮影したスクリーンショット画像で、sample.txtもaaa等が記載された確認用のテキストファイルになります。
infra/docker/rustfs-setup/share/images/sample.pnginfra/docker/rustfs-setup/share/sample.txt
上記の2ファイルは、rustfs-setupサービスからバケットに初期ファイル(オブジェクト)群がアップロードされているかを確認するために利用します。
次に、以下ファイルを用意します。docker compose exec app /bin/bash後に、php artisan app:rustfs_debugのように呼び出して利用します。rustfs_test()にてバケットに対してオブジェクトの作成・中身の取得・削除ができるか確認しており、rustfs_share_check()にて、初期オブジェクト群がバケット内にアップロードされているかを確認しています。
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class RustFsDebugCommand extends Command
{
protected $signature = 'app:rustfs_debug';
protected $description = 'RustFSに対して簡単なオペレーションを行います';
public function handle(): void
{
$this->rustfs_test();
$this->rustfs_share_check();
}
private function rustfs_test(): void
{
$disk = 's3';
$path = 'if-test/rustfs/' . Str::uuid() . '.txt';
$body = 'rustfs connectivity check: ' . now()->toDateTimeString();
try {
$this->info("[rustfs_test] start: disk={$disk}, path={$path}");
// オブジェクトの保存(trueであれば想定通り)
$putResult = Storage::disk($disk)->put($path, $body);
// オブジェクト保存後の存在チェック(trueであれば想定通り)
$existsAfterPut = Storage::disk($disk)->exists($path);
// オブジェクトの内容を取得
$storedBody = Storage::disk($disk)->get($path);
// オブジェクトの削除(trueであれば想定通り)
$deleteResult = Storage::disk($disk)->delete($path);
// オブジェクト削除後の存在チェック(falseであれば想定通り)
$existsAfterDelete = Storage::disk($disk)->exists($path);
$this->info('[rustfs_test] put_result=' . var_export($putResult, true));
$this->info('[rustfs_test] exists_after_put=' . var_export($existsAfterPut, true));
$this->info('[rustfs_test] stored_body=' . $storedBody);
$this->info('[rustfs_test] delete_result=' . var_export($deleteResult, true));
$this->info('[rustfs_test] exists_after_delete=' . var_export($existsAfterDelete, true));
} catch (\Throwable $e) {
Log::error('[rustfs_test] failed', [
'disk' => $disk,
'path' => $path,
'message' => $e->getMessage(),
]);
$this->error('[rustfs_test] failed: ' . $e->getMessage());
}
}
private function rustfs_share_check(): void
{
$disk = 's3';
$samplePath = 'sample.txt';
$imagePath = 'images/sample.png';
try {
$this->info("[rustfs_share_check] start: disk={$disk}");
$sampleExists = Storage::disk($disk)->exists($samplePath);
$sampleBody = $sampleExists ? Storage::disk($disk)->get($samplePath) : null;
$imageExists = Storage::disk($disk)->exists($imagePath);
$imageBody = $imageExists ? Storage::disk($disk)->get($imagePath) : null;
$imageSize = $imageBody !== null ? strlen($imageBody) : null;
Log::debug('[rustfs_share_check] result', [
'disk' => $disk,
'sample_path' => $samplePath,
'sample_exists' => $sampleExists,
'sample_body' => $sampleBody,
'image_path' => $imagePath,
'image_exists' => $imageExists,
'image_size' => $imageSize,
]);
$this->info('[rustfs_share_check] sample_exists=' . var_export($sampleExists, true));
if ($sampleBody !== null) {
$this->info('[rustfs_share_check] sample_body=' . $sampleBody);
}
$this->info('[rustfs_share_check] image_exists=' . var_export($imageExists, true));
if ($imageSize !== null) {
$this->info('[rustfs_share_check] image_size=' . $imageSize);
}
} catch (\Throwable $e) {
Log::error('[rustfs_share_check] failed', [
'disk' => $disk,
'sample_path' => $samplePath,
'image_path' => $imagePath,
'message' => $e->getMessage(),
]);
$this->error('[rustfs_share_check] failed: ' . $e->getMessage());
}
}
}
実行結果としては以下のような感じになります。
[rustfs_test] start: disk=s3, path=if-test/rustfs/33169e70-e838-4042-b151-2c2188632ba9.txt
[rustfs_test] put_result=true
[rustfs_test] exists_after_put=true
[rustfs_test] stored_body=rustfs connectivity check: 2026-05-04 01:08:28
[rustfs_test] delete_result=true
[rustfs_test] exists_after_delete=false
[rustfs_share_check] start: disk=s3
[rustfs_share_check] sample_exists=true
[rustfs_share_check] sample_body=aaa
bbb
ccc
[rustfs_share_check] image_exists=true
[rustfs_share_check] image_size=1275532
補足
公式では、以下のような説明があります。
RustFS container run as non-root user rustfs with id 10001, if you run docker with -v to mount host directory into docker container, please make sure the owner of host directory has been changed to 10001, otherwise you will encounter permission denied error. You can run chown -R 10001:10001 /path/to/host_directory to grant the necessary permissions.
今回構築したものに関しては、rustfsサービスにてホスト側とコンテナ側をバインドマウントしていないため、UID10001に関する権限設定を行なっていません。行なっているのはDockerが管理する永続領域となる名前付きボリューム(rustfs_data)をコンテナ側の/dataにマウントさせているのと、rustfs-setupサービスを利用した初期ファイル群をバケットにアップロードしているのみのため権限に関する設定が特に不要でした。
最後に
RustFSをローカル環境に導入してみようとしている方の一助になれれば幸いです。
最後まで見ていただきありがとうございます。