Releases: gruntwork-io/terragrunt
Release list
v1.1.4
✨ 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 di...
v1.1.3
🐛 Bug Fixes
Fixed Unsupported attribute errors for values.* inputs that autoinclude overrides
A unit input referencing a values.* key that the unit's values file doesn't define no longer fails with Unsupported attribute when an autoinclude block supplies that input. The autoinclude value is applied as intended.
# stacks/terragrunt.stack.hcl
unit "subnet" {
source = "../units/subnet"
path = "subnet"
autoinclude {
dependency "vpc" {
config_path = unit.vpc.path
mock_outputs = { vpc_id = "mock" }
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
}
}
values = {
cidr_block = "10.0.0.0/24"
}
}# units/subnet/terragrunt.hcl
inputs = {
vpc_id = values.vpc_id # supplied by autoinclude, not the values file
cidr_block = values.cidr_block # still resolves from values file
}Fixed overwrite_terragrunt and remove_terragrunt on files with no trailing newline
generate blocks using if_exists = "overwrite_terragrunt" or if_disabled = "remove_terragrunt" failed to properly handle existing files when the file at the target path had no newline after its first line, empty files included.
Terragrunt now properly handles files like this, so a file carrying the Terragrunt signature is overwritten or removed as configured, and a file without it produces the usual error naming the path Terragrunt would not touch.
Dependency mock_outputs apply when the state bucket doesn't exist yet
When reading a dependency's outputs directly from remote state (--dependency-fetch-output-from-state), Terragrunt fell back to mock_outputs only when the state object was missing, not when the S3 bucket itself didn't exist. A dependency on an environment that hadn't been bootstrapped yet would fail instead of using its mocks.
A missing bucket is now treated the same as a missing state object, so commands like plan and validate can resolve mocks before the dependency's backend has been created.
Source permissions preserved on hidden directories copied by include_in_copy
With the fast-copy strict control enabled, a hidden directory that Terragrunt copied due to include_in_copy matching something within it took the permissions of the first file generated within it, instead of the permissions it had in the source.
Those directories now keep their source permissions, matching the copy Terragrunt performs with the control disabled.
Applied the positive half of a filter that begins with a negation
When a --filter query began with a negation, Terragrunt treated the whole query as an exclusion. The expressions chained after the negation stopped restricting the selection and only narrowed what got subtracted, so components matching none of them came back in the results. Those expressions are now applied.
$ terragrunt list
bar baz foo$ terragrunt list --filter '!name=foo | name=bar'
bar baz foo$ terragrunt list --filter '!name=foo | name=bar'
barThis follows the left-to-right refinement that | has everywhere else: each expression narrows what the one before it selected. A query is only treated as an exclusion when every one of its expressions is negated, such as '!name=foo' or '!name=foo | !name=bar'.
See Combining Expressions for how negation, intersection and union interact.
Fixed a race condition that left cached provider archives in the working directory
With the provider cache server enabled via --provider-cache, a race let the server start responding to requests before it had finished preparing the directories it caches into. A provider requested in that window had its archive and lock file written relative to the working directory instead of into the cache, leaving zip files behind in your project.
That race condition has been fixed. Providers now always download into the cache directory.
Fixed a race condition between concurrent Terragrunt runs downloading providers
A race condition in the logic used to synchronize provider downloads meant that two Terragrunt runs on the same machine could interfere with each other while caching the same provider. Each run staged its downloads at the same path, so a run that finished first could delete an archive another run was still unpacking, failing that run with failed to open zip archive.
That race condition is now fixed. Two runs can cache the same provider at the same time.
Fixed space-delimited flag values in providers lock
The space-delimited form, providers lock -platform linux_amd64, now reaches OpenTofu and Terraform intact. Previously it was the attached form, -platform=linux_amd64, that worked: given the value as a separate argument, Terragrunt moved it to the end of the command, where it was read as a provider address and the run failed with Invalid provider type "linux_amd64".
-fs-mirror and -net-mirror were moved the same way, and now keep their values too.
With --provider-cache enabled, platforms are also split correctly across the per-platform providers lock runs used to warm the cache.
Fixed scaffold on units and stacks
terragrunt scaffold read every source as an OpenTofu/Terraform module. Given a unit or a stack, which are Terragrunt configurations rather than OpenTofu/Terraform modules, it exited successfully having written an invalid terragrunt.hcl file.
Units and stacks are now scaffolded the way the Catalog TUI scaffolds them: their files are copied into the working directory for you to edit in place, along with a terragrunt.values.hcl listing every values.* reference the configuration makes.
terragrunt scaffold 'github.com/gruntwork-io/terragrunt-scale-catalog//units/aws/oidc/iam-oidc-role'Copying refuses to overwrite: a file that would land on an existing path stops the command before anything is written. Modules and templates are unaffected and are still scaffolded from their variables.
See Scaffold for what gets copied and how the values file is filled in.
Answered every prompt when input is piped in
A run that asks for confirmation more than once, such as terragrunt backend delete prompting for both the lock table entry and the state object, used to read only the first answer when the answers were piped in rather than typed. The remaining answers were discarded while reading ahead, and the next prompt failed with an end-of-input error. Every prompt in a run now reads from the same input, so piping yes for each one works.
Stack dependencies honor mock_outputs with --dependency-fetch-output-from-state
A dependency block that reads outputs from a stack (its config_path points at a terragrunt.stack.hcl directory) used to fail when a unit in that stack had no state yet, even when the dependency declared mock_outputs. This blocked commands like plan and validate against a stack that hadn't been applied.
Such a dependency now falls back to mock_outputs for the units that have no state yet. In a partially applied stack, applied units resolve to their real outputs while the rest use their mocks.
Mocks for a stack dependency are keyed by unit name, so mock_outputs has to be a map or object. Declaring it as any other type now reports that directly, instead of leaving the units it can't cover out of the stack outputs.
Fixed --config= being ignored by the tflint hook
The built-in tflint hook reads the configuration file out of the arguments you give it, then uses that path for tflint init and for the lint run. It only recognized the space-separated --config <path> spelling, so a hook written as:
before_hook "tflint" {
commands = ["plan"]
execute = ["tflint", "--config=custom.tflint.hcl"]
}was treated as though no configuration file had been named at all. Terragrunt searched the unit directory and its parents for a .tflint.hcl file instead, and either failed with a config-not-found error or ran tflint init against whatever unrelated configuration the search turned up. Terragrunt now recognizes --config <path>, --config=<path>, -c <path>, and -c=<path>.
The hook also builds --var arguments from the unit's inputs and from TF_VAR_ entries in extra_arguments blocks. Those arguments came out in a different order on every run, which made the logged command line, and anything comparing it between runs, needlessly unstable. They are now ordered by variable name.
🧪 Experiments Added
block-iteration experiment reserves the expansion block
The block-iteration experiment has been added as the gate for iterating a dependency, unit, or stack block over a count or for_each, declared through a nested expansion block, along with an enabled attribute on unit and stack blocks.
In this release the flag is reserved only, and enabling it has no behavioral effect. Writing an expansion block without the experiment now reports an error naming the flag, rather than leaving the block to be silently discarded:
the unit "app" block in /path/to/terragrunt.stack.hcl uses an expansion block, which requires the 'block-iteration' experiment; enable it with --experiment block-iteration
Track progress and share feedback in #4504.
bounded-discovery — Added a directory boundary for graph traversal
Filter expressions that traverse the dependency graph reach beyond the wo...
v1.1.2
✨ New Features
Scaffold straight from the catalog README view with ctrl+d
In the terragrunt catalog TUI, pressing ctrl+d while reading a component's README now scaffolds it immediately, skipping the interactive form. Module and template inputs are written as # TODO placeholders, and unit/stack copies get a fully placeholder terragrunt.values.hcl. The hint bar at the bottom of the README view advertises the new key.
🏎️ Performance Improvements
Fewer filesystem checks when resolving find_in_parent_folders()
find_in_parent_folders() walks up from a unit toward the filesystem root, checking each directory for the configuration file it was asked to find. Even when the call named a file, as in find_in_parent_folders("root.hcl"), each directory along the way was also checked for the default configuration filenames. Units sharing a parent chain then repeated every check their siblings had already made.
Terragrunt now checks only the filename the call names, and reuses what it already learned about a directory for the rest of the command. Deeply nested estates benefit most, since every level between a unit and its root configuration used to be re-checked once per unit.
In micro-benchmarks, resolving the root configuration for 100 units nested eight directories deep went from 4.8ms to 0.49ms. Across the benchmarked shapes the lookups run between 7x and 10x faster, and the time saved grows with both the number of units and how deeply they sit below their root configuration.
🐛 Bug Fixes
Fixed roles assuming themselves for backend operations
A regression in v1.1.1 broke setups that provide static AWS credentials and configure a role via the iam_role attribute, the --iam-assume-role flag, or TG_IAM_ASSUME_ROLE.
In those setups, Terragrunt assumes the role once at the start of a run, and every later AWS call uses that role session. In v1.1.1, backend operations like bootstrapping the state bucket started performing an extra role assumption of their own. Since the run was already using the role session at that point, the role tried to assume itself, and AWS rejected the call with an AccessDenied error unless the role's trust policy happened to include the role itself.
Backend operations now reuse the role session from the start of the run, as they did before v1.1.1.
This does not affect the assume_role attribute of the remote_state block. Roles configured there are backend-specific and are still assumed on top of the supplied credentials, so the cross-account role assumption should continue to work as expected.
Local sources no longer re-init when uncopied files change
For units with a local source, Terragrunt decides whether the cached copy is stale by hashing the source directory. That hash previously covered every file in the directory, including hidden files and exclude_from_copy matches that are never copied into the cache. Creating or touching such a file (an editor swap file, a scratch note) changed the hash, forcing a needless re-copy and auto-init on the next run.
The hash now covers only the files a copy would deliver, honoring the default hidden-file rule along with include_in_copy and exclude_from_copy. Files that never reach the cache no longer trigger re-initialization.
Fixed width truncation of colored and multi-byte log content
The width option in a custom log format sizes a column to a fixed number of visible characters. When the content held color codes or multi-byte characters and was longer than the column, truncation cut the raw bytes: it could slice through the middle of a color code, leaving color bleeding into the rest of the line, or split a multi-byte character into invalid output, and it dropped more visible text than the configured width.
width now measures and cuts by visible characters. Color codes are preserved intact, multi-byte characters are never split, and the column keeps exactly the requested number of visible characters.
Provider cache downloads now require a secret URL
The Provider Cache Server now hardens the download endpoint that fetches provider archives on the caller's behalf. That endpoint attaches whatever registry credentials are configured for the upstream host, and it was the only one on the server that did not require the token generated for the run, so any other process on the machine could use a running cache server to pull artifacts from a private registry with the credentials of whoever started the run.
The download URLs handed to OpenTofu and Terraform now carry a secret path segment, generated fresh each time the cache server starts and redacted from the server's own logs. Requests that omit the segment get a 404.
Run report no longer mangles the names of paths that share a prefix with the working directory
When a run's path shared a string prefix with the working directory without being nested under it, the run report shortened its name by shearing off the prefix mid-segment. A working directory of /repo/project alongside a run at /repo/project-staging/unit produced the name -staging/unit.
The report now shortens a path only when it is genuinely nested under the working directory. Sibling paths keep their full name.
Feature flag defaults no longer leak between units in run --all
A feature block's default was recorded once per run and shared by every unit. During run --all, the first unit to be parsed set the value for a flag name, so a unit defining default = false could evaluate feature.toggle.value as true because a sibling unit was parsed first. Which unit won depended on parsing order, making the result vary between runs.
Defaults are now resolved per unit, including defaults inherited through include. Overrides passed with --feature or TG_FEATURE continue to apply to every unit in the run.
Thanks to @dhotcolorado for reporting and fixing this!
Fixed S3 source downloads under EKS Pod Identity
Downloading unit sources from private S3 buckets (s3::https://...) now works when EKS Pod Identity is the only credential source. Previously, the bundled aws-sdk-go v1 rejected the Pod Identity Agent endpoint (169.254.170.23) because it only allowed loopback hosts. Terragrunt now uses aws-sdk-go v1.55.6, which allows the EKS and ECS container credential endpoints.
🧪 Experiments Added
otel-logs experiment exports logs to OpenTelemetry
Terragrunt previously emitted only traces and metrics, so there was no way to ship its log output to an OpenTelemetry backend or correlate log lines with the spans of a failed run.
Enable the new otel-logs experiment to add an OpenTelemetry logs signal, configured with TG_TELEMETRY_LOGS_EXPORTER:
none- no log exporting, the default.console- write log records to the console as JSON.otlpHttp- export logs to an OpenTelemetry collector over HTTP.otlpGrpc- export logs to an OpenTelemetry collector over gRPC.
TG_TELEMETRY_LOGS_EXPORTER=otlpHttp terragrunt run --all --experiment otel-logs -- applyThe OTLP exporters read the endpoint from the standard OTEL_EXPORTER_OTLP_ENDPOINT environment variable. Set TG_TELEMETRY_LOGS_EXPORTER_INSECURE_ENDPOINT=true to disable TLS when collecting locally. Records emitted while a span is active carry its trace and span IDs, so a failed unit's logs link to its span in the backend. Without the experiment enabled, the logs exporter stays inert regardless of TG_TELEMETRY_LOGS_EXPORTER.
profiling experiment adds pprof collection for Terragrunt runs
Enable the new profiling experiment to collect CPU profiles, memory (heap) profiles, and goroutine profiles (stack traces of all goroutines) using CLI flags. Profiling is intended for debugging the performance of Terragrunt itself, and for exploring ways to optimize Terragrunt as an application; it will not help with improving the performance of the infrastructure Terragrunt manages.
Example:
terragrunt --experiment=profiling --profile-cpu cpu.prof --profile-mem mem.prof --profile-goroutine goroutine.prof run -- planUse --profile-dir to collect all profiles into a single directory with conventional names (terragrunt_cpu.prof, terragrunt_mem.prof, terragrunt_goroutine.prof):
terragrunt --experiment=profiling --profile-dir /tmp/profiles run --all -- planThe same behavior is available via environment variables when the profiling experiment is enabled:
TG_PROFILE_CPUTG_PROFILE_MEMTG_PROFILE_GOROUTINETG_PROFILE_DIR
When using --profile-dir or TG_PROFILE_DIR, Terragrunt also sets TOFU_CPU_PROFILE for each unit so downstream OpenTofu processes (OpenTofu 1.11 or later) write their own CPU profiles into unit-specific subdirectories. An explicitly set TOFU_CPU_PROFILE is never overridden.
🧪 Experiments Updated
azure-backend now manages Azure Storage remote state
The azure-backend experiment now enables functional Terragrunt support for the Azure Storage (azurerm) remote-state backend.
When the experiment is enabled, Terragrunt can bootstrap the resource group, storage account, and blob container used by remote_state { backend = "azurerm" }, detect whether the backend needs bootstrapping, converge blob versioning and soft-delete settings, delete state blobs or containers, a...
v1.1.1
🐛 Bug Fixes
Chained role assumption for the S3 backend
When AWS credentials were supplied through --auth-provider-cmd or environment variables, Terragrunt ignored the assume_role attribute of the remote_state block for its own backend operations, such as bootstrapping the state bucket. In cross-account setups this caused access errors, even though OpenTofu/Terraform itself assumed the role correctly during runs.
Terragrunt now uses the supplied credentials as the source identity and assumes the configured role on top of them. The same applies to roles configured via the iam_role attribute or the --iam-assume-role flag, and to fetching dependency outputs directly from S3 state.
Safer temporary clone directories for terragrunt catalog
Terragrunt now creates a fresh temporary clone directory for each catalog load, rejects symlinked clone roots, and removes catalog clones when the TUI session exits.
Resolve dependency outputs for units that reference a dependency in a hook, extra_arguments, or remote_state block
Resolving a unit's dependency outputs for a downstream unit no longer fails when that unit references its own dependency in:
- a
before_hook,after_hook, orerror_hook - an
extra_argumentsblock - a
remote_stateblock
Previously these raised There is no variable named "dependency" on the downstream unit, and a remote_state reference could crash Terragrunt.
Limit IaC engine archive extraction
Terragrunt now protects developer machines and CI runners from engine archives that expand into unexpectedly large amounts of data. If an IaC engine package is unusually large or contains too many files, Terragrunt stops processing it before it can consume excessive disk space.
--filter-allow-destroy with ...[] dependent-traversal filters no longer fails
--filter-allow-destroy --filter '...[HEAD~1...HEAD]' failed with "Too many command line arguments" or hung when the deleted unit had dependents. Terragrunt now correctly plans and destroys deleted units regardless of whether dependents are included in the run.
Fix find and list missing units inside generated stacks for Git-based filters
terragrunt find and terragrunt list with a Git-based filter (for example --filter '[HEAD^1...HEAD]') now detect units inside generated stacks. Previously they did not generate stacks in the worktrees they create for the comparison, so no unit nested in a generated stack was ever surfaced, while terragrunt run --all with the same filter targeted those units correctly.
This affected every change that lands inside a generated stack, including a modified terragrunt.stack.hcl, a change to a unit's own files, and a change to a file the stack reads via read_terragrunt_config or mark_glob_as_read.
Stacks are generated only inside the comparison worktrees; find and list still do not generate stacks in your current working directory by default.
Treat Git source ref values strictly as references
Terragrunt now passes the ref from a Git module source to git strictly as a reference when downloading through content-addressable storage. Previously a source whose ref began with a git option (for example a value starting with --) could be interpreted by git as an option rather than a reference while fetching the source.
Terragrunt now terminates git option parsing before the repository and reference arguments in its fetch, clone, and ls-remote invocations, so these values can only ever be read as the repository and reference they are meant to be. Normal refs, branches, tags, and commit SHAs continue to work unchanged.
hcl validate resolves get_original_terragrunt_dir() to the discovered unit
terragrunt hcl validate and terragrunt hcl validate --inputs now resolve get_original_terragrunt_dir() to each discovered unit's own directory instead of the directory the command was launched from. Previously, when the command ran from a parent directory that discovered units in subdirectories, any read_terragrunt_config() call that built a path relative to get_original_terragrunt_dir() resolved against the wrong directory and failed with "You attempted to run terragrunt in a folder that does not contain a terragrunt.hcl file", even though plan, apply, and run validate worked on the same configuration.
Both commands now set the original config path per discovered unit before parsing, matching the behavior of run and backend bootstrap, so relative paths resolve against the unit that owns them.
Respect -lockfile=readonly during provider caching
When you pass -lockfile=readonly to init, Terragrunt no longer generates or updates .terraform.lock.hcl while warming the provider cache. Previously the cache step could write the lock file before OpenTofu/Terraform ran, so the read-only check always passed and silently defeated the flag.
Terragrunt now leaves the lock file untouched and lets OpenTofu/Terraform enforce it, failing when the lock file is missing or incomplete. The flag is honored whether it is supplied on the command line or through the TF_CLI_ARGS or TF_CLI_ARGS_init environment variables.
run --all no longer crashes on dependency discovery with graph filters
Running run --all with a filter that expands a git range through the dependency graph (for example [HEAD~1...HEAD]...) could fail during dependency discovery, reporting that a component "is missing its working directory". Whether it happened depended on the size and shape of the changed unit's dependency closure, so the same filter succeeded on smaller branches and find was unaffected.
A dependency reached from several units at once could become visible to discovery before its working directory was set, so a concurrent traversal could read it before it was complete. Dependencies now have their working directory set before they become visible, so run --all behaves the same regardless of graph size.
terraform_binary respected by run --all when both tofu and terraform are on PATH
run --all ignored a unit's terraform_binary setting and fell back to the auto-detected default (OpenTofu when both binaries are on PATH). The per-unit options used to execute each unit are cloned from the stack options, whose binary path is the auto-detected default, and the configured value was never applied to them.
Each unit now honors its own terraform_binary, matching the behavior of a single run. Setting --tf-path or TG_TF_PATH still takes precedence over the config value.
S3 bucket creation failures report the underlying error
When creating the state bucket failed during backend bootstrap, the reported error was a misleading NoSuchBucket from a follow-up access check, hiding the actual cause. The original creation error, such as AccessDenied, is now part of the reported message.
Allow empty locals blocks in terragrunt.stack.hcl
Fixed a bug where an empty locals {} block in a stack configuration could break stack generate.
Clear error when terraform.source references a dependency output
A terraform.source that references dependency.<name>.outputs.<key> is now rejected with a message explaining that the module source must be resolvable before dependencies are evaluated.
Terragrunt resolves the source while discovering units and building the run queue, before any dependency has run, so such a source can never be satisfied. Previously it surfaced a cryptic decode error.
🧪 Experiments Added
oci - Module sources from OCI registries
The oci experiment has been added as the gate for downloading source code (including OpenTofu modules) from OCI Distribution registries using oci:// schema URLs in Terragrunt configurations (including terraform.source attributes). This targets the same registries OpenTofu 1.10 supports natively, such as Amazon ECR, GitHub Container Registry, Azure Container Registry, Google Artifact Registry, and self-hosted or air-gapped registries.
Enabling the experiment has no behavioral effect yet: the getter that will resolve oci:// sources is not wired into source downloading, so oci:// sources still fail to download. Functional support will land in follow-up releases, gated by this experiment.
For setup steps, see the experiment documentation.
version-attribute - Resolve registry modules from a version constraint
The version-attribute experiment has been added to gate a new version attribute on the terraform block. It holds a version constraint (such as ~> 3.3 or >= 1.0.0, < 2.0.0) for a tfr:// registry module, and Terragrunt resolves it to the highest published version that satisfies the constraint before downloading:
terraform {
source = "tfr://registry.opentofu.org/terraform-aws-modules/vpc/aws"
version = "~> 3.3"
}This brings the terraform block to parity with the version argument on OpenTofu and Terraform module blocks. The attribute applies to tfr:// sources only, and cannot be combined with an inline ?version= on the same source.
Enable it with --experiment version-attribute. For setup steps and the criteria for stabilization, see the experiment documentation.
⚙️ Process Updates
Friendly panic reports
Terragrunt now writes a terragrunt-crash-YYYYMMDDTHHMMSSZ-<pid>.log file when it crashes.
The report includes runtime details, the command line, the panic message, and the stack trace. You can conveniently share this file (after reviewing for sensitive information) to report panics if Terragrunt crashes.
Pull Requests
✨ Features
v1.1.0
✨ New Features
Stack dependencies
A stack generates a tree of units from a single terragrunt.stack.hcl file. Wiring one of those units to another used to mean defining dependency blocks in your catalog and threading dependency paths through values. Stack dependencies let you declare those relationships up front instead.
Add an autoinclude block inside a unit or stack block, and Terragrunt generates a partial configuration (a terragrunt.autoinclude.hcl file) next to the generated terragrunt.hcl or terragrunt.stack.hcl that's automatically merged into the unit or stack definition. The new unit.<name>.path and stack.<name>.path references resolve to generated paths, so you don't have to hardcode them:
# terragrunt.stack.hcl
unit "vpc" {
source = "github.com/acme/catalog//units/vpc"
path = "vpc"
}
unit "app" {
source = "github.com/acme/catalog//units/app"
path = "app"
autoinclude {
dependency "vpc" {
config_path = unit.vpc.path
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
}
}
}Anything that's valid in a unit configuration is valid in its autoinclude block, so you can also patch catalog units with configuration they don't ship with, like retry rules:
# terragrunt.stack.hcl
unit "app" {
source = "github.com/acme/catalog//units/app"
path = "app"
autoinclude {
errors {
retry "transient_errors" {
retryable_errors = [".*Error: transient network issue.*"]
max_attempts = 3
sleep_interval_sec = 5
}
}
}
}The same works for nested stacks: an autoinclude block inside a stack block patches the generated terragrunt.stack.hcl, so you can, for example, add an extra unit to one environment without forking the stack in your catalog.
Stack configurations also gained two capabilities along the way:
includeblocks now work interragrunt.stack.hclfiles, so shared stack configuration can live in a parent folder.dependencyblocks can target stack directories, and the run queue expands them to the units inside. Note that this relationship only goes one way: units can depend on stacks, but stacks cannot depend on stacks or units.
See the stacks documentation for the full reference. Previously gated behind the stack-dependencies experiment, all of this is now enabled by default.
Content Addressable Store (CAS)
The Content Addressable Store (CAS) deduplicates source downloads across configurations. It addresses repositories and modules by their content, stores them locally, and serves later requests from that local store instead of repeating the fetch. This speeds up catalog cloning, OpenTofu/Terraform source fetching, and stack generation, and identical files occupy disk space once regardless of how many configurations use them.
The CAS is no longer limited to Git. It also deduplicates HTTP, Amazon S3, Google Cloud Storage, Mercurial, and SMB sources, along with OpenTofu/Terraform registry sources fetched via tfr://. See supported sources for how each one resolves and deduplicates content.
CAS is enabled by default. Use the --no-cas flag (or TG_NO_CAS=true) to opt out of it for a run:
terragrunt run --all --no-cas -- planTwo new attributes give you finer control, and both default to off:
-
update_source_with_casmakes a generated stack self-contained. Set it on aunit,stack, orterraformblock with a relativesource, andterragrunt stack generaterewrites that source into a content-addressedcas::reference, so the generated tree no longer depends on the surrounding repository layout. Catalog authors can keep relative paths in their sources and still ship a portable, reproducible stack:# stacks/networking/terragrunt.stack.hcl unit "vpc" { source = "../..//units/vpc" path = "vpc" update_source_with_cas = true }
After
terragrunt stack generate, the relative path is replaced by a reference to the exact tree the CAS stored:# Generated output unit "vpc" { source = "cas::sha1:f39ea0ebf891c9954c89d07b73b487ff938ef08b" path = "vpc" update_source_with_cas = true }
-
mutablecontrols how the CAS places fetched content on disk. By default, the CAS hard links files from its shared store into.terragrunt-cacheand marks them read-only, which is fast and uses no extra space, but means the files can't be edited in place. Setmutable = trueon aterraformblock to copy the content instead, making the working tree safe to edit at the cost of extra I/O and disk space:# units/vpc/terragrunt.hcl terraform { source = "github.com/acme/catalog//modules/vpc" mutable = true }
Previously gated behind the cas experiment, the CAS no longer requires --experiment cas.
Redesigned terragrunt catalog
The catalog command has been redesigned. It now starts without any configuration, discovers components across your catalog repositories in the background, and streams them into the TUI as they're found.
Discovery is no longer limited to a modules/ directory; components can live anywhere in a catalog repository. To control what gets discovered, add a .terragrunt-catalog-ignore file with .gitignore-style globs for the paths you want filtered out.
Components in the TUI now carry metadata to help you navigate a large catalog: each one shows a kind label (template, stack, unit, or module) and optional tags defined in the front-matter of its README.md. From the component list, press s to open a new screen that interactively collects the values used to scaffold the component into your repository.
Previously gated behind the catalog-redesign experiment, the redesigned catalog is now the default terragrunt catalog experience.
Reading detection for local module sources
Terragrunt can select units by the files they read, which is the basis of change-based runs in CI. Previously, pointing a unit's terraform block at a local directory didn't mark the files inside that directory as read, so a change to the module wouldn't select the unit.
When a unit's source is a local module, Terragrunt now records the module's *.tf, *.tf.json, *.hcl, *.tofu, and *.tofu.json files as read by that unit, so --filter 'reading=<path>' and --queue-include-units-reading select the unit when a module file changes:
terragrunt run --all --filter 'reading=./modules/vpc/main.tf' -- planFor files that reading detection doesn't track on its own, the new mark_glob_as_read() HCL function expands a glob and marks every matching file as read in one call:
locals {
configs = mark_glob_as_read("${get_terragrunt_dir()}/config/{*.yaml,**/*.yaml}")
}Existing pipelines built on --queue-include-units-reading or reading= filters may select more units than before, because changes to local module files now count as reads. Previously gated behind the mark-many-as-read experiment, these behaviors no longer require --experiment mark-many-as-read.
Skip auth during discovery with --no-discovery-auth-provider-cmd
By default, Terragrunt runs your --auth-provider-cmd once for every unit it discovers, so HCL functions that need credentials resolve correctly during parsing. In a large repository, that can mean hundreds of invocations before any unit runs, which can dominate wall-clock time on change-based runs.
The --no-discovery-auth-provider-cmd flag (env: TG_NO_DISCOVERY_AUTH_PROVIDER_CMD) skips those invocations during the discovery phase, leaving auth to run only for the units that actually execute:
terragrunt run --all \
--no-discovery-auth-provider-cmd \
--queue-include-units-reading=./changed-file.txt \
-- planWarning
Use this only when you know parsing resolves without credentials. Units whose configuration depends on values from --auth-provider-cmd during discovery (for example, via get_aws_account_id()) will fail to parse when the flag is set.
Previously gated behind the opt-out-auth experiment, the flag now works without --experiment opt-out-auth.
Run queue displayed as a dependency tree
Before a run --all, Terragrunt lists the units it's about to run. That list now renders as a dependency tree by default instead of a flat list, with units nested under their dependencies, so the run order and the relationships between units are visible before anything executes:
The following units will be run, starting with dependencies and then their dependents:
.
├── monitoring
╰── vpc
╰── database
╰── backend-app
The header adapts to direction: dependencies come before dependents on apply, and the order reverses on destroy.
Previously gated behind the dag-queue-display experiment, the tree display no longer requires --experiment dag-queue-display.
💡 Tips Added
Tip when filtering a stack leaves nested stacks ungenerated
terragrunt stack generate --filter './my-stack | type=stack' generates only the selected
stack, not the nested stacks it contains, which can be surprising for a stack of stacks.
When a non-glob | type=stack filter leaves a stack's nested stacks ungenerated, Terragrunt
now prints a tip showing how to generate them too, for example
...
v1.1.0-rc3
🎉 v1.1.0 Release Candidate
This is the third release candidate for Terragrunt v1.1.
It carries the same six completed experiments as v1.1.0-rc2, plus bug fixes for those experiments and improvements to how releases are published and verified.
This release completes the following experiments:
stack-dependenciescascatalog-redesignmark-many-as-readopt-out-authdag-queue-display
Future release candidates for v1.1.0 will include bug fixes related to these experiments or other urgent bug fixes as necessary, and documentation improvements.
Please try out this release candidate in lower environments and share your feedback in the associated GitHub discussion.
🆕 Changes since rc2
🐛 Bug Fixes
-
Dependency output resolution: Resolving a
dependencyblock's outputs now applies theenv_varsfrom the unit'sextra_argumentsblocks whosecommandsincludeoutput(#6396). -
Stack autoinclude: Transitive
autoincludedependencies that point at a stack directory now resolve correctly (#6389). -
Scaffold variable detection:
terragrunt scaffoldnow reads input variables from the module directory only, sovariableblocks in nested modules or examples no longer leak into the scaffolded inputs (#6381).
💡 Tips Added
- Nested stack generation: When a non-glob
| type=stackfilter generates a stack but leaves its nested stacks ungenerated,terragrunt stack generatenow prints a tip showing how to generate them too (#6387).
📚 Documentation
✨ New Features
Stack dependencies
A stack generates a tree of units from a single terragrunt.stack.hcl file. Wiring one of those units to another used to mean defining dependency blocks in your catalog and threading dependency paths through values. Stack dependencies let you declare those relationships up front instead.
Add an autoinclude block inside a unit or stack block, and Terragrunt generates a partial configuration (a terragrunt.autoinclude.hcl file) next to the generated terragrunt.hcl or terragrunt.stack.hcl that's automatically merged into the unit or stack definition. The new unit.<name>.path and stack.<name>.path references resolve to generated paths, so you don't have to hardcode them:
# terragrunt.stack.hcl
unit "vpc" {
source = "github.com/acme/catalog//units/vpc"
path = "vpc"
}
unit "app" {
source = "github.com/acme/catalog//units/app"
path = "app"
autoinclude {
dependency "vpc" {
config_path = unit.vpc.path
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
}
}
}Anything that's valid in a unit configuration is valid in its autoinclude block, so you can also patch catalog units with configuration they don't ship with, like retry rules:
# terragrunt.stack.hcl
unit "app" {
source = "github.com/acme/catalog//units/app"
path = "app"
autoinclude {
errors {
retry "transient_errors" {
retryable_errors = [".*Error: transient network issue.*"]
max_attempts = 3
sleep_interval_sec = 5
}
}
}
}The same works for nested stacks: an autoinclude block inside a stack block patches the generated terragrunt.stack.hcl, so you can, for example, add an extra unit to one environment without forking the stack in your catalog.
Stack configurations also gained two capabilities along the way:
includeblocks now work interragrunt.stack.hclfiles, so shared stack configuration can live in a parent folder.dependencyblocks can target stack directories, and the run queue expands them to the units inside. Note that this relationship only goes one way: units can depend on stacks, but stacks cannot depend on stacks or units.
See the stacks documentation for the full reference. Previously gated behind the stack-dependencies experiment, all of this is now enabled by default.
Content Addressable Store (CAS)
The Content Addressable Store (CAS) deduplicates source downloads across configurations. It addresses repositories and modules by their content, stores them locally, and serves later requests from that local store instead of repeating the fetch. This speeds up catalog cloning, OpenTofu/Terraform source fetching, and stack generation, and identical files occupy disk space once regardless of how many configurations use them.
The CAS is no longer limited to Git. It also deduplicates HTTP, Amazon S3, Google Cloud Storage, Mercurial, and SMB sources, along with OpenTofu/Terraform registry sources fetched via tfr://. See supported sources for how each one resolves and deduplicates content.
CAS is enabled by default. Use the --no-cas flag (or TG_NO_CAS=true) to opt out of it for a run:
terragrunt run --all --no-cas -- planTwo new attributes give you finer control, and both default to off:
-
update_source_with_casmakes a generated stack self-contained. Set it on aunit,stack, orterraformblock with a relativesource, andterragrunt stack generaterewrites that source into a content-addressedcas::reference, so the generated tree no longer depends on the surrounding repository layout. Catalog authors can keep relative paths in their sources and still ship a portable, reproducible stack:# stacks/networking/terragrunt.stack.hcl unit "vpc" { source = "../..//units/vpc" path = "vpc" update_source_with_cas = true }
After
terragrunt stack generate, the relative path is replaced by a reference to the exact tree the CAS stored:# Generated output unit "vpc" { source = "cas::sha1:f39ea0ebf891c9954c89d07b73b487ff938ef08b" path = "vpc" update_source_with_cas = true }
-
mutablecontrols how the CAS places fetched content on disk. By default, the CAS hard links files from its shared store into.terragrunt-cacheand marks them read-only, which is fast and uses no extra space, but means the files can't be edited in place. Setmutable = trueon aterraformblock to copy the content instead, making the working tree safe to edit at the cost of extra I/O and disk space:# units/vpc/terragrunt.hcl terraform { source = "github.com/acme/catalog//modules/vpc" mutable = true }
Previously gated behind the cas experiment, the CAS no longer requires --experiment cas.
Redesigned terragrunt catalog
The catalog command has been redesigned. It now starts without any configuration, discovers components across your catalog repositories in the background, and streams them into the TUI as they're found.
Discovery is no longer limited to a modules/ directory; components can live anywhere in a catalog repository. To control what gets discovered, add a .terragrunt-catalog-ignore file with .gitignore-style globs for the paths you want filtered out.
Components in the TUI now carry metadata to help you navigate a large catalog: each one shows a kind label (template, stack, unit, or module) and optional tags defined in the front-matter of its README.md. From the component list, press s to open a new screen that interactively collects the values used to scaffold the component into your repository.
Previously gated behind the catalog-redesign experiment, the redesigned catalog is now the default terragrunt catalog experience.
Reading detection for local module sources
Terragrunt can select units by the files they read, which is the basis of change-based runs in CI. Previously, pointing a unit's terraform block at a local directory didn't mark the files inside that directory as read, so a change to the module wouldn't select the unit.
When a unit's source is a local module, Terragrunt now records the module's *.tf, *.tf.json, *.hcl, *.tofu, and *.tofu.json files as read by that unit, so --filter 'reading=<path>' and --queue-include-units-reading select the unit when a module file changes:
terragrunt run --all --filter 'reading=./modules/vpc/main.tf' -- planFor files that reading detection doesn't track on its own, the new mark_glob_as_read() HCL function expands a glob and marks every matching file as read in one call:
locals {
configs = mark_glob_as_read("${get_terragrunt_dir()}/config/{*.yaml,**/*.yaml}")
}Existing pipelines built on --queue-include-units-reading or reading= filters may select more units than before, because changes to local module files now count as reads. Previously gated behind the mark-many-as-read experiment, these behaviors no longer require --experiment mark-many-as-read.
Skip auth during discovery with --no-discovery-auth-provider-cmd
By default, Terragrunt runs your --auth-provider-cmd once for every unit it discovers, so HCL functions that need credentials resolve correctly during parsing. In a large repository, that can mean hundreds of invocations before any unit runs, which can dominate wall-clock time on change-based runs.
The --no-discovery-auth-provider-cmd flag (env: TG_NO_DISCOVERY_AUTH_PROVIDER_CMD) skips those invocations during the discovery phase, ...
v1.1.0-rc2
🎉 v1.1.0 Release Candidate
This is the second release candidate for Terragrunt v1.1.
It carries the same six completed experiments as v1.1.0-rc1, plus bug fixes for those experiments and improvements to how releases are published and verified.
This release completes the following experiments:
stack-dependenciescascatalog-redesignmark-many-as-readopt-out-authdag-queue-display
Future release candidates for v1.1.0 will include bug fixes related to these experiments or other urgent bug fixes as necessary, and documentation improvements.
Please try out this release candidate in lower environments and share your feedback in the associated GitHub discussion.
🆕 Changes since rc1
🐛 Bug Fixes
mark_glob_as_readconstrains its walk to a boundary. Glob expansion is now confined to a boundary directory, defaulting to the enclosing Git repository root. A pattern whose walk would begin outside the boundary returns an error instead of expanding, so a pattern like"${local.dir}/{*.yaml}"that collapses to/{*.yaml}no longer walks the entire filesystem. Pass a leading--terragrunt-boundaryargument to set the boundary explicitly. (#6351)- Git-based filters select units reading added or deleted glob files. Filters like
--filter '[HEAD^1...HEAD]'now select units that read an added or deleted file throughmark_glob_as_read, even when that file lives outside the unit's own directory. Previously only modified files outside a unit reached those units. (#6352) update_source_with_casis rejected when CAS is disabled.terragrunt stack generate --no-casnow fails when a generated unit'sterraformblock setsupdate_source_with_cas = true, instead of silently emitting an unresolvable relativesource. This matches the existing behavior for the same attribute onunitandstackblocks. (#6363)
- Git-based filters select units reading added or deleted glob files. Filters like
⚙️ Process Updates
- Immutable releases. Starting with v1.1.0-rc1, Terragrunt releases are published as immutable releases on GitHub. Once published, a release's tag and assets can no longer be modified or deleted. (#6337)
- Install script verifies release attestations. The install script now checks downloaded assets against the release attestation that ships with immutable releases. When an authenticated GitHub CLI (v2.81.0 or later) is available, it verifies the checksums file and binary before installing, and aborts on a mismatch. Use
--no-verify-attestationto opt out. (#6344)
✨ New Features
Stack dependencies
A stack generates a tree of units from a single terragrunt.stack.hcl file. Wiring one of those units to another used to mean defining dependency blocks in your catalog and threading dependency paths through values. Stack dependencies let you declare those relationships up front instead.
Add an autoinclude block inside a unit or stack block, and Terragrunt generates a partial configuration (a terragrunt.autoinclude.hcl file) next to the generated terragrunt.hcl or terragrunt.stack.hcl that's automatically merged into the unit or stack definition. The new unit.<name>.path and stack.<name>.path references resolve to generated paths, so you don't have to hardcode them:
# terragrunt.stack.hcl
unit "vpc" {
source = "github.com/acme/catalog//units/vpc"
path = "vpc"
}
unit "app" {
source = "github.com/acme/catalog//units/app"
path = "app"
autoinclude {
dependency "vpc" {
config_path = unit.vpc.path
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
}
}
}Anything that's valid in a unit configuration is valid in its autoinclude block, so you can also patch catalog units with configuration they don't ship with, like retry rules:
# terragrunt.stack.hcl
unit "app" {
source = "github.com/acme/catalog//units/app"
path = "app"
autoinclude {
errors {
retry "transient_errors" {
retryable_errors = [".*Error: transient network issue.*"]
max_attempts = 3
sleep_interval_sec = 5
}
}
}
}The same works for nested stacks: an autoinclude block inside a stack block patches the generated terragrunt.stack.hcl, so you can, for example, add an extra unit to one environment without forking the stack in your catalog.
Stack configurations also gained two capabilities along the way:
includeblocks now work interragrunt.stack.hclfiles, so shared stack configuration can live in a parent folder.dependencyblocks can target stack directories, and the run queue expands them to the units inside. Note that this relationship only goes one way: units can depend on stacks, but stacks cannot depend on stacks or units.
See the stacks documentation for the full reference. Previously gated behind the stack-dependencies experiment, all of this is now enabled by default.
Content Addressable Store (CAS)
The Content Addressable Store (CAS) deduplicates source downloads across configurations. It addresses repositories and modules by their content, stores them locally, and serves later requests from that local store instead of repeating the fetch. This speeds up catalog cloning, OpenTofu/Terraform source fetching, and stack generation, and identical files occupy disk space once regardless of how many configurations use them.
The CAS is no longer limited to Git. It also deduplicates HTTP, Amazon S3, Google Cloud Storage, Mercurial, and SMB sources, along with OpenTofu/Terraform registry sources fetched via tfr://. See supported sources for how each one resolves and deduplicates content.
CAS is enabled by default. Use the --no-cas flag (or TG_NO_CAS=true) to opt out of it for a run:
terragrunt run --all --no-cas -- planTwo new attributes give you finer control, and both default to off:
-
update_source_with_casmakes a generated stack self-contained. Set it on aunit,stack, orterraformblock with a relativesource, andterragrunt stack generaterewrites that source into a content-addressedcas::reference, so the generated tree no longer depends on the surrounding repository layout. Catalog authors can keep relative paths in their sources and still ship a portable, reproducible stack:# stacks/networking/terragrunt.stack.hcl unit "vpc" { source = "../..//units/vpc" path = "vpc" update_source_with_cas = true }
After
terragrunt stack generate, the relative path is replaced by a reference to the exact tree the CAS stored:# Generated output unit "vpc" { source = "cas::sha1:f39ea0ebf891c9954c89d07b73b487ff938ef08b" path = "vpc" update_source_with_cas = true }
-
mutablecontrols how the CAS places fetched content on disk. By default, the CAS hard links files from its shared store into.terragrunt-cacheand marks them read-only, which is fast and uses no extra space, but means the files can't be edited in place. Setmutable = trueon aterraformblock to copy the content instead, making the working tree safe to edit at the cost of extra I/O and disk space:# units/vpc/terragrunt.hcl terraform { source = "github.com/acme/catalog//modules/vpc" mutable = true }
Previously gated behind the cas experiment, the CAS no longer requires --experiment cas.
Redesigned terragrunt catalog
The catalog command has been redesigned. It now starts without any configuration, discovers components across your catalog repositories in the background, and streams them into the TUI as they're found.
Discovery is no longer limited to a modules/ directory; components can live anywhere in a catalog repository. To control what gets discovered, add a .terragrunt-catalog-ignore file with .gitignore-style globs for the paths you want filtered out.
Components in the TUI now carry metadata to help you navigate a large catalog: each one shows a kind label (template, stack, unit, or module) and optional tags defined in the front-matter of its README.md. From the component list, press s to open a new screen that interactively collects the values used to scaffold the component into your repository.
Previously gated behind the catalog-redesign experiment, the redesigned catalog is now the default terragrunt catalog experience.
Reading detection for local module sources
Terragrunt can select units by the files they read, which is the basis of change-based runs in CI. Previously, pointing a unit's terraform block at a local directory didn't mark the files inside that directory as read, so a change to the module wouldn't select the unit.
When a unit's source is a local module, Terragrunt now records the module's *.tf, *.tf.json, *.hcl, *.tofu, and *.tofu.json files as read by that unit, so --filter 'reading=<path>' and --queue-include-units-reading select the unit when a module file changes:
terragrunt run --all --filter 'reading=./modules/vpc/main.tf' -- planFor files that reading detection doesn't track on its own, the new mark_glob_as_read() HCL function expands a glob and marks eve...
v1.1.0-rc1
🎉 v1.1.0 Release Candidate
This is the first release candidate for Terragrunt v1.1.
This release completes the following experiments:
stack-dependenciescascatalog-redesignmark-many-as-readopt-out-authdag-queue-display
Future release candidates for v1.1.0 will include bug fixes related to these experiments or other urgent bug fixes as necessary, and documentation improvements.
Please try out this release candidate in lower environments and share your feedback in the Associated GitHub discussion.
✨ New Features
Stack dependencies
A stack generates a tree of units from a single terragrunt.stack.hcl file. Wiring one of those units to another used to mean defining dependency blocks in your catalog and threading dependency paths through values. Stack dependencies let you declare those relationships up front instead.
Add an autoinclude block inside a unit or stack block, and Terragrunt generates a partial configuration (a terragrunt.autoinclude.hcl file) next to the generated terragrunt.hcl or terragrunt.stack.hcl that's automatically merged into the unit or stack definition. The new unit.<name>.path and stack.<name>.path references resolve to generated paths, so you don't have to hardcode them:
# terragrunt.stack.hcl
unit "vpc" {
source = "github.com/acme/catalog//units/vpc"
path = "vpc"
}
unit "app" {
source = "github.com/acme/catalog//units/app"
path = "app"
autoinclude {
dependency "vpc" {
config_path = unit.vpc.path
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
}
}
}Anything that's valid in a unit configuration is valid in its autoinclude block, so you can also patch catalog units with configuration they don't ship with, like retry rules:
# terragrunt.stack.hcl
unit "app" {
source = "github.com/acme/catalog//units/app"
path = "app"
autoinclude {
errors {
retry "transient_errors" {
retryable_errors = [".*Error: transient network issue.*"]
max_attempts = 3
sleep_interval_sec = 5
}
}
}
}The same works for nested stacks: an autoinclude block inside a stack block patches the generated terragrunt.stack.hcl, so you can, for example, add an extra unit to one environment without forking the stack in your catalog.
Stack configurations also gained two capabilities along the way:
includeblocks now work interragrunt.stack.hclfiles, so shared stack configuration can live in a parent folder.dependencyblocks can target stack directories, and the run queue expands them to the units inside. Note that this relationship only goes one way: units can depend on stacks, but stacks cannot depend on stacks or units.
See the stacks documentation for the full reference. Previously gated behind the stack-dependencies experiment, all of this is now enabled by default.
Content Addressable Store (CAS)
The Content Addressable Store (CAS) deduplicates source downloads across configurations. It addresses repositories and modules by their content, stores them locally, and serves later requests from that local store instead of repeating the fetch. This speeds up catalog cloning, OpenTofu/Terraform source fetching, and stack generation, and identical files occupy disk space once regardless of how many configurations use them.
The CAS is no longer limited to Git. It also deduplicates HTTP, Amazon S3, Google Cloud Storage, Mercurial, and SMB sources, along with OpenTofu/Terraform registry sources fetched via tfr://. See supported sources for how each one resolves and deduplicates content.
CAS is enabled by default. Use the --no-cas flag (or TG_NO_CAS=true) to opt out of it for a run:
terragrunt run --all --no-cas -- planTwo new attributes give you finer control, and both default to off:
-
update_source_with_casmakes a generated stack self-contained. Set it on aunit,stack, orterraformblock with a relativesource, andterragrunt stack generaterewrites that source into a content-addressedcas::reference, so the generated tree no longer depends on the surrounding repository layout. Catalog authors can keep relative paths in their sources and still ship a portable, reproducible stack:# stacks/networking/terragrunt.stack.hcl unit "vpc" { source = "../..//units/vpc" path = "vpc" update_source_with_cas = true }
After
terragrunt stack generate, the relative path is replaced by a reference to the exact tree the CAS stored:# Generated output unit "vpc" { source = "cas::sha1:f39ea0ebf891c9954c89d07b73b487ff938ef08b" path = "vpc" update_source_with_cas = true }
-
mutablecontrols how the CAS places fetched content on disk. By default, the CAS hard links files from its shared store into.terragrunt-cacheand marks them read-only, which is fast and uses no extra space, but means the files can't be edited in place. Setmutable = trueon aterraformblock to copy the content instead, making the working tree safe to edit at the cost of extra I/O and disk space:# units/vpc/terragrunt.hcl terraform { source = "github.com/acme/catalog//modules/vpc" mutable = true }
Previously gated behind the cas experiment, the CAS no longer requires --experiment cas.
Redesigned terragrunt catalog
The catalog command has been redesigned. It now starts without any configuration, discovers components across your catalog repositories in the background, and streams them into the TUI as they're found.
Discovery is no longer limited to a modules/ directory; components can live anywhere in a catalog repository. To control what gets discovered, add a .terragrunt-catalog-ignore file with .gitignore-style globs for the paths you want filtered out.
Components in the TUI now carry metadata to help you navigate a large catalog: each one shows a kind label (template, stack, unit, or module) and optional tags defined in the front-matter of its README.md. From the component list, press s to open a new screen that interactively collects the values used to scaffold the component into your repository.
Previously gated behind the catalog-redesign experiment, the redesigned catalog is now the default terragrunt catalog experience.
Reading detection for local module sources
Terragrunt can select units by the files they read, which is the basis of change-based runs in CI. Previously, pointing a unit's terraform block at a local directory didn't mark the files inside that directory as read, so a change to the module wouldn't select the unit.
When a unit's source is a local module, Terragrunt now records the module's *.tf, *.tf.json, *.hcl, *.tofu, and *.tofu.json files as read by that unit, so --filter 'reading=<path>' and --queue-include-units-reading select the unit when a module file changes:
terragrunt run --all --filter 'reading=./modules/vpc/main.tf' -- planFor files that reading detection doesn't track on its own, the new mark_glob_as_read() HCL function expands a glob and marks every matching file as read in one call:
locals {
configs = mark_glob_as_read("${get_terragrunt_dir()}/config/{*.yaml,**/*.yaml}")
}Existing pipelines built on --queue-include-units-reading or reading= filters may select more units than before, because changes to local module files now count as reads. Previously gated behind the mark-many-as-read experiment, these behaviors no longer require --experiment mark-many-as-read.
Skip auth during discovery with --no-discovery-auth-provider-cmd
By default, Terragrunt runs your --auth-provider-cmd once for every unit it discovers, so HCL functions that need credentials resolve correctly during parsing. In a large repository, that can mean hundreds of invocations before any unit runs, which can dominate wall-clock time on change-based runs.
The --no-discovery-auth-provider-cmd flag (env: TG_NO_DISCOVERY_AUTH_PROVIDER_CMD) skips those invocations during the discovery phase, leaving auth to run only for the units that actually execute:
terragrunt run --all \
--no-discovery-auth-provider-cmd \
--queue-include-units-reading=./changed-file.txt \
-- planWarning
Use this only when you know parsing resolves without credentials. Units whose configuration depends on values from --auth-provider-cmd during discovery (for example, via get_aws_account_id()) will fail to parse when the flag is set.
Previously gated behind the opt-out-auth experiment, the flag now works without --experiment opt-out-auth.
Run queue displayed as a dependency tree
Before a run --all, Terragrunt lists the units it's about to run. That list now renders as a dependency tree by default instead of a flat list, with units nested under their dependencies, so the run order and the relationships between units are visible before anything executes:
The following units will be run, starting with dependencies and then their dependents:
.
├── monitoring
╰── vpc
╰── database
╰── backend-app
The header adapts to direction: dependencies come before...
v1.0.8
🏎️ Performance Improvements
Faster read-file tracking with the mark-many-as-read experiment
With the mark-many-as-read experiment enabled, Terragrunt records every module file it marks as read during parsing. The bookkeeping for that record scaled quadratically: each new path was checked against every path recorded so far, which got expensive for units with large local module sources, and monorepos paid that cost again for every unit and every command.
Recording a path now takes constant time no matter how many paths came before it, and re-marking already-recorded files is cheaper still. The reading lists reported by find and list are unchanged.
🐛 Bug Fixes
assume_role: preserve commas inside list expressions
Terragrunt previously failed to correctly parse assume_role attributes containing list values such as transitive_tag_keys or policy_arns. Commas inside nested list expressions were incorrectly treated as top-level separators, causing generated configurations to fail with parsing errors.
assume_role = {
role_arn = "arn:aws:iam::123456789012:role/test-role"
transitive_tag_keys = ["Project", "Projects"]
}This resulted in errors similar to:
Missing item separator; Expected a comma to mark the beginning of the next item.
Terragrunt now preserves commas inside nested list and object expressions when parsing assume_role blocks, allowing configurations containing array attributes to be processed correctly.
Thanks to @Rahul-Kumar-prog for contributing this fix!
Completed experiments now evaluate as permanently enabled
Features gated behind a completed experiment were treated as disabled instead of permanently enabled, so functionality that graduated out of experiment status could silently stop working.
The one affected code path was hcl validate --inputs with a git filter expression such as --filter '[HEAD~1...HEAD]': after the filter-flag experiment completed, the command stopped preparing git worktrees for the filter. Git filter expressions now work with hcl validate --inputs again, matching find, list, and the other commands that accept filters.
Exposed-include resolution errors now name the include block, file, and failing field
When resolving an include block with expose = true, Terragrunt surfaced low-level parsing or conversion errors with no indication of which include block, file, or field was at fault. This was especially hard to debug for errors that carry no source location, such as:
unsuitable value: a bool is required
The error is now annotated with the include block name, the included (parent) file path, and a single dotted locator for the failing field — the top-level config field (dependency, inputs, locals, or feature) plus the attribute path within it when go-cty can determine one:
exposed include "root" (/path/to/root.hcl): dependency.outputs["enabled"]: unsuitable value: a bool is required
When go-cty cannot resolve a precise attribute path, the locator degrades to just the field name:
exposed include "root" (/path/to/root.hcl): dependency: unsuitable value: a bool is required
Errors that originate in HCL parsing already carry a source range (file:line:column) and are preserved unchanged. This narrows the search from the entire configuration tree to a specific file and field.
Intersecting a graph traversal with another filter no longer drops the traversed components
A graph traversal combined with an intersected filter dropped the components reached in discovery.
e.g., ...a-dependent | type=unit (the dependents of a-dependent, intersected with type of units) returned only a-dependent itself instead of its dependents, and git-change traversals such as ...[HEAD~1...HEAD] | type=unit lost the dependents of the changed units.
A component that matched both a graph expression target and a positive filesystem or git filter was classified as discovered before the graph traversal ran, so the traversal never expanded from it. Terragrunt now checks graph expression targets first, so intersecting a traversal with another filter keeps the dependencies and dependents it reaches.
generate blocks now honor hcl_fmt
Terragrunt now accepts hcl_fmt on generate blocks and preserves the setting when configurations are parsed, written, and parsed again. This lets generated .tf, .hcl, and .tofu files opt out of automatic HCL formatting by setting hcl_fmt = false, matching the existing generate = { ... } attribute-map behavior.
Telemetry resource now honors OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES
Terragrunt previously hardcoded the service.name resource attribute to terragrunt for every emitted trace and metric, ignoring the standard OpenTelemetry environment variables. Multiple Terragrunt invocations could not be distinguished in an OpenTelemetry backend without an intermediate collector to rewrite the attribute.
The resource is now composed via resource.New with WithFromEnv() placed after Terragrunt's defaults, so OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES are honored on every span and metric. Per the OpenTelemetry specification, OTEL_SERVICE_NAME takes precedence over a service.name entry in OTEL_RESOURCE_ATTRIBUTES. The default service.name remains terragrunt when neither variable is set.
s3:: sources: support virtual-hosted-style URLs
s3:: source URLs using the virtual-hosted-style S3 endpoint format were rejected:
terraform {
source = "s3::https://my-bucket.s3.us-west-2.amazonaws.com/terraform/modules/myapp.zip"
}This resulted in errors like:
ERROR downloading source url s3::https://my-bucket.s3.us-west-2.amazonaws.com/...
* URL is not a valid S3 URL
Terragrunt now accepts every AWS S3 endpoint form, including virtual-hosted-style URLs (<bucket>.s3.<region>.amazonaws.com) and modern path-style URLs (s3.<region>.amazonaws.com).
Windows console mode is restored when Terragrunt exits
On Windows, running a Terragrunt command from Nushell could leave the shell unable to read input afterward, with keystrokes such as the arrow keys appearing as raw escape sequences instead of being interpreted.
While it runs, Terragrunt reconfigures the console it shares with the parent shell so that terminal escape sequences are processed, but it did not put the original mode back when it exited. PowerShell reapplies its own console settings on every prompt and recovers on its own, so the problem surfaces only in shells that keep the inherited mode, such as Nushell. Terragrunt now records the console mode at startup and restores it on exit, returning the shell to the state it was in beforehand.
Reported in #6245.
📖 Documentation Updates
Clean Markdown is available for every docs page at <url>.md
Every docs page is now served as clean Markdown at the same URL with .md appended. For example, /getting-started/install is also available at /getting-started/install.md.
curl https://docs.terragrunt.com/getting-started/install.mdThe .md version contains the page content without the site navigation or other surrounding HTML, which makes it well suited as context for LLMs and AI tooling: it is smaller and carries only the documentation itself. Coverage includes every page, including the CLI command reference and the changelog.
This complements the existing llms.txt and llms-full.txt files by providing a per-page Markdown source.
🧪 Experiments Added
optional-hooks — Add experimental --no-hooks flag support for terragrunt run
The terragrunt run command now supports an experimental --no-hooks flag for disabling hook execution during command runs.
The feature is gated behind the optional-hooks experiment and skips execution of before_hook, after_hook, and error_hook blocks when enabled.
TG_EXPERIMENT=optional-hooks terragrunt run --no-hooks planThis feature is currently experimental because disabling hooks changes Terragrunt execution semantics and may evolve in future releases.
Using --no-hooks without enabling the optional-hooks experiment will return an error.
hook-context-env experiment exposes additional TG_CTX_* env vars to hooks
Enable the new hook-context-env experiment to surface three additional environment variables to every before_hook, after_hook, and error_hook:
TG_CTX_HOOK_TYPE—before_hook,after_hook, orerror_hook, identifying which lifecycle phase invoked the hook.TG_CTX_SOURCE— the resolved terraform source URL (CLI--sourceoverride, else evaluatedterraform.sourcewith source-map applied, else.).TG_CTX_TERRAGRUNT_DIR— the directory of the current Terragrunt config.
terragrunt run --all --experiment hook-context-env -- applyThese variables make it easier to share a single hook script across lifecycle phases and to access the unit's source and config directory without threading them through hook arguments.
🧪 Experiments Updated
cas: fallbacks now emit telemetry
When the cas experiment is enabled and a CAS operation cannot complete, Terragrunt falls back to a slower path (the standard download client, or a temporary clone when the shared git store is unavailable) and keeps going. Until now the only record o...
v1.0.7
✨ New Features
tfr:// source URLs accept an optional version
The version query parameter on tfr:// source URLs is now optional. When omitted, Terragrunt queries the registry's list-versions endpoint and downloads the latest stable version, matching how OpenTofu and Terraform resolve a module reference that has no version constraint.
terraform {
source = "tfr:///terraform-aws-modules/vpc/aws"
}Prereleases are excluded from resolution, so a registry that only publishes 4.0.0-rc1 alongside 3.3.0 will pin to 3.3.0. Pin a version explicitly with ?version= when you need reproducible builds or want to opt into a prerelease.
Thanks to @raman1236 for contributing this feature!
🐛 Bug Fixes
update_source_with_cas: preserve //subdir on a unit's terraform.source
When a unit's terraform.source used the // subdir convention (for example, source = "../..//modules/foo") and opted into update_source_with_cas, the rewritten source dropped the //subdir tail and the synthetic tree contained only the leaf module's files. A module that referenced a sibling via a relative path (source = "../bar") could not resolve that reference after materialization.
Rewrites now preserve the original //subdir (for example, cas::sha1:<hash>//modules/foo), and the synthetic tree is rooted at the path before //, so sibling files reachable via relative paths land in the materialized working directory.
Sources without // are unchanged: the tree stays scoped to the leaf module, and the rewritten reference has no //subdir tail.
--filter now detects affected units on Windows
On Windows, terragrunt find --filter '[origin/main...HEAD]' (and its variants) returned no affected units even when git diff reported changed files. The source= and reading= filters were affected by the same problem.
Filter glob patterns are always written with forward slashes, but the affected-unit comparison was being made with Windows backslash separators, so nothing matched. Terragrunt now compares paths consistently with forward slashes on every platform, and the filter detects changed units on Windows as it already did on Linux and macOS.
Reported in #6214.
startswith, endswith, strcontains, and run_cmd no longer panic on malformed calls
Calling startswith, endswith, or strcontains with the wrong number of arguments (for example a single argument instead of two) crashed Terragrunt instead of reporting a configuration error. Calling run_cmd with only option flags and no command (for example run_cmd("--terragrunt-quiet")) crashed the same way.
These calls now return a clear error: a wrong-number-of-parameters error for the string functions, and an empty-command error for run_cmd.
The --parallelism flag no longer accepts non-positive numbers
Previously, terragrunt commands that accept the --parallelism flag (or equivalently the $TG_PARALLELISM environment variable) used to hang indefinitely when invoked with --parallelism=0.
Terragrunt now validates that the value is positive and exits with an error otherwise.
Reported in #6211
🧪 Experiments Updated
cas — content-addressing for non-git sources
CAS now covers module sources beyond git: http(s), Amazon S3, Google Cloud Storage, and Mercurial. Repeat runs against an unchanged remote reuse the cached tree instead of downloading the bytes again.
Before fetching, CAS issues a cheap remote probe (an HTTP HEAD, an S3 object-attributes lookup, a GCS metadata read, or hg identify) to derive a cache key without pulling the source. On a hit, the cached tree is linked directly; on a miss, or when the remote exposes no usable signal, CAS downloads the source, ingests it, and keys the resulting tree by its content hash. A remote that publishes a new version under the same address pins to a new entry, so a stale cache cannot serve outdated bytes.
cas — OpenTofu/Terraform registry sources
Module sources of the form tfr://... are now content-addressed in CAS. Repeat runs against the same pinned registry version reuse the cached module instead of re-downloading the archive from the registry.
CAS resolves a tfr:// source by asking the registry where the underlying archive lives and uses that resolved URL as the cache key. Two runs that pin the same version share one entry; a republish under the same version pins to a new entry, so a stale cache cannot serve outdated bytes.
stack-dependencies: unit.<name>.path and stack.<name>.path resolve in values
The stack-dependencies experiment now exposes unit.<name>.path and stack.<name>.path when evaluating the values attribute of a unit or stack block, not only inside autoinclude blocks. A parent stack can pass the generated path of a sibling component down into a child stack, so a unit nested in that child stack can depend on a unit that lives at a different level of the hierarchy.
unit "vpc" {
source = "../catalog/units/vpc"
path = "vpc"
}
stack "app" {
source = "../catalog/stacks/app"
path = "app"
values = {
vpc_path = unit.vpc.path
}
}A unit inside the app stack reads values.vpc_path and uses it as the config_path of an autoinclude dependency, wiring the cross-level relationship at generation time. Paths follow the same layout the generator produces, including no_dot_terragrunt_stack on the referenced block.
stack-dependencies: simplified unit.* / stack.* ref shape
The stack-dependencies experiment no longer resolves stack.<name>.<unit_name>.path or stack.<name>.<nested_stack>.path. Only the top-level stack.<name>.path and unit.<name>.path forms remain. stack.<name>.name and unit.<name>.name are gone too; both only ever echoed the label that the reference already had to spell out.
Nested references required parsing every nested catalog up front and conflicted with the reserved name and path attributes on each ref: a nested unit named name or path could not be addressed.
To depend on a generated unit inside a stack, compute the path as ${stack.<name>.path}/<unit-relative-path> directly. The layout under a stack's generated directory follows no_dot_terragrunt_stack on the parent stack and on each unit, so hand-computed paths must mirror that resolution.
stack-dependencies: .terragrunt-stack-origin no longer written
Terragrunt no longer writes the .terragrunt-stack-origin file when generating nested stacks. Set update_source_with_cas = true on your unit and stack blocks if you would like relative paths in your catalog to resolve correctly instead.
Pull Requests
✨ Features
- feat: Make version optional in
tfr://module registry URLs by @yhakbar in #6112 - feat: Supporting all getters in CAS by @yhakbar in #6076
🐛 Bug Fixes
- fix: Removing use of
cas.WithFSby @yhakbar in #6195 - fix: panics in startswith / endswith / strcontains / run_cmd by @denis256 in #5984
- fix: Allowing all TG HCL fns in
terragrunt.stack.hcl, includingautoincludeby @yhakbar in #6166 - fix: Fixing synthetic trees for
terraform.sourceURLs with//by @yhakbar in #6218 - fix: Fixing nested generation for paths with
//by @yhakbar in #6234 - fix: Fixing stack path variables in values by @yhakbar in #6235
- fix: Fixing stack autoinclude by @yhakbar in #6236
- fix: Fixing git filters on Windows by @yhakbar in #6242
- fix: validate that
--parallelismvalue is positive by @ccmtaylor in #6212
📖 Documentation
- docs: Cleaning up experiment docs by @yhakbar in #6228
- docs: Fixing unreleased changelog page by @yhakbar in #6237
- docs: Cleaning up changelogs for v1.0.7 by @yhakbar in #6246
🧹 Chores
- chore: Getting rid of
fatih/structsdependency by @yhakbar in #6186 - chore: Getting rid of
go-homedirdirect dependency by @yhakbar in #6184 - chore: improved coderabbit rules by @denis256 in #6193
- chore: Consolidating on lipgloss for color by @yhakbar in #6188
- chore: Removing
go-errorsas a dependency by @yhakbar in #6182 - cho...