Skip to content

Terraform_terragrunt

Terragrunt is a thin wrapper for Terraform that provides extra tools for working with multiple Terraform modules. It helps keep your configuration DRY (Don’t Repeat Yourself) and manages remote state across multiple components.

  • DRY Terraform: Avoid repeating provider/backend configurations
  • Multiple environments: Manage dev, staging, prod easily
  • Remote state management: Built-in S3/GCS state handling
  • Dependency management: Handle module dependencies
  • Plan/apply locking: Prevent concurrent executions
┌─────────────────────────────────────────────────────────────────┐
│ Terragrunt Architecture │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ terragrunt.hcl │ │
│ │ │ │
│ │ terraform { │ │
│ │ source = "git::https://github.com/module.git" │ │
│ │ } │ │
│ │ │ │
│ │ inputs = { │ │
│ │ instance_type = "t3.micro" │ │
│ │ } │ │
│ └──────────────────────┬──────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Terraform │ │
│ │ │ │
│ │ Generates: main.tf, variables.tf, providers.tf │ │
│ │ Runs: terraform init, plan, apply │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
terraform {
source = "git::https://github.com/example/modules.git//ec2?ref=v1.0.0"
}
inputs = {
instance_type = "t3.micro"
ami_id = "ami-0c55b159cbfafe1f0"
tags = {
Environment = "dev"
ManagedBy = "terragrunt"
}
}
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
template = <<EOF
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "%s"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
EOF
}
├── environments/
│ ├── dev/
│ │ ├── terragrunt.hcl
│ │ └── services/
│ │ └── app/
│ │ └── terragrunt.hcl
│ ├── staging/
│ │ └── ...
│ └── prod/
│ └── ...
environments/dev/terragru.hcl
locals {
environment = "dev"
}
inputs = {
environment = local.environment
}
services/app/terragrunt.hcl
dependencies {
paths = ["../database", "../vpc"]
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
subnet_ids = dependency.vpc.outputs.private_subnets
db_host = dependency.database.outputs.db_host
}
remote_state {
backend = "s3"
config = {
bucket = "my-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
s3_bucket_tags = {
Name = "Terraform State"
Environment = "Production"
}
}
generate = {
path = "backend.tf"
if_exists = "overwrite"
}
}
remote_state {
backend = "gcs"
config = {
bucket = "my-terraform-state"
prefix = "environments/prod"
project = "my-project"
}
}
Terminal window
# Initialize
terragrunt init
# Plan
terragrunt plan
terragrunt plan -out=plan.tfplan
# Apply
terragrunt apply
terragrunt apply plan.tfplan
# Destroy
terragrunt destroy
# Run all in directory
terragrunt run-all plan
terragrunt run-all apply
terragrunt run-all destroy
# Import
terragrunt import aws_instance.example i-1234567890abcdef0
# Validate
terragrunt validate
terragrunt validate-all
# Output
terragrunt output
terragrunt output -json > outputs.json
# Prevent concurrent runs
lock = {
enable = true
}
# Configure retry
retry {
max_attempts = 3
sleep = "5s"
}
# Generate provider configuration
generate = {
path = "providers.tf"
if_exists = "overwrite_terragrunt"
template = <<EOF
provider "aws" {
region = "${var.aws_region}"
default_tags {
tags = {
Environment = "${var.environment}"
ManagedBy = "Terragrunt"
}
}
}
provider "azurerm" {
features {}
skip_provider_registration = true
}
provider "google" {
project = "${var.gcp_project}"
region = "${var.gcp_region}"
}
EOF
}
Terminal window
# Always use --terragrunt-non-interactive
terragrunt plan --terragrunt-non-interactive
# Reuse tested modules
terraform {
source = "git::https://github.com/terraform-aws-modules/terraform-aws-vpc.git?ref=v3.0.0"
}
# Use descriptive state keys
key = "environments/${var.environment}/${path_relative_to_include()}/terraform.tfstate"
# Single module, different inputs per environment
inputs = {
instance_type = var.environment == "prod" ? "t3.medium" : "t3.micro"
}
name: Terragrunt
on: [push, pull_request]
jobs:
terragrunt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Terragrunt
uses: autero1/action-terragrunt@v1.0.0
with:
terragrunt_version: 0.40.0
- name: Terragrunt Plan
run: |
terragrunt run-all plan --terragrunt-non-interactive
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
stages:
- validate
- plan
- apply
variables:
TERRAGRUNT_VERSION: "0.40.0"
terragrunt-init:
stage: validate
image:
name: alpine/terragrunt:${TERRAGRUNT_VERSION}
entrypoint: [""]
script:
- terragrunt init
- terragrunt validate
terragrunt-plan:
stage: plan
image:
name: alpine/terragrunt:${TERRAGRUNT_VERSION}
entrypoint: [""]
script:
- terragrunt run-all plan --terragrunt-non-interactive
artifacts:
paths:
- plan.out
terragrunt-apply:
stage: apply
image:
name: alpine/terragrunt:${TERRAGRUNT_VERSION}
entrypoint: [""]
script:
- terragrunt run-all apply --terragrunt-non-interactive
when: manual
# Automatically use workspace as environment
locals {
workspace = runterragru.workspace
environment_map = {
"default" = "dev"
"production" = "prod"
}
environment = lookup(local.environment_map, local.workspace, local.workspace)
}
inputs = {
environment = local.environment
}

Terragrunt is essential for:

  • DRY configurations: Avoid repeating code
  • Multi-environment management: Dev, staging, prod
  • State management: Built-in remote state handling
  • Dependencies: Handle module dependencies
  • Team collaboration: Locking and coordination

Key concepts:

  • terragrunt.hcl: Configuration file
  • run-all: Execute across multiple modules
  • generate: Generate Terraform code
  • remote_state: Built-in state management
  • dependencies: Module dependencies