I tried dynamic registration of IoT devices with AWS IoT Greengrass V2
This page has been translated by machine translation. View original
Introduction
Imagine a factory-like scenario where IoT devices are added one after another. It's not practical for a human to manually create a Thing in AWS IoT Core and issue a certificate for each device one by one. What we want is a mechanism where devices can register their own IDs at startup.
This time, I decided to try this with AWS IoT Greengrass V2. I put together a configuration—entirely using Podman containers—where a Greengrass core acts as a local broker and client devices obtain their own certificates and complete Thing registration on first boot. The repository is here: https://github.com/cm-obuchi-hugo-examples/iot-greengrass-minimal
Verification Environment
- macOS 26.6.2
- Podman 6.1.1 +
podman-compose1.6.0 (Compose functionality is not included inpodmanitself; installed separately via Homebrew) - Terraform (
required_version = ">= 1.10"ininfra/*/versions.tf, AWS provider~> 6.0, awscc provider~> 1.0) - AWS IoT Core / Greengrass V2 (region is
ap-northeast-1, the default value of theregionvariable ininfra/*/variables.tf) - Greengrass Nucleus 2.18.3 (the version pinned in
infra/20-fleet/deployment.tf, same as the Greengrass core container image tag2.18.3) - AWS IoT Device SDK for Python on the client side (
awsiotsdk==1.31.0, the version pip-installed inclient-image/Containerfile)
The repository created this time is iot-greengrass-minimal.
Architecture Diagram
Based on the Architecture in README.md, here is the configuration with only the key points extracted.

The abbreviations in the diagram refer to Nucleus (core runtime), client device auth (certificate verification and connection authorization), Moquette broker (local MQTT broker), MQTT Bridge (relay with the cloud), and IP detector (publishing the connection address). The detailed role of each is explained in the next section, "What is Greengrass."
There is one Greengrass core container, with N client containers hanging beneath it. Clients cannot reach cloud AWS IoT Core without going through the Greengrass core.
What is Greengrass
A Greengrass core is not a special entity in itself—it is a device (or process) that is first registered as a single AWS IoT Thing and then runs the complete Greengrass core software on top of it.
Inside the Greengrass core, a core runtime called Nucleus resides as a single JVM process, managing the installation, startup, and monitoring of other components. When configured to accept client devices, the following components run within the same JVM in addition to Nucleus.

A "client device" here refers to an end device that does not run Greengrass itself, but participates in the AWS IoT world by connecting to the local broker of a nearby Greengrass core. It does not communicate directly with cloud-side AWS IoT Core, but goes through the Greengrass core.
The flow until a client device communicates with a Greengrass core can be summarized as follows.

Minimum Requirements for Greengrass Core-as-Gateway
From here, rather than tracing the file structure of this repository, I will reorganize things by component from the perspective of "what is needed to run a Greengrass core as a gateway." For each component, I will list only the resources actually prepared and their purposes.
1. Account Prerequisite: Greengrass Service Role
This needs to be done before creating even a single Greengrass core.
- IAM Role (service role): The role that allows Greengrass to call AWS APIs on its own behalf for Thing registration and Greengrass core state management.
Since this is an account-level singleton that can only have one per account+region—not one per Greengrass core or per fleet—and because the AWS account I'm using is a sandbox shared with other projects, I first check for an existing association and only create a new one if none exists, to ensure I never overwrite an existing association.
2. Greengrass Core Identity: Thing and X.509 Certificate
- AWS IoT Thing: Registers the Greengrass core itself as a single entity.
- X.509 Certificate: Authentication credentials for the Greengrass core. Only the CSR (Certificate Signing Request) is sent to the cloud for issuance, and the private key is generated locally and never passed to the cloud or to Terraform state.
3. IoT Policy for Greengrass Core
The certificate requires IoT Policy permissions corresponding to what the Greengrass core actually does.
- Runtime Policy: Permissions for the Nucleus itself and MQTT Bridge to maintain persistent MQTT sessions with the cloud side.
{
"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: Permissions for verifying client device certificates (such as
VerifyClientDeviceIdentity).
{
"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: Permissions for relaying application topics (telemetry/commands).
{
"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"
}
]
}
Although split into three separate policies by role, all three are attached together to the same single certificate, so AWS evaluates them as the union of these three sets of permissions.
4. Token Exchange Role (AWS API Credentials)
The Greengrass core not only uses IoT Policies but also has occasions where it calls AWS APIs directly (for example, artifact resolution for AWS-published components = retrieval from S3). Rather than holding static AWS access keys, we set up a mechanism to exchange certificates for temporary AWS credentials.
- IAM Role + IAM Policy: The content of the temporary credentials actually issued by the token exchange. CloudWatch Logs-related permissions not used by this lab are omitted, limited to only
s3:GetBucketLocation. - IoT Role Alias: The bridge that allows the Greengrass core certificate to assume this IAM Role. The Greengrass core receives temporary credentials via this alias using
iot:AssumeRoleWithCertificate(this permission itself is included in the runtime policy).
Bringing together the Thing, certificate, three IoT Policies, and token exchange role, the overall picture starting from the Greengrass core certificate looks like this.

