2026 年 9 月 14 日の朝から、FIXIT で開発しているプロジェクトの CI が失敗するようになりました。調査すると、開発・テスト環境で使っていた MinIO のコンテナイメージを取得できなくなっていました。そこで、ローカル開発用の S3 互換ストレージと、一部の統合テスト用ストレージを RustFS に切り替えました。

CI の失敗を調べる中で確認したのが、Docker Hub の minio/minio を pull すると失敗する問題です。イメージ名を変えて終わると思うかもしれませんが、実際には初期化用の minio/mc、バケットの設定、ブラウザーからのアクセス、テスト用コンテナの起動方法まで確認が必要でした。

この記事では、当日の作業記録とマージ済みの変更をもとに、移行時の判断と確認項目を紹介します。対象はローカル開発と統合テストです。本番の AWS S3 は変更しておらず、既存データを RustFS へ移送した事例でもありません。

MinIO のコンテナイメージを取得できない

最初に確認したエラーは、次の形式でした。

pull access denied for minio/minio, repository does not exist or may require 'docker login'

エラー文にはログインを求める可能性も書かれています。ただし、この文字列だけで認証不備と決めることはできません。今回の確認では、Docker Hub のリポジトリ情報を返す API も 404 でした。

確認先2026 年 9 月 14 日の確認結果
minio/minio のリポジトリ情報HTTP 404、object not found
minio/mc のリポジトリ情報HTTP 404、object not found
既存の Compose 設定上記イメージを参照していた
本番のストレージAWS S3 を利用。今回の変更対象外

MinIO サーバーの Docker Hub APIMinIO Client の Docker Hub API は、執筆時にも確認しました。これはその時点の取得結果であり、削除日や削除理由を示すものではありません。

公式の MinIO リポジトリ には、メンテナンス終了と、コミュニティ版をソースコードのみで配布する旨が記載されています。ここから確認できる配布方針と、今回観測した Docker Hub の応答は区別して扱います。「MinIO があらゆる環境で使用不能になった」とまでは言えません。

キャッシュ済みのイメージで起動している開発者と、新しく取得する開発者では、症状が異なる可能性があります。自分の端末でコンテナが起動できても、新しい作業環境を再現できるとは限りません。切り分けでは、取得、起動、初期化、アプリからの利用を順に見ます。

同じ MinIO という名前でも、サーバーと初期化用クライアントは別のイメージです。サーバーの参照先だけ直しても、minio/mc を使う setup サービス用のイメージを取得できなければ、バケットを用意できません。検索対象は Compose のサービス名だけに絞らないほうがよいです。

最初に、どの環境が MinIO を使っているか調べた

当日の作業では、組織内のリポジトリから MinIO の参照を探しました。ただし、検索結果の件数をそのまま移行対象の件数にはしていません。過去の設計資料、コメント、別の識別子に含まれる文字列も見つかるためです。

実際に確認したのは、Compose、テストのセットアップ、環境変数のサンプル、開発環境を用意するスクリプトです。CI が Compose を使うのか、Testcontainers で独立したコンテナを起動するのかも調べました。

利用箇所見るもの見落とすと起きること
ローカル開発Compose のイメージ、ポート、ボリューム新規セットアップが失敗する
初期化処理バケット作成、Versioning、起動待ちサーバーは起動しても保存先がない
統合テストテスト内で指定するイメージと起動クラスCompose を直してもテストが旧実装を使う
ブラウザー署名付き URL と CORSSDK のテストは成功しても画面からアップロードできない
開発用設定.env.example と設定生成スクリプト認証情報や接続先が食い違う

当日の朝から CI が失敗するようになったことをきっかけに、他のプロジェクトへの影響も調べました。統合テストが MinIO を起動する構成では、CI で使うイメージも対応対象になります。一方、最初に RustFS へ移行したプロジェクトでは、CI は MinIO を使っていませんでした。こちらはローカル開発環境の変更です。各プロジェクトの CI 失敗を、一律に同じ原因と扱わないようにしました。

