My External Storage

Jun 2, 2019 - 6 minute read - Comments - aws circleci

DynamoDBを操作するコードをamazon/dynamodb-localコンテナとCircleCIを使ってCIする #aws

DynamoDBを操作するコードをCircleCI上でテストする方法をまとめた。
dynamodb-localをCircleCI上で起動することで、実際にDBアクセスする状態でテストが実行できる。

TL;DR

  • AWSはローカル環境で動作確認できるようにDynamoDBを起動できるコンテナを配布している
  • このコンテナを使ってオフラインでDybamoDBを使ったテストを実行できる
  • CircleCI上でもこのコンテナを起動して、DynamoDBを使ったCIを定義した
    • ダミーで良いのでAWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYを設定しておく
    • 念のためテスト実行前にncコマンドでDynamoDBと疎通できるまで待機するようにした

なお、サンプルコードは以下になる。 GoのLambdaからDynamoDBを操作することを想定している。

CircleCIの実行結果はこちら。

はじめに

DynamoDBはAWSが提供するNoSQL DBだ。Lambdaなどと組み合わせて使われることも多い。

LambdaとDyanamoDBを使ったコードを書こうと思ったのだが、できればローカルで動作確認が完了すると嬉しい。
DynamoDBクローンのDockerイメージがあるとのことだったので、こちらを利用することにした。

開発するにあたり、CircleCI上のテストでもこのDockerイメージを使うことにした。

CircleCIで実行するテストコード

まず、CircleCIで実行するリポジトリは以下になる。

Goで書かれたテストコードはこちら。テスト対象のコードを実行する前にDynamo DB上にテーブルを作成、作成したテーブルにアイテムを保存しておく。
また、呼び出しているテスト対象のコード自体はDynamoDB上からアイテムを取得するコードだ。

// コード全体はこちら
// https://github.com/budougumi0617/lambda-go-dynamodb-local/blob/add-circleci/user/main_test.go

func TestGetUser(t *testing.T) {
	ep := os.Getenv("DYNAMODB_ENDPOINT")
	r := os.Getenv("AWS_REGION")
	tn := os.Getenv("DYNAMODB_TABLE_NAME")

	dyn := dynamodb.New(
		session.Must(
			session.NewSession(
				&aws.Config{
					Endpoint: aws.String(ep),
					Region:   aws.String(r),
				},
			),
		),
	)

	if err := deleteTestData(dyn, tn); err != nil {
		t.Logf("delete table failed %v\n", err)
	}

	if err := createTestData(dyn, tn); err != nil {
		t.Fatalf("table created failed rr: %v\n", err)
	}

	h := generateHandler(ep, r, tn)
	in := events.APIGatewayProxyRequest{}
	in.PathParameters = make(map[string]string)
	in.PathParameters["id"] = "1"

	res, err := h(in)
	if err != nil {
		t.Fatalf("err: %v\n", err)
	}
	t.Logf("result = %+v\n", res)
}

func createTestData(db *dynamodb.DynamoDB, tn string) error {
	input := &dynamodb.CreateTableInput{
		// ...
	}

	if _, err := db.CreateTable(input); err != nil {
		return err
	}

	// PutItem
	putParams := &dynamodb.PutItemInput{
		// ...
	}

	if _, err := db.PutItem(putParams); err != nil {
		return err
	}
	return nil
}

func deleteTestData(db *dynamodb.DynamoDB, tn string) error {
	input := &dynamodb.DeleteTableInput{
		TableName: aws.String(tn),
	}
	_, err := db.DeleteTable(input)
	if err != nil {
		return err
	}
	return nil
}

このコードをモックを使わずCircleCI上で実行する。

ローカルに起動したDynamoDBを使ってテストを実行する

ちなみに、amazon/dynamodb-localコンテナを使えばローカルにDynamoDBを起動することもできる。 ローカルのDynamoDBを使って上記のテストコードを実行するときは以下のようになる。

$ cat docker-compose.yaml
version: '3'

services:
  dynamodb:
    image: amazon/dynamodb-local
    container_name: dynamodb
    ports:
      - 18000:8000
$ docker-compose up -d
$ DYNAMODB_ENDPOINT="http://localhost:18000" go test ./...

