こんにちは! デバイスソフトウエア開発部の米森です。
今回は、開発中に直面しがちな「ローカル環境でのS3画像配信問題」の解決策をご紹介します。クライアントがサーバーから画像のpath(オブジェクトキー)を受け取り、そのパスとCloudFrontを使って画像を表示するというシンプルなシステム構成を例に、MinIOを活用したローカルS3環境の構築方法を解説させていただきます!
前提
本記事では、以下のようなシステム構成を想定します。

クライアントがバックエンドに画像のリクエストを送り、バックエンドがS3に格納されている画像のpathを返却する。クライアントがそのpathとCloudFrontで画像を表示する。シンプルな構成です。
フロントエンド(React+TypeScript)の実装イメージ。
| 
					 1 2 3 4 5 6 7 8  | 
						// 環境変数からベースURLを取得 const IMAGE_BASE_URL: string = process.env.REACT_APP_IMAGE_BASE_URL; function SampleImage({ path }: { path: string }) {   const src = `${IMAGE_BASE_URL}${path}`;   return <img src={src} alt="" />; }  | 
					
バックエンドから来たpathを受け取り、環境変数化されているベースURLと合体させて配信画像のsrcを生成し、表示するコンポーネントです。
バックエンドアプリケーションからは、下記のようなJSONが返却されると仮定します。
| 
					 1 2 3 4 5 6 7 8 9 10  | 
						[     {         "path": "/aaa/bb_sample.jpg",         "foo": "bar1"     },     {         "path": "/xxx/yy_sample.jpg",         "foo": "bar2"     } ]  | 
					
上記が組み合わさり、最終的に以下のようなURLが生成されます。
| 
					 1  | 
						https://foofoobarbar.cloudfront.net/aaa/bb_sample.jpg  | 
					
このURLを用いて、
| 
					 1  | 
						 <img src="https://foofoobarbar.cloudfront.net/aaa/bb_sample.jpg" alt="">  | 
					
のようなimgタグが生成され、画像が表示されるという流れです。
ローカル環境での問題点
画像を表示するには、CloudFrontのベースURLに紐づくS3バケットに”/aaa/bb_sample.jpg”というオブジェクトが存在している必要があります。
しかしローカル環境だとS3バケット自体がそもそもないという場合が多くあります。DEV環境のバケットを間借りすることもできますが、データの混在は避けたいですし、それぞれの環境は独立させたいです。
MinIOでローカルS3構築
ローカル開発をローカルで完結させるために、MinIOを使って疑似S3を構築しましょう。
以下の内容でdocker-compose.ymlファイルを作成し、コンテナを立ち上げるだけで基本的な準備は完了します。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25  | 
						version: "3.2" services:   minio:     image: minio/minio:latest     environment:       MINIO_ROOT_USER: test-user       MINIO_ROOT_PASSWORD: test-password     ports:       - 9000:9000       - 9001:9001     volumes:       - ./.minio-storage/:/storage     command: ["server", "/storage", "--console-address", ":9001"]   bucket-setup:     image: minio/mc     depends_on:       - minio     entrypoint: >       /bin/sh -c "       /usr/bin/mc config host add myminio http://minio:9000 test-user test-password;       /usr/bin/mc mb --ignore-existing myminio/test-bucket;       /usr/bin/mc anonymous set public myminio/test-bucket;       exit 0;       ",  | 
					
minio:
MinIOサーバー本体のサービス定義です。
image: minio/minio:latest:- MinIOサーバーとして使用するDockerイメージを指定しています。 
latestタグは、利用可能な最新の安定版MinIOイメージをプルします。 
- MinIOサーバーとして使用するDockerイメージを指定しています。 
 environment::- MinIOサーバー内で使用される環境変数を設定します。
 MINIO_ROOT_USER: test-user: MinIOコンソールおよびAPIにアクセスするためのルートユーザー名をtest-userに設定します。MINIO_ROOT_PASSWORD: test-password: 上記ユーザーのパスワードをtest-passwordに設定します。
ports::- ホストマシンとコンテナ間のポートマッピングを定義します。
 - 9000:9000: ホストの9000番ポートをコンテナの9000番ポート(MinIOのAPIエンドポイント)にマッピングします。フロントエンドから画像を取得する際のベースURLとして使用されます。- 9001:9001: ホストの9001番ポートをコンテナの9001番ポート(MinIOのWebコンソール)にマッピングします。ブラウザでhttp://localhost:9001にアクセスすると、MinIOの管理画面にログインできます。
volumes::- ホストマシンとコンテナ間でデータを共有するためのボリュームを定義します。
 - ./.minio-storage/:/storage: ホストマシン上の現在のディレクトリにある.minio-storageフォルダを、MinIOコンテナ内の/storageパスにマウントします。これにより、MinIOにアップロードされたデータは、コンテナが停止・削除されてもホストマシン上に永続的に保存されます。
command: ["server", "/storage", "--console-address", ":9001"]:- MinIOコンテナが起動したときに実行されるコマンドです。
 server /storage:/storageディレクトリをストレージとしてMinIOサーバーを起動します。--console-address ":9001": MinIOのWebコンソールを9001番ポートで利用可能にします。
