0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWS IoTでOTAのデモを試してみる

0
Posted at

AWS ROS2 OTA デモ 実施ガイド

AWSが公開しているサンプルリポジトリ ros2-ota-firmware-updates を使って、AWS IoT Greengrass + IoT Jobs によるROS2ファームウェアのOTA更新デモを実際に動かした記録です。
(主にgithub RepoをClaudeに読み込ませて、実行手順などをガイドしてもらいました。この記事もほぼClaudeが書いてます)

Mac上でDockerを使い、仮想ロボット2台にOTAでファームウェアを配信・更新する仕組みを体験できます。各ステップで「何が起きているか」の解説も含めています。


全体アーキテクチャ

                          ┌─── AWS クラウド ────────────────────────────────┐
                          │                                                 │
                          │  IoT Core                                       │
                          │  ┌─────────────┐  ┌───────────┐  ┌──────────┐ │
                          │  │  IoT Things │  │ IoT Jobs  │  │ IoT Rule │ │
                          │  │ -1-agent    │  │           │  │    ↓     │ │
                          │  │ -1-firmware │  │ OTA指示   │  │ Lambda   │ │
                          │  │ -2-agent    │  │ 管理      │  │ (状態記録)│ │
                          │  │ -2-firmware │  │           │  │          │ │
                          │  └─────────────┘  └───────────┘  └──────────┘ │
                          │                                                 │
                          └─────────────────────┬───────────────────────────┘
                                                │ MQTT over TLS (8883)
                                                │ ジョブ通知・状態報告
                                                │
┌─── Mac ローカル (Docker network: greengrass) ─┼──────────────────────────┐
│                                               │                          │
│                                     ┌─────────▼────────┐                │
│                                     │   greengrass     │                │
│                                     │                  │                │
│                                     │  Greengrass v2   │                │
│                                     │  Core デバイス   │                │
│                                     │  (AWSとの通信    │                │
│                                     │   ブリッジ)      │                │
│                                     └────────┬─────────┘                │
│                                              │ MQTT (Greengrass          │
│                            ┌─────────────────┤  Discovery経由)           │
│                            │                 │                           │
│                  ┌─────────▼──────┐ ┌────────▼───────┐                 │
│                  │    device1     │ │    device2     │                 │
│                  │                │ │                │                 │
│                  │ OTAエージェント │ │ OTAエージェント │                 │
│                  │ (Python)       │ │ (Python)       │                 │
│                  │    +           │ │    +           │                 │
│                  │ Docker-in-     │ │ Docker-in-     │                 │
│                  │ Docker         │ │ Docker         │                 │
│                  │ [ROS2コンテナ] │ │ [ROS2コンテナ] │                 │
│                  └────────┬───────┘ └────────┬───────┘                 │
│                           │ Docker pull        │ Docker pull             │
│                           │ firmware:N         │ firmware:N              │
│                           └─────────┬──────────┘                        │
│                                     │                                   │
│                            ┌────────▼──────┐                            │
│                            │   registry    │                            │
│                            │               │                            │
│                            │ ローカル      │                            │
│                            │ Docker        │                            │
│                            │ Registry      │                            │
│                            │ :5555         │                            │
│                            │               │                            │
│                            │ firmware:1    │                            │
│                            │ firmware:2    │                            │
│                            │ firmware:3    │                            │
│                            └───────────────┘                            │
└─────────────────────────────────────────────────────────────────────────┘

ポイント:通信経路は2種類ある

経路 経由 用途
device ↔ AWS IoT Greengrass経由 ジョブ通知・完了報告(MQTT)
device → registry 直接(同じDockerネットワーク内) ファームウェアイメージのpull

Greengrassは「AWSとの通信」のゲートウェイであって、ファームウェアイメージの配信には関与しない。
registryとdeviceはローカルネットワーク内で直接やり取りする。


なぜECRではなくローカルregistryを使うのか

ECRを使うイメージが強いが、このデモがローカルregistryにしている理由はデバイスがインターネットに直接アクセスできない前提だから。

工場・倉庫・ロボットが動く閉じたネットワークを想定しており、
デバイスはGreengrass経由でAWS IoT(制御)には繋がれるが、ECRには直接届かない。

