DRY Configuration with include
The idea in one sentence
Section titled “The idea in one sentence”A unit’s terragrunt.hcl uses an include block plus find_in_parent_folders() to pull in everything root.hcl already defines, and can layer in smaller intermediate config files the same way, so none of that shared setup is ever typed twice.
The include block: inheriting root.hcl without repeating it
Section titled “The include block: inheriting root.hcl without repeating it”The previous lesson showed a unit’s terragrunt.hcl referencing root.hcl in three lines, without explaining how that reference works. The mechanism is the include block:
include "root" { path = find_in_parent_folders("root.hcl")}include "root" tells Terragrunt to read the file at path and merge its remote_state, generate, and any other top-level blocks into this unit’s own config, as though the unit had written all of that itself. The label "root" is just a name for this particular include block — a unit could in principle have more than one include, each pulling in a different file, and the label is how you would tell them apart. Without this block, every unit would need its own copy of the remote_state and generate blocks from the previous lesson, which is exactly the repetition Terragrunt is meant to remove.
find_in_parent_folders: walking up the tree to find the file
Section titled “find_in_parent_folders: walking up the tree to find the file”find_in_parent_folders("root.hcl") does not assume any fixed distance between a unit and the shared config above it. It starts in the unit’s own directory and walks upward, one parent directory at a time, until it finds a file named root.hcl, then returns the path to it. That means a unit two levels deep and a unit five levels deep can both write the exact same include block and still find the correct file:
acme-infra/├── root.hcl└── prod/ ├── project.hcl └── network/ └── terragrunt.hclFrom prod/network/terragrunt.hcl, find_in_parent_folders("root.hcl") climbs past prod/ and lands on acme-infra/root.hcl. If no file with that name exists anywhere above the unit, Terragrunt raises an error rather than silently proceeding without it — a missing root.hcl is treated as a configuration mistake, not something to skip past.
locals and read_terragrunt_config: sharing config between levels
Section titled “locals and read_terragrunt_config: sharing config between levels”include is built for one specific job: inheriting the single shared root.hcl. Plenty of config is not truly global, though — it might apply to just one GCP project’s worth of units, sitting one level below the root. For that, a locals block combined with read_terragrunt_config() lets any unit read a smaller, more targeted file and pull specific values out of it. Continuing the layout above, prod/project.hcl holds values specific to that one GCP project:
locals { project_id = "acme-prod-123456" region = "us-central1"}Every unit under prod/ reads it the same way:
include "root" { path = find_in_parent_folders("root.hcl")}
locals { project_vars = read_terragrunt_config(find_in_parent_folders("project.hcl"))}
inputs = { project_id = local.project_vars.locals.project_id region = local.project_vars.locals.region cidr_block = "10.0.0.0/16"}read_terragrunt_config() parses the target file and hands back its contents as a value, so local.project_vars.locals.project_id reaches into that file’s own locals block to pull out project_id. Nothing stops a directory tree from having several such files at different levels — a project.hcl per GCP project, an env.hcl per environment — each read the same way, each avoiding one more piece of copy-pasted config.
inputs: the Terragrunt equivalent of a .tfvars file
Section titled “inputs: the Terragrunt equivalent of a .tfvars file”The inputs block seen in both examples above is how a unit’s terragrunt.hcl supplies variable values to the underlying Terraform module, the same job a .tfvars file would normally do. Every key inside inputs is passed through to Terraform as a variable value for the module that unit’s terraform block points at — no separate .tfvars file needs to exist in the module at all. This is what makes combining inherited values with unit-specific ones so direct: project_id and region above come from project.hcl by way of local.project_vars, while cidr_block is written directly in the unit, and both land in the same inputs map with no special syntax to mix the two.
flowchart TD root["root.hcl (remote_state + generate)"] project["prod/project.hcl (locals: project_id, region)"] unit["prod/network/terragrunt.hcl"] root -->|include + find_in_parent_folders| unit project -->|read_terragrunt_config + find_in_parent_folders| unit unit --> inputsBlock["inputs: project_id, region, cidr_block"]