RemitAid Tech Blog
💾

MinioからLocalStackに乗り換えました

に公開

こんにちは!RemitAidでソフトウェアエンジニアをしているnos0611です。

はじめに

RemitAidではローカル開発環境でS3互換ストレージとしてMinioを使用していたのですが、この度LocalStackに乗り換えました。
本記事ではその背景と対応内容についてご紹介します。

なぜ乗り換えたのか?

2025年10月、MinioがDockerイメージの無料配布を終了するというニュースが流れました。

https://gigazine.net/news/20251023-minio-stops-distributing-free-docker-images/

これにより、今後ローカル開発環境でMinioを使い続けることが難しくなる可能性が出てきたため、S3のローカルエミュレーションをLocalStackに移行することにしました。

LocalStackとは

LocalStackは、AWSのクラウドサービスをローカル環境でエミュレートするツールです。
S3だけでなく、SQS、Lambda、DynamoDBなど多くのサービスに対応しており、ローカルでAWSライクな開発・テストが可能になります。
単にS3互換というだけであれば他にも選択肢はありましたが、SQSやLambdaについてもローカルで構築する予定があったためLocalStackを選択しています。

https://docs.localstack.cloud/aws/services/s3/

移行作業の内容

※Minio関連の既存リソースについては削除するだけなので割愛しています

1. docker-compose.yamlの変更

LocalStackの設定を追加しました。

localstack:
  image: localstack/localstack:3.0
  container_name: localstack
  ports:
    - "4566:4566"
  environment:
    - SERVICES=s3
    - AWS_DEFAULT_REGION=ap-northeast-1
    - DEBUG=1
    - PERSISTENCE=1
    - MAIN_CONTAINER_NAME=localstack
    - S3_SKIP_SIGNATURE_VALIDATION=0
  volumes:
    - ../../data/localstack:/var/lib/localstack
    - ./localstack/initialize:/etc/localstack/init/ready.d

各設定値については以下の様な意味合いです(公式ドキュメント)。

環境変数 説明
SERVICES LocalStackが起動時にロードするサービスを指定。デフォルトでは全サービスが有効ですが、必要なサービスのみ指定することで起動時間を短縮できるようです。
AWS_DEFAULT_REGION デフォルトのAWSリージョン。
DEBUG 1に設定すると詳細なログを出力します。
PERSISTENCE 1に設定するとデータの永続化が有効になり、コンテナ再起動後もデータが保持されます。
MAIN_CONTAINER_NAME Dockerコンテナ名を指定します。
S3_SKIP_SIGNATURE_VALIDATION S3の署名検証をスキップするかどうかのフラグ。デフォルトは1(スキップ)です。

volumesについては以下の通りです。

  • ../../data/localstack:/var/lib/localstack: 永続化データの保存先
  • ./localstack/initialize:/etc/localstack/init/ready.d: 初期化スクリプトをマウント

2. バケット作成の初期化スクリプト

LocalStackでは/etc/localstack/init/ready.d/配下にスクリプトを配置すると、LocalStack起動時に自動実行されます。
S3バケットの作成とCORS設定を行う初期化スクリプトを用意しました。

init.sh:

#!/bin/bash

echo "Creating S3 buckets..."

# バケット作成(ap-northeast-1リージョン)
awslocal s3api create-bucket \
  --bucket my-app-images-local \
  --create-bucket-configuration LocationConstraint=ap-northeast-1

# CORS設定
awslocal s3api put-bucket-cors \
  --bucket my-app-images-local \
  --cors-configuration file:///etc/localstack/init/ready.d/cors_config.json

# 他のバケットも同様に作成...

S3へのダイレクトアップロードなども利用しているためCORSを設定しています。ローカルでの動作確認用のためかなり緩めです。
cors_config.json:

{
  "CORSRules": [
    {
      "AllowedHeaders": ["*"],
      "AllowedMethods": ["GET", "POST", "PUT", "HEAD", "DELETE"],
      "AllowedOrigins": ["*"],
      "ExposeHeaders": ["ETag"],
      "MaxAgeSeconds": 3600
    }
  ]
}

3. 署名付きURLのエンドポイント問題

LocalStackで署名付きURLを使う場合は少し工夫が必要です。

アプリケーションはDockerコンテナ内で動作しているため、S3へのアクセスはhost.docker.internal:4566を使います。
しかし、署名付きURLはブラウザから直接アクセスするため、localhost:4566でないとアクセスできません。
そのため、S3クライアントと署名付きURL生成用のPresignClientで異なるエンドポイントを設定しています。
以下はコードのイメージです。

// 環境ごとの設定を管理する構造体
type clientConfig struct {
    endpoint          *string // S3クライアント用エンドポイント
    presignedEndpoint *string // 署名付きURL用エンドポイント
    credentials       aws.CredentialsProvider
    usePathStyle      bool
}

// 環境に応じた設定を返す
func buildClientConfig(isLocal bool) clientConfig {
    if isLocal {
        // LocalStack用の設定
        return clientConfig{
            endpoint:          aws.String("http://host.docker.internal:4566"),
            presignedEndpoint: aws.String("http://localhost:4566"),
            credentials:       credentials.NewStaticCredentialsProvider("test", "test", ""),
            usePathStyle:      true,
        }
    }
    // AWS環境用の設定(デフォルト)
    return clientConfig{
        endpoint:          nil,
        presignedEndpoint: nil,
        credentials:       nil,
        usePathStyle:      false,
    }
}

func NewClient(cfg config.Config) Client {
    clientCfg := buildClientConfig(cfg.IsLocal)

    // AWS SDK設定の読み込み
    awsCfg, _ := awsconfig.LoadDefaultConfig(context.Background(),
        awsconfig.WithRegion(cfg.Region),
    )

    // S3クライアント(コンテナ内通信用: host.docker.internal:4566)
    s3Client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
        if clientCfg.endpoint != nil {
            o.BaseEndpoint = clientCfg.endpoint
        }
        o.UsePathStyle = clientCfg.usePathStyle
    })

    // PresignClient(ブラウザからのアクセス用: localhost:4566)
    var presignOpts []func(*s3.PresignOptions)
    if clientCfg.presignedEndpoint != nil {
        presignOpts = append(presignOpts, func(po *s3.PresignOptions) {
            po.ClientOptions = append(po.ClientOptions, func(o *s3.Options) {
                o.BaseEndpoint = clientCfg.presignedEndpoint
                o.UsePathStyle = clientCfg.usePathStyle
            })
        })
    }

    return &client{
        s3Client:      s3Client,
        presignClient: s3.NewPresignClient(s3Client, presignOpts...),
    }
}

ポイントは以下の通りです。

  • s3Client: コンテナ内からLocalStackにアクセスするためhost.docker.internal:4566を使用
  • presignClient: 生成されたURLはブラウザから直接アクセスされるためlocalhost:4566を使用
  • usePathStyle: true: LocalStackはパススタイル(http://localhost:4566/bucket-name)でのアクセスが必要

まとめ

  • Minioの無料Dockerイメージ配布終了に伴い、LocalStackに乗り換えました
  • LocalStackでは初期化スクリプトでバケット作成やCORS設定が可能です

告知

Podcast 「RemiTalk」では普段の会社の様子を赤裸々に語っています。
もし良ければ聴いてみてください!

https://podcasts.apple.com/jp/podcast/remitalk/id1826516525

Podcast 文字起こしはこちら
https://note.com/remitaid

RemitAid Tech Blog
RemitAid Tech Blog

Discussion