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, not measured in this repository)

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 is a production result from moving a live estate to this pattern, and was not measured in this repository.

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: Generates the provider and common inputs at the root, so no unit repeats them.

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: Connects outputs between modules and validates with mock values before plan.

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

Terragrunt does not replace Terraform. It organizes the code. Writing dependencies as code removes the need to track run order by hand.

What I would improve

State is local so the demo stays self-contained. A team should move to the S3 backend shown in root.hcl. us-east-1 holds only a region file, and units are validated in CI but not applied.

Related field notes

Open to work · Cloud infrastructure and security