I tried dynamic registration of IoT devices with AWS IoT Greengrass V2

I tried dynamic registration of IoT devices with AWS IoT Greengrass V2

I implemented automatic device registration and local gateway operation using AWS IoT Greengrass V2. I will introduce the overall picture of combining fleet provisioning by claim and client device authentication using only Podman containers in a scenario where the number of devices dynamically increases and decreases, such as in a factory.
2026.09.17

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-compose 1.6.0 (Compose functionality is not included in podman itself; installed separately via Homebrew)
  • Terraform (required_version = ">= 1.10" in infra/*/versions.tf, AWS provider ~> 6.0, awscc provider ~> 1.0)
  • AWS IoT Core / Greengrass V2 (region is ap-northeast-1, the default value of the region variable in infra/*/variables.tf)
  • Greengrass Nucleus 2.18.3 (the version pinned in infra/20-fleet/deployment.tf, same as the Greengrass core container image tag 2.18.3)
  • AWS IoT Device SDK for Python on the client side (awsiotsdk==1.31.0, the version pip-installed in client-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.

architecture-overview.drawio

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.

greengrass-core-components.drawio

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.

client-device-flow.drawio

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.

core-identity-policies.drawio

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.

deployment-components.drawio

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—CreateCertificateFromCsr and RegisterThing—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 have iot:Connect to the cloud MQTT endpoint. Local authorization is separately handled by the clientdevices.Auth configuration 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.

  • BatchAssociateClientDeviceWithCoreDevice API 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.

client-provisioning.drawio

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.

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.

hugo-ggv2_01_v2

hugo-ggv2_02_v2

hugo-ggv2_03

hugo-ggv2_04

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.

hugo-ggv2_05

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

hugo-ggv2_06

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.

Share this article

AWSのお困り事はクラスメソッドへ