lambda＋API Gateway構成を作ってみる

lambdaとAPI辺りの学習です。
外部APIをコールするlambdaをコールするAPI(via API Gateway) を意味もなく作る備忘録。
それだけだとつまらないので天気予報を取得し、取ってきたjsonをそのままDynamoDBに格納する構成を作る。

_contents_

* Node.jsでAPIをコールする方法。
* AWS環境でAPI Gateway + lambdaの設定方法。
* ローカル環境でAPI Gateway + lambdaの設定・実行方法。

## 事前準備

* OpenWeatherにSign UpしてAPIキーを入手。
* AWS SSM パラメータストアへURIとキーを登録。

## SSMとDynamoDBのSDK(Node.js)の使い方

### Node.jsからパラメータストアを参照

```js
const AWS = require('aws-sdk');
const ssm = new AWS.SSM();
const res = ssm.getParameter({Name: "パラメータ名", WithDecryption: false});
//WithDecryptionは復号化の有無
```

### Node.jsからDynamoDBへ書き込み

```js
const AWS = require('aws-sdk');
const documentClient = new AWS.DynamoDB.DocumentClient({region: 'ap-northeast-1'});
const params = {
    TableName: 'テーブル名',
    Item: {
        '属性1': '値1',
        '属性2': '値2'
    }
}
documentClient.put(params, (err, data) => {
    if (err) console.log(err);
    else console.log(data);
});
```
## API Gatewayの設定

### コンソールから作成する場合

1. REST APIを選択。
2. API名を新規作成。対象となるlambdaを選択して指定。
3. [アクション]からメソッドの作成→POST
4. テスト⚡︎をクリックし、POSTするJSONメッセージを入力したらテストボタン押下。右側にレスポンスが表示される。なお、このままデプロイしてURIを発行しても良いが、この状態だとどこからでもアクセスし放題なためセキュリティ的に不安。
5. サイドバーの[リソース]から[メソッドリクエスト]を選択。APIキーの必要性を`true`に。
6. [アクション]からAPIのデプロイを選択→ステージ名を選択or新規作成。→URIが発行される。
7. サイドバーの[APIキー]からAPIキーを作成する。命名するのみ。
8. サイドバーの[使用量プラン]を選択し、`関連付けられたAPIステージ`に6で作成したAPI名とステージ名を設定。`APIキー`に8で作成したAPIキーを設定する。
9. postmanからPOST。キーを設定しないと`{"message":"Forbidden"}`が返り、`x-api-key`に指定して投げるとDynamoDBに記録され成功する。

## DynamoDBの設定

### AWS CLIから作成する場合

```bash:テーブル作成
$ aws dynamodb create-table --table-name 'テーブル名' \
--attribute-definitions '[{"AttributeName":"プライマリキー名","AttributeType": "S"}]' \
--key-schema '[{"AttributeName":"プライマリキー名","KeyType": "HASH"}]' \
--provisioned-throughput '{"ReadCapacityUnits": 5,"WriteCapacityUnits": 5}'
```

設定値についての参考：https://qiita.com/propella/items/75e1a68468e37c3ebf45

```bash:データ挿入
$ aws dynamodb put-item --table-name テーブル名 \
--item '{"date":{"N":"20200110"},"test":{"S":"TEST"}}'
```

なお、次のエラーが出るのは以下の場合が考えられます。

> Error parsing parameter '--item': Expected: '=', received: '"' for input:

* コマンドプロンプトから実行している場合。  
→ シングルクオートを使うからいけないらしい。中身のダブルクオートをエスケープして、  
`"{\"date\":{\"N\":\"20200110\"},\"test\":{\"S\":\"TEST\"}}"`。
* 外側の`{}`を忘れている場合。

### コード

