つい先日serverlessに入門したばかりですが、
色々実験した結果APIから召喚されたLambdaが新たなLambdaを動的に作り出すという黒魔術を習得したのでご紹介します。
ややこしいので、以下APIから召喚されるLambdaファンクションを親ラムダ、親ラムダに作り出されるLambdaファンクションを子ラムダと表記します。
実装方針
動的にコードをつくるといってもjsのコードをゴリゴリ作り出すのは辛いので、方針としては
- S3にひな型となるjsのコードを用意(index.jsとする)
- ひな型の動的に変化しうる箇所は外部ファイルから読み込むようにする(parameters.jsonから読み込む)
- paramaters.jsonの実体は親ラムダ内でAPIのリクエストボディから作成
- index.jsとparametes.jsonをまとめてzip圧縮
- 4で作成したzipをソースに子ラムダ作成
なんだかいけそうな気がしますね。
環境構築
Serverlessのインストール
基本的には拙稿を参考にしていただければと
serverless.ymlの編集
ただし、今回はLambdaで新しくファンクションを作るのでlambda:CreateFunctionの権限を付与しておく必要があります。
あと盲点になりやすいと思うのですが、iam:PassRoleの権限も必要です。
(新しく作った子ラムダにIAMロールを付与することが不可避のため。私はこれでしばらくはまりました。)
ファンクション名はシンプルにlambdaとしときましょう。
service: serverless-test
provider:
name: aws
runtime: nodejs4.3
stage: dev
region: us-east-1
iamRoleStatements:
- Effect: "Allow"
Action:
- "s3:getObject"
- "s3:putObject"
Resource: "arn:aws:s3:::my-bucket-name/*"
- Effect: "Allow"
Action:
- "lambda:CreateFunction"
Resource: "*"
- Effect: "Allow"
Action:
- "iam:PassRole"
Resource: "arn:aws:iam::my-account-id:role/serverless-created-function"
functions:
lambda:
description: "create lambda function"
handler: handler.lambda
memorySize: 256
timeout: 60
events:
- http:
path: lambda
method: post
子ラムダにつけるIAMロールはあらかじめ作成しておきましょう。
ここではserverless-created-functionという名前で作成してます。
実装
子ラムダ用のひな型
実験用なのでシンプルに、parameters.jsonのoutputプロパティがあったらログに出すだけです。
'use strict';
const params = require('./parameters.json');
exports.handler = (event, context, callback) => {
if(!params.output){
callback("Parameter Empty!!");
}else{
console.log(params.output);
callback(null, "function succeed");
}
};
parameters.jsonは親ラムダが生み出してくれるので、ここで作る必要はありません。
親ラムダのコード
さて親ラムダの方ですがここで実装方針を思い返しましょう。
察しの良い方ならコールバック地獄の呼び声が聞こえてきたかと思います。
実験とはいえコールバックが二重三重になるのはさすがに辛いので、
ここはcoさんの力を借ります。
そしてzipの生成にはnode-zipを使います。
最初は
let zipContent = zip.generate({base64: false,compression:'DEFLATE'});
lambda.createFunction({
Code: {ZipFile: zipContent},
FunctionName: 'function_' + moment().format('YYYYMMDDHHmmssSSS'),
Handler: 'index.handler',
Role: IAM_ROLE,
Runtime: 'nodejs4.3',
MemorySize: 256,
Timeout: 60
}, (lambdaErr, lambdaData) => {
if(lambdaErr !== null){
reject(lambdaErr);
}else{
resolve(lambdaData);
}
});
みたいにやってみたのですが「お前の上げたファイル解凍できへんわ」と怒られたので、やむなく一度zipファイルを保存することにしました。
上記のコードだとzipContentがバイナリになってないのが理由っぽいのですが、ソース内でバイナリに変換してやる方法がわからなかったので...
この記事によるとLambda内でも/tmp以下は一時作業領域として使えるようです。
※ただし、同じ環境でほかに誰が使ってるかわからないのでファイル名に注意し、後片付けを忘れずやっときましょう。
完成した呪文がこちら
'use strict';
const AWS = require('aws-sdk');
const moment = require('moment');
const IAM_ROLE = "arn:aws:iam::my-account-id:role/serverless-created-function"; //子ラムダに付与するIAMロール
const nodeZip = require('node-zip');
const fs = require('fs');
const co = require('co');
// Your first function handler
module.exports = (event, context, callback) => {
const BUCKET = "my-bucket-name";
const zip = new nodeZip();
co(function *() {
try{
let objectData = yield s3GetObject({Bucket:BUCKET, Key: 'serverless-test/templates/index.js'});
zip.file('index.js', objectData.toString());
zip.file('parameters.json', JSON.stringify(event.body)); //body以下をjsonに吐き出し
let zipContent = zip.generate({base64: false,compression:'DEFLATE'});
let zipFile = '/tmp/' + moment().format('YYYYMMDDHHmmssSSS') + '.zip'; //ファイル名が被らないように 真面目にやるならuuidとか使うべき
fs.writeFileSync(zipFile, zipContent, 'binary'); //zipファイル作成
let result = yield lambdaCreateFunction(fs.readFileSync(zipFile));
fs.unlinkSync(zipFile); //zipファイル削除
return result;
}catch(e){
console.log('failed:' + e);
}
}).then((data) => {
callback(null, data);
}).catch((error) => {
callback(error);
});
};
function s3GetObject(params){
const s3 = new AWS.S3();
return new Promise((resolve, reject) => {
s3.getObject(params,(err, data) => {
if(err){
reject(err);
}else{
resolve(data.Body);
}
});
});
}
function lambdaCreateFunction(zipFileContent){
const lambda = new AWS.Lambda({
region: 'us-east-1', //今回私は北ヴァージニアリージョンを使ってます
apiVersion: '2015-03-31'
});
return new Promise((resolve, reject) => {
lambda.createFunction({
Code: {ZipFile: zipFileContent},
FunctionName: 'function_' + moment().format('YYYYMMDDHHmmssSSS'),
Handler: 'index.handler',
Role: IAM_ROLE,
Runtime: 'nodejs4.3',
MemorySize: 256,
Timeout: 60
}, (lambdaErr, lambdaData) => {
if(lambdaErr !== null){
reject(lambdaErr);
}else{
resolve(lambdaData);
}
});
});
}
非同期処理がこんなにスッキリ書けるなんて、coさんは偉大です。
あとはhandler.jsにmodule.exports.lambda = require('./functions/create_function');
と追記すれば黒魔術の完成
実行
では、黒魔術を実行してみましょう。
まずテスト用にevent.jsonを編集
{
"body":{
"output": "ラムダがラムダを生み出す黒魔術"
}
}
デプロイ&実行
serverless deploy
serverless invoke --function lambda --path ./event.json
こうすることでevent.jsonの中身が親ラムダの引数であるeventにそのまま放り込まれます。
APIからリクエストした場合は、リクエストのパラメーターの中身がeventに入ります。
そして...
生まれた...!新たなるLambdaが...!
生まれたての子ラムダにはトリガーが設定されていないのでコンソールからテスト実行します。
わかりにくいかもしれませんが、event.jsonで設定した文字列がコンソールから出力されてます。
今回は単なる実験って感じでしたが、もう少し頑張れば動的にAPIを生成なんてこともできそうで夢が広がりんぐですね。