AWS IoT Greengrass V2でIoTデバイスの動的登録を試してみた
はじめに
工場のようなシナリオを想像すると、IoTデバイスは後から次々に追加されていくもので、1台ずつ人間がAWS IoT Core側にThingを作って証明書を発行して…という運用は現実的ではありません。デバイス自身が起動時に自分のIDを登録できる仕組みが欲しいところです。
今回はそれをAWS IoT Greengrass V2で試してみることにしました。Greengrass coreをlocal brokerとして立て、クライアントデバイスが初回起動時に自分で証明書を取得してThing登録まで済ませる、という構成をPodmanコンテナだけで組んでみます。リポジトリはこちらです: https://github.com/cm-obuchi-hugo-examples/iot-greengrass-minimal
検証環境
- macOS 26.6.2
- Podman 6.1.1 +
podman-compose1.6.0(podman本体にCompose機能は含まれておらず、Homebrewで別途インストールしたもの) - Terraform(
infra/*/versions.tfのrequired_version = ">= 1.10"、AWS providerは~> 6.0、awscc providerは~> 1.0) - AWS IoT Core / Greengrass V2(リージョンは
ap-northeast-1。infra/*/variables.tfのregion変数のデフォルト値) - Greengrass Nucleus 2.18.3(
infra/20-fleet/deployment.tfで固定しているバージョンで、Greengrass coreコンテナのimage tagも同じ2.18.3) - クライアント側のAWS IoT Device SDK for Python(
awsiotsdk==1.31.0。client-image/Containerfileでpip installしているバージョン)
構成図
README.md のArchitectureをベースに、要点だけ抜き出すとこんな構成です。

図中の略語は、Nucleus(中核ランタイム)、client device auth(証明書検証・接続許可)、Moquette broker(local MQTT broker)、MQTT Bridge(クラウドとの中継)、IP detector(接続先addressの公開)のことです。それぞれの詳しい役割は次の「Greengrassとは」で説明します。
Greengrass coreコンテナが1つ、その配下にclientコンテナがN個ぶら下がる形です。clientはGreengrass coreを経由しない限りクラウドのAWS IoT Coreに触れません。
Greengrassとは
Greengrass coreは、それ自体が特別な存在ではなく、まず1つのAWS IoT Thingとして登録されたうえで、その上でGreengrassのcoreソフトウェア一式を動かしているデバイス(またはプロセス)のことです。
Greengrass core内部では、Nucleusという中核ランタイムが1つのJVMプロセスとして常駐していて、他のコンポーネントの導入・起動・監視を行っています。client deviceを受け入れる構成にする場合は、Nucleusに加えて次のようなコンポーネントが同じJVMの中で動きます。

ここでいう「client device」とは、Greengrassそのものは動かさず、近くのGreengrass coreのlocal brokerに接続することでAWS IoTの世界に参加する末端デバイスのことです。クラウド側のAWS IoT Coreとは直接通信せず、Greengrass coreを経由します。
client deviceがGreengrass coreとやり取りするまでの流れをまとめると、次のようになります。

