Generating backend & provider config

Stop copy-pasting boilerplate.

Advanced12 min · lesson 3 of 12

The single biggest boilerplate in multi-module Terraform is the backend block — every root module needs one, and they are nearly identical except for the state key. Terragrunt’s generate and remote_state features produce that config so you write it once. remote_state defines the backend for a unit (and can live in a parent file every unit inherits), and Terragrunt writes the corresponding backend.tf into the generated module at run time.

terragrunt.hcl
remote_state {
backend = "s3"
generate = { path = "backend.tf", if_exists = "overwrite" }
config = {
bucket = "acme-tf-state"
key = "${path_relative_to_include()}/terraform.tfstate" # unique per unit
region = "us-east-1"
dynamodb_table = "tf-locks"
encrypt = true
}
}

Generating provider config too

The same generate mechanism produces provider blocks — a common provider.tf (with the right region, assume-role, default tags) injected into every unit, so you configure the provider once instead of in every module. The key trick above is path_relative_to_include(), which gives each unit a distinct state key automatically based on its folder, so you never hand-assign state paths again.

generate provider
generate "provider" {
path = "provider.tf"
if_exists = "overwrite"
contents = <<EOF
provider "aws" {
region = "us-east-1"
default_tags { tags = { managed_by = "terragrunt" } }
}
EOF
}
Generated files are overwritten — do not hand-edit them
backend.tf and provider.tf that Terragrunt generates are recreated on every run (if_exists = overwrite), so any manual edit is silently lost. Change the generation config in terragrunt.hcl, not the generated file, and add the generated files to .gitignore in the module so nobody mistakes them for source. Editing the output instead of the generator is a classic Terragrunt confusion.