この区別は、対応の優先順位にも関わります。新規の開発環境だけが作れないのか、テストが開始できないのか、稼働中のシステムが使えないのか。それぞれ確認するログと復旧の範囲が違います。今回の記事で紹介するのは、開発とテストの環境を整え直す作業です。

RustFS に切り替える際の前提を決めた

RustFS は S3 互換のオブジェクトストレージです。製品の位置づけは RustFS の公式リポジトリ で確認できます。今回の作業では、rustfs/rustfs:1.0.0-rc.6 を使いました。このリリース はプレリリースとして公開されています。

選定時に重視したのは、既存の S3 クライアントから必要な操作を実行できることと、開発・テスト環境の設定を管理できることでした。性能比較のための移行ではなく、今回の作業で MinIO と RustFS の速度を比較したわけでもありません。

イメージは latest にせず、確認に使う版を固定しました。Compose と統合テストが別のバージョンを使っていると、ローカルで確認した挙動をテスト結果と対応づけにくくなるからです。これは当日の検証条件であり、将来も同じ RC 版を使い続けるという推奨ではありません。

また、既存の開発用ボリュームを再利用する互換対応は行わず、新しいボリュームで環境を作る方針にしました。保存形式の互換性や、過去データの移送を確認したわけではありません。手元のデータを残す必要がある場合は、今回とは別にバックアップと移送の計画が必要です。

FIXITFIXIT

S3 互換なら、イメージの名前を変えるだけでいいの?

DodaiDodai

API の互換性と、起動・初期化の手順は別です。使う操作と周辺設定を確認します。

Compose はストレージと初期化サービスを変更した

変更の中心は、MinIO のサーバーを RustFS に、mc を使う初期化処理を AWS CLI に置き換えることでした。主要な対応関係は次のとおりです。

変更前変更後
minio/miniorustfs/rustfs:1.0.0-rc.6
MINIO_ROOT_USERRUSTFS_ACCESS_KEY
MINIO_ROOT_PASSWORDRUSTFS_SECRET_KEY
minio/mc による初期化amazon/aws-cli:2.36.44 による初期化
mc mbaws s3api create-bucket
mc version enableaws s3api put-bucket-versioning
MinIO の起動待ちRustFS の /health/ready を確認

下の YAML は、実際の変更からストレージの設定を抜粋したものです。説明用にまとめた部分であり、バケットを作成する setup サービスは含めていません。認証情報はローカル用のダミー値で、公開ポートはループバックアドレスに限定しています。

services:
  rustfs:
    image: rustfs/rustfs:1.0.0-rc.6
    ports:
      - "127.0.0.1:${S3_PORT:-9000}:9000"
      - "127.0.0.1:${S3_CONSOLE_PORT:-9001}:9001"
    environment:
      RUSTFS_ACCESS_KEY: local-access-key
      RUSTFS_SECRET_KEY: local-secret-key
      RUSTFS_CONSOLE_ENABLE: "true"
    volumes:
      - rustfs-data:/data
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/health/ready"]
      interval: 5s
      timeout: 5s
      retries: 12
      start_period: 10s
 
volumes:
  rustfs-data:

コンテナから接続する場合と、ホストのアプリから接続する場合では、エンドポイントも分かれます。今回の setup サービスは http://rustfs:9000 を使い、ホスト側は公開ポートに接続します。コンテナ内の localhost をホストの開発環境と混同しないよう、接続元ごとに設定を確認しました。

既定の公開ポートを変えるプロジェクトでは、.env.example だけでなく、環境変数を生成するスクリプトも合わせて変更します。今回は MINIO_PORTMINIO_CONSOLE_PORT を、製品名に依存しない S3_PORTS3_CONSOLE_PORT に改名しました。環境変数を読むコードに旧名が残ると、想定と違うポートへ接続してしまいます。