Greengrass core-as-gatewayに最低限必要なもの
ここからは、このリポジトリのファイル構成をなぞるのではなく、「Greengrass coreをゲートウェイとして動かすには何が要るか」という観点でコンポーネントごとに並べ直してみます。各コンポーネントについて、実際に用意したリソースとその目的だけを挙げます。
1. アカウントの前提: Greengrassサービスロール
Greengrass coreを1台も作る前に済ませておく必要があるものです。
- IAM Role(service role): GreengrassがThing登録やGreengrass coreの状態管理のために、自分の代わりにAWS APIを呼ぶためのロール。
「アカウント+リージョンにつき1つしか持てない」account-level singletonなので、Greengrass core台数分でもフリートの数分でもありません。今回使っているAWSアカウントは他のプロジェクトとも共有しているサンドボックスなので、すでにassociationがある場合は絶対に上書きしないよう、既存のassociationを先に確認してから、無いときだけ新しく作るようにしています。
2. Greengrass coreの身元: ThingとX.509証明書
- AWS IoT Thing: Greengrass core自身を1つのモノとして登録する。
- X.509証明書: Greengrass coreの認証情報。CSR(Certificate Signing Request)だけをクラウドに渡して発行してもらう形にしており、秘密鍵はローカルで生成したままクラウド側にもTerraform stateにも渡していない。
3. Greengrass coreのIoT Policy
証明書には、Greengrass coreが実際にやることに対応するIoT Policyのpermissionが要ります。
- runtime用Policy: Nucleus本体・MQTT Bridgeがクラウド側と持続的なMQTTセッションを保つためのpermission。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "iot:Connect",
"Resource": "arn:aws:iot:ap-northeast-1:<account-id>:client/lab-gg-core-*"
},
{
"Effect": "Allow",
"Action": ["iot:Publish", "iot:Receive"],
"Resource": [
"arn:aws:iot:ap-northeast-1:<account-id>:topic/$aws/things/lab-gg-core-*/greengrassv2/health/json",
"arn:aws:iot:ap-northeast-1:<account-id>:topic/$aws/things/lab-gg-core-*/jobs/*",
"arn:aws:iot:ap-northeast-1:<account-id>:topic/$aws/things/lab-gg-core-*/shadow/*"
]
},
{
"Effect": "Allow",
"Action": "iot:Subscribe",
"Resource": [
"arn:aws:iot:ap-northeast-1:<account-id>:topicfilter/$aws/things/lab-gg-core-*/jobs/*",
"arn:aws:iot:ap-northeast-1:<account-id>:topicfilter/$aws/things/lab-gg-core-*/shadow/*"
]
},
{
"Effect": "Allow",
"Action": "iot:AssumeRoleWithCertificate",
"Resource": "arn:aws:iot:ap-northeast-1:<account-id>:rolealias/lab-gg-token-exchange-alias"
},
{
"Effect": "Allow",
"Action": [
"greengrass:GetComponentVersionArtifact",
"greengrass:ResolveComponentCandidates",
"greengrass:GetDeploymentConfiguration",
"greengrass:ListThingGroupsForCoreDevice"
],
"Resource": "*"
}
]
}
- client-auth用Policy: client deviceの証明書を検証する(
VerifyClientDeviceIdentityなど)ためのpermission。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["greengrass:PutCertificateAuthorities", "greengrass:VerifyClientDeviceIdentity"],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "greengrass:VerifyClientDeviceIoTCertificateAssociation",
"Resource": "arn:aws:iot:ap-northeast-1:<account-id>:thing/lab-gg-device-*"
},
{
"Effect": "Allow",
"Action": ["greengrass:GetConnectivityInfo", "greengrass:UpdateConnectivityInfo"],
"Resource": "arn:aws:iot:ap-northeast-1:<account-id>:thing/lab-gg-core-*"
},
{
"Effect": "Allow",
"Action": "iot:Publish",
"Resource": "arn:aws:iot:ap-northeast-1:<account-id>:topic/$aws/things/lab-gg-core-*-gci/shadow/get"
},
{
"Effect": "Allow",
"Action": ["iot:Subscribe", "iot:Receive"],
"Resource": [
"arn:aws:iot:ap-northeast-1:<account-id>:topicfilter/$aws/things/lab-gg-core-*-gci/shadow/update/delta",
"arn:aws:iot:ap-northeast-1:<account-id>:topicfilter/$aws/things/lab-gg-core-*-gci/shadow/get/accepted",
"arn:aws:iot:ap-northeast-1:<account-id>:topic/$aws/things/lab-gg-core-*-gci/shadow/update/delta",
"arn:aws:iot:ap-northeast-1:<account-id>:topic/$aws/things/lab-gg-core-*-gci/shadow/get/accepted"
]
}
]
}
- bridge用Policy: application topic(telemetry/commands)を中継するためのpermission。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "iot:Publish",
"Resource": "arn:aws:iot:ap-northeast-1:<account-id>:topic/lab/greengrass/devices/*/telemetry"
},
{
"Effect": "Allow",
"Action": "iot:Subscribe",
"Resource": "arn:aws:iot:ap-northeast-1:<account-id>:topicfilter/lab/greengrass/devices/+/commands"
},
{
"Effect": "Allow",
"Action": "iot:Receive",
"Resource": "arn:aws:iot:ap-northeast-1:<account-id>:topic/lab/greengrass/devices/*/commands"
}
]
}
役割ごとに3枚に分かれていますが、どれも同じ1枚の証明書にまとめてアタッチされるので、AWS側からはこの3つの許可の和集合として評価されます。
4. token exchangeロール(AWS API資格情報)
Greengrass coreはIoT Policyだけでなく、AWS APIを直接呼ぶ場面もあります(例えば、AWSが公開しているコンポーネントのartifact解決=S3からの取得)。ここで静的なAWSアクセスキーを持たせたくないので、証明書を一時的なAWS認証情報と交換する仕組みを用意します。
- IAM Role + IAM Policy: token exchangeで実際に発行される一時認証情報の中身。今回のラボが使わないCloudWatch Logs関連のpermissionは持たせず、
s3:GetBucketLocationだけに絞っている。 - IoT Role Alias: Greengrass core証明書がこのIAM Roleをassumeしていいという橋渡し。Greengrass coreは
iot:AssumeRoleWithCertificateでこのalias経由に一時認証情報を受け取る(このpermission自体はruntime用Policy側に入っている)。
Thing・証明書・3枚のIoT Policy・token exchangeロールをまとめると、Greengrass core証明書を起点にした全体像は次のようになります。