❌ device → ECR(インターネット越し)              ← 閉じたネットワークでは不可
✅ device → registry(同じDockerネットワーク内)   ← 可

本番でのパターン:

パターン 構成 向いているケース
このデモ 施設内にローカルregistryを設置 工場・倉庫など閉じたネットワーク
ECR + VPCエンドポイント VPC内でECRにプライベートアクセス AWSと専用線/VPN接続している場合
ECR Public / S3 デバイスがインターネットに出られる クラウド接続ロボット
Greengrassコンポーネント機能 Greengrass自身がコンテナをデプロイ より本格的なGreengrass活用

理想的な発展形:

ローカルregistryをECRのプロキシキャッシュとして構成する。
ECRにプッシュ → ローカルregistryが自動でミラー → デバイスはローカルからpull。
READMEのNext Stepsにも Setting up the registry to proxy an upstream docker registry と記載されている。


OTA更新の完全な流れ

1. python deploy_job.py 2   ← MacからAWS IoT Jobsにジョブを発行

2. AWS IoT Jobs が device-thing-1-agent に向けてジョブを通知
   (MQTT トピック: $aws/things/.../jobs/notify-next)

3. Greengrass Core がジョブ通知をデバイスに中継
   (デバイスはGreengrass経由でAWSと通信 = インターネット直接不要)

4. device1 コンテナ内のOTAエージェントがジョブを受信
   → 実行中の ROS2コンテナ(firmware:1)を停止
   → ローカルregistryから firmware:2 をpull
   → firmware:2 を起動

5. ジョブの成否を AWS IoT Jobs に報告 (SUCCEEDED)

6. IoT TopicRule が SUCCEEDED イベントを検知
   → Lambda が発火して以下の2つをAWS IoTに書き込む:
      - IoT Thing Shadow("firmware")の reported.firmwareVersion を更新
      - IoT Thing の attributes.firmwareVersion を更新

OTA完了後にLambdaが書き込む2つの意味

Thing 属性(attributes):フリート全体の検索・集計に使う

# v1 を動かしているデバイスを一覧
aws iot search-index --query-string 'attributes.firmwareVersion:1'

# v2 未適用のデバイスを探す
aws iot search-index --query-string 'NOT attributes.firmwareVersion:2'

数百台・数千台のロボットを運用するとき「まだ古いバージョンが残っているデバイスはどれか」を把握するために使う(IoT Fleet Indexing)。

Thing Shadow(firmware):デバイス1台の「あるべき状態」と「実際の状態」の管理に使う

IoT Thing Shadow は desired(望む状態)と reported(実際の状態)を持つ仕組み。
Lambda は SUCCEEDED 時に reported.firmwareVersion を書き込む。

{
  "state": {
    "desired":  { "firmwareVersion": "2" },
    "reported": { "firmwareVersion": "2" }
  }
}

desiredreported が一致していれば更新成功、ズレていれば失敗・未反映を検知できる。

まとめると:

書き込み先 用途
Thing attributes フリート横断で「今どのバージョンが何台動いているか」を検索
Thing Shadow 1台ごとに「OTAが正しく反映されたか」を状態として管理

前提条件

ツール 必要バージョン 確認コマンド
Docker 26.1.1+ docker --version
AWS CLI 2.16.1+ aws --version
Node.js 20.14.0+ node --version
npm 10.7.0+ npm --version
CDK 任意 npx cdk --version
Python 3.9+ (3.12推奨) python3 --version
boto3 任意 pip3 install boto3

AWSクレデンシャルとリージョンが設定済みであること:

aws sts get-caller-identity       # 疎通確認
export AWS_DEFAULT_REGION=us-east-1

Step 1: リポジトリのクローン

git clone https://github.com/aws-samples/ros2-ota-firmware-updates.git
cd ros2-ota-firmware-updates

注意: ワーキングディレクトリに既存ファイルがあって git clone が失敗する場合は以下で取得する:

git init
git remote add origin https://github.com/aws-samples/ros2-ota-firmware-updates.git
git fetch origin
git checkout -b main origin/main

Step 2: IoT Things の作成

