Cloud 35 min read

AWS OpenTofu #110: EC2 Inside Your Own VPC

Unite networking, compute, and security. Provision an Amazon EC2 instance inside a custom multi-AZ VPC, manage it via SSM with zero open ports, and trace the complete end-to-end packet flow.

Mohammad Rizky Prawira portrait
Mohammad Rizky Prawira
terminal Series Guide • Part 10 of 15

AWS + OpenTofu Level 100: Infrastructure as Code from Zero

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

In Lab 109, we constructed a production-grade, multi-AZ custom Virtual Private Cloud. We calculated CIDR blocks, partitioned public and private subnets, attached an Internet Gateway, and wired up custom route tables.

However, our custom network was an “empty highway”: pristine asphalt, painted lanes, and off-ramps, but zero vehicles driving on it.

Meanwhile, back in Lab 106 and Lab 107, we launched EC2 instances and managed them via AWS Systems Manager Session Manager—but we were still constrained to the default, flat AWS VPC.

Now, we bring everything together.

In this lab, you will unite Networking, Compute, and Identity into a single, cohesive infrastructure stack. You will deploy an EC2 web server directly into your custom public subnet, attach a hardened security group scoped to your custom VPC, inject an IAM instance profile for zero-trust SSM management, and trace the end-to-end packet flow from your local terminal to the Linux hypervisor.


🎯 What You Will Learn

End-to-End AWS NetworkingOpenTofu Dependency GraphsOperational Verification
• Placing Elastic Network Interfaces (ENI) in subnets• The 6-tier infrastructure dependency chain• Auditing internal Linux routing (ip route)
• Scoping Security Groups to custom VPC IDs• Inter-resource attribute referencing across files• Testing HTTP ingress through the Internet Gateway
• How SSM communicates over custom VPC routes• Clean multi-file modularization• Validating SSM connectivity with zero open ports
• Packet traversal: Client ➔ IGW ➔ RT ➔ Subnet ➔ ENI• Parallel vs. sequential resource provisioning• Safe teardown of complex network/compute graphs

💰 Estimated Lab Cost

ResourceConfigurationFree Tier CostStandard Account (Past Free Tier)
Amazon EC2 Virtual Machinet3.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$0.00$0.00 (Always 100% Free)
Internet Gateway (IGW)1 Attached Gateway$0.00$0.00 (Always 100% Free)
AWS Systems Manager (SSM)Session Manager interactive 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 primitives (VPC, Subnets, Route Tables, Internet Gateway) and SSM Session Manager are 100% free forever. If your AWS account has passed the 12-month Free Tier, your only cost will be the t3.micro compute time during this lab (~2 cents).


🌐 Architecture: The End-to-End Packet Flow

Before writing code, let’s trace exactly how network packets traverse your custom infrastructure:

+───────────────────────────────────────────────────────────────────────────────────────────────────+
|                                      THE END-TO-END PACKET PATH                                   |
+───────────────────────────────────────────────────────────────────────────────────────────────────+
                                                  │
                 [ Public Internet User ]         │       [ Engineer Terminal (SSM) ]
                            │                     │                    │
                            │ HTTP GET (Port 80)  │                    │ aws ssm start-session
                            ▼                     │                    ▼
