🗂

AWS SDK for Go v2を使用したCloudflare R2へのファイルのアップロード方法

2024/09/27に公開

はじめに

Cloudflare R2はCloudflare社が提供するS3互換のオブジェクトストレージサービスです。
この記事では、aws-sdk-go-v2を使用してCloudflare R2にファイルをアップロードするAPIを作成する手順をステップバイステップで解説します。Webフレームワークはechoを採用しました。

前提条件

  • Goがインスール済み (バージョン1.21以上)
  • Cloudflare Accountを作成済み
  • GitHubのアカウントが作成済み

Cloudfare R2のセットアップ

以下の手順でR2のバケットを作成し、必要な情報を取得します。

1. R2バケットの作成

Cloudflareダッシュボードの左側にあるサイドバーのR2タブをクリックしてページ遷移します。
その後「R2サブスクリプションを開始する」ボタンをクリックするとことでR2バケットを作成できるようになります。
サブスリプション処理が正常に完了すると以下のようなページに遷移し、バケットに対する操作をダッシュボードから行えるようになります。

次にバケット名を指定してバケットを作成します。ここではr2-golang-demoというバケット名にしました。

バケットの作成に成功すると以下のようなバケットのダッシュボードに遷移します。

2. API Tokenの作成

Cloudflare R2を外部から操作するためにはAPI tokenを作成する必要があります。

https://developers.cloudflare.com/r2/api/s3/tokens/

R2のホーム画面の右側、アカウント詳細の下側に「R2 APIトークンの管理」というボタンがあるのでクリックしてAPIトークン管理画面に遷移します。

つぎにAPIトークンを作成するボタンをクリックします。
APIトークンを作成する画面では以下の内容を指定します。

  • トークン名: トークンの名前。ここではR2 Token for r2-golang-demoとしました。
  • 権限: トークンの権限タイプを指定します。今回は書き込み権限があれば良いのでオブジェクトの読み取りと書き込みを選択しました。
  • バケットの指定: 今回はr2-golang-demoバケットのみを操作できれば十分なため、r2-golang-demoのみを指定しました。
  • TTL: トークンのアクティブ期間を指定します。今回は無期限にしました。
  • クライアントIPアドレスフィルタリング: APIトークンを使用できるクライアントをIPアドレスで制限する場合はIPアドレスを入力します。今回は空白としました。

必要な情報の入力が終わったら「APIトークンを作成する」ボタンをクリックしてAPIトークンを作成します。

トークンが正常に作成されると以下の情報が表示されます。これらの情報は一度しか表示されないためどこかに保存しておきます。
ここでは1Passwordに保存しました。

  • トークンの値
  • アクセスキーID
  • シークレットアクセスキー
  • S3クライントのエンドポイント

Gitリポジトリのセットアップ

1. リモートリポジトリの作成

以下のドキュメントを参考にしてGitHubのWebから新しいリポジトリを作成します。
今回はリポジトリ名はr2-aws-go-v2-demoとしました。

https://docs.github.com/ja/repositories/creating-and-managing-repositories/creating-a-new-repository

2. ローカル環境にクローン

git clone git@github.com:kira924age/r2-aws-go-v2-demo.git

Goプロジェクトのセットアップ

以下の手順でGoでAPIを開発する上で必要な準備を行います。

1. ディレクトリの移動

クローンして作成されたプロジェクトのディレクトリに移動します。

cd r2-aws-go-v2-demo

2. go moduleの初期化

go mod init commandを使用してmoduleを初期化します。

go mod init github.com/kira924age/r2-aws-go-v2-demo

3. AWS SDK for Go v2のインストール

https://github.com/aws/aws-sdk-go-v2

go get github.com/aws/aws-sdk-go-v2/aws
go get github.com/aws/aws-sdk-go-v2/config
go get github.com/aws/aws-sdk-go-v2/service/s3

4. echoのインストール

https://github.com/labstack/echo

go get github.com/labstack/echo/v4
go get github.com/labstack/echo/v4/middleware

APIの開発

1. 仕様の決定

今回はmultipart/form-data形式のデータをリクエストボディを経由してファイルを受け取るようにしました。

Key Value description
file File R2にアップロードするファイル

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

POST /file

2. S3クライアントのセットアップ

aws-sdk-go-v2/configのLoadDefaultConfigメソッドを使用して認証情報を読み込ませます。
読み込み方法は環境変数、AWS shared configuration file (~/.aws/config)、AWS shared credentials file (~/.aws/credentials)の3つの方法があります。
今回は環境変数を利用して認証情報を渡すようにします。

https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config

このとき必要な環境変数は以下の通りです。

  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • AWS_DEFAULT_REGION

https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html

またR2のエンドポイントとバケット名の情報も必要なため、これらも環境変数経由で渡すようにします。
この情報の環境変数名は何でも良いです。ここではそれぞれAWS_S3_ENDPOINT、AWS_S3_BUCKET_NAMEとしました。

ローカルで開発する場合、環境変数をセットするスクリプトを用意しておくと便利です。
ここでは以下のシェルスクリプトを作成し、set_env.shというファイル名で保存しました。

#!/bin/bash