認証情報も、サーバー、setup サービス、アプリ、テストでそろえる必要があります。サーバーだけが新しい値を使い、アプリが古い値を読む状態では、イメージの取得に成功しても認証に失敗します。ダミー値の変更であっても、設定をまたぐ整合性の確認は省けません。

起動成功と、アプリから利用できる状態を分ける

コンテナのプロセスが開始したことと、ストレージがリクエストを受け付けられることは同じではありません。さらに、ストレージが準備できても、アプリが必要とするバケットはまだ作られていない場合があります。

今回、最初のプロジェクトでは依存関係を次の順序にしました。

  1. RustFS を起動する
  2. /health/ready の成功をヘルスチェックで確認する
  3. setup サービスがバケットと必要な設定を作る
  4. setup サービスの正常終了後に、依存する worker を起動する

Compose では、setup から RustFS への依存に service_healthy を使いました。worker から setup への依存には service_completed_successfully を指定しています。Docker の起動順序の説明 でも、この 2 つは異なる条件として定義されています。

RustFS の Status Check には、/health/ready と、単なる生存確認を行うエンドポイントの違いが記載されています。今回の待機に使ったのは readiness です。新しいボリュームで RustFS を起動し、コンテナが healthy になることと、setup が終了コード 0 で完了することを確認しました。

setup は、一度実行したら終わりという扱いにもしていません。バケットの存在を確認し、必要なときだけ作成したあと、Versioning などの設定を適用する処理にしました。同じ環境で setup を再実行しても、終了コード 0 になることを確認しています。

ここで再実行の成功と、あらゆる障害時の復旧は区別が必要です。たとえば認証エラーが起きているのに、単に「バケットがない」と解釈して作成を続けても解決しません。初期化処理が失敗した場合には、終了コードとログから、接続・認証・作成のどこで失敗したかを追えることが大切です。

Versioning と削除処理も確認した

最初のプロジェクトでは、複数のバケットで Versioning を有効にしていました。アプリの削除処理は、オブジェクトのバージョンを列挙し、対象の VersionId を指定して削除する構成です。そのため、単純な PUT と GET の確認だけでは足りませんでした。

当日の確認では、同じキーへ複数回書き込み、バージョンが作成されることを確かめています。続けて ListObjectVersionsDeleteObjects を使う処理を確認しました。バケットが空の状態で削除メソッドを呼ぶだけでは、実際の保存データを削除できるか判断できません。

移行先が同じ S3 API を提供していても、アプリが前提とするバケット設定まで自動で移るわけではありません。設定が欠けた状態でも基本的なアップロードは成功する可能性があるため、実際の業務処理が使う経路を確認対象にします。

検証項目はストレージの機能一覧から無制限に増やすのではなく、アプリの実装から決めました。今回であれば、保存、取得、バージョンを含む削除、署名付き URL、マルチパートアップロードです。使っていない機能まで検証済みと扱わないことも、結果を読む人に必要な情報です。

ブラウザーからのアップロードでは CORS を調整した

今回の移行で手当てが必要になったのが CORS でした。最初のプロジェクトでは、ブラウザーや拡張機能から署名付き URL を使ってアップロードするため、バケットごとにローカル用の CORS 設定を追加しました。

CORS は、ブラウザーが異なるオリジンへアクセスする際の制御です。サーバー側の SDK やコマンドラインで PUT に成功しても、ブラウザーから同じ経路を使えるとは限りません。実際の確認では、事前確認リクエストに対する許可ヘッダーと、ETag の読み取りも対象にしました。

当日の設定では、可変ポートの開発環境と拡張機能を扱うため、ローカル用バケットの許可オリジンに * を使っています。この設定を、インターネットに公開するストレージの推奨例にはしません。RustFS の CORS 設定資料 でも、オリジンとブラウザーの認証情報の扱いを確認できます。バケット設定とサーバー全体の環境変数も区別して読む必要があります。

