AWS SAMを使って簡単なWebページを作ってみる


AWS Summitに参加して，AWS SAMの存在を知りました．
コマンドラインベースでとても簡単にサーバーレスなバックエンドを作れるようです．
面白そうだったのでSAMを使って簡単なWebページを作ってみようと思います．

### 環境

|OS|macOS 10.13.4|
|:---:|:---:|
|python|2.7.15|
|nodejs|9.5.0|
|npm|6.1.0|

※python,nodejs,npmのインストールは省略します．

### SAMのインストール

```bash
$ pip install aws-sam-cli

$ sam --version
SAM CLI, version 0.3.0
```
pip3でインストールするとsamコマンドの実行時にエラーが出ます．．
どうやら[python3には対応していないとのこと](https://github.com/awslabs/aws-sam-cli/issues/426)

### アプリケーションの作成

runtimeオプションは利用する言語に合わせてください．

```bash
$ sam init --runtime nodejs
```

`sam-app`というディレクトリが作成されているはずです．

```bash
$ ls sam-app
README.md	hello_world	template.yaml
```

`hello_world`以下にlambda関数のコードやテストコード等が作成されています．

### npmパッケージのインストール

```bash
$ cd sam-app/hello_world
$ npm install
```

`hello_world`フォルダにある`package.json`に沿ってパッケージがインストールされます．

### ユニットテストの実行

```bash
$ npm test

~
  Tests index
    ✓ verifies successful response (444ms)

  1 passing (451ms)
```

`sam init`で作成されたテストが実行され，passしていることが確認できます．
自分はまだどのようなテストが行われているのか具体的に把握できていないのが課題．

### ローカルでのlambda実行

まずはlambda実行のトリガーとなるイベントファイルを作ります．

```bash
$ sam local generate-event api > event_file.json
$ head event_file.json
{
    "body": "{ \"test\": \"body\"}", 
    "httpMethod": "POST", 
    "resource": "/{proxy+}", 
    "queryStringParameters": {
        "foo": "bar"
    }, 
    "requestContext": {
        "httpMethod": "POST", 
        "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 
```

作成したファイルを使ってlambdaを実行します．

```bash
$ sam local invoke HelloWorldFunction --event event_file.json

Error: Template file not found at /Users/tocomi/Documents/aws/aws-workspace/sam/sam-app/hello_world/template.yml
```

...あれ．`template.yml`がないと言われている．
`template.yml`は一つ上の階層にあるのでそちらで実行すれば良さそう．

```bash
$ cd ..
$ sam local generate-event api > event_file.json
$ sam local invoke HelloWorldFunction --event event_file.json

Error: Running AWS SAM projects locally requires Docker. Have you got it installed?
```

今度はDockerのインストールが必要と言われる．．
Dockerのインストールは[こちら]((https://qiita.com/kurkuru/items/127fa99ef5b2f0288b81))を参考にさせていただきました．

気を取り直してもう一度実行．

```bash
$ sam local invoke HelloWorldFunction --event event_file.json

~
START RequestId: e57f82e4-466d-1d8d-ba9f-202b9d91b190 Version: $LATEST
END RequestId: e57f82e4-466d-1d8d-ba9f-202b9d91b190
REPORT RequestId: e57f82e4-466d-1d8d-ba9f-202b9d91b190	Duration: 1208.06 ms	Billed Duration: 1300 ms	Memory Size: 128 MB	Max Memory Used: 35 MB	

{"statusCode":200,"body":"{\"message\":\"hello world\",\"location\":\"221.184.69.27\"}"}
```

やっと成功！

### ローカルAPI経由での実行

今度はAPI経由での実行をローカルで行います．

```bash
$ sam local start-api
2018-06-02 02:47:58 Mounting HelloWorldFunction at http://127.0.0.1:3000/hello [GET]
~

$ curl http://127.0.0.1:3000/hello
{"message":"hello world","location":"221.184.69.27"}
```

ちゃんとレスポンスが返ってきました．うれしい．

### デプロイ

いよいよAWS上にデプロイをしていきます．
...の前に，一応テンプレートの検証をしておきます．

```bash
$ sam validate
2018-06-02 10:49:58 Found credentials in shared credentials file: ~/.aws/credentials
2018-06-02 10:49:59 Starting new HTTPS connection (1): iam.amazonaws.com
/Users/tocomi/Documents/aws/aws-workspace/sam/sam-app/template.yaml is a valid SAM Template
```

`aws configure`を設定していないとおそらくここで怒られる．[設定方法](https://qiita.com/n0bisuke/items/1ea245318283fa118f4a)

問題なければ次にパッケージングをします．
S3バケットを事前に作成しておき，そのバケット名を`bucketname`で指定します．

```bash
$ sam package --template-file template.yaml --s3-bucket <bucketname> --output-template-file packaged.yaml

Uploading to b757548be1b347e234e4b5ec2def5946  956345 / 956345.0  (100.00%)
Successfully packaged artifacts and wrote output template to file packaged.yaml.
```

これでS3にモジュールのパッケージがアップロードされました．

パッケージを使ってcloudformationでデプロイをします．
`stack-name`は任意の名前で登録してください．
(`--capabilities CAPABILITY_IAM`の部分が何を意味するのかわからないので後で調べる)

```bash
$ sam deploy --template-file packaged.yaml --stack-name sam-sample-stack --capabilities CAPABILITY_IAM

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - sam-sample-stack
```

実際にデプロイされたAPIを叩いてみます．

```bash
$ aws cloudformation describe-stacks --stack-name sam-sample-stack --query 'Stacks[].Outputs[1]'

[
    {
        "OutputKey": "HelloWorldApi",
        "OutputValue": "https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/",
        "Description": "API Gateway endpoint URL for Prod stage for Hello World function"
    }
]

# OutputValueのURLにアクセスする
$ curl https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
{"message":"hello world","location":"52.199.27.92"}
```

ちゃんとレスポンス返ってきました．

### DynamoDBとの連携

DynamoDBのレコードをレスポンスとして返すようにしてみます．
template.yamlに

- DynamoDBのテーブル情報
- DynamoDBへのアクセス権限

を追加します．

```yml:template.yaml
Resources:
  HelloWorldFunction:
  ~
    Policies: AmazonDynamoDBFullAccess
      
  ~
  Records:
    Type: AWS::Serverless::SimpleTable
    Properties:
      PrimaryKey:
        Name: id
        Type: String
      ProvisionedThroughput:
        ReadCapacityUnits: 5
        WriteCapacityUnits: 5
```

ここで一度，`sam package`と`sam delploy`を行いました．
DynamoDBにテーブルが作成されているのを確認して，テストデータを追加します．
idに加えてname属性を加えています．

```json
[
  {"id":"1","name":"EZ DO DANCE"},
  {"id":"2","name":"BOY MEETS GIRL"},
  {"id":"3","name":"survival dAnce"}
]
```

続いてlambda関数を修正します．
`tableName`にはDynamoDBに作られたテーブル名を指定します．(これ動的に指定できないのかな)

```javascript:app.js
'use strict'

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

const tableName = 'sam-sample-stack-Records-XXXXXXXXXX';

exports.lambda_handler = (event, context, callback) => {

  const params = {
    TableName: tableName
  };

  docClient.scan(params, function(err, data) {
    if (err) {
      callback(err, null);
      return;
    }
    var response = {
      'statusCode': 200,
      'headers': { // headerを設定しないとajax経由でデータを取得するときにエラーが出る
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin' : '*'
      },
      'body': JSON.stringify(data.Items)
    }
    callback(null, response);
  });
};
```

再度デプロイしたら，APIを叩いてみます．

```bash
$ curl https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/

[{"id":"2","name":"BOY MEETS GIRL"},{"id":"1","name":"EZ DO DANCE"},{"id":"3","name":"survival dAnce"}]
```

登録したレコードが表示されました！

### データを表示するページを作る

フロントにはVue.js，ajax通信にはaxiosを使ってます．

```html:index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>SAM Sample App</title>
  <!--[if IE]>
  <script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
  <![endif]-->
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
  <div id="app">
    <ul v-for="record in records">
      <li>{{ record.id }} - {{ record.name }}</li>
    </ul>
  </div>
</body>
<script>
  var app = new Vue({
    el: '#app',
    data: {
      records: [],
    },
    created: function() {
      axios.get('https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/').then(
        res => {
          this.records = res.data
        }
      )
    },
  })
</script>
</html>
```

`index.html`をブラウザから開くと，DynamoDBに登録したレコードがリストで表示されるはずです．
デプロイのときに`index.html`もS3にアップロードしてそこから参照できるようにしたいけど，どうすればよいのだろうか．

### おわりに
レコードの追加・削除とかもできればいいかなと思ったけど，長くなっちゃったのと疲れたのでここまで．

### 参考
- [aws-sam-local 改め aws-sam-cli の新機能 sam init を試す](https://qiita.com/hayao_k/items/841026f9675d163b58d5)
- [DockerをMacにインストールする](https://qiita.com/kurkuru/items/127fa99ef5b2f0288b81)
- [AWS CLIのインストールから初期設定メモ](https://qiita.com/n0bisuke/items/1ea245318283fa118f4a)
- [Cross-Origin設定](https://qiita.com/aiwas/items/116a1039558bec1c5edd)
- [サーバーレス公式ドキュメント](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/serverless_app.html)
- [DynamoDBリファレンス](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html)