Advanced authoring
Layer inputs, templates, remote resources, validations, transforms, and native operations for more sophisticated stacks.
Inputs
Input resolution order from lowest to highest priority.
- Vars files from
STACKSMITH_VARS, when used without--runfile - Environment variables prefixed with
STACKSMITH_VAR_ stacksmith.yamlvarssources, when a runfile is used- Explicit
--varsand--var key=valueentries, deep-merged in the order they appear on the command line
Runfile inline vars sources support Jinja in two stages. Stage 1 runs while the runfile itself is loaded and provides runfile metadata fields. Stage 2 runs during input resolution and provides inputs and stack.
The runfile metadata fields available during stage 1 are runfile.path, runfile.dir, runfile.name, and runfile.stem.
When Stacksmith renders a stack from a Git working tree with an origin remote, git_repository contains that remote URL. Stack rendering, input resolution, property transforms, and default module source templates resolve it from the stack file's directory, not Stacksmith's launch directory. Runfile stage 1 resolves it from the runfile directory. For direct input resolution without a stack, Stacksmith uses its current working directory. The variable is undefined when the relevant directory is not in a Git working tree or origin is not configured. For example, a stack can use iac_repository: "{{ git_repository }}" in an AWS resource tag.
When --runfile is used, Stacksmith applies runfile vars sources before CLI-provided variable layers and does not apply STACKSMITH_VARS defaults.
Templating matrix
Stacksmith supports Jinja in specific surfaces rather than as a global feature.
| Surface | Render timing | Context | Notes |
|---|---|---|---|
Stack source (stack.yaml, stack.yml, stack.json) |
Before YAML or JSON parse and schema validation | inputs, stack, components, reserved stack-transform output, git_repository when available |
Full-file render for structural generation. Component references and stack output transform values are preserved and bound during OpenTofu generation. |
| Resolved input values | After vars, env vars, runfile vars, and CLI vars are merged | inputs, stack, git_repository when available |
Value-level render across merged inputs. |
Runfile stage 1 (stacksmith.yaml) |
During runfile load before schema validation | runfile.path, runfile.dir, runfile.name, runfile.stem, git_repository when available |
Primarily for structured references and inline vars source data. |
| Runfile stage 2 (runfile inline vars after merge) | During input resolution | inputs, stack, git_repository when available |
Lets runfile-provided values compose with final merged inputs and stack metadata. |
default_module_mapping.source |
During module mapping resolution when no explicit mapping exists | component.type, component.name, env.git_repository when available |
Strict sandboxed render with post-render source validation. |
Module property default values |
During module input generation when the component omits the property | property.name, property.kind, property.output_name, component.name, component.type, inputs, stack, components, env.git_repository when available |
Applies to explicit and default module mappings. Recursively renders configured defaults before transforms and validations; deferred component output references are preserved and bound afterward. |
properties.*.transform.jinja |
During input transform execution | property.value plus transform context (property.name, property.kind, property.output_name, component.name, component.type, inputs, stack, env.git_repository when available) |
Adapts a resolved stack property into a module input. |
module_mappings.*.outputs.*.transform.jinja |
During component output binding | output.value, output.name, output.module_output, component.name, component.type, stack, env.git_repository when available |
Adapts the unresolved module output reference into the public component output. |
Stack outputs.*.transform.jinja |
During root output generation | output.value, output.name, stack, env.git_repository when available |
Safely adapts the exported value and its mock after component output binding. |
Other managed-config fields are intentionally non-templated.
Remote resources
Stacksmith can pull scripts, config files, vars files, stack files, and runfiles from remote locations. Anywhere a local file path is accepted for validation scripts, transform scripts, vars files, stack files, config files, or stacksmith.yaml, a remote URL can be used instead.
Runfiles and config script references use a structured source + data object.
Supported sources are:
localwithdata.pathgitwithdata.repo,data.path, optionaldata.refhttpwithdata.urlregistrywithdata.address,data.version
Stacksmith treats this as the canonical representation and renders tool-specific syntax server-side before invoking downstream tools.
Canonical vs rendered target syntax
| Canonical reference | OpenTofu rendered value | CLI flag rendered value |
|---|---|---|
source: local, data.path: ./vars.dev.yaml |
./vars.dev.yaml |
./vars.dev.yaml |
source: http, data.url: https://example.com/base.yaml |
https://example.com/base.yaml |
https://example.com/base.yaml |
source: git, data.repo: https://github.com/org/shared.git, data.path: vars/base.yaml, data.ref: v1.2.3 |
git::https://github.com/org/shared.git//vars/base.yaml?ref=v1.2.3 |
git+https://github.com/org/shared.git//vars/base.yaml@v1.2.3 |
source: registry, data.address: hashicorp/aws, data.version: ~> 6.0 |
{ source = "hashicorp/aws", version = "~> 6.0" } (provider/module fields) |
Not used for file-style CLI flags |
Usage examples
In config validations/transforms, use a structured script reference.
# stacksmith-config.yaml â remote managed input validation script
var_validations:
bucket_name:
script:
source: http
data:
url: https://raw.githubusercontent.com/my-org/shared/main/validators/bucket.py
# stacksmith-config.yaml â remote transform script from a git repo
module_mappings:
aws_s3_bucket:
source:
source: git
data:
repo: https://github.com/my-org/terraform-aws-s3.git
ref: 3.2.1
properties:
acl:
mapped_to: bucket_acl
transform:
script:
source: git
data:
repo: https://github.com/my-org/shared.git
path: transforms/acl.py
ref: v2.0.0
Config files, vars files, stack files, and runfiles also support remote URLs via CLI flags (--config, --vars, --stack, --runfile) where URL strings are passed directly.
stacksmith plan \
--config https://example.com/org-config.yaml \
--vars git+https://github.com/org/defaults.git//env/base.yaml@v1.2.0 \
--vars git+https://github.com/org/service-defaults.git//bucket-writer/dev.yaml@v3.4.1
stacksmith validate \
--runfile git+https://github.com/org/platform-live.git//services/payments/stacksmith.yaml@main
Caching
Stacksmith and Terragrunt now use two cache layers.
- Stacksmith cache stores Stacksmith-resolved remote references (for example config files, vars files, stack files, runfiles, and Python scripts referenced by validations/transforms) under
.cache/inside the build output directory, or.stacksmith/.cache/when no build directory is set. - Terragrunt CAS caches Terragrunt source fetching (modules/catalog/stack sources) and is enabled by default in Terragrunt
>= 1.1.0.
Use --no-cache to force Stacksmith to re-fetch its own remote references. On runtime commands (init, plan, apply, destroy, and run-all), --no-cache also disables Terragrunt CAS for that invocation.
Use --no-cas when you only want to disable Terragrunt CAS without clearing the Stacksmith cache.
Environment variable defaults
STACKSMITH_CONFIG and STACKSMITH_VARS can provide default config and vars references when the corresponding CLI flags are omitted.
STACKSMITH_STACK can provide a default stack file path when no positional stack argument is given.
STACKSMITH_RUN_FILE can provide a default runfile reference when --runfile is omitted. If it is not set, Stacksmith auto-loads ./stacksmith.yaml when present.
Use colon-delimited lists.
If an item contains colons, such as a remote URL, wrap that item in quotes.
export STACKSMITH_VARS='"git+https://github.com/org/platform-defaults.git//env/base.yaml@v1.2.0":"git+https://github.com/org/service-defaults.git//bucket-writer/dev.yaml@v3.4.1"'
Authentication
Authentication is resolved by checking the remote_auth config section first, then falling back to environment variables.
Config-based auth
Add a remote_auth section to stacksmith-config.yaml, keyed by hostname.
remote_auth:
github.com:
type: token
token_env: GITHUB_TOKEN
username_env: GITHUB_USERNAME
gitlab.internal.com:
type: basic
username_env: GITLAB_USER
password_env: GITLAB_PASS
git.private.com:
type: ssh
ssh_key_path: /home/ci/.ssh/deploy_key
Supported auth types are token (HTTP Bearer or git token), basic (HTTP Basic), and ssh (Git SSH key).
When Stacksmith executes Terragrunt runtime commands, Stacksmith forwards HTTPS token auth through a temporary Git credential helper so CAS-backed and OpenTofu-initiated Git fetches can reuse your configured credentials. The helper reads tokens from the subprocess environment, contains no credential values itself, and is deleted when the subprocess exits. Token auth preserves a username supplied by Git configuration or the source URL, uses username_env when configured, and otherwise defaults the username to git.
Environment variable fallbacks
When no matching remote_auth entry exists, stacksmith checks the following environment variables.
| Variable | Purpose |
|---|---|
STACKSMITH_HTTP_TOKEN |
Bearer token for HTTP(S) requests |
STACKSMITH_HTTP_USERNAME / STACKSMITH_HTTP_PASSWORD |
Basic auth for HTTP(S) |
STACKSMITH_GIT_TOKEN |
Token auth for git clone (HTTPS) |
STACKSMITH_GIT_SSH_KEY |
Path to SSH private key for git clone |
STACKSMITH_SSL_VERIFY |
Set to false to disable TLS verification |
âšī¸ Note: Remote config files are fetched before the config is loaded, so
remote_authentries are not available for config-level URLs. Use environment variables for authentication when fetching remote configs.
Validation and transforms
Stacksmith supports Python validation hooks and Python or Jinja transform hooks.
- Validations use either
inlinePython orscript. - Transforms use
inline,script, orjinjadepending on context. - Relative script paths resolve from the declaring file.
- Validation and transform specifications accept an optional
description, including entries invar_validations, managed module properties and outputs, and stack output transforms.
Machine-facing mapping keys remain stable identifiers, while description carries prose for people and inspection output. Optional descriptions are also supported on managed configurations, provider families and instances, module mappings and properties, operation definitions and inputs, merge rules, runfiles and their stack/config/variable references, stacks and their components, outputs, operation invocations, and test manifests.
Plan validations
The managed configuration can define plan_validations that run after plan and run-all plan against OpenTofu plan JSON output.
Plan validation rules can return pass, warn, or fail outcomes.
- Truthy values pass and falsey values fail.
- Warnings are non-blocking by default; use
--strict-validation-warningsto treat warning outcomes as failures. - Use
--fail-on-changesonplanorrun-all planto return a non-zero exit code whenever the rendered plan contains any resource changes. This is useful for automated drift detection or CI checks where only a non-empty plan should fail.
Native operations
Operations are config-owned imperative actions. Stacksmith compiles them into a separate runner-only Terraform root backed by <stack-path>/operations/terraform.tfstate. The infrastructure root never contains operation resources, and the operation root never contains infrastructure resources or providers.
Every approved public component output is exposed from the infrastructure root through the nested, sensitive _stacksmith_operation_bridge output, even before an operation references it. The bridge can therefore appear as one sensitive value under Changes to Outputs in an infrastructure plan, but it is not an operation resource and does not execute anything. It must live in the infrastructure state because that is where the component values are produced. Operations select individual values from the bridge through read-only remote state access. Predeclaring the bridge values means adding or changing an operation does not require an infrastructure apply merely to establish its dependency contract.
The managed config fixes the runner details, including the local command argument vector or Jenkins job and credentials. A stack can only select an approved operation and supply declared inputs. Operation inputs support the same Jinja templates and deferred public component outputs as component properties, so an operation can consume an output such as {{ components.app.release_name }}. Operations use the manual trigger by default; set trigger: after_apply in managed config to run them after a successful apply.
Local operation environments can automatically expose every declared operation input. Names are uppercased by default; overrides and exclusions are nested under inputs so they cannot collide with environment policy fields:
environment:
mode: auto
inputs:
overrides:
KUBE_CONTEXT: kubeconfig_context
exclude:
- secret_value
OpenTofu suppresses all local-exec output when its command or environment contains a sensitive value. To support operation-level output control, Stacksmith declassifies the runner specification only at the local process boundary while keeping it sensitive in plans, then discards the child process's standard output and standard error by default. Set stream_output: true on a managed local operation to inherit those streams through OpenTofu.
Use output_masking to define literal redaction rules for streamed local operation output. output_masking.literals masks fixed literal strings, and output_masking.inputs masks resolved values for selected operation inputs. When streaming is enabled, every secret input must be listed in output_masking.inputs.
# stacksmith-config.yaml
module_mappings:
application:
source:
source: registry
data:
address: example/application
version: "1.0.0"
outputs:
release_name:
description: Deployed application release name.
operations:
deploy:
description: Deploy an approved application release.
runner: local
trigger: after_apply
stream_output: true
command: [./bin/deploy]
output_masking:
literals:
- DO-NOT-LEAK
inputs:
- release_name
environment:
APP_ENV: environment
RELEASE_NAME: release_name
inputs:
environment:
description: Deployment environment.
required: true
release_name:
description: Immutable application release identifier.
required: true
# stack.yaml
components:
app:
type: application
operations:
deploy_app:
use: deploy
with:
environment: "{{ inputs.environment }}"
release_name: "{{ components.app.release_name }}"
Dry-run a manual operation by its stack-local name before executing it.
stacksmith operation plan deploy_app --stack stack.yaml --config stacksmith-config.yaml
Omit operation names to dry-run every operation declared by the stack. The same omission for operation run executes all declared operations.
stacksmith operation plan --stack stack.yaml --config stacksmith-config.yaml
Infrastructure plans use --after-apply to restrict the operation preview to operations configured with the after_apply trigger. The Jenkins and GitHub Actions GitOps pipelines set this automatically for plan and apply commands.
stacksmith operation plan --after-apply --stack stack.yaml --config stacksmith-config.yaml
Run the operation after reviewing the plan.
stacksmith operation run deploy_app --stack stack.yaml --config stacksmith-config.yaml
Select multiple operations with a comma-delimited list. Stacksmith includes transitive depends_on operations automatically and generates a root containing only that batch. Independent operations run concurrently, while dependency edges preserve ordering. Set STACKSMITH_MAX_PARALLEL_OPERATIONS to cap concurrency; it defaults to 10 and is intentionally not a command-line or CI parameter.
STACKSMITH_MAX_PARALLEL_OPERATIONS=3 \
stacksmith operation run publish_image,deploy_app,smoke_test \
--stack stack.yaml \
--config stacksmith-config.yaml
For a one-time definite dispatch without changing the stack definition, add --force-rerun or set STACKSMITH_FORCE_RERUN=1. This marks each explicitly selected operation resource for replacement in the operation plan.
stacksmith operation run deploy_app --force-rerun --stack stack.yaml --config stacksmith-config.yaml
Alternatively, change rerun_token in the stack definition when the rerun request should remain declarative and reviewable. operation plan creates a saved operation-only OpenTofu plan without invoking provisioners. operation run validates that the plan contains no managed changes outside module.stacksmith_operation_* and applies that exact saved plan. Operations with the after_apply trigger run in stack dependency order during stacksmith apply and stacksmith run-all apply; Stacksmith replans them after infrastructure succeeds so component outputs are current. Stack-local depends_on can order multiple operations within a stack. Jenkins runners poll the queued build through completion, so a dependent operation starts only after its Jenkins prerequisite succeeds. Managed Jenkins definitions can set poll_interval_seconds and timeout_seconds, which default to 5 and 3600.
Preview removal of the complete isolated operation state without running operation provisioners, then destroy it after review.
stacksmith operation plan --destroy --stack stack.yaml --config stacksmith-config.yaml
stacksmith operation destroy --stack stack.yaml --config stacksmith-config.yaml
operation destroy generates and validates a fresh destroy plan, prompts for confirmation, and applies that exact saved plan. Add --auto-approve only in an already approved automation context. Operation names, --after-apply, and --force-rerun are rejected in destroy mode because cleanup removes the complete isolated state rather than dispatching selected operations.
Declarative application deployments through Jenkins
An approved Jenkins operation can deploy application code while the GitOps repository records exactly what should be deployed. Keep the Jenkins URL, job, credential variable names, and allowed parameter mapping in managed configuration. Set trigger: after_apply explicitly because operations remain manual by default.
# stacksmith-config.yaml
operations:
deploy_application:
runner: jenkins
trigger: after_apply
url: https://jenkins.example.com
job_name: deployments/application
username_env: STACKSMITH_JENKINS_USERNAME
api_token_env: STACKSMITH_JENKINS_API_TOKEN
parameters:
ENVIRONMENT: environment
GIT_COMMIT: commit_id
inputs:
environment:
description: Deployment environment.
required: true
commit_id:
description: Immutable application Git commit to deploy.
required: true
The stack selects that approved operation and binds its parameters to declarative inputs.
# application.stack.yaml
operations:
deploy_application:
use: deploy_application
with:
environment: "{{ inputs.environment }}"
commit_id: "{{ inputs.application_commit }}"
Each environment layer pins the desired application revision.
# environments/prod.yaml or its referenced vars file
vars:
- source: inline
data:
environment: prod
application_commit: "8c9f20bd0cbf2c70f7f728f4e92bf6ad239a45b1"
On a merge to the default branch, the normal apply workflow reconciles the operation in its isolated state after infrastructure succeeds. Changing application_commit, the approved Jenkins definition, a component output, or another bound parameter changes the operation specification, so OpenTofu replaces the operation resource and Stacksmith starts the Jenkins build. An unchanged specification is a no-op. The runner passes GIT_COMMIT to Jenkins, waits for the queued build to finish, and fails the operation phase if Jenkins does not report success. Jenkins credentials stay in the CI environment and never enter the application manifest.
Testing policies and transforms
Stacksmith tests are declared in tests.yaml manifests. stacksmith test compiles those manifests into an ephemeral pytest module and runs it with Stacksmith's managed-config fixture wiring, cache behavior, and layered merge behavior.
When --config points at one or more managed config layers, Stacksmith discovers tests.yaml beside each selected config and merges them in order. You can also pass explicit manifest paths after Stacksmith options.
stacksmith test \
--config examples/shared-config-repo/stacksmith-base-config.yaml \
--config examples/shared-config-repo/stacksmith-config.yaml
stacksmith test \
--config platform/base-config.yaml \
--config platform/prod-config.yaml \
platform/tests.yaml \
-- -k imdsv2
Use --dump-tests when you want to inspect the generated pytest code.
stacksmith test \
--config examples/shared-config-repo/stacksmith-base-config.yaml \
--config examples/shared-config-repo/stacksmith-config.yaml \
--dump-tests /tmp/stacksmith-generated-tests.py
Manifest test cases cover variable policies, plan policies, and component properties. Plan policy cases can include optional context (for example stack metadata), and manifests can define optional setup/teardown fixtures using either inline Python or script references. Fixture execution mode can be set to per-suite (default) or per-test-case.
Generated test names remain unique when case or policy names normalize to the same Python identifier. Policy exceptions, missing scripts, and invalid return values fail tests even when a case expects fail. Use message_contains on variable and plan cases to check a literal substring in the returned diagnostic; outcome assertion failures also display that diagnostic.
Fixture scripts support local, HTTP, and Git references. Local paths resolve relative to their declaring manifest, and remote scripts use the managed configuration's authentication and the test command's cache.
The test command reports named variable and plan policies without cases in the merged manifest, including disabled policies. This is an informational inventory of declared cases, not line coverage or a count of tests selected by pytest filters.
description: Policy and transform tests for the production platform configuration.
fixtures:
mode: per-test-case
setup:
inline: |
fixture_state["ready"] = True
var_validations:
aws_region:
- value: us-east-1
expect: pass
plan_validations:
ec2_t3_micro_warning:
- resources:
- type: aws_instance
after:
instance_type: t3.micro
context:
stack_name: production
expect: warn
component_properties:
aws_s3_bucket:
bucket_name:
- value: My_Bucket
inputs:
environment: prod
expect:
output_name: bucket
value: prod-my-bucket
Each resources item requires type; Stacksmith supplies address: <type>.this and change.actions: [create] by default. Set address or actions explicitly for address-sensitive policies, multiple resources of the same type, deletes, replacements, or other non-default plan behavior. Use plan instead of resources when a test requires exact OpenTofu plan JSON.
Property cases also accept component_name (default test-component), stack, and git_repository. These values use the same context structure as production transforms and validations, alongside inputs.
For a property with a configured validation, use expect: fail to require an explicit rejection. An unexpected successful result, a transform error, or a broken validation fails the test. Property message_contains is available only with expect: fail.
component_properties:
storage:
access:
- name: Rejects public access
value: public
component_name: audit-logs
stack:
name: production
git_repository: https://example.com/platform.git
expect: fail
message_contains: Private access is required
Local path resolution
- Local paths in
stacksmith.yamlrunfilestacks,configs, and localvarssources resolve relative to the runfile that declares them. - Local script paths and local module source paths in
stacksmith-config.yamlresolve relative to the config file that declares them.