DRY Configuration with include
The idea in one sentence
Section titled “The idea in one sentence”An include block plus find_in_parent_folders() lets a unit pull in everything a shared config file already defines, and locals combined with read_terragrunt_config() extends that same trick to any number of intermediate files sitting between the root and a unit.
The include block: inheriting root.hcl without repeating it
Section titled “The include block: inheriting root.hcl without repeating it”The previous lesson showed root.hcl declaring remote_state and a provider generate block once, at the top of the tree. A unit gets access to both of those without copying a single line, through an include block:
include "root" { path = find_in_parent_folders("root.hcl")}find_in_parent_folders("root.hcl") walks up the directory tree, starting from the unit’s own directory, until it finds a file named root.hcl. It does not matter how many directories deep the unit lives — one level under the root or five — the function keeps climbing until it locates the file, so moving a unit to a different depth in the tree never breaks the reference. The label on the include block, "root" here, is just a name Terragrunt uses internally to track this particular inclusion; a unit can have more than one include block, each with its own label, each pointing at a different file.
Once included, every block root.hcl defines — remote_state, the provider generate block, and anything else placed there — behaves as if it had been written directly inside the unit’s own terragrunt.hcl. That is the entire mechanism: one include block replaces what would otherwise be dozens of repeated lines across every unit in the tree.
Sharing granular config downward with locals and read_terragrunt_config
Section titled “Sharing granular config downward with locals and read_terragrunt_config”root.hcl is not the only place shared configuration can live. A large tree often has a middle layer too — for example, one subscription.hcl file per Azure subscription, sitting above every unit that belongs to that subscription, holding values specific to it:
locals { subscription_id = "11111111-1111-1111-1111-111111111111" environment = "networking"}A unit underneath that subscription reads it with read_terragrunt_config(), wrapped in a locals block of its own:
include "root" { path = find_in_parent_folders("root.hcl")}
locals { subscription_vars = read_terragrunt_config(find_in_parent_folders("subscription.hcl"))}
inputs = { subscription_id = local.subscription_vars.locals.subscription_id address_space = ["10.1.0.0/16"] tags = { environment = local.subscription_vars.locals.environment }}read_terragrunt_config() parses the file it is given and hands back a map of everything defined inside it, so local.subscription_vars.locals.subscription_id reaches straight into the locals block of subscription.hcl. Notice this is a different mechanism from include — include inherits blocks like remote_state wholesale, while read_terragrunt_config() gives a unit fine-grained access to specific values from a file it chooses to read, without inheriting the whole thing. That distinction is what makes it useful for an intermediate file: root.hcl still applies to every unit in the entire tree via include, while subscription.hcl only needs to be read by the handful of units that actually live under that one subscription.
inputs: the Terragrunt equivalent of a tfvars file
Section titled “inputs: the Terragrunt equivalent of a tfvars file”Every example so far has ended with an inputs block, and it deserves its own explanation. In plain Terraform, you hand a root module its variable values with a .tfvars file or -var flags on the command line. Terragrunt’s inputs block does the same job, except the values live directly inside the unit’s own HCL instead of a separate file:
inputs = { address_space = ["10.0.0.0/16"] location = "westeurope"}Every key in inputs must match a variable the underlying Terraform module actually declares — Terragrunt passes each one straight through as if it had been supplied with -var. Because inputs is ordinary HCL, it can reference locals, the result of read_terragrunt_config(), or later in this module, another unit’s outputs — none of which a static .tfvars file could ever do.
flowchart TD root["root.hcl (remote_state, provider generate)"] sub["subscription.hcl (locals: subscription_id, environment)"] unit["vnet/terragrunt.hcl"] root -->|include find_in_parent_folders root.hcl| unit sub -->|read_terragrunt_config find_in_parent_folders subscription.hcl| unit unit --> inputs["inputs = merge of root, subscription, and unit values"]