MinIO on Docker Compose を試してみた
ストレージとしてS3を選択した際のローカル開発環境のストレージをどうするか?の一つの解決手段として、MinIO on Docker Composeを試してみた。
- LocalStackを使う
 - 実際にAWS上のS3にアクセスする
 
等の手段もあると思うが、Goで実装されていると聞いて単純に使ってみたくなったという経緯である。
環境
- macOS Monterey 12.3.1
 - Docker 20.10.10
 - Docker Compose 2.1.1
 - go1.18
 
コンテナ起動
https://raw.githubusercontent.com/minio/minio/master/docs/orchestration/docker-compose/docker-compose.yaml を参考にしてdocker-compose.ymlにMinIOの定義をする。
version: '3.9'
services:
  minio:
    image: quay.io/minio/minio:latest
    container_name: example-minio
    environment:
      MINIO_ROOT_USER: root
      MINIO_ROOT_PASSWORD: password
    command: server --console-address ":9001" /data
    ports:
      - 9000:9000
      - 9001:9001
commandではコンソールのエンドポイントを設定している。
MINIO_ROOT_USERとMINIO_ROOT_PASSWORDはログイン時、API実行時に使う。
起動してコンソールにアクセスしてみる。
$ docker compose up -d
localhost:9001にアクセスするとログイン画面が表示される。

docker-compose.ymlにて定義したMINIO_ROOT_USERとMINIO_ROOT_PASSWORDをそれぞれ入力してログインする。

シンプルで良い感じのUIだ。
バケット作成
aws-sdk-goのS3のAPIを使ってバケットを作成してみる。
せっかくなので、指定のバケットが既に存在していれば一度削除して再作成するプログラムとする。
package main
import (
	"fmt"
	"os"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
)
func main() {
	sess := session.Must(
		session.NewSession(
			&aws.Config{
				Credentials:      credentials.NewStaticCredentials("root", "password", ""),
				Endpoint:         aws.String("http://localhost:9000"),
				Region:           aws.String("ap-northeast-1"),
				S3ForcePathStyle: aws.Bool(true),
			}))
	svc := s3.New(sess)
	bucket := "example"
	exists, err := existsBucket(svc, bucket)
	if err != nil {
		fmt.Printf("failed to exists bucket: %s\n", err)
		os.Exit(1)
	}
	if exists {
		if err := deleteBucket(svc, bucket); err != nil {
			fmt.Printf("failed to delete bucket: %s\n", err)
			os.Exit(1)
		}
	}
	if err := createBucket(svc, bucket); err != nil {
		fmt.Printf("failed to create bucket: %s\n", err)
		os.Exit(1)
	}
}
func existsBucket(svc *s3.S3, bucket string) (bool, error) {
	_, err := svc.HeadBucket(&s3.HeadBucketInput{
		Bucket: aws.String(bucket),
	})
	if err != nil {
		if aerr, ok := err.(awserr.Error); ok {
			switch aerr.Code() {
			case "NotFound":
				return false, nil
			default:
				return false, err
			}
		} else {
			return false, err
		}
	}
	return true, nil
}
func deleteBucket(svc *s3.S3, bucket string) error {
	_, err := svc.DeleteBucket(&s3.DeleteBucketInput{
		Bucket: aws.String(bucket),
	})
	return err
}
func createBucket(svc *s3.S3, bucket string) error {
	_, err := svc.CreateBucket(&s3.CreateBucketInput{
		Bucket: aws.String(bucket),
	})
	return err
}
少しだけコードの内容を解説する。
	sess := session.Must(
		session.NewSession(
			&aws.Config{
				Credentials:      credentials.NewStaticCredentials("root", "password", ""),
				Endpoint:         aws.String("http://localhost:9000"),
				Region:           aws.String("ap-northeast-1"),
				S3ForcePathStyle: aws.Bool(true),
			}))
	svc := s3.New(sess)
credentials.NewStaticCredentialsの第一引数がアクセスキーID、第二引数がシークレットアクセスキーとなるが、ここはdocker-compose.ymlにて定義したMINIO_ROOT_USERとMINIO_ROOT_PASSWORDを渡す。
Regionはいくつか適当に試してみたところ、別になんでもいいっぽいけど指定がないとエラーになる。
S3ForcePathStyleはtrueにしておかないと、http://{バケット名}.localhost:9000みたいなエンドポイントにアクセスしにいってしまう。
	_, err := svc.HeadBucket(&s3.HeadBucketInput{
		Bucket: aws.String(bucket),
	})
	if err != nil {
		if aerr, ok := err.(awserr.Error); ok {
			switch aerr.Code() {
			case "NotFound":
				return false, nil
			default:
				return false, err
			}
		} else {
			return false, err
		}
	}
	return true, nil
MinIOは関係ない?けどHeadBucket実行時にバケットがないと、
&s3err.RequestFailure{
  RequestFailure: &awserr.requestError{
    awsError: &awserr.baseError{
      code:    "NotFound",
      message: "Not Found",
      errs:    []error{},
    },
    statusCode: 404,
    requestID:  "16ED174B7BB404D8",
    bytes:      []uint8{},
  },
  hostID: "",
}
みたいなエラーが返ってくる。
aws-sdk-goのドキュメントのExampleが
svc := s3.New(session.New())
input := &s3.HeadBucketInput{
    Bucket: aws.String("acl1"),
}
result, err := svc.HeadBucket(input)
if err != nil {
    if aerr, ok := err.(awserr.Error); ok {
        switch aerr.Code() {
        case s3.ErrCodeNoSuchBucket:
            fmt.Println(s3.ErrCodeNoSuchBucket, aerr.Error())
        default:
            fmt.Println(aerr.Error())
        }
    } else {
        // Print the error, cast err to awserr.Error to get the Code and
        // Message from an error.
        fmt.Println(err.Error())
    }
    return
}
fmt.Println(result)
になっていたので、最初はcase "NotFound":の部分をcase s3.ErrCodeNoSuchBucketで実装していた。
バケットがない時に全部defaultに入ってしまって上手くいかなかったので、プリントデバッグしてエラーの実体を確認しcase "NotFound":に書き換えた。
(NotFoundは定数化されていなかった。。。)
プログラムを実行してみる。
$ go run main.go
無事バケットができていた。

まとめ
ドキュメントがしっかりしていてあまり躓くことなく試せた。
これから個人開発で使っていってもいいなと思った。
Discussion