Lesson 5: Advanced HCL Mastery & Phase 2 Hands-On Lab Solutions
This module covers advanced Terraform HCL constructs (dynamic blocks, lifecycle rules, depends_on) and provides end-to-end solutions for the Phase 2 Job-Essential Exercises.
π§© Advanced HCL Mastery
1. The dynamic Block Pattern
Avoid repeating multiple nested blocks (like Security Group rules or S3 lifecycle rules). Use dynamic blocks to generate them from a map or list:
variable "ingress_rules" {
type = list(object({
port = number
description = string
}))
default = [
{ port = 80, description = "HTTP Web Traffic" },
{ port = 443, description = "HTTPS Web Traffic" },
{ port = 8080, description = "API Microservice" }
]
}
resource "aws_security_group" "web_lb" {
name = "alb-security-group"
description = "Dynamic ingress rules generated from variable list"
vpc_id = var.vpc_id
dynamic "ingress" {
for_each = var.ingress_rules
content {
description = ingress.value.description
from_port = ingress.value.port
to_port = ingress.value.port
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"]
}
}
2. Resource lifecycle Rules
create_before_destroy = true: Replaces resources with zero downtime (e.g. creating the new Auto Scaling Launch Configuration before destroying the old one).prevent_destroy = true: Protects critical persistent storage (e.g. state buckets, production RDS databases) from accidental deletion viaterraform destroy.ignore_changes = [tags, desired_count]: Tells Terraform to ignore changes made by external systems (e.g. Kubernetes Karpenter autoscaling pod replicas, or AWS Auto Scaling adjusting instance counts).
resource "aws_instance" "app_node" {
ami = var.ami_id
instance_type = var.instance_type
lifecycle {
create_before_destroy = true
ignore_changes = [
tags["LastScannedTime"], # Ignore tags injected by external security scanner
]
}
}
π οΈ Lab 1: S3 & DynamoDB Remote Backend Concurrency Test
Objective
Create an S3 bucket and DynamoDB locking table, configure a Terraform backend block, and simulate a race condition by running terraform apply in two terminal windows simultaneously.
# backend-bootstrap/main.tf (Run this once to create the backend infrastructure)
resource "aws_s3_bucket" "state" {
bucket = "corp-platform-tfstate-lock-test"
lifecycle { prevent_destroy = true }
}
resource "aws_s3_bucket_versioning" "state_versioning" {
bucket = aws_s3_bucket.state.id
versioning_configuration { status = "Enabled" }
}
resource "aws_dynamodb_table" "locks" {
name = "corp-platform-tfstate-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
Race Condition Simulation Test
- Add a
time_sleepresource that pauses Terraform for 45 seconds: - Open Terminal A: Run
terraform apply -auto-approve. (Acquires lock and pauses). - Immediately open Terminal B: Run
terraform apply.
Expected Result in Terminal B:
Error: Error acquiring the state lock
Lock Info:
ID: 87291a45-6672-9214-5d9b-11728192a830
Path: corp-platform-tfstate-lock-test/env/terraform.tfstate
Operation: OperationTypeApply
Who: ar@ARs-MacBook-Pro.local
Created: 2026-10-09 15:00:02 UTC
Terraform acquires a state lock to protect the state from being written
by multiple users at the same time. Please resolve the issue above and try again.
π οΈ Lab 2: The Multi-Account Cross-Deployer
Objective
Deploy an S3 bucket in Account A and an SQS queue in Account B in a single atomic terraform apply.
# Provider for Management/Primary Account A
provider "aws" {
alias = "account_a"
region = "us-east-1"
}
# Provider for Workload Account B using Role Assumption
provider "aws" {
alias = "account_b"
region = "us-east-1"
assume_role {
role_arn = "arn:aws:iam::987654321098:role/OrganizationAccountAccessRole"
}
}
resource "aws_s3_bucket" "bucket_in_a" {
provider = aws.account_a
bucket = "corp-shared-artifacts-account-a"
}
resource "aws_sqs_queue" "queue_in_b" {
provider = aws.account_b
name = "workload-processing-queue-account-b"
}
π οΈ Lab 3: The State Import Challenge
Objective
You find an EC2 instance that an engineer created manually in the AWS Console. Bring it under Terraform code management without destroying or rebooting it.
- Step 1: Write the minimal Terraform block in
main.tf: - Step 2: Execute the import command pointing to the AWS instance ID:
- Step 3: Run
terraform plan. Terraform will show discrepancies between your minimal.tffile and reality. Update your code untilterraform plansays:
π οΈ Lab 4: Shift-Left Checkov Security Gate
Objective
Create an intentionally insecure S3 bucket, run Checkov to catch the violation, fix the code, and achieve a clean pass.
1. Insecure Code (insecure_s3.tf)
2. Run Checkov
Output:FAILED for resource: aws_s3_bucket.test_bucket (CKV_AWS_144: Ensure that S3 bucket has cross-region replication enabled, CKV_AWS_145: Ensure that S3 bucket has encryption enabled).
3. Hardened Code (hardened_s3.tf)
resource "aws_s3_bucket" "test_bucket" {
bucket = "corp-customer-hardened-data"
}
resource "aws_s3_bucket_server_side_encryption_configuration" "encrypt" {
bucket = aws_s3_bucket.test_bucket.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_public_access_block" "block_public" {
bucket = aws_s3_bucket.test_bucket.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
4. Re-run Checkov
Output:Passed checks: 5, Failed checks: 0, Skipped checks: 0. PASSED!