VoiceFlow で RaspberryPi on Node-RED を操作

Amazon Ehco から、自宅に設置した「インターネットに公開していない RaspberryPi」上の Node-RED を動かすことができる。
「Alexa、kkmarl1200 のおうち IOT 開いて」と話しかけて 自宅 IoT したりできる。
Node-RED と VoiceFlow という強力なローコード開発ツールを利用し、
極力ガチ開発せず、スピーディにローコストで環境を作成することを目指す。

# はじめに

* もともと [node-red-contrib-amazon-echo](https://flows.nodered.org/node/node-red-contrib-amazon-echo) を利用して、自宅に設置した Amazon Echo と  
  RaspberryPi 上の Node-RED を連携させておうち IoT していた
* node-red-contrib-amazon-echo は素晴らしいプロダクトだが、  
  「Alexa, turn on/off {device}」「Alexa, switch on/off {device}」「Alexa, set {device} to 50%」  
  のように定型的なコマンドを実行するものなので、コマンドを覚えていなければならないことと、  
  IoT デバイスを操作するワードが不自然になるのを解決したかった
* Alexa Skills Kitを使ってスキルを開発するのが王道だが、VoiceFlowを利用することで  
  ガチ開発をせずとも自由かつ自然なワードで Alexaスキルを作成できる
* VoiceFlow は SaaS であるため、自宅 RaspberryPi 上の Node-RED と連携するにはひと手間必要


# 全体像

![voiceflow-nodered.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/603012/a6ed662f-039f-d5a3-6f2a-776ec72376b9.png)

* 自宅
  * Amazon Echo（スクリーン付きの Echo Show 5 であれば \9,980。セール時などを狙って安く購入する）
  * RaspberryPi on Node-RED（Raspberry Pi 4 Model B/4GB であれば \6,000 程度。Node-RED は無償提供されている）
* AWS
  * Amazon API Gateway（$5/月程度）
  * AWS Lambda（100万リクエスト/月、400,000 GB/秒の実行時間が無料利用枠。実質無料と考えられる）
  * AWS Systems Manager（無料）
* VoiceFlow（5000発話/月まで無料）

## 処理フロー
* Echo に話しかけると Alexa Skill 発動
* Alexa Skill は VoiceFlow により実装されており、VoiceFlow が API Gateway の API をキック
* API Gateway が Lambda をキック
* Lambda が Systems Manager の RunCommand をキック
* RaspberryPi にインストールされた SSM Agent が自身の Node-RED API エンドポイントをキック  
  RaspberryPi に接続された赤外線モジュールやカメラモジュールを Node-RED から制御する

構築の手順は、動作確認しながら進められるよう、上記の処理フローとは逆順から説明していく。

# 前提条件

* AWSアカウント取得済み
* Amazon 開発者アカウント取得済み
* Amazon Echo 購入済みで開発者アカウントと連携済み
* VoiceFlowアカウント取得済み
* RaspberryPiに Node-RED がセットアップ済み
* (オプション) [Serverless Framework](https://serverless.com/) がセットアップ済み


# 1. RaspberryPi 上の Node-RED で API を作成

とりあえずテスト的に、アクセスされると「ローカルの Node-REDですよ」と返すだけの APIを作成。
![nodered1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/603012/8b9ca0d6-26ff-531a-a925-e75695622b33.png)

```JSON:flow.json
[{"id":"e1358b3.2e03478","type":"tab","label":"localtest","disabled":false,"info":""},{"id":"1a0a4d0d.d93b23","type":"http in","z":"e1358b3.2e03478","name":"","url":"/localtest","method":"post","upload":false,"swaggerDoc":"","x":100,"y":60,"wires":[["7af0c9e5.b982b8"]]},{"id":"7af0c9e5.b982b8","type":"function","z":"e1358b3.2e03478","name":"","func":"msg.payload = \"ローカルの Node-REDですよ\";\n\nreturn msg;","outputs":1,"noerr":0,"x":270,"y":60,"wires":[["435fd448.0761bc","e44d785c.9ea538"]]},{"id":"435fd448.0761bc","type":"http response","z":"e1358b3.2e03478","name":"","statusCode":"","headers":{},"x":410,"y":60,"wires":[]},{"id":"e44d785c.9ea538","type":"debug","z":"e1358b3.2e03478","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":430,"y":120,"wires":[]}]
```

RaspberryPi にて下記コマンドを実行して「ローカルの Node-REDですよ」と返ってくれば OK。

```bash:curl_command
curl -s -X POST http://127.0.0.1:1880/localtest
```

# 2. RaspberryPi を Systems Manager から操作できるように設定する

RaspberryPi を [AWS Systems Manager のマネージドインスタンス](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/managed_instances.html) として登録する。
EC2インスタンスではないオンプレミスインスタンスであっても Systems Manager の管理対象とすることが可能で、
管理対象とすることで [RunCommand](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/execute-remote-commands.html) という機能により対象でアドホックにコマンドを実行することができる。
RaspberryPi に [SSM Agentをインストール](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/sysman-install-managed-linux.html) すれば、RaspberryPi をインターネットに公開していなくても
Systems Manager からアドホックにコマンドを実行することが可能になるため（RaspberryPi がインターネットにアクセスできる必要はある）、
項番 1 の curl コマンドを RunCommand で実行することで Systems Managerから RaspberryPi 上の Node-RED の API をキックできる。
Systems Manager の開始方法は下記公式手順を参照。

* [高速セットアップ](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/systems-manager-quick-setup.html)
  * Systems Managerの初期構築を自動でやってくれる
* [ハイブリッド環境で AWS Systems Manager を設定する](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/systems-manager-managedinstances.html)
  * マネージドインスタンスのアクティベーション作成
  * RaspberryPi に SSM Agent インストール


Systems Manager に RaspberryPi がマネージドインスタンスとしてぶら下がることを確認する。
（下記のスクリーンショットではマネージドインスタンスが「高度なインスタンス」となっているが、
RunCommand の実行には高度なインスタンス化は必須ではない）
![ssm1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/603012/9c186313-b095-efff-c94f-50b8ed5223ed.png)
RunCommand を実行して「ローカルの Node-REDですよ」が出力されることを確認する。
![ssm2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/603012/6dc16d48-bbd7-5658-aba5-295e98e9df89.png)
Node-RED のデバッグウィンドウにもログが表示されることを確認する。
![nodered2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/603012/bf6cb1ec-1e12-ade7-b07d-c78609806ecd.png)

# 3. API Gateway / Lambda から RunCommand 実行

項番 2 の RunCommand を API Gateway - Lambda から実行できるようにする。
AWS マネジメントコンソールから API Gateway、Lambda を手動で作成してもよいが、
今回は Serverless Framework で手早く作成する。
（[Quick Start](https://github.com/serverless/serverless) で簡単に始められる）
下記コマンドを手元の開発マシンなどから実行。

```bash:serverless_command
serverless create --template aws-nodejs --path localtest
cd localtest
npm install aws-sdk
serverless plugin install --name serverless-layers
```

■ serverless.yml

* 下記の例では serverless-layers プラグインを使っているため、S3バケットを事前に作成の上指定する
* API Gateway には API Key を知っているものだけアクセス可能とする
  （適宜 Cognito などを利用してより拡張性の高い認証・認可を行うのが望ましい）

```yaml:serverles.yml
service: localtest
provider:
  name: aws
  runtime: nodejs12.x
  region: ap-northeast-1
  apiKeys:
    - localtestKey
  usagePlan:
    quota:
      limit: 5000
      offset: 0
      period: DAY
    throttle:
      burstLimit: 200
      rateLimit: 100
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "ssm:*"
      Resource: "*"
functions:
  localtest:
    handler: handler.localtest
    events:
      - http:
          private: true
          path: /
          method: post
          integration: lambda
plugins:
  - serverless-layers
custom:
  serverless-layers:
    layersDeploymentBucket: バケット名

```

■ handler.js

* targetInstanceId には RaspberryPi のマネージドインスタンス ID を指定

```JavaScript
'use strict';

module.exports.invokeNodered = async event => {
  const AWS = require('aws-sdk');
  const targetInstanceId = ['RaspberryPi のマネージドインスタンス ID'];
  const runCommand = `/sbin/runuser -l ssm-user -c 'curl -s -X POST http://127.0.0.1:1880/localtest'`;

  const ssm = new AWS.SSM();
  const ssmSendCommandParams = {
    DocumentName: 'AWS-RunShellScript',
    InstanceIds: targetInstanceId,
    Parameters: {
        'commands': [
            runCommand,
        ],
    },
  };

  await ssm.sendCommand(ssmSendCommandParams).promise().catch(e => console.error(e));
  return { message: 'Go Serverless v1.0! Your function executed successfully!' };
};

```

下記コマンドを実行してまずはローカルで確認する。
Node-RED のデバッグウィンドウにもログが表示されることを確認する。

```bash:serverless_command
serverless invoke local -f localtest -l
```

問題なければ AWSにデプロイ。
デプロイ後にも確認し、Node-RED のデバッグウィンドウにもログが表示されることを確認する。

```bash:serverless_command
serverless deploy -v
// デプロイのログがズラズラと表示される
// api keys と endpoints がログとして表示されるので控えておく

serverless invoke -f localtest -l
```

デプロイの結果、「https://***.execute-api.ap-northeast-1.amazonaws.com/dev」という API エンドポイントが作成されるので、
API キーをヘッダに設定の上リクエストを発行し、Node-RED のデバッグウィンドウにもログが表示されることを確認する。
例えば手元の Windows PC からリクエストを行うには下記コマンドレットを実行する。

```PowerShell:Invoke-RestMethod_command
$headers = @{ 'x-api-key' = 'api keys の値' }
Invoke-RestMethod -Uri "endpoints の値" -Method POST -Headers $headers
```

![nodered3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/603012/c9216619-b0c9-d996-22ae-4cb0f7a78079.png)


# 4. VoiceFlow から API Gateway のエンドポイントをキック

[Voiceflowハンズオン Vol.1 : 5. スキルのアップロードとテスト](https://vf-handson-01.netlify.com/#4) を参考に、 VoiceFlow と Amazon 開発者アカウントの紐付けを行う。
Projectを新規作成し、下記の通りフローを作成する。

* フロー作成
  * Design
    * Project Name : Node-RED TEST
    * ① Speak「Node Red のテストを実行します」
    * ② Integrations  
        Request URL : endpoints の値  
        Headers Header Assignments : x-api-key / api keys の値  
    * ③ Speak「テスト実行しました」
    * ④ Speak「テスト失敗しました」
    ![voiceflow1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/603012/f5665d61-0d7b-e5eb-87e1-b1f5f4b36e18.png)
  * Build - Skill Invocation - Invocations 「ノードレッド テスト」
    ![voiceflow4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/603012/5b6f85d1-64ef-9127-f6fb-c27e7eb35e66.png)

Prototypeからテストして、VoiceFlow が問題なく完了し、Node-RED のデバッグウィンドウにもログが表示されることを確認する。
Upload to Alexa して [Amazon Alexa ログイン](https://alexa.amazon.co.jp/spa/index.html) よりログインを行い、
[スキル] - [有効なスキル] - [開発スキル] に「Node-RED TEST」が表示されることを確認する。
![alexa.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/603012/facd6c18-6060-417c-fb7e-ac4b9fc860bd.png)
Amazon Echo に「Alexa、ノードレッドテスト」と呼びかけると Node-RED のデバッグウィンドウにもログが表示されることを確認する。


# 5. 拡張

* 項番 1 では localtest というログ表示するだけのエンドポイントしかないが、赤外線スイッチやカメラを操作するノード等を
  エンドポイント化すれば、おうち IoT に転用できる。
* 項番 2 では RaspberryPi をマネージドインスタンスとして登録しているが、自宅内の Windows 10 PC や
  EC2 のインスタンスを管理対象とすることもできる。  
  また Node-RED の API をキックするために RunCommand を利用しているが、システムリブート、OS内に設置されているスクリプトの実行なども
  実行可能であるため様々な作業の機械化・自動化を行うことができる。
* 項番 3 では Lambda から Systems Manageer を操作しているが、その他の AWS マネージドサービスを操作することも可能。
  例えば Amazon Connect を利用して電話を呼び出したり、Amazon SES を利用してメール配信を行ったりすることも可能。
* 項番 3 では Lambda Function の中で Node-RED の localtest API エンドポイントをベタ書きにしてしまっているが、
  VoiceFlow からのリクエストにパラメータを埋め込み、Lambda Function 内で受け取ったパラメータによって
  処理を分岐した上で Node-RED へのリクエストを組み立てるなどすればより汎用性が高まる。


# 参考文献

* [Voiceﬂow & VUI -虎の巻-](https://techbookfest.org/product/5687859226869760) Voice UI Experts 著
* [Alexa-localが突然使えなくなった！【対処法】](https://qiita.com/naka-kazz/items/9dce91beaf46a91b4c86) @naka-kazz
* [初めてのサーバーレスアプリケーション開発 ～Serverless Framework を使ってAWSリソースをデプロイする～](https://dev.classmethod.jp/cloud/aws/serverless-first-serverlessframework/) @Developers.IO
* [AWS Lambda functionと EC2 Run Commandで、かんたんSlash Commandデプロイ](https://qiita.com/terukizm/items/6d5d3ad4bf7e82c193a3) @terukizm