[Lambda + APIGateway] ソース管理のためにServerlessを使ってみた

AWS Lambda + API Gatewayで今流行りのサーバーレスなWebサービスを使ってみる機会がありました。
Lambda関数を管理コンソール上でインライン編集をしていたのですが、ある時間違えてソースをいじってしまい、関数、ひいてはAPI自体が動かないということに...。
ソース管理は大事！！ということを身にしみて感じたので、Lambdaのソース管理を行う方法を調べました。

- AWS SAM (AWS公式)
- Serverless Framework

２つの方法があることを見つけたのですが、[AWS SAMは現時点でAPI GatewayでのCORS対応が行えない](https://github.com/awslabs/serverless-application-model/issues/23)ようなので、Serverless Frameworkを使用してLambdaのソース管理を行うことにしました。
（ローカルでServerlessのプロジェクトを作成し、gitでソース管理を行う。）

Serverlessで、Lambdaのソース管理も、今までAWS管理コンソール上でポチポチ設定していた内容も一括でローカルからデプロイできるようになりました。

「Serverlessってどんなフレームワークなの？何ができるの？」　→検索してみてください。
参考：[Serverless Framework入門](http://qiita.com/mpppk/items/40badc30008a86928406)


#概要

Lambda + API Gatewayのソース管理をするために、ローカルでServerlessプロジェクトを作成し、AWS上にデプロイする + α　を紹介します。
+ α では、Serverlessを使ってみて便利だった点、注意する点をメモします。

**この記事でやっていること**
STEP1：　Serverlessで新規テンプレートアプリ作成
STEP2：　Lambda + APIGateway サービス作成
STEP3：　Serverless 便利なところ、注意するところ


**Serverlessを使ってデプロイするAWSサービス**

- Lambda
- API Gateway
- DynamoDB
- S3
- IAM ロール

##前提条件
**ローカル環境**

- Node.jsのインストール(npm使用)
- awscliのインストール
- serverless frameworkのインストール
- (gitのインストール)

**その他**

- AWSアカウント取得済み


**開発環境(動作確認済)**

Node.js **v6.10**
npm **v5.3.0**
awscli **v1.11.123**
serverless **v1.21.1**


#Serverlessで新規テンプレートの作成

※serverless frameworkやawscliのインストール、AWSアカウントとの紐付けが済んでいない場合、こちらを参考にセットアップしてみてください。
参考：[今から始めるServerless Frameworkで簡単Lambda開発環境の構築](http://dev.classmethod.jp/cloud/aws/easy-deploy-of-lambda-with-serverless-framework/)

awscli, serverlessが使用できる状態であるところからスタートします。

```
$ aws --version
aws-cli/1.11.123 Python/2.7.10 Darwin/16.7.0 botocore/1.5.86
$ serverless --version
1.21.1
```

##Serverlessの新規テンプレートアプリ作成
任意のディレクトリで以下のコマンドを実行、テンプレートアプリを作成します。
`sls create -t <テンプレート種類> -p <プロジェクト名>`

*<テンプレート種類>*のところで、使用する言語を選択できます。node以外にもPythonなど使用できます。

ターミナルで実行すると以下のようになります。

```
$ sls create -t aws-nodejs -p lambdatest

Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/Users/yukakoyokota/Desktop/lambdatest"
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.21.1
 -------'

Serverless: Successfully generated boilerplate for template: "aws-nodejs"

```

lambdatestというフォルダが作成されました。

**テンプレートアプリ内容**
以下の二つのファイルが入っています。
**serverless.yml** 　LambdaやAPIGatewayなど、AWSの管理コンソールでポチポチ設定していた内容を記述するファイル（設定関係は全てここに書く）
**handler.js** 　Lambda関数のnodeのプログラムを記述するファイル

```serverless.yml
service: lambdatest     # アプリ名

provider:               # アプリ全般の設定を記述するところ
  name: aws             # プロバイダ名
  runtime: nodejs6.10   # 言語

functions:              # Lambda関数の設定
  hello:                # 関数名
    handler: handler.hello     #Lambdaのプログラムが置いてあるパス指定
```

→　「helloというLambda関数一つをAWS上にデプロイするよ」(serverless.yml)

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

→　「index.jsのhelloが呼ばれたら、メッセージ返すよ」(index.js)


##新規プロジェクトのデプロイ
プロジェクトをAWS上にデプロイします。

```
$ cd lambdatest
$ sls deploy
```

成功すると、以下のような結果が返ってきます。

```
Service Information
service: lambdatest
stage: dev
region: us-east-1
stack: lambdatest-dev
api keys:
  None
endpoints:
  None
functions:
  hello: lambdatest-dev-hello
```

hello関数がAWSにデプロイされました。
yamlでリージョンの設定がない場合、デフォルトでus-east-1に作成されます。

##関数の実行

```
$ sls invoke -f UserGet
{
    "statusCode": 200,
    "body": "{\"message\":\"Go Serverless v1.0! Your function executed successfully!\",\"input\":{}}"
}
```

関数のローカル実行もできるそうです。
参考：[Serverless Framework v1.6.0でPythonのローカル実行がサポートされます](http://dev.classmethod.jp/cloud/aws/serverless-framework-supports-local-invoke-for-python-in-v-1-5-2/)

##サービスの削除
デプロイしたサービス群をローカルからコマンドで全て削除することができます。

```
$ sls remove
```

オプションで`--stage`や`--region`などを指定でき、特定のステージやリージョンのサービスを削除することもできるようです。


#Lambda + APIGateway サービス作成

LambdaとAPI Gatewayを統合してAPIを作成する方法です。

##Lambda関数の作成
Lambda関数の作成方法です。

serverless.ymlを以下のように編集します。

```ruby:serverless.yml
service: lambdatest

# 一般設定
provider:
  name: aws
  runtime: nodejs6.10
  stage: dev               # サービスのステージ
  region: ap-northeast-1   # AWSのリージョン

# 関数定義
functions:
  UserGet:   # 関数名
    handler: manage/project.get   # 関数の場所
    timeout: 30                   # タイムアウトの設定
    description: Serverless test  # 関数説明
    memorySize: 512               # メモリー(default: 1024MB)
```

ymlファイルで指定したUserGet関数を作成します。

`handler`で関数の置いてある場所を指定しています。
`handler: manage/project.get`
→　manageフォルダの配下のproject.jsの`module.exports.get`を見に行きます。

以下のようなフォルダ構成にします。

```
[lambdatest]
   - [manage]
        - project.js
   - serverless.yml
```




project.jsファイルを以下のように編集します。

```ruby:project.js
'use strict';

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

  callback(null, response);
};

```

##APIGatewayとの統合
serverless.ymlのfunctions:の中に、events:を以下のように追加します。

```ruby:serverless.yml
# 関数定義
functions:
  UserGet:   # 関数名
    handler: manage/project.get   # 関数の場所
    timeout: 30
    description: Serverless test  # 関数説明
    memorySize: 512               # メモリー(default: 1024MB)
    tags:
      type: test                  # タグ 
    events:
      - http:                     # APIGateway設定
          path: manage/project    # APIのパス
          method: get             # HTTPメソッド
```

eventsセクションで関数を呼び出すトリガーを記述します。
例では、`- http:`以下でAPIGatewayのセットをしています。

`events:` > `- http:` > `path` にAPIのパスをセットします。
この場合、/manage/projectでGETリクエストを受けるためAPIが作成されます。

また、`tags:`では、`key:value`の形で関数にタグをつけることができます。


※ 全ての関数に同じタグをつけたい場合は以下のように設定することもできます。

```serverless.yml
provider:
  name: aws
  runtime: nodejs6.10
  stage: dev
  region: ap-northeast-1
  stackTags:
      type: serviceX       # functionセクションで定義された関数に付くタグ
```

デプロイをします。

```
$ sls deploy -v
```

AWSの管理コンソールで確認すると、サービスが作成されていることがわかります。

APIGateway
![スクリーンショット 2017-09-14 15.10.25.png](https://qiita-image-store.s3.amazonaws.com/0/68056/3c994bbd-4bc2-547f-3cd1-fd6df79055c5.png)

Lambda Function
![スクリーンショット 2017-09-14 15.12.55.png](https://qiita-image-store.s3.amazonaws.com/0/68056/4fcc9d9b-dc02-8cd7-1145-f594f136c808.png)

#Serverless 便利なところ

ServerlessアプリでLambdaとAPI Gateway以外にもAWSのサービスのセットアップができます。
例えば、Lambdaの処理で使用するDynamoDBやS3のセットアップ、ロールの設定も行えます。

##テーブルの作成
Lambda関数で使用するDynamoDBのテーブルを作成することができます。

ymlファイルでテーブルの定義とロールの設定を行うと自動デプロイされ、Lambdaから使用できるようになります。

serverless.ymlにテーブル定義を追加します。
以下の例では、lamtestUsers、lamtestProjectsという２つのテーブルの定義をしました。

```ruby:serverless.yml
service: lambdatest

provider:
  name: aws
  runtime: nodejs6.10
  stage: dev
  region: ap-northeast-1

# ロール設定(DynamoDB)
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:*
      Resource: arn:aws:dynamodb:*:*:table/*

# 関数定義
functions:
  UserGet:
    handler: manage/project.get
    timeout: 30
    events:
      - http:
          path: manage/project
          method: get
          cors: true

# テーブル定義
resources:
  Resources:
    lamtestUsers:    # テーブル名
      Type: 'AWS::DynamoDB::Table'
      # DeletionPolicy: Retain　　プロジェクト削除等でこのテーブルを残すか
      Properties:
        AttributeDefinitions:
          -
            AttributeName: id    # プライマリキー
            AttributeType: S
        KeySchema:
          -
            AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 2    # Read Capacity
          WriteCapacityUnits: 2   # Write Capacity
        TableName: lamtestUsers   # テーブル名

    lamtestProjects:
      Type: 'AWS::DynamoDB::Table'
      # DeletionPolicy: Retain
      Properties:
        AttributeDefinitions:
          -
            AttributeName: projectId
            AttributeType: S
        KeySchema:
          -
            AttributeName: projectId
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 3
          WriteCapacityUnits: 3
        TableName: lamtestProjects
```

このymlファイルをデプロイすると新しく２つのテーブルが作成されます。
LambdaからDynamoDBを利用するためにロールの設定も行っています。(`iamRoleStatements:`)
ロールの設定を記述すると、IAMで新しくロールが作成されます。


```
$ sls deploy -v
```

:warning:  **yamlファイルでテーブル名を変更してしまうと、元の名前のテーブルはレコード毎削除されて新しいテーブルが作成されるのでご注意ください。
「このテーブルは削除しない！」という場合は、`DeletionPolicy: Retain`をyamlのテーブルの設定に入れて置いてください。（上記のコメントアウト部分）**



##環境変数の使用
例えば、テーブル名を環境変数として設定しておくことで、nodeのコードからも環境変数を使ってテーブルを呼び出すことができるようになります。
（ymlファイルでテーブル名を書き換えても、jsソースをいじらずに済むので便利）

ymlファイルでテーブル名を環境変数として設定します。

```ruby:serverless.yml
service: lambdatest

provider:
  name: aws
  runtime: nodejs6.10
  stage: dev
  region: ap-northeast-1

  # 環境変数
  environment:
    DYNAMODB_USER_TABLE: lamtestUsers    # 環境変数名:テーブル名
    DYNAMODB_PROJECT_TABLE: lamtestProjects


# 関数定義
functions:
  ...

# テーブル定義
resources:
  Resources:
    lamtestUsers:
      Type: 'AWS::DynamoDB::Table'
      # DeletionPolicy: Retain
      Properties:
        # ... テーブルの設定　...
        TableName: lamtestUsers    #このテーブル名が環境変数に反映される

    lamtestProjects:
      Type: 'AWS::DynamoDB::Table'
      # DeletionPolicy: Retain
      Properties:
        # ... テーブルの設定 ...
        TableName: lamtestProjects
```

`provider`セクションの直下、`environment:`に記述します。
`JSで呼び出す環境変数名 : 呼び出したい値`の形式で記述します。

設定した環境変数は、以下のようにnodeで取得できます。

```ruby:project.js
'use strict';

let AWS = require('aws-sdk');
let docClient = new AWS.DynamoDB.DocumentClient();

// 使用するテーブル(環境変数で設定されたもの)
const projectTable = process.env.DYNAMODB_PROJECT_TABLE;
const robotTable = process.env.DYNAMODB_ROBOT_TABLE;

...

```

##S3のバケットの作成
S3のバケットもymlで定義できます。
ここでは、ロールの設定とバケットの定義をしています。

```serverless.yml
service: lambdatest

provider:
  name: aws
  runtime: nodejs6.10
  stage: dev
  region: ap-northeast-1

  # 環境変数
  environment:
    DYNAMODB_USER_TABLE: lamtestUsers
    DYNAMODB_PROJECT_TABLE: lamtestProjects
    S3_IMAGE_BUCKET: image-bucket

  # ロール設定(DynamoDB, S3)
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:*
      Resource: arn:aws:dynamodb:*:*:table/*

    - Effect: Allow
      Action:
        - s3:*
      Resource: arn:aws:s3:::nao-image-bucket/*

# 関数定義
functions:
  UserGet:
    handler: manage/project.get
    timeout: 30
    events:
      - http:
          path: manage/project
          method: get
          cors: true

resources:
  Resources:
    # テーブル定義
    # ...

    #S3 バケット
    imageBucket:                  # バケット名
      Type: AWS::S3::Bucket       # S3の指定
      Properties:
        BucketName: image-bucket  # バケット名
```

これでデプロイすると、S3のバケットが新規作成され、ロール設定をしてあると、Lambda関数からアクセスできるようになります。
また、環境変数も設定しておくと、コーディングの際便利です。

#Serverless 注意するところ
ServerlessからLambda＋APIGatewayを使用する時に、オンラインでコーディングしたり、管理コンソールで設定したりするのと異なる部分があります。
気づいた点などをメモします。

##CORSをTrueにしている時の注意

APIのCORS許可を行いたい場合以下のようにAPIGatewayの設定に`cors:true`を記述します。

```serverless.yml
# 関数定義
functions:
  UserPost:
    handler: manage/user.post
    timeout: 30 
    description: ユーザー新規作成
    events:
      - http:
          path: manage/user
          method: post
          cors: true           # CORSの許可
```

この場合、nodeでリクエストレスポンスを書くときに注意が必要になります。

- ヘッダーで`Access-Control-Allow-Origin`をセット
- ボディのコンテンツはString

エラーレスポンスのマッピングもコードで行う必要があります。
以下のようにレスポンスを返すようにしてみました。

```project.js
'use strict';

let AWS = require('aws-sdk');
let docClient = new AWS.DynamoDB.DocumentClient();

const s3 = new AWS.S3({apiVersion: '2006-03-01'});
const bucket = process.env.S3_IMAGE_BUCKET;

// 使用するテーブル
const projectTable = process.env.DYNAMODB_PROJECT_TABLE;
const userTable = process.env.DYNAMODB_USER_TABLE;

// 成功時のレスポンス
const createResponse = (statusCode, body) => {
    return {
        statusCode: statusCode,
        headers: {
            "Access-Control-Allow-Origin" : "*" // Required for CORS support to work
          },
        body: JSON.stringify(body)
    }
};

// エラーレスポンス
const createErrorResponse = (statusCode, errMessage) => {
    return {
        isBase64Encoded: false,
        statusCode: statusCode,
        headers: {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin' : '*' // Required for CORS support to work
          },
        body: JSON.stringify({message: errMessage})
    }
};

module.exports.post = (event, context, callback) => {
    // クエリパラメータ取得
    let userId = !event.queryStringParameters ? null : event.queryStringParameters.userId;

    // 取得できなければエラーにする
    if (!userId) {
        const res = createErrorResponse(400, 'Bad Request. query parameter[userId] is empty.');
        callback(null,res);
        return;
    } 
    
    // ユーザーが存在するかチェック
    const userParams = {
        TableName: userTable,
        Key: {
            "id": userId
        }
    };
    
    docClient.get(userParams, function(err, data) {
        // 様々な処理......

        //成功時のレスポンス
        callback(null, createResponse(200, projectJsonData));
    });
};

```
成功時は、レスポンスの中に入れたJSON
失敗時は、`{"message": "何かしらのエラーメッセージ"}`の形式でリターンされます。

##リクエストのクエリパラメータ取得
GETリクエストの時などにクエリパラメータ(http://~~~?id=xxxxxの時のidの値)が使用されている際の取得方法は以下のようにします。

```project.js
module.exports.get = (event, context, callback) => {
    // クエリパラメータ(userId)
    let userId = event.queryStringParameters.userId;

   // .......
}

```

##リクエストボディに含まれるパラメータ取得
POSTなどでリクエストボディの値を取得するには以下のようにします。

```project.js
module.exports.post = (event, context, callback) => {
    // リクエストボディ取得
    const item_data = JSON.parse(event.body);
    
    // パラメータ取得
    let userId = item_data.userId;
    let projectTitle = item_data.title;
    let description = item_data.description;

    // なんやかんや......
}

```


以上、Serverlessの使い方入門でした。


----

Lambda＋APIGatewayのサービスをServerlessフレームワークに移行してみて、デプロイが簡単で驚きでした！yamlでのサービスのセットアップの記述も慣れれば、わかりやすいです。
AWS SAMも、早くCORS対応してくれると良いですなぁ...:relaxed: