はじめに
こちらはSORACOM Advent Calendar 2023 1日目の記事です。
IoTプラットフォームのSORACOMがリリースされた当時からあったSORACOM Beamは、デバイスに認証情報やCPUパワーのいる暗号化処理などをさせることなく、安全に任意のWebサーバーとの連携ができる、便利なサービスです。
その後特定のサービスとの連携をより簡単にできるようにしたSORACOM Funnelや、AWS LambdaのようなFaaSとの連携が簡単にできるSORACOM Funkといったサービスがリリースされ、ある程度決まった構成であればこれらを使うことでより簡単にIoTサービスを構築することができるようになりましたが、様々なサービスに柔軟に対応できるSORACOM Beamの利点は失われていません。
そんな中2022/12/19に「SORACOM Beam で認証情報をもとに Authorization ヘッダーを作成できるようになりました」とのリリースがありました。
従来はSORACOM Beamが提供する署名ヘッダを受け取り側のプログラムにて検証することでデバイス認証をしていましたが、その検証を実装する手間もあり、またすでに認証方法の決まっているサービスとの連携に使用することはできませんでした。
そこがこのリリースによって、Basic認証やBearerトークンによる認証などに対応したサービスと直接連携できるようになったことはもちろん、AWSサービスで利用されているSigV4という署名に対応することにより、デバイスにAWSの認証情報やライブラリを持たせることなく、様々なAWSサービスを直接利用することが可能となりました。
リリースされてから1年経つ機能ではありますが、まだ試せていなかったので、どんな感じで利用できるか試してみたいと思います。
構成
今回作成する構成は以下の図のようになります。
やりたいことはデバイスへのコマンド送信です。毎年遠隔操作の記事を書いてる気がしますが、やりたくなっちゃうので仕方ないですね。
デバイスからクラウドに対するデータ送信は比較的容易ですが、クラウドからデバイスへのコマンド送信はいくつか手段は用意されているもののやや難しいです。これまでもSORACOM Beam経由のMQTT通信、SORACOM Inventoryを用いたLwM2M通信などを用いることができましたが、それぞれMQTTやLwM2Mのライブラリを必要としていました。
サーバーなどに対しては、Amazon SQS(Simple Queue Service)を用いたメッセージの送信がよく用いられており、これは比較的簡単なHTTP通信により実現することができます。また、ロングポーリングを用いることにより、リアルタイム性を保ちつつ、通信頻度をある程度落とすことができます。
今回はこれをマイコンデバイスでできるようにしてみましょう。対象となるデバイスはひとまずWio LTEとしましたが、HTTP通信ができるデバイスであれば何でも実現可能です。
試すサービスをSQSとしたのは、SORACOMのドキュメントにはS3、Location Service、Lambda、Sage Makerへの連携方法は記載されていますが、その他のサービスに連携できるかはっきりしなかったためです。(ので、本記事の方法はSORACOM公式の方法ではありません)
設定
必要な設定は以下の4つです。
- SQSのキューの作成
- キューからメッセージを受信、メッセージの削除をするIAM Policy、IAM Roleの作成
- SORACOMの認証情報の作成
- SORACOM Beamの設定
SQSのキューの作成
メッセージのやり取りをするためのキューを作成します。
SQSのサービスページにアクセスします。
作成後、キューのURLとARNを控えておく(後でコピーして使用します)
SQSの設定は以上です。
IAM Policy、IAM Roleの作成
キューに対して、SORACOM Beamからメッセージの受信と削除が実行できる権限をつけるため、AWS内でサービスの権限を管理するIAM(Identity and Access Management)の設定をします。
IAM のサービスページにアクセス
アクションとして「ReceiveMessage」と「DeleteMessage」を許可
指定方法として「テキスト」を選択し、キューのARNを貼り付けて、「ARNを追加」をクリック
エンティティタイプを「AWSアカウント」、別のAWSアカウントとして、SORACOMのアカウントである「762707677580」を入力、外部IDを要求し推測できない外部IDを設定し、「次へ」をクリック
これで権限に関するAWSの設定は終了です。毎度面倒ですが仕方ありませんね。
SORACOMの認証情報の作成
SORACOMサービスからAWSを利用するための認証情報を登録します。この認証情報により、デバイスに認証情報を持たなくともSORACOM経由でAWSサービスが利用できるようになります。
SORACOMコンソールにログイン
認証情報IDに任意のIDを入力、種別に「AWS IAM ロール認証情報」を指定、ロールARNに作成したロールのARNを入力、外部IDにロール作成時に指定した外部IDを入力し、「登録」をクリック
SORACOMの認証情報の作成はこれで終了です。
SORACOM Beamの設定
最後にSORACOM Beamの設定をします。AWS SigV4の設定ができる登録方法は「HTTPエントリポイント」と「Webサイトエントリポイント」の2種類があり、いずれも利用可能です。
HTTPエントリポイントは、操作一つ一つに宛先やHTTPヘッダを設定する必要がありますが、複数のサービスを登録することができます。例えばSQS、Lambdaを両方利用することができたり、複数のLambdaを使い分けたりすることが可能です。
Webサイトエントリポイントは、宛先サービスが同じであれば設定は一つで良いですが、複数のサービスを登録することはできません。
状況により使い分けると良いでしょう。両方を登録することもできるので、メインに利用するサービスをWebサイトエントリポイントに割り当て、単機能のみ利用する機能をHTTPエントリポイントに登録する、などということもできます。
まずは登録が簡単なWebサイトエントリポイントを登録します。
SIM管理画面にて、対象のSIMにチェックを入れ、操作→所属グループ変更をクリック
新しい所属グループにて新しいグループを適当な名前で作成し、「グループ変更」をクリック
SORACOM Beam設定を開き、設定を追加する開いて、Webサイトエントリポイントを選択
転送先のホスト名に「sqs.ap-northeast-1.amazonaws.com」と入力。このホスト名はAWSのドキュメントの「Amazon Simple Queue Service エンドポイントとクォータ」に記載されています。他のサービスについても同じようなページが用意されているので、他のサービスに繋ぎたい場合は「サービス名 エンドポイント」などで検索すると良いでしょう。ポート番号は未入力で良いです。
ヘッダ操作や事前共有鍵、カスタムヘッダの変更は必要ありません。
AUTHORIZATIONヘッダをONにし、タイプを「AWS Signature V4」に選択、サービスは「sqs」と入力(選択肢にはないので直接入力してください)、リージョンはSQSのキューを作成したリージョンを選択(今回は東京リージョンを表すap-northeast-1)、認証情報IDに先ほど作成した認証情報を選択し、「保存」をクリックします。
気になるのは「サービス」に入れる文字列なのですが、AWSのドキュメントによると、
認証情報 — アクセスキー ID、YYYYMMDD 形式の日付、リージョンコード、サービスコード、およびスラッシュ (/) で区切った aws4_request 終了文字列から成る文字列。リージョンコード、サービスコード、および終了文字列には、小文字を使用する必要があります。
AKIAIOSFODNN7EXAMPLE/YYYYMMDD/region/service/aws4_request
と記載されていて、この「サービスコード」を署名に入れる必要があるようです。このサービスコードはIAMの許可設定などなんとなく色々なところになんとなく出てくるのですが、正式にはどこに記載されているのかは見つけることができませんでした。AWS CLIにて以下のコマンドを実行すると一覧が取得できるので、こちらを参考にすると良いでしょう。
aws service-quotas list-services
ここまでで設定は完了です。デバイスにコードを書き込んで実行してみましょう。
コード
以下のコードをWio LTEに書き込みます。{QUEUE_URL}の部分は作成したキューのURLに置き換えてください。(余裕があればSORACOM Air メタデータサービスから取得するとより良いと思いますが今回はやっていません)
#include <WioLTEforArduino.h>
#include <stdio.h>
#define APN "soracom.io"
#define USERNAME "sora"
#define PASSWORD "sora"
#define BEAM_PORT 18080
#define QUEUE_URL "{QUEUE_URL}"
WioLTE Wio;
void setup() {
delay(200);
SerialUSB.println("");
SerialUSB.println("--- START ---------------------------------------------------");
SerialUSB.println("### I/O Initialize.");
Wio.Init();
SerialUSB.println("### Power supply ON.");
Wio.PowerSupplyLTE(true);
delay(500);
SerialUSB.println("### Turn on or reset.");
if (!Wio.TurnOnOrReset()) {
SerialUSB.println("### ERROR! ###");
return;
}
SerialUSB.println("### Connecting to \""APN"\".");
if (!Wio.Activate(APN, USERNAME, PASSWORD)) {
SerialUSB.println("### ERROR! ###");
return;
}
SerialUSB.println("### Setup completed.");
}
void executeCommand(char *command){
// コマンドに対応する処理を記載
if (strcmp(command, "flash") == 0){
Wio.LedSetRGB(255, 255, 255);
delay(1000);
Wio.LedSetRGB(0, 0, 0);
}
}
int receiveMessage(char *command, char *receiptHandle){
char receiveRequestBody[256];
sprintf(receiveRequestBody, "{\"QueueUrl\":\"%s\",\"MaxNumberOfMessages\":1,\"WaitTimeSeconds\":20}", QUEUE_URL);
char receiveRequestHeader[256];
sprintf(receiveRequestHeader,
"POST / HTTP/1.1\r\n"
"Host: beam.soracom.io:%d\r\n"
"X-Amz-Target: AmazonSQS.ReceiveMessage\r\n"
"Content-Type: application/x-amz-json-1.0\r\n"
"Content-Length: %d\r\n",
BEAM_PORT,
strlen(receiveRequestBody));
char receiveRequest[512];
sprintf(receiveRequest, "%s\r\n%s", receiveRequestHeader, receiveRequestBody);
int receiveSocket;
receiveSocket = Wio.SocketOpen("beam.soracom.io", BEAM_PORT, WIOLTE_TCP);
if (receiveSocket < 0) {
return -1;
}
int receiveSendResult;
receiveSendResult = Wio.SocketSend(receiveSocket, receiveRequest);
if (!receiveSendResult){
Wio.SocketClose(receiveSocket);
return -1;
}
char receiveResponseData[1024];
int receiveResponseLength;
receiveResponseLength = Wio.SocketReceive(receiveSocket, receiveResponseData, sizeof(receiveResponseData), 30000);
Wio.SocketClose(receiveSocket);
char *p1;
char *p2;
p1 = strstr(receiveResponseData, "Body");
if (p1 == NULL){
// Empty
return -1;
}
p1 = p1 + 7;
p2 = strstr(p1, "\"");
*p2 = 0;
strcpy(command, p1);
p1 = p2 + 1;
p1 = strstr(p1, "ReceiptHandle");
if (p1 == NULL){
// Error
return -1;
}
p1 = p1 + 16;
p2 = strstr(p1, "\"");
*p2 = 0;
strcpy(receiptHandle, p1);
return 0;
}
void deleteMessage(char *receiptHandle){
char deleteRequestBody[1024];
sprintf(deleteRequestBody, "{\"QueueUrl\":\"%s\",\"ReceiptHandle\":\"%s\"}", QUEUE_URL, receiptHandle);
char deleteRequestHeader[256];
sprintf(deleteRequestHeader,
"POST / HTTP/1.1\r\n"
"Host: beam.soracom.io:%d\r\n"
"X-Amz-Target: AmazonSQS.DeleteMessage\r\n"
"Content-Type: application/x-amz-json-1.0\r\n"
"Content-Length: %d\r\n",
BEAM_PORT,
strlen(deleteRequestBody));
char deleteRequest[512];
sprintf(deleteRequest, "%s\r\n%s", deleteRequestHeader, deleteRequestBody);
int deleteSocket;
deleteSocket = Wio.SocketOpen("beam.soracom.io", BEAM_PORT, WIOLTE_TCP);
if (deleteSocket < 0) {
return;
}
int deleteSendResult;
deleteSendResult = Wio.SocketSend(deleteSocket, deleteRequest);
if (!deleteSendResult){
Wio.SocketClose(deleteSocket);
return;
}
char deleteResponseData[1024];
int deleteResponseLength;
deleteResponseLength = Wio.SocketReceive(deleteSocket, deleteResponseData, sizeof(deleteResponseData), 5000);
Wio.SocketClose(deleteSocket);
}
void loop() {
char command[256];
char receiptHandle[512];
if (receiveMessage(command, receiptHandle) >= 0){
executeCommand(command);
deleteMessage(receiptHandle);
}
}
簡単にコードの内容を説明すると、SQSのキューからメッセージを取得し、メッセージがあればメッセージ内のコマンドを実行して削除、メッセージがなければ再度取得する、を繰り返すプログラムです。
Wio.HttpPostではなくWio.SocketSendなどを使っているのは、このプログラムではPOSTメソッドでリクエストした結果を取得する必要がありますが、HttpPostでは結果が取得できないためです。結果が取得できるHttpのライブラリなどがある環境であればそれを使った方が良いでしょう。
肝心のSQSへのアクセスですが、どのようなアクセスをすれば良いかはそれぞれReceiveMessageのドキュメントとDeleteMessageのドキュメントを読むと良いでしょう。他のサービスに対するリクエストもドキュメントを見ると記載されています。特にサンプルリクエストを読めば大体わかります。
AWSのAPIで特徴的なのは、通常のWeb APIではメソッドとパスでルーティングするAPIを決めるところ、AWSのAPI(少なくともSQS)ではX-Amz-Targetヘッダを用いてルーティングしているところです。このX-Amz-TargetとContent-Type: application/x-amz-json-1.0は入れておかないとルーティングされないので、ちゃんとヘッダーに入れておきましょう。一番難しいAuthorizationはSORACOM Beamが付与してくれます。
メッセージの取得はWaitTimeSeconds(ロングポーリングの時間)を20秒(最大値)、MaxNumberOfMessages(一度の取得するメッセージ数)を1つとしています。メッセージを1つにしているのは、複数になると確保するメモリ量が多くなり、また複数のコマンドを扱うのがやや面倒であるためです。
executeCommand関数にて、取得したコマンドに応じた処理を実行する形としています。上のコードではflashというコマンドが届くとLEDが光るようにしています。ここはブザーを鳴らすなりセンサーの値を取るなり必要に応じて記載すれば良いでしょう。
ひとまずこれで動作するようになったはずです。
動作確認
補足
HTTPエントリポイントを利用する場合は、上記のコードのBEAM_PORTを8888にして、SORACOM BeamのHTTPエントリポイントを設定すれば良いです。
#define BEAM_PORT 8888
設定項目はWebサイトエントリポイントとほぼ同一で、パスの設定をエントリポイント、転送先ともに「/」とすれば良いです。
今回は二種類のWeb APIを使っているので、よくあるAPIではAPIごとにパスが違うため、APIごとにエントリポイントを作成する必要があるのですが、AWSのWeb APIではパスは同じでヘッダーでAPIをルーティングしているため、一つのHTTPエントリポイントで対応できるのですね。
なお、複数のAWSサービスを使いたい場合は、例えばSQSはエントリポイントのパスを/sqsとし、Lambdaはエントリポイントのパスを/lambdaとして作成し、それぞれリクエスト時にパスを呼び分けることで対応可能です。
おわりに
SORACOM BeamのAWS SigV4対応で、ローパワーなマイコンでもAWSのサービスが直接利用できるようになりました。
今回はSQSを使いましたが、DynamoDBやTimestreamに直接データを保存したり、SNSにメッセージを送信してアラートを送信したり、各種AIサービスにデータを投げて処理させたり、デバイスだけでは難しいことでも、クラウドと連携させて色々できそうですね。
明日は@kizawa2020さんです。よろしくお願いします!