cd containers
bash setup-device-thing.sh

何が起きるか:

AWS IoT Core に4つの Thing(デバイスの論理表現)を作成する。
各デバイスを「firmware」と「agent」の2ロールに分けている理由は、
ROS2アプリ本体(firmware)とOTAエージェント(agent)を別々の
IoT IDとして管理することで、それぞれ独立した証明書・ポリシーを持てるため。

作成されるThing:
  device-thing-1-firmware  ← device1上で動くROS2アプリのIoT ID
  device-thing-1-agent     ← device1上で動くOTAエージェントのIoT ID
  device-thing-2-firmware
  device-thing-2-agent

各Thingごとに:
  - X.509証明書を生成 (device.pem.crt / private.pem.key / public.pem.key)
  - AmazonルートCA証明書をダウンロード (AmazonRootCA1.pem)
  - IoT Policy (device-thing-policy) を作成・アタッチ
  → certs/{thing-name}/ フォルダに保存

IoT Policy が許可するアクション:
iot:Publish, iot:Subscribe, iot:Receive, iot:Connect,
iot:GetThingShadow, iot:UpdateThingShadow,
greengrass:Discover, greengrass:PutCertificateAuthorities

macOSの注意: スクリプトが wget を使っているが macOS にはない。
setup-device-thing.sh の該当行を以下に修正する:

# 変更前: wget ... -O certs/...
# 変更後:
curl -s https://www.amazontrust.com/repository/AmazonRootCA1.pem -o certs/${THING_NAME}/AmazonRootCA1.pem

Step 3: IoT イベントの有効化

aws iot update-event-configurations \
  --event-configurations '{"JOB": {"Enabled": true}, "JOB_EXECUTION": {"Enabled": true}}'

何が起きるか:

IoT Jobs のジョブ作成・完了・失敗などのイベントを MQTT トピックに配信する設定。
これを有効にすることで:

  • デバイスがジョブの開始/完了をリアルタイムで検知できる
  • IoT TopicRule(後でCDKで作成)がジョブ完了イベントをトリガーにLambdaを呼べる

Step 4: AWSクレデンシャルをコンテナ用に準備

cd containers
bash aws-credentials.sh

何が起きるか:

~/.aws/credentials の内容を containers/credentials ファイルにコピーする。
これは compose.yaml で Greengrass コンテナに以下のようにマウントされる:

volumes:
  - ./credentials:/root/.aws/credentials:ro

Greengrass コンテナの初回起動時にこのクレデンシャルを使って
AWS IoT に自身を Thing として登録(プロビジョニング)する。

macOSの注意: スクリプト内の grep -P がエラーになるが、
cat によるファイルコピーは成功するため実害なし。


Step 5: Greengrassサービスロールの作成

cd containers
bash check-gg-service-role.sh

何が起きるか:

Greengrass が AWS の各サービスにアクセスするために必要な IAM ロールを作成する。
Greengrass はデバイスのプロキシとして動作し、IoT Core や S3 などにアクセスするため
専用のサービスロールが必要。

作成・設定されるもの:
  IAM Role: AWSGreengrassServiceRole
    Trust Policy: greengrass.amazonaws.com が AssumeRole できる
    Attached Policies:
      - AWSGreengrassResourceAccessRolePolicy
      - AmazonS3ReadOnlyAccess

  → aws greengrassv2 associate-service-role-to-account でアカウントに関連付け

Step 6: Dockerコンテナ起動

cd containers
bash start.sh
# 内部: docker compose --project-name ros-ota up --build

何が起きるか:

4つのコンテナが起動する:

registry コンテナ

  • registry:2 イメージをそのまま使用
  • ポート 5555 でローカル Docker Registry を起動
  • ROS2ファームウェアイメージ (firmware:1〜3) を保存する場所

greengrass コンテナ

  • Amazon Linux 2023 ベースのイメージをビルド
  • 起動時に Greengrass v2 をインストール・プロビジョニング
    • THING_NAME=RosProvisioningGreengrassCore で IoT Thing を自動作成
    • Greengrass Core デバイスとして AWS IoT に登録
  • ポート 8883 (MQTT over TLS) を公開
  • デバイスとAWSの通信ブリッジとして機能

