AWS SAM + LocalStack によるサンプルアプリケーションの作成

Serverless Framework,AWS SAM,AWS CDKなど, サーバーレスのローカル開発環境を行うツールが増えてきました.

使っている人が多いのはおそらくServerless Frameworkですが, AWS公式がメンテナンスしていて, ローカルでのLambda関数のテストができるAWS SAMは今なお有力な選択肢だと思います.

しかしサーバーレスは,クラウド環境では手軽に扱える反面, ローカル環境での開発はハマりどころや辛いところが多いです.

そこで本記事では,AWS SAM を使って,API Gateway を通してLambdaを呼び出し, DynamoDBとやりとりするアプリを作っていきます. LambdaのランタイムはNode.jsです.

また,DynamoDBはLocalStackのコンテナにホストします. DynamoDBだけであればAWS公式が提供している DynamoDBのDockerイメージを使うことができますが, SQSやS3などのサービスも使うことが多いため, LocalStackの使用をおすすめします.

バージョン情報

macOS Catalina 10.15.4
AWS CLI 1.16.310
AWS SAM CLI 0.44.0
Docker 19.03.8

構築手順

プロジェクト作成

sam initコマンドでプロジェクトを作成します. 今回はQuick Start Templatesから作っていきます.

> sam init --name sam-sample --runtime nodejs12.x

Which template source would you like to use?                                                                                                                                                                        
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1

Cloning app templates from https://github.com/awslabs/aws-sam-cli-app-templates.git

-----------------------
Generating application:
-----------------------
Name: sam-sample
Runtime: nodejs12.x
Dependency Manager: npm
Application Template: hello-world
Output Directory: .

Next steps can be found in the README file at ./sam-sample/README.md

これで,簡単なAPIが作られました.

確認

サンプルのAPIの動作確認です.

> sam local start-api 

Mounting HelloWorldFunction at http://127.0.0.1:3000/hello [GET]
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
2020-04-29 10:23:23  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)

ターミナルの別ウィンドウを開いてcurlします.

> curl http://localhost:3000/hello

{"message":"hello world"}

hello worldが返ってきました.

LocalStackの設定

Docker networkの作成

> docker network create nw-sam-sample

docker-composeでLocalStackコンテナを作成・起動

今回はDynamoDBしか使いませんが, この記事をご覧の方の中には, DynamoDBとSQSを両方使いたい場合があるかもしれないので, 参考までに両方ホストするためのファイルを書いておきます.

docker-compose.yaml

version: "3.3"

services:
  localstack:
    container_name: localstack-sam-sample
    image: localstack/localstack
    ports:
      - 8080:8080
      - 4569:4569 # DynamoDB
      - 4576:4576 # SQS
    environment:
      - SERVICES=dynamodb,sqs
    networks:
      - nw-sam-sample

networks:
  nw-sam-sample:
    external: true

LocalStackを起動します.

> docker-compose up -d

DynamoDB テーブルを作成

スキーマ情報のファイルを作成

Id をプライマリキーとするテーブルを作っていきます.

schema.json

{
    "AttributeDefinitions": [
        {
            "AttributeName": "Id",
            "AttributeType": "S"
        }
    ],
    "TableName": "tb-sam-sample",
    "KeySchema": [
        {
            "AttributeName": "Id",
            "KeyType": "HASH"
        }
    ],
    "BillingMode": "PAY_PER_REQUEST"
}

LocalStackに作成

> aws dynamodb create-table --endpoint-url http://localhost:4569 --cli-input-json file://schema.json

コンテナをdownすると,テーブルが全て消えるので注意

確認

> aws dynamodb list-tables --endpoint-url http://localhost:4569

{
    "TableNames": [
        "tb-sam-sample"
    ]
}

Lambda関数の追加

サンプルプロジェクトに含まれているHelloWorldFunctionの下に, DynamoDBとやり取りするLambda関数であるSamSampleFunctionを作成します.

今回は3つのパスを用意します.

  • POST /sample:新たにリソースを作成
  • GET /sample:全てのリソースを表示
  • GET /sample/{id}:パスパラメータidで指定したIdを持つリソースを表示

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-sample

  Sample SAM Template for sam-sample
  
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello-world/
      Handler: app.lambdaHandler
      Runtime: nodejs12.x
      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello
            Method: get

  # add
  SamSampleFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello-world/
      Handler: sam-sample.handler
      Runtime: nodejs12.x
      Events:
        Create:
          Type: Api
          Properties:
            Path: /sample
            Method: post
        Index:
          Type: Api
          Properties:
            Path: /sample
            Method: get
        Show:
          Type: Api
          Properties:
            Path: /sample/{id}
            Method: get

Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  HelloWorldApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn

次に,そのハンドラをhello-world/sam-sample.js内に書いていきます.

注意点

Macの場合, ハンドラ内のDynamoDBエンドポイントは http://localhost:4569ではなく http://host.docker.internal:4569としてください.

今回,Dockerコンテナ同士をDockerネットワークでつなぐので, localhostでは動きません.

ちなみに,Linuxではhttp://localstack:4569になります.

hello-world/sam-sample.js

const AWS = require('aws-sdk');
AWS.config.update({ region: 'ap-northeast-1' });

const dynamoDBEndpoint = 'http://host.docker.internal:4569';
const dynamoDB = new AWS.DynamoDB({ endpoint: dynamoDBEndpoint });
const docClient = new AWS.DynamoDB.DocumentClient({ service: dynamoDB });

const tableName = 'tb-sam-sample';

exports.handler = async (event, context) => {
    try {
        switch (event.httpMethod) {
            case 'POST': {
                const payload = JSON.parse(event.body);

                await docClient.put({
                    Item: {
                        Id: payload.id,
                        Data: payload.data
                    },
                    TableName: tableName
                }).promise();

                return {
                    statusCode: 200,
                    body: {}
                }
            }

            case 'GET': {
                if (event.pathParameters && event.pathParameters.id) {
                    const id = event.pathParameters.id;

                    const res = await docClient.get({
                        Key: { Id: id },
                        TableName: tableName
                    }).promise();

                    return {
                        statusCode: 200,
                        body: JSON.stringify(res.Item)
                    }
                } else {
                    const res = await docClient.scan({
                        TableName: tableName
                    }).promise();

                    return {
                        statusCode: 200,
                        body: JSON.stringify(res.Items)
                    }
                }
            }

            default: {
                return {
                    statusCode: 400,
                    body: JSON.stringify({
                        message: 'invalid input'
                    })
                }
            }
        }
    } catch (err) {
        console.log(err);
        return {
            statusCode: 500,
            body: JSON.stringify({
                message: 'internal server error'
            })
        }
    }
};

起動

> sam local start-api --docker-network nw-sam-sample

確認

> curl http://localhost:3000/sample -X POST -d '{"id": "1", "data": "hello"}' 
> curl http://localhost:3000/sample
[{"Data":"hello","Id":"1"}]
> curl http://localhost:3000/sample/1
{"Data":"hello","Id":"1"}

完成です!

最後に

今回はAWS SAMとLocalStackでLambda関数のローカル開発環境を構築し, API Gatewayを通してDynamoDBのCRUDを行うサンプルアプリケーションを作成しました.

今回作成したアプリケーションは, LocalStackにアクセスするためのものなので, これを参考にしてデプロイする際は, 適宜環境変数を用いてAWSサービスに対するエンドポイントをスイッチしてください.

この記事が少しでも参考になれば幸いです.

今回作成したリポジトリです.

github.com