lesson ── terraform-advanced ── ~18 мин ── 6 шагов
Terragrunt, обёртка Gruntwork над Terraform. Решает проблему
«directory-per-env» дублирования: один terragrunt.hcl на env, общий
через include. На уроке сделаешь dev и prod из одного модуля,
поймёшь generate-блоки и 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-параметры, обычно в реальном 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
Этот файл, общий конфиг. Каждый env включит его через include.
✓ Root terragrunt.hcl написан. Сейчас env'ы.
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/
Два env, одинаковая структура. Различие, только inputs.
✓ Env-roots готовы. Каждый указывает env-specific inputs.
cd /home/student/tg/live/dev
terragrunt apply --terragrunt-non-interactive -auto-approve 2>&1 | tail -20
Что произошло за кулисами:
live/dev/terragrunt.hcl.find_in_parent_folders().modules/app в .terragrunt-cache/.backend.tf и provider.tf в кэш-папке.terraform init + apply с inputs.Бакет создан в LocalStack.
aws --endpoint-url=http://localstack:4566 s3 ls
Видишь linuxlab-tg-dev-api.
✓ Dev развёрнут. Один HCL, один 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
Теперь два бакета: dev и prod. Из одного модуля, без копипасты.
path_relative_to_include() в root'е использует относительный путь
к env-директории как ключ в S3, dev/terraform.tfstate и
prod/terraform.tfstate, разные state'ы, разный lock.
✓ Prod развёрнут. Изоляция state'ов работает.
OpenTofu держит CLI и state совместимыми с Terraform по командам
этого шага: миграция обычно проходит через mv .terraform .terraform.bak; tofu init -upgrade. Но при первом переходе
сделай backup state и прогон на feature-branch - расхождения
концентрируются в новых фичах (variables в backend,
state-encryption, OCI registry-backed модули). См.
tf-opentofu-parity для полной матрицы.
Когда env'ов много, по одному запускать неудобно.
cd /home/student/tg/live
terragrunt run-all plan --terragrunt-non-interactive 2>&1 | tail -20
run-all plan идёт по всем terragrunt.hcl рекурсивно, строит DAG
зависимостей (если есть dependency-блоки), гоняет в правильном
порядке.
Для destroy:
terragrunt run-all destroy --terragrunt-non-interactive -auto-approve 2>&1 | tail -5
Snесёт обоих, в правильном reverse-порядке.
✓ Terragrunt оркестрация прошла. На 10+ env'ах разница ощутима.
Реальный сценарий: stack network создаёт VPC, stack app
читает его 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, чтобы terragrunt plan работал даже если network
ещё не создан. Полезно в init-фазе CI или для standalone plan'ов.
terragrunt run-all apply сам поймёт: сначала network, потом
app. DAG между stack'ами.
Структура: root terragrunt.hcl (общий provider, backend) + per-env
live/<env>/terragrunt.hcl (inputs, include). Generate-блоки
создают backend.tf/provider.tf на лету. terragrunt apply гоняет
в одном env; terragrunt run-all apply, все.
команды
terragrunt applyapply в текущем env.terragrunt run-all planplan по всем env, parallel.terragrunt run-all apply --terragrunt-non-interactiveapply на все, для CI.концепции