Skip to content
All projects

PF-04 · AWS · Terragrunt

AWS Terragrunt DRY IaC

Sixteen Terraform modules behind one root config, with Terragrunt running them in order from their dependency graph.

Role
Design and implementation
Period
Personal project · released Nov 2026
Cloud
AWS
Scale
16 modules · 16 units · 4 dependency levels
Key tech
Terragrunt · Terraform · AWS
GitHub
16
Terraform modules and units
1
Root config replacing 16 copies
- 40 %
Deployment time (production result)

At a glance

iam_custom_policyiam_useriam_custom_roleiam_groupvpcelastic_ipebsinternet_gatewaysecurity_groupvpc_endpointvpc_peeringelasticache_subnet_groupnat_gatewayroute_tableinstance_profileec2_instance

Problem

Each Terraform root repeated its backend and provider blocks, and the order between roots lived in people's heads.

Approach

Modules stay plain Terraform and Terragrunt wraps them. The backend, provider, and common inputs are generated once from live/root.hcl. _envcommon holds module sources and dependencies, and each unit states only its inputs. dependency blocks build the graph, and mock_outputs let the whole stack validate without a cloud account.

Result

This repository groups 16 modules and 16 units into four dependency levels and validates them without cloud credentials. The 40% shorter deployment time came from moving a live production estate to this pattern.

Up close

live/root.hclbackend · provider · common inputslive/dev/account.hclPer-account values<region>/region.hclPer-region values_envcommon/<module>.hclsource · dependency · mock_outputs<resource>/terragrunt.hclStates only its own inputsbackend.tfprovider.tf1 root config replaces 16 copies
Terragrunt generates backend.tf and provider.tf. One root config replaces 16 copies.
terragrunt: module version upgrade runbook$ terragrunt state pull > backup.tfstate$ find . -name '.terragrunt-cache' -type d -prune -exec rm -rf {} +# terraform.source를 새 버전으로 변경$ terragrunt init$ terragrunt state push backup.tfstate$ terragrunt plan# Back up state# Clear caches# The only code change# Download the new module# Restore state only if needed# Must show no changesNo changes.
The upgrade is done only when plan prints No changes. If it shows a change, stop and read the diff.

The provider generated from the root

What this code does: Terragrunt’s generate block writes a provider.tf file into each unit folder. The region comes from the region settings, the Environment tag from the account name, and every resource automatically gets a ManagedBy = terragrunt tag. The last line merges the account and region locals into the common inputs of every unit, so a unit file keeps only the values specific to that unit.

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<PROVIDER
provider "aws" {
  region = "${local.aws_region}"

  default_tags {
    tags = {
      Environment = "${local.account_name}"
      ManagedBy   = "terragrunt"
    }
  }
}
PROVIDER
}

inputs = merge(local.account_vars.locals, local.region_vars.locals)

dependency and mock_outputs

What this code does: Declares that this unit depends on the elastic_ip unit in the neighboring folder. From this, Terragrunt runs elastic_ip first and passes its output (eip_id) into this unit as input. When no real output exists yet, the fake IDs in mock_outputs are used, but only for init, validate, and plan, so the whole stack can be validated without a cloud while fake values can never reach a real apply.

dependency "elastic_ip" {
  config_path = "${get_terragrunt_dir()}/../elastic_ip"

  mock_outputs = {
    eip_id = {
      "demo-eip-1" = "eipalloc-00000000000000001"
      "demo-eip-2" = "eipalloc-00000000000000002"
    }
  }
  mock_outputs_allowed_terraform_commands = ["init", "validate", "plan"]
}

What I learned

The key point was that Terragrunt does not replace Terraform; it decides where code lives. Modules stay plain Terraform while the backend, provider, and common inputs are generated from one root, so a structure that copied the same settings 16 times became one where a single edit reaches everything. Writing run order as dependency blocks moves it from people's memory into a graph, and mock_outputs let the whole stack validate without cloud credentials. The demo keeps state local, but where several people share an environment, where state lives has to be settled first, and I now treat that as part of the design from day one.

Related field notes

Open to work · Cloud infrastructure and security

Choose a theme

Click a badge to switch to that theme on the same content. Dragging only swings it.

Back to the front page →