CloudFormationを使用してAWS IoT Core入門

CloudFormationを使用してAWS IoT Core入門

CloudFormationを使用してAWS IoT Core入門

本ページではAWS IoT Coreを扱います。

AWS IoT Coreサービスは IoT デバイスを AWS IoTサービスおよび他の AWSサービスと接続します。AWS IoT Core には、IoT デバイスとクラウドを接続してその間のメッセージを処理する、デバイスゲートウェイとメッセージブローカーが含まれています。

AWS IoT Core の開始方法

今回はAWS IoT Core入門ということで、以下の公式チュートリアルを参考に進めます。

https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/creating-a-virtual-thing.html

https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/using-laptop-as-device.html

具体的には、EC2インスタンスをIoTデバイスに見立て、AWS IoT CoreにMQTTメッセージを送信する方法を確認します。

AWS IoT CoreはAWSが提供する様々なIoT関係のサービスの中で、最も中心的なサービスの1つです。AWS IoT Coreについて理解を深めることで、スムーズにIoTアプリを構築することができるようになるでしょう。

構築する環境

Diagram of Introduction to AWS IoT Core using CloudFormation.

VPC内にパブリックサブネットを作成します。
同サブネット内にEC2インスタンスを配置します。
このインスタンスをIoTデバイス(Thing)に見立てます。
このインスタンスはAmazon Linux 2023です。

このデバイスからAWS IoT Coreに接続して、MQTTメッセージを送信します。
AWS IoT Coreへの接続およびメッセージの送信には、AWS IoT Device SDK for Pythonを使用します。

検証のシナリオ

本ページでは、以下を確認します。

  • モノのオブジェクト(thing objects)
  • AWS IoT ポリシー
  • クライアント証明書
  • AWS IoT Device SDKを使用したAWS IoT Coreへの接続

CloudFormationテンプレートファイル

上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。

https://github.com/awstut-an-r/awstut-fa/tree/main/151

テンプレートファイルのポイント解説

AWS IoT ThingはIoTデバイスを意味する

モノのオブジェクトとは、AWS IoTによって管理する対象のデバイスを意味します。

AWS IoT Core に接続されたデバイスは、AWS IoT レジストリでモノのオブジェクトによって表されます。モノのオブジェクトは、特定のデバイスまたは論理エンティティを表します。物理的なデバイスやセンサー (電球、または電気をつけるための壁にあるスイッチなど) は、モノとして扱うことができます。

モノのオブジェクトを作成する
Resources:
  Thing:
    Type: AWS::IoT::Thing
    Properties:
      ThingName: !Ref Prefix
Code language: YAML (yaml)

今回はEC2インスタンスをIoTデバイスとして見立てますので、このモノのオブジェクトがインスタンスを意味することになります。

今回はシンプルにモノのオブジェクト名だけを設定します。
名前は「fa-151」とします。

AWS IoT ポリシーでモノの権限を制御する

AWS IoT ポリシーはデバイスが使用する証明書に紐づくもので、デバイスがAWS IoTに対して実行可能なアクションを定義するものです。

デバイスは、AWS IoT Core での認証に X.509 証明書を使用します。証明書には AWS IoT ポリシーがアタッチされています。これらのポリシーは、デバイスで実行できる AWS IoT オペレーション (MQTT トピックへのサブスクライブや公開など) を決定します。デバイスは AWS IoT Core に接続してメッセージを送信するときに、証明書を提示します。

AWS IoT ポリシーを作成する
Resources:
  Policy:
    Type: AWS::IoT::Policy
    Properties:
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action:
              - iot:Publish
              - iot:Receive
            Resource:
              - !Sub "arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topic/${TopicName}"
          - Effect: Allow
            Action:
              - iot:Subscribe
            Resource:
              - !Sub "arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topicfilter/${TopicName}"
          - Effect: Allow
            Action:
              - iot:Connect
            Resource:
              - !Sub "arn:aws:iot:${AWS::Region}:${AWS::AccountId}:client/${Thing}"
      PolicyName: !Ref Prefix
Code language: YAML (yaml)

ご覧の通り、AWS IoTポリシーはIAMポリシーに似ています。

今回は検証ということで、以下の公式のチュートリアルに記載されているものを参考に作成しました。

https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/using-laptop-as-device.html#gs-pc-python-app-run

このポリシーの名前は「fa-151」とします。

EC2インスタンスをIoTデバイスとしてセットアップする

先述の通り、今回のEC2インスタンスの役割はIoTデバイスです。
ユーザデータを使用して、IoTデバイス側で必要となる各種の設定を行います。

ユーザデータに関する基本的な事項については、以下のページをご確認ください。

https://awstut.com/2021/12/02/ec2-init-4ways

Resources:
  Instance:
    Type: AWS::EC2::Instance
    Properties:
      IamInstanceProfile: !Ref InstanceProfile
      ImageId: !Ref ImageId
      InstanceType: !Ref InstanceType
      NetworkInterfaces:
        - AssociatePublicIpAddress: true
          DeviceIndex: 0
          GroupSet:
            - !Ref InstanceSecurityGroup
          SubnetId: !Ref PublicSubnet1
      UserData: !Base64
        Fn::Sub: |
          #!/bin/bash -xe
          dnf update -y
          dnf install python3.11-pip -y
          dnf install -y git

          mkdir ~/certs

          curl -o ~/certs/Amazon-root-CA-1.pem \
            https://www.amazontrust.com/repository/AmazonRootCA1.pem

          aws iot create-keys-and-certificate \
            --set-as-active \
            --certificate-pem-outfile "~/certs/device.pem.crt" \
            --public-key-outfile "~/certs/public.pem.key" \
            --private-key-outfile "~/certs/private.pem.key" > ~/output.txt

          certificate_arn=`cat ~/output.txt | jq -r .certificateArn`

          aws iot attach-thing-principal \
            --thing-name "${Thing}" \
            --principal "$certificate_arn"

          aws iot attach-policy \
            --policy-name "${Policy}" \
            --target "$certificate_arn"

          aws iot describe-endpoint \
            --endpoint-type iot:Data-ATS > ~/endpoint.txt

          endpoint=`cat ~/endpoint.txt| jq -r .endpointAddress`

          python3.11 -m pip install awsiotsdk
          cd ~ && git clone https://github.com/aws/aws-iot-device-sdk-python-v2.git

          cd ~/aws-iot-device-sdk-python-v2/samples && python3.11 pubsub.py \
            --endpoint "$endpoint" \
            --ca_file ~/certs/Amazon-root-CA-1.pem \
            --cert ~/certs/device.pem.crt \
            --key ~/certs/private.pem.key \
            --client_id "${Thing}" \
            --topic "${TopicName}" \
            --count 5
Code language: YAML (yaml)

UserDataプロパティに注目します。

まずdnfのアップデート後、dnfを使用してPython11とGitをインストールします。
これらはAWS IoT Device SDK for Pythonの取得と実行のために使用します。

続いてcurlコマンドを使用して、Amazon認証機関証明書(AmazonRootCA1.pem)を取得します。
この証明書はIoTデバイスからAWS IoT CoreにMQTTメッセージを送信する際に、送信先が正しいかを確認するためのサーバ証明のために使用します。

aws iot create-keys-and-certificateコマンドで証明書を作成します。
モノがAWS IoT Coreと通信する際に使用するクライアント証明書(device.pem.crt)を生成します。
証明書はEC2インスタンスに生成されるとともに、AWS IoT側にも登録されます。

またコマンドを実行すると、証明書を作成する際に使用した公開鍵(public.pem.key)と秘密鍵(private.pem.key)も取得することができます。
特に秘密鍵の方は、AWS IoT CoreにMQTTメッセージを送信する際に使用します。

このコマンドの実行結果を、テキストファイル(output.txt)に保存します。
これは証明書がAWS IoTに登録された際に、証明書に割り振られたARNを取得するためです。
jqコマンドを使用して、これを取得して変数に格納します。
ちなみにこのコマンドの実行結果は以下のフォーマットで返されます。

