Single Page Application を Serverless Framework と React でやる Tutorial(3)

今回の記事では DynamoDB を Lambda 関数で呼び出してデプロイするまでを解説します。まずはローカル環境で以下の API を準備します。

- GET http://localhost:5000/api/articles
- GET http://localhost:5000/api/articles/:id
- PUT http://localhost:5000/api/articles/:id/favorite
- PUT http://localhost:5000/api/articles/:id/unfavorite

## 準備

```
sls create --template aws-nodejs --path serverless-tutorial-three && cd $_
yarn init -y
yarn add aws-sdk express serverless-http --save
yarn add serverless-offline serverless-dynamodb-local facker --dev
mkdir migrations
touch migrations/articles.json
```

## DynamoDB

create `migratios/articles.json`

```
[
{
  "id": "1",
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "description": "quia et suscipit suscipit recusandae consequuntur expedita et cum reprehenderit molestiae ut ut quas totam nostrum rerum est autem sunt rem eveniet architecto",  
  "isFavorite": false
},
{
  "id": "2",
  "title": "qui est esse",
  "description": "est rerum tempore vitae sequi sint nihil reprehenderit dolor beatae ea dolores neque fugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis qui aperiam non debitis possimus qui neque nisi nulla",  
  "isFavorite": true
}
]
```

`edit serverless.yml`

```
service: serverless-tutorial-trhee
plugins:
   - serverless-dynamodb-local
   - serverless-offline

custom:
  dynamodb:
    start:
      port: 8000
      inMemory: true
      migrate: true
      seed: true
    seed:
      development:
        sources:
          - table: articles
            sources: [./migrations/articles.json]

resources:
  Resources:
    ArticlesTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: articles
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1

provider:
  name: aws
  runtime: nodejs6.10
  stage: dev
  region: ap-northeast-1

functions:
  app:
    handler: handler.main
    events:
      - http:
          method: ANY 
          path: '/' 
          cors: true
      - http:
          method: ANY 
          path: '{proxy+}'
          cors: true
```

install and run local dynamoDB

```
sls dynamodb install
sls dynamodb start
```

## Lambda

edit `handler.js`

```
const serverless = require('serverless-http');
const express = require('express');
const app = express();

const aws = require('aws-sdk');

const localDocClient = new aws.DynamoDB.DocumentClient({
  region: 'ap-northeast-1',
  endpoint: "http://localhost:8000"
});

const docClient = new aws.DynamoDB.DocumentClient({
  region: 'ap-northeast-1',
});

function getDocClientByIP(ip) {
  return ip === "127.0.0.1" ? localDocClient : docClient;
}

app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "http://localhost:3000")
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
  res.header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS")
  next()
});

app.get('/api/articles', (req, res) => {

  const docClient = getDocClientByIP(req.ip);

  docClient.scan({
    TableName: 'articles',
    Limit: 100
  }).promise().then(result => {
    res.json({ articles: result.Items });
  });
});

app.get('/api/articles/:id', (req, res) => {

  const docClient = getDocClientByIP(req.ip);

  const getResult = docClient.get({
    TableName: 'articles',
    Key: {
      id: req.params.id,
    }
  }).promise().then(result => {
    res.json({ article: result.Item });
  })
});

app.put('/api/articles/:id/favorite', (req, res) => {

  const docClient = getDocClientByIP(req.ip);

  docClient.update({
    TableName: 'articles',
    Key: {
      id: req.params.id,
    },
    UpdateExpression: "set isFavorite = :val",
    ExpressionAttributeValues:{
      ":val": true
    },
    ReturnValues: "UPDATED_NEW"
  }).promise().then(result => {
    res.json(result);
  });
});

app.put('/api/articles/:id/unfavorite', (req, res) => {

  const docClient = getDocClientByIP(req.ip);

  docClient.update({
    TableName: 'articles',
    Key: {
      id: req.params.id,
    },
    UpdateExpression: "set isFavorite = :val",
    ExpressionAttributeValues:{
      ":val": false
    },
    ReturnValues: "UPDATED_NEW"
  }).promise().then(result => {
    res.json(result);
  });
});

module.exports.main = serverless(app);
```

