今日から始めるサーバーレス 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](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html)

```
$ 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
```

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

```md
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には以下のように関数が追加されているはずです。

<img width="1134" alt="Screenshot 0032-02-02 at 11.43.32 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245704/d2920f9c-3d37-f19d-639c-6975923ba386.png">

API Gatewayには以下のように。

<img width="1151" alt="Screenshot 0032-02-02 at 11.43.47 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245704/6fffdba8-01b5-59f7-6990-042f545f1fc4.png">

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

<img width="1427" alt="Screenshot 0032-02-02 at 11.45.47 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245704/0750ae99-763c-11df-9666-6f7326373eb1.png">

URLへアクセスすると、

```
{"message":"hello world"}
```

と表示されるはずです。

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

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

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

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

以下を`template.yaml`の`Resources`に追記してください（丁度既存の`HelloWorldFunction`の次あたりに）。

```yaml: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について](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html)
[AWS::DynamoDB::Tableについて](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html)
[AWS::IAM::Roleについて](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html)

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

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

```js: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にテーブルが追加されます。

<img width="1422" alt="Screenshot 0032-02-03 at 12.19.57 AM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245704/6db300a0-572f-d5ac-adc3-33a8918a38ec.png">

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

<img width="1144" alt="Screenshot 0032-02-03 at 12.20.48 AM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245704/378321cf-fea1-ff84-d0de-fb381ae63e34.png">

## 試しにPOSTしてみる

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

<img width="1427" alt="Screenshot 0032-02-02 at 11.45.47 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245704/d6797b70-bade-09f2-be93-a210db21c8a3.png">

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

<img width="854" alt="Screenshot 0032-02-03 at 12.38.41 AM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245704/05881a3c-cce2-3a65-0574-ae18ed50436a.png">

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

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

<img width="1252" alt="Screenshot 0032-02-03 at 12.55.08 AM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245704/ededba46-0f6f-46d7-d5e7-9422bd956caf.png">

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

<img width="1332" alt="Screenshot 0032-02-03 at 1.01.17 AM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245704/bd714514-b36c-7c40-1697-732e99eaf979.png">

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

<img width="1437" alt="Screenshot 0032-02-03 at 1.02.29 AM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245704/69e4e9e2-a0dc-1132-3cdc-7f465b606654.png">


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