Serverless アプリケーションをローカルで開発する

AWSに代表されるServerless Architectureはクラウド上での動作が前提ですが、Serverless Frameworkのプラグインを用いることにより、ローカル環境でも動作させることが可能になるのでご紹介します。AWSにデプロイすることなく開発が可能になるので、より素早く開発ができます。また、AWSのアカウントを持っていない方もServerlessの世界を体験できるかと思います。ここではじゃんけんを行うAPIの開発を通して、ローカルでの開発方法を説明します。完成版のソースコードは以下にあります

* [noralife/serverless-janken](https://github.com/noralife/serverless-janken)


## 構成

API Gateway、Lambda、DynamoDBを用いたアーキテクチャをここでは想定します。ローカル開発環境ではそれぞれ serverless-offline、javascriptファイル、DynamoDB Local が対応します。

![Screen Shot 2016-12-23 at 14.55.06.png](https://qiita-image-store.s3.amazonaws.com/0/27509/b36f68ce-5e37-b597-2eae-0e67bd06d8dd.png "Screen Shot 2016-12-23 at 14.55.06.png")



## 環境

* macOS sierra
* Node.js v4.6.2
* Serverless Framework v1.20.1

## プロジェクトの作成

Serverless Frameworkを用いて開発するのでインストールを行い、新しいサービスを作成します。

```bash
$ npm install -g serverless
$ mkdir serverless-janken
$ sls create -t aws-nodejs -n serverless-janken
```

## 関連するパッケージのインストール

API Gatewayの代用として利用する **serverless-offline** プラグイン、Serverless FrameworkからDynamoDB Localを操作できるようにする **serverless-dynamodb-local** プラグインをインストールします。

```bash
$ npm install aws-sdk
$ npm install --save-dev serverless-offline
$ npm install --save-dev serverless-dynamodb-local
```

インストール後、Serverless Frameworkからプラグインとして利用できるように設定に記入します。

```bash
$ vi serverless.yml
# service: serverless-janken の下に以下を追記
plugins: 
 - serverless-dynamodb-local
 - serverless-offline
```

## DynamoDB Local のインストール

**serverless-dynamodb-local** プラグインを利用してDynamoDB Localをインストールします。

```bash
$ sls dynamodb install
```

## DynamoDB Local テーブルの定義

DynamoDB Localで利用するテーブルを定義します。サンプルとして、プレイヤー名とUnixtimeをキーとするテーブルを作成しました。

```bash
$ mkdir migrations
$ vi migrations/jankens.json
# 下記内容で保存する
[
    {
        "player": "user1",
        "unixtime": 1482418800,
        "player_hand": "rock",
        "computer_hand": "paper",
        "judge": "lose"
    }
]
```

## DynamoDB Local の起動

DynamoDB Local起動時にテーブルの作成とシードデータの挿入を行うため、 `serverless.yml` に設定を入れます。

```serverless.yml
# service: serverless-janken の下に以下を追記
custom:
  dynamodb:
    stages:
      - dev
    start:
      port: 8000
      inMemory: true
      migrate: true
      seed: true
    seed:
      development:
        sources:
          - table: jankens
            sources: [./migrations/jankens.json]

resources:
  Resources:
    JankensTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: jankens
        AttributeDefinitions:
          - AttributeName: player
            AttributeType: S
          - AttributeName: unixtime
            AttributeType: N
        KeySchema:
          - AttributeName: player
            KeyType: HASH
          - AttributeName: unixtime
            KeyType: RANGE
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
```

DynamoDB Localを起動します。 

```bash
$ sls dynamodb start
Dynamodb Local Started, Visit: http://localhost:8000/shell

Serverless: DynamoDB - created table jankens
Seed running complete for table: jankens
```

ブラウザで **http://localhost:8000/shell** にアクセスし、テーブルの中身を確認します。左側のエディタに下記を記入し、再生ボタンを押します。


```javascript
var params = {
    TableName: 'jankens',
};
dynamodb.scan(params, function(err, data) {
    if (err) ppJson(err);
    else ppJson(data);
});
```

図のように右側にシードデータの内容が確認できれば、テーブルの作成＆シードの挿入が完了しています。

![Screen Shot 2016-12-23 at 15.08.59.png](https://qiita-image-store.s3.amazonaws.com/0/27509/7a3fe40c-3101-084d-86a9-8f61ab2d22c0.png "Screen Shot 2016-12-23 at 15.08.59.png")

## Lambdaの開発

データベースの設定ができたので、ロジック部分を書いていきます。`handler.js` を開いて以下の内容で保存します。じゃんけんを行うAPI `playJanken` とじゃんけん結果を参照するAPI `listJankens` のためのロジックを書いています。このコードはAWSでもローカルでも動作が可能なように、 `process.env.IS_OFFLINE` を見て接続するDynamoDBを切り替えています。

```hander.js
"use strict";

var AWS = require("aws-sdk");

var judgeJanken = function (a, b) {
    var c = (a - b + 3) % 3;
    if (c === 0) return "draw";
    if (c === 2) return "win";
    return "lose";
}

var getDynamoClient = function (event) {
    var dynamodb = null;
    if (process.env.IS_OFFLINE) {
        dynamodb = new AWS.DynamoDB.DocumentClient({
            region: "localhost",
            endpoint: "http://localhost:8000"
        });
    } else { 
        dynamodb = new AWS.DynamoDB.DocumentClient();
    }
    return dynamodb;
}

module.exports.playJanken = function (event, context, callback) {
    console.log("Received event:", JSON.stringify(event, null, 2));
    console.log("Received context:", JSON.stringify(context, null, 2));

    var dynamodb    = getDynamoClient(event);
    var date        = new Date();
    var unixtime    = Math.floor(date.getTime() /1000);

    var hand        = ["rock", "scissors", "paper"];
    var player_name = event.queryStringParameters.name;
    var player_hand = event.queryStringParameters.hand;
    var player      = hand.indexOf(player_hand);
    var computer    = Math.floor( Math.random() * 3) ;
    var judge       = judgeJanken(player, computer);

    var params = {
        TableName: "jankens",
        Item: {
            player: player_name,
            unixtime: unixtime,
            player_hand: player_hand,
            computer_hand: hand[computer],
            judge: judge
        }
    };

    dynamodb.put(params, function(err) {
        var response = {statusCode: null, body: null};
        if (err) {
            console.log(err);
            response.statusCode = 500;
            response.body = {code: 500, message: "PutItem Error"};
        } else {
            response.statusCode = 200;
            response.body = JSON.stringify({
                player: player_hand,
                computer: hand[computer],
                unixtime: unixtime,
                judge: judge
            });
        }
        callback(null, response);
    });
};

module.exports.listJankens = function (event, context, callback) {
    console.log("Received event:", JSON.stringify(event, null, 2));
    console.log("Received context:", JSON.stringify(context, null, 2));

    var dynamodb = getDynamoClient(event);
    var params   = { TableName: "jankens" };

    dynamodb.scan(params, function(err, data) {
        var response = {statusCode: null, body: null};
        if (err) {
            console.log(err);
            response.statusCode = 500;
            response.body = {code: 500, message: "ScanItem Error"};
        } else if ("Items" in data) {
            response.statusCode = 200;
            response.body = JSON.stringify({jankens: data["Items"]});
        }
        callback(null, response);
    });
};

```

## API Gatewayの設定

最後に前項で作成したロジックを呼ぶエンドポイントを作成するために `serverless.yml` に設定を入れます。

* GET /jankens... じゃんけん結果の参照
* POST /jankens... じゃんけんを行い結果をDynamoDB Localに保存

```serverless.yml
service: serverless-janken

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

plugins:
  - serverless-dynamodb-local
  - serverless-offline

provider:
  name: aws
  runtime: nodejs4.3

functions:
  playJanken:
    handler: handler.playJanken
    events:
      - http:
          path: jankens
          method: post
  listJankens:
    handler: handler.listJankens
    events:
      - http:
          path: jankens
          method: get

resources:
  Resources:
    JankensTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: jankens
        AttributeDefinitions:
          - AttributeName: player
            AttributeType: S
          - AttributeName: unixtime
            AttributeType: N
        KeySchema:
          - AttributeName: player
            KeyType: HASH
          - AttributeName: unixtime
            KeyType: RANGE
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
```

## テスト

以上で、データベース、ロジック、エンドポイントがそろったのでローカルで起動させて利用してみます。

```
$ sls offline
```

別のシェルでcurlでAPIを叩いて利用してみます。うまくいかない場合は `sls dynamodb start` でDynamoDB Localが起動していることを確認してください。

```bash
$ curl 'http://localhost:3000/jankens?hand=rock&name=test' -X POST
{"player":"rock","computer":"scissors","unixtime":1482469235,"judge":"win"}
$ curl 'http://localhost:3000/jankens'
{"jankens":[{"unixtime":1482469235,"player_hand":"rock","judge":"win","player":"test","computer_hand":"scissors"},{"unixtime":1482418800,"player_hand":"rock","judge":"lose","player":"user1","computer_hand":"paper"}]}
```

## AWSにデプロイ

ローカルで開発ができたら、AWS上にデプロイします。AWSで動かすには、DynamoDBのテーブル定義と、IAMロールの設定が必要でしたので、`serverless.yml` に追記して下記のようにします。

```serverless.yml
service: serverless-janken

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

plugins:
  - serverless-dynamodb-local
  - serverless-offline

package:
  exclude:
    - node_modules/**
    - migrations/**
    - .git/**

provider:
  name: aws
  runtime: nodejs4.3
  # DynamoDBの利用の許可
  iamRoleStatements:
    -  Effect: 'Allow'
       Action:
         - 'dynamodb:PutItem'
         - 'dynamodb:Scan'
       Resource: '*'

functions:
  playJanken:
    handler: handler.playJanken
    events:
      - http:
          path: jankens
          method: post
  listJankens:
    handler: handler.listJankens
    events:
      - http:
          path: jankens
          method: get

# DynamoDB Tableの作成
resources:
  Resources:
    JankensTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: jankens
        KeySchema:
          - AttributeName: player
            KeyType: HASH
          - AttributeName: unixtime
            KeyType: RANGE
        AttributeDefinitions:
          - AttributeName: player
            AttributeType: S
          - AttributeName: unixtime
            AttributeType: N
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
```

最後にServerless Commandでデプロイします。

```bash
$ sls deploy
Serverless: Packaging service...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading service .zip file to S3 (1.81 KB)...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
......................
Serverless: Stack update finished...
Service Information
service: serverless-janken
stage: dev
region: ***
api keys:
  None
endpoints:
  POST - https://***.amazonaws.com/dev/jankens
  GET - https://***.amazonaws.com/dev/jankens
functions:
  serverless-janken-dev-playJanken: arn:aws:lambda:***:***:function:serverless-janken-dev-playJanken
  serverless-janken-dev-listJankens: arn:aws:lambda:***:***:function:serverless-janken-dev-listJankens
```

以上でローカルで開発したServerless アプリケーションをAWSにデプロイができました。