Terragrunt is the deployment orchestrator. It handles remote state, provider generation, and multi-account role assumption.
- Always use
terragruntCLI commands — never calltofudirectly for deployment - When running commands across all modules:
terragrunt run-all <command> - When targeting a single module: run
terragrunt <command>from within itslive/directory - Terragrunt version managed by mise (
terragrunt = "1.0.0")
project.hcl— single source of truth for org-wide values (project name, account IDs, emails, org/OU IDs)root.hcl— remote state config (S3 bucket naming:{project}-{env}-terraform-state) and provider generation with role assumption (terragrunt-execution-role)account.hcl— account name and AWS account ID onlyenv.hcl— all module configuration values for a given account+region scope; includesskip_modulemap andtagsblock
Each module deployment is a thin terragrunt.hcl that:
- Sources either a local module (
../../../../modules/{name}) or external git module with pinned ref - Includes
root.hcl(for backend/provider) - Includes
env.hclwithexpose = trueandmerge_strategy = "no_merge" - Passes inputs from
include.env.locals.* - Uses
excludeblock withinclude.env.locals.skip_module.{name}for conditional deployment
- Always include
root.hclfor backend/provider - Include
env.hclwithexpose = trueandmerge_strategy = "no_merge" - Pass inputs from
include.env.locals.* - Add
excludeblock referencingskip_modulemap - Source local modules as
../../../../modules/{name}or external with pinned ref
Each env.hcl defines a skip_module map with boolean values per module. Set to true to exclude a module from deployment without removing its directory:
skip_module = {
vpc = false
s3 = true # skipped
}Pin external modules with exact git refs (no latest, no branch names):
terraform {
source = "git::https://github.com/terraform-aws-modules/terraform-aws-vpc.git?ref=v6.0.1"
}- S3 state buckets:
{project}-{account}-terraform-state - Variable names in env.hcl: snake_case, prefixed by module context (
vpc_cidr,budget_limit_amount)
This project uses S3 native state locking (use_lockfile = true) instead of DynamoDB. This requires OpenTofu >= 1.10 (we use 1.11.2). S3 creates a .tflock file next to the state file using conditional writes — no DynamoDB table needed.
- Do NOT use
dynamodb_tablein backend config — it is deprecated - Do NOT create DynamoDB lock tables for new accounts
- If migrating an existing account from DynamoDB locking, remove the
dynamodb_tableargument and adduse_lockfile = true
All resources must be tagged. Tags are defined once in env.hcl and passed to modules via inputs.
Every resource must include these tags (no exceptions):
| Tag | Value | Description |
|---|---|---|
AccountType |
platform or workload |
Distinguishes shared infra accounts from workload accounts |
CreatedBy |
terragrunt |
Always this value for IaC-managed resources |
Environment |
{account_name} |
Matches the account name (e.g., production, development) |
Owner |
{project} |
Project name from project.hcl |
Project |
{project} |
Project name from project.hcl |
Version |
{project_version} |
Semantic version from project.hcl |
tags = {
AccountType = "platform" # or "workload"
CreatedBy = "terragrunt"
Environment = "${local.env}"
Owner = "${local.project}"
Project = "${local.project}"
Version = "${local.project_version}"
}- When creating a new
env.hcl, always include the fulltagsblock — copy from existing env files - When creating a new module, accept a
tagsvariable of typemap(string)and apply it to all resources - Never hardcode tag values inside modules — they must come from
env.hclvia inputs AccountTypemust be"platform"for shared accounts (management, security, shared-services, monitoring) and"workload"for application accounts (development, production, sandbox)- Never add tags that contain sensitive info (account IDs, secrets, internal URLs)
- If a resource supports
tags_all(e.g., AWS provider default tags), prefer passing tags via the provider; otherwise pass explicitly per resource
Every module that creates taggable resources must include:
variable "tags" {
description = "Tags to apply to all resources"
type = map(string)
default = {}
}And apply in resources:
resource "aws_whatever" "this" {
# ...
tags = var.tags
}# Plan all modules in an account/region
terragrunt run-all plan
# Apply a single module
terragrunt apply # (from within live/{account}/{region}/{module}/)
# Format all HCL
terragrunt run-all fmt
# Validate all modules
terragrunt run-all validate- Semantic versioning via semantic-release
- Conventional commits required (enforced by commitizen)
- Version tracked in
project.hclasproject_version