5. デプロイするコンポーネント
Thing・証明書・Policy・token exchangeロールが揃うと、ようやくGreengrassのデプロイを組めます。ターゲットは個別のGreengrass coreではなく、複数のGreengrass coreをまとめたthing groupです。clientdevices.Authとclientdevices.mqtt.Bridgeの2つは、コンポーネント本体とは別にデプロイ単位のJSON設定(IoT Policyとは別物)を持ちます。
| コンポーネント | 役割 | 設定(ある場合) |
|---|---|---|
| Nucleus | 最低限必須 | - |
| clientdevices.Auth(client device auth) | client deviceの認証・ローカル認可 | Thing名のwildcardで対象を決め、自分のThing名のtelemetry Topicにしかpublish、commands Topicにしかsubscribeできないよう制限 |
| clientdevices.mqtt.Moquette(Moquette) | local MQTT broker本体 | - |
| clientdevices.mqtt.Bridge(MQTT Bridge) | クラウドとの中継 | telemetryはローカル→クラウド、commandsはクラウド→ローカル、の2 Topicだけをwildcardで中継 |
| clientdevices.IPDetector(IP detector) | discoveryのために接続先addressを提供 | - |
どちらの設定もThing名やTopicをwildcardで表現しているため、デバイス台数が増減してもこの設定自体は変わりません。client deviceを受け付けないなら、Nucleus以外は不要です。

