Cloud 45 min read

AWS OpenTofu #203: Compose Infrastructure Modules

Orchestrate multi-tiered architectures by composing focused child modules. Pass outputs as inputs across networking, security, and compute layers, and master dependency graphs.

Mohammad Rizky Prawira portrait
Mohammad Rizky Prawira
terminal Series Guide β€’ Part 3 of 24

AWS + OpenTofu Level 200: Build a Reusable AWS Application Platform

Part of the CLI-first comprehensive learning path from foundational setup to production cloud architectures.

In Lab 202, you built your first reusable child module: an encapsulated multi-AZ VPC package.

Now, a critical architectural question arises: How do multiple independent modules work together to create a cohesive cloud platform?

A complete application environment requires networking, security firewalls, compute workloads, databases, and identity. If you cram all of those into one giant module, you recreate the dreaded β€œGod Module”. But if you keep them separate, how do you wire them together cleanly?

How does an EC2 instance know which subnet to launch into? How does a Security Group bind itself to a VPC created by another module? And how does OpenTofu determine the exact order in which to create, update, or destroy these cross-module resources?

The answer is Module Composition.

In this lab, you will master the art of infrastructure composition. You will build three focused, single-responsibility modules (vpc, security-groups, ec2), compose them in your root orchestration layer, pipe outputs into inputs, master implicit dependency graphs, and learn why overusing depends_on is one of the most destructive habits in Infrastructure as Code.


🎯 Learning Objectives

Modular CompositionDependency Graphs & DAGOperational Rigor
β€’ The Root Module as the orchestration layerβ€’ Implicit dependencies via attribute referencingβ€’ Multi-module state tracking (module.<A>.<B>)
β€’ Constructing single-responsibility child modulesβ€’ Explicit dependencies (depends_on) and anti-patternsβ€’ Verifying SSM console access across composed tiers
β€’ Piping module outputs directly into module inputsβ€’ Concurrency & parallel provisioning mechanicsβ€’ Safe teardown of interdependent module graphs

πŸ’° Estimated Lab Cost

ResourceConfigurationFree Tier CostStandard Account (Past Free Tier)
Amazon EC2 Virtual Machine1x t3.micro On-Demand (~1 hour)$0.00~$0.012 / hour
Amazon EBS Storage20 GB gp3 Root Volume (~1 hour)$0.00~$0.002 / hour ($0.08/GB-mo)
Public IPv4 AddressAuto-assigned IPv4 (~1 hour)$0.00$0.005 / hour (AWS IPv4 charge)
Custom VPC & Subnets1 VPC, 4 Subnets, 2 Route Tables, 1 IGW$0.00$0.00 (Always 100% Free)
Security Groups & IAMScoped Web SG & SSM Instance Profile$0.00$0.00 (Always 100% Free)
AWS Systems Manager (SSM)Zero-trust interactive console access$0.00$0.00 (Always 100% Free)
Total Estimated CostComplete Hands-On Session (~1 hour)$0.00~$0.019 (~2 cents total)

[!NOTE] All networking configuration (VPC, Subnets, Route Tables, Internet Gateway) and SSM management are 100% free forever. Your only expense is running one t3.micro instance for testing (~2 cents if your account has passed Free Tier). We will destroy all resources at the end of the lab.


πŸ—οΈ Architecture: The 3-Tier Module Composition

Rather than building a monolithic configuration, we divide our system into three specialized modules orchestrated by a root composition layer:

                        ROOT MODULE (Orchestration Layer)
 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 β”‚                                                                          β”‚
 β”‚  1. module "network" (modules/vpc)                                       β”‚
 β”‚     β€’ Creates VPC (10.10.0.0/16), Subnets, IGW, Route Tables             β”‚
 β”‚     β€’ Exports: vpc_id, public_subnet_ids                                 β”‚
 β”‚                           β”‚                                              β”‚
 β”‚                           β–Ό passes: vpc_id                               β”‚
 β”‚  2. module "security" (modules/security-groups)                          β”‚
 β”‚     β€’ Creates Web SG (Port 80 Ingress, Zero SSH Port 22)                 β”‚
 β”‚     β€’ Exports: web_sg_id                                                 β”‚
 β”‚                           β”‚                                              β”‚
 β”‚                           β–Ό passes: public_subnet_ids[0], web_sg_id      β”‚
 β”‚  3. module "compute" (modules/ec2)                                       β”‚
 β”‚     β€’ Creates EC2 Instance (t3.micro), IMDSv2, gp3 EBS, SSM IAM Profile  β”‚
 β”‚     β€’ Exports: instance_id, public_ip                                    β”‚
 β”‚                                                                          β”‚
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The Data Flow Contract

