Skip to content

DRY Configuration with include

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:

network/terragrunt.hcl
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.hcl

From 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:

prod/project.hcl
locals {
project_id = "acme-prod-123456"
region = "us-central1"
}

Every unit under prod/ reads it the same way:

prod/network/terragrunt.hcl
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"]
root.hcl, an intermediate project.hcl, and a leaf unit combining both
What does an include block with find_in_parent_folders actually do?
What is read_terragrunt_config paired with a locals block typically used for?
The inputs block in a unit terragrunt.hcl is the Terragrunt equivalent of what?
What happens if find_in_parent_folders cannot locate the named file anywhere above the unit?