AWS IoT Device SDK for Rubyを使ってPub/Sub/Shadow updateしてみた

AWS IoT Device SDK for Rubyを使って、AWS IoTとPublish/Subscribe/Shadow updateしてみたサンプルです。

# 前書き

`aws_iot_device` というGemがあります。AWS IoT Device SDK for 〜系の非公式Ruby版ですが使用例が見当たらなく、下記Qiitaの使ってみた記事もスクリーンショットなど古い箇所が多いので、自分のログを記事として残すことにしました。

## 参考

[AWS IoT Device SDK for Ruby on RubyGems](https://rubygems.org/gems/aws_iot_device)
[AWS IoT Device SDK for Ruby on GitHub](https://github.com/RubyDevInc/aws-iot-device-sdk-ruby)
[AWS IoT Device SDK for Rubyを使ってみた - Qiita](https://qiita.com/Satoshi_Numasawa/items/2b89799a92e5a4edefcd)

## 環境

ホスト : macOS Mojave 10.14.5
ゲスト（実動作環境） : Docker ruby:latest (2.6.3) コンテナ
ruby : 2.6.3p62
gem 'aws_iot_device' : 1.0.0

## やったこと

1. AWS IoTの設定
2. AWS IoT Device SDK for Rubyのインストール
3. AWS IoT Device SDK for Rubyを使ったPub/Subサンプル
4. AWS IoT Device SDK for Rubyを使ったShadows updateサンプル

本来はRaspberry piなどの上で動かすものですが、動作環境に依存しないコードなのでMacOS上のDockerで動作させています。

## 1. AWS IoTの設定（モノの作成と証明書のダウンロード）

AWS IoTの設定（モノの作成と証明書のダウンロード）は他言語のSDKを使う場合と変わりません。
AWSはUIがよく変わるので公式のガイドを参考にするのが良い気がします。（画面キャプチャが英語版だったり項目名が違ったりしますが...。）

> [AWS IoT の使用開始 - AWS IoT](https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/iot-gs.html)

### モノの作成（デバイスの登録）

以下ガイド通りに作成していきます。

> [Registry でデバイスを登録する - AWS IoT](https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/register-device.html)

### 証明書

`証明書が作成されました!` 画面からダウンロードできる証明書・プライベートキーはSDKに必要です。
画面にもあるとおり、ダウンロードし損ねるとプライベートキーは再取得出来ません。

またAWS IoT のルート CAもダウンロードしておきます。
[X.509 証明書と AWS IoT - AWS IoT](https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/managing-device-certs.html#server-authentication)

curlでダウンロードする場合は以下のような感じで。
`curl -O https://www.amazontrust.com/repository/AmazonRootCA1.pem`

### ポリシーの作成とアタッチ

ガイドには `セキュア` と書かれていますが実際の項目名は `安全性` です。
AWS IoTデバイスには `管理 → モノ → 先ほど作ったモノの名前（ガイドだとMyIotThing）` の順でアクセス出来ます。

ガイド通りに作成すると、 `MyIotThing` というモノができ手元に証明書・プライベートキー・ルートCAがあるはずです。

## 2. AWS IoT Device SDK for Rubyのインストール

RubyGemsにあるため `gem install aws_iot_device` でインストール出来ます。
Gemfileで書いたり直接インストールしたりはお好みで。

今回はローカル環境を汚したくないのでDockerを使いました。
dockerコマンドを叩いたディレクトリをバインドし、先ほどダウンロードした証明書等をコンテナに渡しています。
※ `--rm` オプションありで立ち上げているので一度コンテナから抜けるとコンテナが削除されます。

### コンテナの立ち上げとバインドの確認

```bash
$ docker run --rm -it -v "$(pwd):/app" -w /app ruby:latest bash
root@ad0bd381c93c:/app# ls -1
certificate.pem.crt # 証明書
private.pem.key # プライベートキー
AmazonRootCA1.pem # CAルート証明書
```

これより下のコマンドは基本的に起動したDockerコンテナ上で実行しています。

### AWS IoT Device SDK for Rubyのインストール

```bash
$ gem install aws_iot_device
...
$ gem list | grep aws_iot
aws_iot_device (1.0.0)
```

## 3. AWS IoT Device SDK for Rubyを使ったPub/Subサンプル

単なるPub/Subであれば `ruby-mqtt` という別のgemでも可能でした。
次のShadowを使わないのであれば、こちらの方がスリムで良いかもしれません。（使い方は割愛）

### エンドポイント

Pub/Subするためには先の証明書類とエンドポイントが必要です。
`管理 → モノ → 先ほど作ったモノの名前（ガイドだとMyIotThing） → 操作 → HTTPS` で調べておきます。

aws cliが動くならコマンドでも取得出来ます。

```bash
$ aws iot describe-endpoint --endpoint-type iot:Data-ATS
{
    "endpointAddress": "xxx-ats.iot.us-east-2.amazonaws.com"
}
```

~~AWS IoT Device SDK for Rubyは `--endpoint-type iot:Data-ATS` 付きで取得したエンドポイントでしか動きませんでした。~~
~~逆にruby-mqttはオプション無しで取得したエンドポイントでしか動かないようでした。環境依存かもしれませんがご参考に。~~

上記記載は間違いで使うCAルート証明書によってエンドポイントが変わります。
`VeriSign エンドポイント (レガシー)` を使う場合は、 オプションを指定せず `-ats` と付かないエンドポイントを使います。
`Amazon Trust Services エンドポイント (推奨)` を使う場合は、 `--endpoint-type iot:Data-ATS` を付け `-ats` が含まれるエンドポイントを使います。
組み合わせを間違えるとconnectが失敗するので注意してください。

### サンプルコード

AWS IoT Device SDK for Rubyにはサンプルコード（samples配下）が含まれています。
特に単なるPub/SubはReadmeだとさらっと流されているのでサンプルコードを読んだ方が早いかと思います。

### Publish

AWS IoT Device SDK for Rubyからpublishし、別のデバイスでsubscribeするパターンです。
AWS IoT コンソールにはMQTTクライアントというsubscribeのテスト機能があるのでそれで確かめます。

[AWS IoT MQTT クライアントでデバイスの MQTT メッセージを確認する - AWS IoT](https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/view-mqtt-messages.html)

topic名にはガイド通り `my/topic` と入力しておきました。

publishのサンプルコードは以下のような感じです。

```ruby:publish.rb
require 'aws_iot_device'

host = "xxx-ats.iot.us-east-2.amazonaws.com"
port = 8883

ca_file = "AmazonRootCA1.pem"
key_file = "private.pem.key"
cert_file = "certificate.pem.crt"

mqtt_client = AwsIotDevice::MqttShadowClient::MqttManager.new(host: host, port: port, ssl: true)
mqtt_client.config_ssl_context(ca_file, key_file, cert_file)
mqtt_client.connect

mqtt_client.publish("my/topic", '"Hello from AWS IoT SDK for ruby"')
sleep 1

mqtt_client.disconnect

```

上記サンプルコードを実行すると、MQTTクライアントに `"Hello from AWS IoT SDK for ruby"` と表示されるはずです。
ここで送るメッセージはjson形式である必要があります。MQTTクライアントは警告を出しつつ表示してくれます。

```bash
ruby publish.rb
```

<img width="573" alt="publish.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249068/7e72250a-c227-c5c3-658d-ce6e4fbeb2fc.png">

### Subscribe

今後は逆にAWS IoT コンソールのMQTTクライアントからpublishされたメッセージをSDK側で受け取ります。

```ruby:subscribe.rb
require 'aws_iot_device'

host = "xxx-ats.iot.us-east-2.amazonaws.com"
port = 8883

ca_file = "AmazonRootCA1.pem"
key_file = "private.pem.key"
cert_file = "certificate.pem.crt"

mqtt_client = AwsIotDevice::MqttShadowClient::MqttManager.new(host: host, port: port, ssl: true)
mqtt_client.config_ssl_context(ca_file, key_file, cert_file)
mqtt_client.connect

callback = proc do |message|
  puts message.topic, message.payload
end

mqtt_client.subscribe("my/topic", 0, callback)
sleep 10 # subscribe待機時間

mqtt_client.disconnect
```

connectするところまではpublishと同じです。
メッセージを受け取ったときのcallbackを設定し10秒間subscribe待ちします。
ruby-mqttには `client.get` というメッセージを受け取るまで待つ良い感じのメソッドがありますが、AWS IoT Device SDK for Rubyにはないようでした。

※ Python版にもなさそうですが、subscribeの使い方として主処理は無限ループで待ち続けて、メッセージが届いたらcallbackで処理するような感じになると思います。であれば自前でループ処理を書いた方が都合が良いのかもしれません。

上記サンプルコードを実行後、10秒の待ち時間以内にMQTTクライアントからpublishするとコンソールにメッセージが出力されます。
サンプルコードを実行 → 10秒以内にMQTTクライアントからpublish → コンソールにpublishしたtopicとメッセージが表示される。

```bash
$ ruby subscribe.rb
my/topic
{
  "message": "Hello from AWS IoT console"
}
```

<img width="573" alt="subscribe.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249068/0dd78ead-1f86-4a75-541d-60d259811269.png">

## 4. AWS IoT Device SDK for Rubyを使ったShadows updateサンプル

やっとモノ（Shadow）をupdateします。
Shadow操作は色々とできるはずですがupdateだけやってみます。

### Thing名の確認

Shadow updateには先のPub/Subで使ったものに加えThing名が必要です。
`管理 → モノ` に表示されているモノがThing名で、ガイド通りに作成していれば `MyIotThing` になっています。
こちらもaws cliが使えれば `aws iot list-things` でも確認出来ます。

```bash
$ aws iot list-things --profile=private
{
    "things": [
        {
            "thingName": "MyIotThing",
            "thingArn": "arn:aws:iot:xxx",
            "attributes": {},
            "version": 1
        }
    ]
}
```

### Shadow updateのサンプルコード

Shadow操作するときは `AwsIotDevice::MqttShadowClient::ShadowClient` を使います。
MqttManagerと違いShadowClientの使い方はgemのReadmeに詳しく書いてあります。そちらを参照した方が良いでしょう。

最低限のコードであれば `thing` が増えている程度でPub/Subとほぼ同じです。Shadowのmessageを実行時刻でupdateしてみます。

```ruby:shadow_update.rb
require "aws_iot_device"

host = "xxx-ats.iot.us-east-2.amazonaws.com"
port = 8883
thing = "MyIotThing"

ca_file = "AmazonRootCA1.pem"
key_file = "private.pem.key"
cert_file = "certificate.pem.crt"

shadow_client = AwsIotDevice::MqttShadowClient::ShadowClient.new
shadow_client.configure_endpoint(host, port)
shadow_client.configure_credentials(ca_file, key_file, cert_file)
shadow_client.create_shadow_handler_with_name(thing, true)
shadow_client.connect

shadow_client.update_shadow('{"state":{"desired":{"message":"shadow updated at ' + Time.now.to_s + '"}}}')
sleep 2

shadow_client.disconnect
```

```bash
ruby shadow_update.rb
```

<img width="573" alt="shadow_update.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249068/6e5b8c4b-d116-acf1-4695-4bf6be1a5107.png">

Webコンソールでシャドウステータスを見る場合は、 `管理 → モノ → 先ほど作ったモノの名前（ガイドだとMyIotThing） → シャドウ → シャドウステータス` から確認出来ます。
先ほどのサンプルコードを実行すると、シャドウステータスの `message:` が実行時刻で更新されていきます。

今回はサンプルなので最低限のコードですが、ちゃんと使う場合はcallbackを設定したり、update_shadowのパラメータはto_jsonで生成したりしたほうが良いと思います。

## さいごに

無事、Pub/SubとShadow updateの動作が確認出来ました。

最初に使ってみたときは設定ミスでconnectionでエラーになってしまったのですが、なにぶん参考にする記事がなかったので、他言語の記事を見たりgemのソースを読んだりpythonのSDKを読んだり苦労しました。
本記事が参考になれば幸いです。