Look at how data flows naturally between the modules:

  1. module.network runs first because nothing depends on it. It outputs vpc_id and public_subnet_ids.
  2. module.security receives vpc_id = module.network.vpc_id. OpenTofu instantly knows that security depends on network.
  3. module.compute receives subnet_id = module.network.public_subnet_ids[0] and security_group_id = module.security.web_sg_id. OpenTofu knows that compute must wait for both network and security to finish.

🧠 Deep Dive: Implicit vs. Explicit Dependencies

Understanding how OpenTofu builds its dependency graph is the difference between junior copy-pasters and senior platform engineers.

1. Implicit Dependencies (The Standard Way)

Whenever you write:

module "security" {
  source = "./modules/security-groups"
  vpc_id = module.network.vpc_id # << IMPLICIT DEPENDENCY!
}

OpenTofu analyzes the expression module.network.vpc_id. It infers an implicit dependency:

  • module.network must be evaluated and created before module.security.
  • During destruction (tofu destroy), OpenTofu automatically reverses the order: module.security is destroyed before module.network.

You do not need to write depends_on. The attribute reference communicates the relationship with 100% mathematical precision.


2. Explicit Dependencies (depends_on) and Its Traps

The depends_on meta-argument allows you to manually force an order when an attribute reference does not exist:

# USE SPARINGLY: Explicit Dependency
resource "aws_s3_bucket_policy" "example" {
  bucket     = aws_s3_bucket.example.id
  policy     = data.aws_iam_policy_document.example.json
  depends_on = [aws_s3_bucket_public_access_block.example]
}

Here, depends_on is valid because S3 bucket policies will fail if the public access block is not yet active, but the policy resource does not take an attribute from the access block.

The Disaster of depends_on on Entire Modules:

Beginners often slap depends_on = [module.network] onto their module blocks:

# ❌ ANTI-PATTERN: DO NOT DO THIS!
module "compute" {
  source     = "./modules/ec2"
  subnet_id  = module.network.public_subnet_ids[0]
  depends_on = [module.network] # ❌ REDUNDANT & HARMFUL!
}

Why this is harmful:

  1. Destroys Concurrency: When you put depends_on on a module, OpenTofu treats the entire module as a single opaque block. OpenTofu cannot calculate plan diffs for the child module until the upstream module is 100% applied.
  2. False Replacements: If anything in module.network is updated in-place (e.g., adding a tag), OpenTofu may assume the downstream module is compromised and trigger unnecessary resource replacements!
  3. Redundant Noise: If you already pass module.network.public_subnet_ids[0], OpenTofu already guarantees the order. Adding depends_on adds zero value and massive overhead.

[!IMPORTANT] Golden Rule: Rely on implicit dependencies via output-to-input attribute referencing. Only use depends_on when a dependency exists in the physical AWS cloud that OpenTofu cannot see in HCL code (such as IAM role propagation delays).


πŸ—‚οΈ Step 1: Workspace Directory Structure

Let’s set up our project workspace with three dedicated child modules under modules/:

mkdir -p ~/opentofu-labs/lab-203-module-composition/modules/vpc
mkdir -p ~/opentofu-labs/lab-203-module-composition/modules/security-groups
mkdir -p ~/opentofu-labs/lab-203-module-composition/modules/ec2
cd ~/opentofu-labs/lab-203-module-composition

Our complete architecture will be structured as follows:

lab-203-module-composition/
β”œβ”€β”€ modules/
β”‚   β”œβ”€β”€ vpc/                     # Network Layer (VPC, Subnets, IGW, Route Tables)
β”‚   β”‚   β”œβ”€β”€ main.tf
β”‚   β”‚   β”œβ”€β”€ variables.tf
β”‚   β”‚   └── outputs.tf
β”‚   β”œβ”€β”€ security-groups/         # Firewall Layer (Stateful Security Groups)
β”‚   β”‚   β”œβ”€β”€ main.tf
β”‚   β”‚   β”œβ”€β”€ variables.tf
β”‚   β”‚   └── outputs.tf
β”‚   └── ec2/                     # Compute Layer (EC2 Instance, SSM IAM, User Data)
β”‚       β”œβ”€β”€ main.tf
β”‚       β”œβ”€β”€ variables.tf
β”‚       └── outputs.tf
β”œβ”€β”€ versions.tf                  # Engine & provider version constraints
β”œβ”€β”€ providers.tf                 # Provider setup & default tags
β”œβ”€β”€ variables.tf                 # Root inputs
β”œβ”€β”€ locals.tf                    # Root naming conventions & common tags
β”œβ”€β”€ main.tf                      # ROOT COMPOSITION LAYER (Chaining the modules)
└── outputs.tf                   # Public endpoints and operational verification

🌐 Step 2: Module 1 β€” Networking (modules/vpc)

Let’s create the networking child module.

modules/vpc/variables.tf

# modules/vpc/variables.tf
variable "name_prefix" {
  description = "Prefix prepended to resource names"
  type        = string
}

variable "vpc_cidr" {
  description = "Base CIDR block for the VPC"
  type        = string
  default     = "10.10.0.0/16"
}

variable "availability_zones" {
  description = "List of two Availability Zones"
  type        = list(string)
}

modules/vpc/main.tf

# modules/vpc/main.tf

resource "aws_vpc" "this" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "${var.name_prefix}-vpc"
  }
}

resource "aws_internet_gateway" "this" {
  vpc_id = aws_vpc.this.id

  tags = {
    Name = "${var.name_prefix}-igw"
  }
}

resource "aws_subnet" "public_1" {
  vpc_id                  = aws_vpc.this.id
  cidr_block              = cidrsubnet(var.vpc_cidr, 8, 1) # e.g. 10.10.1.0/24
  availability_zone       = var.availability_zones[0]
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.name_prefix}-public-1"
    Tier = "Public"
  }
}

resource "aws_subnet" "public_2" {
  vpc_id                  = aws_vpc.this.id
  cidr_block              = cidrsubnet(var.vpc_cidr, 8, 2) # e.g. 10.10.2.0/24
  availability_zone       = var.availability_zones[1]
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.name_prefix}-public-2"
    Tier = "Public"
  }
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.this.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.this.id
  }

  tags = {
    Name = "${var.name_prefix}-public-rt"
  }
}

