S3エミュレーションでrustfsを使ってみたメモとPresigned URLの仕組み¶
チェック¶
- [ ] 本文を確認した
- [ ] 概要を確認した
- [ ] タグを確認した
- [ ]
inbox/直下へ移行した
概要¶
ローカルテスト用の S3 互換オブジェクトストレージとして rustfs を Docker Compose で使った記事。
minio の Docker image 配布停止や LocalStack のユーザー登録必須化を背景に、S3 だけ欲しい用途で rustfs を選んでいる。
AWS SDK での読み書きは簡単だが、Docker 内部 endpoint とブラウザから見える public endpoint が違うため、Presigned URL 発行時だけ endpoint を localhost 側に差し替える必要があった、という学びが中心。
本文¶
記事は、オブジェクトストレージ前提の小さな Web backend をローカルテストするため、rustfs を使ったメモ。 S3 互換ストレージとしては minio を使うことが多かったが、minio の Docker image 配布停止や LocalStack のユーザー登録必須化が話題になっていたため、代替を検討した。
用途は、Docker Compose でアプリと一緒に起動し、S3 互換 API で object を読み書きできること。 S3 以外の AWS service emulator までは不要。 複数候補の中から、単一 container で使いやすい rustfs を選んでいる。
compose.yaml¶
rustfs は公式 image を使う。
API endpoint はデフォルトで 9000、管理画面は RUSTFS_CONSOLE_ENABLE を有効にすると 9001 で開く。
テスト用途で可用性は不要なため、volume は1つにしている。
起動時に bucket を作っておきたいので、amazon/aws-cli image を使った init container 的な service を置く。
構成イメージは次の通り。
services:
rustfs:
image: rustfs/rustfs:latest
environment:
RUSTFS_CONSOLE_ENABLE: "true"
RUSTFS_ACCESS_KEY: rustfsadmin
RUSTFS_SECRET_KEY: rustfsadmin
RUSTFS_VOLUMES: /data/rustfs0
volumes:
- rustfs-data:/data
- rustfs-logs:/logs
ports:
- "9000:9000"
- "9001:9001"
healthcheck:
test: ["CMD", "sh", "-c", "curl -sS http://localhost:9000/ >/dev/null"]
interval: 1s
timeout: 5s
retries: 20
rustfs-init:
image: amazon/aws-cli:2.31.15
entrypoint: ["/bin/sh", "-c"]
command:
- |
set -eu
until aws --endpoint-url http://rustfs:9000 s3api list-buckets >/dev/null 2>&1; do
sleep 2
done
for bucket in data-bucket log-bucket; do
aws --endpoint-url http://rustfs:9000 s3api create-bucket --bucket "$bucket" || true
done
environment:
AWS_ACCESS_KEY_ID: rustfsadmin
AWS_SECRET_ACCESS_KEY: rustfsadmin
AWS_REGION: us-east-1
depends_on:
rustfs:
condition: service_healthy
volumes:
rustfs-data:
rustfs-logs:
RUSTFS_ADDRESS のような環境変数を設定する例もあるが、記事の構成ではデフォルトの 9000/9001 で問題なく動いた。
管理画面も軽く、ファイル管理画面としての体験が良いと評価されている。
Presigned URL で起きた問題¶
AWS SDK を使った backend からの object 読み書きは問題なかった。 問題になったのは Presigned URL。
Docker Compose 内の backend service から rustfs にアクセスするときの endpoint は次のようになる。
しかし、ブラウザや host machine からアクセスできる endpoint は次のようになる。
rustfs では、Presigned URL で発行される URL が request 時の host 情報をもとに作られる。
backend container 内から http://rustfs:9000 で presign すると、発行される URL も rustfs:9000 になる。
その URL を browser に渡しても、browser からは rustfs という Docker 内部 DNS 名を解決できない。
このため、Presigned URL 発行時だけ public endpoint を使う必要がある。
Presigned URL 発行時だけ endpoint を差し替える¶
解決策は、Presigned URL 発行時に一時的な S3 client を作り、endpoint を http://localhost:9000 など外部から見える値に差し替えること。
通常の backend から rustfs への API call では Docker 内部 endpoint を使い、presign だけ public endpoint を使う。
Go の実装イメージは次のようなもの。
func (s *Store) PresignGet(ctx context.Context, key string, expiry time.Duration) (string, error) {
if endpoint := os.Getenv("RUSTFS_OBJECT_PUBLIC_ENDPOINT"); endpoint != "" {
options := s.options
options.Endpoint = endpoint
client, err := newS3Client(ctx, options)
if err == nil {
presigner := s3.NewPresignClient(client)
out, err := presigner.PresignGetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
}, func(o *s3.PresignOptions) {
o.Expires = expiry
})
if err == nil {
return out.URL, nil
}
}
}
presigner := s3.NewPresignClient(s.client)
out, err := presigner.PresignGetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
}, func(o *s3.PresignOptions) {
o.Expires = expiry
})
if err != nil {
return "", err
}
return out.URL, nil
}
最初は、endpoint を localhost にすると backend container から rustfs へ接続できなくなると思っていた。
しかし、Presigned URL 発行では SDK が実際に S3 API を叩くわけではない。
署名付き URL は SDK 内で生成される。
そのため、URL 生成に使う endpoint だけを browser から見える host に差し替えてもよい。
Presigned URL の仕組み¶
Presigned URL は、サーバー側に一時 token を保存しているわけではない。 URL に access key ID、期限、署名などが含まれ、それを secret access key で署名する。
client は credential を持たず、その URL だけを使って object を GET/PUT する。 S3 や rustfs は URL 内の署名を検証し、改ざんされていないこと、誰が署名したか、期限内かを確認する。
署名対象には host や path も関係する。 そのため、Docker 内部の host で署名された URL を外部から別 host で使うと、署名検証や接続で問題が起きる。 Presigned URL は、実際に client がアクセスする URL として生成する必要がある。
他の選択肢¶
記事では、S3 以外の AWS emulator も必要なら moto や floci なども候補になると触れられている。 しかし、今回の要件は S3 だけだったため rustfs が合っていた。
seaweedfs なども良さそうだったが、複数 container が必要そうだったため、1 container で済む rustfs を選んだ。 メモリ消費は約 90MB 程度で、動きも軽快。
要点¶
- S3 だけ欲しいローカルテスト用途では rustfs が軽量な候補になる。
- Docker Compose では rustfs service と aws-cli による bucket 作成 service を組み合わせると便利。
- backend container からの endpoint は
http://rustfs:9000、browser からの endpoint はhttp://localhost:9000になる。 - Presigned URL は実際の S3 API call ではなく SDK 内で署名付き URL を生成する。
- Presigned URL 発行時だけ public endpoint に差し替えた S3 client を作ると、browser から使える URL になる。
- 署名対象には host/path/期限などが含まれるため、client が実際に使う URL として発行する必要がある。