Node-REDをAWS Lambdaで動かす話

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` という名前で定義ファイルを格納しているものとします。

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

```js:hub.js
var events = require('events');
module.exports = new events.EventEmitter();
```

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

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

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

<img width="1425" alt="Node-RED.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/6495/8ef4cd49-b4d6-0feb-a1fc-eec27125b3d2.jpeg">

Flowを作成したら、Node-REDのユーザディレクトリ(`.node-red`)に格納されている `flow_<hostname>.json` および `flow_<hostname>_cred.json` をコピーし、Lambda配布用のユーザディレクトリである `.node-red-lambda` ディレクトリ内に入れておきます。

## 配布

Node.jsアプリケーションをAWS Lambdaに配布するには [node-lambda](https://www.npmjs.com/package/node-lambda) というCLIツールが良さげです。node_moduleを含むプロジェクト内のファイルを全コピってZIPにまとめて配布まで`node-lambda deploy` コマンドで一気にやってくれます。AWSへの接続情報およびLambdaの設定は`.env`ファイルに記載しておきます。

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

<img width="1568" alt="AWS Lambda-1.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/6495/fc8d9528-b09d-5dc4-c3b5-f5512cd5285c.jpeg">

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

<img width="1568" alt="AWS Lambda-2.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/6495/15ff9aad-435a-36a6-e149-23f3527a792d.jpeg">


## まとめ

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

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