lambda
node-red
Node-REDDay 13

Node-REDをAWS Lambdaで動かす話

More than 3 years have passed since last update.

Node-REDは、Node.jsの環境上で動くのですが、AWS LambdaはNode.jsのアプリケーションを動作させることができるので、うまくNode−REDのflowを動かせないかなと思って試してみた話です。


課題

AWS LambdaはNode.jsの関数を記述できますが、いろいろ決まり事および環境上の制約があります。


  1. タイムアウト(最大300秒まで設定可能)

  2. HTTPのアクセスはAPI Gateway経由

  3. ローカルファイルシステムの書き込みは不可(/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 という名前で定義ファイルを格納しているものとします。


index.js

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のほうで振り分ければ何とかなりそうなのでまあこれでいいのではないでしょうか。


hub.js

var events = require('events');

module.exports = new events.EventEmitter();


aws-lambda-io.js

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として登録するもの)はこちらです。


index.js

// ...

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というオブジェクトがあった場合、そこに登録されているコールバックを呼び出す処理を行います。


aws-lambda-io.js

// ...

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 ノードを追加設定するのを忘れないようにしましょう。

Node-RED.jpg

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ファイルに記載しておきます。


.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アクションで設定できます。

AWS Lambda-1.jpg

実行結果はExecution resultに出力されます。Log outputにはNode-REDのコンソールメッセージが表示されているのが分かるかと思います。

AWS Lambda-2.jpg


まとめ

LambdaでNode-REDのflowを実行できていることが確認できたので、あとはAPI Gatewayを前面に置くなり、S3の変更イベントにつなげるなり、いろいろ応用は可能ですね。あと、初回起動時に10sec以上かかっていたりしますが、一度起動されてしまえばそれなりの速度でレスポンス帰ってきます。

なお、今度のバージョンからエディタなしRuntimeだけのNode-REDが出るみたいな話がありますね。その際にはもうすこし起動時のスピードが改善されたり制約のある環境でもスマートにデプロイできるようになるといいですね。