production.log

ピクスタ株式会社で開発部の部長をやっている星直史のブログです。

LocalStackを使用してGoで書いたLambdaからDynamoDBを呼び出す方法

概要

以前の記事でLocalStackをインストールしました。 今回はLocalStackを使用してGoで書いたLambdaからDynamoDBを呼び出す方法を紹介します。

DynamoDBの操作

LocalStackはAWSのマネジメントコンソールと違い、各サービスに対しての操作はCLIから行います。 まずは、DynamoDBに対してテーブルとデータを登録します。

テーブル作成

LocalStackを立ち上げた直後はテーブルが存在しません。

$ aws --endpoint-url=http://localhost:4569 dynamodb list-tables
{
    "TableNames": []
}

テーブル生成コマンドは下記の通りです。 このコマンドでは、「Nameという文字列のカラムがキー」である「testテーブル」を作成しています。

aws --endpoint-url=http://localhost:4569 dynamodb create-table \
--table-name test \
--attribute-definitions AttributeName=Name,AttributeType=S \
--key-schema AttributeName=Name,KeyType=HASH \
--provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1

次にデータを1つ登録します。

aws --endpoint-url=http://localhost:4569 dynamodb put-item \
--table-name test \
--item '{ "Name": { "S": "testtest" } }'

最後に、登録したデータが取得できるかを確認します。

aws --endpoint-url=http://localhost:4569 dynamodb scan --table-name test

GoでLambdaの処理を書く

続いて、DynamoDBに登録したデータを呼び出すLambdaの処理を書きます。 今回はGo言語で書きます。

Go言語の環境構築

環境構築はbrewで済ませることができます。

brew install go

続いて、Lambda関数で使用するパッケージをインストールしていきます。

go get -u github.com/aws/aws-sdk-go/aws \
  github.com/aws/aws-sdk-go/aws/session \
  github.com/aws/aws-sdk-go/aws/credentials \
  github.com/aws/aws-lambda-go/lambda \
  github.com/guregu/dynamo 

DynamoDBへの操作はSDKをそのまま使う操作がだいぶ辛いです。 そこで、DynamoDBへの操作を容易に行えるgithub.com/guregu/dynamoを使います。
具体的にどれだけ楽になるかを同じデータ更新処理で比較してみます。

SDKの場合

param := &dynamodb.UpdateItemInput{
    TableName: aws.String("TableName"),
    Key: map[string]*dynamodb.AttributeValue{
      "id": {
        N: aws.String("123"),
      },
    },
    ExpressionAttributeNames: map[string]*string{
      "#username": aws.String("username"),
    },
    ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
      ":username_value": {
        S: aws.String("hoge"),
      },
    },
    UpdateExpression: aws.String("set #username = :username_value"),
    ReturnConsumedCapacity: aws.String("NONE"),
    ReturnItemCollectionMetrics: aws.String("NONE"),
    ReturnValues: aws.String("NONE"),
  }

  resp, err := ddb.UpdateItem(param)

guregu/dynamoの場合

type User struct {
  id       int `dynamo:"id"`
  username string
}

db := dynamo.New(session.New(), &aws.Config{
  Region: aws.String("us-east-1"),
})

table := db.Table("TableName")
user  := User{id: 123, username: "hoge"}
err   := table.Put(user).Run()

guregu/dynamoを使用した方が簡潔に書けますね!

処理を書く

先述のguregu/dynamoを使ってDynamoDBに接続する処理を書きます。 ポイントは、エンドポイントをLocalStackのエンドポイントに変更することです。

package main

import (
  "github.com/aws/aws-sdk-go/aws"
  "github.com/aws/aws-sdk-go/aws/session"
  "github.com/aws/aws-sdk-go/aws/credentials"
  "github.com/aws/aws-lambda-go/lambda"
  "github.com/guregu/dynamo"
)

type Event struct {
  Name string `json:"name"`
}

type Response struct {
  Result string `json:"Name:"`
}


type Test struct {
  Name string `dynamo:"Name"`
}