device1, device2 コンテナ

  • docker:24-dind (Docker-in-Docker) ベースのイメージをビルド
  • コンテナ内で別の Docker コンテナ(ROS2ファームウェア)を動かすための環境
  • OTAエージェント(Python)を起動し、Greengrass Discovery を実行
    • Greengrassのエンドポイントを自動検出してMQTT接続
    • IoT Jobs を監視してOTA更新を待機

起動直後のdeviceログ(正常):

device1-1 | Performing greengrass discovery...   ← 接続先を探索中
device1-1 | Connected!                            ← Greengrass経由でAWSに接続成功
device1-1 | Waiting for further jobs...           ← ジョブ待機状態

初回はGreengrass Discoveryが404になりながらリトライを繰り返す。
Step 7の関連付けが完了すると接続が成功する(想定内の動作)。

このデモに登場するIoT Thingは3種類

RosProvisioningGreengrassCore は Greengrassコンテナ自身のIoT Thingで、device-thing-1/2 とは別物。
AWS上でこのThingに対してGreengrassの動作設定(デプロイメント)が紐付けられる。

deployment-template.json に定義されたコンポーネントがこのThingにデプロイされる:

コンポーネント 役割
aws.greengrass.Nucleus Greengrass本体
aws.greengrass.clientdevices.Auth device-thing-* パターンのデバイスを認証・接続許可
aws.greengrass.clientdevices.IPDetector GreengrassのIPアドレスをデバイスに通知(Discovery用)
aws.greengrass.clientdevices.mqtt.Moquette ローカルMQTTブローカー
aws.greengrass.clientdevices.mqtt.Bridge ローカルMQTT ⇔ AWS IoT Core 間でトピックを中継

MQTTブリッジの設定(deployment-template.json より):

  • clients/+/hello/world → ローカル発 → IoT Core へ転送(ROS2のハートビート)
  • $aws/things/+/jobs/# → IoT Core ⇔ ローカル 双方向(ジョブ通知・応答)
  • $aws/things/+/shadow/# → IoT Core ⇔ ローカル 双方向(Shadow更新)
Thing名 正体 役割
RosProvisioningGreengrassCore Greengrassコンテナ自身 MQTTブローカー・デバイス認証・AWS IoT中継の設定管理
device-thing-{1,2}-agent deviceコンテナのOTAエージェント ジョブを受け取りコンテナ入れ替えを実行
device-thing-{1,2}-firmware deviceコンテナ内のROS2アプリ ハートビートをIoT Coreに送信

Step 7: デバイスをGreengrassに関連付け

コンテナが起動したら別ターミナルで実行:

cd containers
bash associate-client-devices.sh

確認コマンド:

aws greengrassv2 list-client-devices-associated-with-core-device \
    --core-device-thing-name RosProvisioningGreengrassCore

4つのデバイスが返ってくれば成功:

{
  "associatedClientDevices": [
    {"thingName": "device-thing-1-firmware"},
    {"thingName": "device-thing-1-agent"},
    {"thingName": "device-thing-2-firmware"},
    {"thingName": "device-thing-2-agent"}
  ]
}

何が起きるか:

Greengrass Core(RosProvisioningGreengrassCore)に対して
4つのクライアントデバイスを関連付ける。

この関連付けがないとデバイスが Greengrass Discovery API を叩いても
404 が返り続ける。関連付け後、デバイスは Greengrass Core の
エンドポイント(IPアドレス・ポート)を教えてもらえるようになり、
MQTT接続が確立する。


Step 8: ROS2ファームウェアイメージのビルドとプッシュ

# IoTエンドポイントを取得
aws iot describe-endpoint --endpoint-type iot:Data-ATS --query 'endpointAddress' --output text

containers/ros-image-v1/config/iot_config_template.json
iot_config.json としてコピーし、REGIONENDPOINT を実際の値に書き換える:

{
    "endpoint": "xxxxxxxxxxxx-ats.iot.us-east-1.amazonaws.com",
    "rootCAPath": "/certs/AmazonRootCA1.pem",
    "certificatePath": "/certs/device.pem.crt",
    "privateKeyPath": "/certs/private.pem.key",
    "port": 8333,
    "region": "us-east-1",
    "retryWaitTime": 5,
    "retryAttempts": 10
}
cd containers/ros-image-v1
bash build.sh

何が起きるか:

build.sh がバージョン1〜3の ROS2 ファームウェアイメージをビルドし、
ローカルの registry コンテナにプッシュする:

docker build -t firmware:1 --build-arg VERSION=1 ...
docker tag firmware:1 localhost:5555/firmware:1
docker push localhost:5555/firmware:1
# → バージョン2、3も同様

各バージョンのイメージは ROS2 ノードを含み、起動すると
IoT Core に MQTT で接続してハートビートメッセージを定期送信する。
VERSION の値が変わるだけで動作は同じ(バージョン確認用のデモ用途)。


Step 9: CDKでAWSインフラをデプロイ

cd deploy
npm install
npx aws-cdk bootstrap
npx aws-cdk deploy --require-approval never

何が起きるか:

CloudFormation スタック (DeployStack) で以下を作成する:

リソース 役割
ECR Repository Lambda関数自体のコンテナイメージ格納用(Lambda本体のデプロイに使用)
Lambda Function (iotJobUpdateFunction) ジョブ完了時にデバイスのfirmwareVersionをIoT ShadowとThing属性に記録する
IoT TopicRule ジョブ完了/失敗イベントでLambdaをトリガー
IAM Role / Policy Lambda の実行権限

bootstrap は CDK が使う S3バケット・ECRリポジトリ・IAMロールを
アカウントに1回だけ準備するコマンド。


Step 10: OTAジョブを発行してデモ実行

cd jobs
python3 deploy_job.py 1  # v1をデプロイ
python3 deploy_job.py 2  # v2にアップデート

何が起きるか:

deploy_job.py は以下の2つをAWS APIで実行する:

  1. aws iot create_jobdevice-thing-1-agentDeploy-ROS-Firmware ジョブを発行
  2. aws iot update_thing → Thingの firmwareVersion 属性を更新

デバイス側のログ(実際に確認した動作):

# v1完了後、v2ジョブを受信
device1-1 | Received Next Job Execution Changed event. job_id:f34a... version: '2'
device1-1 | Starting local work on job...

# v1コンテナを停止して v2 を起動
device1-1 | Stopping device-thing-1-firmware-1     ← v1コンテナ停止
device1-1 | Starting device-thing-1-firmware-2...  ← v2コンテナ起動
device1-1 | Pulling image registry:5000/firmware:2 ← ローカルregistryからpull

# ジョブ完了を報告
device1-1 | job_handler_callback_start_firmware_update complete with status True
device1-1 | Publishing request to update job status to SUCCEEDED
device1-1 | Received Next Job Execution Changed event: None. Waiting for further jobs...

registryコンテナのログ(イメージ配信の様子):

registry-1 | GET /v2/firmware/blobs/sha256:... → 200  ← デバイスがダウンロード中

デバイスのファームウェアバージョンを検索する

IoT Fleet Indexing を有効にすると firmwareVersion 属性で検索できる:

aws iot update-indexing-configuration \
  --thing-indexing-configuration '{
    "thingIndexingMode": "REGISTRY_AND_SHADOW",
    "thingConnectivityIndexingMode": "STATUS",
    "customFields": [{"name": "attributes.firmwareVersion", "type": "String"}]
  }'

# v1が動いているデバイスを検索
aws iot search-index --query-string 'attributes.firmwareVersion:1'

クリーンアップ

# CDKインフラ削除
cd /path/to/ros2-ota-firmware-updates/deploy
npx aws-cdk destroy

# Dockerコンテナ停止
cd /path/to/ros2-ota-firmware-updates/containers
docker compose --project-name ros-ota down

# Things・証明書・Greengrassリソース削除
cd /path/to/ros2-ota-firmware-updates/cleanup
bash undo-setup-device-thing.sh
bash undo-greengrass-entrypoint.sh
bash undo-start.sh
bash undo-associate-client-devices.sh
bash undo-deployment.sh
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?