Comprehensive toolkit for validating, linting, testing, and automating Terragrunt configurations, HCL files, and Stacks. Use this skill when working with Terragrunt files (.hcl, terragrunt.hcl, terragrunt.stack.hcl), validating infrastructure-as-code, debugging Terragrunt configurations, performing dry-run testing with terragrunt plan, working with Terragrunt Stacks, or working with custom providers and modules.
Overall
score
92%
Does it follow best practices?
Validation for skill structure
This reference document provides best practices, common patterns, and anti-patterns for Terragrunt configurations. Use this as a guide when validating or creating Terragrunt code.
infrastructure/
├── terragrunt.hcl # Root Terragrunt config
├── common.hcl # Shared configuration
├── prod/
│ ├── terragrunt.hcl # Environment-level config
│ ├── vpc/
│ │ └── terragrunt.hcl # Module-specific config
│ ├── database/
│ │ └── terragrunt.hcl
│ └── app/
│ └── terragrunt.hcl
├── staging/
│ └── ... (similar structure)
└── dev/
└── ... (similar structure)❌ Avoid flat structures without environment separation:
infrastructure/
├── vpc.hcl
├── database.hcl
├── app.hclinclude for Shared Configuration✅ Good Practice:
# Root terragrunt.hcl
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"
}
}
# Child terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}read_terragrunt_config for Shared Variables✅ Good Practice:
# common.hcl
locals {
region = "us-east-1"
environment = "prod"
tags = {
Terraform = "true"
Environment = local.environment
}
}
# terragrunt.hcl
locals {
common = read_terragrunt_config(find_in_parent_folders("common.hcl"))
}
inputs = {
region = local.common.locals.region
tags = local.common.locals.tags
}✅ Good Practice:
dependency "vpc" {
config_path = "../vpc"
}
dependency "database" {
config_path = "../database"
# Mock outputs for validation
mock_outputs = {
endpoint = "mock-db-endpoint"
port = 5432
}
# Allow mock outputs during plan
mock_outputs_allowed_terraform_commands = ["validate", "plan"]
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
database_endpoint = dependency.database.outputs.endpoint
}❌ Avoid accessing remote state directly:
# This makes dependencies unclear
inputs = {
vpc_id = data.terraform_remote_state.vpc.outputs.vpc_id
}✅ Good Practice:
dependency "network" {
config_path = "../network"
mock_outputs = {
vpc_id = "vpc-mock123"
subnet_ids = ["subnet-mock1", "subnet-mock2"]
}
mock_outputs_allowed_terraform_commands = ["validate", "plan", "init"]
mock_outputs_merge_strategy_with_state = "shallow"
}This allows running terragrunt plan without deploying dependencies first.
generate for Provider Configuration✅ Good Practice:
generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
provider "aws" {
region = "${local.region}"
assume_role {
role_arn = "arn:aws:iam::${local.account_id}:role/TerraformRole"
}
default_tags {
tags = ${jsonencode(local.tags)}
}
}
EOF
}generate for Backend Configuration✅ Good Practice:
generate "backend" {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
terraform {
backend "s3" {}
}
EOF
}inputs Block✅ Good Practice:
inputs = {
environment = local.environment
region = local.region
# Use dependency outputs
vpc_id = dependency.vpc.outputs.vpc_id
# Use functions
instance_count = get_env("INSTANCE_COUNT", 3)
# Merge tags
tags = merge(
local.common_tags,
{
Module = "app"
}
)
}❌ Avoid repeating the same inputs:
# Don't do this across multiple modules
inputs = {
region = "us-east-1" # Repeated everywhere
tags = { # Repeated everywhere
Terraform = "true"
}
}✅ Good Practice:
terraform {
source = "tfr:///terraform-aws-modules/vpc/aws?version=5.1.0"
}
generate "versions" {
path = "versions.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
EOF
}get_env with Defaults✅ Good Practice:
locals {
account_id = get_env("AWS_ACCOUNT_ID", "")
}
# Validate required environment variables
inputs = {
account_id = local.account_id != "" ? local.account_id : run_cmd("--terragrunt-quiet", "aws", "sts", "get-caller-identity", "--query", "Account", "--output", "text")
}try for Optional Values✅ Good Practice:
locals {
env_config = read_terragrunt_config(find_in_parent_folders("env.hcl", "empty.hcl"))
# Safely access potentially missing values
instance_type = try(local.env_config.locals.instance_type, "t3.micro")
}❌ Bad:
inputs = {
region = "us-east-1" # Hardcoded
account_id = "123456789012" # Hardcoded
}✅ Good:
locals {
region = get_env("AWS_REGION", "us-east-1")
account_id = get_aws_account_id()
}
inputs = {
region = local.region
account_id = local.account_id
}❌ Bad:
dependency "vpc" {
config_path = "../vpc"
# No mock outputs - can't validate without deploying vpc
}❌ Bad:
infrastructure/
└── prod/
└── us-east-1/
└── vpc/
└── public/
└── subnet-1/
└── terragrunt.hcl✅ Good:
infrastructure/
└── prod/
└── vpc/
└── terragrunt.hcl # Configure all subnets here❌ Bad:
# Manually maintaining paths
remote_state {
config = {
key = "prod/vpc/terraform.tfstate"
}
}✅ Good:
remote_state {
config = {
key = "${path_relative_to_include()}/terraform.tfstate"
}
}remote_state {
backend = "s3"
config = {
encrypt = true
kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/..."
}
}generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
provider "aws" {
assume_role {
role_arn = "arn:aws:iam::${local.account_id}:role/TerraformRole"
}
}
EOF
}remote_state {
backend = "s3"
config = {
dynamodb_table = "terraform-locks"
}
}inputs = {
# Mark sensitive inputs
database_password = get_env("DB_PASSWORD") # Never hardcode
}Note: Terragrunt 0.93+ uses a redesigned CLI with significant changes:
run-all is deprecated → use run --allhclfmt is deprecated → use hcl fmtvalidate-inputs is deprecated → use hcl validate --inputsgraph-dependencies is deprecated → use dag graph--terragrunt-non-interactive flag is no longer needed or supported# Format check (new syntax)
terragrunt hcl fmt --check
# Input validation (new in 0.93+)
terragrunt hcl validate --inputs
# Initialize (required for validation)
terragrunt init
# Validate Terraform configuration
terragrunt validate
# Generate plan
terragrunt planrun --all for Multi-Module OperationsNote:
run-allis deprecated. Userun --allinstead.
# Validate all modules
terragrunt run --all validate
# Plan all modules
terragrunt run --all plan
# Apply all modules
terragrunt run --all apply
# With strict mode (errors on deprecated features)
terragrunt --strict-mode run --all plan
# Or via environment variable
TG_STRICT_MODE=true terragrunt run --all plandependency "vpc" {
config_path = "../vpc"
# Only fetch specific outputs
mock_outputs_merge_strategy_with_state = "shallow"
}# Run operations in parallel (new syntax)
terragrunt run --all apply --parallelism 4
# Legacy syntax (deprecated)
# terragrunt run-all apply --terragrunt-parallelism=4# Cache downloaded modules
terraform {
source = "tfr:///terraform-aws-modules/vpc/aws?version=5.1.0"
}Symptom: "Cycle detected in dependency graph"
Solution:
Symptom: "Error acquiring the state lock"
Solution:
# Force unlock (use with caution)
terragrunt force-unlock <LOCK_ID>Symptom: "Module not found"
Solution:
# Clear cache and reinitialize
rm -rf .terragrunt-cache
terragrunt initSpecify minimum Terragrunt version:
# For new CLI features (recommended)
terragrunt_version_constraint = ">= 0.93.0"
# For backwards compatibility with older features
# terragrunt_version_constraint = ">= 0.48.0"terraform_version_constraint = ">= 1.6.0, < 2.0.0"Install with Tessl CLI
npx tessl i pantheon-ai/terragrunt-validator@0.1.1