func handler(event Event) (Response, error) {
  conf := &aws.Config{
    Credentials: credentials.NewStaticCredentials("dumy", "dumy", ""),
    Region:      "us-east-1",
    Endpoint:    "http://localhost:4569",
  }
  sess, err := session.NewSession(conf)

  db    := dynamo.New(sess)
  table := db.Table("test")

  var result Test
  err = table.Get("Name", "testtest").One(&result)

  println(result.Name)

  return Response{Result: result.Name}, err
}
func main(){
  //handler(Event{Name: "testtest"})
  lambda.Start(handler)
}

デプロイパッケージの作成

スクリプトを書いたあとは、デプロイパッケージの作成をしてLambdaにデプロイします。

macOSを使用している場合は、ハンドラ関数がLambda実行コンテキストと互換性を持たせなければなりません。そのため、コンパイルするときにLinux用のGOOS*1環境変数を設定する必要があります。

GOOS=linux GOARCH=amd64 go build -o handler
zip handler.zip ./handler

Lambdaへのデプロイは下記コマンドです。

# 新規作成(コンテナ立ち上げ直後)
aws lambda create-function \
  --endpoint-url http://localhost:4574 \
  --region us-east-1 \
  --profile localstack \
  --function-name handler \
  --runtime go1.x \
  --role r1 \
  --handler handler \
  --zip-file fileb://handler.zip

# 既存ハンドラ関数の更新
aws lambda update-function-code \
  --endpoint-url http://localhost:4574 \
  --region us-east-1 \
  --profile localstack \
  --function-name handler \
  --zip-file fileb://handler.zip --publish

動作確認

デプロイまでしておきつつ、動作確認はLocalStackを経由せずに手元から直接Goを動かすのが速くて楽です。

実行は下記コマンドで行います。

go run handler.go

LocalStackに登録したLambda関数を呼び出す時の問題点

せっかくデプロイしたので、LocalStack上のLambdaを呼び出したいですよね。 実行自体は下記のコマンドで実行します。

aws lambda --endpoint-url=http://localhost:4574 invoke \
  --function-name handler \
  --payload '{ "name": "testtest"}'\
  result.log

しかし、LocalStack上のLambdaからDynamoDBに接続できない問題があります。 エンドポイントを変えずにAWSにデプロイし、実行すると問題なく接続できるので、LocalStack固有の問題なのではないかと思います。

参考までに、発生したエラーはこちらです。

HTTPConnectionPool(host='localhost', port=4574): Read timed out. (read timeout=60)
localstack_1  | START RequestId: b7879ca4-4d51-15b6-ea01-1145f4c29e0c Version: $LATEST
localstack_1  | RequestError: send request failed
localstack_1  | caused by: Post http://localhost:4569//: dial tcp 127.0.0.1:4569: connect: connection refused
localstack_1  | END RequestId: b7879ca4-4d51-15b6-ea01-1145f4c29e0c
localstack_1  | REPORT RequestId: b7879ca4-4d51-15b6-ea01-1145f4c29e0c  Duration: 51179.39 ms   Billed Duration: 51200 ms   Memory Size: 1536 MB    Max Memory Used: 9 MB
localstack_1  | {
localstack_1  |   "errorMessage": "RequestError: send request failed\ncaused by: Post http://localhost:4569//: dial tcp 127.0.0.1:4569: connect: connection refused",
localstack_1  |   "errorType": "baseError"
localstack_1  | }

まとめ

今回はGoで書いたLambdaからDynamoDBを呼び出す方法を紹介しました。 DynamoDBに接続するためのGoの処理はguregu/dynamoを使用することで、複雑な処理を書かなくて済みました。 LocalStackはCLIでしか操作できない点は難点であるものの、Lambdaを手元ですぐに実行できるのは大きなメリットだと思います。ただし、LocalStackに登録したLambda関数からDynamoDBへはうまく接続できないので、go runなどで直接goの関数を叩かなければならないという手間も発生します。*2

*1:Goオペレーティングシステム

*2:未開の地感がありますね