serverless frameworkを使って本格的なAPIサーバーを構築（ハンズオン編）


# serverless frameworkを使って本格的なAPIサーバーを構築（ハンズオン編）
最近、推しメンの　serverless framework の記事の第２弾です。
保守メンテが楽になりつつも、実戦で速攻で構築ができます。

## 目次

- [serverless frameworkを使って本格的なAPIサーバーを構築（魅力編）](https://qiita.com/okamu_/items/e246887cb6f2621ad40a)
- **[serverless frameworkを使って本格的なAPIサーバーを構築（ハンズオン編）](https://qiita.com/okamu_/items/0f1055e5c91ed99ebec6)←　今ここ**
- [serverless frameworkを使って本格的なAPIサーバーを構築（Express編）](https://qiita.com/okamu_/items/c19e2a682afe41a47eb1)
- [serverless frameworkを使って本格的なAPIサーバーを構築（テストコード編）](https://qiita.com/okamu_/items/2967726eef27420e6c63)


![framework_repo.png](https://qiita-image-store.s3.amazonaws.com/0/62822/e9ffa044-cdaf-c024-2dba-b670407ab1dd.png)


前回、serverless frameworkの魅力の記事を書きました。
今回は、serverless frameworkで 「 **lambda + APIGateway + DynamoDB** 」　の構成で簡単なサンプルアプリを作成します。

## この記事でできるようになること
- **REST FULL**なAPIを構築する
- **DynamoDB**と連携できる
- **スケージューリング**で実行する
- **DynamoStream**が使えるようになる

## 前準備
- serverlessを**install**しておく

```
$ npm install -g serverless
```

- プロジェクトを**生成**する

```
$ serverless create --template aws-nodejs --path my-service
```

すると、以下のファイルが出来ているはずです。

```
$ ls
serverless.yml
handler.js
```

また、サービス用のテンプレートですが、以下の言語用のテンプレートが用意されています。

#### 使える言語

```
aws-nodejs
aws-python
aws-java-maven
aws-java-gradle
aws-scala-sbt
```




## まずは、単純にJSONを返してみる
- 最初はHelloWorld!をJSONで返すAPIで表示してみます。
- `serverless create` をした時点でhandler.jsはこのようになっているので、messageを `hello world` など適当に変えれば良いです。

```handler.js
'use strict';

module.exports.hello = (event, context, callback) => {
  const response = {
    statusCode: 200,
    body: JSON.stringify({
      message: 'Go Serverless v1.0! Your function executed successfully!',
      input: event,
    }),
  };

  callback(null, response);

  // Use this code if you don't use the http event with the LAMBDA-PROXY integration
  // callback(null, { message: 'Go Serverless v1.0! Your function executed successfully!', event });
};

```

- 最初のserverless.ymlを少し編集します
    - regionがdefaultでは `us-east-1` なので `ap-northeast-1` にする
    - getでアクセスできるように eventsを追加する

```serverless.yml
service: my-service

provider:
  name: aws
  runtime: nodejs6.10

  stage: dev
  region: ap-northeast-1

functions:
  hello:
    handler: handler.hello
    events:
      - http:
          path: hello
          method: get
```

- これでOKです！！　さっそく、**deploy**してみましょう！！！！！！

```
$ sls deploy -v -s dev
```

- `-v` は進捗を表示
- `-s` はステージの名前を指示します（defaultはdev）
- APIGatewayでは、プロジェクトからステージごとにデプロイしていましたが、serverlessではステージごとにプロジェクトが作られます。

```
Serverless: Stack update finished...
Service Information
service: XXXXXXXX
stage: dev
region: ap-northeast-1
api keys:
  None
endpoints:
  GET - https://XXXXXXXX.ap-northeast-1.amazonaws.com/dev/hello
functions:
  hello: XXXXXXXX
Stack Outputs

Serverless: Removing old service versions...
```

デプロイが成功すると、 APIGateWayには設定済みのプロジェクトと、`endpoints` を教えてくれるのでそこにアクセスしてみてhelloとJSONが表示されればOKです！！！

<img width="1356" alt="スクリーンショット 2017-09-23 15.28.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/62822/7bbdf0f4-bf83-a618-493b-f1f4aae8d0c9.png">


<img width="764" alt="スクリーンショット 2017-09-23 15.28.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/62822/6a543975-6b39-1830-bacb-d4ba8feb3264.png">

このように、GETでアクセスをするとさっき作成したJSONを返していることがわかります。
完璧だー 🍙🍙🍙

## REST APIを作ってみる
- 次は、**DynamoDBと連携** して、get post put deleteを実装してみます。

- serverless.ymlを編集します
- REST APIのサンプルは、[aws-node-rest-api-with-dynamodb](https://github.com/serverless/examples/tree/master/aws-node-rest-api-with-dynamodb)にあります。

### serverless.yml

```serverless.yml
service: serverless-rest-api-with-dynamodb

frameworkVersion: ">=1.1.0 <2.0.0"

provider:
  name: aws
  runtime: nodejs4.3
  environment:
    DYNAMODB_TABLE: ${self:service}-${opt:stage, self:provider.stage}
  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}"

functions:
  create:
    handler: todos/create.create
    events:
      - http:
          path: todos
          method: post
          cors: true

  list:
    handler: todos/list.list
    events:
      - http:
          path: todos
          method: get
          cors: true

  get:
    handler: todos/get.get
    events:
      - http:
          path: todos/{id}
          method: get
          cors: true

  update:
    handler: todos/update.update
    events:
      - http:
          path: todos/{id}
          method: put
          cors: true

  delete:
    handler: todos/delete.delete
    events:
      - http:
          path: todos/{id}
          method: delete
          cors: true

resources:
  Resources:
    TodosDynamoDbTable:
      Type: 'AWS::DynamoDB::Table'
      DeletionPolicy: Retain
      Properties:
        AttributeDefinitions:
          -
            AttributeName: id
            AttributeType: S
        KeySchema:
          -
            AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        TableName: ${self:provider.environment.DYNAMODB_TABLE}
```
- serverless.ymlのポイント
    - environmentで、どのLambdaからでも `DYNAMODB_TABLE` を参照することができます。
    - todosというフォルダの中に create.js get.js list.js delete.js update.jsを置いておきます。
    - resourcesでDynamoDBを生成しています。

 次に、それぞれのLambda関数を作成します

### create.js

```todos/create.js
'use strict';

const uuid = require('uuid');
const AWS = require('aws-sdk'); // eslint-disable-line import/no-extraneous-dependencies

const dynamoDb = new AWS.DynamoDB.DocumentClient();

module.exports.create = (event, context, callback) => {
  const timestamp = new Date().getTime();
  const data = JSON.parse(event.body);
  if (typeof data.text !== 'string') {
    console.error('Validation Failed');
    callback(null, {
      statusCode: 400,
      headers: { 'Content-Type': 'text/plain' },
      body: 'Couldn\'t create the todo item.',
    });
    return;
  }

  const params = {
    TableName: process.env.DYNAMODB_TABLE,
    Item: {
      id: uuid.v1(),
      text: data.text,
      checked: false,
      createdAt: timestamp,
      updatedAt: timestamp,
    },
  };

  // write the todo to the database
  dynamoDb.put(params, (error) => {
    // handle potential errors
    if (error) {
      console.error(error);
      callback(null, {
        statusCode: error.statusCode || 501,
        headers: { 'Content-Type': 'text/plain' },
        body: 'Couldn\'t create the todo item.',
      });
      return;
    }

    // create a response
    const response = {
      statusCode: 200,
      body: JSON.stringify(params.Item),
    };
    callback(null, response);
  });
};
```
### delete.js

```todos/delete.js
'use strict';

const AWS = require('aws-sdk'); // eslint-disable-line import/no-extraneous-dependencies

const dynamoDb = new AWS.DynamoDB.DocumentClient();

module.exports.delete = (event, context, callback) => {
  const params = {
    TableName: process.env.DYNAMODB_TABLE,
    Key: {
      id: event.pathParameters.id,
    },
  };

  // delete the todo from the database
  dynamoDb.delete(params, (error) => {
    // handle potential errors
    if (error) {
      console.error(error);
      callback(null, {
        statusCode: error.statusCode || 501,
        headers: { 'Content-Type': 'text/plain' },
        body: 'Couldn\'t remove the todo item.',
      });
      return;
    }

    // create a response
    const response = {
      statusCode: 200,
      body: JSON.stringify({}),
    };
    callback(null, response);
  });
};
```
### get.js

```todos/get.js
'use strict';

const AWS = require('aws-sdk'); // eslint-disable-line import/no-extraneous-dependencies

const dynamoDb = new AWS.DynamoDB.DocumentClient();

module.exports.get = (event, context, callback) => {
  const params = {
    TableName: process.env.DYNAMODB_TABLE,
    Key: {
      id: event.pathParameters.id,
    },
  };

  // fetch todo from the database
  dynamoDb.get(params, (error, result) => {
    // handle potential errors
    if (error) {
      console.error(error);
      callback(null, {
        statusCode: error.statusCode || 501,
        headers: { 'Content-Type': 'text/plain' },
        body: 'Couldn\'t fetch the todo item.',
      });
      return;
    }

    // create a response
    const response = {
      statusCode: 200,
      body: JSON.stringify(result.Item),
    };
    callback(null, response);
  });
};
```
### list.js

```todos/list.js
'use strict';

const AWS = require('aws-sdk'); // eslint-disable-line import/no-extraneous-dependencies

const dynamoDb = new AWS.DynamoDB.DocumentClient();
const params = {
  TableName: process.env.DYNAMODB_TABLE,
};

module.exports.list = (event, context, callback) => {
  // fetch all todos from the database
  dynamoDb.scan(params, (error, result) => {
    // handle potential errors
    if (error) {
      console.error(error);
      callback(null, {
        statusCode: error.statusCode || 501,
        headers: { 'Content-Type': 'text/plain' },
        body: 'Couldn\'t fetch the todos.',
      });
      return;
    }

    // create a response
    const response = {
      statusCode: 200,
      body: JSON.stringify(result.Items),
    };
    callback(null, response);
  });
};
```
### update.js

```todos/update.js
'use strict';

const AWS = require('aws-sdk'); // eslint-disable-line import/no-extraneous-dependencies

const dynamoDb = new AWS.DynamoDB.DocumentClient();

module.exports.update = (event, context, callback) => {
  const timestamp = new Date().getTime();
  const data = JSON.parse(event.body);

  // validation
  if (typeof data.text !== 'string' || typeof data.checked !== 'boolean') {
    console.error('Validation Failed');
    callback(null, {
      statusCode: 400,
      headers: { 'Content-Type': 'text/plain' },
      body: 'Couldn\'t update the todo item.',
    });
    return;
  }

  const params = {
    TableName: process.env.DYNAMODB_TABLE,
    Key: {
      id: event.pathParameters.id,
    },
    ExpressionAttributeNames: {
      '#todo_text': 'text',
    },
    ExpressionAttributeValues: {
      ':text': data.text,
      ':checked': data.checked,
      ':updatedAt': timestamp,
    },
    UpdateExpression: 'SET #todo_text = :text, checked = :checked, updatedAt = :updatedAt',
    ReturnValues: 'ALL_NEW',
  };

  // update the todo in the database
  dynamoDb.update(params, (error, result) => {
    // handle potential errors
    if (error) {
      console.error(error);
      callback(null, {
        statusCode: error.statusCode || 501,
        headers: { 'Content-Type': 'text/plain' },
        body: 'Couldn\'t fetch the todo item.',
      });
      return;
    }

    // create a response
    const response = {
      statusCode: 200,
      body: JSON.stringify(result.Attributes),
    };
    callback(null, response);
  });
};
```
これもデプロイをしてみてください！！

postmanなどで GET POST PUT DELETE すると確認ができるはずです。


## スケジュールを設定して定期実行してみる
- Lambdaの定期実行の形式をserverless.ymlにそのまま記述することもできます。

- [scheduled-events-schedule-expressions](http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html)

```serverless.yml
  hoge:
    handler: functions/aggregate/index.handler
    events:
      - schedule:
          rate: cron(0 1 * * ? *)

```
```serverless.yml
  hoge:
    handler: functions/aggregate/index.handler
    events:
      - schedule:
          rate: rate(5 minutes)
```


## DynamoStreamを使ってputされたときに何か実行してみる
- DynamoStreamとは、DynamoDBにputやupdateがあった場合に、そのイベントをトリガーにLambdaでまた処理させることができます。

- DynamoのResourcesに`StreamViewType` を追加

```serverless.yml
    HogeDynamoDbTable:
      Type: 'AWS::DynamoDB::Table'
      DeletionPolicy: Retain
      Properties:
        AttributeDefinitions:
          -
            AttributeName: column
            AttributeType: S
        KeySchema:
          -
            AttributeName: column
            KeyType: HASH

        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        TableName: "hoge-${self:provider.stage}"
        StreamSpecification:
          StreamViewType: KEYS_ONLY
```

そして、Streamでどの関数が呼ばれるかを指定します
以下のようにすると、二つのDynamoDBに変更がある場合に１つのLambda関数が呼ばれます。

```serverless.yml
  hoge:
    handler: functions/hoge/index.handler
    events:
      - stream:
          type: dynamodb
          arn:
            Fn::GetAtt:
              - HogeDynamoDbTable
              - StreamArn
          batchSize: 1
      - stream:
          type: dynamodb
          arn:
            Fn::GetAtt:
              - HogeDynamoDbTable2
              - StreamArn
          batchSize: 1
```

batchSizeは１度にどれだけ項目がほしいか設定できます。

ちなみに、StreamViewTypeには

```
KEYS_ONLY => HASHキーのみ関数で取得できる
NEW_IMAGE => 　新しいものだけ取得できます
OLD_IMAGE => 古いものだけ取得できます
NEW_AND_OLD_IMAGES => 新旧のデータが取得できます
```
があります。

## その他できること
- serverlessで、SNS IoT S3 Kinesisのイベントなどをトリガーとして発動させることができます。
- https://serverless.com/framework/docs/providers/aws/events/

## まとめ
- serverlessを使うとかんたんにAPIが作成できました！！
- イベントの作成やオプションもまったく困らないと思います。
- サンプルやプラグインが豊富で、ベストプラクティスには迷いません！

## 余談
- 前回serverlessの魅了の紹介で、serverlessを使わないほうがいいときはないのかという質問を受けました。

#### serverlessを使わないほうがいいとき
- 複雑なクエリを要するアプリを作るとき
    - 複雑なクエリを要するアプリを作るときは、DynamoDBでも設計を入念に行えばいいかと思いますが、whereなどのクエリが使えないので注意が必要です
    - MySQLクライアントもLambdaで使用することができますが、若干使いにくいようです。
- Dynamoに書き込む容量が大きいとき
    - DynamoDBはとても安いですが、書き込み読み込みのキャパシティが多くなると料金が結構高くなりますので注意が必要です。
    - https://www.slideshare.net/AmazonWebServicesJapan/20150805-aws-blackbeltdynamodb
    - https://qiita.com/YU81/items/e1e336990ed8cfb938d9


## 次回
- [serverless frameworkを使って本格的なAPIサーバーを構築（Express編）です！！](https://qiita.com/okamu_/items/c19e2a682afe41a47eb1)