確認する内容は、アップロード成功の一言でまとめず、リクエストの段階ごとに分けました。

段階確認すること
署名付き URL の発行アプリが想定する接続先と認証情報を使う
事前確認必要なオリジン・メソッド・ヘッダーが許可される
PUT の実行オブジェクトが保存される
レスポンスの利用ブラウザー側で必要な ETag を読める
GET の実行保存した内容を取得できる

一方、別のプロジェクトでは、サーバー側から S3 API を呼び出すだけだったため、同じ CORS 設定を追加していません。また、署名付き URL を別ウィンドウで開く経路と、JavaScript がレスポンスを読む経路も同一には扱いませんでした。

複数のプロジェクトへ移行を広げるときに、先に成功した設定を丸ごとコピーしたくなります。しかし、ブラウザーがどこでストレージへ接続するかはプロジェクトごとに違います。CORS は、実際のアクセス経路を確認してから必要なところへ適用する設定です。

Testcontainers は MinioContainer から GenericContainer に変更した

統合テストで MinIO を起動していたプロジェクトでは、Compose の変更だけでは不十分でした。テスト側が @testcontainers/minioMinioContainer を使っていたため、RustFS を GenericContainer で起動する形に変更しています。

この変更では、環境変数、公開ポート、準備完了の待機条件を明示しました。起動後のエンドポイントは、固定のホストポートを仮定せず、コンテナから取得したホスト名と割り当てポートを使っています。

Testcontainers for Node.js のコンテナ資料 には、汎用コンテナの起動と環境変数の指定方法が載っています。以下は、当日のテストセットアップから起動部分を抜き出し、変数名を一般化した例です。プロジェクト固有のテストへの値の受け渡しと、バケット初期化処理は含めていません。

import { GenericContainer, Wait } from "testcontainers";
 
const storage = await new GenericContainer("rustfs/rustfs:1.0.0-rc.6")
  .withEnvironment({
    RUSTFS_ACCESS_KEY: "local-access-key",
    RUSTFS_SECRET_KEY: "local-secret-key",
    RUSTFS_VOLUMES: "/data",
  })
  .withExposedPorts(9000)
  .withWaitStrategy(Wait.forHttp("/health/ready", 9000).forStatusCode(200))
  .start();
 
const endpoint = `http://${storage.getHost()}:${storage.getMappedPort(9000)}`;
 
// endpoint をテスト用の S3 クライアントへ渡します。
// テスト終了時の teardown で await storage.stop() を呼びます。

アプリ側の S3 クライアントは既存のものを使い、接続先と認証情報をテストへ渡す形を維持しました。終了時に RustFS のコンテナを停止する処理も更新しています。起動する名前だけ変えて、終了処理に旧コンテナの変数を残さないようにします。

このプロジェクトでは、RustFS に対するストレージの統合テスト 15 件が成功しました。その後、マージで実行された CI の成功も作業記録で確認しています。ただし、アプリ全体のテスト件数を「すべて RustFS の互換性を検証した件数」と読み替えることはできません。

AI を使ってテストを整備する際にも、何を保証するテストなのかを先に決める必要があります。進め方は AI 駆動 TDDAI テスト自動化の実践 でも整理しています。

コンソールの URL は、起動後に実際に開いて確認する

設定変更後に残った修正の 1 つが、管理コンソールの URL でした。最初の案内では http://localhost:9001 と書いていましたが、今回使った構成では / が 403、/rustfs/console/ が 200 でした。

そのため、開発用の案内を次の形へ直しています。

http://localhost:9001/rustfs/console/

ポートを変えている場合は、ホスト側に公開したコンソール用ポートへ読み替えます。これは今回の版と構成で確認した結果です。RustFS の コンソール資料 と、実際に使う版の動作を合わせて確認してください。

