コンテンツにスキップ

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 は次のようになる。

http://rustfs:9000

しかし、ブラウザや host machine からアクセスできる endpoint は次のようになる。

http://localhost:9000

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 として発行する必要がある。

タグ

s3 #rustfs #presigned-url #docker-compose #go #local-testing