nodebrew + npm + CoffeeScript + gulp で作る AWS Lambda の自動デプロイ環境


#はじめに
Lambda を開発する環境について色々と調べたのですが、[Fluct](https://github.com/r7kamura/fluct) や [lamjet](http://qiita.com/yuyakato/items/268ee2e2059bef3c6717) 、[node-lambda](https://www.npmjs.com/package/node-lambda) のようなオールインワンタイプのビルドツールを使うのが便利そうです。
こういうツールも良いなあと思うのですが、環境によってアプリケーションをデプロイする手順が違うし、場合によって事前にやっておいて欲しい作業などがあったりします。
そんな面倒くさい作業を全部請け負ってくれる痒いところに手が届くような環境を gulp を使って構築してみました。

なお「AWS Lambda とは」「 gulp とは」については、この記事で詳しく触れていませんのであしからず。

#セットアップする環境
MacOS Version 10.10.4 Yosemite

```
$ uname -mrsv
Darwin 14.5.0 Darwin Kernel Version 14.5.0: Wed Jul 29 02:26:53 PDT 2015; root:xnu-2782.40.9~1/RELEASE_X86_64 x86_64
```

#セットアップするもの
- node.js
- npm
- CoffeeScript
- gulp


#セットアップ

##1. nodebrew のインストール

nodebrew はバージョンの違う node.js を管理するためのツールです。

nodebrew のダウンロードとインストールします。
~/.nodebrew へインストールされます。

```
$ curl -L git.io/nodebrew | perl - setup
```

nodebrew へのパスを通す

```
$ vim ~/.zshrc

export PATH=$HOME/.nodebrew/current/bin:$PATH
```

.zshrc の再読み込み

```
$ . ~/.zshrc
```

パスが通ってるか確認

```
$ nodebrew
```

## 2. node.js のインストール

![node.png](https://qiita-image-store.s3.amazonaws.com/0/12391/cae2aaff-a52f-e0c4-a363-aa95effb2866.png)
Lambda で 動作している node.js のバージョンは **v0.10.33** なので、同じバージョンをローカルへインストールします。

[ 参考 ]
[Lambda 実行環境と利用できるライブラリ](http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/current-supported-versions.html)

インストール可能な node.js のバージョン一覧を確認

```
$ nodebrew ls-remote
```

v0.10.33 を指定してインストール

```
$ nodebrew install-binary v0.10.33
```

インストール済みの version を確認

```
$ nodebrew ls
v0.10.33

current: none
```

利用する node.js の version を指定

```
$ nodebrew use v0.10.33
```

v0.10.33 に切り替わっているか確認

```
$ nodebrew ls
v0.10.33

current: v0.10.33
```

```
$ node -v
v0.10.33
```

##3. npm で外部ライブラリをインストール

![npm.png](https://qiita-image-store.s3.amazonaws.com/0/12391/4e254c11-a80f-b922-6c37-fc89a5ceee2a.png)

Lambda で利用する外部ライブラリは npm で管理します。
npm ( Node Package Manager ) は node.js をインストールすれば使えるようになるので、インストール等の手順は必要ありません。
次の手順で利用する glup もここでインストールしておきましょう。

- インストールするライブラリの一覧
    - **gulp**
        - gulp ライブラリ本体
    - **gulp-coffee**
        - gulp で CoffeeScript をコンパイルする 
    - **gulp-install**
        - package.json を元に所定のディレクトリへライブラリをインストールする
    - **gulp-zip**
        - gulp で zip 圧縮/解凍 を扱う
    - **del**
        - ファイルやディレクトリを削除する
    - **node-aws-lambda**
        - gulp で Lambda を管理する
    - **run-sequence**
        - gulp で登録したタスクを直列/並列で処理する

Lambda Function の本体を入れるディレクトリを作成

```
$ mkdir sample
$ cd sample
```

npm の package.json(パッケージの依存関係を記述するjsonファイル)を生成します。
生成時に色々と聞かれますが、後で変更可能なので全て Enter で(デフォルトで)設定を終えます。

```
$ npm init
```

開発時に利用するライブラリをインストールします。
`npm install` コマンド実行時に `--save-dev` オプションを付与することで、package.json に 開発用途(devDependencies) という括りでパッケージ一覧が記録されます。

```
$ npm install gulp gulp-coffee coffee-script gulp-zip node-aws-lambda gulp-install del run-sequence --save-dev
```

実行環境で利用するライブラリをインストールします。
`--save` オプションを付与することで、実行環境で利用する依存ライブラリとして package.json にパッケージ一覧が記録されます。

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

上記コマンド実行後、 package.json を見てみると依存パッケージがちゃんと記録されています。

```
{
  "name": "sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "coffee-script": "^1.10.0",
    "del": "^2.0.2",
    "gulp": "^3.9.0",
    "gulp-coffee": "^2.3.1",
    "gulp-install": "^0.6.0",
    "gulp-zip": "^3.0.2",
    "node-aws-lambda": "^0.1.6",
    "run-sequence": "^1.1.4"
  },
  "dependencies": {
    "request": "^2.65.0",
    "aws-sdk": "^2.2.15"
  }
}
```

ここまでの作業で、ディレクトリの構成は下記のようになっているはずです。

```
sample
├── node_modules
│   ├── aws-sdk
│   ├── coffee-script
│   ├── del
│   ├── gulp
│   ├── gulp-coffee
│   ├── gulp-install
│   ├── gulp-zip
│   ├── node-aws-lambda
│   ├── request
│   └── run-sequence
└── package.json
```

##4. gulp のタスク作成と実行環境の作成

<img src="https://qiita-image-store.s3.amazonaws.com/0/12391/17fc4dc8-9329-e30a-8ae5-8119fc7633a4.png" width="100">

gulp ( ガルプ ) は Node.js を使ったフロントエンド寄りのタスクランナーで、アプリケーションのデプロイ時に必要な様々な作業を自動化することが出来ます。

今回は、gulp を使って `CoffeeScript をコンパイル -> 外部ライブラリとまとめてzip化 -> AWS Lambda へアップロード -> Lambda Function 実行 -> Lambda Function 実行結果ログを出力` という一連の作業を全て自動化します。

まずは本体となる Lambda Function を書きます。
request ライブラリを読み込み、`hello world` を呟いて終了するというシンプルなものです。

```
$ vim index.coffee
```

```index.coffee
request = require('request')

exports.handler = (event, context)->
  console.log(event)
  console.log('hello world')
  context.done()
```

Lambda へデプロイするための設定ファイルを準備します。
アクセスキー、シークレットアクセスキー、ロール は適宜書き換えてください。
後述する `node-aws-lambda` で利用します。

```lambda-config.js
module.exports = {
  accessKeyId: 'AKIXXXXXXXXXXXXXXXXXXX',
  secretAccessKey: 'XXXXXXXXXXXXXXXXXXXXXXX',
  region: 'ap-northeast-1',
  handler: 'index.handler',
  role: 'arn:aws:iam::xxxxxxxxxxxxxx:role/xxxxxxxxxxxxxxx',
  functionName: 'SampleFunction',
  timeout: 10,
  memorySize: 128
}
```

gulpfile.coffee ファイルを作成して、実行するタスクを書きます。

```gulpfile.coffee
# 必要なライブラリの読み込み
aws         = require('aws-sdk')
zip         = require('gulp-zip')
del         = require('del')
gulp        = require('gulp')
coffee      = require('gulp-coffee')
install     = require('gulp-install')
runSequence = require('run-sequence')
awsLambda   = require('node-aws-lambda')

# CoffeeScript をコンパイル
gulp.task 'coffee', ()->
  gulp.src ['./*.coffee', '!gulpfile.coffee']
      .pipe coffee({bare:true})
      .pipe gulp.dest('dist')

# 外部ライブラリをzip化するディレクトリに移動
# dependencies のライブラリのみ移動する
gulp.task 'module-install', ->
  return gulp.src './package.json'
    .pipe gulp.dest('dist/')
    .pipe install({production: true})

# ディレクトリをzip化する
gulp.task 'zip', ->
  return gulp.src ['dist/**/*', '!dist/package.json']
    .pipe zip('dist.zip')
    .pipe gulp.dest('./')

# 一時ファイルを削除する
gulp.task 'clean', (callback)->
  del ['./dist', './dist.zip'], callback

# Lambda Function をデプロイ
gulp.task 'lambda-deploy', (callback)->
  awsLambda.deploy './dist.zip', require("./lambda-config.js"), callback

# 上記タスクを直列で順番に実行する
gulp.task 'deploy', (callback)->
  runSequence(
    ['clean'],
    ['coffee'],
    ['module-install'],
    ['zip'],
    ['lambda-deploy']
    callback
  )
```

gulp のタスクを実行します。
gulp はグローバルでインストールしていないため、`$(npm bin)` で node_module 配下にある gulp を呼び出します。

```
$ $(npm bin)/gulp deploy

[00:29:20] Requiring external module coffee-script/register
[00:29:21] Using gulpfile ~/sample/gulpfile.coffee
[00:29:21] Starting 'deploy'...
[00:29:21] Starting 'coffee'...
[00:29:21] Finished 'coffee' after 41 ms
[00:29:21] Starting 'module-install'...
npm WARN package.json sample@1.0.0 No description
npm WARN package.json sample@1.0.0 No repository field.
npm WARN package.json sample@1.0.0 No README data
npm WARN engine hoek@2.16.3: wanted: {"node":">=0.10.40"} (current: {"node":"0.10.33","npm":"1.4.28"})
npm WARN engine boom@2.10.1: wanted: {"node":">=0.10.40"} (current: {"node":"0.10.33","npm":"1.4.28"})
npm WARN engine cryptiles@2.0.5: wanted: {"node":">=0.10.40"} (current: {"node":"0.10.33","npm":"1.4.28"})
aws-sdk@2.2.15 node_modules/aws-sdk
├── xmlbuilder@0.4.2
├── xml2js@0.2.8
└── sax@0.5.3

request@2.65.0 node_modules/request
├── forever-agent@0.6.1
├── aws-sign2@0.6.0
├── caseless@0.11.0
├── stringstream@0.0.5
├── tunnel-agent@0.4.1
├── oauth-sign@0.8.0
├── isstream@0.1.2
├── json-stringify-safe@5.0.1
├── extend@3.0.0
├── node-uuid@1.4.3
├── combined-stream@1.0.5 (delayed-stream@1.0.0)
├── qs@5.2.0
├── tough-cookie@2.2.0
├── mime-types@2.1.7 (mime-db@1.19.0)
├── form-data@1.0.0-rc3 (async@1.5.0)
├── http-signature@0.11.0 (assert-plus@0.1.5, asn1@0.1.11, ctype@0.5.3)
├── bl@1.0.0 (readable-stream@2.0.4)
├── hawk@3.1.1 (cryptiles@2.0.5, sntp@1.0.9, boom@2.10.1, hoek@2.16.3)
└── har-validator@2.0.2 (pinkie-promise@1.0.0, commander@2.9.0, chalk@1.1.1, is-my-json-valid@2.12.3)
[00:29:24] Finished 'module-install' after 3.26 s
[00:29:24] Starting 'zip'...
[00:29:25] Finished 'zip' after 594 ms
[00:29:25] Starting 'lambda-deploy'...
[00:29:26] Finished 'lambda-deploy' after 1.67 s
[00:29:26] Starting 'lambda-run'...
[00:29:26] Finished 'lambda-run' after 3.64 ms
[00:29:26] Starting 'dist-clean'...
[00:29:27] Finished 'dist-clean' after 117 ms
[00:29:27] Finished 'deploy' after 5.69 s
```

無事にデプロイされました。

## 5.ローカルで実行する

デプロイする前にローカルで実行して動作を事前に確認しておきたい、とかあると思います。
ローカルで実行するには、下記のようなドライバスクリプトを用意してそこから自作した関数を呼び出してあげます。

[ 参考 ]
[AWS Lambdaのための関数のローカル開発とテスト](http://qiita.com/imaifactory/items/092e32b4fc0f69a94061)
[AWS Lambdaの関数をnpmでパッケージ管理](http://qiita.com/imaifactory/items/ac81b4a3ff4a5f5dec85)

```driver.coffee
# Lambda Function の event 引数へ渡す値
event = { hoge: "hoge" }

context = {
    invokeid: 'invokeid',
    done: function(err,message){
        return;
    }
}

lambda = require("./index.js")
lambda.handler(event, context)
```

gulpfile にタスクを3つ追加します。

```gulpfile.coffee
# ライブラリを追加
exec = require('child_process').exec

# ドライバスクリプトをdistへコピー
gulp.task 'driver-copy', ->
  gulp.src './driver.js'
    .pipe gulp.dest('dist/')

# ドライバスクリプトを実行
gulp.task 'driver-run', (callback)->
  exec 'node ./dist/driver.js', (err, stdout, stderr)->
    console.log(stdout)
    console.log(stderr)

# 上記タスクを直列で実行
gulp.task 'local-run', (callback)->
  runSequence(
    ['clean']
    ['coffee'],
    ['module-install'],
    ['driver-copy'],
    ['driver-run'],
    callback
  )
```

gulp タスクを実行します。

```
$ $(npm bin)/gulp local-run

[20:26:35] Requiring external module coffee-script/register
[20:26:39] Using gulpfile ~/sample/gulpfile.coffee
[20:26:39] Starting 'local-run'...
[20:26:39] Starting 'dist-clean'...
[20:26:40] Finished 'dist-clean' after 117 ms
[20:26:40] Starting 'coffee'...
[20:26:40] Finished 'coffee' after 21 ms
[20:26:40] Starting 'module-install'...
npm WARN package.json sample@1.0.0 No description
npm WARN package.json sample@1.0.0 No repository field.
npm WARN package.json sample@1.0.0 No README data
npm WARN engine cryptiles@2.0.5: wanted: {"node":">=0.10.40"} (current: {"node":"0.10.33","npm":"1.4.28"})npm WARN engine hoek@2.16.3: wanted: {"node":">=0.10.40"} (current: {"node":"0.10.33","npm":"1.4.28"})
npm WARN engine boom@2.10.1: wanted: {"node":">=0.10.40"} (current: {"node":"0.10.33","npm":"1.4.28"})
request@2.65.0 node_modules/request
├── forever-agent@0.6.1
├── aws-sign2@0.6.0
├── caseless@0.11.0
├── stringstream@0.0.5
├── tunnel-agent@0.4.1
├── oauth-sign@0.8.0
├── isstream@0.1.2
├── json-stringify-safe@5.0.1
├── extend@3.0.0
├── node-uuid@1.4.3
├── qs@5.2.0
├── combined-stream@1.0.5 (delayed-stream@1.0.0)
├── tough-cookie@2.2.0
├── mime-types@2.1.7 (mime-db@1.19.0)
├── form-data@1.0.0-rc3 (async@1.5.0)
├── http-signature@0.11.0 (assert-plus@0.1.5, asn1@0.1.11, ctype@0.5.3)
├── bl@1.0.0 (readable-stream@2.0.4)
├── hawk@3.1.1 (cryptiles@2.0.5, sntp@1.0.9, boom@2.10.1, hoek@2.16.3)
└── har-validator@2.0.2 (pinkie-promise@1.0.0, commander@2.9.0, chalk@1.1.1, is-my-json-valid@2.12.3)
[20:26:43] Finished 'module-install' after 3.53 s
[20:26:43] Starting 'driver-copy'...
[20:26:43] Finished 'driver-copy' after 4.97 ms
[20:26:43] Starting 'driver-run'...
{ hoge: 'hoge' }
hello world
```

# おまけ 本番環境で実行する

最後に、本番環境で関数を実行して結果を取得してみましょう。
デプロイする部分までは出来ているので、あとは実行して結果を取得するだけです。

gulpfile にタスクを追加します。
アクセスキーシークレットアクセスキー等の設定情報は `lambda-config.js` の値を持ってきています。

```gulpfile.coffee
gulp.task 'lambda-run', ->
  config = require("./lambda-config.js")
  aws.config.update({
    accessKeyId: config.accessKeyId,
    secretAccessKey: config.secretAccessKey,
    region: config.region
  })
  lambda = new aws.Lambda()

  # Lambda Function に渡す値
  payload = { hoge: "fuga" }

  lambda.invoke(invoke_params = {
    FunctionName: config.functionName,
    InvocationType: 'RequestResponse',
    LogType: 'Tail',
    Payload: JSON.stringify(payload)
    }, (err, data)->
      if err?
        console.log(err)
        return
      console.log(new Buffer(data.LogResult, 'base64').toString())
  )
```

`deploy` タスクに `lambda-run` を追加します。

```gulpfile.coffee
gulp.task 'deploy', (callback)->
  runSequence(
    ['clean']
    ['coffee'],
    ['module-install'],
    ['zip'],
    ['lambda-deploy'],
    ['lambda-run']
    callback
  )
```

gulp タスクを実行します。

```
$(npm bin)/gulp deploy

[20:27] Requiring external module coffee-script/register
[20:28] Using gulpfile ~/sample/gulpfile.coffee
[20:28] Starting 'deploy'...
[20:28] Starting 'dist-clean'...
[20:28] Finished 'dist-clean' after 79 ms
[20:28] Starting 'coffee'...
[20:28] Finished 'coffee' after 25 ms
[20:28] Starting 'module-install'...
npm WARN package.json sample@1.0.0 No description
npm WARN package.json sample@1.0.0 No repository field.
npm WARN package.json sample@1.0.0 No README data
npm WARN engine hoek@2.16.3: wanted: {"node":">=0.10.40"} (current: {"node":"0.10.33","npm":"1.4.28"})
npm WARN engine boom@2.10.1: wanted: {"node":">=0.10.40"} (current: {"node":"0.10.33","npm":"1.4.28"})
npm WARN engine cryptiles@2.0.5: wanted: {"node":">=0.10.40"} (current: {"node":"0.10.33","npm":"1.4.28"})
request@2.65.0 node_modules/request
├── aws-sign2@0.6.0
├── forever-agent@0.6.1
├── caseless@0.11.0
├── stringstream@0.0.5
├── tunnel-agent@0.4.1
├── oauth-sign@0.8.0
├── isstream@0.1.2
├── json-stringify-safe@5.0.1
├── extend@3.0.0
├── node-uuid@1.4.3
├── combined-stream@1.0.5 (delayed-stream@1.0.0)
├── qs@5.2.0
├── mime-types@2.1.7 (mime-db@1.19.0)
├── tough-cookie@2.2.0
├── form-data@1.0.0-rc3 (async@1.5.0)
├── http-signature@0.11.0 (assert-plus@0.1.5, asn1@0.1.11, ctype@0.5.3)
├── har-validator@2.0.2 (pinkie-promise@1.0.0, commander@2.9.0, chalk@1.1.1, is-my-json-valid@2.12.3)
├── bl@1.0.0 (readable-stream@2.0.4)
└── hawk@3.1.1 (cryptiles@2.0.5, sntp@1.0.9, boom@2.10.1, hoek@2.16.3)
[20:30] Finished 'module-install' after 2.32 s
[20:30] Starting 'zip'...
[20:30] Finished 'zip' after 367 ms
[20:30] Starting 'lambda-deploy'...
[20:32] Finished 'lambda-deploy' after 1.45 s
[20:32] Starting 'lambda-run'...
[20:32] Finished 'lambda-run' after 14 ms
[20:32] Finished 'deploy' after 4.26 s
START RequestId: 918d4566-86c2-11e5-baa8-65c58268480f Version: $LATEST
2015-11-09T11:16:34.312Z	918d4566-86c2-11e5-baa8-65c58268480f	{ hoge: 'hoge' }
2015-11-09T11:16:34.313Z	918d4566-86c2-11e5-baa8-65c58268480f	hello world
END RequestId: 918d4566-86c2-11e5-baa8-65c58268480f
REPORT RequestId: 918d4566-86c2-11e5-baa8-65c58268480f	Duration: 3.51 ms	Billed Duration: 100 ms 	Memory Size: 128 MB	Max Memory Used: 34 MB
```

正常に実行され、結果 ( hello world ) が返ってきてますね。

```
2015-11-09T11:16:34.312Z	918d4566-86c2-11e5-baa8-65c58268480f	{ hoge: 'hoge' }
2015-11-09T11:16:34.313Z	918d4566-86c2-11e5-baa8-65c58268480f	hello world

```