resource "aws_route_table_association" "pub_1" {
  subnet_id      = aws_subnet.public_1.id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "pub_2" {
  subnet_id      = aws_subnet.public_2.id
  route_table_id = aws_route_table.public.id
}

modules/vpc/outputs.tf

# modules/vpc/outputs.tf

output "vpc_id" {
  description = "The ID of the provisioned custom VPC"
  value       = aws_vpc.this.id
}

output "public_subnet_ids" {
  description = "List of public subnet IDs"
  value       = [aws_subnet.public_1.id, aws_subnet.public_2.id]
}

πŸ›‘οΈ Step 3: Module 2 β€” Security Firewalls (modules/security-groups)

Now create the security groups module. This module takes vpc_id as an input and creates an ingress-restricted security group:

modules/security-groups/variables.tf

# modules/security-groups/variables.tf

variable "name_prefix" {
  description = "Prefix prepended to resource names"
  type        = string
}

variable "vpc_id" {
  description = "The VPC ID where the security groups will be created"
  type        = string
}

modules/security-groups/main.tf

# modules/security-groups/main.tf

resource "aws_security_group" "web" {
  name        = "${var.name_prefix}-web-sg"
  description = "Allow inbound HTTP; zero inbound SSH"
  vpc_id      = var.vpc_id

  # Inbound HTTP from world
  ingress {
    description = "Allow HTTP"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  # STRICT ZERO-TRUST: Port 22 SSH closed!

  # Outbound all
  egress {
    description = "Allow all outbound"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.name_prefix}-web-sg"
  }
}

modules/security-groups/outputs.tf

# modules/security-groups/outputs.tf

output "web_security_group_id" {
  description = "ID of the web tier security group"
  value       = aws_security_group.web.id
}

πŸ’» Step 4: Module 3 β€” Compute Workloads (modules/ec2)

Now create the compute child module. It requires a subnet_id (from the VPC module) and a security_group_id (from the security module). It also packages an IAM role for out-of-band AWS Systems Manager Session Manager access:

modules/ec2/variables.tf

# modules/ec2/variables.tf

variable "name_prefix" {
  description = "Prefix prepended to resource names"
  type        = string
}

variable "subnet_id" {
  description = "The subnet ID where the instance will be placed"
  type        = string
}

variable "security_group_id" {
  description = "The security group ID to attach to the instance"
  type        = string
}

variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t3.micro"
}

modules/ec2/main.tf

# modules/ec2/main.tf

# 1. Discover latest official Amazon Linux 2023 AMI
data "aws_ami" "al2023" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-2023.*-x86_64"]
  }

  filter {
    name   = "state"
    values = ["available"]
  }
}

# 2. IAM Role for Zero-Trust SSM Session Manager
resource "aws_iam_role" "this" {
  name = "${var.name_prefix}-ssm-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "ssm" {
  role       = aws_iam_role.this.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_instance_profile" "this" {
  name = "${var.name_prefix}-instance-profile"
  role = aws_iam_role.this.name
}

# 3. EC2 Instance
resource "aws_instance" "this" {
  ami                  = data.aws_ami.al2023.id
  instance_type        = var.instance_type
  subnet_id            = var.subnet_id
  iam_instance_profile = aws_iam_instance_profile.this.name

  vpc_security_group_ids = [var.security_group_id]

  # Root block device hardening
  root_block_device {
    volume_size           = 20
    volume_type           = "gp3"
    encrypted             = true
    delete_on_termination = true
  }

  # Hardened IMDSv2
  metadata_options {
    http_endpoint               = "enabled"
    http_tokens                 = "required"
    http_put_response_hop_limit = 1
  }

  user_data = <<-EOF
              #!/bin/bash
              dnf update -y
              dnf install -y nginx
              systemctl enable nginx
              systemctl start nginx
              echo "<h1>Module Composition Active</h1><p>VPC + Security Group + EC2 Modules Chained Successfully</p>" > /usr/share/nginx/html/index.html
              EOF

  tags = {
    Name = "${var.name_prefix}-web-server"
  }
}

modules/ec2/outputs.tf

# modules/ec2/outputs.tf

output "instance_id" {
  description = "ID of the provisioned EC2 instance"
  value       = aws_instance.this.id
}

output "public_ip" {
  description = "Public IPv4 address of the EC2 instance"
  value       = aws_instance.this.public_ip
}

🎼 Step 5: The Root Composition Layer (main.tf)

Now we return to the root folder and write our orchestration files.

1. Root versions.tf & providers.tf

Create versions.tf:

# versions.tf
terraform {
  required_version = ">= 1.8.0, < 2.0.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.50"
    }
  }
}

Create providers.tf:

# providers.tf
provider "aws" {
  region = var.aws_region

  default_tags {
    tags = local.common_tags
  }
}

2. Root variables.tf & locals.tf

Create variables.tf:

# variables.tf
variable "aws_region" {
  type    = string
  default = "us-east-1"
}

variable "environment" {
  type    = string
  default = "dev"
}

variable "project_name" {
  type    = string
  default = "composite-stack"
}

