AWS IoT Core に Mosquitto でメッセージを Pub/Sub する構成を AWS CDK で実装してみた
こんにちは、製造ビジネステクノロジー部の若槻です。
AWS IoT Core でアップデートがあった際に、簡単に動作確認ができる環境が欲しい場合があります。
直近だと下記のように MQTT 接続を簡単に削除できる API が追加されましたが、このような新機能を試すために、AWS IoT Core の Pub/Sub(Publish 及び Subscribe)構成を手軽に構築できる方法があると便利ですね。
そこで今回は、AWS IoT Core に Mosquitto クライアントで MQTT メッセージを Pub/Sub する構成を AWS CDK を使用して実装する方法を確認してみました。
AWS リソースの実装
まずは次の AWS リソースを作成および設定していきます。AWS CDK を使用して〜、と前述しましたが、一部のリソースは AWS CLI を使用して手動で作成します。
- AWS CLI
- 秘密鍵および証明書の作成
- AWS CDK
- IoT Thing の作成
- IoT ポリシーの作成
- 証明書へのポリシーのアタッチ
- 証明書への Thing のアタッチ
上記はそれぞれサブスクライブ用とパブリッシュ用の両方に対して行います。
AWS CLI でのリソース作成
クライアントが使用する秘密鍵および証明書を AWS CLI で作成します。CLI を使う理由としては、これらリソースの作成を CDK で行おうとすると処理が煩雑となってしまうためです。
# サブスクライブ用の秘密鍵および証明書の作成
aws iot create-keys-and-certificate \
--set-as-active \
--public-key-outfile subscriber_public_key.pem \
--private-key-outfile subscriber_private_key.pem \
--certificate-pem-outfile subscriber_cert.pem
# パブリッシュ用の秘密鍵および証明書の作成
aws iot create-keys-and-certificate \
--set-as-active \
--public-key-outfile publisher_public_key.pem \
--private-key-outfile publisher_private_key.pem \
--certificate-pem-outfile publisher_cert.pem
- ローカルに保存された秘密鍵
private_key.pem
とデバイス証明書cert.pem
は、デバイス(mosquitto クライアント)側でトピックへの接続時に使用します。 - コマンド実行出力
certificateDescription.certificateArn
の証明書 ID(cert/
以降の値)は後続の AWS CDK のコードで使用します。
AWS CDK でのリソース作成
続いて、AWS CDK を使用して IoT Thing やポリシーを作成します。以下のコードは、サブスクライブ用とパブリッシュ用の両方の証明書に対して、IoT Thing とポリシーを作成し、それらをアタッチするというものです。AWS IoT のリソースは CDK の aws-cdk-lib/aws-iot モジュールを使用して作成できますが、現在提供されているのは L1 コンストラクトのみです。
import * as cdk from "aws-cdk-lib";
import * as iot from "aws-cdk-lib/aws-iot";
import { Construct } from "constructs";
export class MainStack extends cdk.Stack {
constructor(scope: Construct, id: string) {
super(scope, id);
/**
* サブスクライブ用証明書 ID
*/
const subscriberCertificateId = "<サブスクライブ用証明書の ID>";
/**
* パブリッシュ用証明書 ID
*/
const publisherCertificateId = "<パブリッシュ用証明書の ID>";
/**
* 証明書 ARN の作成
*/
const subscriberCertificateArn = cdk.Arn.format(
{
service: "iot",
resource: "cert",
resourceName: subscriberCertificateId,
},
this
);
const publisherCertificateArn = cdk.Arn.format(
{
service: "iot",
resource: "cert",
resourceName: publisherCertificateId,
},
this
);
/**
* IoT Thing(サブスクライブ用デバイス)の作成
*/
const subscriberThing = new iot.CfnThing(this, "SubscriberThing", {
thingName: "subscriber-device-001",
});
/**
* IoT Thing(パブリッシュ用デバイス)の作成
*/
const publisherThing = new iot.CfnThing(this, "PublisherThing", {
thingName: "publisher-device-001",
});
/**
* サブスクライブ用ポリシーの作成
*/
const subscriberPolicy = new iot.CfnPolicy(this, "SubscriberPolicy", {
policyName: "SubscriberOnlyPolicy",
policyDocument: {
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Action: ["iot:Connect"],
Resource: cdk.Arn.format(
{
service: "iot",
resource: "client",
resourceName: "${iot:Connection.Thing.ThingName}", // クライアント ID として Thing 名を使用する前提でポリシー変数を使用
},
this
),
Condition: {
Bool: {
"iot:Connection.Thing.IsAttached": ["true"], // 証明書がアタッチされている Thing のみ接続を許可
},
},
},
{
Effect: "Allow",
Action: ["iot:Subscribe"],
Resource: [
cdk.Arn.format(
{
service: "iot",
resource: "topicfilter",
resourceName: "hoge", // hoge トピックへのサブスクライブを許可
},
this
),
],
},
{
Effect: "Allow",
Action: ["iot:Receive"],
Resource: [
cdk.Arn.format(
{
service: "iot",
resource: "topic",
resourceName: "hoge", // hoge トピックからのメッセージ受信を許可
},
this
),
],
},
],
},
});
/**
* パブリッシュ用ポリシーの作成
*/
const publisherPolicy = new iot.CfnPolicy(this, "PublisherPolicy", {
policyName: "PublisherOnlyPolicy",
policyDocument: {
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Action: ["iot:Connect"],
Resource: cdk.Arn.format(
{
service: "iot",
resource: "client",
resourceName: "${iot:Connection.Thing.ThingName}", // クライアント ID として Thing 名を使用する前提でポリシー変数を使用
},
this
),
Condition: {
Bool: {
"iot:Connection.Thing.IsAttached": ["true"], // 証明書がアタッチされている Thing のみ接続を許可
},
},
},
{
Effect: "Allow",
Action: ["iot:Publish"],
Resource: [
cdk.Arn.format(
{
service: "iot",
resource: "topic",
resourceName: "hoge", // hoge トピックへのパブリッシュを許可
},
this
),
],
},
],
},
});
/**
* サブスクライブ用証明書へのポリシーのアタッチ
*/
const subscriberPolicyAttachment = new iot.CfnPolicyPrincipalAttachment(
this,
"SubscriberPolicyAttachment",
{
policyName: subscriberPolicy.policyName!,
principal: subscriberCertificateArn,
}
);
subscriberPolicyAttachment.addDependency(subscriberPolicy);
/**
* パブリッシュ用証明書へのポリシーのアタッチ
*/
const publisherPolicyAttachment = new iot.CfnPolicyPrincipalAttachment(
this,
"PublisherPolicyAttachment",
{
policyName: publisherPolicy.policyName!,
principal: publisherCertificateArn,
}
);
publisherPolicyAttachment.addDependency(publisherPolicy);
/**
* サブスクライブ用証明書への Thing のアタッチ
*/
const subscriberThingAttachment = new iot.CfnThingPrincipalAttachment(
this,
"SubscriberThingAttachment",
{
thingName: subscriberThing.thingName!,
principal: subscriberCertificateArn,
}
);
subscriberThingAttachment.addDependency(subscriberThing);
/**
* パブリッシュ用証明書への Thing のアタッチ
*/
const publisherThingAttachment = new iot.CfnThingPrincipalAttachment(
this,
"PublisherThingAttachment",
{
thingName: publisherThing.thingName!,
principal: publisherCertificateArn,
}
);
publisherThingAttachment.addDependency(publisherThing);
}
}
サブスクライブとパブリッシュで必要な権限は異なります。それぞれ以下のようになります。
- サブスクライブ
- iot:Connect
- iot:Receive
- iot:Subscribe
- パブリッシュ
- iot:Connect
- iot:Publish
ポリシーでの action/resources の記述形式は下記ドキュメントが参考になります。
また iot:Connect
action で ${iot:Connection.Thing.ThingName}
ポリシー変数を使用して、AWS IoT Core レジストリにモノとして登録されている Thing 名と同じ ID のクライアントからの接続のみを許可しています。
クライアント ID と Thing 名の一致は、管理効率化の観点で1つの証明書を複数の Thing で共有する場合のような非排他的な関連付けを行う場合に推奨されています。
ルート CA 証明書のダウンロード
Amazon Trust Services エンドポイントから、サーバー認証用のルート CA 証明書をダウンロードします。エンドポイントは、下記の AWS IoT Core のドキュメントに記載されています。
curl -o root_ca.pem \
-s https://www.amazontrust.com/repository/AmazonRootCA1.pem
取得したルート CA 証明書は、クライアント側での接続時に、サーバー証明書の検証に使用します。
デバイスエンドポイントの取得
AWS IoT Core では、AWS アカウントごとにデバイスエンドポイントが提供されます。デバイスエンドポイントは、クライアントが AWS IoT Core に接続する際に使用するホスト名です。
デバイスエンドポイントを取得するには、AWS CLI を使用して次のコマンドを実行します。
aws iot describe-endpoint --endpoint-type iot:Data-ATS
出力結果の endpointAddress
の xxxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com
のような形式の値がデバイスエンドポイントになります。クライアントによる接続時にホスト名として使用します。
Mosquitto のインストール
ローカル環境に Mosquitto クライアントをインストールします。Mosquitto は、Eclipse Foundation が提供するオープンソースの MQTT ブローカーおよびクライアントです。
今回は macOS 環境なので、Homebrew を使用してインストールします。(インストール開始から完了まで思ったより時間が掛かりました。)
brew install mosquitto
Mosquitto で Pub/Sub を実施
サブスクライブの開始
まずは、mosquitto_sub を使用して、サブスクライブを開始します。以下のコマンドを実行して、AWS IoT Core に接続し、トピック hoge
をサブスクライブします。ポート番号 8883 は MQTT over TLS のデフォルトポートです。
$ DEVICE_ENDPOINT=<デバイスエンドポイント>
$ mosquitto_sub \
--id "subscriber-device-001" \
--cafile "root_ca.pem" \
--cert "subscriber_cert.pem" \
--key "subscriber_private_key.pem" \
--host "${DEVICE_ENDPOINT}" \
--port 8883 \
--topic "hoge" \
--debug
次のような出力が表示されたらサブスクライブ成功です。
Client subscriber-device-001 sending CONNECT
Client subscriber-device-001 received CONNACK (0)
Client subscriber-device-001 sending SUBSCRIBE (Mid: 1, Topic: hoge, QoS: 0, Options: 0x00)
Client subscriber-device-001 received SUBACK
Subscribed (mid: 1): 0
AWS IoT のマネジメントコンソールから MQTT テストクライアントを使用して、トピック hoge
にメッセージをパブリッシュしてみます。
すると、サブスクライブしているターミナルに次のようなメッセージが表示されます。
Client subscriber-device-001 received PUBLISH (d0, q0, r0, m0, 'hoge', ... (57 bytes))
{
"message": "AWS IoT コンソールからの挨拶"
}
サブスクライブによりちゃんとメッセージが受信できていることが確認できました。
パブリッシュ
次の別のターミナルで、mosquitto_pub を使用して、hoge
トピックにメッセージをパブリッシュしてみます。
$ DEVICE_ENDPOINT=<デバイスエンドポイント>
$ mosquitto_pub \
--id "publisher-device-001" \
--cafile "root_ca.pem" \
--cert "publisher_cert.pem" \
--key "publisher_private_key.pem" \
--host "${DEVICE_ENDPOINT}" \
--port 8883 \
--topic "hoge" \
--message '{"message":"Hello, World!"}' \
--debug
次のような出力が表示されたらパブリッシュ成功です。
Client publisher-device-001 sending CONNECT
Client publisher-device-001 received CONNACK (0)
Client publisher-device-001 sending PUBLISH (d0, q0, r0, m1, 'hoge', ... (27 bytes))
Client publisher-device-001 sending DISCONNECT
mosquitto_sub を実行したサブスクリプション側のコンソールで、次のように表示されていればメッセージ受信が成功しています。
Client subscriber-device-001 received PUBLISH (d0, q0, r0, m0, 'hoge', ... (27 bytes))
{"message":"Hello, World!"}
また AWS IoT のマネジメントコンソールから MQTT テストクライアントでも、同トピックでメッセージが受信できていることが確認できます。
パブリッシュしたメッセージがサブスクライブ側で受信できることが確認できました。
後片付け
CDK で作成したリソースの削除
# CDK スタックを削除
cdk destroy
AWS CLI で作成した証明書の削除
# 証明書一覧を確認
aws iot list-certificates
# サブスクライブ用証明書の削除
SUBSCRIBER_CERT_ID="<サブスクライブ用証明書のID>"
aws iot update-certificate --certificate-id $SUBSCRIBER_CERT_ID --new-status INACTIVE
aws iot delete-certificate --certificate-id $SUBSCRIBER_CERT_ID
# パブリッシュ用証明書の削除
PUBLISHER_CERT_ID="<パブリッシュ用証明書のID>"
aws iot update-certificate --certificate-id $PUBLISHER_CERT_ID --new-status INACTIVE
aws iot delete-certificate --certificate-id $PUBLISHER_CERT_ID
ローカルファイルの削除
# 証明書関連ファイルの削除
rm -f subscriber_public_key.pem
rm -f subscriber_private_key.pem
rm -f subscriber_cert.pem
rm -f publisher_public_key.pem
rm -f publisher_private_key.pem
rm -f publisher_cert.pem
rm -f root_ca.pem
おわりに
AWS IoT Core に Mosquitto クライアントで MQTT メッセージを Pub/Sub する構成を AWS CDK を使用して実装する方法を確認してみました。
ポリシーを適切に定義してセキュリティを考慮しつつ、IoT Core への接続が行える最低限のリソースを用意することができました。
参考
以上