Serverless 使って Lambda から RDS を使用する

先日 [Lambda の VPC サポート実装がアナウンスされてた](https://aws.amazon.com/jp/blogs/aws/new-access-resources-in-a-vpc-from-your-lambda-functions/)ので試してみました。

VPC サポートの実装により、Lambda と RDS, ElasticCache などを連携させられるようになりました。
使ってみた所感としては、今まで用途が限定されていた気がする Lambda の使いどころがかなり広がったんじゃないかと思います。
小規模な案件なら本当にサーバいらなくなるんじゃないでしょうか。

## 諸々インストール
フレームワークは jaws の後継 serverless を使います。
動作には node V4 が必要です。
作業は適当に立ち上げた EC2 インスタンス上で行いました。

```bash
$ git clone git://github.com/creationix/nvm.git ~/.nvm
$ source ~/.nvm/nvm.sh
$ nvm ls-remote # 最新のバージョンを確認
$ nvm install 4.3.1
$ nvm use v4.3.1
Now using node v4.3.1 (npm v2.14.12)
$ npm install serverless
$ mkdir ~/.aws
$ vi ~/.aws/credentials
===
[default]
aws_access_key_id=<<アクセスキー>>
aws_secret_access_key=<<シークレットアクセスキー>>
===

$ vi ~/.bashrc # パス通す
===
...
PATH="$PATH":/usr/local/bin:~/node_modules/.bin
if [[ -s ~/.nvm/nvm.sh ]];
 then source ~/.nvm/nvm.sh
fi
===

$ sls help  # 動作確認
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v0.4.2
`-------'


```

## RDS 設定
* VPC: 「デフォルト VPC」 を選択
* パブリックアクセス可能: いいえ
* VPC セキュリティグループ: 下記のように同じセキュリティグループからの MySQL の通信を許可

![ScreenShot 2016-03-01 at 14.27.01.png](https://qiita-image-store.s3.amazonaws.com/0/10160/e89aa500-92af-b4e9-40eb-010d32857c38.png "ScreenShot 2016-03-01 at 14.27.01.png")

それ以外は適当に設定。

### テーブル作成
```bash
$ mysql {RDS DB 名} -h {RDS エンドポイント} -u {RDS ユーザ名} -p 

mysql> CREATE TABLE lambda_test (
`id` INT( 11 ) NOT NULL AUTO_INCREMENT,
`text` TEXT NOT NULL ,
`created` TimeStamp DEFAULT '0000000000',
PRIMARY KEY ( `id` )
);
```

### ローカルでのテスト用に EC2 上でも MySQL 動作するようにしておく
```bash
$ sudo yum install mysql-server
$ sudo chkconfig mysqld on
$ sudo chkconfig --list # mysqld on になってるの確認
$ sudo service mysqld start
$ sudo /usr/bin/mysql_secure_installation
対話形式で全部 Y
$ mysql -u root -p
mysql> CREATE DATABASE  `lambda_test` CHARACTER SET utf8 COLLATE utf8_general_ci;

mysql> use lambda_test

mysql> CREATE TABLE lambda_test (
`id` INT( 11 ) NOT NULL AUTO_INCREMENT,
`text` TEXT NOT NULL ,
`created` TimeStamp DEFAULT '0000000000',
PRIMARY KEY ( `id` )
);
```


## プロジェクト作成
```bash
$ sls project create
Serverless: Initializing Serverless Project...  
Serverless: Enter a name for this project:  (serverless-nkeoqm) SayHello # プロジェクト名を入力
Serverless: Enter a universally unique project bucket name:  (serverless-nkeoqm) sayhello.masarun.co # S3 バケット名を入力
Serverless: Enter an email to use for AWS alarms:  (me@serverless-nkeoqm.com) someone+aws@example.com # メールアドレスを入力
error:   Please enter a valid email # shit!
Serverless: Enter an email to use for AWS alarms:  (me@serverless-nkeoqm.com) someone@example.com
Serverless: Select a region for your project: # リージョン選択
    us-east-1
    us-west-2
    eu-west-1
  > ap-northeast-1
Serverless: Select an AWS profile for your project: 
  > runco # プロフィール選択
Serverless: Creating stage "dev"...  
Serverless: Creating region "ap-northeast-1" in stage "dev"...  
Serverless: Creating your project bucket on S3: serverless.ap-northeast-1.sayhello.masarun.co...  
Serverless: Deploying resources to stage "dev" in region "ap-northeast-1" via Cloudformation (~3 minutes)...  
Serverless: Successfully deployed "dev" resources to "ap-northeast-1"  
Serverless: Successfully created region "ap-northeast-1" within stage "dev"  
Serverless: Successfully created stage "dev"  
Serverless: Successfully initialized project "SayHello" 

$ tree
.
├── admin.env
├── _meta
│   ├── resources
│   │   └── s-resources-cf-dev-apnortheast1.json
│   └── variables
│       ├── s-variables-common.json
│       ├── s-variables-dev-apnortheast1.json
│       └── s-variables-dev.json
├── package.json
├── README.md
├── s-project.json
└── s-resources-cf.json

```

## Component, Function 作成
ちょっと前まで Module と呼ばれていたものが Component になってます。
ヘルプとかマニュアルにはまだ混在してる箇所もあるので注意。

```bash
$ sls component create greetings
└── dotenv@1.2.0
Serverless: -----------------  
Serverless: Successfully created new serverless component: greetings  

$ sls function create greetings/say
Serverless: Successfully created function: "greetings/say"  

$ tree
.
├── admin.env
├── greetings
│   ├── lib
│   │   └── index.js
│   ├── node_modules
│   ├── package.json
│   ├── say
│   │   ├── event.json
│   │   ├── handler.js
│   │   └── s-function.json
│   └── s-component.json
├── _meta
│   ├── resources
│   │   └── s-resources-cf-dev-apnortheast1.json
│   └── variables
│       ├── s-variables-common.json
│       ├── s-variables-dev-apnortheast1.json
│       └── s-variables-dev.json
├── package.json
├── README.md
├── s-project.json
└── s-resources-cf.json

```
Component, Function のディレクトリができました。
greetings/say/handler.js を見てみると、

```greetings/say/handler.js
// Require Logic
var lib = require('../lib');
```

Component 直下の lib から読もうとしてるので、
ここに Function の中身記述するのがよさそうですね。

他にも Function 作るかもしれないのでこうしときました。

```bash
$ mv greetings/lib/index.js greetings/lib/say.js
$ vi greetings/say/handler.js
===
var lib = require('../lib/say'); 
===

```

実行してみる

```bash
$ sls function run greetings/say
Serverless: Running greetings/say...  
Serverless: -----------------  
Serverless: Success! - This Response Was Returned:  
Serverless: {"message":"Your Serverless function ran successfully!"}  
```

## Serverless の stage について
* Serverless では stage (dev, beta, prod, etc..) を作成して、stage 別に環境変数を設定することができる
* これにより開発環境・BETA環境・本番環境など違う環境のソースをまとめて管理できるようになる
* プロジェクト作った時点で　local, dev ステージができる
* local でステージを指定せずにテスト実行した場合、local ステージが使われる
* ステージ別に env を設定できる
* env はこういうコマンドで設定する
```
$ sls env set -k {キー名} -v {値} -s {ステージ名}
```
	* ステージごとに1個1個設定していくのはとてもめんどくさいぞ
	* 自動化しないとミスりそう
* local ステージの env だけは .env に記録されてる

詳しくは[ドキュメント](http://docs.serverless.com/docs/env-list)参照ください

## env 設定
前述のとおり、env の設定がとてもめんどくさかったので JSON でまとめて設定してみました。
このやり方で問題出ないのかは知りません。

```bash
# dev ステージの MySQL 接続情報（RDS）
$ sls env set -s dev -k mysqlconfig -v '{"host": "{RDS のエンドポイント}", "user": "{RDS のユーザ名}", "db": "{RDS の DB名}", "password": "{RDS のパスワード}"}'

# local ステージの MySQL 接続情報
$ sls env set -s dev -k mysqlconfig -v '{"host": "localhost", "user": "{MySQL のユーザ名}", "db": "{MySQL の DB名}", "password": "{MySQL のパスワード}"}'

# 確認
$ sls env list -s dev
$ sls env list -s local

```


## コード
MySQL 使うので node-mysql インストールします。

```bash
$ npm install mysql --save
```

コードはとりあえずこんなんです。
すぐコールバック地獄になるのでプロミスとか使った方がいいと思います。

```greetings/lib/say.js 
/**
 * Lib
 */
var mysql = require('mysql');

module.exports.respond = function(event, cb) {
  mysqlconfig = JSON.parse(process.env.mysqlconfig);
  var connection = mysql.createConnection({
    host     : mysqlconfig.host, 
    user     : mysqlconfig.user,
    password : mysqlconfig.password,
    database : mysqlconfig.db,
    acquireTimeout: 1000
  });

  connection.connect(function(err){
    if (err) throw err;
  });

  connection.query("insert into lambda_test values(0, ?, NOW())", [event.text], function(err, re) {
    if (err) throw err;
    connection.end();

    var response = {
      message: "Your Message: " + event.text
    };
    return cb(null, response);
  });
};

```


下記はローカルのテスト用です。

```say/event.json 
{"text": "hello!"}
```


## ローカル実行
```bash
$ sls function run greetings/say
Serverless: Running greetings/say...  
Serverless: -----------------  
Serverless: Success! - This Response Was Returned:  
Serverless: {"message":"Your Message: hello!"}  

# mysql でも確認
mysql> SELECT * FROM lambda_test ;
+----+--------+---------------------+
| id | text   | created             |
+----+--------+---------------------+
|  1 | hello! | 2016-03-01 06:54:03 |
+----+--------+---------------------+
1 row in set (0.00 sec)
```

## デプロイ
ローカルで OK ぽいんでデプロイします。

```bash
# 対話式コマンドでデプロイ
$ sls dash deploy
Serverless: Select the assets you wish to deploy:
    greetings/say
      function - greetings/say  # Enter で選択
      endpoint - greetings/say@say~GET  # Enter で選択
    - - - - -
  > Deploy  # Enter で選択
    Cancel

Serverless: Deploying functions in "dev" to the following regions: ap-northeast-1  
Serverless: ------------------------  
Serverless: Successfully deployed functions in "dev" to the following regions:   
Serverless: ap-northeast-1 ------------------------  
Serverless:   greetings/say (SayHello-greetings-say): arn:aws:lambda:ap-northeast-1:908049199897:function:SayHello-greetings-say:dev  

Serverless: Deploying endpoints in "dev" to the following regions: ap-northeast-1  
Serverless: Successfully deployed endpoints in "dev" to the following regions:  
Serverless: ap-northeast-1 ------------------------  
Serverless:   GET - say - https://***.execute-api.ap-northeast-1.amazonaws.com/dev/say  
```

これでデプロイされてエンドポイントが作成されます。
でも間違えて GET のまま作ってしまいました。
このままだと謎の 403 エラーが出ます。

```bash
$ curl -v -d '{"text":"Piyo-"}' https://***.execute-api.ap-northeast-1.amazonaws.com/dev/say{"message":"Missing Authentication Token"}
```

405 ではないんですね...

s-function.json の method を変更します。

```greetings/say/s-function.json 
...
  "endpoints": [
    { 
      "path": "say",
      "method": "POST",  # GET から変更
...
```
デプロイし直して再実行

```bash
$ curl -v -d '{"text":"Piyo-"}' https://***.execute-api.ap-northeast-1.amazonaws.com/dev/say
{"errorMessage":"2016-03-01T07:14:09.991Z 2d851397-df7d-11e5-b9a2-494cd39b6049 Task timed out after 6.00 seconds"}

```
えらい具体的なエラーが出たけどまあ想定通りです。

Console にログインして、Lambda VPC の設定を行います。

* Lambda > Functions > {該当のファンクション} の Configuration タブを表示
 * Role: 「Basic with VPC」から新たに作成
 * VPC: Default VPC を選択
 * Security Groups から RDS に設定したのと同じものを選択
![ScreenShot 2016-03-01 at 16.23.00.png](https://qiita-image-store.s3.amazonaws.com/0/10160/e02d89e1-9665-c4b0-3390-4cc9ec44205f.png "ScreenShot 2016-03-01 at 16.23.00.png")
* Save and Test
	* テスト用入力値の JSON が表示されるので、event.js と同様に記述する

![ScreenShot 2016-03-01 at 16.29.49.png](https://qiita-image-store.s3.amazonaws.com/0/10160/22c33710-e407-549b-eb23-b8f7539dadf7.png "ScreenShot 2016-03-01 at 16.29.49.png")

成功しました。

curl からもう一度実行してみます

```bash
$ curl -v -d '{"text":"Piyo-"}' https://***.execute-api.ap-northeast-1.amazonaws.com/dev/say
{"message":"Your Message: Piyo-"}
```

RDS からも確認

```bash
mysql> select * FROM lambda_test;
+----+-------------+---------------------+
| id | text        | created             |
+----+-------------+---------------------+
|  1 | Piyo-       | 2016-03-01 07:34:06 |
+----+-------------+---------------------+
1 rows in set (0.01 sec)

```
OK!!

## カスタムロールの設定
上記で Lambda に Basic with VPC から作成した Role を設定しましたが、このままだとデプロイする度にもともと設定されてたやつに戻ってしまうので、s-function.json の customRole を設定します。

```greetings/say/s-function.json 

  "customRole": "arn:aws:iam::***:role/lambda_basic_vpc_execution",

```

## 気になってる点
一通りやってみて気になった下記についても追々調べていこうと思います。

* API のエンドポイントに独自ドメインは使えるんだろうか
* 費用の算出方法
	* EC2 使って API 作ってた時とはまた変わると思うのでどうしようかな？
* 実行速度・スケーラブル具合
	* エンドポイントにめっちゃ負荷かけてみたいけど、自前でやりたくない