bucket-setup:
MinIOサーバーを初期設定するためのクライアントサービスの定義です。
image: minio/mc:- MinIOのコマンドラインツール(
mc)を含むDockerイメージを使用します。 
- MinIOのコマンドラインツール(
 depends_on::- このサービスが依存する他のサービスを指定します。
 - minio:minio-clientサービスはminioサービスが起動するまで待機します。これにより、MinIOサーバーが完全に起動してからクライアントが操作を開始できるようになります。
entrypoint: >:- コンテナが起動したときに実行されるコマンドのシーケンスです。複数行のシェルスクリプトを実行するために 
>(YAMLのブロック引用スタイル) を使用しています。 /usr/bin/mc config host add myminio http://minio:9000 test-user test-password:mc config host add myminio http://minio:9000 test-user test-password:myminioという名前でMinIOサーバーへの接続情報を登録します。http://minio:9000は、minioサービス名を使って、Dockerネットワーク内でMinIOサーバーにアクセスできることを示しています。ユーザー名とパスワードはminioサービスで設定したものと同じです。
/usr/bin/mc mb --ignore-existing myminio/test-bucket;:mc mb myminio/test-bucket:myminio接続を使用してtest-bucketという名前のバケットを作成します。--ignore-existing: もしtest-bucketが既に存在していてもエラーにせず無視します。
/usr/bin/mc anonymous set public myminio/test-bucket;:mc anonymous set public myminio/test-bucket:test-bucketのアクセス権限を「パブリック」に設定します。これにより、フロントエンドから認証なしで画像にアクセスできるようになります。
exit 0;: スクリプトが正常に終了したことを示します。
- コンテナが起動したときに実行されるコマンドのシーケンスです。複数行のシェルスクリプトを実行するために 
 
コンソールにアクセス
コンテナを立ち上げた状態で、localhost:9001にアクセスするとログイン画面に遷移します。

認証情報はymlに記載した
| 
					 1 2  | 
						MINIO_ROOT_USER: test-user MINIO_ROOT_PASSWORD: test-password  | 
					
です。
ログインすると、test-bucketというバケットがあります。
このバケットに”/aaa/bb_sample.jpg”オブジェクトを追加してみましょう。
右側の”Create new path”から “aaa/”というpathを作成し、

Upload Fileでbb_sample.jpgをアップロード。

いけました!

ちなみに、このアップロードしたオブジェクトは設定したホストマシンのディレクトリ(今回は “.minio-storage”)に格納されます。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23  | 
						. ├── .minio-storage │   ├── .minio.sys │   │   ├── buckets │   │   │   ├── .bloomcycle.bin │   │   │   ├── .usage-cache.bin │   │   │   ├── .usage-cache.bin.bkp │   │   │   ├── .usage.json │   │   │   └── test-bucket │   │   ├── config │   │   │   ├── config.json │   │   │   └── iam │   │   ├── format.json │   │   ├── multipart │   │   ├── pool.bin │   │   │   └── xl.meta │   │   └── tmp │   │       ├── .trash │   │       └── 87d8e64d-7dd0-4d50-a592-b94ebaea1882 │   └── test-bucket │       └── aaa │           └── bb_sample.jpg └── docker-compose.yml  | 
					
MinIOは飽くまでインターフェースで、データの実態はここにあります。
API経由で画像を取得する
画像を設置したので、次はAPI経由でその画像を取得してみます。
9000番ポートをAPIエンドポイントとして設定したため、
| 
					 1  | 
						localhost:9000/<bucket_name>/<object_key>  | 
					
の形式で画像にアクセスできます。

取れましたね!
あとは、フロント側の環境変数でローカル環境の場合は”localhost:9000″を見るようにすれば、疑似的なS3画像配信をローカルで完結できます。
はまったポイント
★データはホストマシン側にあることを忘れずに
上述した通り、データストレージの実態はホストマシンにある特定のディレクトリにあり、それをコンテナにマウントしています。バケットの設定内容などもここにあります。
MinIOコンテナのライフサイクルとは独立しているため、注意が必要です。
★MinIOコンソールとAPIは別のポート
MinIOはAPIとコンソールのポートをデフォルトでそれぞれ9000と9001に割り当てます。構築当初、9001でコンソールにアクセスできるようになったので、APIも9001でアクセスしようとしたらうまくいかなくて気づきました。
★フロント側のベースURLが若干違う
CloudFrontでS3の画像を配信する際、ディストリビューションドメイン(今回でいう”foofoobarbar.cloudfront.net”)は画像バケットに紐づいている想定です。なので、URLは下記のように「ベースURL + path(オブジェクトキー)」です。バケット名は含まれていません。
| 
					 1  | 
						https://foofoobarbar.cloudfront.net + /aaa/bb_sample.jpg  | 
					
しかし、MinIOサービスにAPI経由で当たる場合、localhostドメインにはバケットの情報が紐づいていないので、パスに対象バケットを含める必要があります。
| 
					 1  | 
						localhost:9000/test-bucket + /aaa/bb_sample.jpg  | 
					
今回の実装イメージでは、ベースURLをフロントの環境変数で持っているので、ローカルの場合は対象バケット名もURLに含めるようにしましょう。
まとめ
画像をCloudFront経由で配信する、くらいのシンプルな構成ならMinIOで簡単に再現できます。ローカル疑似環境と実際のAWS環境の違いには留意する必要がありますが、便利なのでぜひ使ってみてください!
また、エコモットでは一緒にモノづくりをしていく仲間を随時募集しています。弊社に少しでも興味がある方はぜひ下記の採用ページをご覧ください!
      