{
    "certificateArn": "arn:aws:iot:[region]:[account-id]:cert/[cert-id]",
    "certificateId": "[cert-id]",
    "certificatePem": "-----BEGIN CERTIFICATE-----\n[cert-pem]\n-----END CERTIFICATE-----\n",
    "keyPair": {
        "PublicKey": "-----BEGIN PUBLIC KEY-----\n[public-key]\n-----END PUBLIC KEY-----\n",
        "PrivateKey": "-----BEGIN RSA PRIVATE KEY-----\n[private-key]\n-----END RSA PRIVATE KEY-----\n"
    }
}
Code language: JSON / JSON with Comments (json)

aws iot attach-thing-principalコマンドを実行して、モノと証明書を関連付けます。
それぞれ先述のモノのリソース名と証明書のARNを指定します。

aws iot attach-policyコマンドを実行して、AWS IoT ポリシーと証明書を関連づけます。
注意点は、AWS IoT ポリシーに関連づける対象はモノではなく、証明書であるという点です。
先述のポリシー名と証明書のARNを指定します。

aws iot describe-endpointコマンドを実行して、MQTTメッセージの送信先であるエンドポイントを取得します。
エンドポイントは以下の通り、2種類あります。

すべてのカスタマーには iot:Data-ATS および iot:Data エンドポイントがあります。各エンドポイントは X.509 証明書を使用して、クライアントを認証します。Symantec 認証機関の広範な不信用に関連する問題を避けるために、新しい iot:Data-ATS エンドポイントタイプを使用することを強くお勧めします。

デバイスの AWS IoT への接続

上記に従い、endpoint-typeオプションに「iot:Data-ATS」を指定します。

このコマンドの実行結果をテキストファイル(endpoint.txt)に保存し、エンドポイント名を変数に格納します。
ここでもjqコマンドを使用して、以下のJSON形式のデータからエンドポイント名を抽出します。

{
    "endpointAddress": "[endpoint-id]-ats.iot.[region].amazonaws.com"
}
Code language: JSON / JSON with Comments (json)

pipを使用して、AWS IoT Device SDK for Python(awsiotsdk)をインストールします。
そしてGitを使用して、本SDKのサンプルスクリプトも取得します。

最後にサンプルスクリプトpubsub.pyを実行します。
このサンプルスクリプトはAWS IoT Coreにテストメッセージ(Hello World!)を送信します。

このスクリプトを実行するために、これまで取得したエンドポイント、Amazon認証機関証明書、クライアント証明書、クライアント証明書用の秘密鍵を渡します。

加えて2つのパラメータを指定します。

1つ目はクライアントIDです。
これについてはAWS公式で以下の通りに説明されています。

一般的なデバイスのユースケースでは、デフォルトの MQTT クライアント ID としてモノの名前が使用されます。MQTT クライアント ID、証明書、またはシャドウ状態をモノのレジストリ名として使用するというマッピングは強制されませんが、レジストリと Device Shadow サービスの両方で、モノの名前を MQTT クライアント ID として使用することをお勧めします。

AWS IoT によるデバイスの管理

上記に従い、本パラメータにモノの名前を指定します。

2つ目はトピック名です。
これについてはAWS公式で以下の通りに説明されています。

MQTT トピックでは、AWS IoT メッセージを識別します。AWS IoT クライアントは、メッセージのトピック名を指定してパブリッシュするメッセージを識別します。クライアントは、トピックフィルターを AWS IoT Core に登録して、サブスクライブ (受信) するメッセージを識別します。メッセージブローカーはトピック名とトピックフィルターを使用して、パブリッシュするクライアントからサブスクライブするクライアントに、 メッセージを振り分けます。

MQTT トピック

こちらについては「test/topic」を指定します。

EC2インスタンス用のIAMロール

Resources:
  InstanceRole:
    Type: AWS::IAM::Role
    DeletionPolicy: Delete
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service:
                - ec2.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
      Policies:
        - PolicyName: InstancePolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - iot:AttachPolicy
                  - iot:AttachThingPrincipal
                  - iot:CreateKeysAndCertificate
                  - iot:DescribeEndpoint
                Resource:
                  - "*"
Code language: YAML (yaml)