5. Components to Deploy
Once the Thing, certificate, Policy, and token exchange role are in place, we can finally set up the Greengrass deployment. The target is not an individual Greengrass core but a thing group that aggregates multiple Greengrass cores. Both clientdevices.Auth and clientdevices.mqtt.Bridge have per-deployment JSON configuration (separate from IoT Policy) in addition to the component itself.
| Component | Role | Configuration (if any) |
|---|---|---|
| Nucleus | Minimum required | - |
| clientdevices.Auth (client device auth) | Client device authentication and local authorization | Targets are determined by Thing name wildcard; restricted so that devices can only publish to their own Thing name's telemetry topic and only subscribe to the commands topic |
| clientdevices.mqtt.Moquette (Moquette) | Local MQTT broker itself | - |
| clientdevices.mqtt.Bridge (MQTT Bridge) | Relay with the cloud | Only 2 topics are relayed with wildcards: telemetry from local→cloud, commands from cloud→local |
| clientdevices.IPDetector (IP detector) | Provides connection address for discovery | - |
Because both configurations express Thing names and topics using wildcards, this configuration itself does not change even as the number of devices increases or decreases. If you don't accept client devices, nothing other than Nucleus is needed.

6. When Using Client Devices: How to Prepare the Client's Own Identity
Client device auth and MQTT Bridge assumed that clients already have certificates. There are two ways to prepare a client's own certificate and Thing: having a human create them one by one in advance, or using fleet provisioning by claim to let the device self-register only on first boot. Since I wanted to test a scenario where the number of devices dynamically increases and decreases, I used the latter.
- IoT Policy for claim certificate: Permissions granted to the shared certificate dedicated to self-registration. Only Publish/Subscribe to two topics—
CreateCertificateFromCsrandRegisterThing—is permitted; access to application-side topics is not allowed at all. - Fleet provisioning template: A blueprint that determines which Thing name, thing group, and Policy to use. The device only passes a serial number; the Thing name, thing group membership, and Policy to attach are all decided by this template (server side).
- IoT Policy for the client's actual certificate: Permissions granted to the certificate the client actually holds after self-registration is complete. The only permission is
greengrass:Discover; it does not even haveiot:Connectto the cloud MQTT endpoint. Local authorization is separately handled by theclientdevices.Authconfiguration in step 5.
7. Associating Greengrass Core and Client Devices
Even after a client device completes self-registration, the Greengrass core does not automatically know about that client's existence.
BatchAssociateClientDeviceWithCoreDeviceAPI call: The procedure to explicitly inform the Greengrass core of a client's existence. This API does not support specifying a thing group as a whole; it only accepts individually enumerated Thing names. This is the only gap in the entire mechanism where AWS has not provided a declarative solution.
This is bridged with a script that reads the current members of a thing group and passes them in batches to this API. Since no client names are hardcoded in the script itself, it associates whatever members exist at the time self-registration is complete.
The flow from self-registration with a claim certificate to association with the Greengrass core can be summarized as follows.

