stacks is a Terraform code pre-processor written in Python and distributed
as a Click-based command-line tool. It assembles a "layer" of Terraform code
on the fly from environment-specific variables, shared modules, Jinja
templates, and RSA-encrypted secrets, then drives terraform against the
rendered output. A small set of state-surgery commands round out the toolset
for safely moving, importing, and editing resources after a layer has been
rendered.
This guide walks end users — platform engineers and developers who deploy
infrastructure with stacks — from a first install to day-to-day usage and
recipe-level workflows. Architecture, code internals, and the full
configuration reference live in the companion Administrator & Developer
Guide.
1. Quick Start
Get a working stacks CLI on a workstation in under a minute, then verify
it can drive Terraform against a layer.
1.1 What stacks is in 30 seconds
- A pre-processor for Terraform that renders a single "layer directory" into
a self-contained Terraform working directory called
stacks.out/. - A CLI built on top of Click with the
entry point
stacks(Python modulestacks.main:cli). - Layer state lives in regular Terraform state —
stacksshells out to your installedterraformbinary, it does not reimplement Terraform. - Inline secrets are encrypted with RSA + AES-GCM and embedded as
ENC[...]strings inside*.tfvarsso the same files are safe to commit. - State-surgery commands (
stacks surgery list/import/remove/move/rename/edit) let operators repair or reorganise resources without leaving the tool.
1.2 Prerequisites checklist
- [ ] Python 3 with
pipavailable (the package is installable from the standard Python tooling). - [ ] A
terraformbinary onPATH(or setSTACKS_TERRAFORM_PATHto the absolute path of the binary you want). - [ ]
gitavailable onPATH— required bystacks diff. - [ ]
bashavailable —stacks surgery editinvokesbash -cto redirect Terraform output. - [ ] An editor for
stacks surgery edit— defaults toviif neitherSTACKS_EDITORnorEDITORis set.
1.3 Install (lowest-friction path)
The repository ships a pyproject.toml and is structured for pip-based
installation. Install the CLI directly from a clone:
git clone https://github.com/cisco-open/stacks.git
cd stacks
pip install .
Verify the binary is on PATH:
stacks version
Stacks 1.x.x
Python 3.x.x
Tip
The repository's lockfile is
uv.lock, which means contributors typically useuvfor reproducible builds. End users who only want the CLI on their machine can install with plainpip— thepyproject.tomlis the canonical metadata source.
1.4 First render
stacks render is the workhorse. It must be run from inside a layer
directory (a directory of the form
stacks/<stack>/layer/<env>[@<subenv>][_<instance>]):
cd stacks/my-stack/layer/dev
stacks render
stacks will:
- Clear the output directory
stacks.out/while preserving any cached.terraform/state. - Copy
*.tfand*.tfvars.jinjafiles from the global, stack, base, and layer scopes intostacks.out/with prefix-namespacing (common_*,stack_*,base_*,layer_*). - Render every
*.tfvars.jinjaand*.tffile with Jinja2 using merged variables fromenvironments/<env>/env.tfvars, sub-environment directories, and any.tfvarsfound upstream in the stack tree. - Emit
stacks.tf.json(the variable universe) andzzz.auto.tfvars.json(resolved variables) at the top ofstacks.out/. - Run
terraform -chdir=stacks.out initautomatically when no.terraform/terraform.tfstateexists yet — controlled by--init(auto,always,never).
Important
stacks render,stacks diff,stacks terraform, and everystacks surgerysubcommand assert at startup that you are in a valid layer directory — they walk four parents up to find the repository root and requireenvironments/<env>/to exist. Running from elsewhere raises anAssertionError.
1.5 What to try next
- Run
stacks terraform planto drive the wrapped Terraform binary against the rendered output (see Section 4.4). - Encrypt your first secret with
stacks encrypt(Section 5.3). - Walk through the layer directory layout in Section 3.
2. Installation
stacks is distributed as a standard Python package. Three install paths
are supported.
2.1 System requirements
| Component | Minimum | Notes |
|---|---|---|
| Python | 3.x | The package is namespaced as stacks and built with pyproject.toml. |
terraform binary |
Any HCL2-compatible release | Resolved via STACKS_TERRAFORM_PATH (default: terraform). |
git |
Any | Required for stacks diff (uses GitPython). |
bash |
Any POSIX shell with bash -c |
Required for stacks surgery edit redirections. |
| Editor | vi by default |
Override with STACKS_EDITOR or EDITOR. |
2.2 Install methods
From source (canonical)
git clone https://github.com/cisco-open/stacks.git
cd stacks
pip install .
Editable / development install
git clone https://github.com/cisco-open/stacks.git
cd stacks
pip install -e .
uv (matches lockfile)
git clone https://github.com/cisco-open/stacks.git
cd stacks
uv sync
uv run stacks version
Warning
The published Python distribution registers the entry point as
stacks. If your shell already has anotherstacksbinary onPATH(some unrelated Python tools also use the name), prepend the venv'sbin/directory toPATHor invoke the tool aspython -m stacks.main.
2.3 Runtime dependencies
The CLI imports the following third-party packages at runtime — they are
declared in pyproject.toml and installed automatically by pip install .:
| Package | Purpose |
|---|---|
click |
Command, group, option, and argument decorators that build the CLI. |
jinja2 |
Renders *.tf and *.tfvars.jinja templates with StrictUndefined. |
python-hcl2 (hcl2) |
Parses Terraform *.tfvars and *.tf files into Python data. |
pyyaml (yaml) |
YAML config decoding via helpers.yaml_read. |
cryptography |
RSA + AES-GCM encrypt/decrypt of ENC[...] secrets. |
deepmerge |
Used by helpers.merge to combine successive variable layers. |
gitpython (git) |
stacks diff stashes/unstashes against HEAD. |
2.4 Verify the install
stacks --help
Usage: stacks [OPTIONS] COMMAND [ARGS]...
Stacks, the Terraform code pre-processor.
All commands MUST run within a layer directory, unless noted otherwise.
Commands:
decrypt Decrypt an encrypted string using a private key.
diff Render and compare Git HEAD vs current uncommitted changes.
encrypt Encrypt a secret string using a public key.
render Render a layer into working Terraform code.
surgery Terraform state surgery utilities.
terraform Terraform command wrapper.
version Show version information.
Note
Two commands are intentionally hidden from
--help:stacks preinit(an internal stage ofrender, exposed only for advanced debugging) andstacks genkey(the key pair encrypts a fixed format, so generating a key pair is a one-time operation). Both still resolve when invoked explicitly.
2.5 Install troubleshooting
stacks: command not found after pip install .
The Python interpreter's bin/ (or Scripts/ on Windows) is not on
PATH. Either activate the virtualenv that owns the install or invoke
python -m stacks.main.
ModuleNotFoundError: No module named 'hcl2' at startup
A previous install was performed against a different Python interpreter.
Re-run pip install . in the interpreter you actually use, or use a clean
virtualenv.
terraform: command not found when running stacks render
stacks invokes the binary named in STACKS_TERRAFORM_PATH (default
terraform). Either install Terraform globally or set the environment
variable to an absolute path:
export STACKS_TERRAFORM_PATH=/usr/local/bin/terraform
3. Configuration
stacks itself is configured by a small set of environment variables — the
heavy configuration lives in the layer / stack / environment directory tree
that the tool reads during render.
3.1 Environment variables (user-facing)
All defaults are visible in src/stacks/cmd/config.py:
| Variable | Default | Purpose |
|---|---|---|
STACKS_ENVIRONMENTS_DIR |
environments |
Directory under repo root that holds per-environment .tfvars. |
STACKS_STACKS_DIR |
stacks |
Directory under repo root that holds stack definitions. |
STACKS_BASE_DIR |
base |
Per-stack base directory containing modules and shared .tf. |
STACKS_LAYERS_DIR |
layers |
Per-stack layers directory holding individual layer dirs. |
STACKS_OUTPUT_DIR |
stacks.out |
Name of the per-layer output directory created by render. |
STACKS_TERRAFORM_PATH |
terraform |
Path to the Terraform binary stacks should invoke. |
STACKS_EDITOR |
unset | Preferred editor for stacks surgery edit. Falls back to EDITOR, then vi. |
STACKS_PRIVATE_KEY_PATH |
unset | Comma-separated list of RSA private key files used for decrypt. |
Important
STACKS_PRIVATE_KEY_PATHaccepts a comma-separated list.stackstries each key in order and keeps the first that successfully decrypts — this is how you rotate keys without re-encrypting every secret in one change.
3.2 Repository layout stacks expects
stacks walks four directory levels up from the current working
directory to locate the repository root:
<repo-root>/
environments/ # STACKS_ENVIRONMENTS_DIR
<env>/
env.tfvars # Required if `<env>/` is used
<subenv>/
*.tfvars # Optional sub-environment overrides
stacks/ # STACKS_STACKS_DIR
<stack>/
base/ # STACKS_BASE_DIR (modules + .tf)
*.tf
modules/...
layer/ # STACKS_LAYERS_DIR per the layer name
<env>[@<subenv>][_<instance>]/
*.tfvars.jinja # The layer body
stacks.out/ # Created by `stacks render`
3.3 Layer directory naming convention
Layer directory names encode the environment, optional sub-environment, and optional instance:
| Pattern | Parsed as | Example |
|---|---|---|
<env> |
env only | dev, prod |
<env>@<subenv> |
env + sub-environment | prod@us-east-1 |
<env>_<instance> |
env + instance | dev_alice |
<env>@<subenv>_<instance> |
env + sub-env + instance | prod@us-east-1_blue |
The Context object splits on _ once, then splits the head on @ once, so
sub-environment and instance can co-exist freely.
3.4 Predefined Jinja variables
When stacks render evaluates *.tfvars.jinja and *.tf templates, it
makes a fixed set of predefined variables available under var:
| Variable | Source |
|---|---|
var.stacks_path |
stacks/<stack>/layer/<layer> |
var.stacks_root |
Relative path from the output dir back to the repo root |
var.stacks_stack |
Name of the stack directory |
var.stacks_layer |
Name of the layer directory |
var.stacks_environment |
Parsed environment from the layer name |
var.stacks_subenvironment |
Parsed sub-environment, or empty string |
var.stacks_instance |
Parsed instance, or empty string |
var.stacks_environments |
Map of every environments/<env>/env.tfvars, keyed by env name |
Note
All other variables are merged from the on-disk
.tfvarsfiles in ascending priority order: env → sub-env → stacks-wide → stack-wide → layer. The Admin Guide documents the full precedence chain in Section 3.
4. Core Usage
stacks exposes nine public commands plus a surgery subgroup. Every
command is annotated below with its purpose, required directory context, and
the Click-defined arguments and options.
4.1 stacks version
Prints the installed stacks version and the underlying Python version.
Can be run from any directory.
stacks version
Stacks 1.x.x
Python 3.x.x
4.2 stacks render
Renders the current layer into stacks.out/ and (optionally) runs
terraform init.
stacks render [--init auto|always|never]
| Option | Default | Effect |
|---|---|---|
--init |
auto |
auto runs terraform init only when no state is cached locally. always always runs it. never skips it. |
Behaviour:
- Calls the internal
preinitstage — clearsstacks.out/(preserving.terraform,.terraformrc,.terraform.lock.hcl), copies in*.tfand*.tfvars.jinjafrom common/stack/base/layer with prefixed names, renders all Jinja templates with the merged variable set, and writesstacks.tf.jsonandzzz.auto.tfvars.json. - Runs
terraform -chdir=stacks.out initaccording to--init.
Tip
zzz.auto.tfvars.jsonis named with three leadingzs on purpose — it ensures the file has the highest precedence when Terraform applies auto-loaded variables. Don't rename it.
4.3 stacks diff
Renders the layer twice — once against git stash-ed working tree (the
HEAD copy) and once against the live working tree — and runs
diff -ur --color between the two output directories.
stacks diff
The temporary directories stacks.out.old/ and stacks.out.new/ are
removed at the end.
Warning
stacks diffcallsgit stashandgit stash popagainst the repository the layer lives in. Ifgit stashfails (e.g., the working tree contains staged changes the stash refuses to take), Terraform will be rendered against an inconsistent file set. Commit or stash known-good changes before running.
4.4 stacks terraform
Wraps the configured terraform binary, but first re-renders the layer:
stacks terraform [--init auto|always|never] -- <terraform-args>...
| Option | Default | Effect |
|---|---|---|
--init |
auto |
Same semantics as stacks render --init. |
args |
— | Forwarded verbatim to terraform -chdir=stacks.out. |
Examples:
stacks terraform plan
stacks terraform apply
stacks terraform output
stacks terraform fmt
The wrapper sets -chdir=<stacks.out> for you, so you do not need to
navigate into the output directory manually.
4.5 stacks encrypt
Encrypts a string with a public key and prints the resulting ENC[...]
ciphertext. Can be run from any directory.
stacks encrypt --public-key-path <PATH> <STRING>
stacks encrypt --public-key-path <PATH> --from-stdin < secret.txt
| Option / Argument | Required | Description |
|---|---|---|
--public-key-path |
yes | Path to the PEM-encoded RSA public key. |
--from-stdin |
no | Read the plaintext from standard input instead of an argument. |
STRING |
conditional | Required when --from-stdin is not set. |
Output format:
ENC[<sym_key_b64>;<gcm_tag_b64>;<iv_b64>;<ciphertext_b64>]
The ciphertext can be embedded directly in *.tfvars files — stacks
recognises the ENC[...] envelope on read and decrypts it before passing
the value to Jinja or Terraform.
Caution
The CLI prints the ciphertext to stdout. Be deliberate about where you redirect it (a file, a clipboard, a CI variable) — copy/paste into shell history is a real disclosure path for the metadata even though the plaintext stays sealed.
4.6 stacks decrypt
Decrypts an ENC[...] string using a private key.
stacks decrypt --private-key-path <PATH> "ENC[...]"
| Option / Argument | Required | Description |
|---|---|---|
--private-key-path |
yes | Path to the PEM-encoded RSA private key. Comma-separated list is accepted. |
STRING |
yes | The full ENC[...] payload, including the brackets. |
Warning
Issue #14 (closed) reported
stacks decrypt --private-key-path <...>raisingAttributeError. If you hit that on an older build, upgrade to commita1fadd8or later.
4.7 stacks surgery list
Lists every Terraform-managed resource in the current layer's state by
address. Wraps terraform state list after re-rendering the layer.
stacks surgery list
4.8 stacks surgery import
Imports an existing cloud resource into the current layer's state by id.
stacks surgery import <ADDRESS> <RESOURCE-ID>
Example:
stacks surgery import aws_instance.web i-0123456789abcdef0
4.9 stacks surgery remove
Removes a resource from the current layer's state without destroying the
underlying infrastructure (delegates to terraform state rm).
stacks surgery remove <ADDRESS>
4.10 stacks surgery move
Moves a resource from the current layer's state into another layer's
state, preserving its real-world identity. Internally re-renders the
current layer, captures the resource id from terraform state show,
imports it into the destination layer (a Context is created at
<repo-root>/<TO-PATH>), then removes it from the source.
stacks surgery move <FROM-ADDRESS> <TO-ADDRESS> <TO-PATH>
| Argument | Description |
|---|---|
FROM-ADDRESS |
Source resource address (e.g. module.network.aws_subnet.public[0]). |
TO-ADDRESS |
Destination resource address in the target layer's state. |
TO-PATH |
Path to the destination layer directory, relative to the repository root. |
4.11 stacks surgery rename
Renames a resource within the current layer's state. Implemented as a
stacks surgery move whose destination path is the same layer.
stacks surgery rename <FROM-ADDRESS> <TO-ADDRESS>
4.12 stacks surgery edit
Pulls the current Terraform state, copies it to a temporary file, opens it
with STACKS_EDITOR / EDITOR / vi, shows a colour diff against the
original, and pushes the edited state back if you confirm.
stacks surgery edit
Caution
The state serial is not incremented automatically, on purpose — it protects non-experts from accidentally corrupting state. If you need a serial bump (e.g., to force remote backends to accept your push), edit the
serialfield manually insidevi.
4.13 Hidden commands
| Command | Purpose | When to use it |
|---|---|---|
stacks preinit |
Runs only the variable-resolution + file-prep stage of render (no terraform init). |
Debugging template rendering or variable precedence in isolation. |
stacks genkey |
Generates a 2048-bit RSA key pair and writes both halves to disk. | Initial key bootstrap. Hidden because the format is fixed and the operation is one-shot. |
stacks genkey \
--public-key-path keys/stacks.pub \
--private-key-path keys/stacks.key
5. Common Tasks
Realistic workflows assembled from the commands above.
5.1 Stand up a new layer for a new environment
Goal: add a staging environment to an existing stack.
-
Create the env directory with at least one
.tfvarsfile:mkdir -p environments/staging echo 'region = "us-west-2"' > environments/staging/env.tfvars -
Create the layer directory and a minimal
*.tfvars.jinja:mkdir -p stacks/<stack>/layer/staging cat > stacks/<stack>/layer/staging/main.tfvars.jinja <<'EOF' region = "{{ var.region }}" EOF -
Render and inspect:
cd stacks/<stack>/layer/staging stacks render stacks terraform plan
5.2 Promote a change from working tree to a real apply
Goal: preview every Terraform-visible side effect of an in-flight edit
before running apply.
cd stacks/<stack>/layer/prod
stacks diff # rendered HEAD vs rendered working tree
stacks terraform plan # Terraform-level plan against the rendered layer
stacks terraform apply
Tip
stacks diffshows changes to rendered Terraform — i.e. what the Jinja templates evaluated to — which catches variable-resolution bugs thatterraform plancannot.
5.3 Bootstrap encryption keys and store a secret
Goal: start using encrypted ENC[...] strings in .tfvars.
-
Generate a key pair (one-time, hidden command):
stacks genkey \ --public-key-path keys/stacks.pub \ --private-key-path keys/stacks.key -
Encrypt your first secret:
stacks encrypt \ --public-key-path keys/stacks.pub \ 'super-secret-value'ENC[NWE5...;ZGYz...;NjBh...;OWUz...] -
Drop the result into a
.tfvars:db_password = "ENC[NWE5...;ZGYz...;NjBh...;OWUz...]" -
Make
STACKS_PRIVATE_KEY_PATHpoint at the private key whereverstacks renderruns (CI runners, operator workstations):export STACKS_PRIVATE_KEY_PATH=keys/stacks.key
Caution
Commit only the public key. Distribute the private key out-of-band and store it on disk only where renders happen.
5.4 Rotate the RSA key without re-encrypting every secret
STACKS_PRIVATE_KEY_PATH accepts a comma-separated list, and the decrypt
helper tries each key in order. Use this to roll keys gradually:
export STACKS_PRIVATE_KEY_PATH=keys/stacks.key.new,keys/stacks.key.old
Re-encrypt secrets at your leisure with stacks encrypt --public-key-path keys/stacks.pub.new. Once every ENC[...] payload decrypts under the new
key, drop the old one from the list.
5.5 Reorganise resources across layers
Goal: a resource lives in layer/dev but logically belongs in
layer/dev_alice.
cd stacks/<stack>/layer/dev
stacks surgery move \
aws_s3_bucket.user_data \
aws_s3_bucket.user_data \
stacks/<stack>/layer/dev_alice
The third argument is the destination layer path relative to the repository root, not relative to the current layer.
5.6 Adopt an existing cloud resource
Goal: import an EC2 instance that was created by hand.
cd stacks/<stack>/layer/prod
stacks surgery import aws_instance.bastion i-0123456789abcdef0
stacks terraform plan # confirm there is no diff
Note
Always follow
importwithterraform plan. If the imported resource's Terraform configuration drifts from what cloud reports, the nextapplywill rewrite or destroy it.
5.7 Run an in-place state edit
Goal: fix a corrupted serial or remove an orphaned resource block by hand.
cd stacks/<stack>/layer/prod
stacks surgery edit
stacks pulls the current state, copies it to .terraform/new.tfstate,
opens that file in your editor, prints a diff against the original on save,
and asks Proceed with changes? [y/N] before pushing.
5.8 Clean rebuild after upstream changes
Goal: force a fresh terraform init after pinning a new provider
version or pulling new modules.
cd stacks/<stack>/layer/prod
stacks render --init always
--init always re-runs terraform init even when local state already
exists.
6. Troubleshooting
The patterns below are derived from the exact exception strings in the shipped source and from real GitHub issues filed against the project.
Error: please set the STACKS_PRIVATE_KEY_PATH environment variable
Cause: stacks encountered an ENC[...] payload while rendering or
running decrypt, but no private key path is configured.
Solution:
-
Export the variable, pointing at a private key that owns the encrypted payloads:
export STACKS_PRIVATE_KEY_PATH=keys/stacks.key -
If you want decryption to be optional (e.g. in a CI lint job), call the underlying
helpers.decrypt(..., must_decrypt=False)in code — the CLI itself does not expose a soft-decrypt flag.
Prevention: Set the variable in the same shell profile / CI secret store that holds the rest of your stack credentials.
Error: Failure to render <PATH>: <jinja2-error>
Cause: a Jinja2 template referenced a variable that was not defined
in the merged variable set. stacks uses jinja2.StrictUndefined, so any
unknown variable raises UndefinedError.
Solution:
- Re-read the wrapped Jinja error after the colon — it names the missing variable.
- Confirm the variable is declared in one of the precedence layers
(
environments/<env>/env.tfvars,<subenv>/*.tfvars,stacks/*.tfvars,stack/*.tfvars, layer-level*.tfvars). - Run
stacks preinitto re-run only the rendering stage and inspectstacks.out/without invoking Terraform.
Prevention: Validate every new template by running stacks render in
the lowest environment first.
Error: Failure to render <PATH> at line <N>, in statement <STMT>: <jinja2-error>
Cause: Jinja2 raised a TemplateSyntaxError — the template body has
malformed {% %} / {{ }} syntax.
Solution:
- Open the file at the named line; fix the syntax.
- Test in isolation with
python -c "import jinja2; jinja2.Template(open('PATH').read())".
Prevention: Keep templates terse. The shipped helpers do not provide
debug-time template linting — a CI step that calls stacks render against
every layer is the practical guard.
Error: No input received from stdin.
Cause: stacks encrypt --from-stdin was invoked with no piped data.
Solution: Either pipe data in (echo 'secret' | stacks encrypt --from-stdin --public-key-path ...) or pass the secret as a positional
argument.
Error: You must provide either a string argument or use --from-stdin.
Cause: stacks encrypt was invoked without any plaintext source.
Solution: Provide the string as a positional argument or set
--from-stdin.
Error: AttributeError from stacks decrypt --private-key-path
Cause: Reported in #14
(closed) — affected older revisions of helpers/crypto.py.
Solution: Upgrade to commit a1fadd8 or later. The current code
splits private_key_path correctly and iterates over each key.
Symptom: stacks render errors with AssertionError and no message
Cause: The current working directory is not a valid layer directory.
The Context constructor walks four levels up
(path.parent.parent.parent.parent) to find the repository root and asserts
that environments/<env>/ exists, where <env> is parsed from the layer
directory name.
Solution:
cdinto a directory of the form<repo>/stacks/<stack>/layer/<env>[@<subenv>][_<instance>].- Confirm
environments/<env>/exists from the repo root. - Confirm any sub-environment directory (
environments/<env>/<subenv>/) exists when the layer name contains@<subenv>.
Prevention: Wrap operator workflows in shell aliases that cd to the
right layer before invoking stacks.
Symptom: stacks diff shows no differences but terraform plan does
Cause: stacks diff only sees changes that affect rendered Terraform.
A pure variable-file change that flows through unchanged Jinja produces
identical rendered output but Terraform will still pick up new variable
values from zzz.auto.tfvars.json.
Solution: Trust terraform plan for Terraform-level diffs. Use
stacks diff to confirm template changes evaluate as expected.
Symptom: stacks surgery edit opens the wrong editor
Cause: Editor resolution order is STACKS_EDITOR → EDITOR → vi.
Solution: Set STACKS_EDITOR explicitly:
export STACKS_EDITOR=nano
stacks surgery edit
Symptom: stacks render succeeds but terraform init fails to fetch modules
Cause: stacks render does not pre-fetch modules — it copies the
base/ modules directly under stacks.out/ and relies on terraform init
for everything else. Network or registry credentials missing from the
shell will surface as init errors.
Solution: Configure your ~/.terraformrc and credential helpers
exactly as a non-stacks Terraform project would.
Tip
When in doubt,
cd stacks.out && terraform init -upgradereproduces the exact init the wrapper would run, but lets you append flags interactively.
7. FAQ
Q: What problem is stacks solving over plain Terraform plus tfvars?
stacks lets you keep variable definitions, environment-specific values,
shared modules, and templated .tf files in one Git repository, then
materialise a per-environment Terraform working directory on demand. The
key value-adds are Jinja templating across both *.tfvars.jinja and *.tf
files, inline ENC[...] secret encryption, and a surgery group of
state-management commands.
Q: Where do my Terraform plan/apply cycles run?
Inside stacks.out/ (controlled by STACKS_OUTPUT_DIR), which stacks render rebuilds before each invocation. You generally never need to cd
into it — stacks terraform and stacks surgery set -chdir=stacks.out
for you.
Q: Does stacks ship its own Terraform binary?
No. It shells out to whatever STACKS_TERRAFORM_PATH points to (default:
terraform on PATH). Use this to pin Terraform versions per-stack.
Q: How do I share encrypted secrets between teams?
Distribute the public key (stacks.pub) widely; restrict the private key
(stacks.key) to operators who need to render. Anyone can encrypt
new payloads against the public key without ever holding the private key.
Q: Can I rotate the RSA key without re-encrypting every secret in one shot?
Yes — set STACKS_PRIVATE_KEY_PATH to a comma-separated list of paths.
stacks tries each key in order and uses the first one that successfully
decrypts. Re-encrypt secrets piecewise, then drop the old key from the
list.
Q: Why is stacks preinit hidden?
Because it executes only the rendering and variable-resolution step of
render, with no terraform init. Useful for debugging template logic,
but not for normal operation.
Q: Why is stacks genkey hidden?
Because the key pair format is fixed (RSA-2048 with public exponent
2**16 + 1, PEM-encoded, PKCS#8 unencrypted private) and the operation is
done once per project.
Q: What state backend does stacks use?
Whatever you configure inside the layer. stacks does not impose a state
backend — it only renders the Terraform code and runs terraform init.
Configure remote state via the usual terraform { backend "..." } block.
Q: Does stacks surgery edit increment the state serial automatically?
No. By design — to protect non-experts from accidentally corrupting
remote state. Bump serial manually inside the editor when you need to.
Q: What is the project's status?
Issue #8 (closed) tracked
an inquiry about project status. The codebase is concise (24 source
files, ~30k characters of code at commit a1fadd8) and currently lives
under the cisco-open organisation. See the Admin Guide for governance
notes.
Q: How does stacks handle nested encrypted values inside lists or
maps?
The helpers.decrypt function recurses into lists and dicts, so any
ENC[...] string at any depth in a parsed .tfvars value is decrypted.
Plain strings without the ENC[ prefix are returned untouched.