今日から始めるサーバーレス SAM【API Gateway + Lambda + DynamoDB】


SAM CLIでAPI Gateway + Lambda + DynamoDBを使う

AWSでのサーバレス構築を考えた時に最も無難でポピュラーな構成(悪く言えばあまり面白みのない)として挙げられる、

  • API Gateway
  • Lambda
  • DynamoDB

の構築を、SAM(Serverless Application Model) で行います。

書くこと

  1. SAM CLIでプロジェクトの作成
  2. SAMプロジェクトのデプロイ
  3. SAMプロジェクトを修正してDynamoDBにテーブルを作成
  4. SAMプロジェクトの更新

SAM CLIでプロジェクトの作成

まずSAM CLIをインストールします。

Installing the AWS SAM CLI

$ sam --version
SAM CLI, version 0.40.0

インストールができれば早速SAMプロジェクトします。

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

Which runtime would you like to use?
    1 - nodejs12.x
    2 - python3.8
    3 - ruby2.5
    4 - go1.x
    5 - java11
    6 - dotnetcore2.1
    7 - nodejs10.x
    8 - python3.7
    9 - python3.6
    10 - python2.7
    11 - java8
    12 - dotnetcore2.0
    13 - dotnetcore1.0
Runtime: 1

Project name [sam-app]: sample

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

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

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

sam initを実行すると、3点の質問を尋ねられます。
ここでは、

  1. SAMのテンプレートとして1 - AWS Quick Start Templates
  2. lambdaのランタイムとしてnodejs12.xを選択し、
  3. プロジェクト名を入力

しています。

そうすると以下の構造のディレクトリが作成されると思います。

.
├── README.md
├── events
│   └── event.json
├── hello-world
│   ├── app.js
│   ├── package.json
│   └── tests
│       └── unit
│           └── test-handler.js
└── template.yaml

SAMプロジェクトのデプロイ

何もしていませんが、早速このままデプロイしてみましょう。

まず、デプロイの準備をします。
上記のソースのままではデプロイコマンドsam deployを使うことができないので、一旦以下のコマンドを実行します。

$ sam build

sam buildを実行すると、SAMプロジェクトのトップレベルに.aws-sam/buildが作成され、
その中にhelloWorldFunctionディレクトリとtemplate.yamlが配置されます。

helloWorldFunctionディレクトリはSAMプロジェクトのトップレベルにあるhello-worldのアーティファクト(成果物、生成物)でこいつをAWS Lambdaへデプロイします。

template.yamlはSAMプロジェクトのトップレベルにある同名のtemplate.yamlを整形したものになっています。
このyamlファイルがcloudformationへアップロードされて、サーバレスを構成するリソースたちのスタックが組み立てられます。

ビルドが成功したら以下のコマンドを実行します。

$ sam deploy --guided

すると以下の質問事項に対する応答を求められます。

1. Stack Name // スタック名を入力
2. AWS Region // お好きなAWSリージョン
3. Confirm changes before deploy
  // デプロイ実行前にデプロイによって変更されるスタックの状態を確認した上で、
  // デプロイを実行できるようにするかどうか(yesにしておいて問題ありません)
4. Allow SAM CLI IAM role creation
  // SAM CLIがIAMロールを作っても良いかどうか(yesにしておいて問題ありません)
5. Save arguments to samconfig.toml
  // samconfig.tomlを作成し、
  // その中にデフォルトのデプロイパラメータを書き込んでおくかどうか(yesにしておいて問題ありません)

そのままデプロイが実行されることになりますが、
先ほどのConfirm changes before deployを有効にしていると、
Deploy this changeset? [y/N]:
と聞かれます。

Cloudformationスタックの変更部分の一覧が表示されるので確認の上yとしてあげると、
デプロイが最後まで実行されます。

デプロイ後、AWSコンソールのLambdaには以下のように関数が追加されているはずです。

API Gatewayには以下のように。

API GatewayのDashboardからエンドポイントを確認して、

URLへアクセスすると、

{"message":"hello world"}

と表示されるはずです。

SAMプロジェクトを修正してDynamoDBにテーブルを作成

ここまででhelloWorldFunctionのデプロイとその実行をトリガーするAPI Gatewayのデプロイが成功しました。

ですが、ここではDynamoDBでのデータの読み書きについても触れたいと思います。

まず、hello-worldのSAMテンプレートにあるtemplate.yamlにはDynamoDBリソースが記載されていないので追記する必要があります。

以下をtemplate.yamlResourcesに追記してください(丁度既存のHelloWorldFunctionの次あたりに)。

