Node-REDは、Node.jsの環境上で動くのですが、AWS LambdaはNode.jsのアプリケーションを動作させることができるので、うまくNode−REDのflowを動かせないかなと思って試してみた話です。
課題
AWS LambdaはNode.jsの関数を記述できますが、いろいろ決まり事および環境上の制約があります。
- タイムアウト(最大300秒まで設定可能)
- HTTPのアクセスはAPI Gateway経由
- ローカルファイルシステムの書き込みは不可(
/tmp
ディレクトリをのぞく)
ここで 1. については、それほど問題なさそうです。もしバッチのような処理をやりたいなら別の環境でやるべきかと思います。
また、2. については、HTTP in / WebSocket in ノードなどうまく動かないものも出てくるでしょうが、これらのノードは諦めれば何とかなるでしょう。
しかし 3.については、単にそのままNode-REDを起動すると、ユーザディレクトリのflow.jsonファイルをWrite Modeでオープンしに行く挙動になっているようです。なので起動自体がうまく行きませんでした。
仕方ないので、勝手にstorageモジュールを定義して、flow.jsonの読み込みを自前でやることにしました。下記では.node-red-lambda
というディレクトリにflows.json
, flows_cred.json
という名前で定義ファイルを格納しているものとします。
var RED = require('node-red');
var fs = require('fs');
var http = require('http');
var path = require('path');
var when = require('when');
var userDir = './.node-red-lambda';
function delay(msec) {
return when.promise(function(resolve) {
setTimeout(function() {
resolve();
}, msec);
});
}
// userDirectoryからJSONファイルを読み込む
function loadConfig(fileName) {
return when.promise(function(resolve, reject) {
fs.readFile(path.join(userDir, fileName), 'utf8', function(err, data) {
if (err) { reject(err); }
else { resolve(JSON.parse(data)); }
});
});
}
// RED.init() に渡すユーザ設定。storageModuleを上書きしている(書き込みには対応してない)
var userSettings = {
storageModule: { // dummy storage. only implements the least functions for running
init: function() {},
getFlows: function() {
return loadConfig('flows.json');
},
getCredentials: function() {
return loadConfig('flows_cred.json');
}
}
};
var init = (function() {
var httpServer = http.createServer(); // HTTPサーバはダミー(Listenしない)
RED.init(httpServer, userSettings);
return RED.start().then(function() {
console.log('Node-RED server started.');
return delay(1000);
});
})();
/**
* AWS Lambda に登録する関数
*/
module.exports = function(event, context) {
init.then(function() { // 必ずinit処理が完了後に実行することを保証
// ...
})
}
Lambdaイベント処理用ノードの開発
AWS Lambdaではeventという形で外部から入力を受け取ります。このeventを表すものを新しくinノードとして実装しておきます。
Lambda FunctionからNode-REDへの受け渡しは、どうせ同じメモリ空間のはずなのでEventEmitterでやっちゃいます。スロット分けるなり何なりしてもかまわないですけど、Flowのほうで振り分ければ何とかなりそうなのでまあこれでいいのではないでしょうか。
var events = require('events');
module.exports = new events.EventEmitter();
var hub = require('./hub');
module.exports = function(RED) {
function AWSLambdaRequestNode(config) {
RED.nodes.createNode(this, config);
var node = this;
console.log('## registering listener to emitter ##');
hub.on('fire', function(event, context) {
console.log('# accept lambda event #');
node.send({ lambdaContext: context, payload: event });
});
}
RED.nodes.registerType('aws-lambda-request', AWSLambdaRequestNode);
// ...
};
呼び出す側(AWS Lambda Functionとして登録するもの)はこちらです。
// ...
var lambdaHub = require('node-red-node-aws-lambda-io');
/**
*
*/
module.exports.handler = function(event, context) {
init.then(function() {
console.log('passing event to aws-lambda-io event emitter');
console.log('event =', event);
lambdaHub.emit('fire', event, context);
})
.catch(function(err) {
console.error(err.stack);
context.done(err);
});
};
flowの処理が終わったら、Lambdaを呼び出した元にレスポンスを返したいですね。返さないと延々とLambdaのタイムアウトまで待ってしまいます。なのでレスポンスを返すためのoutノードを同じように作成しましょう。
このノードでは、input
イベントで伝達されたmsgのプロパティにlambdaContext
というオブジェクトがあった場合、そこに登録されているコールバックを呼び出す処理を行います。
// ...
module.exports = function(RED) {
// ...
function AWSLambdaResponseNode() {
this.on('input', function(msg) {
if (!msg.lambdaContext) {
node.error('No lambda request');
return;
}
console.log('# callback to lambda #');
msg.lambdaContext.done(null, msg.payload);
});
}
RED.nodes.registerType('aws-lambda-response', AWSLambdaResponseNode);
};
作成したNode-REDのノードを登録し、Node-REDをローカルで起動します。
Flowの作成
Flowの作成の際には、InjectノードやHTTP in、およびその他の入力の代わりに、AWS Lambda Request
ノードを設定します。Flow処理の最後にAWS Lambda Resopnse
ノードを追加設定するのを忘れないようにしましょう。
Flowを作成したら、Node-REDのユーザディレクトリ(.node-red
)に格納されている flow_<hostname>.json
および flow_<hostname>_cred.json
をコピーし、Lambda配布用のユーザディレクトリである .node-red-lambda
ディレクトリ内に入れておきます。
配布
Node.jsアプリケーションをAWS Lambdaに配布するには node-lambda というCLIツールが良さげです。node_moduleを含むプロジェクト内のファイルを全コピってZIPにまとめて配布までnode-lambda deploy
コマンドで一気にやってくれます。AWSへの接続情報およびLambdaの設定は.env
ファイルに記載しておきます。
AWS_ENVIRONMENT=development
AWS_ACCESS_KEY_ID=<your_aws_key_id>
AWS_SECRET_ACCESS_KEY=<your_aws_secret_access_key>
AWS_ROLE_ARN=arn:aws:iam::xxxxxxxxxx:role/lambda_basic_execution
AWS_REGION=us-east-1
AWS_FUNCTION_NAME=MyNodeRedTest
AWS_HANDLER=index.handler
AWS_MODE=event
AWS_MEMORY_SIZE=128
AWS_TIMEOUT=10
AWS_DESCRIPTION=
AWS_RUNTIME=nodejs
最初のリクエストではNode-REDの初期化も走るので、AWS_TIMEOUT
の値は10秒以上にしておいたほうがいいと思います。
実行
AWS Lambda Console上でテストリクエストを出してみます。eventに渡すデータはConfigure test event
アクションで設定できます。
実行結果はExecution resultに出力されます。Log outputにはNode-REDのコンソールメッセージが表示されているのが分かるかと思います。
まとめ
LambdaでNode-REDのflowを実行できていることが確認できたので、あとはAPI Gatewayを前面に置くなり、S3の変更イベントにつなげるなり、いろいろ応用は可能ですね。あと、初回起動時に10sec以上かかっていたりしますが、一度起動されてしまえばそれなりの速度でレスポンス帰ってきます。
なお、今度のバージョンからエディタなしRuntimeだけのNode-REDが出るみたいな話がありますね。その際にはもうすこし起動時のスピードが改善されたり制約のある環境でもスマートにデプロイできるようになるといいですね。