+─────────────────────────────────────────────────┼─────────────────────────────────────────────────+
| CUSTOM VPC (10.0.0.0/16)                        │         AWS SSM CONTROL PLANE                   |
|                                                 │         (ssm.ap-southeast-1.amazonaws.com)      |
|   +─────────────────────────────────────────+   │                    │
|   | 1. INTERNET GATEWAY (IGW)               |   │                    │
|   |    Translates public IP to private ENI  |   │                    │
|   +─────────────────────────────────────────+   │                    │
|                        │                        │                    │
|                        ▼ Destination: 0.0.0.0/0 │                    │
|   +─────────────────────────────────────────+   │                    │
|   | 2. PUBLIC ROUTE TABLE                   |   │                    │
|   |    Routes 10.0.0.0/16 locally           |   │                    │
|   |    Routes 0.0.0.0/0 to IGW              |   │                    │
|   +─────────────────────────────────────────+   │                    │
|                        │                        │                    │
|                        ▼ Subnet Routing         │                    │
|   +─────────────────────────────────────────+   │                    │
|   | 3. PUBLIC SUBNET 1 (10.0.1.0/24 - AZ-a) |   │                    │
|   +─────────────────────────────────────────+   │                    │
|                        │                        │                    │
|                        ▼ Hypervisor Filtering   │                    │
|   +─────────────────────────────────────────+   │                    │
|   | 4. SECURITY GROUP                       |   │                    │
|   |    - Ingress: TCP 80 (Allowed)          |   │                    │
|   |    - Ingress: Port 22 (BLOCKED)         |   │                    │
|   |    - Egress:  All Traffic (Allowed) ────┼───┼────────────────────┘ (Outbound TLS 443)
|   +─────────────────────────────────────────+   │
|                        │                        │
|                        ▼ Virtual Interface      │
|   +─────────────────────────────────────────+   │
|   | 5. EC2 INSTANCE (Private IP: 10.0.1.x)  |   │
|   |    - Nitro Hypervisor                   |   │
|   |    - Encrypted gp3 Root EBS Volume      |   │
|   |    - IAM Instance Profile (SSM Core)    |   │
|   |    - Apache Web Service (cloud-init)    |   │
|   +─────────────────────────────────────────+   │
+─────────────────────────────────────────────────┴─────────────────────────────────────────────────+

🧩 The 6-Tier OpenTofu Dependency Chain

Notice the deep dependency chain required to launch this server. OpenTofu must evaluate and provision resources in a precise mathematical order:

Tier 1:  aws_vpc.main
              │
              ├───────────────────────────────────┐
              ▼                                   ▼
Tier 2:  aws_internet_gateway.gw            aws_subnet.public_1
              │                                   │
              ▼                                   │
Tier 3:  aws_route_table.public                   │
              │                                   │
              ▼                                   ▼
Tier 4:  aws_route_table_association.public_1   aws_security_group.web   aws_iam_role.ssm
                                                          │                    │
                                                          │                    ▼
Tier 5:                                                   │          aws_iam_instance_profile.ssm
                                                          │                    │
                                                          ▼                    ▼
Tier 6:                                           aws_instance.web_server ◄────┘

Because each resource references the attributes of its upstream parent (subnet_id = aws_subnet.public_1.id, vpc_security_group_ids = [aws_security_group.web.id]), OpenTofu automatically creates the Directed Acyclic Graph (DAG) and ensures no resource is created until its prerequisites exist.


📁 Step 1: Set Up Lab Directory Structure

Navigate to your ~/opentofu-labs repository and create the dedicated directory for Lab 110:

cd ~/opentofu-labs
mkdir -p lab-110-ec2-custom-vpc
cd lab-110-ec2-custom-vpc

We will organize our configuration into five clean, decoupled files:

lab-110-ec2-custom-vpc/
├── providers.tf             # Engine & AWS Provider configuration
├── variables.tf             # Parameterized inputs (region, CIDRs, instance type)
├── data.tf                  # Dynamic AMI & AZ discovery
├── vpc.tf                   # Custom VPC, Subnets, IGW & Routing
├── security.tf              # Custom VPC Security Group
├── iam.tf                   # IAM Role, Policy Attachment & Instance Profile
├── compute.tf               # EC2 Instance with User Data
└── outputs.tf               # Public IP, Web URL & SSM connect command

⚙️ Step 2: Configure Providers, Variables & Data Sources

Create providers.tf:

terraform {
  required_version = ">= 1.6.0"

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

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Project     = "OpenTofu-Level-100"
      Environment = var.environment
      ManagedBy   = "OpenTofu"
      LabPart     = "110"
    }
  }
}