export AWS_ACCESS_KEY_ID="XXX"
export AWS_SECRET_ACCESS_KEY="XXX"
export AWS_DEFAULT_REGION="auto"
export AWS_S3_ENDPOINT="XXX"
export AWS_S3_BUCKET_NAME="r2-golang-demo"

AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY、AWS_ENDPOINTはそれぞれAPI Token作成時に表示された値を使用します。AWS_DEFAULT_REGIONはautoを指定します。

また、AWS_ACCESS_KEY_IDやAWS_SECRET_ACCESS_KEYは秘匿すべき情報であるため.gitignoreにset_env.shを追加しました。

config.LoadDefaultConfig()により環境変数から認証情報を読み込み、その情報をもとにs3クライントを作成するコードは例えば以下のように記述できます。

var client *s3.Client

func createS3Client() error {
	cfg, err := config.LoadDefaultConfig(context.TODO())
	if err != nil {
		return err
	}

	var s3endpoint string = os.Getenv("AWS_S3_ENDPOINT")

	client = s3.NewFromConfig(cfg, func(o *s3.Options) {
		o.BaseEndpoint = aws.String(s3endpoint)
	})

	return nil
}

3. ファイルアップロード関数の実装

aws-sdk-go-v2を利用してS3互換オブジェクトストレージにファイルをアップロードするにはPutObjectメソッドを使用します。

https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/s3#Client.PutObject

PutObjectのパラメータにはBucket名、Key、Body、ContentTypeを渡す必要があります。
ここではBucket名は環境変数から取得した値を固定値として利用し、ContentTypeはObjectKeyの拡張子をもとに決定するようにしています。

func uploadFile(objectKey string, r io.Reader) error {
	var bucketName string = os.Getenv("AWS_S3_BUCKET_NAME")

	var objectKeyParts []string = strings.Split(objectKey, ".")
	var ext string = "." + objectKeyParts[len(objectKeyParts)-1]
	var contentType string = mime.TypeByExtension(ext)

	_, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
		Bucket:      aws.String(bucketName),
		Key:         aws.String(objectKey),
		Body:        r,
		ContentType: aws.String(contentType),
	})

	return err
}

4. APIの実装

https://echo.labstack.com/docs/cookbook/file-upload

echoのFormFileを利用すると簡単にmultipart/form-dataを利用したファイルのやり取りを記述できます。

func postFileHandler(c echo.Context) error {
	file, err := c.FormFile("file")
	if err != nil {
		return err
	}

	src, err := file.Open()
	if err != nil {
		return err
	}
	defer src.Close()

	err = uploadFile(file.Filename, src)
	if err != nil {
		return err
	}

	return c.String(http.StatusOK, "File uploaded")
}

ここまでのコードを全て一つのmain.goで記述すると以下のようになります。

package main

import (
	"context"
	"io"
	"mime"
	"net/http"
	"os"
	"strings"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

var client *s3.Client

func createS3Client() error {
	cfg, err := config.LoadDefaultConfig(context.TODO())
	if err != nil {
		return err
	}

	var s3endpoint string = os.Getenv("AWS_S3_ENDPOINT")

	client = s3.NewFromConfig(cfg, func(o *s3.Options) {
		o.BaseEndpoint = aws.String(s3endpoint)
	})

	return nil
}

func uploadFile(objectKey string, r io.Reader) error {
	var bucketName string = os.Getenv("AWS_S3_BUCKET_NAME")

	var objectKeyParts []string = strings.Split(objectKey, ".")
	var ext string = "." + objectKeyParts[len(objectKeyParts)-1]
	var contentType string = mime.TypeByExtension(ext)

	_, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
		Bucket:      aws.String(bucketName),
		Key:         aws.String(objectKey),
		Body:        r,
		ContentType: aws.String(contentType),
	})

	return err
}

func main() {
	err := createS3Client()
	if err != nil {
		panic(err)
	}

	e := echo.New()

	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	e.POST("/file", postFileHandler)

	e.Logger.Fatal(e.Start(":1323"))
}

func postFileHandler(c echo.Context) error {
	file, err := c.FormFile("file")
	if err != nil {
		return err
	}

	src, err := file.Open()
	if err != nil {
		return err
	}
	defer src.Close()

	err = uploadFile(file.Filename, src)
	if err != nil {
		return err
	}

	return c.String(http.StatusOK, "File uploaded")
}

5. 動作確認

ローカル環境で実行し、きちんとファイルがアップロードできているかどうかの動作確認を行います。
環境変数設定スクリプトを実行し、開発サーバーをgo runコマンドで起動します。

$ source set_env.sh && go run main.go

   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.12.0
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\
⇨ http server started on [::]:1323

エンドポイントはPOST /file。リクエスト形式はmultipart/form-data。KeyはfileとしてAPIを実装したので、例えばPostmanを利用すると以下のようにしてAPIを呼び出すことができます。

おわりに

この記事では、Cloudflare R2を利用しaws-sdk-go-v2とEchoフレームワークを用いてGoでファイルアップロードAPIを構築する方法を解説しました。

なお、今回作成したコードは以下のGitHubリポジトリより閲覧可能です。

https://github.com/kira924age/r2-aws-go-v2-demo

参考文献

Urth

Discussion