The repo is the source of truth
Nothing is clicked together in the AWS console. If it exists, it is in code, and anyone can read how it was built.
Infrastructure as code
Every piece of AWS behind my products is code: Terraform modules for what to build, Terragrunt for where, in which account, and in what order. Here's how it's set up and why.
Why Terraform
Nothing is clicked together in the AWS console. If it exists, it is in code, and anyone can read how it was built.
A plan shows exactly what will be created, changed or destroyed. Infrastructure changes get the same review as application code.
Each module does one job: a Lambda, a queue wired to a worker, a CloudFront site. A new product reuses them instead of starting from a blank page.
AWS and Cloudflare live in the same codebase and the same workflow, so DNS and certificates are never a separate manual step.
Why Terragrunt on top
One root.hcl generates the provider, version and backend files for every unit. No backend block copied into 85 folders, and no drift between them.
A unit under infrastructure/production/ deploys to the production account, with the production role. The account comes from the path, not from flags someone can forget.
Each unit has its own state file, keyed by its path in one encrypted S3 bucket with native lockfiles. A bad change to one unit cannot corrupt another.
Units read each other’s outputs through dependency blocks: a site gets its certificate ARN and hosted zone ID from the units that created them. Nothing is hardcoded twice.
Account IDs, roles and tokens come from environment variables loaded by direnv. The .hcl files are safe to share.
Dependencies
A web app unit never hardcodes a certificate or a zone ID. It reads them from the units that created them.
Resolve: Find root.hcl and work out the AWS account and role from the unit’s folder path.
The code
A simplified, anonymised version of the real setup. Every unit is about this size.
include "root" {
path = find_in_parent_folders("root.hcl")
}
dependency "hosted_zone" {
config_path = "${get_repo_root()}/infrastructure/shared-services/route-53/example-zone"
}
dependency "certificate" {
config_path = "${get_repo_root()}/infrastructure/production/certificate/example-cert"
}
dependency "lambda" {
config_path = "${get_repo_root()}/infrastructure/production/lambda/example-web"
}
terraform {
source = "${get_repo_root()}/modules/cloudfront-sveltekit"
}
inputs = {
subdomain_name = "app.example.com"
hosted_zone_id = dependency.hosted_zone.outputs.hosted_zone_id
cloudfront_acm_certificate_arn = dependency.certificate.outputs.certificate_arn
lambda_function_arn = dependency.lambda.outputs.lambda_arn
}At a glance
| Concern | Terraform alone | With Terragrunt |
|---|---|---|
| Backend and provider config | Repeated in every root module | Generated once from root.hcl |
| Choosing the AWS account | Variables or workspaces per environment | Derived from the folder path, with a role per account |
| State | Tends to grow into a few large state files | One small state file per unit |
| Passing values between stacks | Remote state data sources, or hardcoded values | dependency blocks that read another unit’s outputs |
| Applying in the right order | Manual sequencing | terragrunt run --all follows the dependency graph |
This is the setup behind everything on my AWS stack, the multi-account setup that limits the blast radius, and my own products.
Questions
Terraform describes infrastructure as code and creates it through providers like AWS. Terragrunt, by Gruntwork, is a thin wrapper around Terraform that orchestrates many Terraform modules: it generates shared configuration such as providers and remote state, passes outputs between modules through dependency blocks, and runs them in the right order.
For one environment and a handful of resources, Terraform on its own is enough. Terragrunt pays off once you have several accounts or environments and many separate stacks, because otherwise the backend, provider and account configuration gets copied into every one of them and slowly drifts.
One encrypted S3 bucket holds every state file, and each Terragrunt unit gets its own key based on its folder path. Locking uses S3 native lockfiles, so there is no separate DynamoDB table. The backend is configured once in root.hcl and generated for every unit.
Each environment is a separate AWS account with its own role. Folders make that visible: the path a unit lives in decides which account it deploys to, it is obvious in code review, and development and production can hold different resources instead of being forced to mirror each other.
Yes. I can design the module and folder structure, set up multi-account state and roles, or review an existing setup that has grown hard to change. Get in touch with a short description of where you are now.
Setting up Terraform and Terragrunt from scratch, or untangling a setup that's grown hard to change. Let's talk it through.
Terraform is a trademark of HashiCorp. Terragrunt is a trademark of Gruntwork. AWS service icons are the official AWS Architecture Icons. Logos are used to identify the tools; no endorsement is implied.