Create locals.tf:

# locals.tf
locals {
  name_prefix = "${var.project_name}-${var.environment}"

  common_tags = {
    Project     = var.project_name
    Environment = var.environment
    ManagedBy   = "OpenTofu"
    Lab         = "203-Compose-Modules"
  }
}

3. Root main.tf: Chaining the Modules Together!

This is where the magic happens. Look at how readable, declarative, and expressive the root main.tf is:

# main.tf (THE ROOT COMPOSITION LAYER)

# 1. Discover Availability Zones
data "aws_availability_zones" "available" {
  state = "available"
}

# 2. Tier 1: Networking Module
module "network" {
  source = "./modules/vpc"

  name_prefix        = local.name_prefix
  vpc_cidr           = "10.10.0.0/16"
  availability_zones = slice(data.aws_availability_zones.available.names, 0, 2)
}

# 3. Tier 2: Security Groups Module (Consumes VPC Output)
module "security" {
  source = "./modules/security-groups"

  name_prefix = local.name_prefix
  vpc_id      = module.network.vpc_id # << PIPED FROM NETWORK MODULE!
}

# 4. Tier 3: Compute Module (Consumes Network & Security Outputs)
module "compute" {
  source = "./modules/ec2"

  name_prefix       = local.name_prefix
  subnet_id         = module.network.public_subnet_ids[0] # << PIPED FROM NETWORK!
  security_group_id = module.security.web_security_group_id # << PIPED FROM SECURITY!
}

Look at the elegance of this structure:

  • main.tf is only 30 lines long, yet it orchestrates a complete 3-tier cloud architecture.
  • We have zero hardcoded IDs.
  • OpenTofu automatically infers the Directed Acyclic Graph (DAG) without a single line of depends_on.

4. Root outputs.tf

Create outputs.tf to expose operational commands:

# outputs.tf

output "web_url" {
  description = "HTTP URL of the web server"
  value       = "http://${module.compute.public_ip}"
}

output "ssm_connect" {
  description = "AWS CLI command to connect via SSM Session Manager"
  value       = "aws ssm start-session --target ${module.compute.instance_id}"
}

output "vpc_id" {
  description = "Underlying VPC ID"
  value       = module.network.vpc_id
}

πŸš€ Step 6: Initialization and Deployment

Run tofu init to register all three child modules:

tofu init

Output:

Initializing modules...
- compute in modules/ec2
- network in modules/vpc
- security in modules/security-groups
...
OpenTofu has been successfully initialized!

Verify formatting and syntax:

tofu fmt -recursive
tofu validate

Now run tofu plan:

tofu plan

Notice the plan summary:

Plan: 12 to add, 0 to change, 0 to destroy.

OpenTofu recognized all resources across the three modules:

  • module.network.* (VPC, Subnets, IGW, Routes)
  • module.security.aws_security_group.web
  • module.compute.aws_instance.this & IAM roles

Apply the deployment:

tofu apply -auto-approve

Wait roughly 60 seconds for the EC2 instance to launch and bootstrap Nginx.


πŸ§ͺ Step 7: Verification Across Composed Tiers

Let’s test our live infrastructure:

1. Test Web Server Ingress

Query the web server endpoint from the output:

curl -Iv --connect-timeout 5 $(tofu output -raw web_url)

Expected Response:

* Connected to 54.x.x.x port 80
< HTTP/1.1 200 OK
< Server: nginx/1.24.0
< 
<h1>Module Composition Active</h1><p>VPC + Security Group + EC2 Modules Chained Successfully</p>

2. Verify Zero-Trust SSM Console Access

Verify that we can establish an encrypted out-of-band terminal session without open SSH ports:

$(tofu output -raw ssm_connect)

Inside the session:

sh-5.2$ whoami
ssm-user
sh-5.2$ exit

3. Inspect Composed State

List the state resources to see the multi-tier hierarchy:

tofu state list

Output:

module.compute.aws_iam_instance_profile.this
module.compute.aws_iam_role.this
module.compute.aws_iam_role_policy_attachment.ssm
module.compute.aws_instance.this
module.network.aws_internet_gateway.this
module.network.aws_route_table.public
module.network.aws_route_table_association.pub_1
module.network.aws_route_table_association.pub_2
module.network.aws_subnet.public_1
module.network.aws_subnet.public_2
module.network.aws_vpc.this
module.security.aws_security_group.web

πŸ› οΈ Common Troubleshooting & Gotchas

1. Error: Cycle: module.compute -> module.security -> module.compute

  • Cause: Creating circular output-to-input loops between modules (e.g., passing an output from Module A into Module B, while simultaneously trying to pass an output from Module B into Module A).
  • Rule: Infrastructure dependencies must strictly form a Directed Acyclic Graph (DAG). Data can only flow downstream, never backwards.

2. Output Not Found: module.network has no output "public_subnets"

  • Cause: Typo between what the child module exported in modules/vpc/outputs.tf (public_subnet_ids) and what the root module referenced (public_subnets).
  • Rule: Root module references must match the exact block label declared in the child’s outputs.tf.

3. Concurrency Lockup Caused by Module-Level depends_on

  • Symptom: tofu plan takes minutes to evaluate in large codebases.
  • Cause: Putting depends_on = [module.network] on downstream modules.
  • Fix: Delete the module-level depends_on. Let attribute references handle dependency timing naturally.

🧹 Step 8: Clean Teardown

Destroy all resources across the three composed tiers:

tofu destroy -auto-approve

Output:

module.compute.aws_instance.this: Destroying...
...
module.security.aws_security_group.web: Destroying...
...
module.network.aws_vpc.this: Destroying...
Destroy complete! Resources: 12 destroyed.

Notice the destruction order: OpenTofu terminated the EC2 instance first, deleted the security group second, and removed the VPC last. Reverse dependency resolution executed automatically!


πŸ‹οΈ Hands-On Challenge

Before moving to Lab 204:

  1. Inside modules/security-groups, create a second Security Group: aws_security_group.db designed for an internal database (Port 5432).
  2. Use Security Group Chaining: make its ingress rule allow traffic strictly from aws_security_group.web.id (security_groups = [aws_security_group.web.id]).
  3. Export db_security_group_id from modules/security-groups/outputs.tf.
  4. In your root outputs.tf, expose output "db_sg_id" { value = module.security.db_security_group_id }.
  5. Run tofu plan to observe how OpenTofu expands the firewall tier without disturbing compute or networking!

πŸ“– Summary & Key Takeaways

In this lab, you mastered the standard architecture of enterprise cloud engineering:

  1. The Composition Pattern: The root module acts as an orchestrator, declaring child modules and connecting them via input/output data pipelines.
  2. Single Responsibility: Decomposing infrastructure into focused modules (vpc, security-groups, ec2) prevents blast-radius catastrophes and keeps code maintainable.
  3. Implicit Dependencies: Referencing module.network.vpc_id automatically builds the correct Directed Acyclic Graph (DAG) for provisioning and reverse teardown.
  4. Avoid Module depends_on: Do not place depends_on on entire module blocks; it serializes execution, eliminates concurrency, and causes false resource replacements.
  5. Clean Interface Contracts: Child modules should remain strictly encapsulated, exposing only high-level identifiers through outputs.tf.

πŸš€ What’s Next? (Lab 204)

In our current VPC and EC2 modules, we manually created subnets and security rules one by one (public_1, public_2).

What happens when you need to provision 12 subnets across 3 regions, or generate 20 security group rules from a dynamic map of ports?

In [AWS OpenTofu #204: for_each, count, and Dynamic Infrastructure], we will:

  • Compare count vs. for_each.
  • Uncover the β€œList Index Shift” trap that silently destroys production resources when using count.
  • Master stable map addressing: aws_subnet.this["app-a"] vs. aws_subnet.this[0].
  • Construct dynamic nested blocks cleanly in HCL.

Related & Recommended Guides

Continue exploring related systems architectures and engineering field notes.