```js:index.js
const AWS = require('aws-sdk');
const ssm = new AWS.SSM();
const documentClient = new AWS.DynamoDB.DocumentClient({region: 'ap-northeast-1'});
const https = require('https');
const url = require('url');

const date = new Date();

const getWeatherInfo = async function (latitude, longitude, context) {
    const ssmResultUri = await ssm.getParameter({Name: "OPENWEATHER_URI", WithDecryption: false}).promise();
    const ssmResultKey = await ssm.getParameter({Name: "OPENWEATHER_APPID", WithDecryption: true}).promise();
    const openWeatherUri = ssmResultUri.Parameter.Value;
    const appid = ssmResultKey.Parameter.Value;

    const endPoint = openWeatherUri + "?lon=" + longitude + "&lat=" + latitude + "&appid=" + appid;
    const options = url.parse(endPoint);
    options.method = 'GET';
    options.headers = {'Content-Type': 'application/json'};
    const req = https.request(options, (res) => {
        res.on('data', async (chunk) => {
            if (JSON.parse(chunk).cod == 200){
                const params = {
                    TableName: 'weather_forecast',
                    Item: {
                      'date': date.getTime(),
                      'forecast': JSON.parse(chunk)
                    }
                };
                console.log("write DB");
                await documentClient.put(params, (err, data) => {
                    if (err) console.log(err);
                    else console.log(data);
                }).promise();
                context.succeed("success");
            }
        });
    });

    req.on('error', (e) => {
        console.log('problem with request: ' + e.message);
        context.fail(e.message);
    });
    req.end();
};

exports.handler = function(event, context) {
    console.log("lambda start");
    getWeatherInfo(event.latitude, event.longitude, context);
    console.log("lambda end");
};
```

# localでAPIをテストする場合。

## SAMでローカルにlambdaの実行環境を作成。

```terminal
$ sam init --runtime nodejs10.x
```

## SAM CLI + Swagger

※ SAMからのapi-keyの設定は未対応らしい。
https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-specification-api-gateway-extensions.html

swagger.ymlファイルを作成しAPIを定義し、template.ymlから参照する。なお、`DefinitionUri`ではなく`Definitionbody`を使うならtemplate.yml内に続けて記述可能。
(swaggerについては、コンソールから作成したAPIがあればエクスポートすることができるためそれをベースに改良するほうが楽)

```yml:swagger.yml
swagger: "2.0"
info:
  description: "get weather info and insert to DynamoDB"
  version: "1.0.0"
  title: "get_weather_info"
basePath: "/dev"
schemes:
- "https"
paths:
  /:
    post:
      produces:
      - "application/json"
      responses:
        200:
          description: "200 response"
          schema:
            $ref: "#/definitions/Empty"
      security:
      - api_key: []
      x-amazon-apigateway-integration:
        uri: "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:getOpenWeatherInfo/invocations"
        responses:
          default:
            statusCode: "200"
        passthroughBehavior: "when_no_match"
        httpMethod: "POST"
        contentHandling: "CONVERT_TO_TEXT"
        type: "aws"
definitions:
  Empty:
    type: "object"
    title: "Empty Schema"
```

```yml:template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: call open weather API function

Globals:
  Function:
    Timeout: 10

Resources:
  getOpenWeatherInfo:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: getOpenWeatherInfo/
      Handler: index.handler
      Runtime: nodejs10.x
      Role: 'arn:aws:iam::xxxxxxxxxxxx:role/getWeatherInfoRole'
      Events:
        Api:
          Type: Api
          Properties:
            Path: /dev
            Method: post

  getWeatherInfo:
    Type: AWS::Serverless::Api
    Properties:
        StageName: dev
        DefinitionUri: swagger.yml
```

ローカルのlambdaをAPIコールするには`AWS::Serverless::Api`の設定ではなく`AWS::Serverless::Function`の`Events:`以下が重要。これがないと以下のエラーを生じる。

> Error: Template does not have any APIs connected to Lambda functions

## ローカルにAPIを立てる

```
$ sam build
$ sam local start-api
Mounting getOpenWeatherInfo at http://127.0.0.1:3000/dev [POST]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2020-01-11 14:31:44  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
```

※追記

実際同じコードを用いてローカルのAPIを呼ぶと以下のエラーが出る。指示通りステータスコード等を含むオブジェクトをreturnしてあげると解決する。
localでやる場合とAWS環境でやる場合でレスポンスに求められる制限が違うのだろうか？

> Function returned an invalid response (must include one of: body, headers, multiValueHeaders or statusCode in the response object). Response received: null