AWS Lambdaの関数をnpmでパッケージ管理


この記事は[AWS Lambdaアドベントカレンダー](http://qiita.com/advent-calendar/2014/lambda)の15日めのぶんです。エントリーした当初はRedshiftへのデータロード云々を書こうと思ってたんだけど、すっかりその熱は冷めてしまったので開発手法やパッケージングについて書くことにします。

お約束ですが、これは個人のメモ/意見であり、所属する組織を代表するものではありません。

## はじめに

以前[AWS Lambdaのための関数のローカル開発とテスト](http://qiita.com/imaifactory/items/092e32b4fc0f69a94061)というのを書いたのだけど、この
記事もそこベースにして続きを書き足している感じなのでだいぶかぶっている部分があるのはご容赦を。

## 考え方

AWSのサービスを使った開発をするときによく出てくるネタとしてあるのが「ローカルでの開発」。たとえばDynamoDBだったら[DynamoDB Local](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.html)というモックみたいなものが配布されていて、基本的な考え方としては本物のサービスを使わずにローカルで動く代替物を用意して開発しようという話。

一方Lambdaはどうなのかというと **自分で実装した関数がAWSサービスから呼び出される**サービスなので、モックではなくてドライバ的なものが必要、ということになる。現在のところLambdaを外から見る限り、以下のように実装した関数が呼び出されている。(app.jsというファイルにhandlerという名前でイベントハンドラを実装した場合）したがって、下記のようなドライバがあればよいということにあんる。

```javascript:Lambda
var yourLambdaFunction = require('./app');
yourLambdaFunction.handler(event,context);
```

[eventオブジェクトやcontextオブジェクトについての補足は前回の記事に書いたので気になる方はそちらをどうぞ。](http://qiita.com/imaifactory/items/092e32b4fc0f69a94061)

```javascript:driver.js
// We will put dummy objects for event and context;
var event = {};
var context = {
  // InvokeID may be unique invocation id for AWS Lambda.
  invokeid: 'string',
  // context.done() should be called for end of each invocation.
  // We would want to stub this.
  done: function(err,data){
    return;
  }
};

// Then we can load and run your function.
var yourLambdaFunction = require('./app');
yourLambdaFunction.handler(event,context);
```

以上のようなしくみを利用すればローカルでの動作確認だけではなく、いわゆる自動テスト等も実装できるようになる。例えば以下のようなイメージだ。

```javascript:test/basic.js
var assert = require('assert');

var data = {}; // Your event
var context = {
    invokeid: 'invokeid',
    done: function(err,message){
        return;
    }
};

describe('myFirstLambda',function(){
    it('Should have event as a property', function(){
        var lambda = require("../app");
        lambda.handler(data,context);
        assert(lambda.event);
        assert.deepEqual(data,lambda.event);
    });
});

```

## パッケージ管理

テストを走らせることができるようになったので、ついでにパッケージ管理もしたくなる・・・ような気がするのでその辺も書いてみた。ここからが前回の記事からの続きの部分。node.jsにはnpmというパッケージ管理システム（rubyでいうgem、PHPでいうcomposerのようなもの）があるので、今回はこれに乗っかりたいと思う。ちなみに今回の **パッケージ管理**はnpmリポジトリでモジュールを配りましょう！という話ではなく、npmを使ってライブラリの依存関係やテストからデプロイまでを管理しましょうというお話。

npmにはバージョン番号の管理や依存関係の管理を始めとしていろいろな機能があるが、今回は以下の部分につかっている。

- 依存するnpmモジュールの管理
- スクリプトの管理。今回用意したのは以下のスクリプト。
 - ユニットテストやLintなどのスクリプト。
 - Lambdaにアップロードするためのパッケージング用のスクリプト。
 - Lambdaアップロードのためのもろもろの設定（リージョンやらなにやら）を準備するスクリプト。
 - 実際にアップロードするスクリプト。

これらを利用して、今回は以下のような流れでAWS Lambdaのための関数の開発からデプロイまでを行えるような[サンプルプロジェクト](https://github.com/imaifactory/myFirstLambda)を作ってみた。

1. ローカルで開発
2. **npm test**でユニットテストやLintを走らせる
3. **npm build**でパッケージ(Lambda用のzipファイル)のビルド
4. **npm initLambda**でLambda用の設定ファイルを作る
5. **npm publish**でLambdaにアップロード

## 実際にやってみる

順を追って見て行きたいと思うので、まずはpackage.jsonはから。一般的なnpmのお作法通りだが、今回特に必要になる部分は以下の3つ。

 - 本番で動くモジュールは **dependencies**に定義。
 - ローカル開発に必要なモジュール群を **devDependencies**に定義。aws-sdkはLambda側のランタイム環境に含まれているので、特定のバージョンが使いたい、みたいなことがないかぎりこちらからアップロードする必要はない。
 - 上記で触れたスクリプト群は **script**に定義。

```json:package.json
{
  "name": "myFirstLambda",
  "version": "0.0.1",
  "private": true,
  "engines" : { "node" : "0.10.32" },
  "main": "app.js",
  "dependencies": {},
  "devDependencies": {
    "aws-sdk":"2.1.2", 
    "mocha": "*",
    "eslint": "*",
    "istanbul": "*"
  },
  "scripts" : {
    "test" : "npm -s run-script lint && npm -s run-script unit",
    "unit" : "istanbul `[ $COVERAGE ] && echo 'cover _mocha' || echo 'test mocha'` -- test test/basic",
    "lint" : "eslint ./*.js",
    "initLambda" : "node ./script/initLambda.js",
    "build": "node ./script/build.js",
    "publish": "node ./script/publish.js"
  }
}
```
### Testing/Linting

テストやLintには基本的になんのモジュールでも使える。このサンプルではistanbul, mocha, eslintを使ってみた。(aws sdk for node.jsでやってる方法をそのまま真似してみた)

### Building  

アップロード用のファイルはZip形式に圧縮されている必要があるので、このZipファイルをパッケージに見立てて、そのファイル作成をビルドと呼ぶことにしている。パッケージのディレクトリ構成は以下のようにしてやる必要がある。

```bash:sample.zip
sample.zip
  |-app.js
  |-your_library_directory/
  |-node_modules/
```

```javascript:build.js
var exec = require('child_process').exec,
    package = require('../package');

var COMMAND_PREFIX = 'rm -fr pkg; mkdir pkg; zip -r';

var target = package.name,
    main = package.main,
    npm_dir = 'node_modules',
    excludes_str = '';

Object.keys(package.devDependencies).forEach(function(key){
  excludes_str = excludes_str + build_exclude_str(key);
});
exec(build_command_str(target,main,excludes_str),function(err,stdout,stderr){
  if(err) console.log(err);
  console.log(stdout);
  console.log(stderr);
});

function build_exclude_str (key){
  return npm_dir + '/' + key + '\\* ';
}

function build_command_str(target,main,excludes_str){
  return COMMAND_PREFIX
         + ' pkg/' +
         target
         + '.zip ' +
         main
         + ' ' +
         npm_dir
         + ' -x ' +
         excludes_str;
}
```

### Uploading 

ここまで来たらあとはこのZipファイルのパッケージをLambdaにアップロードしてやればOK!なんだけど、アップロード時にもろもろの引数をわたしてやる必要があるので設定ファイルを出力するスクリプトを一段はさむ。

```javascript:lambdaConfig.json
{
  "region": "",  // AWS Lambda region
  "description": "", // Description for the function
  "role": "", // AWS Lambda execution IAM Role
  "memorySize": "128", // Memory size to be allocated in AWS Lambda
  "timeout": "3", // Timeout for the function in AWS Lambda
  "handlerFile": "", // File name to be invoked in AWS Lambda
  "handlerMethod": "" // Method name to be invoked in AWS Lambda
}
```

上記のようなJSONを単純に吐き出すだけのスクリプト。

```javascript:initLambdaConfig.js
var fs = require('fs');
var config = {
  region: '',
  description: '',
  role: '',
  memorySize: '128',
  timeout: '3'
};
fs.writeFileSync('./lambdaConfig.json',JSON.stringify(config));

```

やっとアップロードできる！ということで **npm publish**したら以下のようなスクリプトが起動される感じにしてみた。これは[awscli](http://aws.amazon.com/cli)を使ってシェルで実装することも考えられたが、npmで制御できない依存関係を作りたくなかったのでnode.jsで実装した。

```javascript:publish.js
var aws = require('aws-sdk'),
    fs  = require('fs'),
    lambdaConfig = require('../lambdaConfig'),
    pkgConfig = require('../package');

var lambda  = new aws.Lambda({region: lambdaConfig.region}),
    zipPath = 'pkg/' + pkgConfig.name + '.zip';

var params = {
  FunctionName: pkgConfig.name,
  FunctionZip: fs.readFileSync(zipPath),
  Handler: buildHnadlerName(lambdaConfig),
  Mode: 'event',
  Role: lambdaConfig.role,
  Runtime: 'nodejs',
  Description: lambdaConfig.description,
  MemorySize: lambdaConfig.memorySize,
  Timeout: lambdaConfig.timeout
};
lambda.uploadFunction(params, function(err, data) {
  if (err) console.log(err, err.stack);
  else     console.log(data);
});


function buildHnadlerName(lambdaConfig){
  return lambdaConfig.handlerFile + '.' + lambdaConfig.handlerMethod;
}

```

## まとめ

ということで、何をやってきたかをまとめてみると、以下のようなステップを実現してみようぜという話でした。

1. ローカルで開発
2. **npm test**でユニットテストやLintを走らせる
3. **npm build**でパッケージ(Lambda用のzipファイル)のビルド
4. **npm initLambda**でLambda用の設定ファイルを作る
5. **npm publish**でLambdaにアップロード

あとは **6. npm invoke -args ... で本番のLambdaを動かしてみる**くらいまで出来てもいいかなぁと思ったのだけど、戻り値の受け取り方なのでいろいろ悩むところがあったのでそれはまたこんど。

## ライセンス

この記事中に記載されているコードスニペットはすべてMITライセンスにて提供されるものとします。