Serverless Application Model に入門してみた

# はじめに
クラウドを低コストで運用できるAWS Serverless(API Gateway + Lambda + DynamoDB)に入門してみました。

![Serverless.png](https://qiita-image-store.s3.amazonaws.com/0/107056/decaf640-eac2-d43e-79a5-c04938f859ee.png)


基本的には、クラスメソッドさんの記事が分かりやすく参考になりました。  
[WS Serverless Application Model (AWS SAM) を使ってサーバーレスアプリケーションを構築する](http://dev.classmethod.jp/cloud/aws/aws-serverless-application-model/)

ただし、そのままでは動作しない部分があったので、今回は、その変更部分を中心に記載します。

# 環境
私の環境は以下です。

+ OS: Windows 10
+ AWS CLI: aws-cli/1.11.82 Python/2.7.9 Windows/8 botocore/1.5.45

Windows環境というのが参考にした記事との差分になります。

# ディレクトリ構成
ディレクトリ構成はこのようにしました。
![dir.png](https://qiita-image-store.s3.amazonaws.com/0/107056/1534710c-cdd8-d1d9-3f16-bf7ab71de1ec.png)


コマンドは、`C:\Git\AWS\Serverless\api_backend` で実行しました。

# Lambda
get, put, delete を１つのJavaScriptファイルにまとめて記載します。
テーブル名は環境変数から取得するようにしています。

```index.js
'use strict';

const AWS = require('aws-sdk');
const dynamo = new AWS.DynamoDB.DocumentClient();

const tableName = process.env.TABLE_NAME;

const createResponse = (statusCode, body) => {
    
    return {
        statusCode: statusCode,
        body: body
    };
};

exports.get = (event, context, callback) => {
    
    let params = {
        TableName: tableName,
        Key: {
            id: event.pathParameters.resourceId
        }
    };
    
    let dbGet = (params) => { return dynamo.get(params).promise() };
    
    dbGet(params).then( (data) => {
        if (!data.Item) {
            callback(null, createResponse(404, "ITEM NOT FOUND"));
            return;
        }
        console.log(`RETRIEVED ITEM SUCCESSFULLY WITH doc = ${data.Item.doc}`);
        callback(null, createResponse(200, data.Item.doc));
    }).catch( (err) => { 
        console.log(`GET ITEM FAILED FOR doc = ${params.Key.id}, WITH ERROR: ${err}`);
        callback(null, createResponse(500, err));
    });
};

exports.put = (event, context, callback) => {
    
    let item = {
        id: event.pathParameters.resourceId,
        doc: event.body
    };

    let params = {
        TableName: tableName,
        Item: item
    };
    
    let dbPut = (params) => { return dynamo.put(params).promise() };
    
    dbPut(params).then( (data) => {
        console.log(`PUT ITEM SUCCEEDED WITH doc = ${item.doc}`);
        callback(null, createResponse(200, null));
    }).catch( (err) => { 
        console.log(`PUT ITEM FAILED FOR doc = ${item.doc}, WITH ERROR: ${err}`);
        callback(null, createResponse(500, err)); 
    });
};

exports.delete = (event, context, callback) => {
    
    let params = {
        TableName: tableName,
        Key: {
            id: event.pathParameters.resourceId
        },
        ReturnValues: 'ALL_OLD'
    };
    
    let dbDelete = (params) => { return dynamo.delete(params).promise() };
    
    dbDelete(params).then( (data) => {
        if (!data.Attributes) {
            callback(null, createResponse(404, "ITEM NOT FOUND FOR DELETION"));
            return;
        }
        console.log(`DELETED ITEM SUCCESSFULLY WITH id = ${event.pathParameters.resourceId}`);
        callback(null, createResponse(200, null));
    }).catch( (err) => { 
        console.log(`DELETE ITEM FAILED FOR id = ${event.pathParameters.resourceId}, WITH ERROR: ${err}`);
        callback(null, createResponse(500, err));
    });
};
```

# テンプレート
```template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Simple CRUD webservice. State is stored in a SimpleTable (DynamoDB) resource.
Resources:
  GetFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.get
      Runtime: nodejs6.10
      Policies: AmazonDynamoDBReadOnlyAccess
      CodeUri: /Git/AWS/Serverless/api_backend/src
      Environment:
        Variables:
          TABLE_NAME: !Ref Table
      Events:
        GetResource:
          Type: Api
          Properties:
            Path: /resource/{resourceId}
            Method: get
 
  PutFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.put
      Runtime: nodejs6.10
      Policies: AmazonDynamoDBFullAccess
      CodeUri: /Git/AWS/Serverless/api_backend/src
      Environment:
        Variables:
          TABLE_NAME: !Ref Table
      Events:
        PutResource:
          Type: Api
          Properties:
            Path: /resource/{resourceId}
            Method: put
 
  DeleteFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.delete
      Runtime: nodejs6.10
      Policies: AmazonDynamoDBFullAccess
      CodeUri: /Git/AWS/Serverless/api_backend/src
      Environment:
        Variables:
          TABLE_NAME: !Ref Table
      Events:
        DeleteResource:
          Type: Api
          Properties:
            Path: /resource/{resourceId}
            Method: delete
 
  Table:
    Type: AWS::Serverless::SimpleTable
```

LambdaのJavaScriptファイルを配置したディレクトリを`CodeUri`として指定しました。
CodeUriはフルパスで指定する必要がありました。
CodeUriを相対パスで `CodeUri:src` のように指定するとLambdaの参照を解決できず、以下のエラーがでていました。

```
Unable to upload artifact src referenced by CodeUri parameter of GetFunction resource.
Parameter CodeUri of resource GetFunction refers to a file or folder that does not exist C:\Git\AWS\Serverless\api_backend\config\src
```

# package
packageとは、LambdaをS3にアップロードし、CloudFormationのテンプレート(template.yaml)を環境に合わせて実体化する作業を言います。

+ **Lambdaのアップロード**  
![package.png](https://qiita-image-store.s3.amazonaws.com/0/107056/5537e4a0-d98f-2f8f-b70c-3dc77377e514.png)  
　  
+ **Templateの実体化**  
![template.png](https://qiita-image-store.s3.amazonaws.com/0/107056/f354f655-f08c-b22c-bb2d-1362fa5d5035.png)
　  

+ **コマンド**  
事前にS3のバケット（<bucket-name>）を作成しておきます。
以下のコマンドの`<bucket-name>`部分を作成したS3のバケット名に置き換えてpackageコマンドを実行します。

```
aws cloudformation package --template-file config/template.yaml --output-template-file config/serverless-output.yaml --s3-bucket <bucket-name>
```

+ **実体化されたTemplate**  
packageコマンドを実行すると、以下のような実体化されたTemplateが生成されます。

```serverless-output.yam
AWSTemplateFormatVersion: '2010-09-09'
Description: Simple CRUD webservice. State is stored in a SimpleTable (DynamoDB) resource.
Resources:
  DeleteFunction:
    Properties:
      CodeUri: s3://<bucket-name>/a14396c7934ad2583710bf066a1340aa
      Environment:
        Variables:
          TABLE_NAME:
            Ref: Table
      Events:
        DeleteResource:
          Properties:
            Method: delete
            Path: /resource/{resourceId}
          Type: Api
      Handler: index.delete
      Policies: AmazonDynamoDBFullAccess
      Runtime: nodejs6.10
    Type: AWS::Serverless::Function
  GetFunction:
    Properties:
      CodeUri: s3://<bucket-name>/a14396c7934ad2583710bf066a1340aa
      Environment:
        Variables:
          TABLE_NAME:
            Ref: Table
      Events:
        GetResource:
          Properties:
            Method: get
            Path: /resource/{resourceId}
          Type: Api
      Handler: index.get
      Policies: AmazonDynamoDBReadOnlyAccess
      Runtime: nodejs6.10
    Type: AWS::Serverless::Function
  PutFunction:
    Properties:
      CodeUri: s3://<bucket-name>/a14396c7934ad2583710bf066a1340aa
      Environment:
        Variables:
          TABLE_NAME:
            Ref: Table
      Events:
        PutResource:
          Properties:
            Method: put
            Path: /resource/{resourceId}
          Type: Api
      Handler: index.put
      Policies: AmazonDynamoDBFullAccess
      Runtime: nodejs6.10
    Type: AWS::Serverless::Function
  Table:
    Type: AWS::Serverless::SimpleTable
Transform: AWS::Serverless-2016-10-31
```

# deploy
deployコマンドで、API Gateway, Lambda, DynamoDBのテーブルを実際に作成します。
stack-nameは、今回作成する一式を管理するための名前です。

```
aws cloudformation deploy --template-file config/serverless-output.yaml --stack-name serverless-api-backend-20170503 --capabilities CAPABILITY_IAM
```

CloudFormationのマネージメントコンソールでスタックが生成されていることが確認できます。

![CloudFormation.png](https://qiita-image-store.s3.amazonaws.com/0/107056/07f92325-5f3e-82c0-4222-81de6f786e6d.png)

ここで重要なのが `--capabilities CAPABILITY_IAM` オプションです。これは、LambdaからDynamoDBを呼び出したりするためのポリシー設定をCloudFrontに許可するかどうかの指定になります。これを忘れると以下のエラーメッセージになります。

```
An error occurred (InsufficientCapabilitiesException) when calling the ExecuteChangeSet operation: Requires capabilities : [CAPABILITY_IAM]
```


# Test
最後に作成したServerless Application ModelのテストとしてAPI GatewayからLambdaを呼び出し、DynamoDBを操作してみます。

API Gatewayのマネージメントコンソールで、`serverless-api-backend-20170503`を開きます。

リソース → /resource/{resourceId} → PUT → テスト

の順にクリックしてメソッドテストの画面を開き、メソッドテストの各項目には、次のように入力します。

| 項目 | 値 |
|:-----|:-----|
| パス |res001|
| クエリ文字列 |param1=value1&param2=value2|
| ヘッダー |Accept: application/json<br>Content-type: application/json|
| リクエスト本文 |{"message1":"Hello","message2":"Lambda"}|

最後に「テスト」ボタンを押下すると、DynamoDBのテーブルにitemが追加され、テストが成功します。

+ **DynamoDB**
![DynamoDB.png](https://qiita-image-store.s3.amazonaws.com/0/107056/e36c3d4c-9066-f2e9-38b2-7b843649ac30.png)