SAMでAPIGateWay＋Lambdaのサーバーレスアプリを作って見た 



# 概要
AWS SAMはサーバーレスアプリケーションを定義するモデルで、その定義に従ってAWS上に環境構築してくれるCloudFormationのサーバーレス版です。
CloudFormationでも同じことができますが、権限周りや設定すべき項目が桁違いに増えるので、LambdaやAPIGateWayを使ったサーバーレス環境を構築するときはSAM一択になると思います。

今回はこのSAMを使って、よく見かけるサーバーレスアプリを構成して見ます。

# 作成内容
簡単なツイートアプリを作ります。
できることはツイートと過去のツイート一覧表示のみです。誰からもツイートは見られないし他の人のを見ることもできません。シンプルイズベスト。
ソースコードはこちらhttps://github.com/YuuSatoh/sam-lambda

# 説明することしないこと
* 説明すること
    * SAMでサーバーレスアプリを作った話
    * 実装の簡単な説明
* 説明しないこと
    * SAMのインストール方法とか使い方とか細かい話

# アーキテクチャ

S3に置いたHTMLファイルをウェブサイトホスティングして、バックエンドはAPIGateWay＋Lambdaのサーバーレス。DBはDynamoDBを使用します。
![Web App Reference Architecture.png](https://qiita-image-store.s3.amazonaws.com/0/257378/190c9040-5ff9-2e1b-cc43-92746fba341a.png)



## CORSの有効化
SAMでLambdaの呼び出し元をAPIGateWayにすると、構築されたAPIGateWayはデフォルトでLambdaプロキシ統合の使用にチェックが入ってしまう
（リファレンス見ても外し方がわからなかった…誰か知ってたら教えてください…）

そのためAPIGateWayでCORSを有効化するだけではうまくいきませんでした。
<img width="1053" alt="スクリーンショット 2018-09-12 17.42.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/257378/fb63dea6-f769-edfd-7e0c-eb88889178af.png">
<img width="1932" alt="スクリーンショット 2018-09-12 18.22.36.png" src="https://qiita-image-store.s3.amazonaws.com/0/257378/77280c12-f160-5eb7-6860-508855e4a335.png">



Lambdaプロキシ統合はAPIGateWayがこれまで丸めていたHTTPヘッダーの情報をLambdaに丸ごと送るからそっちでいい感じにしてね！となっているようなので、Lambdaの方でCORSを有効化する処理を入れる必要がありました。

結果ヘッダーに"Access-Control-Allow-Origin”を入れるだけだったけどCORSは結構ハマって辛かった…


# コード
## template.yaml
```
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: "サーバーレスハンズオン"

Globals:
    Function:
        Environment: 
            Variables:
                TABLE_NAME: 
                    !Ref HandsOnTable
        Runtime: nodejs8.10
    

Resources:
    GetFunction:
        Type: AWS::Serverless::Function 
        Properties:
            CodeUri: functions/src/
            Handler: index_get.handler
            Policies: AmazonDynamoDBReadOnlyAccess
            Events:
                getAPI:
                    Type: Api
                    Properties:
                        Path: /tweet
                        Method: get
    PostFunction:
        Type: AWS::Serverless::Function 
        Properties:
            CodeUri: functions/src/
            Handler: index_post.handler
            Policies: AmazonDynamoDBFullAccess
            Events:
                postAPI:
                    Type: Api
                    Properties:
                        Path: /tweet
                        Method: post
    HandsOnTable:
        Type: AWS::Serverless::SimpleTable
        Properties:
            PrimaryKey:
                Name: tweetId
                Type: String
            ProvisionedThroughput:
                ReadCapacityUnits: 5
                WriteCapacityUnits: 5

Outputs:
    HandsOn:
        Description: "API Gateway endpoint URL for Prod stage for HandsOn function"
        Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/tweet/"


```
* Resources:
	* LambdaやDynamoDBといったリソースを設定するところ（そのまんま）
	* CodeUri
		* Lambdaのソースコードを相対パス指定する
		* packaged.yamlに変換後S3にアップロードされたソースへのパスが自動的に入る
	* パスとかメソッドとかハンドラーとかポリシーとかは見たまんま
* Outputs:
	* CloudFormationの出力欄に表示されるもの
	* APIGateWayのエンドポイントを出力するようにしているのでAPIGateWayに見に行かなくていい。便利
* Globals:
	* リソースに共通するところをまとめて書ける
		* ランタイムとか環境変数とか
	* Resources:で同じパラメータを指定すると上書きされる


## Lambda(index_get.js)
```
'use strict';

var aws = require('aws-sdk');
var docClient = new aws.DynamoDB.DocumentClient({ region: 'ap-northeast-1' });

exports.handler = function (event, context, callback) {
    docClient.scan({ TableName: process.env.TABLE_NAME }, function (err, data) {
        if (err) {
            context.fail(err);
        } else {
            var response = {
                statusCode: 200,
                headers: {
                    "Access-Control-Allow-Origin": "*"
                },
                body: JSON.stringify(data),
                isBase64Encoded: false
            };
            callback(null, response);
        }
    });
};
```
DynamoDBからツイートデータを取得するコード。DynamoDBにフルスキャンかけてます。
レスポンスにはCORSを有効化するためヘッダーに許可するリソースを指定します。今回はとりあえず全てを許可する設定。


## Lambda(index_post.js)
```

'use strict';

var aws = require('aws-sdk');
var docClient = new aws.DynamoDB.DocumentClient({ region: 'ap-northeast-1' });

exports.handler = function (event, context, callback) {
  var requestBody = JSON.parse(event.body);
  var message = decodeURIComponent(requestBody.message);
  var item = {
    tweetId: new Date().getTime().toString(),
    message: message
  };

  var params = {
    TableName: process.env.TABLE_NAME,
    Item: item
  };

  docClient.put(params, function (err, data) {
    if (err) {
      console.log(err);
    } else {
      var response = {
        statusCode: 200,
        headers: {
          "Access-Control-Allow-Origin": "*"
        },
        body: JSON.stringify(data),
        isBase64Encoded: false
      };
      console.log("Success:", data);
      callback(null, response);
    }
  });
};
```

こちらはDynamoDBにツイートデータをputするコードです。DynamoDBにputしてるだけ。
こちらも同様にCORSを許可するヘッダーを付加

## index.html(一部)

```
<script language="javascript">
    // tweetのget
    $.ajax({
      type: 'get',
      url: "APIGatewayURL Change Todo",
      success: function (res) {
        res.Items.forEach(function (item) {
          var tweet = `
            <div class="popover right show" style="position:relative; max-width:100%;">
            <div class="arrow"></div>
            <div class="popover-content">${item.message}</div>
          </div>`;
          $("#tweets").append(tweet);
        });
      }
    });
    // tweetのpost
    function post() {
      var $form = $('#tweet-form');
      var JSONdata = {
        message: $('#message').val()
      };

      $.ajax({
        url: $form.attr('action'),
        type: $form.attr('method'),
        data: JSON.stringify(JSONdata),
        timeout: 10000,
        complete: function (xhr, textStatus) {
          window.location.reload(); // post完了後に画面のリロード
        },
      });
    }
  </script>

```

HTMLファイルのJavaScript部分はこんな感じ
Ajaxで非同期にツイートのgetとpostをしてます。
通信先のurlはCloudFormationの出力に表示されるAPIGateWayのエンドポイントを入れてあげます

# 動作確認

1.　SAMでパッケージしてデプロイ
2.　HTMLのツイートを投稿/取得するURLをAPIGateWayのエンドポイントに修正
3.　S3にHTMLをアップロードしてウェブサイトホスティングして公開

<img width="589" alt="スクリーンショット 2018-09-12 18.28.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/257378/4ece908a-5b1c-115f-d1e4-94d846ad3149.png">

#　コマンド(備忘録)
* パッケージ化
    * sam package --template-file template.yaml --s3-bucket "s3BucketName" --output-template-file packaged.yaml
* デプロイ
    * sam deploy --template-file packaged.yaml --stack-name ServerlessHandsON --capabilities CAPABILITY_IAM