この不備は、S3 API のテストだけでは見つかりません。オブジェクトの読み書きに成功していても、開発者向けの案内は間違っていることがあります。README や環境変数サンプルに URL を載せるなら、その URL で画面を開くところまでが確認対象です。

Claude Code と Codex の分担で、確認をどこに残したか

今回の作業では、Claude Code が調査と取りまとめを行い、Codex のワーカーへ実装を委譲しました。最初のプロジェクトで変更内容を確認したあと、他のプロジェクトへ広げています。

作業記録では、実装担当の報告と、取りまとめ側で実施した確認が分けて残されていました。たとえば最初の移行では、実装担当が署名付き URL やマルチパートアップロードの結果を報告し、取りまとめ側でも新しいボリュームからの起動、setup の再実行、Versioning、CORS の応答を確認しています。

確認項目当日の記録で確認できた範囲
新規環境の起動healthy と setup の正常終了を確認
setup の再実行再実行後も終了コード 0
Versioning対象バケットで有効化を確認
バージョンを含む削除列挙と VersionId を指定した削除を確認
署名付き URLGET と PUT の成功を記録
マルチパート6 MiB のアップロード成功を記録
統合テスト別プロジェクトでストレージ関連 15 件が成功
コンソール案内パスを修正し、HTTP 200 を確認

この表は複数プロジェクトの確認をまとめたもので、全プロジェクトに同じ機能とテストがあるという意味ではありません。記事を書く際にも、マージ済みの差分と作業記録を照合しました。

分担して実装を進められることと、確認を省けることは別です。今回もコンソールの URL や、不要な CORS 設定は、実装後の確認で修正しました。複数の AI エージェントを使うときの役割分担は、AI エージェントの並列運用 にまとめています。

FIXITFIXIT

テストが通ったら、移行完了にしていいの?

DodaiDodai

新しい環境の起動、画面からの操作、案内 URL も確認します。テストが扱う範囲を見ます。

移行後に残る確認と、今回扱っていないこと

開発環境を切り替えるときは、以前のコンテナやポート割り当ても確認が必要です。今回もレビューで、旧サービスのコンテナが残り、新サービスの公開ポートと競合する可能性が指摘されました。サービス名を変更しただけでは、既存環境が新しい構成へ自動でそろうとは限りません。

既存環境を更新する人向けには、利用中のコンテナとボリュームを識別し、残すデータを決めたうえで切り替え手順を案内します。新規ボリュームでの検証に成功したことを、既存データの移行保証として扱わないようにします。

今回、確認していないのは、本番ストレージの移行、過去データの移送、全 S3 API の互換性、長期運用の安定性、性能の優劣です。RC 版を開発とテストで使った結果から、本番でも同じ判断ができるとは言えません。実運用で必要な可用性や復旧条件は、別途評価する対象です。

また、バージョン固定は、どの版を使うかを明確にするためのものです。参照するイメージ自体を将来も取得できる保証にはなりません。今回の取得失敗を踏まえると、重要な開発用依存については、配布元と取得方法を見直すことも今後の検討事項になります。

まとめ

MinIO のイメージ取得失敗を受け、ローカル開発と一部の統合テストを RustFS に切り替えました。変更の中心は Compose でしたが、初期化用クライアント、起動待ち、Versioning、CORS、署名付き URL、Testcontainers、案内用のコンソール URL まで確認しています。

S3 互換ストレージのリプレイスでは、アプリが実際に使う操作と、開発環境を再現する手順を組にして検証する必要がありました。今回の結果は、新しいボリュームで開発・テスト環境を作り直した範囲のものです。その前提と確認項目を残しておくと、次に環境を用意する人も、何を確かめればよいか判断できます。

FIXIT は AI 駆動開発のクリエイティブスタジオとして、プロダクト開発と、それを支える開発・テスト環境の改善に取り組んでいます。既存環境の見直しや AI を組み込んだ開発体制については、お問い合わせ からご相談ください。