aglex: API Gateway + Lambda + Express4 で サーバレス API を作成する


API Gateway + Lambda の構成は、素で作成すると個別の endpoint の登録作業が膨大でかなり面倒なため、何かしらのフレームワークを使わないとメンテできない問題があります。

個人的に Express で開発したものをほぼそのまま Lambda 化する仕組みを作って運用していたのですが、安定運用出来ているのと体裁がほぼ整ってきたのもあるので [aglex](https://www.npmjs.com/package/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](https://github.com/fluct/fluct) や [JAWS Framework](https://github.com/jaws-framework/JAWS) もあるんですが、

- endpoint 毎に個別の lambda function がたくさんできるのが不都合だった
  - lambda function を他の用途でもいろいろ活用しているため、数が増えるとカオスになりそう
  - endpoint 間での共通モジュールの扱いの問題
- fluct や JAWS を見つけた時には既に開発着手していて、ある程度目処が立っていた

という理由で見送ってます。

## 簡易動作手順

### 1. aglex をインストールする

グローバルでインストールしておきます。

```bash:
npm install aglex -g
```

> `aglex --help` で help が確認できます。

### 2. Express のプロジェクトを作成して開発する

express-generator 等を使って普通に作りましょう。

```bash:
npm install express-generator -g
express myapp
cd myapp && npm install
```

お手軽に試すのであれば、`routes/users.js` をちょっと編集して JSON を返すようにしておきます。

```diff:
6c6
<   res.send('respond with a resource');
---
>   res.json({message: 'respond with a resource'});
```

### 3. AWS Lambda 用の実行ロールを作成する

Lambda 実行用のロール `lambda-api` を作成します。
まずは、ロールを作成する際に割り当てる信頼ポリシーの JSON を作成します。

```assumeRolePolicy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
```

作成した JSON を指定してロールを作成します。

```bash:
aws iam create-role --role-name lambda-api --assume-role-policy-document file://assumeRolePolicy.json
```

作成したロール `lambda-api` に AWS 管理ポリシー `AWSLambdaExecute` をアタッチします。

```bash:
aws iam attach-role-policy --role-name lambda-api --policy-arn arn:aws:iam::aws:policy/AWSLambdaExecute
```

### 4. aglex 用の config yaml を生成する

generate config で標準出力に設定の雛形を吐くので、適当にファイルに保存します。

```bash:
aglex generate config > aglex.yml
```

設定ファイルを編集して、Lambda や API Gateway の情報を設定します。

RoleName には、先ほど作成した `lambda-api` を設定します。
API の endpoint（resource & method）は、`GET /users` だけになるよう修正します。

```diff:
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 のエントリー用ハンドラを生成します。

```bash:
aglex generate lambda-handler > lambda.js
```

CoffeeScript が好みの場合は、`--coffee` を付加すれば OK です。

```bash:
aglex generate lambda-handler --coffee > src/lambda.coffee
```

### 6. Lambda 登録用 zip ファイルの生成

簡易的に zip コマンドで必要なファイルを纏めて zip 化します。

```bash:
zip -r lambda.zip app.js lambda.js routes views node_modules
```

> 実際にプロジェクトとして継続開発するのであれば、Gulp や Grunt で生成するほうがいいでしょう。
> [AWS Lambda を作成・更新するための gulpfile 雛形](http://qiita.com/u-minor/items/17802910bcf54e10625f) 

### 7. Lambda を登録・更新する

Lambda の登録も更新も同じコマンドで行います。
生成した設定ファイルと zip ファイルを指定してください。

```bash:
aglex --config aglex.yml lambda update --zip lambda.zip
```

このコマンドは Express のコードを修正して Lambda に反映するたびに利用します。

### 8. Lambda function に API Gateway からの呼び出しを許可する

作成した Lambda function に API Gateway からアクセスできるようにする許可を与えます。

> この設定は Lambda 作成時の初回のみで OK です。

```bash:
aglex --config aglex.yml lambda add-permission
```

### 9. API を登録・更新する

設定ファイルの定義に従って、API の登録・更新を行います。

```bash:
aglex --config aglex.yml apigateway update
```

このコマンドは設定ファイルで API の endpoint を追加・修正するたびに利用します。

### 10. API の stage へのデプロイ

test という stage にデプロイするには、以下のようにします。

```bash:
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>` となります。

> マネジメントコンソールの Stages からも確認可能です。
> ![API_Gateway.png](https://qiita-image-store.s3.amazonaws.com/0/15611/3ecf93b3-522a-b8da-7859-b34c5362be41.png "API_Gateway.png")

`https://<REST_API_ID>.execute-api.<REGION>.amazonaws.com/<STAGE_NAME>/users` にアクセスすると、正しく設定できていれば以下の様な応答が得られるはずです。

```json:
{
  "message": "respond with a resource"
}
```

このまま利用してもいいのですが、Custom Domain Names から独自ドメインを設定するといい感じになります。

## gulp/grunt からの利用

ライブラリとしても利用できるようになっていますので、gulp/grunt と連携することもできます。

```javascript:gulpfile.js
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 です。

```javascript:
var app = express();
```

通常は http.createServer() に 引数として渡し、request イベントが発生したタイミングで call されます。

```javascript:
var server = http.createServer(app);
```

Lambda function が実行された際にこの呼び出しを偽装してやれば、あとは Express でうまく処理してくれるはずなので、どうやって偽装するか、に問題が移ります。

Lambda は実行時に event データをオブジェクト形式で受け取りますが、通常の HTTP リクエストとは異なり、URL や Header 等に関する情報は標準では存在しません。
これらの情報を event データに付加するために、API Gateway の Integration Request にある Mapping Template を利用します。

```mappingTemplate.json
{
  "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 に受け渡す部分を担当しています。