先ほど確認した通り、今回のEC2インスタンスはIoTデバイスとして動作することを目的としています。
そのためにユーザデータによる初期化処理の定義において、必要な処理を記載しています。
このIAMロールはそれらの処理を実行するために必要な許可をインスタンスに与えます。

環境構築

CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。

CloudFormationスタックを作成してリソースを確認する

CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

https://awstut.com/2021/12/02/cloudformation-nested-stacks

AWS IoT の各種リソースを確認します。

AWS IoT Thing(モノ)を確認します。

Detail of AWS IoT 01.

確かにモノが作成されています。

AWS IoT ポリシーを確認します。

Detail of AWS IoT 02.

サンプルスクリプトを実行するために必要な権限が与えられています。
トピック名には「test/topic」、クライアントIDには「fa-151」が指定されています。
これらはEC2インスタンスがサンプルスクリプトを実行する際に指定することになっている値です。

証明書を確認します。

Detail of AWS IoT 03.
Detail of AWS IoT 04.

確かに証明書が作成されています。
つまりEC2インスタンスにおいて、aws iot create-keys-and-certificateコマンドが正常に実行されたということです。

またこの証明書に関連づいているリソースを見ると、先ほど確認したポリシーとモノが確認できます。
つまり同じくインスタンスにおいて、aws iot attach-thing-principalコマンドおよびaws iot attach-policyコマンドが正常に実行されたということです。

エンドポイント名を確認します。

Detail of AWS IoT 05.

私のアカウントに割り当てられたAWS IoTエンドポイントです。
EC2インスタンスはこちらにMQTTメッセージを送信することになります。

動作確認

準備が整いましたので、EC2インスタンス()にアクセスします。
インスタンスへのアクセスはSSM Session Managerを使用します。

% aws ssm start-session --target i-045ca338a33b06229
...
sh-5.2$
Code language: Bash (bash)

SSM Session Managerに関しては、以下のページをご確認ください。

https://awstut.com/2021/12/01/ssm-session-manager-linux

ユーザデータに定義した処理の実行状況を確認します。

treeコマンドを使用して、/rootディレクトリの中身を確認します。

sh-5.2$ sudo tree /root
/root
├── aws-iot-device-sdk-python-v2
│    ├── ...
│    ├── samples
│   │    │   ├── ...
│   │    │   ├── pubsub.py
│   │    │   └── ...
│    └── ...
├── certs
│    ├── Amazon-root-CA-1.pem
│    ├── device.pem.crt
│    ├── private.pem.key
│    └── public.pem.key
├── endpoint.txt
└── output.txt
Code language: Bash (bash)

確かにユーザデータに定義した処理が正常に実行されて、各種ファイルが配置されています。

journalctlコマンドを実行して、インスタンスのログを確認します。