6. client deviceを使う場合: client自身の身元をどう用意するか
client device authとMQTT Bridgeは「すでにclientが証明書を持っている」前提の話でした。client自身の証明書とThingを用意する方法には、あらかじめ人間が1台ずつ作っておく方法と、fleet provisioning by claimでデバイス自身に初回起動時だけ自己登録させる方法があります。台数が動的に増減するシナリオを試したかったので、今回は後者を使っています。
- claim証明書用のIoT Policy: 自己登録専用の共有証明書に付与するpermission。できるのは
CreateCertificateFromCsrとRegisterThingの2つのTopicへのPublish/Subscribeだけで、アプリケーション側のTopicへのアクセスは一切許可しない。 - fleet provisioningテンプレート: どのThing名・thing group・Policyを使うかを決める設計図。デバイスが渡すのはシリアル番号だけで、Thing名・thing group所属・アタッチするPolicyはすべてこのテンプレート側(サーバー側)が決める。
- client本来の証明書用のIoT Policy: 自己登録が終わったあと、clientが実際に持つ証明書に付与するpermission。許可は
greengrass:Discoverのみで、クラウドのMQTTエンドポイントに対するiot:Connectすら持たない。ローカルでの認可は5.のclientdevices.Auth設定が別途担う。
7. Greengrass coreとclient deviceの関連付け
client deviceが自己登録を済ませても、Greengrass coreは自動的にはそのclientの存在を知りません。
BatchAssociateClientDeviceWithCoreDeviceAPI呼び出し: Greengrass core側に明示的にclientの存在を教える手続き。このAPIにはthing groupをまとめて指定する形が存在せず、Thing名を個別に列挙する形しかありません。ここまでの仕組みの中で、AWSが宣言的な解決策を用意していない唯一のギャップです。
thing groupの現在のメンバーを読み取り、それをバッチでこのAPIに渡すスクリプトで埋めています。クライアント名をスクリプト自体には一切書いていないので、台数が変わってもスクリプトは変わりません。
claim証明書での自己登録から、Greengrass coreとの関連付けまでの流れをまとめると次のようになります。

起動して動かしてみる
リポジトリ(https://github.com/cm-obuchi-hugo-examples/iot-greengrass-minimal )を使って、実際に手を動かしてみます。ここまで説明してきた要素が、リポジトリの中でどう配置されているかをざっと見ておきます。
iot-greengrass-minimal/
├── infra/
│ ├── 10-account/ アカウント全体で1回だけ: service role, provisioning role
│ └── 20-fleet/ フリートごと: core Thing/証明書, IoT Policy, deployment, thing group
├── local/
│ └── compose.yaml Podman Composeのランタイム定義
├── client-image/ Containerfile, provision.py, client.py, entrypoint.sh
├── scripts/ gen-core-csr.sh, gen-core-config.sh, associate.sh, verify.sh ...
├── certs/ ローカル生成の秘密鍵・証明書(gitignore)
├── config/ core向けに生成されたconfig.yaml(gitignore)
├── greengrass.env coreコンテナの静的Nucleus環境変数
└── client.env clientコンテナ向けに生成された環境変数(gitignore)
infra/10-account はアカウント・リージョンにつき1回だけ、infra/20-fleet はフリートごとに適用するレイヤーです。certs/・config/・client.env はどれもgitignoreされていて、リポジトリ自体にはこれらの実体を含みません。
実際に手を動かした手順は、README.md のRunbookに沿って次の通りです。
1) アカウント全体のリソースを1回だけ適用
terraform -chdir=infra/10-account apply。service roleの既存associationを確認したうえで、なければ新規作成します。
2) 証明書関連のリソースをローカルで生成
scripts/gen-core-csr.sh / scripts/gen-claim-csr.sh。秘密鍵はここで初めて作られ、Terraformには一切渡りません。
3) フリートレイヤーのリソースを適用
terraform -chdir=infra/20-fleet apply。coreのThing/証明書、claim証明書、各種IoT Policy、fleet provisioningテンプレート、token exchangeロール、2つのthing group、そしてGreengrassデプロイ(5コンポーネント)が一括で作られます。
4) coreのローカル設定を生成
scripts/gen-core-config.sh。Terraform outputsからconfig/config.yamlとclient.envをレンダリングします。
5) coreイメージのビルド元をclone
aws-greengrass-docker を、infra/20-fleet/deployment.tf で固定しているNucleusバージョンに対応するコミットでcheckoutします。
ここまでで作られたAWS上のリソース
ここまでの手順で、AWS上には実際に次のリソースが作られています(この時点ではまだコンテナは1つも起動していません)。
infra/10-account(アカウント単位、1回だけ)
├── IAM Role: provisioning role(fleet provisioning by claimが使う)
└── IAM Role: service role(既存のassociationが無い場合のみ新規作成)
infra/20-fleet(フリートごと)
├── AWS IoT Core
│ ├── Thing: lab-gg-core-01(core本体)
│ ├── X.509証明書 ×1(core用。IoT Policy 3枚をまとめてアタッチ)
│ ├── X.509証明書 ×1(claim用、共有)
│ ├── IoT Policy ×5(core runtime / core client-auth / core bridge / claim / client discovery)
│ ├── Fleet provisioningテンプレート
│ └── Thing Group ×2(lab-gg-cores / lab-gg-clients)
├── IAM(token exchange)
│ ├── IAM Role + IAM Policy
│ └── IoT Role Alias
└── AWS IoT Greengrass V2
└── Deployment(thing group宛て、5コンポーネント: Nucleus, clientdevices.Auth,
clientdevices.mqtt.Moquette, clientdevices.mqtt.Bridge, clientdevices.IPDetector)
client自身のThing・証明書はこの時点ではまだ存在しません。client起動時にfleet provisioning by claimで自己登録して初めて作られます。




