Fatskills
Practice. Master. Repeat.
Study Guide: TECH **Terragrunt for Environment Orchestration: Zero-Fluff, Hands-On Guide**
Source: https://www.fatskills.com/cloud-application-developer/chapter/tech-terragrunt-for-environment-orchestration-zero-fluff-hands-on-guide

TECH **Terragrunt for Environment Orchestration: Zero-Fluff, Hands-On Guide**

By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.

⏱️ ~8 min read

Terragrunt for Environment Orchestration: Zero-Fluff, Hands-On Guide

(For Terraform Engineers Who Need to Scale Without Losing Their Sanity)


1. What This Is & Why It Matters

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.

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.

Terragrunt makes this trivial.


2. Core Concepts & Components


1. terragrunt.hcl

  • Definition: The configuration file Terragrunt uses to generate Terraform commands. Lives in each environment/module directory.
  • Production insight: If you don’t version-control terragrunt.hcl, you’ll lose track of which config was used for which deployment. Always commit it.

2. include Block

  • Definition: Lets you inherit configurations from a parent terragrunt.hcl (e.g., shared backend settings).
  • Production insight: Without include, you’ll duplicate backend configs in every environment, leading to state file fragmentation.

3. dependency Block

  • Definition: Declares module dependencies (e.g., "Deploy VPC before ECS").
  • Production insight: If you don’t use dependency, Terragrunt will deploy modules in random order, causing race conditions (e.g., ECS cluster tries to launch before VPC exists).

4. generate Block

  • Definition: Dynamically generates Terraform files (e.g., backend configs, provider blocks).
  • Production insight: Without generate, you’ll manually write backend.tf for every environment, violating DRY principles.

5. remote_state Block

  • Definition: Configures Terraform remote state (S3/DynamoDB) for a module.
  • Production insight: If you don’t lock state files (DynamoDB), two engineers can overwrite each other’s changes, corrupting your infrastructure.

6. inputs Block

  • Definition: Passes variables to Terraform modules (replaces terraform.tfvars).
  • Production insight: Without inputs, you’ll hardcode values in modules, making them impossible to reuse across environments.

7. locals Block

  • Definition: Defines local variables (e.g., environment names, tags) for reuse in terragrunt.hcl.
  • Production insight: Without locals, you’ll repeat the same values (e.g., env = "prod") across files, increasing the risk of typos.

8. terraform Block

  • Definition: Specifies Terraform version and source module (e.g., GitHub, Terraform Registry).
  • Production insight: If you don’t pin Terraform versions, upgrades can break your deployments (e.g., 0.131.0 syntax changes).

9. run_cmd

  • Definition: Runs arbitrary shell commands before/after Terraform (e.g., aws s3 sync for static assets).
  • Production insight: Without run_cmd, you’ll manually run scripts before/after terraform apply, increasing human error.

10. --terragrunt-working-dir

  • Definition: CLI flag to run Terragrunt in a specific directory (e.g., terragrunt run-all apply --terragrunt-working-dir prod).
  • Production insight: Without this, you’ll cd into directories manually, slowing down deployments.


3. Step-by-Step Hands-On: Deploy a Multi-Environment VPC with Terragrunt


Prerequisites

✅ 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.

Step 1: Project Structure

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

Step 2: Define the VPC Module (Terraform)

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)
}

Step 3: Configure Global Backend (_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 }

Step 4: Define Environment-Specific Configs

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.

Step 5: Deploy with Terragrunt

# 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"


4. ? Production-Ready Best Practices


Security

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.

Cost Optimization

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.

Reliability & Maintainability

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).

Observability

Enable CloudTrail for API logging.
Use CloudWatch Alarms for critical resources (e.g., high CPU).
Tag resources for cost allocation (CostCenter, Project).


5. ⚠️ Common Mistakes & Traps

Mistake Symptom Fix/Prevention
Not using dependency blocks ECS cluster fails to launch because VPC doesn’t exist. Declare dependencies explicitly: dependency "vpc" { config_path = "../vpc" }
Hardcoding backend configs State files get scattered across S3 buckets. Use include to inherit backend configs from _global/terragrunt.hcl.
Not pinning Terraform versions terraform apply fails after an upgrade. Pin versions in terraform block: required_version = ">= 1.0.0"
Using terraform.tfvars instead of inputs Variables get out of sync across environments. Use inputs in terragrunt.hcl for environment-specific values.
Not locking state files (DynamoDB) Two engineers overwrite each other’s changes. Always enable dynamodb_table in remote_state.


6. ? Exam/Certification Focus


Typical Question Patterns

  1. "How do you deploy the same Terraform module across dev/stage/prod with different configs?"
  2. Answer: Use Terragrunt with inputs in environment-specific terragrunt.hcl files.

  3. "How do you prevent state file corruption when multiple engineers run terraform apply?"

  4. Answer: Use DynamoDB for state locking (dynamodb_table in remote_state).

  5. "How do you enforce module deployment order (e.g., VPC before ECS)?"

  6. Answer: Use dependency blocks in terragrunt.hcl.

Key ⚠️ Trap Distinctions

  • Terraform vs. Terragrunt:
  • Terraform = infrastructure definition.
  • Terragrunt = orchestration (DRY, dependencies, backends).
  • include vs. dependency:
  • include = inherit configs (e.g., backend).
  • dependency = enforce deployment order.

Scenario-Based Question

"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.


7. ? Hands-On Challenge

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.

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.

Why it works:
Terragrunt inherits the VPC config via dependency and overrides ECS settings per environment.


8. ? Rapid-Reference Crib Sheet

Command Purpose
terragrunt init Initialize Terragrunt (downloads providers/modules).
terragrunt apply Apply changes to a single module.
terragrunt run-all apply Apply changes to all modules in a directory.
terragrunt output Show outputs from a module.
terragrunt destroy Destroy a single module.
terragrunt run-all destroy Destroy all modules in a directory.
Key Config Default/Example
remote_state backend = "s3"
dynamodb_table ⚠️ Required for locking (e.g., terraform-locks)
dependency config_path = "../vpc"
inputs vpc_cidr = "10.0.0.0/16"
include path = find_in_parent_folders()


9. ? Where to Go Next

  1. Terragrunt Official Docs – Best reference.
  2. Gruntwork’s Terragrunt Examples – Real-world repo.
  3. Terraform Best Practices – DRY, modularization.
  4. AWS Well-Architected Framework – For production-grade setups.

Final Tip:
Terragrunt doesn’t replace Terraform—it enhances it. Start small (e.g., one module), then scale. Your future self will thank you. ?



ADVERTISEMENT