Terragrunt workflows
Skill Goodsmileduck/claude-registry/plugins/iac-skills/skills/terragrunt-workflows
Community marketplace of Claude Code plugins: DevOps skill packs (Terraform, Kubernetes, CI/CD, cloud platforms, DigitalOcean), CLAUDE.md optimization, and diagramming — gated by a best-practices lint + security-audit CI.
npx -y skills add Goodsmileduck/claude-registry --skill terragrunt-workflowsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Terragrunt-specific orchestration patterns — CLI redesign migration (run/run --all, --terragrunt-* flag removal, TG_* env vars, strict controls), config composition (include, locals, inputs deep-merge, generate blocks), dependency wiring (mock_outputs semantics), run --all safety, hooks, and the new terragrunt.stack.hcl. Use when working with Terragrunt, `terragrunt.hcl`, `terragrunt.stack.hcl`, the deprecated `run-all`, `--terragrunt-*` flags, `TERRAGRUNT_*` env vars, `include` blocks, `dependency` blocks, or `terragrunt run --all`.
SKILL.md
15.7 KB, as published. Nobody here has run it
Terragrunt Workflows
Terragrunt-specific orchestration. Pairs with the terraform-workflows skill — its cross-cutting rules apply on top of this skill's. See Cross-skill notes for the exact boundary.
When to invoke
Symptoms:
- A command that worked last quarter now warns or fails:
--terragrunt-*flag deprecated,run-alldeprecated, "unknown command" for what used to beterragrunt plan. - An
includeblock isn't producing the merged config you expected. - A child unit's
inputsmap seems to keep parent keys you tried to drop. - A
dependencyblock'smock_outputsproduces values the actual upstream never emits. terragrunt run --all applyis on the table for a production rollout.- A
terragrunt.stack.hclfile is in the repo and you need to know how it generates units. - Hooks (
before_hook,after_hook,error_hook) fire at the wrong moment or silently swallow errors.
Cross-cutting rules
These layer on top of terraform-workflows cross-cutting rules. Plan-then-apply, never bump pins in passing, never -auto-approve on shared state — all of those still apply.
- Treat the CLI redesign as the default. Even if a repo still uses
run-alland--terragrunt-*flags, write new code in the new form (run --all,--<flag>,TG_*env vars). SetTG_STRICT_CONTROL=cli-redesignlocally so you can't accidentally regress. run --all <mutation>requires a plan-aggregation step. It does NOT produce a unified plan. Without aggregation, blast radius is invisible until mid-pipeline. See run --all safety.- Never edit a generated file. Files inside
.terragrunt-cache/and.terragrunt-stack/are derived. Editing them produces drift that vanishes on the next run. - Hooks run on every invocation. A
before_hookwithcommands = ["plan", "apply"]runs both duringplanandapply. If a hook performs auth refresh or state pre-fetch, it costs that work twice on a plan→apply cycle. Scope hooks tightly. mock_outputsare placeholders for plan-time, not stand-ins for unapplied state. If a plan contains a recognizable mock literal where you'd expect a real ID, the upstream dependency hasn't been applied. Apply it; do not adjust the mock to match reality.
CLI redesign (the big one)
Terragrunt's CLI was redesigned in the v0.73 series. Training data and many internal scripts predate this — assume Claude's first guess is the deprecated form.
The four renames
| Old (deprecated) | New |
|---|---|
terragrunt plan (auto-forwards to OpenTofu) | terragrunt run plan |
terragrunt run-all <cmd> | terragrunt run --all <cmd> |
--terragrunt-<flag> | --<flag> (e.g. --terragrunt-source-update → --source-update) |
TERRAGRUNT_<VAR> | TG_<VAR> (e.g. TERRAGRUNT_NON_INTERACTIVE → TG_NON_INTERACTIVE) |
Bare terragrunt plan still works for backward compatibility, but no longer auto-forwards unknown commands by default — under strict mode it errors.
New top-level commands
terragrunt run <cmd> [--all]— execute an OpenTofu/Terraform command in one or all unitsterragrunt find— discover units (replaces ad-hocfindforterragrunt.hcl); supports--dag --jsonfor dependency orderterragrunt list— list units with optional--dag --treeterragrunt stack {generate,run,output,clean}— manageterragrunt.stack.hclfilesterragrunt backend {bootstrap,migrate,delete}— explicit backend management (previously implicit)
Strict controls
TG_STRICT_MODE=true turns every deprecation into an error.
TG_STRICT_CONTROL=cli-redesign,default-command enables specific groups. Names of controls are stable across versions; check the current list with terragrunt --help or the docs.
Recommended in CI from day one — catches regressions before they accumulate.
Migration heuristic
When you see deprecated syntax in a repo, do not mass-rewrite. Instead:
- Set
TG_STRICT_CONTROL=cli-redesignin CI. - Let the failures surface where the bumps would be load-bearing.
- Rewrite per-call as you touch each file.
Mass rewrites churn diffs and risk missing a call site that depends on the old shape (e.g. a wrapper script forwarding TERRAGRUNT_DEBUG).
Config composition
Terragrunt configs compose via include blocks. The merge semantics are the most common source of "but I told it to override that" surprises.
Include blocks
# envs/prod/db/terragrunt.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
expose = true # makes locals/blocks available as include.root.*
}
include "region" {
path = find_in_parent_folders("region.hcl")
expose = true
merge_strategy = "no_merge" # don't merge into the child; only expose
}
merge_strategy values:
- (default — omit the field) — block-aware merge: top-level fields (
inputs,locals,mock_outputs) deep-merge; named blocks (remote_state,generate,terraform) shallow-replace. shallow— same as default for most cases; explicit form.deep— opt-in deep merge of everything including named blocks. Use sparingly; surprising.no_merge— don't merge at all. Useful when you want toexposeparent values without inheriting parent config.
Inputs and mock_outputs deep-merge — the trap
# parent: _envcommon/db.hcl
inputs = {
tags = { team = "platform", env = "prod" }
}
# child: envs/stg/db/terragrunt.hcl
inputs = {
tags = { env = "stg" } # ← child does NOT erase `team`
}
# effective:
# tags = { team = "platform", env = "stg" }
For scalars, child wins at the leaf. For nested maps and lists:
- Scalars in maps: child wins per key.
- Maps: recursively merged, not replaced.
- Lists: concatenated (parent first, then child).
To force a child-only map, three options ordered by preference:
- Compute the map in
locals { }and assigntags = local.tags. Sidesteps the merge entirely. - Restate every key in the child so the merged result equals what you want.
- Set
merge_strategy = "no_merge"on the include — but this drops everything inherited, not just one map.
Setting a key to null in the child does not delete the parent key. HCL deep-merge treats null as a value, not a delete sentinel.
remote_state and generate are shallow
In contrast to inputs, named blocks are shallow:
# parent
remote_state {
backend = "s3"
config = { bucket = "p", key = "x" }
}
# child
remote_state {
backend = "local" # child block replaces parent block entirely; config is lost
}
If you want to inherit most of the parent's remote_state and tweak one field, you usually need to define it once in the parent and not redeclare in children.
Generate blocks
generate writes a file into each unit's working directory before OpenTofu/Terraform runs. Standard uses: backend block, provider block, version pins.
generate "backend" {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
terraform {
backend "s3" {}
}
EOF
}
if_exists values:
overwrite_terragrunt— overwrite only if Terragrunt itself created the file previously. Default. Use this.overwrite— clobber any existing file. Dangerous; can stomp committedbackend.tf.skip— never overwrite. Use when generation is opt-in.error— fail if a file exists.
The silent-overwrite trap: a developer commits a provider.tf for local convenience, then later a parent adds generate "provider" with if_exists = "overwrite". The committed file vanishes on the next terragrunt run plan and nobody notices until a feature it provided breaks.
Dependency wiring
dependency "vpc" {
config_path = "../vpc"
mock_outputs = {
vpc_id = "vpc-fake-12345"
subnet_ids = ["subnet-fake-1", "subnet-fake-2"]
}
mock_outputs_allowed_terraform_commands = ["plan", "validate"]
mock_outputs_merge_strategy_with_state = "shallow"
}
dependencies {
paths = ["../iam-role"] # ordering only, no outputs
}
Key distinctions:
dependencyblock: provides outputs viadependency.vpc.outputs.<x>AND establishes ordering.dependenciesblock: ordering only, no outputs read.
mock_outputs_allowed_terraform_commands — restrict when mocks are allowed. Default: only plan/validate/output need mocks; apply should fail loudly if the dependency hasn't been applied. Don't add apply to this list unless you've thought hard about it.
mock_outputs_merge_strategy_with_state — when state exists but is missing some output keys (e.g., upstream module added new outputs), what to do:
no_merge(default) — if state exists, use state outputs exclusively; missing keys error out.shallow— fill in missing top-level keys frommock_outputs.deep— recursively fill in missing nested keys.
Use shallow when an upstream module adds new outputs faster than downstream units apply. Use no_merge (default) for stricter coupling.
For tracing dependency errors and cache misdirection (Reference to undeclared output, "Module not installed" after a ref bump, mock literals appearing in plan output) → see terraform-workflows/references/terragrunt.md.
run --all orchestration
The mental model: terragrunt run --all <cmd> executes <cmd> in each unit in dependency order. It is NOT a unified plan/apply across units. Each unit's <cmd> is independent.
Safe — read-only operations
terragrunt run --all plan -out=tfplan
terragrunt run --all output
terragrunt run --all validate
terragrunt run --all state list
These can be automated freely.
Unsafe by default — mutations across many units
terragrunt run --all apply # ← per-unit applies; mid-pipeline failure leaves stack inconsistent
terragrunt run --all destroy
The safer pattern:
terragrunt run --all plan -out=tfplan— writes a plan file in each unit's.terragrunt-cache/.../tfplan.- Aggregate the blast-radius summary across units (e.g.
terragrunt find --dag --jsonto enumerate; parse each unit's plan). - Human review — destroys, recreates, IAM/policy changes.
- Either
terragrunt run --all apply tfplan(applies the saved plans, no replan) OR targeted per-unit applies for changed units only.
Useful flags
--parallelism <N>— limit concurrent unit operations. Default is high; lower it for rate-limited backends.--queue-include-dir <pattern>/--queue-exclude-dir <pattern>— scope which units run, useful for partial rollouts.--queue-include-units-reading <file>— include units that read a specific config file. Targets the blast radius of a config change.--ignore-dependency-errors— continue past failed units. Usually wrong forapply; sometimes right forplanwhen you want to see all failures at once.
Hooks
terraform {
before_hook "auth_refresh" {
commands = ["apply"] # scoped tight — plan reuses cached creds
execute = ["aws", "sso", "login", "--no-browser"]
}
after_hook "tag_state_bucket" {
commands = ["apply"]
execute = ["bash", "-c", "echo applied at $(date) >> /tmp/applies.log"]
run_on_error = false
}
error_hook "alert" {
commands = ["apply"]
execute = ["bash", "-c", "curl -X POST $SLACK_WEBHOOK -d '{\"text\":\"apply failed\"}'"]
on_errors = [".*"]
}
}
Key rules:
commandsis an inclusion list. A hook withcommands = ["apply"]does not fire onplan.run_on_errordefaults totrueforafter_hook— after-hooks fire even when the command failed. Setfalseif the hook should only run on success.error_hookfires only on error; pattern-match withon_errors(regex strings).- Hook commands run in the unit's working directory by default. Set
working_dirif you need otherwise. - Hooks are NOT executed inside the OpenTofu/Terraform process — they're spawned by Terragrunt. Exit codes propagate (a failing
before_hookaborts the command).
Anti-pattern: a before_hook "auth" that does aws sso login interactively. Fine on a laptop, blocks forever in CI. Gate with if:
before_hook "auth" {
commands = ["plan", "apply"]
execute = ["bash", "-c", "[ -z \"$CI\" ] && aws sso login || true"]
}
Stacks (terragrunt.stack.hcl)
The newer feature for generating units from templates. A terragrunt.stack.hcl file declares units; terragrunt stack generate materializes them under .terragrunt-stack/.
# envs/prod/svc/terragrunt.stack.hcl
unit "vpc" {
source = "git::[email protected]:acme/infra-catalog.git//units/vpc?ref=v1.0.0"
path = "vpc"
values = {
vpc_name = "main"
cidr = "10.0.0.0/16"
}
}
unit "database" {
source = "git::[email protected]:acme/infra-catalog.git//units/database?ref=v1.0.0"
path = "database"
values = {
engine = "postgres"
vpc_path = "../vpc"
}
}
Run:
terragrunt stack generate # materialize units into .terragrunt-stack/
terragrunt stack run plan # plan all units in the stack
terragrunt stack run apply tfplan # apply saved plans
Trap: .terragrunt-stack/ is generated. Do NOT edit files inside it. Edit the template unit modules (the source = "..." targets) or the terragrunt.stack.hcl values.
no_dot_terragrunt_stack = true on a unit prevents generation into the .terragrunt-stack/ subdirectory — useful for preserving path_relative_to_include() behavior during migrations from hand-authored unit directories.
When to reach for a stack:
- Many near-identical units (e.g. one per region, one per tenant).
- Catalog pattern where unit definitions live in a separate repo and consumers compose stacks from them.
When NOT to use stacks:
- Each unit is meaningfully different. Per-unit
terragrunt.hclstays clearer. - A small infra (under ~10 units). The generation step is overhead.
Anti-patterns checklist
- ❌ Editing files inside
.terragrunt-cache/or.terragrunt-stack/. Always wrong — regenerated on next run. - ❌
terragrunt run --all applyfrom CI without per-unit plan aggregation. - ❌
--terragrunt-source-update,TERRAGRUNT_NON_INTERACTIVE, orrun-allin new code — use the redesigned CLI (--source-update,TG_NON_INTERACTIVE,run --all). - ❌ Setting a parent input to
nullin a child config expecting it to delete. Uselocals { }or restate the whole map. - ❌
generateblock withif_exists = "overwrite"over a committed*.tffile the developer doesn't realize is at risk. - ❌ Adding
"apply"tomock_outputs_allowed_terraform_commandsto "make CI green." It hides unapplied dependencies. - ❌
before_hookthat auths interactively without a CI guard. - ❌ Mass-rewriting deprecated CLI calls in one PR. Migrate per-touch under
TG_STRICT_CONTROL.
Cross-skill notes
- For Terraform-level concerns (plan review, drift, state surgery, provider upgrades, multi-cloud identity verification), see the
terraform-workflowsskill — its cross-cutting rules apply on top of this skill's. - For
.terragrunt-cache/<hash>/<hash>/error tracing and dependency-DAG inspection commands, seeterraform-workflows/references/terragrunt.md. - For Kubernetes resources managed via Terragrunt with potential Argo overlap, consult the
argocd-operationsskill on ownership posture.