Skip to content

hagzag/tf-live

Repository files navigation

tf-live — Demo Terragrunt Live Repository

⚠️ This is a demo / illustrative repository. Account IDs, domain names, org identifiers, and profile names are all placeholders. Do not commit real AWS account IDs, credentials, or production values. See Wiring guide below for what to replace before using this repo.

This repository is the companion reference implementation for the Declarative IaC with Terraform & Terragrunt blog series. It demonstrates a production-grade multi-account Terragrunt layout for AWS, paired with the hagzag/tf-modules versioned module library.


What this repo shows

  • The account / region / environment / module directory hierarchy that replaces Terraform workspaces
  • A root.hcl that auto-generates backend.tf and provider.tf for every module leaf using path_relative_to_include for unique S3 state keys
  • Per-level HCL config files (account.hcl, region.hcl, env.hcl) assembled at runtime by find_in_parent_folders
  • Terragrunt built-in functions: find_in_parent_folders, path_relative_to_include, get_terragrunt_dir, get_repo_root, read_terragrunt_config
  • OIDC-based keyless AWS auth (no static IAM access keys) for GitHub Actions and GitLab CI
  • A hub-and-spoke multi-account IAM design: CI assumes a mgmt role; Terraform provider assumes target-account roles
  • The allowed_account_ids guard in the generated provider that prevents wrong-account applies
  • A .bootstrap/ one-time setup that creates TerraformAutomationRole in every account before CI exists

Repository structure

tf-live/
├── root.hcl                    # Generates backend.tf + provider.tf for every module leaf
├── proj.hcl                    # Project-wide constants: module repo URL, pinned tag, org name, tags
│
├── mgmt/                       # Management account (IAM, OIDC providers, shared infra)
│   ├── account.hcl             # account_id = "111111111111" (placeholder)
│   └── eu-west-1/
│       ├── region.hcl
│       └── global/
│           ├── env.hcl
│           ├── github-oidc/    → terragrunt.hcl   # GitHub Actions OIDC provider + role
│           ├── gitlab-oidc/    → terragrunt.hcl   # GitLab CI OIDC provider + role
│           ├── regional-info/  → terragrunt.hcl
│           └── role-validator/ → terragrunt.hcl
│
├── dev/                        # Development account
│   ├── account.hcl             # account_id = "222222222222" (placeholder)
│   └── eu-west-1/
│       ├── region.hcl
│       ├── dev/
│       │   ├── env.hcl
│       │   ├── regional-info/  → terragrunt.hcl
│       │   └── role-validator/ → terragrunt.hcl
│       └── playground/
│           ├── env.hcl
│           ├── regional-info/  → terragrunt.hcl
│           └── role-validator/ → terragrunt.hcl
│
├── prod/                       # Production account
│   ├── account.hcl             # account_id = "333333333333" (placeholder)
│   └── us-east-1/
│       ├── region.hcl
│       └── prod/
│           ├── env.hcl
│           ├── regional-info/  → terragrunt.hcl
│           └── role-validator/ → terragrunt.hcl
│
└── .bootstrap/                 # One-time setup — run locally before CI exists
    ├── root.hcl                # Uses SSO profiles directly (no role assumption yet)
    ├── accounts.hcl            # Flat map of all accounts for bootstrap providers
    └── cross-account-role/
        ├── README.md           # Step-by-step bootstrap instructions
        ├── terragrunt.hcl      # Generates per-account IAM role resources
        ├── main.tf             # mgmt TerraformAutomationRole + SSO trust
        └── variables.tf

How the hierarchy assembles at runtime

Every module leaf contains a single terragrunt.hcl with include { path = find_in_parent_folders("root.hcl") }. When Terragrunt runs that module, root.hcl walks up the directory tree and reads:

locals {
  region  = read_terragrunt_config(find_in_parent_folders("region.hcl"))
  env     = read_terragrunt_config(find_in_parent_folders("env.hcl"))
  proj    = read_terragrunt_config(find_in_parent_folders("proj.hcl"))
  account = read_terragrunt_config(find_in_parent_folders("account.hcl"))
}

It then generates two files in the module's .terragrunt-cache/:

backend.tf — unique S3 key per module, derived from the directory path:

# For dev/eu-west-1/dev/regional-info/
key = "dev/eu-west-1/dev/regional-info/terraform.tfstate"

provider.tf — AWS provider scoped to the correct account, with allowed_account_ids as a guard:

provider "aws" {
  region              = "eu-west-1"
  allowed_account_ids = ["222222222222"]

  assume_role {
    role_arn     = "arn:aws:iam::222222222222:role/TerraformAutomationRole"
    session_name = "Terraform-222222222222"
  }
}

The allowed_account_ids field will cause Terraform to fail immediately if the assumed credentials belong to a different account — catching misconfigured CI matrix entries before any API calls.

Terragrunt built-in functions used

Function What it returns Where used
find_in_parent_folders("x.hcl") Absolute path to the nearest x.hcl walking up the tree include blocks; read_terragrunt_config() calls
path_relative_to_include() Path of the current module relative to the root include S3 state key — makes every module's state path unique
get_terragrunt_dir() Absolute path of the current terragrunt.hcl basename(get_terragrunt_dir()) → derive alias/env/region from dir name
get_repo_root() Git repo root (absolute path) Reference scripts or assets from any module depth
read_terragrunt_config(path) Parsed HCL locals from another config file Load hierarchy locals into root.hcl

