How-to Guides
Recipes for specific tasks. For concepts see explanation.md; for exhaustive options see reference/.
Build and sign a model bundle
Section titled “Build and sign a model bundle”A model bundle is the unit the component installs: one tar archive, optionally gzip-compressed,
that carries manifest.json, the detached signature manifest.sig, and the model files. The
archive’s SHA-256 is the bundle digest, and that digest is what configuration pins. Build a
bundle with tools/make_bundle.py.
-
Create a signing keypair. Do this once per publisher, and keep the private key off the devices that consume bundles.
Terminal window python tools/make_bundle.py --gen-key keys/pharma-model-publisher-1.pemThe command writes three files: the private key PEM, the public key PEM (
...pem.pub.pem), and the raw 32-byte public key (...pem.pub). To protect the private key with a passphrase, add--key-password. -
Lay out the bundle directory. Put the ONNX graph at
model.onnx, and add the files the task family and the operators need beside it:line-clearance-cam-01/manifest.jsonmodel.onnxlabels.jsontransforms.jsonresult.schema.jsonmodel-card.jsonwarmup/input-01.binwarmup/expected-01.json -
Write
manifest.json. Declare the model identity, the runtime and provider requirements, the tensor shapes, the task family and its parameters, the preprocessing, the decision rules, the result bounds, the warmup samples and tolerances, the engine compatibility keys, and the provenance. Leavefilesout: the tool computes every per-file digest.{"schemaVersion": 1,"modelId": "line-clearance-cam-01","version": "2026.08.20","minOnnxRuntime": "1.18.0","providersPermitted": ["CUDAExecutionProvider"],"providerPolicy": "required","inputs": [{ "name": "images", "dtype": "float32", "shape": ["N", 3, 224, 224] }],"outputs": [{ "name": "logits", "dtype": "float32", "shape": ["N", 2] }],"dynamicBatch": true,"family": "classification","familyParams": { "topK": 2, "activation": "softmax" },"preprocess": { "resize": [224, 224], "layout": "NCHW" },"decisionRules": { "pass": "$.classes[0].label == 'clear'", "threshold": 0.8 },"maxResultItems": 10,"estimatedDeviceMiB": 512,"warmup": [{ "input": "warmup/input-01.bin", "expected": "warmup/expected-01.json" }],"tolerances": { "score": 0.001 },"compatibilityKeys": { "gpuClass": "sm_86" },"provenance": { "publisher": "pharma-mlops" },"transformVersion": "2026.08.20-1"} -
Pack and sign the bundle.
--key-idis the name the device configuration maps to the public key, and it is recorded in the manifest.Terminal window python tools/make_bundle.py line-clearance-cam-01 \--out dist/line-clearance-cam-01-2026.08.20.tar.gz \--key keys/pharma-model-publisher-1.pem \--key-id pharma-model-publisher-1 \--schema schemas/model-bundle-manifest.schema.jsonThe tool merges your manifest with the computed
filesdigests, validates it against the schema, signs the exact manifest bytes, packs the archive, and prints the bundle digest:sha256:9f2c1e...c41dBuilds are reproducible. The same directory and key always produce the same digest, so you can rebuild a bundle to confirm the digest you shipped.
-
Publish the archive to a local path, an https origin, or an S3 bucket, and pin it in
component.global.models[]with the digest the tool printed:{"id": "line-clearance-cam-01","version": "2026.08.20","digest": "sha256:9f2c1e...c41d","uri": "s3://approved-models/line-clearance-cam-01/2026.08.20.tar.gz"} -
Trust the signing key on the device. Add the public key under
component.global.signing.trustedKeys[]against the samekeyId, and setsigning.requiredtotruewhere a verified signature is mandatory. The raw public key (...pem.pub) is the value to store; a$secretreference keeps it out of the deployment document."signing": {"required": true,"trustedKeys": [{"keyId": "pharma-model-publisher-1","publicKey": { "$secret": "model-signing/publisher-1" }}]}
The component verifies the tarball digest, then the signature, then extracts under path, count, size, and ratio limits, then verifies the per-file digests and the manifest schema, and only then promotes the bundle into its content-addressed cache. Any bundle that fails a check is refused and the route stays on its last-known-good model.
Serve bundles from S3
Section titled “Serve bundles from S3”s3:// sources need the s3 extra, which is not installed by default:
pip install ".[s3]"Without it, an s3:// URI fails with S3_UNAVAILABLE and the rest of the component keeps
running. Give the model entry a credentials $secret reference to use explicit keys, or leave
it out to use the ambient credentials of the host or the Greengrass token exchange service.
Restrict where bundles come from
Section titled “Restrict where bundles come from”An https:// source is downloaded with certificate and hostname verification, and a URL that
carries credentials is refused. Where the deployment configures allow-listed prefixes, both the
URL and every redirect target must start with one of them, so a redirect cannot walk a download
off the approved origin.
Deploy to a platform
Section titled “Deploy to a platform”HOST: python3 main.py --platform HOST --transport MQTT ./messaging.json -c FILE ./config.json -t my-thing
(or docker compose up --build).
Greengrass: gdk component build && gdk component publish. The recipe’s default configuration
ships two working routes and the CPU development profile, so point the routes at the spool the
device’s camera writes into, name the device’s GPU ordinals in gpu.devices, drop
runtime.allowCpuOnly, and point models[].uri at the object your deployment publishes. Set a real
S3 bucket in gdk-config.json first — a repository with no bucket configured carries a visible
sentinel that component validate treats as an error.
Kubernetes: build ./Dockerfile, push or kind load it, set image: in k8s/deployment.yaml,
then kubectl apply -f k8s/. With --platform auto the library detects KUBERNETES, reads config
from the mounted ConfigMap (hot-reloaded on kubectl apply), and resolves identity from the Downward
API — no CLI args needed.
Add a route
Section titled “Add a route”A route is one entry of component.instances[], and routes are independent, so adding one is a
configuration change rather than a code change. It needs four things: an id that is a valid UNS
token, a source, a modelRef naming an entry of global.models[] by id, version, and digest, and
a completion policy.
- Publish the model the route runs and add it to
global.models[], or reuse a model another route already names — routes bound to the same digest share one resident session. - Add the instance. Sample configurations has a camera route and a trigger route you can copy.
- Deploy. The candidate validator runs before the configuration becomes current, so a route that would claim another route’s spool, archive into a directory it reads, or name a model nobody published is refused and the running configuration is left alone.
The component reconciles routes without dropping admitted work: a route that appeared starts, a route that disappeared stops, and a route whose source or model changed is rebuilt, while jobs already in the ledger keep their pinned model generation and finish under it.
Repair a job a crash or an outage left behind
Section titled “Repair a job a crash or an outage left behind”Every durable state a job can be stuck in has an operator verb, and each one reports what the ledger and the filesystem actually show afterwards rather than assuming it worked.
A result that could not be published. After publish.maxAttempts the job is
PUBLISH_EXHAUSTED, the component emits publish-exhausted, and the input stays where it is —
archiving it would move the evidence out from under a publication that is still expected to happen.
When the broker is back:
ec-uns-cmd --device my-thing --component image-processor retry-publicationec-uns-cmd --device my-thing --component image-processor retry-publication --body '{"inferenceId": "blvuf6ru…"}'A completion that failed. A move that collided with an existing object, or a directory that was
not writable, leaves the job CLEANUP_FAILED with the intent still on record. The supervision pass
retries it every few seconds; to retry now, or to see what is still failing:
ec-uns-cmd --device my-thing --component image-processor retry-cleanupAnything else that looks stuck. reconcile re-decides every open cleanup intent against
observed filesystem state — source present and target absent retries the move, source absent and
target present with a matching digest completes, and a target holding different bytes is a
collision rather than an overwrite:
ec-uns-cmd --device my-thing --component image-processor reconcileec-uns-cmd --device my-thing --component image-processor get-queue --body '{"states": ["CLEANUP_FAILED", "PUBLISH_EXHAUSTED"]}'A model that will not stage. get-models reports the error against the generation. Fix the
source or the digest and re-evaluate the catalog without a redeployment:
ec-uns-cmd --device my-thing --component image-processor get-modelsec-uns-cmd --device my-thing --component image-processor reload-model-catalogA route you need to stop feeding. pause stops it claiming new work and lets in-flight jobs
finish; set-route-activation-override persists the decision across restarts and is reported beside
the configured value, so a deployment stays the source of truth for what the route is.
ec-uns-cmd --device my-thing --component image-processor --instance clearance-cam-01 pauseec-uns-cmd --device my-thing --component image-processor --instance clearance-cam-01 set-route-activation-override --body '{"enabled": false}'Wire up CI
Section titled “Wire up CI”.github/workflows/ci.yml calls the org’s reusable component-CI workflow plus a coverage job
enforcing the 90% line-coverage gate; .github/workflows/deploy-docs.yml refreshes the docs site on
doc-only pushes once this component is registered in registry/components.json. Both are inert until
pushed to GitHub with the org secrets configured.
Run the real-model suite
Section titled “Run the real-model suite”The real-model suite runs seven real ONNX exports — two ImageNet classifiers, three COCO
detectors, a Pascal VOC segmentation model, and a PatchCore anomaly model — over real images and
compares the answers to the JSON goldens in tests/goldens/. It runs nightly and on demand, not on
every pull request, so you turn it on with EC_LIVE_MODELS=1.
Models and images are never committed. tests/assets.json pins each one by URL, SHA-256, and byte
count, and tools/fetch_test_assets.py verifies them into tests/.cache/, which is gitignored.
Fetch the corpus
Section titled “Fetch the corpus”-
See what the corpus holds and what is already cached:
Terminal window python tools/fetch_test_assets.py --list -
Fetch it. The default selection is about 440 MB and skips the assets marked optional:
Terminal window python tools/fetch_test_assets.pyThe command is idempotent: an asset already in the cache is re-hashed rather than downloaded again. A file whose digest does not match the manifest is reported and the command exits non-zero.
-
To fetch one asset, or to add the optional ones, name them:
Terminal window python tools/fetch_test_assets.py --only model-yolox-nanopython tools/fetch_test_assets.py --include-optional
Run the suite
Section titled “Run the suite”-
Run it with the switch set:
Terminal window EC_LIVE_MODELS=1 python -m pytest tests/live_models -o addopts="" -qEach model is packed into a signed bundle, staged through the content-addressed cache, and run on
CPUExecutionProvider, so the suite exercises the same install path a deployment uses. -
A model whose assets are missing skips with the command that fetches them, so a partial corpus runs what it can.
Build the anomaly model
Section titled “Build the anomaly model”The PatchCore model is built rather than downloaded, because PatchCore needs no training epochs and
rebuilding it from the pinned dataset is cheaper than hosting a binary. It needs anomalib and
torch, which are not dependencies of this component, so install them into a scratch environment:
-
Fetch VisA, which is optional and about 1.8 GB:
Terminal window python tools/fetch_test_assets.py --only dataset-visa -
Install the build dependencies and build the model:
Terminal window python -m venv .venv-anomalib.venv-anomalib/bin/pip install anomalib onnx jsonpath-ng jsonschema cryptography.venv-anomalib/bin/python tools/build_anomaly_model.pyThe build writes
model.onnxand abuild.jsonrecord intotests/.cache/model-patchcore-visa-capsules/. The record carries the threshold the bundle declares and the separation the memory bank achieves on held-out images. The build is deterministic: the same VisA archive and the same arguments produce the same graph. -
Run the suite again. The anomaly tests skip with these instructions whenever the model is absent.
Update the goldens
Section titled “Update the goldens”Regenerate a golden after a deliberate change to preprocessing, postprocessing, or the decision rules — never to make a failing comparison pass.
-
Regenerate every golden from a real run:
Terminal window python tools/update_goldens.py -
Regenerate one model’s golden:
Terminal window python tools/update_goldens.py --only mobilenetv2-12 -
Read the diff before committing it. A changed top-1 label, a moved box, or a flipped decision outcome is a behavior change, and the diff is where you see it.
The same thing happens inside pytest with --update-goldens, which is what the tool passes through.