By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.
(For Terraform Engineers Who Need to Scale Without Losing Their Sanity)
You’re a cloud engineer at a mid-sized SaaS company. Your Terraform codebase has grown from a single main.tf file to dozens of modules, environments (dev/stage/prod), and AWS accounts. Every time you deploy, you’re manually copying terraform.tfvars files, updating backend configs, and praying you didn’t fat-finger a variable. One wrong terraform apply in prod, and you’re explaining to the CTO why the database just got nuked.
main.tf
terraform.tfvars
terraform apply
Terragrunt is your escape hatch. It’s a thin wrapper around Terraform that: - Eliminates copy-paste hell (DRY – Don’t Repeat Yourself).- Manages remote state backends (S3/DynamoDB) automatically.- Orchestrates dependencies between modules (e.g., "Deploy VPC before ECS").- Handles environment-specific configs (dev vs. prod) without duplicating code.
Without Terragrunt, you’ll:❌ Waste hours manually updating terraform.tfvars for each environment.❌ Risk state file corruption (e.g., two engineers running terraform apply in the same workspace).❌ Struggle to enforce consistency (e.g., "Why does dev have 3 AZs but prod has 2?").
With Terragrunt, you’ll:✅ Deploy identical infrastructure across dev/stage/prod with one command.✅ Lock down state files so only one engineer can modify them at a time.✅ Reuse modules without rewriting backend configs for every environment.
Real-world scenario:You inherit a Terraform repo where dev/, stage/, and prod/ each have their own main.tf and terraform.tfvars. Your mission: Refactor this into a single source of truth where: - Dev uses t3.micro instances.- Prod uses m5.large with auto-scaling.- All environments share the same VPC module but with different CIDR blocks.
dev/
stage/
prod/
t3.micro
m5.large
Terragrunt makes this trivial.
terragrunt.hcl
include
dependency
generate
backend.tf
remote_state
inputs
locals
env = "prod"
terraform
0.13
1.0
run_cmd
aws s3 sync
--terragrunt-working-dir
terragrunt run-all apply --terragrunt-working-dir prod
cd
✅ AWS account with admin IAM permissions (for simplicity; use least privilege in prod).✅ Terraform (>= 1.0.0) and Terragrunt (>= 0.38.0) installed.✅ AWS CLI configured (aws configure).✅ A GitHub/GitLab repo (or local folder) for your code.
>= 1.0.0
>= 0.38.0
aws configure
Create this directory structure:
terraform-live/ ├── _global/ # Shared configs (e.g., backend, providers) │ └── terragrunt.hcl ├── dev/ │ ├── vpc/ │ │ └── terragrunt.hcl │ └── terragrunt.hcl ├── stage/ │ ├── vpc/ │ │ └── terragrunt.hcl │ └── terragrunt.hcl └── prod/ ├── vpc/ │ └── terragrunt.hcl └── terragrunt.hcl
Create a reusable VPC module in a separate repo (or local folder):
terraform-modules/ └── vpc/ ├── main.tf ├── variables.tf └── outputs.tf
terraform-modules/vpc/main.tf
module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "3.14.0" name = var.vpc_name cidr = var.vpc_cidr azs = var.azs private_subnets = var.private_subnets public_subnets = var.public_subnets enable_nat_gateway = var.enable_nat_gateway single_nat_gateway = var.single_nat_gateway tags = var.tags }
terraform-modules/vpc/variables.tf
variable "vpc_name" { type = string } variable "vpc_cidr" { type = string } variable "azs" { type = list(string) } variable "private_subnets" { type = list(string) } variable "public_subnets" { type = list(string) } variable "enable_nat_gateway" { type = bool } variable "single_nat_gateway" { type = bool } variable "tags" { type = map(string) }
_global/terragrunt.hcl
# _global/terragrunt.hcl remote_state { backend = "s3" config = { bucket = "my-company-terraform-state-${get_aws_account_id()}" key = "${path_relative_to_include()}/terraform.tfstate" region = "us-east-1" encrypt = true dynamodb_table = "my-company-terraform-locks" } } generate "provider" { path = "provider.tf" if_exists = "overwrite_terragrunt" contents = <<EOF provider "aws" { region = "us-east-1" } EOF }
dev/terragrunt.hcl
# dev/terragrunt.hcl include { path = find_in_parent_folders() } locals { env = "dev" }
dev/vpc/terragrunt.hcl
# dev/vpc/terragrunt.hcl include { path = find_in_parent_folders() } terraform { source = "../../../../terraform-modules//vpc" } inputs = { vpc_name = "dev-vpc" vpc_cidr = "10.0.0.0/16" azs = ["us-east-1a", "us-east-1b"] private_subnets = ["10.0.1.0/24", "10.0.2.0/24"] public_subnets = ["10.0.101.0/24", "10.0.102.0/24"] enable_nat_gateway = true single_nat_gateway = true tags = { Environment = "dev" Terraform = "true" } }
Repeat for stage/ and prod/ with different CIDRs and instance sizes.
# Initialize and apply dev VPC cd terraform-live/dev/vpc terragrunt init terragrunt apply # Deploy all environments at once (from root) cd terraform-live terragrunt run-all apply
Expected output:
Apply complete! Resources: 12 added, 0 changed, 0 destroyed.
Verification:
aws ec2 describe-vpcs --filters "Name=tag:Environment,Values=dev"
✅ Least privilege IAM roles: Use aws_iam_role with minimal permissions.✅ Encrypt state files: Enable encrypt = true in remote_state.✅ Lock state files: Use DynamoDB (dynamodb_table) to prevent concurrent writes.✅ Secrets management: Use AWS Secrets Manager or Vault, not terraform.tfvars.
aws_iam_role
encrypt = true
dynamodb_table
✅ Right-size resources: Use t3.micro for dev, m5.large for prod.✅ Enable NAT Gateway only in prod: Set enable_nat_gateway = false in dev/stage.✅ Use spot instances for non-critical workloads.
enable_nat_gateway = false
✅ Tag everything: Environment, Terraform, Owner.✅ Use dependency blocks to enforce module order.✅ Pin Terraform/Terragrunt versions to avoid breaking changes.✅ Modularize code: One module per service (VPC, ECS, RDS).
Environment
Terraform
Owner
✅ Enable CloudTrail for API logging.✅ Use CloudWatch Alarms for critical resources (e.g., high CPU).✅ Tag resources for cost allocation (CostCenter, Project).
CostCenter
Project
dependency "vpc" { config_path = "../vpc" }
required_version = ">= 1.0.0"
Answer: Use Terragrunt with inputs in environment-specific terragrunt.hcl files.
"How do you prevent state file corruption when multiple engineers run terraform apply?"
Answer: Use DynamoDB for state locking (dynamodb_table in remote_state).
"How do you enforce module deployment order (e.g., VPC before ECS)?"
"You need to deploy a VPC in dev/stage/prod with different CIDR blocks. How do you avoid duplicating code?"- Answer: Use Terragrunt with: - A shared VPC module (terraform-modules/vpc). - Environment-specific terragrunt.hcl files with inputs for CIDR blocks.
terraform-modules/vpc
Challenge:Deploy a multi-environment ECS cluster (dev/stage/prod) where: - Dev uses FARGATE_SPOT for cost savings.- Prod uses FARGATE with auto-scaling.- All environments share the same VPC module.
FARGATE_SPOT
FARGATE
Solution:1. Create an ECS module (terraform-modules/ecs).2. In dev/ecs/terragrunt.hcl: hcl inputs = { capacity_provider = "FARGATE_SPOT" desired_count = 1 } 3. In prod/ecs/terragrunt.hcl: hcl inputs = { capacity_provider = "FARGATE" desired_count = 2 min_capacity = 2 max_capacity = 10 } 4. Deploy with terragrunt run-all apply.
terraform-modules/ecs
dev/ecs/terragrunt.hcl
hcl inputs = { capacity_provider = "FARGATE_SPOT" desired_count = 1 }
prod/ecs/terragrunt.hcl
hcl inputs = { capacity_provider = "FARGATE" desired_count = 2 min_capacity = 2 max_capacity = 10 }
terragrunt run-all apply
Why it works:Terragrunt inherits the VPC config via dependency and overrides ECS settings per environment.
terragrunt init
terragrunt apply
terragrunt output
terragrunt destroy
terragrunt run-all destroy
backend = "s3"
terraform-locks
config_path = "../vpc"
vpc_cidr = "10.0.0.0/16"
path = find_in_parent_folders()
Final Tip:Terragrunt doesn’t replace Terraform—it enhances it. Start small (e.g., one module), then scale. Your future self will thank you. ?
Join 4M+ learners. Unlock unlimited quizzes, wrong-answer tracking, flashcards + reminders, study guides, and 1-on-1 challenges.