Running It in Practice
Using the repository (https://github.com/cm-obuchi-hugo-examples/iot-greengrass-minimal ), let's actually try it hands-on. Here is a quick look at how the elements explained so far are arranged in the repository.
iot-greengrass-minimal/
├── infra/
│ ├── 10-account/ Once per account: service role, provisioning role
│ └── 20-fleet/ Per fleet: core Thing/certificate, IoT Policy, deployment, thing group
├── local/
│ └── compose.yaml Podman Compose runtime definition
├── client-image/ Containerfile, provision.py, client.py, entrypoint.sh
├── scripts/ gen-core-csr.sh, gen-core-config.sh, associate.sh, verify.sh ...
├── certs/ Locally generated private keys/certificates (gitignore)
├── config/ Generated config.yaml for core (gitignore)
├── greengrass.env Static Nucleus environment variables for core container
└── client.env Generated environment variables for client containers (gitignore)
infra/10-account is applied once per account/region, and infra/20-fleet is a layer applied per fleet. certs/, config/, and client.env are all gitignored and their actual contents are not included in the repository itself.
The steps I actually performed follow the Runbook in README.md, as follows.
1) Apply Account-Wide Resources Once
terraform -chdir=infra/10-account apply. After checking for an existing service role association, creates a new one if none exists.
2) Generate Certificate-Related Resources Locally
scripts/gen-core-csr.sh / scripts/gen-claim-csr.sh. Private keys are created here for the first time and are never passed to Terraform.
3) Apply Fleet Layer Resources
terraform -chdir=infra/20-fleet apply. The core Thing/certificate, claim certificate, various IoT Policies, fleet provisioning template, token exchange role, two thing groups, and Greengrass deployment (5 components) are all created at once.
4) Generate Core Local Configuration
scripts/gen-core-config.sh. Renders config/config.yaml and client.env from Terraform outputs.
5) Clone the Core Image Build Source
Check out aws-greengrass-docker at the commit corresponding to the Nucleus version pinned in infra/20-fleet/deployment.tf.
AWS Resources Created Up to This Point
Up to this point in the procedure, the following resources have actually been created on AWS (no containers have been started yet at this point).
infra/10-account (per account, once only)
├── IAM Role: provisioning role (used by fleet provisioning by claim)
└── IAM Role: service role (created new only if no existing association)
infra/20-fleet (per fleet)
├── AWS IoT Core
│ ├── Thing: lab-gg-core-01 (core itself)
│ ├── X.509 Certificate ×1 (for core; 3 IoT Policies attached together)
│ ├── X.509 Certificate ×1 (for claim, shared)
│ ├── IoT Policy ×5 (core runtime / core client-auth / core bridge / claim / client discovery)
│ ├── Fleet provisioning template
│ └── Thing Group ×2 (lab-gg-cores / lab-gg-clients)
├── IAM (token exchange)
│ ├── IAM Role + IAM Policy
│ └── IoT Role Alias
└── AWS IoT Greengrass V2
└── Deployment (targeting thing group, 5 components: Nucleus, clientdevices.Auth,
clientdevices.mqtt.Moquette, clientdevices.mqtt.Bridge, clientdevices.IPDetector)
The client's own Thing and certificate do not yet exist at this point. They are created for the first time when the client starts up and self-registers via fleet provisioning by claim.




6) Start the Fleet
podman compose -f local/compose.yaml up -d --build --scale client=3
This started 1 Greengrass core and 3 clients.
7) Run the Association
scripts/associate.sh. Associates the current members of the lab-gg-clients thing group with the Greengrass core using BatchAssociateClientDeviceWithCoreDevice.
What associate.sh and verify.sh Actually Do
associate.sh is a one-way process that simply reads the registry and passes it directly to a single API.
Current member list of lab-gg-clients thing group
│ (list-things-in-thing-group)
▼
List of Thing names (batched up to 100 at a time)
│ (batch-associate-client-device-with-core-device)
▼
Associate with lab-gg-core-01
Since Thing names and counts are not hardcoded in the script itself, it associates the members as they exist at the time self-registration is complete.
Whether the premises built up so far (self-registration, identity persistence, local-only client MQTT, targeted delivery, dynamism, Greengrass core disposability) actually hold cannot be determined by reading the code alone. verify.sh reads the current state of podman ps and the AWS registry, and independently judges each of the 6 claims as PASS/FAIL.
Input: podman ps (running containers) + current state of AWS registry
│
├─ claim 1 self-registration Are all running containers members of the thing group?
├─ claim 2 identity persistence Does restarting one container not create a new Thing?
├─ claim 3 local-only MQTT Does the client certificate only have the discovery policy?
├─ claim 4 targeted delivery Does a command to one specific container not leak to others?
├─ claim 5 dynamism Information display only (the script itself does not depend on count)
└─ claim 6 core disposability(partial) Is the core HEALTHY? Are state-holding volumes still present?
│
▼
Outputs PASS/FAIL per claim; deliberately avoids `set -e` so all claims run even if one FAILs
Claim 3 has a second half deferred to manual verification (confirming that direct connection is refused), which always displays as SKIP. Claim 6 also covers only the non-destructive part—checking registry state and volume existence—while the test of actually destroying the core, recreating it, and comparing certificates is manual.

Subscribing to lab/greengrass/devices/+/telemetry in the AWS IoT Console MQTT test client, ticking messages like the following actually arrive every minute.

Conclusion
It was a small configuration of just 1 Greengrass core and 3 clients, but I was able to put together the full picture of the goal stated at the beginning: "devices prepare their own certificates and Things at startup, and cannot reach the cloud without going through the Greengrass core."
For scenarios like a factory where devices are added and replaced one after another, I felt that the Greengrass V2 client device feature is a mechanism made exactly for this purpose. Being able to actually build and experience a design where Thing names and IoT Policies can all be written with wildcard naming conventions—so there is no need to individually touch Terraform or IoT Policies even as the number of devices changes—was a valuable takeaway. For anyone considering similar dynamic device management, I think this is a configuration worth trying.
