Deploy from the cloud
greengrass-cli local installUpload artifact to S3
Register component version (1.1.0)
Thing-group deployment replaces the root set
- The button loop keeps working — only the install source moves from local disk to S3.
- A Thing-group deploy replaces the whole root set, so the stack ships complete (client-device components, Cli, LogManager, GgEdgeLoop).
- LogManager uploads component logs to CloudWatch every 60 s.
1. Session variables
Section titled “1. Session variables”Every cloud page starts with the same block. Bucket names carry the account ID so they are globally unique.
set -a && source config/walkthrough.env && set +aexport GG_EDGE_ALLOW_AWS=1AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)ARTIFACT_BUCKET="${PROJECT_NAME}-${ENVIRONMENT}-gg-artifacts-${AWS_ACCOUNT_ID}"CORE_THING_GROUP_ARN=$(aws iot describe-thing-group \ --thing-group-name "$CORE_THING_GROUP" --query thingGroupArn --output text)echo "$ARTIFACT_BUCKET"gg-edge-wt-dev-gg-artifacts-1234567890122. Artifact bucket and core read access
Section titled “2. Artifact bucket and core read access”Create the bucket
Section titled “Create the bucket”aws s3 mb "s3://${ARTIFACT_BUCKET}" --region "$AWS_REGION"make_bucket: gg-edge-wt-dev-gg-artifacts-123456789012If the bucket already exists in this account, s3 mb errors with
BucketAlreadyOwnedByYou — that is fine; continue.
Let the core download artifacts
Section titled “Let the core download artifacts”The core downloads artifacts with its
token exchange
IoT role alias + IAM role — lets Nucleus exchange its device certificate for temporary AWS credentials on the core. role. Scope
s3:GetObject to this bucket:
sed "s|ARTIFACT_BUCKET|${ARTIFACT_BUCKET}|g" \ artifacts/policies/token-exchange-artifacts-s3.json > /tmp/tes-artifacts-s3.jsonaws iam put-role-policy \ --role-name "${PROJECT_NAME}-${ENVIRONMENT}-token-exchange" \ --policy-name "${PROJECT_NAME}-${ENVIRONMENT}-gg-artifacts-s3" \ --policy-document file:///tmp/tes-artifacts-s3.json(no output)Register the component
Section titled “Register the component”3. Register GgEdgeLoop 1.1.0 from S3
Section titled “3. Register GgEdgeLoop 1.1.0 from S3”1.1.0 keeps the button loop and adds the cloud path. It subscribes to
gg-edge/cloud/command/+, drives that zone’s RGB blue, and acknowledges on
gg-edge/cloud/ack/<thing>
(loop.py).
Upload the artifact
Section titled “Upload the artifact”aws s3 cp artifacts/components/artifacts/com.example.GgEdgeLoop/1.1.0/loop.py \ "s3://${ARTIFACT_BUCKET}/artifacts/com.example.GgEdgeLoop/1.1.0/loop.py"upload: artifacts/components/artifacts/com.example.GgEdgeLoop/1.1.0/loop.py to s3://gg-edge-wt-dev-gg-artifacts-123456789012/artifacts/com.example.GgEdgeLoop/1.1.0/loop.pyCreate the component version
Section titled “Create the component version”sed "s|ARTIFACT_BUCKET|${ARTIFACT_BUCKET}|g" \ artifacts/components/recipes/com.example.GgEdgeLoop-1.1.0.yaml \ > /tmp/GgEdgeLoop-1.1.0.yamlaws greengrassv2 create-component-version \ --inline-recipe fileb:///tmp/GgEdgeLoop-1.1.0.yaml{ "arn": "arn:aws:greengrass:ap-southeast-2:123456789012:components:com.example.GgEdgeLoop:versions:1.1.0", "componentName": "com.example.GgEdgeLoop", "componentVersion": "1.1.0", "creationTimestamp": "2026-10-10T10:32:41.351000+13:00", "status": { "componentState": "REQUESTED", "message": "NONE", "errors": {}, "vendorGuidance": "ACTIVE", "vendorGuidanceMessage": "NONE" }}If you already registered 1.1.0, this fails with a conflict — skip to the
describe check below.
Wait until it is deployable
Section titled “Wait until it is deployable”aws greengrassv2 describe-component \ --arn "arn:aws:greengrass:${AWS_REGION}:${AWS_ACCOUNT_ID}:components:com.example.GgEdgeLoop:versions:1.1.0" \ --query status.componentState --output textDEPLOYABLEOn this lab’s account the state flipped from REQUESTED to DEPLOYABLE
within a few seconds.
Hand over to the cloud
Section titled “Hand over to the cloud”4. Remove the local deployment
Section titled “4. Remove the local deployment”The local deployment from Loop component still owns
com.example.GgEdgeLoop. Remove it on the NUC, or the cloud version
conflicts with it:
sudo /greengrass/v2/bin/greengrass-cli deployment create \ --remove "com.example.GgEdgeLoop"Local deployment submitted! Deployment Id: 11ec62e3-27bf-43cc-8790-1f99baa80a4bConfirm it is gone before the cloud deployment lands:
sudo /greengrass/v2/bin/greengrass-cli component list | grep GgEdgeLoop || echo "GgEdgeLoop gone"GgEdgeLoop gone5. Deploy the greenhouse stack
Section titled “5. Deploy the greenhouse stack”greenhouse-cloud.json
is the complete root set for the Thing group. It includes the client-device
stack, Cli, LogManager, and GgEdgeLoop 1.1.0. A Thing-group deployment
replaces the previous root set, so nothing may be missing.
The Bridge configurationUpdate resets /mqttTopicMapping then merges the
routes below. Without that reset, earlier mappings (for example
ActuatorLocalToIotCore from the LAN deploy) linger next to the new ones.
| Bridge route | From → to | Consumer |
|---|---|---|
gg-edge/sensor | LocalMqtt → Pubsub, IotCore | GgEdgeLoop (button) |
gg-edge/telemetry/+ | LocalMqtt → Pubsub, IotCore | ZoneAnomaly; IoT rules (S3, DynamoDB, CloudWatch) |
gg-edge/actuator/+ | Pubsub → LocalMqtt, IotCore | ESP32-S3 RGB; DynamoDB rule |
gg-edge/cloud/command/+ | IotCore → Pubsub | GgEdgeLoop (cloud override) |
gg-edge/cloud/ack/+ | Pubsub → IotCore | DynamoDB rule |
gg-edge/inference/+ | Pubsub → IotCore | DynamoDB rule |
Render and create the deployment
Section titled “Render and create the deployment”CLI_VER=$(aws greengrassv2 list-components --scope PUBLIC \ --query "components[?componentName=='aws.greengrass.Cli'].latestVersion.componentVersion" \ --output text)LM_VER=$(aws greengrassv2 list-components --scope PUBLIC \ --query "components[?componentName=='aws.greengrass.LogManager'].latestVersion.componentVersion" \ --output text)echo "Cli=$CLI_VER LogManager=$LM_VER"sed -e "s|TARGET_THING_GROUP_ARN|${CORE_THING_GROUP_ARN}|g" \ -e "s|CLIENT_THING_PREFIX|${CLIENT_THING_PREFIX}|g" \ -e "s|CLI_COMPONENT_VERSION|${CLI_VER}|g" \ -e "s|LOGMANAGER_COMPONENT_VERSION|${LM_VER}|g" \ artifacts/deployments/greenhouse-cloud.json > /tmp/greenhouse-cloud.jsonaws greengrassv2 create-deployment --cli-input-json file:///tmp/greenhouse-cloud.jsonCli=2.18.3 LogManager=2.3.14{ "deploymentId": "50cb5eaf-71e5-438f-a1f1-60995ae5869b", "iotJobId": "7d4be355-9d66-45fa-a52c-c7892fce6197", "iotJobArn": "arn:aws:iot:ap-southeast-2:123456789012:job/7d4be355-9d66-45fa-a52c-c7892fce6197"}If LogManager comes back empty, paginate list-components or set
LM_VER=2.3.14.
Wait for SUCCEEDED
Section titled “Wait for SUCCEEDED”aws greengrassv2 list-effective-deployments \ --core-device-thing-name "$CORE_THING_NAME" \ --query "effectiveDeployments[?deploymentName=='gg-edge-greenhouse'].[deploymentName,coreDeviceExecutionStatus]" \ --output textgg-edge-greenhouse SUCCEEDEDExpect QUEUED → IN_PROGRESS → SUCCEEDED within about a minute on this NUC.
Verify and observe
Section titled “Verify and observe”6. Verify on the NUC
Section titled “6. Verify on the NUC”Component came from the cloud
Section titled “Component came from the cloud”sudo /greengrass/v2/bin/greengrass-cli component list | grep -A3 -E "GgEdgeLoop|LogManager"Component Name: com.example.GgEdgeLoop Version: 1.1.0 State: RUNNING Configuration: {"accessControl":{…}}Component Name: aws.greengrass.LogManager Version: 2.3.14 State: RUNNING Configuration: {"logsUploaderConfiguration":{…},"periodicUploadIntervalSec":"60"}Confirm the Bridge has the Act 2 routes (including telemetry) and no leftover
ActuatorLocalToIotCore:
sudo /greengrass/v2/bin/greengrass-cli component list | grep -A2 mqtt.BridgeComponent Name: aws.greengrass.clientdevices.mqtt.Bridge Version: 2.3.4 State: RUNNINGThe configuration should list TelemetryLocalToIotCore,
ActuatorPubsubToIotCore, and CommandIotCoreToPubsub.
Loop log
Section titled “Loop log”sudo tail -n 20 /greengrass/v2/logs/com.example.GgEdgeLoop.log2026-10-09T21:33:35.394Z [INFO] (Copier) com.example.GgEdgeLoop: stdout. INFO:GgEdgeLoop:subscribed to gg-edge/sensor. {scriptName=services.com.example.GgEdgeLoop.lifecycle.Run, serviceName=com.example.GgEdgeLoop, currentState=RUNNING}2026-10-09T21:33:35.396Z [INFO] (Copier) com.example.GgEdgeLoop: stdout. INFO:GgEdgeLoop:subscribed to gg-edge/cloud/command/+. {scriptName=services.com.example.GgEdgeLoop.lifecycle.Run, serviceName=com.example.GgEdgeLoop, currentState=RUNNING}2026-10-09T21:33:35.396Z [INFO] (Copier) com.example.GgEdgeLoop: stdout. INFO:GgEdgeLoop:publishing on gg-edge/actuator/<thing> and gg-edge/cloud/ack/<thing>. {scriptName=services.com.example.GgEdgeLoop.lifecycle.Run, serviceName=com.example.GgEdgeLoop, currentState=RUNNING}Press BOOT: the RGB still goes green. The button loop survived the handover; only the install source moved from local disk to S3.
7. Logs in CloudWatch
Section titled “7. Logs in CloudWatch”LogManager uploads every 60 s (periodicUploadIntervalSec in the deployment).
Log groups follow /aws/greengrass/<componentType>/<region>/<componentName>
(monitor logs).
After about a minute:
aws logs describe-log-groups --log-group-name-prefix "/aws/greengrass/" \ --query "logGroups[].logGroupName" --output text/aws/greengrass/GreengrassSystemComponent/ap-southeast-2/System /aws/greengrass/UserComponent/ap-southeast-2/com.example.GgEdgeLoopaws logs tail "/aws/greengrass/UserComponent/${AWS_REGION}/com.example.GgEdgeLoop" \ --since 15m2026-10-09T21:33:35.394000+00:00 /2026/10/09/thing/gg-edge-wt-dev-core 2026-10-09T21:33:35.394Z [INFO] (Copier) com.example.GgEdgeLoop: stdout. INFO:GgEdgeLoop:subscribed to gg-edge/sensor. {scriptName=services.com.example.GgEdgeLoop.lifecycle.Run, serviceName=com.example.GgEdgeLoop, currentState=RUNNING}2026-10-09T21:33:35.396000+00:00 /2026/10/09/thing/gg-edge-wt-dev-core 2026-10-09T21:33:35.396Z [INFO] (Copier) com.example.GgEdgeLoop: stdout. INFO:GgEdgeLoop:subscribed to gg-edge/cloud/command/+. {scriptName=services.com.example.GgEdgeLoop.lifecycle.Run, serviceName=com.example.GgEdgeLoop, currentState=RUNNING}2026-10-09T21:33:35.396000+00:00 /2026/10/09/thing/gg-edge-wt-dev-core 2026-10-09T21:33:35.396Z [INFO] (Copier) com.example.GgEdgeLoop: stdout. INFO:GgEdgeLoop:publishing on gg-edge/actuator/<thing> and gg-edge/cloud/ack/<thing>. {scriptName=services.com.example.GgEdgeLoop.lifecycle.Run, serviceName=com.example.GgEdgeLoop, currentState=RUNNING}Telemetry now reaches IoT Core
AWS IoT Core — cloud MQTT and device identity service. Client devices use it for discovery; the MQTT bridge forwards zone topics here, where IoT rules route them to other AWS services. every 10 s
per zone. In the console MQTT test client, subscribe to gg-edge/# (or
gg-edge/telemetry/+) — you should see chip-temperature messages. The next page
gives them a durable consumer.
Next: Archive telemetry.