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" }
}
}
desired と reported が一致していれば更新成功、ズレていれば失敗・未反映を検知できる。
まとめると:
| 書き込み先 | 用途 |
|---|---|
| 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 としてコピーし、REGION と ENDPOINT を実際の値に書き換える:
{
"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で実行する:
-
aws iot create_job→device-thing-1-agentにDeploy-ROS-Firmwareジョブを発行 -
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