`serverless offline` を実行してから以下のコマンドの動作確認をします。

```
BASE_URL="http://localhost:3000"
curl -s -X GET "${BASE_URL}/api/articles"
curl -s -X GET "${BASE_URL}/api/articles/1" | jq .
curl -s -X PUT "${BASE_URL}/api/articles/1/favorite"
curl -s -X GET "${BASE_URL}/api/articles/1" | jq .article.isFavorite
curl -s -X PUT "${BASE_URL}/api/articles/1/unfavorite"
curl -s -X GET "${BASE_URL}/api/articles/1" | jq .article.isFavorite
```

というわけで local で動かすことができました。

## 解説

### getDocClientByIP

local 環境化では DynamoDB の endopoint を local に向けて作業します。
AWS 上では AWS の DynamoDB を参照します。この場合は express の res.ip で local かどうかを判定しています。

```
const localDocClient = new aws.DynamoDB.DocumentClient({
  region: 'ap-northeast-1',
  endpoint: "http://localhost:8000"
});

const docClient = new aws.DynamoDB.DocumentClient({
  region: 'ap-northeast-1',
});

function getDocClientByIP(ip) {
  return ip === "127.0.0.1" ? localDocClient : docClient;
}
```

### CORS

handler.js で全てのリクエストに対して以下のようにレスポンスヘッダーを追加します。

```
app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*")
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
  res.header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS")
  next()
})
```

それと同時に `serverless.yml` の functions に `cors: true` を追加します。

```
functions:
  app:
    handler: handler.main
    events:
      - http:
          method: ANY 
          path: '/' 
          cors: true
      - http:
          method: ANY 
          path: '{proxy+}'
          cors: true
```

他にも `serverless-cors-plugin` を使ったり、そもそも同一ドメインで動作させれば良い気もしますが Route53 などはドメインが必要になる(勘違いだったらすいません)ためちょっと試したい場合に不都合だと思うので今回の Tutorial ではこのやり方を紹介しています。

## Deploy

上記で動作確認したら以下を実行します。

```
serverless deploy
```

deploy が完了すると endpoint が表示されるので以下のようにして動作確認をします。

```
BASE_URL="https://XXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev"
curl -s -X GET "${BASE_URL}/api/articles"
```

もし `{"message": "Internal server error"}` といったエラーメッセージ等が表示されたら以下のようにしてエラーログを確認します。ちなみにログは少し間を置かないと最新のログがとれないようです。

```
sls logs -f app  
```

おそらく `AccessDeniedException: User: dynamodb:Scan on resource` のようなエラーがでてると思います。serverless.yml の provider に IAM の設定を追加します。

```
provider:
  name: aws
  runtime: nodejs6.10
  stage: dev
  region: ap-northeast-1
  environment:
    DYNAMODB_TABLE: articles
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}"
```

改めて `sls deploy` した後以下を実行します。結果が空になっていますがこれは正しい挙動です。 custom.dynamodb.seed は local の dynadmodb の設定だからです。

```
BASE_URL="https://XXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev"
curl -s -X GET "${BASE_URL}/api/articles"
{"articles":[]}
```

以下のようなスクリプトを作成して DynamoDB にデータを挿入してみましょう。

`create faker.js`

```
const faker = require('faker');
const aws = require('aws-sdk');

const docClient = new aws.DynamoDB.DocumentClient({
  region: 'ap-northeast-1',
});

async function main() {

  const articles = []; 

  for (var id = 1; id < 51; id++) {
    const result = await docClient.put({
      TableName: 'articles',
      Item: {
        "id": id + "", 
        "title": faker.lorem.words(),
        "description": faker.lorem.paragraphs(),
        "isFavorite": false
      }   
    }).promise();
  }
}

main();
```


動作確認が終わったら以下で remove しておきます。

```
sls remove
```

### まとめ

Serverless Framework を使って API Gateway + Lambda + Dynamodb の開発をローカルで行ない、すぐにデプロイする方法を学びました。

次回は create-react-app コマンドと serverless コマンドを使って簡単なアプリを開発、デプロイします。