template.yaml
  PostFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: post/
      Handler: app.lambdaHandler
      Runtime: nodejs12.x
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /post
            Method: post
      Role: !GetAtt lambdaFunctionRole.Arn
  PostItems:
    Type: AWS::DynamoDB::Table
    Properties:
      AttributeDefinitions:
        -
          AttributeName: 'partitionKey'
          AttributeType: 'S'
      TableName: 'postItems'
      KeySchema:
        -
          AttributeName: 'partitionKey'
          KeyType: 'HASH'
      ProvisionedThroughput:
        ReadCapacityUnits: 1
        WriteCapacityUnits: 1
  lambdaFunctionRole: # lambda関数がDynamoDBとCloudWatchにアクセスするためのロール
    Type: AWS::IAM::Role
    Properties:
      RoleName: 'RoleForLambdaFunction'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        -
          Effect: Allow
          Principal:
            Service:
              - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      Policies:
        -
          PolicyName: 'FullAccessToDynamoDB'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
            -
              Effect: Allow
              Action:
                - dynamodb:*
              Resource: "*"
        -
          PolicyName: 'WriteLimitedAccessToCloudWatch'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
            -
              Effect: Allow
              Action:
                - logs:*
              Resource: "*"

ここで定義しているリソースは
1. ポストを行うLambda関数
2. ポストされたデータを保管するDynamoDBテーブル
3. Lambda関数にDynamoDBとCloudWatchへのアクセスを許可するロール
の三つです。

リソース定義の詳細についてはここでは触れません(DynamoDBのpartitionKeyとかAWS::Serverless::FuncntionのEvents設定とかIAMロールとか)ので、以下参照のこと。

AWS::Serverless::Functionについて
AWS::DynamoDB::Tableについて
AWS::IAM::Roleについて

上記のPostFunctionリソースでは、CodeUriにpost/を指定しているので、
SAMプロジェクトにトップディレクトリにpostディレクトリを作成し、その中にLambda関数のソースコードを配置します。

中身のapp.jsは例えばこんな感じになります。
postディレクトリ内でyarnを行い、yarn add -D aws-sdkでDynamoDBを使えるようにしておきます。

sample/post/app.js
const AWS = require('aws-sdk')
const dynamo = new AWS.DynamoDB.DocumentClient({region: 'us-west-2'})

exports.lambdaHandler = async (event, _, _) => {
  const requestBody = JSON.parse(event.body)

  const table = 'postItems'
  const items = {
    partitionKey: requestBody.partitionKey,
  }
  const params = {
    TableName: table,
    Item: items,
  }

  try {
    await dynamo.put(params, (err, data) => {
      if (err) console.error(err)
    }).promise()

    const response = {
      statusCode: 200,
      body: JSON.stringify(items)
    }
    return response
  } catch(err) {
    console.error(err)
    return err
  }
}

SAM CLIで作成されるAPI Gatewayでは統合リクエストの設定でプロキシ統合がONになっているので、
ややこしいマッピングテンプレートについて知る必要はなく、渡したリクエストの値はハンドラーの引数eventで参照できます。
上記ではevent.bodyをパースして中の値を取り出しています。

SAMプロジェクトの更新

更新したソースコードとtemplate.yamlを再びビルドしてデプロイします。

まず、SAMプロジェクトのトップレベルで以下を実行します。

$ sam build

すると、.aws-sam/build配下にPostFunctionディレクトリが追加されるはずです。
.aws-sam/build/template.yamlも更新されていると思います。

デプロイは初回で使っていた--guidedを省いて、

$ sam deploy

とだけ実行します。

*この時にCAPABILITY_NAMED_IAMを使えというエラーが出ると思いますので、初回デプロイ時に生成されるsamconfig.toml(SAMプロジェクトのトップディレクトリ)に記載されているパラメータcapabilitiesの値をCAPABILITY_NAMED_IAMに書き換えてください。

CloudFormationスタックの更新状態を確認して、

Deploy this changeset? [y/N]: y

と入力します。

デプロイ後にAWSコンソールに入ると、

DynamoDBにテーブルが追加されます。

Lambdaにも追加されています。

試しにPOSTしてみる

API Gatewayでエンドポイントを確認して、

/postへPOSTを行います。
Advanced Rest ClientとかでBodyにpartitionKeyを指定してPOSTします。

するとPostFunctionのコード内で、リクエストのpartitionKeyをパラメータとして取得してDynamoDBにそのデータを登録します。

こんな感じにデータが入っていると思います。

Lambda関数に紐づけているRoleにはCloudWatchへの書き込み権限もあるので、
CloudWatchのログでlambda関数のログストリームを覗くこともできます(以下画像の右側中央部にある「Views logs in CloudWatch」から別タブで開いてみることができる)。

CloudWatchのログストリーム一覧↓

以上でAWS SAMを使ってAPI Gateway + Lambda + DynamoDBをつかったサーバレス環境を構築できました。
他にもいろいろリソースがあるので自分なりにcloudformationのテンプレートを書き換えてみると面白いと思います。