Skip to content

Configuration

Every GGCommons component is driven by a single JSON config document. The configuration subsystem loads that document from one of several pluggable sources, validates it against the canonical cross-language JSON schema, exposes its sections through typed accessors, substitutes template variables into paths and topics, and hot-reloads when the underlying source changes.

This guide covers how to choose a source, how the document is structured, how to read it from your component code in all four languages, and how to react to live changes. For the exhaustive field list see the configuration schema reference; for the full accessor surface see the config API reference.

There are six config sources, identical across all four languages. You pick one with the -c/--config <SOURCE> [args...] CLI flag. When -c is omitted, the source is chosen by the resolved platform profile: GREENGRASS defaults to GG_CONFIG, HOST to FILE, and KUBERNETES to CONFIGMAP.

Source Select with When to use
FILE -c FILE [path] (default config.json) Local development, HOST/Docker, bare hosts. Watches the file and hot-reloads on change. Default on HOST.
ENV -c ENV [var] (default CONFIG) Pass the entire config JSON through an environment variable — handy in containers and CI.
CONFIGMAP -c CONFIGMAP [mountDir] [key] (defaults /etc/ggcommons, config.json) Kubernetes. Reads a mounted ConfigMap directory and hot-reloads via the kubelet ..data swap. Default on KUBERNETES.
GG_CONFIG -c GG_CONFIG [component] [key] (default key ComponentConfig) On-device: read the config delivered by the Greengrass deployment (Nucleus). Default on GREENGRASS. Loads over IPC, so it needs a messaging client.
SHADOW -c SHADOW [name] (default: the component name sanitized to [A-Za-z0-9:_-]) Read config from an AWS IoT device shadow. Loads over IPC.
CONFIG_COMPONENT -c CONFIG_COMPONENT Read config served by a shared config component over IPC.

GG_CONFIG, SHADOW, and CONFIG_COMPONENT load the document over Greengrass IPC, so a messaging client/provider must be available — selecting one without it is a hard error. FILE, ENV, and CONFIGMAP need nothing extra.

A run command places the -c flag alongside --platform/--transport. The exact launcher differs by language (see the Quickstart); here is a representative HOST/FILE run:

Terminal window
python3 main.py --platform HOST --transport MQTT standalone-messaging.json \
-c FILE config.json -t my-thing

The top level of the document is strict: only the known sections are allowed and component is the only required one. The framework sections (logging, metricEmission, heartbeat, tags, messaging, streaming, credentials, parameters, health) configure the SDK subsystems; the component section carries your application config, split into a shared component.global object and an optional component.instances array for multi-instance components.

{
"logging": { "level": "DEBUG", "rust_format": "{timestamp} [{level}] {target} - {message}" },
"heartbeat": { "intervalSecs": 5, "measures": { "cpu": true, "memory": true }, "targets": [ { "type": "metric" } ] },
"metricEmission": { "target": "log", "namespace": "ggcommons" },
"credentials": { "vault": { "path": "./.vault/vault", "keyProvider": { "type": "file", "keyPath": "./.vault/vault.key" } } },
"parameters": { "source": { "type": "env", "prefix": "GG_PARAM_" }, "refreshIntervalSecs": 0, "sync": { "names": ["/skeleton/region", "/skeleton/poolSize"] } },
"tags": { "site": "factory-1" },
"component": {
"global": { "publish_interval": 3 },
"instances": [ { "id": "main" } ]
}
}

A few things worth knowing about the structure:

  • component.global is an open object — put any keys your component needs there, and every instance sees them.
  • component.instances is an array of objects, each requiring a unique id. Use it when one component process drives several logical instances (for example, one per production line). Each instance object is also open.
  • tags holds custom key/value strings that double as template variables (see below). Keys match ^[a-zA-Z0-9_-]+$.
  • The per-language logging format key is language-scoped: java_format, python_format, rust_format, or ts_format. Each SDK reads only its own key and ignores the other three — there is no shared format key.

String values such as log paths and MQTT topics can contain template variables that the SDK substitutes at resolution time. The supported placeholders are identical in all four languages:

Variable Resolves to
{ThingName} The resolved IoT thing / identity name.
{ComponentName} The short component name (the segment after the last .).
{ComponentFullName} The full component name.
{<tagKey>} Any string key under the tags section — e.g. a tag site is usable as {site}.

Only the substituted values are sanitized — the template literal itself is left intact, so legitimate separators you write in the template (slashes in a path, / in an MQTT topic) survive. Within each injected value, the characters /, \, +, #, and control characters become _, and any remaining .. traversal sequence is collapsed to _. This prevents an attacker-controlled thing name or tag from escaping a path or topic.

The resolution entry point differs by language: Java and Python expose an instance method on the config manager, while Rust and TypeScript expose a free function that takes the Config snapshot.

// instance method on ConfigManager
String path = config.resolveTemplate("/var/log/{ComponentName}-{ThingName}.log");

The SDK also auto-resolves templates inside the streaming, credentials, and parameters sections before handing them to those subsystems — you only call resolve yourself for your own templated strings.

How you reach the config differs by language. Java and Python hand you a ConfigManager object with per-section getters. Rust and TypeScript hand you an immutable Config snapshot directly — re-call config() after a reload to see updates, because the snapshot is replaced atomically.

ConfigManager config = gg.getConfigManager();
// Global config shared by all instances
JsonObject global = config.getGlobalConfig();
long timeout = global.has("timeout") ? global.get("timeout").getAsLong() : 5000L;
// Per-instance config (multi-instance components)
for (String id : config.getInstanceIds()) { // Collection<String>
JsonObject inst = config.getInstanceConfig(id); // null if id is absent
}
// Typed framework sections + identity
LoggingConfiguration logging = config.getLoggingConfig();
HeartbeatConfiguration heartbeat = config.getHeartbeatConfig();
String thing = config.getThingName();
String name = config.getComponentName(); // short name (after the last '.')

Register a change listener to be notified after a hot reload. Here too the SDKs diverge: Java and Python register the listener on the ConfigManager; Rust and TypeScript register it on the top-level runtime (gg). The callback shape also differs — Java’s callback takes no argument (you read the new values back from the manager), while the others receive the new config.

// Register on the ConfigManager. onConfigurationChanged() takes NO argument —
// read the updated values back from the manager. The return value is ignored.
config.addConfigChangeListener(() -> {
JsonObject updated = config.getGlobalConfig();
// ... re-apply what you need
return true;
});

The listener method name is past-tense in Java (onConfigurationChanged) and present-tense elsewhere (on_configuration_change / onConfigurationChange). Java and Python listeners are synchronous; Rust is async; TypeScript may return a Promise<boolean> | boolean. The boolean return is logged but not otherwise acted on. To stop receiving callbacks, use the matching removeConfigChangeListener / remove_config_change_listener (Rust removes by Arc pointer identity).

The FILE source watches the config file (and its parent directory, to handle atomic-rename saves) and reloads on change in every language. The CONFIGMAP source reloads in place via the kubelet ..data directory swap. On every reload the new document is re-validated against the schema.

Configuration is validated against the single canonical schema (schema/ggcommons-config-schema.json), which each SDK embeds or loads. Validation is fail-closed in all four languages — a missing schema or a validation error at startup is a hard error, not a silent skip. Each SDK uses its native validator (networknt / JSON Schema draft V7 in Java, jsonschema in Python, the jsonschema crate in Rust, Ajv in TypeScript).

The top level of the schema is strict (additionalProperties: false, required: ["component"]), so an unknown or mistyped top-level section is rejected. The component.global, component.instances, and tags subtrees are intentionally open so you can put arbitrary application keys there.

Python is also the only SDK that can opt out of validation, by constructing the config manager with validate_config=False. Java, Rust, and TypeScript embed the schema and cannot disable validation.

  • Entry point: Java/Python getConfigManager() / get_config_manager() return a ConfigManager with getters; Rust/TypeScript config() return an immutable Config snapshot (re-call after reload).
  • Template resolution: instance method (resolveTemplate / resolve_template) in Java/Python; free function (template::resolve(&cfg, s) / resolve(cfg, s)) in Rust/TypeScript.
  • Listener registration: on the ConfigManager in Java/Python; on the runtime gg in Rust/TypeScript.
  • Listener callback: Java takes no argument; Python/Rust/TypeScript receive the new config. Rust is async.
  • Missing instance: null (Java) / None (Rust) / undefined (TypeScript) / KeyError (Python).
  • Invalid reload: reject-and-keep in Java/Rust/TypeScript; apply-and-notify in Python.