6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

ローカル開発環境に、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サービスの正常終了を確認するまで起動しないようにしています。
docker-compose.yml
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を実行するのみです

infra/docker/rustfs-setup/Dockerfile
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の追加

infra/docker/rustfs-setup/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の設定を整備しました。

src/config/filesystems.php
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の設定を整備しました。

src/.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.txtaaa等が記載された確認用のテキストファイルになります。

  • infra/docker/rustfs-setup/share/images/sample.png
  • infra/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()にて、初期オブジェクト群がバケット内にアップロードされているかを確認しています。

src/app/Console/Commands/RustFsDebugCommand.php
<?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をローカル環境に導入してみようとしている方の一助になれれば幸いです。
最後まで見ていただきありがとうございます。

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?