6) フリートを起動
podman compose -f local/compose.yaml up -d --build --scale client=3
でGreengrass core 1台とclient 3台を起動しました。
7) 関連付けを実行
scripts/associate.sh。lab-gg-clients thing groupの現在のメンバーをBatchAssociateClientDeviceWithCoreDeviceでGreengrass coreに関連付けます。
associate.sh と verify.sh が実際にやっていること
associate.sh は、レジストリを読んでそのまま1つのAPIに渡すだけの一方向の処理です。
lab-gg-clients thing groupの現在のメンバー一覧
│ (list-things-in-thing-group)
▼
Thing名のリスト(最大100件ずつバッチ化)
│ (batch-associate-client-device-with-core-device)
▼
lab-gg-core-01 に関連付け
Thing名や台数をスクリプト自体にハードコードしていないので、自己登録が終わった時点のメンバーをそのまま関連付ける形になっています。
ここまで積み上げてきた前提(self-registration、identity persistence、local-only client MQTT、targeted delivery、dynamism、Greengrass core disposability)が実際に成り立っているかは、コードを読むだけでは分かりません。verify.sh は podman ps とAWSレジストリの現在の状態を読み取り、6つの主張を独立にPASS/FAIL判定します。
入力: podman ps(実行中コンテナ)+ AWSレジストリの現在の状態
│
├─ claim 1 self-registration 起動中の全containerがthing groupのメンバーか
├─ claim 2 identity persistence 1台をrestartしても新規Thingが増えないか
├─ claim 3 local-only MQTT client証明書がdiscovery policyしか持たないか
├─ claim 4 targeted delivery 特定1台へのcommandが他containerに漏れないか
├─ claim 5 dynamism 情報表示のみ(スクリプト自体は台数に依存しない)
└─ claim 6 core disposability(一部) coreがHEALTHYか、状態を持つvolumeが残っているか
│
▼
claimごとにPASS/FAILを出力し、`set -e` をあえて使わないので1つFAILしても残り全部を実行
claim 3には手動確認に回している後半(direct connection refusedの確認)があり、常にSKIPと表示されます。claim 6も、レジストリの状態とvolumeの存在確認だけの非破壊的な部分のみで、実際にcoreを壊して再作成し証明書を比較するテストは手動です。

AWS IoT ConsoleのMQTT test clientで lab/greengrass/devices/+/telemetry をsubscribeすると、実際に次のようなtickingメッセージが1分おきに届きます。

おわりに
Greengrass core 1台、client 3台という小さな構成でしたが、「デバイス自身が起動時に自分の証明書とThingを用意し、Greengrass coreを経由しないとクラウドに触れない」という、はじめに書いた目的そのものは一通り組み上げられました。
工場のように、デバイスが後から次々に追加・入れ替わるシナリオを扱うなら、Greengrass V2のclient device機能はまさにこの用途のためにある仕組みだと感じました。Thing名やIoT Policyをすべてwildcardの命名規則で書けるおかげで、台数が変わってもTerraformやIoT Policyを個別に触る必要がない、という設計を実際に組んで体験できたのは収穫でした。同じような動的なデバイス管理を検討している人には、試してみる価値のある構成だと思います。







