lesson ── terraform-advanced ── ~35 мин ── 7 шагов
The final capstone. You pull together everything you learned: modules
(intermediate), testing (P2), the policy gate (P2), and
terraform_remote_state for splitting stacks (advanced). The
architecture is a typical mini platform: VPC, ALB, an ECS Fargate
service, and a Lambda function. No EKS, LocalStack Community does not
support it; ECS on Fargate is the close equivalent.
35 minutes is a lot. If you are short on time, do the first 4 steps (network + compute) and leave serverless and tests for a second pass.
интерактивный 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'
The network is deployed: 1 VPC, 4 subnets, 1 IGW. Outputs are saved.
✓ Network is ready. This stack will not need to change for a long time.
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 reads the network through terraform_remote_state. The
coupling between stacks is explicit, through outputs.
✓ Compute is up. The ALB and the ECS cluster are ready.
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 with a CloudWatch log group and an IAM role. The stack is independent, it reads the network only for context (it does not use the VPC config in this example, but in practice you often read it for a VPC-attached Lambda).
✓ The serverless stack is deployed. Three stacks are running.
Business rule: every resource carries a Stack tag. Check the 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
# filter by type, only the large resources (not ALB listeners, etc.)
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
The compute resources have no Stack tags, so the gate should catch them.
✓ The policy gate fired. The main compute resources have no Stack tag.
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
Three runs pass with mock_provider, no cloud needed.
✓ The network tests passed. The module contract is locked in.
Check that the three stacks are wired together correctly:
# network outputs
cd /home/student/capstone/modules/network
VPC_ID=$(terraform output -raw vpc_id)
# does compute see the 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
# does serverless see the same network?
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
Notice: the compute VPC equals the network VPC, and the Lambda exists. The cross-stack references work.
✓ Three-stack integration works. Network, compute, and serverless read each other.
OpenTofu keeps its CLI and state compatible with Terraform for
the commands in this step: a migration usually goes through mv .terraform .terraform.bak; tofu init -upgrade. But on the first
switch, back up the state and do a run on a feature branch, 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.
What the capstone showed:
Splitting stacks by blast radius. Network changes rarely, compute often, serverless is independent. Each one has its own state and its own lock.
terraform_remote_state as a contract. Only outputs cross
between stacks. The internal structure of network is not visible
to compute.
Policy gate on plan.json. The OPA rule Stack-tag required
catches the error before apply. In CI this gate blocks the
merge.
.tftest.hcl with mock_provider. Unit tests on the module
without a cloud, seconds per test run.
LocalStack as a full environment. You can learn, test, and run CI, all of it without an AWS bill.
A real team extends this:
All of this is covered in the KB; this capstone is a starting point, not the end.
echo "TerraformLab complete."
✓ The course is complete. From here on, real work.
This capstone is a minimum-viable production stack. Not covered:
Hashicorp and Gruntwork have published reference architectures that are a useful next step after this course:
Do not try to write everything from scratch, there are ready modules that thousands of teams use.
A multi-stack architecture: network (VPC, subnets), compute (ALB,
ECS, target groups), serverless (Lambda, log group). Between them,
terraform_remote_state. The policy gate runs through OPA, the
tests through .tftest.hcl with mock_provider.
команды
terraform apply -auto-approvedeploys a single stack.terraform testruns the unit tests for the modules.conftest test plan.json --policy policies/policy gate before apply.концепции