この記事は エムスリー Advent Calendar 2018 6日目の記事です。
こんにちは、エムスリー エンジニアリンググループの大和です。 普段は Spring Boot (Java + Kotlin) を使用したサーバサイド開発をしていますが、この度 AWS Lambda を使用したアプリケーション開発を行うことになったため、開発環境を整えるところまでを記事にしたいと思います。
今回は、例として次のシンプルな構成のアプリケーションを作成していきます。
AWS SAM とは
AWS SAM (Serverless Application Model) とは、AWS 上でサーバーレスアプリケーションを構築するために使用できるフレームワークです*1。 YAML もしくは JSON 形式 (正確には AWS CloudFormation のテンプレート) で構成を記述できます。 また、コマンドラインで扱うためのツールとして AWS SAM CLI が提供され、こちらを使用することでローカルで実行できます*2。
似たようなフレームワークに Serverless Framework がありますが、今回は AWS のみを使用することに加え、(Node.js 以外での) ローカルでのテストが行いやすいため AWS SAM を採用しています。
セットアップ
前提として、Python3 と Docker が必要です。 その上で AWS CLI をインストールします。 AWS CLI および AWS SAM CLI は Python で記述されているので、venv や pipenv で管理するとよいでしょう。
$ pip install awscli $ aws configure --profile sam-cli-test AWS Access Key ID [None]: dummy AWS Secret Access Key [None]: dummy Default region name [None]: us-east-1 Default output format [None]: json $ pip install aws-sam-cli
ローカルで使用する場合であっても、リージョン等の設定が必要なため aws configure
を実行しています。
プロファイルの切り替えは、オプション --profile
もしくは環境変数 AWS_PROFILE
で指定できます。
$ export AWS_PROFILE=sam-cli-test
雛形作成
次に雛形を作成していきます。作成は sam init
で行えます。
今回は Go で関数を作成していきます。
$ sam init --runtime go --name sam-cli-test $ cd sam-cli-test $ tree . . ├── Makefile ├── README.md ├── hello-world │ ├── main.go │ └── main_test.go └── template.yaml 1 directory, 5 files
hello-world/main.go
が Lambda 関数の実装、 template.yaml
が AWS SAM によるテンプレートです。
template.yaml
中の関数定義を見ると以下のようになっています。
( template.yaml
の一部抜粋)
Resources: HelloWorldFunction: Type: AWS::Serverless::Function Properties: CodeUri: hello-world/ Handler: hello-world Runtime: go1.x Tracing: Active Events: CatchAll: Type: Api Properties: Path: /hello Method: GET Environment: Variables: PARAM1: VALUE
Type: AWS::Serverless::Function
で Lambda 関数であることを示し、 CatchAll:
以下で API の定義を行っています。
また、今回は Go のライブラリのバージョン管理に dep を使用します*3。
そのため、今回のプロジェクト sam-cli-test
は $GOPATH
以下に作成する必要があります。
(例: ~/go/src/github.com/<username>/sam-cli-test
)
$ dep init Using ^1.7.0 as constraint for direct dep github.com/aws/aws-lambda-go Locking in v1.7.0 (fb8f88d) for direct dep github.com/aws/aws-lambda-go
ローカルでのテスト
単体テスト
作成された雛形に、Go でかかれた単体テストがあるのでそれを実行します。
$ go test ./hello-world ok github.com/<username>/sam-cli-test/hello-world 0.016s
Lambda 関数の実行
sam local start-api
によりローカルで Lambda 関数を実行できます。
Lambda 関数の実行にはバイナリが必要なので、 make
でビルドします。
$ make build GOOS=linux GOARCH=amd64 go build -o hello-world/hello-world ./hello-world $ sam local start-api 2018-12-04 14:39:59 Found credentials in shared credentials file: ~/.aws/credentials 2018-12-04 14:39:59 Mounting HelloWorldFunction at http://127.0.0.1:3000/hello [GET] 2018-12-04 14:39:59 You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template 2018-12-04 14:39:59 * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
この状態で http://localhost:3000
に GET でアクセスすると結果が返ってきます。
( xxx.xxx.xxx.xxx
の部分には IP アドレスが入ります)
$ curl http://localhost:3000/hello Hello, xxx.xxx.xxx.xxx
DynamoDB Local を使用した開発
関数の作成
ここまで sam init
で作成した雛形を基に動作を確認してきましたが、次は DynamoDB のアクセスを行う関数を作成していきます。
まず、 GET /users/{id}
に対応する Lambda 関数を作成します。
ローカルで実行している DynamoDB を指定するために DYNAMODB_ENDPOINT
、およびデプロイ時のテーブル名を指定するために DYNAMODB_TABLE_NAME
の環境変数を使用しています。
( user/main.go
)
package main import ( "encoding/json" "os" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" ) type User struct { ID int `json:"id"` Name string `json:"name"` } func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { // Environment variables endpoint := os.Getenv("DYNAMODB_ENDPOINT") tableName := os.Getenv("DYNAMODB_TABLE_NAME") // Request id, _ := request.PathParameters["id"] // DynamoDB sess := session.Must(session.NewSession()) config := aws.NewConfig().WithRegion("us-east-1") if len(endpoint) > 0 { config = config.WithEndpoint(endpoint) } db := dynamodb.New(sess, config) response, err := db.GetItem(&dynamodb.GetItemInput{ TableName: aws.String(tableName), Key: map[string]*dynamodb.AttributeValue{ "Id": { N: aws.String(string(id)), }, }, AttributesToGet: []*string{ aws.String("Id"), aws.String("Name"), }, ConsistentRead: aws.Bool(true), ReturnConsumedCapacity: aws.String("NONE"), }) if err != nil { return events.APIGatewayProxyResponse{}, err } user := User{} err = dynamodbattribute.Unmarshal(&dynamodb.AttributeValue{M: response.Item}, &user) if err != nil { return events.APIGatewayProxyResponse{}, err } // Json bytes, err := json.Marshal(user) if err != nil { return events.APIGatewayProxyResponse{}, err } return events.APIGatewayProxyResponse{ Body: string(bytes), StatusCode: 200, }, nil } func main() { lambda.Start(handler) }
次に、作成した関数と DynamoDB の定義 (+ 環境変数) を template.yaml
に追加します*4。
( template.yaml
の一部抜粋)
Resources: HelloWorldFunction: # 省略 UserGetFunction: Type: AWS::Serverless::Function Properties: CodeUri: user/ Handler: user Runtime: go1.x Tracing: Active Policies: AmazonDynamoDBFullAccess Events: GetUser: Type: Api Properties: Path: /users/{id} Method: GET Environment: Variables: DYNAMODB_ENDPOINT: "" DYNAMODB_TABLE_NAME: !Ref UserDynamoDBTable UserDynamoDBTable: Type: AWS::Serverless::SimpleTable Properties: PrimaryKey: Name: Id Type: Number ProvisionedThroughput: ReadCapacityUnits: 2 WriteCapacityUnits: 2
dep ensure
で依存関係を更新した後に、作成した関数をビルドします。
このとき、Makefile
にビルドのルールを追記しておきます。
$ dep ensure $ make build GOOS=linux GOARCH=amd64 go build -o hello-world/hello-world ./hello-world GOOS=linux GOARCH=amd64 go build -o user/user ./user
DynamoDB Local
DynamoDB Local は Docker イメージで提供されています*5。
今回は、実行用に次の docker-compose.yaml
を作成しました。
( docker-compose.yaml
)
version: '3' services: dynamodb: image: amazon/dynamodb-local container_name: dynamodb ports: - 8000:8000
次のコマンドで実行します。
$ docker-compose up -d
テストデータ作成
テスト用のテーブルを DynamoDB に作成します。
まずは template.yaml
に記述した内容を基に JSON 形式でテーブルの情報を記述します。
ここでのテーブル名は User
としています。
( test/user_table.json
)
{ "AttributeDefinitions": [ { "AttributeName": "Id", "AttributeType": "N" } ], "TableName": "User", "KeySchema": [ { "AttributeName": "Id", "KeyType": "HASH" } ], "ProvisionedThroughput": { "ReadCapacityUnits": 2, "WriteCapacityUnits": 2 } }
また、テスト用のデータを生成する PutRequest
を同様に JSON 形式で記述します。
( test/user_table_data.json
)
{ "User": [ { "PutRequest": { "Item": { "Id": { "N": "1" }, "Name": { "S": "Foo" } } } }, { "PutRequest": { "Item": { "Id": { "N": "2" }, "Name": { "S": "Bar" } } } }, { "PutRequest": { "Item": { "Id": { "N": "3" }, "Name": { "S": "Baz" } } } } ] }
AWS CLI のコマンドを実行してデータを反映します。
このとき、 --endpoint-url
に Docker 上で実行している DynamoDB のポートを指定します。
$ aws dynamodb create-table --cli-input-json file://test/user_table.json --endpoint-url http://192.0.2.1:8000 { "TableDescription": { "AttributeDefinitions": [ { "AttributeName": "Id", "AttributeType": "N" } ], "TableName": "User", "KeySchema": [ { "AttributeName": "Id", "KeyType": "HASH" } ], "TableStatus": "ACTIVE", "CreationDateTime": 1543988218.179, "ProvisionedThroughput": { "LastIncreaseDateTime": 0.0, "LastDecreaseDateTime": 0.0, "NumberOfDecreasesToday": 0, "ReadCapacityUnits": 2, "WriteCapacityUnits": 2 }, "TableSizeBytes": 0, "ItemCount": 0, "TableArn": "arn:aws:dynamodb:ddblocal:000000000000:table/User" } } $ aws dynamodb batch-write-item --request-items file://test/user_table_data.json --endpoint-url http://192.0.2.1:8000 { "UnprocessedItems": {} }
"TableArn": "arn:aws:dynamodb:ddblocal:000000000000:table/User"
から DynamoDB にデータが反映されたことが確認できます。
テスト
ローカルでのテスト用の環境変数を JSON 形式で定義します。
( test/env.json
)
{ "UserGetFunction": { "DYNAMODB_ENDPOINT": "http://192.0.2.1:8000", "DYNAMODB_TABLE_NAME": "User" } }
--env-vars
で環境変数を指定して実行してみます。
$ sam local start-api --env-vars test/env.json $ curl http://localhost:3000/users/1 {"id":1,"name":"Foo"}
正しくデータを取得できました。
デプロイ
ローカルでの動作確認が行えたため、次は実際に AWS 上にデプロイしてみます。
まず、 aws validate
を使用してテンプレートを確認します。
ここでは AWS にアクセスするためのプロファイル dev
を指定しています。
$ sam validate --profile dev 2018-12-04 14:47:39 Found credentials in shared credentials file: ~/.aws/credentials /Users/<username>/go/src/github.com/<username>/sam-cli-test/template.yaml is a valid SAM Template
テンプレートに問題がなければ s3 にアップロードし、デプロイします。
$ sam package --template-file template.yaml --s3-bucket <bucketname> --output-template-file packaged.yaml --profile dev Uploading to xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 4337276 / 4337276.0 (100.00%) Successfully packaged artifacts and wrote output template to file packaged.yaml. Execute the following command to deploy the packaged template aws cloudformation deploy --template-file /Users/<username>/go/src/github.com/<username>/sam-cli-test/packaged.yaml --stack-name <YOUR STACK NAME> $ aws cloudformation deploy --template-file packaged.yaml --stack-name sam-cli-test --capabilities CAPABILITY_IAM --profile dev Waiting for changeset to be created.. Waiting for stack create/update to complete Successfully created/updated stack - sam-cli-test
AWS コンソールで DynamoDB にデータのセットを行い、デプロイされた API のエンドポイントを確認した後にAPI を呼び出してみます。
$ curl https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/Prod/users/1 {"id":1,"name":"Foo"}
無事に実行できました!
まとめ & 感想
AWS Lambda によるサーバーレスアプリケーションの開発環境を AWS SAM + DynamoDB Local で整えました。 DynamoDB Local 用の JSON ファイルを記述する必要はありますが、ローカルで DynamoDB を使用した関数の開発が行えるのはメリットだと感じています。 また、 AWS 上でのテスト後も CloudFormation で一括削除できるので便利です。
We are hiring!
エムスリーでは、本番環境で AWS や Firebase 等を活用しています。 興味を持たれた方は、是非下のリンクよりお問い合わせください。