Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

準備

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 コマンドを使って簡単なアプリを開発、デプロイします。

okamuuu
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした