API Gateway + Lambda の構成は、素で作成すると個別の endpoint の登録作業が膨大でかなり面倒なため、何かしらのフレームワークを使わないとメンテできない問題があります。
個人的に Express で開発したものをほぼそのまま Lambda 化する仕組みを作って運用していたのですが、安定運用出来ているのと体裁がほぼ整ってきたのもあるので aglex として公開しました。
特長
- Express で作ったものが(ほぼ)そのまま Lambda 化できる
- Lambda function が1つにまとまるので、メンテがしやすい
- Lambda function、API Gateway をコマンドで簡単に作成・更新できる
Lambda の実体は Express を call するための薄い wrapper なので動作も軽く、凝ったことをしていない限りは Express で書いたコードがそのまま動くと思います。
開発経緯
- もともと node には Express というデファクトスタンダードな軽量フレームワークがある
- 開発のノウハウも随所に蓄積されている
- 開発速度を高めるため、Express で開発を進めて頃合いを見て Lambda へのコンバートを狙っていた
- 当初は API Gateway のドキュメント自体が不足しており、サポートに問い合わせて初めて内部仕様がわかる状態だったのでいきなり API Gateway を使うのは結構辛かった
- API Gateway + Lambda はローカルでテストができないため、Express ベースでテストしたかった
既に API Gateway + Lambda で開発できるフレームワークとして fluct や JAWS Framework もあるんですが、
- endpoint 毎に個別の lambda function がたくさんできるのが不都合だった
- lambda function を他の用途でもいろいろ活用しているため、数が増えるとカオスになりそう
- endpoint 間での共通モジュールの扱いの問題
- fluct や JAWS を見つけた時には既に開発着手していて、ある程度目処が立っていた
という理由で見送ってます。
簡易動作手順
1. aglex をインストールする
グローバルでインストールしておきます。
npm install aglex -g
aglex --help
で help が確認できます。
2. Express のプロジェクトを作成して開発する
express-generator 等を使って普通に作りましょう。
npm install express-generator -g
express myapp
cd myapp && npm install
お手軽に試すのであれば、routes/users.js
をちょっと編集して JSON を返すようにしておきます。
6c6
< res.send('respond with a resource');
---
> res.json({message: 'respond with a resource'});
3. AWS Lambda 用の実行ロールを作成する
Lambda 実行用のロール lambda-api
を作成します。
まずは、ロールを作成する際に割り当てる信頼ポリシーの JSON を作成します。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
作成した JSON を指定してロールを作成します。
aws iam create-role --role-name lambda-api --assume-role-policy-document file://assumeRolePolicy.json
作成したロール lambda-api
に AWS 管理ポリシー AWSLambdaExecute
をアタッチします。
aws iam attach-role-policy --role-name lambda-api --policy-arn arn:aws:iam::aws:policy/AWSLambdaExecute
4. aglex 用の config yaml を生成する
generate config で標準出力に設定の雛形を吐くので、適当にファイルに保存します。
aglex generate config > aglex.yml
設定ファイルを編集して、Lambda や API Gateway の情報を設定します。
RoleName には、先ほど作成した lambda-api
を設定します。
API の endpoint(resource & method)は、GET /users
だけになるよう修正します。
13c13
< region: us-east-1
---
> region: ap-northeast-1
20,22c20,22
< FunctionName: YOUR_LAMBDA_FUNCTION_NAME
< Description: YOUR_LAMBDA_DESCRIPTION
< RoleName: YOUR_LAMBDA_EXECUTION_ROLE
---
> FunctionName: myapp
> Description: myapp
> RoleName: lambda-api
26,27c26,27
< name: YOUR_API_NAME
< description: YOUR_API_DESCRIPTION
---
> name: myapp
> description: myapp
53c53
< /path/to/static/endpoint:
---
> /users:
55,58d54
< /path/to/dynamic/endpoint/{param}:
< - GET
< - PUT
< - OPTIONS
5. Lambda 用ハンドラを生成する
以下のコマンドで、Lambda のエントリー用ハンドラを生成します。
aglex generate lambda-handler > lambda.js
CoffeeScript が好みの場合は、--coffee
を付加すれば OK です。
aglex generate lambda-handler --coffee > src/lambda.coffee
6. Lambda 登録用 zip ファイルの生成
簡易的に zip コマンドで必要なファイルを纏めて zip 化します。
zip -r lambda.zip app.js lambda.js routes views node_modules
実際にプロジェクトとして継続開発するのであれば、Gulp や Grunt で生成するほうがいいでしょう。
AWS Lambda を作成・更新するための gulpfile 雛形
7. Lambda を登録・更新する
Lambda の登録も更新も同じコマンドで行います。
生成した設定ファイルと zip ファイルを指定してください。
aglex --config aglex.yml lambda update --zip lambda.zip
このコマンドは Express のコードを修正して Lambda に反映するたびに利用します。
8. Lambda function に API Gateway からの呼び出しを許可する
作成した Lambda function に API Gateway からアクセスできるようにする許可を与えます。
この設定は Lambda 作成時の初回のみで OK です。
aglex --config aglex.yml lambda add-permission
9. API を登録・更新する
設定ファイルの定義に従って、API の登録・更新を行います。
aglex --config aglex.yml apigateway update
このコマンドは設定ファイルで API の endpoint を追加・修正するたびに利用します。
10. API の stage へのデプロイ
test という stage にデプロイするには、以下のようにします。
aglex --config aglex.yml apigateway deploy --stage test
その他のオプションは、
aglex apigateway deploy --help
を参照してください。
11. アクセスしてみる
生成された API の endpoint にアクセスしてみます。
URL ベースは、https://<REST_API_ID>.execute-api.<REGION>.amazonaws.com/<STAGE_NAME>
となります。
https://<REST_API_ID>.execute-api.<REGION>.amazonaws.com/<STAGE_NAME>/users
にアクセスすると、正しく設定できていれば以下の様な応答が得られるはずです。
{
"message": "respond with a resource"
}
このまま利用してもいいのですが、Custom Domain Names から独自ドメインを設定するといい感じになります。
gulp/grunt からの利用
ライブラリとしても利用できるようになっていますので、gulp/grunt と連携することもできます。
var fs = require('fs');
var gulp = require('gulp');
var argv = require('yargs').argv;
var yaml = require('js-yaml');
var config = yaml.safeLoad(fs.readFileSync('aglex.yml', 'utf8'));
var aglex = require('aglex')(config, 'info');
gulp.task('updateLambda', ['zip'], function(done) {
aglex.updateLambda('dist/lambda.zip').then(function() { done(); });
return;
});
gulp.task('addLambdaPermission', function(done) {
aglex.addLambdaPermission().then(function() { done(); });
return;
});
gulp.task('updateApi', function(done) {
aglex.updateApi().then(function() { done(); });
return;
});
gulp.task('deployApi', function(done) {
if (!argv.stage) {
console.log('Please use --stage STAGENAME');
return;
}
aglex.deployApi(argv.desc, argv.stage, argv.stagedesc).then(function() { done(); });
return;
});
Lambda の zip 化までは省略しています。
所感
実際に管理画面用内部 API サービスとして S3 や SimpleDB/DynamoDB 等を組み合わせて利用しているのですが、ちょっと応答速度が遅い以外は問題なく利用できています。
Lambda 自体の無料枠が比較的大きいため、特に内部用途や利用者が限定されるサービスであればこれで十分な気がします。
1 function にまとめることで共通のモジュールも作りやすいですし、開発が非常に楽です。
デメリットは、コードサイズが大きくなることによる展開までの時間+実行時のメモリ使用量になるかと思いますが、動かしている感じでは
- API サービスはそこまでコードサイズが肥大化しにくい
- リクエストを dispatch する Express の薄い層があるだけでいうほどメモリは食わない
です。現状はメモリを最低の 128MB で動作させていますが十分ですね。
補足:動作の仕組み
Express 自体は非常に薄いフレームワークであり、実体としては 単なる function です。
var app = express();
通常は http.createServer() に 引数として渡し、request イベントが発生したタイミングで call されます。
var server = http.createServer(app);
Lambda function が実行された際にこの呼び出しを偽装してやれば、あとは Express でうまく処理してくれるはずなので、どうやって偽装するか、に問題が移ります。
Lambda は実行時に event データをオブジェクト形式で受け取りますが、通常の HTTP リクエストとは異なり、URL や Header 等に関する情報は標準では存在しません。
これらの情報を event データに付加するために、API Gateway の Integration Request にある Mapping Template を利用します。
{
"method": "$context.httpMethod",
"remoteAddr": "$context.identity.sourceIp",
"stage": "$context.stage",
"path": "$context.resourcePath",
"headers": {
#foreach ($key in $input.params().header.keySet())
"$key": "$util.escapeJavaScript($input.params().header.get($key))",
#end
"User-Agent": "$context.identity.userAgent"
},
"pathParams": {
#foreach ($key in $input.params().path.keySet())
"$key": "$util.escapeJavaScript($input.params().path.get($key))"#if ($foreach.hasNext),#end
#end
},
"queryParams": {
#foreach ($key in $input.params().querystring.keySet())
"$key": "$util.escapeJavaScript($input.params().querystring.get($key))"#if ($foreach.hasNext),#end
#end
},
"body": $input.json('$')
}
上記のような Mapping Template を指定することで、通常の HTTP リクエストに含まれる
- method
- remote address
- path
- request header
- query string
- body
等の情報を event オブジェクトとして Lambda に渡すことができます。
あとは、渡された event オブジェクトを元に HTTP リクエストを偽装すればよいことになります。
この部分を担当しているのが、薄い Lambda wrapper である lambda.js
です。
lambda.js
では、event オブジェクトを元に http.IncomingMessage
および http.ServerResponse
を生成し、Express app に受け渡す部分を担当しています。