sh-5.2$ sudo journalctl --no-pager
...
Feb 24 06:15:19 ip-10-0-1-45.ap-northeast-1.compute.internal cloud-init[1672]: + python3.11 pubsub.py --endpoint a2oxckhng7gmur-ats.iot.ap-northeast-1.amazonaws.com --ca_file /root/certs/Amazon-root-CA-1.pem --cert /root/certs/device.pem.crt --key /root/certs/private.pem.key --client_id fa-151 --topic test/topic --count 5
...
Feb 24 06:15:25 ip-10-0-1-45.ap-northeast-1.compute.internal cloud-init[1672]: Connecting to a2oxckhng7gmur-ats.iot.ap-northeast-1.amazonaws.com with client ID 'fa-151'...
Feb 24 06:15:25 ip-10-0-1-45.ap-northeast-1.compute.internal cloud-init[1672]: Connected!
Feb 24 06:15:25 ip-10-0-1-45.ap-northeast-1.compute.internal cloud-init[1672]: Subscribing to topic 'test/topic'...
Feb 24 06:15:25 ip-10-0-1-45.ap-northeast-1.compute.internal cloud-init[1672]: Connection Successful with return code: 0 session present: False
Feb 24 06:15:25 ip-10-0-1-45.ap-northeast-1.compute.internal cloud-init[1672]: Subscribed with 1
Feb 24 06:15:25 ip-10-0-1-45.ap-northeast-1.compute.internal cloud-init[1672]: Sending 5 message(s)
Feb 24 06:15:25 ip-10-0-1-45.ap-northeast-1.compute.internal cloud-init[1672]: Publishing message to topic 'test/topic': Hello World!  [1]
Feb 24 06:15:25 ip-10-0-1-45.ap-northeast-1.compute.internal cloud-init[1672]: Received message from topic 'test/topic': b'"Hello World!  [1]"'
Feb 24 06:15:25 ip-10-0-1-45.ap-northeast-1.compute.internal cloud-init[1672]: Publishing message to topic 'test/topic': Hello World!  [2]
Feb 24 06:15:25 ip-10-0-1-45.ap-northeast-1.compute.internal cloud-init[1672]: Received message from topic 'test/topic': b'"Hello World!  [2]"'
Feb 24 06:15:25 ip-10-0-1-45.ap-northeast-1.compute.internal cloud-init[1672]: Publishing message to topic 'test/topic': Hello World!  [3]
Feb 24 06:15:25 ip-10-0-1-45.ap-northeast-1.compute.internal cloud-init[1672]: Received message from topic 'test/topic': b'"Hello World!  [3]"'
Feb 24 06:15:25 ip-10-0-1-45.ap-northeast-1.compute.internal cloud-init[1672]: Publishing message to topic 'test/topic': Hello World!  [4]
Feb 24 06:15:25 ip-10-0-1-45.ap-northeast-1.compute.internal cloud-init[1672]: Received message from topic 'test/topic': b'"Hello World!  [4]"'
Feb 24 06:15:25 ip-10-0-1-45.ap-northeast-1.compute.internal cloud-init[1672]: Publishing message to topic 'test/topic': Hello World!  [5]
Feb 24 06:15:25 ip-10-0-1-45.ap-northeast-1.compute.internal cloud-init[1672]: Received message from topic 'test/topic': b'"Hello World!  [5]"'
Feb 24 06:15:25 ip-10-0-1-45.ap-northeast-1.compute.internal cloud-init[1672]: 5 message(s) received.
Feb 24 06:15:25 ip-10-0-1-45.ap-northeast-1.compute.internal cloud-init[1672]: Disconnecting...
Feb 24 06:15:25 ip-10-0-1-45.ap-northeast-1.compute.internal cloud-init[1672]: Connection closed
Feb 24 06:15:25 ip-10-0-1-45.ap-northeast-1.compute.internal cloud-init[1672]: Disconnected!
...
Code language: Bash (bash)

AWS IoTエンドポイントにMQTTメッセージを送信している箇所を抽出しました。
サンプルスクリプトを実行してAWS IoTに接続し、5回メッセージを送信していることがわかります。

最後にEC2インスタンスから送信されたメッセージをAWS IoT側で確認します。
確認には、AWSマネージメントコンソール内のAWS IoT MQTTクライアントを使用します。

Detail of AWS IoT 06.

Subscribe to a topicタブのTopic filterにトピック名を入力し、Subscribeを押下します。
EC2インスタンスは「test/topic」宛にメッセージを送信しているので、これを指定します。

Detail of AWS IoT 07.

メッセージが表示されました。
確かに先ほどEC2インスタンスから送信されたものです。

モノとAWS IoT ポリシーを削除する前に、証明書をデタッチすること!

確認が済んだ後にはCloudFormationスタックを削除することになると思いますが、注意点があります。
それはモノおよびAW IoT ポリシーリソースを削除する前に、証明書とのアタッチを解除する必要があるという点です。

Detail of AWS IoT 08.
Detail of AWS IoT 09.

両ページで「Detach」を押下してください。

これを実行する前に両リソースを削除しようとすると、削除に失敗するのでご注意ください。

まとめ

EC2インスタンスをIoTデバイスに見立て、AWS IoT CoreにMQTTメッセージを送信する方法を確認しました。

AWS IoT CoreはAWSが提供する様々なIoT関係のサービスの中で、最も中心的なサービスの1つです。
AWS IoT Coreについて理解を深めることで、スムーズにIoTアプリを構築することができるようになるでしょう。