✨ New Features
duplicate-dependency-labels strict control
Declaring two dependency blocks with the same label in one terragrunt.hcl configuration file parsed without error, and then quietly resolved every reference to that label to whichever block came last. The blocks before it were silently overridden:
dependency "vpc" {
config_path = "../vpc-us-east-1"
}
dependency "vpc" {
config_path = "../vpc-us-west-2"
}
inputs = {
# Reads ../vpc-us-west-2.
vpc_id = dependency.vpc.outputs.vpc_id
}Terragrunt now warns when it finds this. With the new duplicate-dependency-labels strict control enabled, the warning becomes an error naming the address the blocks share:
terragrunt run plan --strict-control duplicate-dependency-labels/path/to/terragrunt.hcl: dependency vpc is declared more than once; every dependency needs an address of its own
Give each block a label of its own. A configuration that was relying on the shadowing to pick the last block should keep only that block.
scaffold asks for values interactively
Scaffolding from the command line wrote # TODO placeholders for every input and left you to fill them in by hand, while scaffolding the same component from the Catalog TUI opened a form and collected them. terragrunt scaffold now opens that same form:
terragrunt scaffold github.com/gruntwork-io/terragrunt-infrastructure-modules-example//modules/mysqlFor a module or a template it lists the source's variables; for a unit or a stack it lists the values.* references its configuration makes, which are written to terragrunt.values.hcl. Dismissing the form with esc writes nothing.
The form is skipped, and the placeholders written as before, when you pass --non-interactive, when stdin is not a terminal, or when the source asks for nothing. A scaffold in a CI job, or one run by another program, therefore behaves exactly as it did.
See Scaffold for the full behavior, and the form's keybindings for driving it.
🏎️ Performance Improvements
Faster startup when --tf-path is not set
When you don't set --tf-path, Terragrunt picks the binary it wraps by looking for tofu on your PATH and falling back to terraform when it isn't there. Terragrunt used to make that choice by running tofu -version, which meant launching a process at the start of every command, including commands like find and list that never run the binary. That process launch is gone, and a terragrunt --version benchmark runs roughly 1.7x faster as a result.
This changes what happens when tofu is on your PATH but can't run: Terragrunt now selects it and reports the failure rather than silently falling back to terraform. Set --tf-path or TG_TF_PATH to pick the binary yourself.
🐛 Bug Fixes
Autoinclude dependency overrides no longer evaluate replaced paths
Terragrunt used to evaluate a dependency's original config_path before applying a sibling autoinclude override. This could prevent a unit from being parsed when the original path referenced a value that the unit no longer supplied, even though the autoinclude replaced that path. Terragrunt now leaves replaced dependency blocks undecoded, then applies the autoinclude override. Dependency blocks without an autoinclude override are still validated.
Blocks that use expansion are still decoded, because a bare autoinclude label does not name their instances. If the autoinclude also declares the same label without expansion, Terragrunt reports a dependency label collision.
Fixed Git sources with a depth query parameter
A terraform.source (or stack source) URL carrying the go-getter depth query parameter, such as ...vpc.git?depth=1&ref=v5.21.0, failed to download since v1.1.0, when the CAS became the default path for Git sources. Terragrunt lifted ref out of the URL but left depth in place, so git received ...vpc.git?depth=1 and rejected it as an invalid repository name. A URL with depth and no ref hit the same failure.
Terragrunt now strips depth, with or without a ref, before invoking git, so these sources download again. The clone depth itself always comes from --cas-clone-depth, which defaults to 1; a depth on a source URL is never applied for CAS clones.
CAS handles local sources that have already been initialized
With CAS enabled, reading a local source that had already been initialized failed and fell back to the slower standard copy. Generating a stack from such a unit logged CAS processing failed ... source escapes repository root.
Provider caching was the cause. Both the Provider Cache Server and the Automatic Provider Cache Dir leave the plugins under .terraform pointing into a shared cache outside the source. CAS read those links as the source reaching outside itself and refused to copy the link for safety.
CAS now leaves .terraform and .terragrunt-cache out of local sources, keeping .terraform.lock.hcl and everything else. OpenTofu, Terraform, and Terragrunt rebuild both directories on demand, so units and stacks no longer receive a stale copy of either. Running tofu init in a source directory no longer changes that source's CAS key.
Fixed the signal sent to a running command during shutdown
On Windows, when a failure rather than Ctrl+C cancelled a run, Terragrunt crashed with a nil pointer panic instead of stopping the command it had started. It now terminates the command, which is the closest thing Windows offers to an interrupt.
On every platform, when a command exited on its own during the grace period after Ctrl+C, Terragrunt could still send it the signal and then log a forwarding error against a process that was already gone.
terraform_binary respected when reading dependency outputs
Reading a dependency block's outputs ignored the terraform_binary of the unit being read and fell back to the auto-detected binary, which is OpenTofu whenever tofu is on your PATH. With terraform_binary = "terraform", a unit ran through Terraform while the dependency it consumed was read through OpenTofu. A run --all over units that each worked on their own then failed with a backend initialization error, followed by a misleading There is no variable named "dependency".
Dependency outputs are now read through the binary the dependency itself configures, so a unit's terraform_binary applies wherever its state is read. --tf-path and TG_TF_PATH still take precedence over the config value.
Numbers with extreme exponents fail fast instead of stalling
A number literal such as 9E9999999 in inputs, locals, or a dependency block's mock_outputs used to cost over a minute of CPU on a single unit. Written out in decimal that number is ten million digits long, and terragrunt render --format=json produced every digit before failing with a ten megabyte error message.
Terragrunt now rejects numbers larger than 1e4096, and non-zero numbers smaller than 1e-4096, before it tries to write them out, and names the attribute holding the value:
count: number is outside the supported range of 1e-4096 to 1e4096
Numbers inside that range are unaffected.
Registry credentials are no longer copied into the generated CLI config
When the Provider Cache Server is enabled, Terragrunt writes a CLI config for OpenTofu/Terraform into each unit's working directory, based on your own CLI config. That generated file used to include a copy of every credentials block from your config, including the ones for registries Terragrunt routes through the cache server.
Those copies were never read. For a routed registry, Terragrunt sets the matching TF_TOKEN_<hostname> environment variable, which takes precedence over a credentials block, and the cache server presents your real credentials when it contacts the registry on your behalf. The generated file now leaves the block out for those registries, so your token stays in the CLI config you put it in instead of being duplicated somewhere it had no effect.
Credentials for hosts the cache server does not route are unchanged, since OpenTofu/Terraform contacts those directly and still reads them from the generated config.
Upgrading does not rewrite the files an earlier version already generated. Each is named .terraformrc and sits in a unit's working directory, which is under .terragrunt-cache for remote sources. Delete those files, or clear the cache, to get the copied credentials off disk.
Generated files are readable only by the user who ran Terragrunt
Terragrunt created several files and directories that other users on the same machine could read:
- The CLI config Terragrunt writes for OpenTofu/Terraform when the Provider Cache Server is enabled, and the directory holding it.
- The JSON plan files written to
--json-out-dir, and that directory. - The directories holding the plan files written to
--out-dir. - The config written by
render --write, which holds the resolved values ofinputs,locals, anddependencyoutputs.
Terragrunt now creates those files as 0600 and those directories as 0700.
hcl fmt --stdin honors --check and --diff
terragrunt hcl fmt --stdin ignored --check and --diff. It printed the reformatted HCL and exited 0 whether or not the input needed formatting.
--check now exits with status code 1 when the input needs formatting, and --diff prints a unified diff labeled old/stdin and new/stdin. Neither flag prints the formatted content, so getting that content back means running --stdin without them.
hcl validate no longer crashes on errors that carry no source location
terragrunt hcl validate crashed while formatting its output when one of the errors it found had no position in the configuration. Terragrunt now prints that error's summary and detail, without a location line.
Fixed the deprecated environment variables for hcl validate
TG_HCLVALIDATE_STRICT_VALIDATE, the deprecated name for --strict, also turned on --show-config-path. --strict only takes effect alongside --inputs, and --show-config-path cannot be combined with --inputs. With that variable set, terragrunt hcl validate --inputs failed with specifying both -show-config-path and -inputs is invalid.
TG_HCLVALIDATE_SHOW_CONFIG_PATH, the deprecated name for --show-config-path, was not recognized at all.
TG_HCLVALIDATE_STRICT_VALIDATE now sets only --strict, and TG_HCLVALIDATE_SHOW_CONFIG_PATH sets --show-config-path. TG_STRICT_VALIDATE, TERRAGRUNT_STRICT_VALIDATE, and TERRAGRUNT_HCLVALIDATE_SHOW_CONFIG_PATH are unchanged.
Fixed panic on invalid if_disabled value with include block
A generate block with an invalid if_disabled value combined with an include block caused a nil pointer panic instead of a descriptive error. Terragrunt now returns an error naming the generate block and the invalid value, consistent with if_exists validation.
OCI sources reject Docker-style :tag suffixes instead of fetching latest
An oci:// source that pinned a version with a Docker-style suffix, like oci://ghcr.io/acme/modules/vpc:1.0.0, silently ignored the suffix and resolved the latest tag, so a run could fetch a different module version than the one pinned. Terragrunt now validates the registry and repository the same way OpenTofu does and rejects such sources with an error that shows the source rewritten in the supported ?tag=/?digest= form, for example oci://ghcr.io/acme/modules/vpc?tag=1.0.0. Repository names that violate the OCI reference grammar are also rejected before any registry is contacted.
Prompts accept a piped answer that has no trailing newline
Piping an answer to a confirmation prompt, as in printf yes | terragrunt run --all destroy, failed with an EOF error because Terragrunt discarded a final answer that ended without a newline. Terragrunt now reads that final answer, and only a prompt that gets no input at all reports EOF.
Provider cache supports signed provider download URLs
When a provider mirror returned a signed download URL, the Provider Cache Server used the entire URL, including its query string, as the archive filename. Long authentication parameters could exceed filesystem filename limits and fail with file name too long.
Terragrunt now derives the archive filename only from the URL path while preserving the query string when downloading it. Signed provider URLs, including archives in nested object paths and relative mirror URLs, now download and cache correctly.
find and list reject a --queue-construct-as value that holds no command
A value made only of shell punctuation, such as terragrunt find --queue-construct-as=';', ended the run with a crash report. A value that quotes an empty command, such as --queue-construct-as='""', was accepted even though it names no command.
find and list now exit with an error that repeats the value you passed and shows what --queue-construct-as expects instead.
render --write picks a default filename without a format flag
terragrunt render --write failed with is a directory unless it was paired with --format or --json. Only those flags set the default filename, so a bare --write had no output path and Terragrunt tried to write to the unit directory itself.
The default now follows the format in use. terragrunt render --write writes terragrunt.rendered.hcl next to the unit configuration, and --json or --format=json writes terragrunt.rendered.json. An explicit --out still takes precedence.
sops_decrypt_file now uses the credentials your auth provider supplies
When a run obtained credentials from --auth-provider-cmd, sops_decrypt_file ignored them for any variable already set in the environment Terragrunt started with. The rest of the run honored the auth provider, and correctly overrode any ambient environment variables. OpenTofu/Terraform received those credentials, and so did the AWS calls Terragrunt makes on a unit's behalf, such as get_aws_account_id.
Decryption now runs as the identity Terragrunt resolved for the unit, the same one the rest of the run uses, regardless of ambient environment variables.
info strict list <name> now honors --all
Passing a control name to info strict list shows that control's subcontrols. Unlike the top-level listing, it ignored the --all flag and always included completed subcontrols.
Terragrunt now applies the same rule when you name a control.
String inputs reach modules with ${...} intact
Passing a string input that contains ${...} to a variable declared with a type other than string used to fail with Variables not allowed, because OpenTofu/Terraform parse those values as HCL expressions and read ${...} as an interpolation. Reading a JSON or YAML file into an input hit this whenever the file happened to contain that sequence:
inputs = {
config = file("./config.json")
}Terragrunt now escapes interpolation sequences in string inputs when the module declares the variable with a type that makes the value parse as HCL, so ${...} arrives as literal text instead of failing the run. Variables declared as string, and variables declared with no type at all, are read verbatim by OpenTofu/Terraform, and their values are still passed through untouched.
🧪 Experiments Updated
Read dependency outputs directly from Azure state
The dependency-fetch-output-from-state experiment can now read dependency outputs directly from Azure Storage (azurerm) state, in addition to S3. This avoids initializing the dependency and running tofu output or terraform output.
Azure direct reads require the azure-backend experiment as well. Unsupported configurations requiring native-only authentication, endpoint, timeout, or customer-provided-key behavior continue to use the native output path.
When a dependency has no state yet, Terragrunt uses that dependency block's mock_outputs, as it already does for S3. When Azure direct reads resolve a storage account key through Azure Resource Manager, which is the case unless access_key, sas_token, or use_azuread_auth is set, a resource_group_name, storage_account_name, or subscription_id naming a resource that does not exist fails with an error naming those keys rather than substituting mock outputs.
Read dependency outputs directly from GCS state
The dependency-fetch-output-from-state experiment can now read dependency outputs directly from GCS state, in addition to S3. This avoids initializing the dependency and running tofu output or terraform output.
Unsupported GCS configurations continue to use the native output path. When a dependency has no state yet, Terragrunt uses that dependency block's mock_outputs, as it already does for S3.
Thanks to @joshmyers for the original GCS implementation.
render previews what an expanded dependency block expanded to
With the block-iteration experiment enabled, a dependency block that carries an expansion block now renders as it was written, followed by the elements it expanded into, commented out and with their bodies resolved:
$ terragrunt render --experiment block-iteration
dependency "aurora" {
expansion {
for_each = toset(["web", "api"])
}
config_path = "../aurora-${each.key}"
}
# Expands to:
#
# dependency "aurora" {
# config_path = "../aurora-api"
# }
#
# dependency "aurora" {
# config_path = "../aurora-web"
# }The elements are comments because they aren't valid Terragrunt HCL configurations (you are not allowed to use the same dependency label twice in Terragrunt configurations), the previews are there to help you understand how expansion will resolve.
⚙️ Process Updates
Go bumped to v1.27
The version of Golang used to compile the Terragrunt binary has been updated from v1.26.6 to v1.27.0.
If you build Terragrunt from source, or import it as a Go module, you now need a Go 1.27 toolchain.
OpenTelemetry SDK updated to v1.45.0
Terragrunt's OpenTelemetry tracing and metrics dependencies have been updated from v1.44.0 to v1.45.0. The logging packages and exporters have also been updated to their compatible releases, and Terragrunt now uses the v1.43.0 semantic conventions.
Telemetry behavior is unchanged.
Pull Requests
✨ Features
- feat: Adding interactive scaffold form by @yhakbar in #6615
- feat(azure): End to end Azure CICD by @denis256 in #6574
- feat: Adding bare
enabledto unit and stack blocks by @yhakbar in #6714 - feat: Keying
stack outputaddresses by iteration key by @yhakbar in #6715 - feat: Adding render preview for expansion by @yhakbar in #6737
- feat: remote state reading for GCP and Azure by @denis256 in #6710
🐛 Bug Fixes
- fix: Strip credentials before local CLI config write by @yhakbar in #6678
- fix: Tightening file permissions for generated files by @yhakbar in #6675
- fix: Reject oci:// sources with docker-style name suffixes instead of resolving latest by @denis256 in #6696
- fix: Escape interpolation in string inputs when type is verified by @yhakbar in #6685
- fix: Fixing local copies when symlinks exist from provider caching by @yhakbar in #6684
- fix(provider-cache): fixed handling arguments in urls by @denis256 in #6680
- fix(cas): strip go-getter
depthquery parameter before invoking git by @HalisCz in #6513 - fix(cas): use venvtest helper in depth query param tests by @denis256 in #6712
- fix(test): fix for failing test TestNewSignalsForwarderMultipleUnix by @denis256 in #6713
- fix: prevent nil pointer panic on invalid if_disabled by @denis256 in #6718
- fix: Fixing
--queue-construct-asresulting in empty tokenization by @yhakbar in #6720 - fix: Fixing
render --writewhen no--formatis supplied by @yhakbar in #6724 - fix: Preventing extremely small or large float exponents from crashing Terragrunt by @yhakbar in #6727
- fix: Handling situation when diagnostics contain nil range by @yhakbar in #6729
- fix: Addressing nil
RangeandSnippetinSourceSnippetsby @yhakbar in #6731 - fix: Propagating flag parse errors instead of swallowing them by @yhakbar in #6732
- fix: Use the appropriate absolute path to unit config file, not basename when checking version constraints by @yhakbar in #6728
- fix(security): upgrade Go to 1.27 by @denis256 in #6736
- fix(test): pass config fixture to setupTest by @denis256 in #6738
- fix: Fixing
info strict listwithout--allby @yhakbar in #6725 - fix: Fixing deprecated
TG_HCLVALIDATE_STRICT_VALIDATEenv var by @yhakbar in #6730 - fix: Fixing
hcl fmtwith--stdincombined with--checkand/or--diffby @yhakbar in #6726 - fix: autoinclude config path fixes by @denis256 in #6711
- fix: Normalize paths using
ToSlashto hande old/new prefix appropriately by @yhakbar in #6744 - fix: Fixing tofu/terraform binary selection for
run --allusage by @yhakbar in #6753 - fix: Updating
render --writefile permissions by @yhakbar in #6756
🏎️ Performance
📖 Documentation
- docs: give each environment its own state backup path in the Terralith guide by @yhakbar in #6683
- docs: serve the Google tag container first-party by @ZachGoldberg in #6699
- docs: proxy CORS-less GTM tags so they load under Partytown by @ZachGoldberg in #6704
- docs: Clean-up for v1.1.4 changelog by @yhakbar in #6747
✅ Tests
🤖 CI
- ci: Add weekly security scans by @denis256 in #6691
- ci(coverage): added support for test suppressions in weekly test report by @denis256 in #6752
🧹 Chores
- chore: tests coverage increase by @denis256 in #6668
- chore(deps): update aws-sdk-go-v2 by @denis256 in #6655
- chore: runner pool test coverage by @denis256 in #6647
- chore: A lot more venv plumbing by @yhakbar in #6644
- chore(deps): bump @astrojs/vercel from 11.0.0 to 11.0.3 in /docs by @dependabot[bot] in #6658
- chore: docs sync by @denis256 in #6676
- chore: venv plumbing for cloud SDKs by @yhakbar in #6645
- chore: Plumbing venv into cloud getters by @yhakbar in #6650
- chore(deps): update OpenTelemetry SDK to v1.45.0 by @denis256 in #6686
- chore: Resolve env var defined flags from venv by @yhakbar in #6652
- chore: Increasing discovery boundary test coverage by @yhakbar in #6671
- chore: Wire expansion into dependency parse by @yhakbar in #6679
- chore: Virtualizing util.file.go functions by @yhakbar in #6653
- chore: Upgrading go to 1.26.6 by @yhakbar in #6697
- chore: Nesting the dependency cty map by iteration key by @yhakbar in #6689
- chore(deps): bump github.com/moby/go-archive from 0.2.0 to 0.3.0 by @dependabot[bot] in #6705
- chore: Increasing venv coverage further by @yhakbar in #6677
- chore: Adding integration coverage for expanded dependencies by @yhakbar in #6693
- chore: Use local catalog for
TestCatalogWithLocalDefaultTemplateby @yhakbar in #6700 - chore: Updating
TestNewSignalsForwarderMultipleUnixto actually check for the signal by @yhakbar in #6701 - chore: Use an injectable cap in
TestPartialEval_DeeplyNestedExpressionReturnsTypedErrorby @yhakbar in #6702 - chore: Moving the
TestDiscovery_GraphConcurrentConfigAccessWithRacingin-memory by @yhakbar in #6703 - chore: Wiring expansion into the unit and stack parse by @yhakbar in #6694
- chore: Completing venv abstraction by @yhakbar in #6698
- chore: addressing PR #6736 comemtns by @denis256 in #6741
- chore: Adding sandboxed unit tests in CI by @yhakbar in #6742
- chore: Isolate
TestDependencyOutputSkipDependencyOutputsFlagfixtures by @yhakbar in #6740 - chore: Preventing flakes from
TestNewSignalsForwarderMultipleUnixby @yhakbar in #6750 - chore: Adding FS sandboxed unit tests in CI by @yhakbar in #6754
- chore: Running
go fix ./...by @yhakbar in #6758