Create variables.tf:

variable "aws_region" {
  type        = string
  description = "Target AWS region."
  default     = "ap-southeast-1"
}

variable "environment" {
  type        = string
  description = "Target deployment environment."
  default     = "dev"
}

variable "vpc_cidr" {
  type        = string
  description = "CIDR block for the custom VPC."
  default     = "10.0.0.0/16"
}

variable "public_subnet_cidr" {
  type        = string
  description = "CIDR block for the public subnet."
  default     = "10.0.1.0/24"
}

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

Create data.tf:

# 1. Dynamically discover active Availability Zones
data "aws_availability_zones" "available" {
  state = "available"

  filter {
    name   = "opt-in-status"
    values = ["opt-in-not-required"]
  }
}

# 2. Dynamically look up the latest official Amazon Linux 2023 AMI
data "aws_ami" "amazon_linux_2023" {
  most_recent = true
  owners      = ["amazon"]

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

  filter {
    name   = "root-device-type"
    values = ["ebs"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

🌐 Step 3: Declare the Custom Network Backbone (vpc.tf)

Create vpc.tf to build our custom VPC, public subnet, Internet Gateway, and route table:

# 1. Custom VPC Container
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "${var.environment}-custom-vpc"
  }
}

# 2. Internet Gateway
resource "aws_internet_gateway" "gw" {
  vpc_id = aws_vpc.main.id

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

# 3. Public Subnet placed in AZ-1
resource "aws_subnet" "public_1" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.public_subnet_cidr
  availability_zone       = data.aws_availability_zones.available.names[0]
  map_public_ip_on_launch = true # Auto-assigns public IPv4

  tags = {
    Name = "${var.environment}-public-subnet-1"
    Tier = "public"
  }
}

# 4. Public Route Table routing 0.0.0.0/0 to the Internet Gateway
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

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

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

# 5. Associate Public Subnet with Public Route Table
resource "aws_route_table_association" "public_1" {
  subnet_id      = aws_subnet.public_1.id
  route_table_id = aws_route_table.public.id
}

🛡️ Step 4: Declare Firewall & Identity Security

1. Security Group (security.tf)

Notice that vpc_id points explicitly to aws_vpc.main.id. A Security Group created in one VPC cannot be attached to an instance in a different VPC!

Create security.tf:

resource "aws_security_group" "web" {
  name        = "${var.environment}-custom-vpc-web-sg"
  description = "Allow inbound HTTP and all outbound egress"
  vpc_id      = aws_vpc.main.id

  # Ingress: Allow HTTP port 80 for web traffic
  ingress {
    description = "Allow public HTTP traffic"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  # Ingress: SSH port 22 is deliberately OMITTED! (We manage via SSM)

  # Egress: Allow outbound traffic (Required for SSM agent & OS package updates)
  egress {
    description = "Allow all outbound traffic"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

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

2. IAM Role & Instance Profile (iam.tf)

Create iam.tf to authorize Systems Manager Session Manager access:

# 1. IAM Role with EC2 AssumeRole Trust Policy
resource "aws_iam_role" "ec2_ssm" {
  name = "${var.environment}-custom-vpc-ec2-ssm-role"

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

# 2. Attach AmazonSSMManagedInstanceCore
resource "aws_iam_role_policy_attachment" "ssm_core" {
  role       = aws_iam_role.ec2_ssm.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

# 3. IAM Instance Profile container
resource "aws_iam_instance_profile" "ec2_ssm" {
  name = "${var.environment}-custom-vpc-instance-profile"
  role = aws_iam_role.ec2_ssm.name
}

🖥️ Step 5: Provision the EC2 Instance (compute.tf)

Now we connect the pieces. Notice line 6: subnet_id = aws_subnet.public_1.id. This instructs AWS to place the instance’s virtual network card directly inside our custom network.

Create compute.tf:

resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux_2023.id
  instance_type = var.instance_type

  # PLACEMENT: Attach directly into our custom public subnet!
  subnet_id = aws_subnet.public_1.id

  # FIREWALL: Attach the Security Group from our custom VPC
  vpc_security_group_ids = [aws_security_group.web.id]

  # IDENTITY: Attach the SSM Instance Profile
  iam_instance_profile = aws_iam_instance_profile.ec2_ssm.name

  # Encrypted Root EBS Block Device
  root_block_device {
    volume_type           = "gp3"
    volume_size           = 20
    encrypted             = true
    delete_on_termination = true
  }

  # Enforce IMDSv2
  metadata_options {
    http_endpoint = "enabled"
    http_tokens   = "required"
  }

  # Cloud-Init User Data: Installs Apache and reports custom network metadata
  user_data = <<-EOF
              #!/bin/bash
              dnf update -y
              dnf install -y httpd
              systemctl start httpd
              systemctl enable httpd

              TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "x-aws-ec2-metadata-token-ttl-seconds: 60")
              INSTANCE_ID=$(curl -s -H "x-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id)
              AZ=$(curl -s -H "x-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/placement/availability-zone)
              LOCAL_IP=$(curl -s -H "x-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/local-ipv4)

              cat <<HTML > /var/www/html/index.html
              <!DOCTYPE html>
              <html>
              <head>
                <title>OpenTofu Lab 110</title>
                <style>
                  body { font-family: sans-serif; background: #0b0f19; color: #f1f5f9; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
                  .card { background: #1e293b; border: 1px solid #334155; border-radius: 12px; padding: 2rem; width: 450px; text-align: center; }
                  h1 { color: #38bdf8; }
                  .stat { text-align: left; background: #0f172a; padding: 0.75rem 1rem; border-radius: 6px; margin: 0.5rem 0; font-family: monospace; }
                  .val { color: #4ade80; float: right; }
                </style>
              </head>
              <body>
                <div class="card">
                  <h1>🚀 Custom VPC Online</h1>
                  <p>EC2 Provisioned in Isolated Custom Subnet</p>
                  <div class="stat">VPC CIDR: <span class="val">10.0.0.0/16</span></div>
                  <div class="stat">Subnet IP: <span class="val">$LOCAL_IP</span></div>
                  <div class="stat">Instance ID: <span class="val">$INSTANCE_ID</span></div>
                  <div class="stat">Availability Zone: <span class="val">$AZ</span></div>
                </div>
              </body>
              </html>
              HTML
              EOF

  tags = {
    Name = "${var.environment}-custom-vpc-web"
    Tier = "PublicWeb"
  }
}

📤 Step 6: Expose Verification Outputs (outputs.tf)

Create outputs.tf:

output "vpc_id" {
  description = "The custom VPC ID."
  value       = aws_vpc.main.id
}

output "subnet_id" {
  description = "The public subnet where the EC2 instance resides."
  value       = aws_subnet.public_1.id
}

output "instance_id" {
  description = "The EC2 Instance ID."
  value       = aws_instance.web.id
}

output "private_ip" {
  description = "The private IP allocated from our 10.0.1.0/24 CIDR."
  value       = aws_instance.web.private_ip
}

output "public_ip" {
  description = "The public IPv4 assigned via our Internet Gateway."
  value       = aws_instance.web.public_ip
}

output "web_url" {
  description = "Clickable HTTP URL to test the web service."
  value       = "http://${aws_instance.web.public_ip}"
}

output "ssm_connect_command" {
  description = "CLI command to open an encrypted shell via SSM."
  value       = "aws ssm start-session --target ${aws_instance.web.id}"
}

🚀 Step 7: Preview & Provision (tofu apply)

Initialize the directory:

tofu init
tofu fmt
tofu validate

Preview the plan:

tofu plan

Notice the plan summary:

Plan: 9 to add, 0 to change, 0 to destroy.
  + aws_iam_instance_profile.ec2_ssm
  + aws_iam_role.ec2_ssm
  + aws_iam_role_policy_attachment.ssm_core
  + aws_instance.web
  + aws_internet_gateway.gw
  + aws_route_table.public
  + aws_route_table_association.public_1
  + aws_security_group.web
  + aws_subnet.public_1
  + aws_vpc.main

Apply the configuration:

tofu apply -auto-approve
aws_vpc.main: Creating...
aws_iam_role.ec2_ssm: Creating...
aws_iam_role.ec2_ssm: Creation complete after 1s
aws_iam_role_policy_attachment.ssm_core: Creating...
aws_iam_instance_profile.ec2_ssm: Creating...
aws_iam_instance_profile.ec2_ssm: Creation complete after 1s
aws_iam_role_policy_attachment.ssm_core: Creation complete after 1s
aws_vpc.main: Creation complete after 2s [id=vpc-0123456789abcdef0]
aws_internet_gateway.gw: Creating...
aws_subnet.public_1: Creating...
aws_security_group.web: Creating...
aws_internet_gateway.gw: Creation complete after 1s [id=igw-0123456789abcdef0]
aws_route_table.public: Creating...
aws_security_group.web: Creation complete after 1s [id=sg-0123456789abcdef0]
aws_subnet.public_1: Creation complete after 1s [id=subnet-0123456789abcdef0]
aws_route_table.public: Creation complete after 1s [id=rtb-0123456789abcdef0]
aws_route_table_association.public_1: Creating...
aws_route_table_association.public_1: Creation complete after 1s
aws_instance.web: Creating...
aws_instance.web: Still creating... [10s elapsed]
aws_instance.web: Still creating... [20s elapsed]
aws_instance.web: Creation complete after 25s [id=i-0123456789abcdef0]

Apply complete! Resources: 9 added, 0 changed, 0 destroyed.

Outputs:

instance_id = "i-0123456789abcdef0"
private_ip = "10.0.1.84"
public_ip = "13.250.88.190"
ssm_connect_command = "aws ssm start-session --target i-0123456789abcdef0"
subnet_id = "subnet-0123456789abcdef0"
vpc_id = "vpc-0123456789abcdef0"
web_url = "http://13.250.88.190"

Look at the private_ip output: 10.0.1.84. The instance was assigned a private IP directly from your custom 10.0.1.0/24 subnet!


🧪 Step 8: Operational Verification & Linux Network Inspection

Wait 45 seconds for Apache to finish installing.

1. Test Web Reachability via curl

curl -s $(tofu output -raw web_url) | grep "Custom VPC Online"

Output:

                  <h1>🚀 Custom VPC Online</h1>

You can open the web_url in your browser. You will see the custom dark-mode card confirming the instance is running at 10.0.1.x inside your custom VPC.

2. Connect via SSM Session Manager

Open a shell using the output command:

$(tofu output -raw ssm_connect_command)

You will be dropped directly into the Linux terminal:

Starting session with SessionId: admin-prawira-0123456789abcdef0
sh-5.2$

3. Inspect Linux Network Routing Inside the Virtual Server

Inside your SSM session, let’s inspect how the operating system perceives the custom VPC:

# View assigned network interfaces and IP addresses
ip addr show dev eth0

Output:

2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc mq state UP group default qlen 1000
    inet 10.0.1.84/24 brd 10.0.1.255 scope global dynamic eth0
       valid_lft 3350sec preferred_lft 3350sec

The Linux kernel confirms its IP is 10.0.1.84 with netmask /24.

Now inspect the kernel routing table:

ip route show

Output:

default via 10.0.1.1 dev eth0 proto dhcp src 10.0.1.84 metric 512 
10.0.1.0/24 dev eth0 proto kernel scope link src 10.0.1.84 metric 512

Look at default via 10.0.1.1: The instance’s default gateway is 10.0.1.1—the AWS VPC Router IP we learned about in Lab 109! When packets leave this server destined for the internet, they hit 10.0.1.1, which evaluates your OpenTofu route table and sends the traffic out through the Internet Gateway.

Exit the SSM session:

exit

🧹 Step 9: Clean Teardown (tofu destroy)

Tear down all 9 resources:

tofu destroy -auto-approve
aws_instance.web: Destroying... [id=i-0123456789abcdef0]
aws_instance.web: Destruction complete after 30s
aws_security_group.web: Destroying...
aws_route_table_association.public_1: Destroying...
aws_iam_instance_profile.ec2_ssm: Destroying...
aws_iam_role_policy_attachment.ssm_core: Destroying...
aws_route_table.public: Destroying...
aws_subnet.public_1: Destroying...
aws_internet_gateway.gw: Destroying...
aws_iam_role.ec2_ssm: Destroying...
aws_vpc.main: Destroying...
Destroy complete! Resources: 9 destroyed.

OpenTofu terminates the EC2 instance first, frees the ENI, removes the security group, deletes the route associations, detaches the Internet Gateway, and destroys the VPC. Zero leftover resources.


🛠️ Common Troubleshooting & Gotchas

1. Error: SecurityGroupAndSubnetVpcMismatch

  • Cause: Trying to attach a Security Group from the Default VPC to an instance running in your custom VPC (or vice versa).
  • Rule: Every Security Group has a fixed vpc_id. Instances can only attach Security Groups that belong to the identical VPC.

2. SSM Connect Fails with TargetNotConnected inside Custom VPC

  • Cause: For SSM to work in a public subnet, the subnet must have an associated Route Table with a default route to the Internet Gateway (0.0.0.0/0 -> igw), and the instance must have a public IP. If the route to the IGW is missing, the SSM agent cannot reach AWS endpoints.

3. Web Request Hangs on curl

  • Check whether map_public_ip_on_launch = true was set on the subnet, or verify that the security group ingress allows port 80.

🏋️ Hands-On Challenge

Before moving to Lab 111:

  1. Deploy to AZ-2: In compute.tf, add a second EC2 instance (aws_instance.web_2) placed in a second public subnet in AZ-2 (availability_zone = data.aws_availability_zones.available.names[1]).
  2. Run tofu plan to observe how OpenTofu expands your compute tier across multiple Availability Zones for high availability.
  3. Verify that both instances receive independent private IPs from their respective subnet CIDRs.

📖 Summary & What You Learned

In this milestone lab, you united the three major pillars of cloud infrastructure:

  • Network Placement: Launched an EC2 instance directly inside a custom public subnet (10.0.1.0/24).
  • Scoped Firewalls: Created a dedicated aws_security_group anchored strictly to aws_vpc.main.id.
  • Packet Traversal: Traced packets from external clients through the Internet Gateway, Route Table, Subnet, and Security Group into the Linux kernel.
  • Kernel Routing: Confirmed that the instance uses 10.0.1.1 (the AWS VPC Router) as its default gateway.
  • Zero-Trust Management: Maintained our zero-open-port security posture, managing the custom VPC instance via SSM Session Manager.

🚀 What’s Next? (Lab 111)

Our web server currently runs in a public subnet. But what about our databases, backend workers, and private microservices?

Instances in private subnets cannot have public IPs and cannot talk to the Internet Gateway directly. But they still need to download Linux security updates and external API data.

In [AWS OpenTofu #111: Outbound Internet with NAT Gateways vs NAT Instances], we will:

  • Explore the FinOps Dilemma: AWS managed NAT Gateways ($32/month) vs lightweight NAT Instances ($3/month).
  • Route outbound traffic from private subnets through a NAT gateway.
  • Preserve zero inbound connectivity while enabling secure outbound updates.

Related & Recommended Guides

Continue exploring related systems architectures and engineering field notes.