CircleCIの設定

CircleCIの設定は以下のようになる。関連する設定部分はこのを個別に解説を入れる。

# https://github.com/budougumi0617/lambda-go-dynamodb-local/blob/add-circleci/.circleci/config.yml

version: 2.1
executors:
  default:
    docker:
      # CircleCI Go images available at: https://hub.docker.com/r/circleci/golang/
      - image: circleci/golang:1.12.5
      - image: amazon/dynamodb-local

    working_directory: /go/src/github.com/budougumi0617/lambda-go-dynamodb-local

    # Environment values for all container
    environment:
      - GO111MODULE: "on"
      - TEST_RESULTS: /tmp/test-results # path to where test results will be saved
      - AWS_REGION: "test-region"
      - DYNAMODB_ENDPOINT: "http://localhost:8000"
      - DYNAMODB_TABLE_NAME: "User"

jobs:
  test:
    executor:
      name: default
    steps:
      - checkout
      - run: mkdir -p $TEST_RESULTS
      - restore_cache:
          name: Restore go modules cache
          keys:
              - v1-mod-{{ .Branch }}-{{ checksum "go.mod" }}
      - run:
          name: Vendoring
          command: go mod download
      # カバレッジの集計などをするためのツールのインストール
      - run:
          name: Install test report tool
          command: go get github.com/jstemmer/go-junit-report
      - save_cache:
          name: Save go modules cache
          key: v1-mod-{{ .Branch }}-{{ checksum "go.mod" }}
          paths:
              - /go/pkg/mod/cache
      - 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: |
            trap "go-junit-report <${TEST_RESULTS}/go-test.out > ${TEST_RESULTS}/go-test-report.xml" EXIT
            go test ./... | tee ${TEST_RESULTS}/go-test.out            
      - store_artifacts:
          path: /tmp/test-results
          destination: raw-test-output
      - store_test_results:
          path: /tmp/test-results

workflows:
  build-and-test:
    jobs:
      - test

CircleCIでamazon/dynamodb-localイメージを起動する

CircleCIは一連のジョブを実行するイメージの他にDBなどのサブイメージを起動できる。
メインイメージからサブイメージへのアクセスはlocalhost経由で可能だ。

executors:
  default:
    docker:
      # CircleCI Go images available at: https://hub.docker.com/r/circleci/golang/
      - image: circleci/golang:1.12.5
      - image: amazon/dynamodb-local
    # Environment values for all container
    environment:
      - DYNAMODB_ENDPOINT: "http://localhost:8000"

AWS_ACCESS_KEY_IDとAWS_SECRET_ACCESS_KEYを設定する

CircleCIのリポジトリの設定にあるEnvironment VariablesAWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYを設定しておく。
dynamodb-localは起動時と接続時に認証情報を内部で利用している。実際にAWSにアクセスできる有効な文字列でなくていいので、認証情報を設定しておく必要がある。

ncコマンドで疎通を確認する

MySQLなどはdockerizeコマンドを使うのだが、dockerizeコマンドでhttpプロトコルの確認をするときは200ステータスが必要になる。
dynamodb-localへの疎通確認をhttpプロトコルで行なうと認証をつけないと400エラーになるため、今回は利用をやめた。
そのため、今回は単純にポートがリッスンしていることを確認するncコマンドを使った。

      - 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            

以上の設定を行えば、CircleCI上でDynamoDBを利用したテストを実行できる。

終わりに

今回はamazon/dynamodb-localコンテナを使ってCircleCIでDynamoDBを操作するテストを実行した。
dynamodb-local.jarをCircleCI上で起動して同様の操作をする記事はあったのだが、このためだけにJavaの環境を用意するのは他の言語を使って開発しているときは少しめんどくさい。
今回の記事はCircleCIの設定を調べるよりローカルでうまくDynamoDBと疎通するコードを作るほうが難しかった(正確にいうと、sam local start-apiコマンドで実行したLambdaからdynamodb-localで起動したDynamoDBにアクセスするのに苦戦していた)。
LambdaとDynamoDBの組み合わせは料金もほぼかからない組み合わせのため、もう少し勉強しておくつもりだ。

参考

関連記事