Terragrunt for Multi-Environment AWS Deployments: A Real-World Pattern
Terraform is powerful. Terraform at scale across multiple AWS accounts and environments without a wrapper is painful. This post isn’t another “what is Terragrunt” intro — it’s the actual pattern I use in production to manage infrastructure across isolated AWS accounts with a single codebase.
The Problem with Raw Terraform at Scale
If you’ve managed multi-environment Terraform without tooling, you’ve hit the wall. The usual approaches:
- Copy-paste modules per environment — Works until you have 6 environments and a bug fix requires touching 6 directories.
- Workspaces — Better, but state isolation is weak and the mental model gets confusing fast. State files share a backend config; one
terraform destroywith the wrong workspace selected is a very bad day. terragrunt— What I use. Keeps modules DRY, enforces consistent backend config, and makes per-environment overrides clean.
Directory Structure
infrastructure/
├── modules/ # Reusable Terraform modules (no root module)
│ ├── eks-cluster/
│ ├── rds-aurora/
│ ├── ecr-repository/
│ └── vpc/
├── live/ # Terragrunt root — one dir per env/account
│ ├── terragrunt.hcl # Root config: backend, provider, common inputs
│ ├── dev/
│ │ ├── account.hcl # Dev account ID, region
│ │ ├── eks/
│ │ │ └── terragrunt.hcl
│ │ └── rds/
│ │ └── terragrunt.hcl
│ ├── staging/
│ │ ├── account.hcl
│ │ ├── eks/
│ │ │ └── terragrunt.hcl
│ │ └── rds/
│ │ └── terragrunt.hcl
│ └── prod/
│ ├── account.hcl
│ ├── eks/
│ │ └── terragrunt.hcl
│ └── rds/
│ └── terragrunt.hcl
The Root terragrunt.hcl
This is the single source of truth for backend configuration. Every child config inherits it — no more copy-pasting S3 bucket names across 20 files.
| |
An Environment-Specific Module Config
Each environment’s component config just points at the shared module and overrides what’s different:
| |
The dev equivalent just changes instance types and sizing — the module and backend wiring are inherited automatically.
GitLab CI Integration
The pipeline uses OIDC to assume roles in each target account — no long-lived keys.
| |
Key Patterns to Internalize
Never run terragrunt run-all apply in production without a plan review step. run-all is great for dev teardowns and rebuilds. In prod, always plan individual components, review the output, then apply. The blast radius of a mis-targeted run-all is significant.
Keep modules generic, keep inputs specific. Your eks-cluster module shouldn’t know what environment it’s in. Pass everything — cluster name, sizing, feature flags — as inputs. This keeps modules reusable across projects, not just environments.
Use dependency blocks over data sources for cross-component references. If your RDS module needs the VPC ID from your VPC module, reference it via Terragrunt’s dependency block rather than a data.terraform_remote_state lookup. It makes the dependency graph explicit and enables --terragrunt-parallelism to work correctly.
| |
The full module library for this pattern is something I’m working toward open-sourcing. More posts in this series will cover the VPC module design, EKS add-on management via Helm, and the RDS Aurora module with automated pg_repack scheduling.