🙌

aws-sdk-go v2を使ってLocalstackのs3, DynamoDBに接続する

2023/05/19に公開1

はじめに

testcontainers-goを利用してlocalstackのコンテナを起動してs3やDynamoDBにデータを保存や取得をするテストを書いています。

先日、ローカル環境ではテスト通るのですが、Github Actionsで実行すると動いてくれない事象が発生してハマったので記事にしておきます。

localstackのコンテナを起動する

func setupDynamoDBTestContainer(t *testing.T) {
	t.Helper()
	ctx := context.Background()
	// Container start
	initScriptPath, err := filepath.Abs("../test/localstack")
	require.NoError(t, err)
	l, err := localstack.StartContainer(ctx,
		localstack.OverrideContainerRequest(testcontainers.ContainerRequest{
			Name: "testcontainers-localstack",
			Env: map[string]string{
				"SERVICES": "dynamodb",
			},
			Mounts: testcontainers.ContainerMounts{
				testcontainers.BindMount(initScriptPath, "/etc/localstack/init/ready.d"),
			},
			WaitingFor: wait.ForLog("AWS dynamodb.CreateTable => 200").
				WithPollInterval(5 * time.Second).
				WithStartupTimeout(180 * time.Second),
		}))
	require.NoError(t, err)
	t.Cleanup(func() {
		require.NoError(t, l.Terminate(ctx))
	})
	// Get LocalStack Container Host
	host, err := l.Host(ctx)
	require.NoError(t, err)
	// Get the mapped port for LocalStack
	port, err := l.MappedPort(ctx, "4566/tcp")
	require.NoError(t, err)
	awsEndpoint := fmt.Sprintf("http://%s:%d", host, port.Int()) //nolint:nosprintfhostport
	awsRegion := "ap-northeast-1"
	t.Setenv("AWS_DYNAMODB_ENDPOINT", awsEndpoint)
	t.Setenv("AWS_DYNAMODB_REGION", awsRegion)
}

../test/localstack ここにはDynamoDBのテーブル作成するためのスクリプトを配置しています。

init_dynamodb.sh
awslocal dynamodb create-table --table-name Test \
    --attribute-definitions \
        AttributeName=MessageID,AttributeType=S \
    --key-schema \
        AttributeName=MessageID,KeyType=HASH \
    --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1

起動時にエンドポイントを取得して次の認証時に利用します。

AWS認証の方法

以下のように LoadDefaultConfig を利用して認証情報を取得することができます。

	// AWS認証情報を取得する
	customResolver := aws.EndpointResolverFunc(func(service, region string) (aws.Endpoint, error) {
		if awsEndpoint != "" {
			return aws.Endpoint{
				PartitionID:   "aws",
				URL:           awsEndpoint,
				SigningRegion: awsRegion,
			}, nil
		}
		// returning EndpointNotFoundError will allow the service to fallback to its default resolution
		return aws.Endpoint{}, &aws.EndpointNotFoundError{}
	})
	cfg, err := config.LoadDefaultConfig(context.TODO(),
		config.WithRegion(awsRegion),
		config.WithEndpointResolver(customResolver),
	)
	if err != nil {
		log.Fatalf("Failed to load AWS config: %v", err)
	}
	// DynamoDBサービスクライアントの初期化
	svc := dynamodb.NewFromConfig(cfg)

localstackで実行させるときには awsEndpoint にlocalstackのエンドポイントを設定します。
上記のような customResolver を用意しておくと、例えばローカルで実行するときには以下のような環境変数を設定しておきAWS上で実行するときにはこれらの環境変数を設定しないようにすれば認証されたAWSアカウント上のサービスに接続するといった切り替えができます。

# DynamoDB
AWS_DYNAMODB_ENDPOINT=http://localhost:4566
AWS_DYNAMODB_REGION=ap-northeast-1

https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/#specifying-credentials

ハマったところ

上記のような設定でlocalstackに接続に行くはずなのですが、Github Actionsで実行すると以下のようなエラーが発生してしまいました。

SDK 2023/05/18 01:04:45 DEBUG Request
PUT /latest/api/token HTTP/1.1
Host: 169.254.169.254
User-Agent: aws-sdk-go-v2/1.18.0 os/linux lang/go/1.20.4 md/GOOS/linux md/GOARCH/amd64 ft/ec2-imds
Content-Length: 0
Amz-Sdk-Request: attempt=1; max=3
X-Aws-Ec2-Metadata-Token-Ttl-Seconds: 300
Accept-Encoding: gzip

SDK 2023/05/18 01:04:45 DEBUG Response
HTTP/1.1 400 Bad Request
Content-Length: 322
Content-Type: text/xml; charset=utf-8
Date: Thu, 18 May 2023 01:04:44 GMT
Server: Microsoft-IIS/10.0

<?xml version="1.0" encoding="utf-8"?>
<Error xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <Code>InvalidHttpVerb</Code>
    <Message>The HTTP verb specified was not recognized by the server.</Message>
    <Details>'PUT' is not a supported verb.</Details>
</Error>
2023/05/18 01:04:45 Failed to put item: operation error DynamoDB: PutItem, failed to sign request: failed to retrieve credentials: failed to refresh cached credentials, no EC2 IMDS role found, operation error ec2imds: GetMetadata, failed to get API token, operation error ec2imds: getToken, http response error StatusCode: 400, request to EC2 IMDS failed

aws-sdkのログは以下のような設定をデバッグのために追加して出したものです。

config.WithClientLogMode(aws.LogRequestWithBody|aws.LogResponseWithBody),

customResolver周りを疑って色々と試行錯誤をしていたのですが、結局原因はAWSの認証情報が設定されていなかったことでした。

解決策

ローカルでは .aws/credentials に認証情報を設定していたのですが、Github Actionsではこれが設定されていないためにエラーが発生していました。

Github Actionsで実行するときには以下のように実行するようにしました。

      - name: Run tests
        env:
          AWS_ACCESS_KEY_ID: "dummy"
          AWS_SECRET_ACCESS_KEY: "dummy"
        run: |
          go test -short -race ./...

localstackのサービスに接続するだけなのでdummyの値は何でも良いです。

まとめ

わかってみればなんともないことでした。
ただエラーの内容から原因の想像がなかなかつかず、ローカルとGithub Actionsで実行されているコンテナ環境の違いは何かを考えていった結果としてこの解決策にたどり着くことができました。

おわり。

GitHubで編集を提案

Discussion

u1u1

ユニットテストでは常にダミーのCredential情報で良いのでテストコードの中で以下のような記述をしても良いですね。

	// Set Dummy AWS Credential for test
	t.Setenv("AWS_ACCESS_KEY_ID", "dummy")
	t.Setenv("AWS_SECRET_ACCESS_KEY", "dummy")