お題
本稿ではこんなAPIをAPIGatewayとLambdaで作りたいというお題だとします。
- USERリソース
- POST /users
- GET /users/{uid}
- DELETE /users/{uid}
- FRIENDリソース
- POST /users/{uid}/friends
- GET /users/{uid}/friends/{fid}
- DELETE /users/{uid}/friends/{fid}
どの単位でLambdaファンクションを作るか
最初に悩んだポイントです。
- 全6個、それぞれを個別のLambda Functionにする?
- Pros: Microservicesな考えで、お互い独立する
- Cons: 流石に数が多すぎてLambda Functionの管理がつらい
- Cons: 各Lambda Functionで共通のロジックどうすんの?
- 全部を1個のLambda Functionにする?
- Pros: 管理するLambda Functionは1個で済む
- 慣れた普通のアプリケーションのような作り方ができる
- Cons: モノリシック
- 影響範囲(Usersだけ修正したのにFriendsについても心配になる)
- 割当リソース(メモリ量、タイムアウト時間)調整が1箇所に引きずられる
- Pros: 管理するLambda Functionは1個で済む
リソース単位にしてみた
結局、中庸でリソース単位にLambdaファンクションを作るようにし、共通ロジックはCommonなるnpmモジュールにしててみました。(これが正解なのかはわかりません)
- SamplaApp_User
- SampleApp_Friends
という2つのLambdaファンクションと、Commonモジュールで構成してみます。
.
├── common
│ ├── index.js
│ ├── lib
│ └── package.json
├── functions
│ ├── friends
│ │ ├── index.js
│ │ └── package.json
│ └── users
│ ├── index.js
│ └── package.json
└── project.json
※ ここで、しれっとproject.json
などが出てきていますが、このディレクトリ構造はapexの使用を前提としたものです。
ルーティング
さて、リソース単位にLambdaファンクションにするということは、SamplaApp_User
ファンクションは
- POST /users
- GET /users/{uid}
- DELETE /users/{uid}
この3つの挙動をひとつのLambdaファンクションが担うことになります。何らかの条件で処理を分岐せねばならないのでその方法を考えます。
Lambdaファンクションのハンドラのインターフェイスは
exports.handle = function(e, ctx, cb) {
};
こうですが、Lambdaファンクションに渡されるイベント変数e
を
{
operation: "get",
params: {
p1: 2222,
p2: "hogehoge"
}
};
このようにしてe.operation
で処理を分岐することにしてみました。
functions/users/
├── index.js
├── operation
│ ├── create.js
│ ├── destroy.js
│ ├── get.js
│ └── index.js
└── package.json
const co = require("co");
const operation = require("./operation");
exports.handle = function(e, ctx, cb) {
console.log("processing event: %j", e);
co(function*(){
const fn = operation[e.operation];
if(!fn || typeof(fn) !== "function")
throw "unsupported operation";
return yield fn(e.params);
}).then(result => {
cb(null, result);
}).catch(err => {
cb(err);
});
};
module.exports = {
"create": require("./create"),
"get": require("./get"),
"destroy": require("./destroy"),
};
module.exports = function*(params) {
console.log("user creating... %j" ,params);
return {id: 1111, name: "hogehoge"};
};
上記のサンプルコードはco
,ジェネレータファンクション
,yield
というES6の文法を多用しています。読み慣れない方は
http://qiita.com/keitarou/items/79a038a29e1f8e39573b
こちらの記事などを参考にしてください。
共通処理
これで、SamplaApp_Users
,SamplaApp_Friends
それぞれについてはこの型で各operationを実装していけばいいのですが、実装を進めていくと共通処理が出てくることになると思います。
ここではnpmでローカルのディレクトリをnpmモジュールとして取り込む方法を使うことにしました。
npm install <folder>:
Install a package that is sitting in a folder on the filesystem.
.
├── common
│ ├── index.js
│ ├── lib
│ └── package.json
├── functions
│ ├── friends
│ │ ├── index.js
│ │ └── package.json
│ └── users
│ ├── index.js
│ └── package.json
└── project.json
※ ここで、しれっとproject.json
などが出てきていますが、このディレクトリ構造はapexの使用を前提としたものです。
users, friendsそれぞれのpackage.jsonのdependencies
に../../common
を参照するように書きます。
"scripts": {
"preinstall": "npm install ../../common"
},
"dependencies": {
"co": "^4.6.0",
"my-common": "../../common" <---ここ
}
これによりnpm install
で my-common
というモジュールとして取り込まれます。
※preinstallを書いているのは、commonが更新された時にnpm install
してもモジュールが更新されないのでその回避策です。
さらにapex deploy
するときに自動的にパッケージングされるようにproject.json
にhookを書きましょう。
"hooks" :{
"build": "npm install"
}
まとめ
まとまっているのかわかりませんが、
- Lambdaファンクションをある程度の単位で切るための方法
- 各Lambdaファンクションに共通な処理の扱い方
を考えてみました。
Lambdaの考え方からするとe.operation
で処理を分岐するのは邪道なのかもしれません。
また逆に、共通処理があるくらいならひとつのLambdaファンクションにまとめるべきなのかもしれません。
最近のServerless 1.0-betaを見ていると、後者の「全部一個にまとめちまえ」という方向に進んでいるようにも見えます。