技術メモ

技術メモ

ラフなメモ

DynamoDB LocalとGoを用いた実装と、CircleCIでのCIの設定方法

本記事ではDynanmoDB Localを用いてローカルでのGoを用いた開発をする方法と、CircleCIを用いてCIを実施する方法を示します。

DynamoDB Localのセットアップ

まずDynamoDBにはDynamoDB Localと言われるエミュレーターがあるので、ローカルでの開発はDynamoDB Localを利用しましょう。

aws_access_key_idaws_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ライフを。