lesson ── terraform-advanced ── ~18 мин ── 6 шагов
Terragrunt, the Gruntwork wrapper around Terraform. It solves the
"directory-per-env" duplication problem: one terragrunt.hcl per env,
shared through include. In this lesson you build dev and prod from a single
module, and you get a feel for generate blocks and dependency.
интерактивный sandbox
Поднимется пара контейнеров: terraform 1.9 и localstack 3.8 в одной сети. В браузере откроется терминал, можно сразу terraform init. Каждый шаг проверяется автоматически. TTL 45 минут, без регистрации.
stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя
cat > live/terragrunt.hcl <<'EOF'
remote_state {backend = "s3"
config = {bucket = "tf-state-tg-demo"
key = "${path_relative_to_include()}/terraform.tfstate"region = "us-east-1"
encrypt = true
s3_bucket_tags = {ManagedBy = "terragrunt"
}
# LocalStack settings, usually not needed against real AWS
endpoints = {s3 = "http://localstack:4566"
dynamodb = "http://localstack:4566"
}
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
force_path_style = true
}
generate = {path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
}
generate "provider" {path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<HCL
provider "aws" {region = "us-east-1"
access_key = "test"
secret_key = "test"
s3_use_path_style = true
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
endpoints {s3 = "http://localstack:4566"
iam = "http://localstack:4566"
sts = "http://localstack:4566"
dynamodb = "http://localstack:4566"
}
}
HCL
}
EOF
This file is the shared config. Each env pulls it in through include.
✓ Root terragrunt.hcl is written. Now the envs.
cat > live/dev/terragrunt.hcl <<'EOF'
include "root" {path = find_in_parent_folders()
}
terraform {source = "../../modules/app"
}
inputs = {env = "dev"
app_name = "api"
}
EOF
cat > live/prod/terragrunt.hcl <<'EOF'
include "root" {path = find_in_parent_folders()
}
terraform {source = "../../modules/app"
}
inputs = {env = "prod"
app_name = "api"
}
EOF
tree live/ modules/
Two envs, the same structure. The only difference is inputs.
✓ The env roots are ready. Each one points at env-specific inputs.
cd /home/student/tg/live/dev
terragrunt apply --terragrunt-non-interactive -auto-approve 2>&1 | tail -20
What happened behind the scenes:
live/dev/terragrunt.hcl.find_in_parent_folders().modules/app into .terragrunt-cache/.backend.tf and provider.tf in the cache folder.terraform init + apply with the inputs.The bucket is created in LocalStack.
aws --endpoint-url=http://localstack:4566 s3 ls
You see linuxlab-tg-dev-api.
✓ Dev is deployed. One HCL, one env.
cd /home/student/tg/live/prod
terragrunt apply --terragrunt-non-interactive -auto-approve 2>&1 | tail -10
aws --endpoint-url=http://localstack:4566 s3 ls
Now there are two buckets: dev and prod. From a single module, no copy-paste.
path_relative_to_include() in the root uses the relative path to the
env directory as the key in S3, dev/terraform.tfstate and
prod/terraform.tfstate, separate states with separate locks.
✓ Prod is deployed. State isolation works.
OpenTofu keeps its CLI and state compatible with Terraform for the
commands in this step: migration usually goes through mv .terraform .terraform.bak; tofu init -upgrade. On your first switch, though, back
up the state and run it on a feature branch first, the differences
cluster in the newer features (variables in the backend,
state encryption, OCI registry-backed modules). See
tf-opentofu-parity for the full matrix.
When you have many envs, running them one at a time gets tedious.
cd /home/student/tg/live
terragrunt run-all plan --terragrunt-non-interactive 2>&1 | tail -20
run-all plan walks every terragrunt.hcl recursively, builds a
dependency DAG (if there are dependency blocks), and runs them in the
right order.
For destroy:
terragrunt run-all destroy --terragrunt-non-interactive -auto-approve 2>&1 | tail -5
It tears down both, in the correct reverse order.
✓ The Terragrunt orchestration ran. Across 10+ envs the difference is real.
A real scenario: the network stack creates the VPC, the app stack
reads its id.
# live/prod/app/terragrunt.hcl
include "root" {path = find_in_parent_folders()
}
terraform {source = "../../../modules/app"
}
dependency "network" {config_path = "../network"
mock_outputs = {vpc_id = "mock-vpc"
subnet_ids = ["mock-subnet"]
}
}
inputs = {env = "prod"
vpc_id = dependency.network.outputs.vpc_id
subnet_ids = dependency.network.outputs.subnet_ids
}
mock_outputs lets terragrunt plan work even when network has not
been created yet. Handy in the init phase of CI, or for standalone plans.
terragrunt run-all apply works it out on its own: network first, then
app. A DAG across the stacks.
Structure: a root terragrunt.hcl (shared provider, backend) plus a
per-env live/<env>/terragrunt.hcl (inputs, include). Generate blocks
create backend.tf/provider.tf on the fly. terragrunt apply runs
inside a single env; terragrunt run-all apply runs across all of them.
команды
terragrunt applyapply in the current env.terragrunt run-all planplan across all envs, in parallel.terragrunt run-all apply --terragrunt-non-interactiveapply to all envs, for CI.концепции