lesson ── terraform-advanced ── ~35 мин ── 7 шагов
Финальный capstone. Используешь всё, что научился: модули (intermediate),
testing (P2), policy-gate (P2), terraform_remote_state для разделения
stack'ов (advanced). Архитектура, типичный mini-platform: VPC,
ALB, ECS Fargate-сервис, Lambda-функция. Никакого EKS, LocalStack
Community его не поддерживает; ECS на Fargate, близкий аналог.
35 минут, это много. Если режется по времени, делай первые 4 шага (network + compute) и оставь serverless+tests на повторное прохождение.
интерактивный sandbox
Поднимется пара контейнеров: terraform 1.9 и localstack 3.8 в одной сети. В браузере откроется терминал, можно сразу terraform init. Каждый шаг проверяется автоматически. TTL 45 минут, без регистрации.
stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя
cd /home/student/capstone/modules/network
cat > main.tf <<'EOF'
resource "aws_vpc" "main" {cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = { Name = "capstone-vpc", Stack = "network" }}
resource "aws_subnet" "public" {count = 2
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
availability_zone = "us-east-1${["a", "b"][count.index]}"map_public_ip_on_launch = true
tags = { Name = "public-${count.index}", Tier = "public" }}
resource "aws_subnet" "private" {count = 2
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index + 10)
availability_zone = "us-east-1${["a", "b"][count.index]}" tags = { Name = "private-${count.index}", Tier = "private" }}
resource "aws_internet_gateway" "main" {vpc_id = aws_vpc.main.id
tags = { Name = "capstone-igw" }}
output "vpc_id" {value = aws_vpc.main.id
}
output "public_subnet_ids" {value = aws_subnet.public[*].id
}
output "private_subnet_ids" {value = aws_subnet.private[*].id
}
EOF
cp ../../provider.tf .
terraform init -no-color > /dev/null
terraform apply -auto-approve -no-color 2>&1 | tail -3
terraform output -json > /tmp/network-outputs.json
cat /tmp/network-outputs.json | jq -r '.vpc_id.value'
Network развёрнут: 1 VPC, 4 subnets, 1 IGW. Outputs сохранены.
✓ Network готова. Stack'у не нужно меняться долго.
cd /home/student/capstone/modules/compute
cat > main.tf <<'EOF'
data "terraform_remote_state" "network" {backend = "local"
config = { path = "../network/terraform.tfstate" }}
resource "aws_security_group" "alb" {name = "capstone-alb-sg"
vpc_id = data.terraform_remote_state.network.outputs.vpc_id
ingress {from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_lb" "main" {name = "capstone-alb"
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = data.terraform_remote_state.network.outputs.public_subnet_ids
}
resource "aws_lb_target_group" "app" {name = "capstone-app-tg"
port = 8080
protocol = "HTTP"
target_type = "ip"
vpc_id = data.terraform_remote_state.network.outputs.vpc_id
}
resource "aws_lb_listener" "http" {load_balancer_arn = aws_lb.main.arn
port = 80
default_action {type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}
resource "aws_ecs_cluster" "main" {name = "capstone-cluster"
}
output "alb_dns" {value = aws_lb.main.dns_name
}
output "ecs_cluster_name" {value = aws_ecs_cluster.main.name
}
EOF
cp ../../provider.tf .
terraform init -no-color > /dev/null
terraform apply -auto-approve -no-color 2>&1 | tail -3
terraform state list
Compute читает network через terraform_remote_state. Stack-coupling
явный, через outputs.
✓ Compute поднят. ALB + ECS-cluster готовы.
cd /home/student/capstone/modules/serverless
mkdir -p lambda-src
cat > lambda-src/handler.py <<'EOF'
def main(event, context):
return {"statusCode": 200, "body": "ok"}EOF
cat > main.tf <<'EOF'
data "terraform_remote_state" "network" {backend = "local"
config = { path = "../network/terraform.tfstate" }}
data "archive_file" "lambda" {type = "zip"
source_file = "${path.module}/lambda-src/handler.py" output_path = "${path.module}/lambda.zip"}
resource "aws_iam_role" "lambda" {name = "capstone-lambda-role"
assume_role_policy = jsonencode({Version = "2012-10-17"
Statement = [{Action = "sts:AssumeRole"
Effect = "Allow"
Principal = { Service = "lambda.amazonaws.com" }}]
})
}
resource "aws_cloudwatch_log_group" "lambda" {name = "/aws/lambda/capstone-lambda"
retention_in_days = 7
}
resource "aws_lambda_function" "demo" {function_name = "capstone-lambda"
filename = data.archive_file.lambda.output_path
source_code_hash = data.archive_file.lambda.output_base64sha256
handler = "handler.main"
runtime = "python3.12"
role = aws_iam_role.lambda.arn
depends_on = [aws_cloudwatch_log_group.lambda]
}
output "lambda_arn" {value = aws_lambda_function.demo.arn
}
EOF
cp ../../provider.tf .
terraform init -no-color > /dev/null
terraform apply -auto-approve -no-color 2>&1 | tail -3
terraform state list
Lambda с CloudWatch log group, IAM-role. Stack независим, читает только network для context'а (хотя в этом примере не использует VPC-config, но в реальности часто читает для VPC-attached Lambda).
✓ Serverless stack развёрнут. Три stack'а в работе.
Бизнес-правило: каждый ресурс с тегом Stack. Проверим compute-plan:
cd /home/student/capstone/modules/compute
terraform plan -no-color -out=plan.tfplan > /dev/null
terraform show -json plan.tfplan > plan.json
mkdir -p ../../policies
cat > ../../policies/tags.rego <<'EOF'
package main
import future.keywords.contains
import future.keywords.if
import future.keywords.in
deny contains msg if {some resource in input.resource_changes
# фильтр по типу, только большие ресурсы (не ALB-listener'ы и т.д.)
resource.type in {"aws_lb", "aws_ecs_cluster", "aws_lambda_function", "aws_vpc"}resource.change.actions[_] == "create"
tags := object.get(resource.change.after, "tags", {})not tags.Stack
msg := sprintf("%s missing Stack tag", [resource.address])}
EOF
conftest test plan.json --policy ../../policies/ 2>&1 | head -10
Compute-ресурсы не имеют Stack-тегов, gate должен поймать.
✓ Policy-gate сработал. На главных ресурсах compute нет Stack-тега.
cd /home/student/capstone/modules/network
mkdir -p tests
cat > tests/network.tftest.hcl <<'EOF'
mock_provider "aws" {}run "vpc_created_with_dns_enabled" {command = plan
assert {condition = aws_vpc.main.enable_dns_hostnames == true
error_message = "VPC must have DNS hostnames enabled"
}
assert {condition = aws_vpc.main.enable_dns_support == true
error_message = "VPC must have DNS support"
}
}
run "subnets_in_different_azs" {command = plan
assert {condition = aws_subnet.public[0].availability_zone != aws_subnet.public[1].availability_zone
error_message = "public subnets must span at least 2 AZs"
}
}
run "subnets_have_tier_tag" {command = plan
assert {condition = aws_subnet.public[0].tags["Tier"] == "public"
error_message = "public subnet must have Tier=public tag"
}
assert {condition = aws_subnet.private[0].tags["Tier"] == "private"
error_message = "private subnet must have Tier=private tag"
}
}
EOF
terraform test 2>&1 | tail -10
Три run'а pass'нулись с mock_provider, облако не нужно.
✓ Тесты на network прошли. Контракт модуля зафиксирован.
Проверяем что три stack'а связаны правильно:
# network outputs
cd /home/student/capstone/modules/network
VPC_ID=$(terraform output -raw vpc_id)
# compute видит VPC?
cd ../compute
COMPUTE_VPC=$(terraform state show aws_lb_target_group.app | grep vpc_id | awk -F'"' '{print $2}')echo "network VPC: $VPC_ID"
echo "compute VPC: $COMPUTE_VPC"
if [ "$VPC_ID" = "$COMPUTE_VPC" ]; then
echo "PASS: compute correctly references network VPC"
else
echo "FAIL: VPC mismatch"
fi
# serverless видит ту же сеть?
cd ../serverless
SERVERLESS_LAMBDA=$(terraform state show aws_lambda_function.demo | grep function_name | awk -F'"' '{print $2}')echo "serverless Lambda: $SERVERLESS_LAMBDA"
# finally
aws --endpoint-url=http://localstack:4566 lambda list-functions \
--query 'Functions[].FunctionName' --output text
Видишь: compute-VPC == network-VPC; Lambda существует. Cross-stack ссылки работают.
✓ Three-stack integration работает. Network, compute, serverless читают друг друга.
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 для полной матрицы.
Что показал capstone:
Stack-разделение по blast-radius. Network редкие changes, compute частые, serverless независим. Каждый, свой state, свой lock.
terraform_remote_state как контракт. Только outputs
кросс-stack. Внутреннее устройство network'а не видно compute'у.
Policy-gate на plan.json. OPA-правило Stack-tag required
ловит ошибку до apply. В CI этот gate блокирует merge.
.tftest.hcl с mock_provider. Unit-тесты на модуль без
облака, секунды на test-run.
LocalStack как полноценная среда. Можно учиться, тестировать, CI'ить, всё без AWS-bill.
Реальная команда расширяет это:
Всё это покрыто в KB; этот capstone, отправная точка, не финал.
echo "TerraformLab завершён."
✓ Курс пройден. Дальше, реальная работа.
Этот capstone, minimum-viable production-stack. Не покрыто:
Hashicorp + Gruntwork опубликовали reference architectures полезный next-step после этого курса:
Не пытайся написать всё с нуля, есть готовые модули, их используют тысячи команд.
Multi-stack архитектура: network (VPC, subnets), compute (ALB,
ECS, target groups), serverless (Lambda, log group). Между ними
terraform_remote_state. policy-gate через OPA, tests через
.tftest.hcl с mock_provider.
команды
terraform apply -auto-approveразворачивает один stack.terraform testзапускает unit-тесты модулей.conftest test plan.json --policy policies/policy-gate перед apply.концепции