とことんサーバーレス①：Serverless Framework入門編

# Serverless Frameworkとは

最近巷で騒がれているサーバーレスなアーキテクチャを容易に作成、管理できる一般的なフレームワークというよりツールに近いもの。

Severless FrameworkのAWS環境においては、CloudFormation・Lambda・API Gateway・DynamoDB・S3・IAMなどのサービスを組み合わせてサーバーレスなアーキテクチャを管理・構成している。

今回は、Severless Frameworkを用いてLambda・API Gateway・DynamoDBのアーキテクチャで簡単なREST APIを実装する。

![cloudcraft - serverless.png](https://qiita-image-store.s3.amazonaws.com/0/34930/7224412a-b177-6992-0509-013bacb69d0e.png "cloudcraft - serverless.png")


なお、本記事では`v1.0.x`を対象としている。
`v0.x`と大きく構成・設定が変わっているので注意。

# 環境
今回はAWS上で、Severless Frameworkを用いて簡単なREST APIを作成する。

Severless Frameworkでは、1.0.x時点でNode.js、Python、Java、Scalaがサポートされている。

今回は、Node.jsを対象として進めていく。
なお、JavaScriptに関しては、ES6を使用する。

|パッケージ|バージョン|
|:--|:--|
|npm|3.10|
|node|6.2|
|Serverless Framework|1.0.3|


# GitHub
[hello-serverless](https://github.com/hiroshik1985/hello-serverless)

# セットアップ

## Serverless Frameworkインストール

早速Serverless Frameworkのインストール。

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

## AWS Credentialsの設定

まずはSeverless Frameworkで使用するIAMを作成する。
作成の仕方は下記の公式ドキュメントを参考にしてください。
一旦ここでは、管理ポリシーとして「AdministratorAccess」のポリシーをもつユーザーを作成する。

http://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_users_create.html


次に、AWSサービスを管理するためのコマンドラインインターフェース(CLI)のツールをインストールしてServerless Frameworkで使用するIAMの設定を行う。

http://docs.aws.amazon.com/ja_jp/streams/latest/dev/kinesis-tutorial-cli-installation.html

``` MacでPython>=2.6.3な方の例
$ curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py"
$ sudo python get-pip.py
$ sudo pip install awscli
```

今回は、DefaultのAWS Profilesを使っていきます。

```
$ aws configure
AWS Access Key ID [None]: 作成したIAMのアクセスキーID
AWS Secret Access Key [None]: 作成したIAMのシークレットアクセスキー
Default region name [None]: ap-northeast-1
Default output format [None]: ENTER
```

既にDefaultのAWS Profilesが設定されている方は、下記を参考にProfileを切り替えてください。

https://serverless.com/framework/docs/providers/aws/guide/credentials/


# プロジェクトの作成


今回は`node.js`で`hello`というプロジェクトを作成する。
※`serverless`コマンドはインストール時に追加される`sls`エイリアスを使っていく。

```
$ mkdir hello-serverless
$ cd hello-serverless
$ sls create --template aws-nodejs --name hello
```

ファイルを確認してみる。

|ファイル|用途|
|:--|:--|
|event.json|各Functionに`event`で渡されるテストデータ|
|handler.js|実際のLambda Functionを記述する|
|serverless.yml|各種設定。Lambda Functionの設定、エンドポイントの設定、リソースの設定などを記述する|

## handler.js

デフォルトのhandler.jsの中身を確認してみる。

``` 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 });
};
```

`Go Serverless v1.0! Your function executed successfully!`というメッセージとevent内容を出力をJSON形式で表示する簡単な処理になっている。
これらを実際に確認してみる。


## デプロイ先のリージョン設定
デフォルトでは、デプロイ先のリージョンとして`es-east-1`（バージニア北部）に指定されているので`ap-northeast-1`（東京）に変更する。

``` serverless.yml
provider:
  name: aws
  runtime: nodejs4.3

# you can overwrite defaults here
  stage: dev
  region: ap-northeast-1
```

## デプロイ

```
$ sls deploy -v
```

## 実行

```
sls invoke -f hello -p event.json
```

メッセージとevent内容が表示されただろうか？

## イベントソース

どのイベントをトリガーにして、それぞれのLambda functionを呼び出すかを定義する。イベントの種類に関しては、AWSでサポートしているLambda functionのトリガーの全てが定義可能。

https://serverless.com/framework/docs/providers/aws/events/

今回はHTTP APIを定義する。
これによりAPI Gatewayへのアクセスによりトリガーされる。


`events`を追記する。

``` serverless.yml
functions:
  hello:
    handler: handler.hello
    events:
       - http:
           path: hello
           method: get
           cors: true
```

再度デプロイ

```
$ sls deploy -v
```

```
Service Information
service: hello
stage: dev
region: ap-northeast-1
api keys:
  None
endpoints:
  GET - https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/greet
functions:
  hello-dev-hello: arn:aws:lambda:ap-northeast-1:xxxxxxxxxx:function:hello-dev-hello
```

実際に、上記endpointsに記載されているAPIを叩いてみる。

```
$ curl -X GET https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/greet
```

#ES6の設定
Babel + WebPackを用いて、ES6の環境を作成する。

## パッケージのインストール
```
$ npm install --save-dev babel-core babel-loader babel-plugin-transform-runtime babel-polyfill babel-preset-es2015 babel-preset-stage-0 serverless-webpack webpack
$ npm install --save babel-runtime babel-polyfill
```

## Babelの設定

``` .babelrc
{
  "plugins": ["transform-runtime"],
  "presets": ["es2015", "stage-0"]
}
```

## webpackの設定
今回は、Babelとローカルの実行環境を作成するためにwebpackを用いる。

``` serverless.yml
plugins:
  - serverless-webpack
```

``` webpack.config.js
module.exports = {
  entry: './handler.js',
  target: 'node',
  module: {
    loaders: [{
      test: /\.js$/,
      loaders: ['babel'],
      include: __dirname,
      exclude: /node_modules/,
    }]
  },
  externals: {
    'aws-sdk': 'aws-sdk'
  }
};
```

## ES6

`handler.js`をまるっとES6へ

``` handler.js
export const 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-webpack`によりローカルでの実行も可能となる。

```
$ sls webpack serve
```

表示されたローカルのエンドポイントにcurlコマンドでリクエストしてみる。


``` 
$ curl -X GET http://localhost:8000/hello
```


# DynamoDBの設定

AWSのNoSQLなDynamoDBを使って、名前を登録、`Hello, {登録名}`とレスポンスを返すように変更していく。

## API概要

ここでは、下記のHTTP APIを用意していく。

|パス|メソッド|リクエストJSONフォーマット|用途|
|:--|:--|:--|:--|
|/names|POST|{"id":ID, "name":"登録名"}|名前のDynamoDBへの登録|
|/hello/{id}|GET||"Hello, {登録名}"メッセージの取得|

## パッケージのインストール

```
$ npm install --save-dev  aws-sdk
```

## DynamoDBの設定
Severless Frameworkでは、`serverless.yml`の`resources`という項目でDynamoDBはじめ、S3などの各種リソースの設定を行える。特にAWSのコンソールを用いる必要はない。

今回は、単純に「名前」を扱うidとnameカラムをもつnamesテーブルの設定を行う。
（nameカラムに関してはここでは定義しないことにする）

``` serverless.yml
  Resources:
    hello:
      Type: "AWS::DynamoDB::Table"
      Properties:
        TableName: names
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: N
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
```

また、合わせてデプロイ時にDynamoDBを扱うIAMを作成するために`provider`にRoleを設定する。

``` serverless.xml
provider:
  name: aws
  runtime: nodejs4.3
# you can overwrite defaults here
  stage: dev
  region: ap-northeast-1
  iamRoleStatements:
    - Effect: Allow
      Resource: "arn:aws:dynamodb:${self:provider.region}:*:table/*"
      Action:
        - "dynamodb:*"
```


## Lambda処理

DynamoDBに名前を登録する`regist`メソッドと指定されたIDに対して、`Hello, {登録名}`を返す`hello`メソッドを定義する。

``` handler.js
import AWS from 'aws-sdk'

AWS.config.update({region: 'ap-northeast-1'})

const db = new AWS.DynamoDB()

export const hello = (event, context, callback) => {
  const params = {
    TableName: 'names',
    Key: {
      id: {N: event.pathParameters.id}
    }
  }

  try {
    db.getItem(params, (error, data) => {
      if (error) {
        callback(null, {statusCode: 400, body: JSON.stringify({message: 'Failed.', error: error})})
      }
      callback(null, {statusCode: 200, body: JSON.stringify({message: `Hello, ${data.Item.name.S}.`})});
    })
  } catch (error) {
    callback(null, {statusCode: 400, body: JSON.stringify({message: 'Failed.', error: error})})
  }
}

export const regist = (event, context, callback) => {
  const body = JSON.parse(event.body)
  const params = {
    TableName: 'names',
    Item: {
      id: {N: String(body.id)},
      name: {S: body.name}
    },
    Expected: {
      id: {Exists: false}
    }
  }


  try {
    db.putItem(params, (error, data) => {
      if (error) {
        callback(null, {statusCode: 400, body: JSON.stringify({message: 'Failed.', error: error})})
      }
      callback(null, {statusCode: 200, body: JSON.stringify({message: 'Succeeded!', params: params})});
    })
  } catch (error) {
    callback(null, {statusCode: 400, body: JSON.stringify({message: 'Failed.', error: error})})
  }
}

```

## Lambda Functionの設定

``` serverless.xml
functions:
  hello:
    handler: handler.hello
    events:
      - http:
           path: hello/{id}
           method: get
           cors: true

  create:
    handler: handler.regist
    events:
      - http:
           path: names
           method: post
           cors: true
```

## 確認

デプロイしてcurlコマンドで確認してみる。

まずはID1でTaroを登録する。

```
$ curl -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"id":1, "name":"Taro"}'  https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello
```

メッセージの確認

```
$ curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello/1
```

レスポンスJSONに`message`に`Hello, Taro.`と表示されればok :)

# 次回
今回は、Serverless Frameworkを使ってREST APIを作成した。
次回は、QraphQLに対応してみる。

# 最後に
[サイバー・バズでは新しいことにチャレンジしつつサービスをつくっていくエンジニアを募集しています。](https://www.wantedly.com/projects/57067)