Claude
Skills
Sign in
Back

terragrunt-workflows

Included with Lifetime
$97 forever

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`.

General

What this skill does


# 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](#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-all` deprecated, "unknown command" for what used to be `terragrunt plan`.
- An `include` block isn't producing the merged config you expected.
- A child unit's `inputs` map seems to keep parent keys you tried to drop.
- A `dependency` block's `mock_outputs` produces values the actual upstream never emits.
- `terragrunt run --all apply` is on the table for a production rollout.
- A `terragrunt.stack.hcl` file 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.

1. **Treat the CLI redesign as the default.** Even if a repo still uses `run-all` and `--terragrunt-*` flags, write new code in the new form (`run --all`, `--<flag>`, `TG_*` env vars). Set `TG_STRICT_CONTROL=cli-redesign` locally so you can't accidentally regress.
2. **`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](#run---all-orchestration).
3. **Never edit a generated file.** Files inside `.terragrunt-cache/` and `.terragrunt-stack/` are derived. Editing them produces drift that vanishes on the next run.
4. **Hooks run on every invocation.** A `before_hook` with `commands = ["plan", "apply"]` runs both during `plan` and `apply`. If a hook performs auth refresh or state pre-fetch, it costs that work twice on a plan→apply cycle. Scope hooks tightly.
5. **`mock_outputs` are 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 units
- `terragrunt find` — discover units (replaces ad-hoc `find` for `terragrunt.hcl`); supports `--dag --json` for dependency order
- `terragrunt list` — list units with optional `--dag --tree`
- `terragrunt stack {generate,run,output,clean}` — manage `terragrunt.stack.hcl` files
- `terragrunt 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:

1. Set `TG_STRICT_CONTROL=cli-redesign` in CI.
2. Let the failures surface where the bumps would be load-bearing.
3. 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

```hcl
# 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 to `expose` parent values without inheriting parent config.

### Inputs and mock_outputs deep-merge — the trap

```hcl
# 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:

1. Compute the map in `locals { }` and assign `tags = local.tags`. Sidesteps the merge entirely.
2. Restate every key in the child so the merged result equals what you want.
3. 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:

```hcl
# 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.

```hcl
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 committed `backend.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

```hcl
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_stra

Related in General