Skip to content

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 via terraform 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

  1. Add a time_sleep resource that pauses Terraform for 45 seconds:
    resource "time_sleep" "wait_45_seconds" {
      create_duration = "45s"
    }
    
  2. Open Terminal A: Run terraform apply -auto-approve. (Acquires lock and pauses).
  3. 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.
(State lock verified! Concurrency failure prevented).


πŸ› οΈ 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.

  1. Step 1: Write the minimal Terraform block in main.tf:
    resource "aws_instance" "legacy_web" {
      ami           = "ami-0c55b159cbfafe1f0" # Current AMI of the instance
      instance_type = "t3.micro"
    }
    
  2. Step 2: Execute the import command pointing to the AWS instance ID:
    terraform import aws_instance.legacy_web i-0123456789abcdef0
    
  3. Step 3: Run terraform plan. Terraform will show discrepancies between your minimal .tf file and reality. Update your code until terraform plan says:
    No changes. Your infrastructure matches the configuration.
    

πŸ› οΈ 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)

resource "aws_s3_bucket" "test_bucket" {
  bucket = "corp-customer-unencrypted-data"
}

2. Run Checkov

checkov -f insecure_s3.tf
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

checkov -f hardened_s3.tf
Output: Passed checks: 5, Failed checks: 0, Skipped checks: 0. PASSED!