DynamoDB LocalとGoを用いた実装と、CircleCIでのCIの設定方法
本記事ではDynanmoDB Localを用いてローカルでのGoを用いた開発をする方法と、CircleCIを用いてCIを実施する方法を示します。
DynamoDB Localのセットアップ
まずDynamoDBにはDynamoDB Localと言われるエミュレーターがあるので、ローカルでの開発はDynamoDB Localを利用しましょう。
aws_access_key_id
や aws_secret_access_key
はダミーでOKです。何かしらの値は設定する必要があります。
# AWS profile(初回のみ) aws configure set aws_access_key_id dummy --profile local aws configure set aws_secret_access_key dummy --profile local aws configure set region ap-northeast-1 --profile local # DynamoDB Localの初回セットアップ docker run -d --name dynamodb -p 8000:8000 amazon/dynamodb-local:1.13.1 -jar DynamoDBLocal.jar -sharedDb
- 停止するとき
docker stop dynamodb
ローカルにコンテナが起動したことがわかります。
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 76d708f8199c amazon/dynamodb-local:1.13.1 "java -jar DynamoDBL…" 3 hours ago Up 3 hours 0.0.0.0:8000->8000/tcp dynamodb
ユーザの情報を管理する user
テーブルを用いることにします。DynamoDB Localを用いるときは、prefixに local
を付与します。テーブル定義はこんな感じ。
{ "TableName": "local_user", "KeySchema": [ { "AttributeName": "user_id", "KeyType": "HASH" } ], "AttributeDefinitions": [ { "AttributeName": "user_id", "AttributeType": "S" } ], "ProvisionedThroughput": { "ReadCapacityUnits": 1, "WriteCapacityUnits": 1 } }
以下のようなアイテムを投入することにします。
{ "user_id": { "S": "001" }, "user_name": { "S": "gopher_1" } }
DynamoDB Localを用いたGoのテストの実装
Goの実装です。local_user
テーブルは環境変数で定義しておきます。DynamoDB Localを使わない場合は user
などというテーブル名を環境変数に定義します。
- db.go
import ( "log" "os" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/dynamodb" ) var ( db *dynamodb.DynamoDB userTable string ) func init() { dbEndpoint := os.Getenv("DYNAMO_ENDPOINT") region := os.Getenv("AWS_REGION") userTable = os.Getenv("DYNAMO_TABLE_USER") if userTable == "" { log.Fatal(`env variable "DYNAMO_TABLE_USER" is required`) } sess := session.Must(session.NewSession(&aws.Config{ Endpoint: aws.String(dbEndpoint), Region: aws.String(region), })) db = dynamodb.New(sess) }
DynamoDBからユーザ情報をフェッチするコードはこんな感じです。dynamodbav
というタグをstructのフィールドに付与することで、DynamoDBのキーを指定して構造体とマッピングすることができます。
- user.go
import ( "context" "fmt" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" ) type User struct { UserID string `dynamodbav:"user_id"` UserName string `dynamodbav:"user_name"` } func FetchUserByID(ctx context.Context, userID string) (*User, error) { q := &dynamodb.GetItemInput{ TableName: aws.String(userTable), Key: map[string]*dynamodb.AttributeValue{ "user_id": { S: aws.String(userID), }, }, ConsistentRead: aws.Bool(true), } out, err := db.GetItemWithContext(ctx, q) if err != nil { return nil, fmt.Errorf("dynamodb fetch user: %w", err) } var user *User if err := dynamodbattribute.UnmarshalMap(out.Item, &user); err != nil { return nil, fmt.Errorf("dynamodb unmarshal: %w", err) } return user, nil }
上記のコードをテストして確認してみましょう。DynamoDB Localにテーブルを作成するコマンドやアイテムを投入するコマンドはexec.Command
を用いてAWS CLIコマンドをGoから実行することにします。なのでローカルの開発マシンからAWS CLIが実行できる必要があります。
- user_test.go
import ( "context" "os/exec" "reflect" "strings" "testing" ) func setupDB(t *testing.T) { cmds := []string{ `aws dynamodb --endpoint-url http://localhost:8000 create-table --cli-input-json file://./testdata/local_user.json`, `aws dynamodb put-item --endpoint-url http://localhost:8000 --table-name local_user --item file://./testdata/input_user.json`, } for _, cmd := range cmds { args := strings.Split(cmd, " ") if err := exec.Command(args[0], args[1:]...).Run(); err != nil { t.Logf("setup DynamoDB %v %s", err, cmd) } } } func teardownDB(t *testing.T) { cmds := []string{ `aws dynamodb --endpoint-url http://localhost:8000 delete-table --table local_user`, } for _, cmd := range cmds { args := strings.Split(cmd, " ") if err := exec.Command(args[0], args[1:]...).Run(); err != nil { t.Logf("teardown DynamoDB %v %s", err, cmd) } } } func TestFetchUserByID(t *testing.T) { setupDB(t) t.Cleanup(func() { teardownDB(t) }) tests := []struct { name string userID string want *User wantErr bool }{ { name: "normal", userID: "001", want: &User{ UserID: "001", UserName: "gopher_1", }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := FetchUserByID(context.TODO(), tt.userID) if (err != nil) != tt.wantErr { t.Errorf("FetchUserByID() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("FetchUserByID() got = %v, want %v", got, tt.want) } }) } }
さあ、テストを実行してみます。
go test -v === RUN TestFetchUserByID === RUN TestFetchUserByID/normal --- PASS: TestFetchUserByID (6.01s) --- PASS: TestFetchUserByID/normal (0.01s) PASS ok github.com/d-tsuji/sample-circleci 6.231s
すばらしいですね。DynamoDB Localを用いてテストすることができました。
Circle CIの設定
天下り的に config.yml
を示します。この設定でCircleCI上でDynamoDB Localを用いたCIを実施することができます。
version: 2 jobs: test: docker: # CircleCI Go images available at: https://hub.docker.com/r/circleci/golang/ - image: circleci/golang:1.14.4 - image: amazon/dynamodb-local working_directory: /go/src/github.com/d-tsuji/sample-circleci # Environment values for all container environment: - GO111MODULE: "on" steps: - checkout - run: name: Install AWS CLI command: | curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" unzip awscliv2.zip sudo ./aws/install - run: name: Fetch dependencies command: go mod download - run: name: Wait for DynamoDB command: | for i in `seq 1 10`; do nc -z localhost 8000 && echo Success && exit 0 echo -n . sleep 1 done echo Failed waiting for DyanmoDB Local && exit 1 - run: name: Run all unit tests command: | export AWS_REGION=ap-northeast-1 export AWS_ACCESS_KEY_ID=dummy export AWS_SECRET_ACCESS_KEY=dummy export DYNAMO_ENDPOINT=http://localhost:8000 export DYNAMO_TABLE_USER=local_user make test workflows: version: 2 build-and-test: jobs: - test
サマリ
DynamoDB Localを用いたGoの実装と、CircleCIの設定を示しました。本記事で扱ったコードは以下のリポジトリにコミットしてあります。
https://github.com/d-tsuji/sample-circleci
よいDynamoDB Localライフを。