MinIOによるローカルS3環境の構築
はじめに
S3互換ストレージであるMinIOを使用して、ローカル環境でS3の開発環境を構築する方法を解説します。
S3(MinIO)への接続はAWS SDK for Go V2を使用します。MinIO SDKは使用しません。
MinIOとは
MinIOは、オープンソースのS3互換オブジェクトストレージサーバーです。
AWS S3と互換性のあるAPIを提供しており、開発環境やテスト環境での利用に適しています。
利点として以下があります:
- AWS S3と同じAPIインターフェースを使用可能
- ローカル環境で完結するため、費用がかからない
- Dockerで簡単に環境構築が可能
- 本番環境との切り替えが容易
別サービスとして、S3だけでなく様々なAWSサービスをエミュレーション可能なlocalstackがあります。
localstackは、コンテナを再起動するとアップロードしていたファイルが消えてしまいます。有料化することで永続化できますが、現状ではコスト面から採用しませんでした。
環境構築
動作環境
- Go 1.22.5
- Docker
- Docker Compose
プロジェクトのセットアップ
まずは新しいプロジェクトディレクトリを作成し、必要なパッケージをインストールします。
mkdir minio-golang-demo
cd minio-golang-demo
go mod init minio-golang-demo
AWS SDK for Go V2の必要なパッケージをインストールします:
go get github.com/aws/aws-sdk-go-v2
go get github.com/aws/aws-sdk-go-v2/config
go get github.com/aws/aws-sdk-go-v2/service/s3
go get github.com/aws/aws-sdk-go-v2/credentials
MinIOのセットアップ
Docker Composeを使ってMinIOを起動します。以下の内容でdocker-compose.yml
を作成します:
version: '3'
services:
minio:
image: minio/minio
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
command: server /data --console-address ":9001"
volumes:
- minio_data:/data
volumes:
minio_data:
※ MINIO_ROOT_USER
、MINIO_ROOT_PASSWORD
は任意の値ですが、8文字以上である必要があります。
MinIOを起動します:
docker-compose up -d
GoコードでS3を操作する
基本的な接続設定
まず、main.go
を作成し、MinIOへの接続設定を実装します。
接続方法はAWS SDK for Go V2で接続します。
公式サイトにサンプルがあるのでそちらをベースに実装します。
公式のサンプルコード
package main
import (
"context"
"errors"
"fmt"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/smithy-go"
)
// main uses the AWS SDK for Go V2 to create an Amazon Simple Storage Service
// (Amazon S3) client and list up to 10 buckets in your account.
// This example uses the default settings specified in your shared credentials
// and config files.
func main() {
ctx := context.Background()
sdkConfig, err := config.LoadDefaultConfig(ctx)
if err != nil {
fmt.Println("Couldn't load default configuration. Have you set up your AWS account?")
fmt.Println(err)
return
}
s3Client := s3.NewFromConfig(sdkConfig)
count := 10
fmt.Printf("Let's list up to %v buckets for your account.\n", count)
result, err := s3Client.ListBuckets(ctx, &s3.ListBucketsInput{})
if err != nil {
var ae smithy.APIError
if errors.As(err, &ae) && ae.ErrorCode() == "AccessDenied" {
fmt.Println("You don't have permission to list buckets for this account.")
} else {
fmt.Printf("Couldn't list buckets for your account. Here's why: %v\n", err)
}
return
}
if len(result.Buckets) == 0 {
fmt.Println("You don't have any buckets!")
} else {
if count > len(result.Buckets) {
count = len(result.Buckets)
}
for _, bucket := range result.Buckets[:count] {
fmt.Printf("\t%v\n", *bucket.Name)
}
}
}
このままですと、AWSの方に接続しようとするため、クライアント生成部分を変更します。
creds := credentials.NewStaticCredentialsProvider("minioadmin", "minioadmin", "")
sdkConfig, err = config.LoadDefaultConfig(context.Background(),
config.WithRegion("us-east-1"),
config.WithBaseEndpoint("http://localhost:9000"),
config.WithCredentialsProvider(creds),
)
s3Client := s3.NewFromConfig(sdkConfig, func(o *s3.Options) {
o.UsePathStyle = true
})
- creds: MinIO接続用のクレデンシャル(dockerコンテナに指定した
MINIO_ROOT_USER
,MINIO_ROOT_PASSWORD
をそれぞれ指定します。 - config.WithRegion:リージョンは
us-east-1
固定 - config.WithBaseEndpoint:エンドポイント。MinIOのエンドポイントを指定
- config.WithCredentialsProvider(creds):MinIO用のクレデンシャルを指定
- s3Client:
UsePathStyle
のオプションをtrue
にします。これはS3のエンドポイントの構成を変更するもので、デフォルトのままですとエンドポイントが、http://{バケット名}.localhost:9000
となってしまいます。UsePathStyle
をtrue
にすることで、http://localhost:9000/{バケット名}
となります。
実際に使ってみる
上記の接続情報を用いて、実際にS3の操作してみましょう:
実装
main.go
と同じディレクトリに、bucketBasics.go
として
「基本を学ぶ」のBucketBasics
のコードをコピー
packageの指定だけ追加しました。ロジックは特に修正していません。
S3操作部分
package main
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"log"
"os"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/aws/smithy-go"
)
// BucketBasics encapsulates the Amazon Simple Storage Service (Amazon S3) actions
// used in the examples.
// It contains S3Client, an Amazon S3 service client that is used to perform bucket
// and object actions.
type BucketBasics struct {
S3Client *s3.Client
}
・・・略・・・
S3のデモ呼び出し部分
main.go
に処理を追加します。
-
main
メソッドの下に「基本を学ぶ」のRunGetStartedScenario
をコピーします。 -
main
メソッドからRunGetStartedScenario
を呼び出します。
func main() {
// 前述の接続設定コード...
RunGetStartedScenario(ctx, sdkConfig, demotools.NewQuestioner())
}
// RunGetStartedScenario is an interactive example that shows you how to use Amazon
// Simple Storage Service (Amazon S3) to create an S3 bucket and use it to store objects.
//
// 1. Create a bucket.
// 2. Upload a local file to the bucket.
// 3. Download an object to a local file.
// 4. Copy an object to a different folder in the bucket.
// 5. List objects in the bucket.
// 6. Delete all objects in the bucket.
// 7. Delete the bucket.
//
// This example creates an Amazon S3 service client from the specified sdkConfig so that
// you can replace it with a mocked or stubbed config for unit testing.
//
// It uses a questioner from the `demotools` package to get input during the example.
// This package can be found in the ..\..\demotools folder of this repo.
func RunGetStartedScenario(ctx context.Context, sdkConfig aws.Config, questioner demotools.IQuestioner) {
defer func() {
if r := recover(); r != nil {
log.Println("Something went wrong with the demo.")
_, isMock := questioner.(*demotools.MockQuestioner)
if isMock || questioner.AskBool("Do you want to see the full error message (y/n)?", "y") {
log.Println(r)
}
}
}()
log.Println(strings.Repeat("-", 88))
log.Println("Welcome to the Amazon S3 getting started demo.")
log.Println(strings.Repeat("-", 88))
// s3Client := s3.NewFromConfig(sdkConfig)
s3Client := s3.NewFromConfig(sdkConfig, func(o *s3.Options) {
o.UsePathStyle = true
})
bucketBasics := actions.BucketBasics{S3Client: s3Client}
count := 10
log.Printf("Let's list up to %v buckets for your account:", count)
buckets, err := bucketBasics.ListBuckets(ctx)
if err != nil {
panic(err)
}
if len(buckets) == 0 {
log.Println("You don't have any buckets!")
} else {
if count > len(buckets) {
count = len(buckets)
}
for _, bucket := range buckets[:count] {
log.Printf("\t%v\n", *bucket.Name)
}
}
bucketName := questioner.Ask("Let's create a bucket. Enter a name for your bucket:",
demotools.NotEmpty{})
bucketExists, err := bucketBasics.BucketExists(ctx, bucketName)
if err != nil {
panic(err)
}
if !bucketExists {
err = bucketBasics.CreateBucket(ctx, bucketName, sdkConfig.Region)
if err != nil {
panic(err)
} else {
log.Println("Bucket created.")
}
}
log.Println(strings.Repeat("-", 88))
fmt.Println("Let's upload a file to your bucket.")
smallFile := questioner.Ask("Enter the path to a file you want to upload:",
demotools.NotEmpty{})
const smallKey = "doc-example-key"
err = bucketBasics.UploadFile(ctx, bucketName, smallKey, smallFile)
if err != nil {
panic(err)
}
log.Printf("Uploaded %v as %v.\n", smallFile, smallKey)
log.Println(strings.Repeat("-", 88))
log.Printf("Let's download %v to a file.", smallKey)
downloadFileName := questioner.Ask("Enter a name for the downloaded file:", demotools.NotEmpty{})
err = bucketBasics.DownloadFile(ctx, bucketName, smallKey, downloadFileName)
if err != nil {
panic(err)
}
log.Printf("File %v downloaded.", downloadFileName)
log.Println(strings.Repeat("-", 88))
log.Printf("Let's copy %v to a folder in the same bucket.", smallKey)
folderName := questioner.Ask("Enter a folder name: ", demotools.NotEmpty{})
err = bucketBasics.CopyToFolder(ctx, bucketName, smallKey, folderName)
if err != nil {
panic(err)
}
log.Printf("Copied %v to %v/%v.\n", smallKey, folderName, smallKey)
log.Println(strings.Repeat("-", 88))
log.Println("Let's list the objects in your bucket.")
questioner.Ask("Press Enter when you're ready.")
objects, err := bucketBasics.ListObjects(ctx, bucketName)
if err != nil {
panic(err)
}
log.Printf("Found %v objects.\n", len(objects))
var objKeys []string
for _, object := range objects {
objKeys = append(objKeys, *object.Key)
log.Printf("\t%v\n", *object.Key)
}
log.Println(strings.Repeat("-", 88))
if questioner.AskBool("Do you want to delete your bucket and all of its "+
"contents? (y/n)", "y") {
log.Println("Deleting objects.")
err = bucketBasics.DeleteObjects(ctx, bucketName, objKeys)
if err != nil {
panic(err)
}
log.Println("Deleting bucket.")
err = bucketBasics.DeleteBucket(ctx, bucketName)
if err != nil {
panic(err)
}
log.Printf("Deleting downloaded file %v.\n", downloadFileName)
err = os.Remove(downloadFileName)
if err != nil {
panic(err)
}
} else {
log.Println("Okay. Don't forget to delete objects from your bucket to avoid charges.")
}
log.Println(strings.Repeat("-", 88))
log.Println("Thanks for watching!")
log.Println(strings.Repeat("-", 88))
}
S3のクライアント生成部分だけ以下のように修正します。
s3Client := s3.NewFromConfig(sdkConfig, func(o *s3.Options) {
o.UsePathStyle = true
})
パッケージが不足しているので、インストールします。
go mod tidy
実行
実行してみます。
go run main.go
サンプルコードを実行すると、対話形式で入力を求められます。
バケットの作成
Let's list up to 10 buckets for your account.
You don't have any buckets!
2025/02/09 17:50:01 ----------------------------------------------------------------------------------------
2025/02/09 17:50:01 Welcome to the Amazon S3 getting started demo.
2025/02/09 17:50:01 ----------------------------------------------------------------------------------------
2025/02/09 17:50:01 Let's list up to 10 buckets for your account:
2025/02/09 17:50:01 You don't have any buckets!
Let's create a bucket. Enter a name for your bucket:
test-bucket
2025/02/09 17:50:11 Bucket test-bucket is available.
2025/02/09 17:50:11 Bucket created.
2025/02/09 17:50:11 ----------------------------------------------------------------------------------------
バケットを作るためにバケット名を入力します。
test-bucket
としました。
ファイルアップロード
ローカルにあるファイルのパスを指定します。
Let's upload a file to your bucket.
Enter the path to a file you want to upload:
/Users/myuser/Downloads/sample.csv
2025/02/09 17:51:27 Uploaded /Users/myuser/Downloads/sample.csv as doc-example-key.
2025/02/09 17:51:27 ----------------------------------------------------------------------------------------
アップロードされたファイルはdoc-example-key
というキー名としてアップロードされます。サンプルソースで固定で指定されているので、変更は可能。
URL的には以下
ファイルダウンロード
ダウンロードする際のファイル名を指定します。
ダウンロード先はmain.goのディレクトリです。
2025/02/09 17:51:27 Let's download doc-example-key to a file.
Enter a name for the downloaded file:
dl-sample.csv
2025/02/09 17:51:45 File dl-sample.csv downloaded.
2025/02/09 17:51:45 ----------------------------------------------------------------------------------------
オブジェクトのコピー
先ほど作成したキーをコピーします。
コピー先のkeyを指定します。copy-key
としました。
2025/02/09 17:51:45 Let's copy doc-example-key to a folder in the same bucket.
Enter a folder name:
copy-key
2025/02/09 17:52:07 Copied doc-example-key to copy-key/doc-example-key.
2025/02/09 17:52:07 ----------------------------------------------------------------------------------------
URL的には以下
バケット内のオブジェクトリスト取得
2025/02/09 17:52:07 Let's list the objects in your bucket.
Press Enter when you're ready.
2025/02/09 17:52:17 Found 2 objects.
2025/02/09 17:52:17 copy-key/doc-example-key
2025/02/09 17:52:17 doc-example-key
2025/02/09 17:52:17 ----------------------------------------------------------------------------------------
バケットやファイルの削除確認
すべて削除するかを確認されます。
内容を確認したいのでn
にします。
Do you want to delete your bucket and all of its contents? (y/n)
n
2025/02/09 17:52:23 Okay. Don't forget to delete objects from your bucket to avoid charges.
2025/02/09 17:52:23 ----------------------------------------------------------------------------------------
2025/02/09 17:52:23 Thanks for watching!
2025/02/09 17:52:23 ----------------------------------------------------------------------------------------
GUIで確認
実際にバケットやファイルが作成されているか確認してみましょう。
ログイン
以下ページにアクセスします。
username、passwordは、dockerコンテナに指定したMINIO_ROOT_USER
, MINIO_ROOT_PASSWORD
をそれぞれ指定します。
バケット一覧
先ほど作成したバケットが確認できます。
クリックでバケットの中身が確認できます。
オブジェクト一覧
無事確認ができました。
作成されたバケットやファイルの実体
docker-compose.yaml
で指定したvolumeに作成されます。
ディレクトリやファイルを直接操作するのではなく、GUIやプログラムから操作するようにしましょう。
一応コピーやリネームは動作はしました。
ローカル環境と本番環境との接続切り替え
エンドポイントの指定があるかどうかで判断するのが良いと思います。
環境変数等にローカル接続用のURLを設定
S3_ENDPOINT=http://localhost:9000
環境変数の値有無によって分岐
import (
"context"
"os"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
type s3Service struct {
Client *s3.Client
}
func NewS3Service() S3Service {
region := os.Getenv("REGION")
var cfg aws.Config
var err error
if endpoint := os.Getenv("S3_ENDPOINT"); endpoint != "" {
creds := credentials.NewStaticCredentialsProvider("minioadmin", "minioadmin", "")
cfg, err = config.LoadDefaultConfig(context.Background(),
config.WithRegion(region),
config.WithBaseEndpoint(endpoint),
config.WithCredentialsProvider(creds),
)
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
o.UsePathStyle = true
})
return &s3Service{Client: client}
}
cfg, err = config.LoadDefaultConfig(context.Background(),
config.WithRegion(region),
)
if err != nil {
panic("unable to load SDK config, " + err.Error())
}
client := s3.NewFromConfig(cfg)
return &s3Service{Client: client}
}
この実装によって、エンドポイントの環境変数自体をなくすか、値を空にしておくことで、S3に対して接続を試みます。
まとめ
今回の記事では、以下の内容を解説しました。
- MinIOのDockerを使ったセットアップ方法
- AWS SDK for Go V2を使ったMinIOへの接続設定
- 基本的なS3操作(バケット作成、オブジェクトのアップロード/ダウンロード)の実装
MinIOを使うことで、ローカル環境でS3の動作確認が簡単にできます。
本番環境へ移行する際も、エンドポイントの設定を変更するだけで、ほぼ同じコードが使えるのが大きな利点です。
発展的な使い方
- プレサインドURL生成
- マルチパートアップロード
- バケットポリシーの設定
- バージョニング機能の利用
これらの機能も、AWS SDK for Go V2で同様に実装できます。必要に応じて、公式ドキュメントを参照してください。
Discussion