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

  • 53
    いいね
  • 5
    コメント

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

構成

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

Screen Shot 2016-12-23 at 14.55.06.png

環境

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

プロジェクトの作成

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

$ 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 プラグインをインストールします。

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

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

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

DynamoDB Local のインストール

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

$ sls dynamodb install

DynamoDB Local テーブルの定義

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

$ 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:
    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を起動します。

$ 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 にアクセスし、テーブルの中身を確認します。左側のエディタに下記を記入し、再生ボタンを押します。

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

Lambdaの開発

データベースの設定ができたので、ロジック部分を書いていきます。handler.js を開いて以下の内容で保存します。じゃんけんを行うAPI playJanken とじゃんけん結果を参照するAPI listJankens のためのロジックを書いています。このコードはAWSでもローカルでも動作が可能なように、 event.isOffline を見て接続する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 ("isOffline" in event && event.isOffline) {
        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が起動していることを確認してください。

$ 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でデプロイします。

$ 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にデプロイができました。