Hub-and-spoke OIDC auth

No long-lived AWS access keys anywhere in this repo or in CI.

GitHub Actions / GitLab CI
  └── OIDC JWT → sts:AssumeRoleWithWebIdentity
        └── mgmt account: TerraformAutomationRole
              ├── sts:AssumeRole → dev account: TerraformAutomationRole
              └── sts:AssumeRole → prod account: TerraformAutomationRole

The OIDC providers and trust policies live in mgmt/eu-west-1/global/github-oidc/ and gitlab-oidc/. They're managed by Terragrunt like everything else — after the one-time bootstrap.


Getting started

Prerequisites

Tool Version Notes
Terraform >= 1.5 or OpenTofu >= 1.6
Terragrunt >= 0.54 uses run --all syntax; see note below
AWS CLI >= 2.x with SSO configured

Terragrunt CLI note: This repo uses terragrunt run --all plan (v0.54+). Older versions used terragrunt run-all plan. If you're on an older version, either upgrade or add --experiment cli-redesign as a bridge flag.

1 — Wire the placeholders

Before running anything, update these files with real values:

proj.hcl

modules_repo = "https://github.com/hagzag/tf-modules"  # already correct for the demo modules
modules_tag  = "v1.0.3"                                  # pin to a real tag
github_org   = "your-actual-github-org"
gitlab_owner = "your-actual-gitlab-group"

mgmt/account.hcl

account_id = "111111111111"  # → your real management account ID

dev/account.hcl

account_id = "222222222222"  # → your real dev account ID

prod/account.hcl

account_id = "333333333333"  # → your real prod account ID

root.hcl

bucket = "your-terraform-state-bucket"  # → your real S3 state bucket name
region = "eu-west-1"                     # → the region where the bucket lives

.bootstrap/accounts.hcl

mgmt = { profile = "your-mgmt-profile", region = "eu-west-1", account_id = "111111111111" }
dev  = { profile = "your-dev-profile",  region = "eu-west-1", account_id = "222222222222" }
prod = { profile = "your-prod-profile", region = "us-east-1", account_id = "333333333333" }

Replace profile names with the AWS SSO profile names in your ~/.aws/config.

2 — Bootstrap (run once, locally)

The bootstrap creates TerraformAutomationRole in every account and the S3 state bucket. It runs with your SSO credentials directly — no role assumption yet because the roles don't exist yet.

# Log in to AWS SSO
aws sso login --profile your-mgmt-profile

# Create the cross-account roles
cd .bootstrap/cross-account-role
TG_SKIP_MODULE=false terragrunt init
TG_SKIP_MODULE=false terragrunt plan
TG_SKIP_MODULE=false terragrunt apply

After apply:

  • TerraformAutomationRole exists in mgmt, dev, and prod
  • The mgmt role can sts:AssumeRole into dev and prod
  • SSO AdministratorAccess roles are trusted to assume the mgmt role

3 — Set up OIDC providers (run once, locally)

# GitHub Actions OIDC
cd mgmt/eu-west-1/global/github-oidc
TG_SKIP_MODULE=false terragrunt apply

# GitLab CI OIDC (if using GitLab)
cd ../gitlab-oidc
TG_SKIP_MODULE=false terragrunt apply

4 — Run via CI

After bootstrap, everything runs through CI. See the blog posts for the full pipeline definitions:

5 — Local plan (any module)

# Plan a single module
cd dev/eu-west-1/dev/regional-info
terragrunt plan

# Plan everything in an environment
cd dev/eu-west-1/dev
terragrunt run --all plan

# Plan across all accounts
cd /path/to/repo
terragrunt run --all plan

Wiring guide

Placeholder File What to replace with
111111111111 mgmt/account.hcl, .bootstrap/accounts.hcl Management AWS account ID
222222222222 dev/account.hcl, .bootstrap/accounts.hcl Dev AWS account ID
333333333333 prod/account.hcl, .bootstrap/accounts.hcl Prod AWS account ID
your-terraform-state-bucket root.hcl, .bootstrap/root.hcl Your S3 bucket name (must already exist)
your-mgmt-profile .bootstrap/accounts.hcl AWS SSO profile name for mgmt
your-dev-profile .bootstrap/accounts.hcl AWS SSO profile name for dev
your-prod-profile .bootstrap/accounts.hcl AWS SSO profile name for prod
example-org proj.hcl Your GitHub org name
example-group proj.hcl Your GitLab group name
example.com proj.hcl Your root domain

Sanitization notes

This repo was sanitized from a production setup. The following were removed or replaced:

Original Replaced with Reason
Real AWS account IDs 111111111111 / 222222222222 / 333333333333 Account enumeration risk
Internal org/team names example-org / example-group Customer privacy
Internal SSO profile names your-*-profile Environment-specific
Real S3 bucket name your-terraform-state-bucket Prevents accidental state writes
Internal app/secret names removed Not relevant to the pattern

Reference

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages