<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Joseph Davis</title>
    <description>The latest articles on DEV Community by Joseph Davis (@joseph_davis_ac8d4e74eced).</description>
    <link>https://dev.to/joseph_davis_ac8d4e74eced</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1813013%2Ff9a445a9-d256-4716-84e4-4df4e3d44c61.jpg</url>
      <title>DEV Community: Joseph Davis</title>
      <link>https://dev.to/joseph_davis_ac8d4e74eced</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/joseph_davis_ac8d4e74eced"/>
    <language>en</language>
    <item>
      <title>Ninety Lines of Terraform, One Whole AWS Network</title>
      <dc:creator>Joseph Davis</dc:creator>
      <pubDate>Sat, 08 Aug 2026 17:28:13 +0000</pubDate>
      <link>https://dev.to/joseph_davis_ac8d4e74eced/ninety-lines-of-terraform-one-whole-aws-network-3489</link>
      <guid>https://dev.to/joseph_davis_ac8d4e74eced/ninety-lines-of-terraform-one-whole-aws-network-3489</guid>
      <description>&lt;p&gt;&lt;em&gt;How a single folder of config spins up a VPC, two subnets, a gateway, a firewall, and a running server — and the two small things that tripped me up.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The AWS console and I have a complicated relationship. Every time I build a network by hand, I click through the same fifteen screens, forget one route table, and end up with a server that can't reach the internet for reasons I can't remember an hour later.&lt;/p&gt;

&lt;p&gt;So I stopped clicking. This is the story of a small Terraform project that builds an entire AWS network from scratch — and tears it back down — with two commands. Nothing exotic. Just the pieces you actually need, wired together so they're reproducible.&lt;/p&gt;

&lt;p&gt;Here's the whole thing on one picture before we get into the code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                        ┌───────────────┐
   Terraform ──────┐    │   Internet    │
                   │    └───────┬───────┘
                   ▼            │ (IGW)
 ┌─────────────────────────────┼──────────────────────────────┐
 │ AWS Account                 │                               │
 │ ┌───────────────────────────┼─────────────────────────────┐│
 │ │ Region                   (IGW)                           ││
 │ │ ┌─────────────────────────┴───────────────────────────┐ ││
 │ │ │ VPC  10.0.0.0/16                                     │ ││
 │ │ │ ┌─────────────────────────────────────────────────┐ │ ││
 │ │ │ │ Availability Zone (eu-central-1a)               │ │ ││
 │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ ││
 │ │ │ │ │ Public subnet  10.0.1.0/24                  │ │ │ ││
 │ │ │ │ │        ┌───────────────────────────────┐    │ │ │ ││
 │ │ │ │ │        │ Security group → [ EC2 ]      │    │ │ │ ││
 │ │ │ │ │        └───────────────────────────────┘    │ │ │ ││
 │ │ │ │ └─────────────────────────────────────────────┘ │ │ ││
 │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ ││
 │ │ │ │ │ Private subnet  10.0.2.0/24  (no internet)  │ │ │ ││
 │ │ │ │ └─────────────────────────────────────────────┘ │ │ ││
 │ │ │ └─────────────────────────────────────────────────┘ │ ││
 │ │ └─────────────────────────────────────────────────────┘ ││
 │ └─────────────────────────────────────────────────────────┘│
 └─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The target: one account, one region, one VPC, two subnets. The public one gets a door to the internet. The private one stays walled off.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem with clicking
&lt;/h2&gt;

&lt;p&gt;A network you build by hand exists only in that one account, exactly as you left it. Nobody can review it. Nobody can rebuild it. And when you want a clean copy for staging, you're back to screen one.&lt;/p&gt;

&lt;p&gt;Terraform flips that around. You describe the network you want in plain text, and it figures out the order to create things, what depends on what, and how to reconcile reality with your file. The whole setup here is five resources the assignment asked for, plus the plumbing that makes the public subnet actually public.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the files are laid out
&lt;/h2&gt;

&lt;p&gt;Before the code, here's the shape of the project. Terraform reads &lt;em&gt;every&lt;/em&gt; &lt;code&gt;.tf&lt;/code&gt; file in the folder and stitches them together, so the split is purely for us humans — inputs in one place, the network in another, results in a third.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Devopsec-terraform/
├── Makefile              # init / validate / plan / apply / destroy shortcuts
└── week1/
    ├── providers.tf      # which cloud, which provider version
    ├── variables.tf      # inputs: region, CIDRs, instance type, SSH range
    ├── main.tf           # the network itself: VPC, subnets, IGW, SG, EC2
    ├── outputs.tf        # what to print after apply (IDs, public IP)
    └── README.md         # how to run it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quick tour of each:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;providers.tf&lt;/code&gt;&lt;/strong&gt; — pins the AWS provider (&lt;code&gt;~&amp;gt; 5.0&lt;/code&gt;) and the region. This is the "talk to AWS" wiring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;variables.tf&lt;/code&gt;&lt;/strong&gt; — every value I might want to change without touching logic: the region, the two subnet CIDRs, the instance type, and the SSH range. Defaults live here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;main.tf&lt;/code&gt;&lt;/strong&gt; — the star. All the resources from the diagram, in one readable file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;outputs.tf&lt;/code&gt;&lt;/strong&gt; — the handful of things I want echoed back after a build, like the instance's public IP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Makefile&lt;/code&gt;&lt;/strong&gt; — thin wrappers so I type &lt;code&gt;make plan&lt;/code&gt; instead of the full &lt;code&gt;terraform -chdir=week1 plan&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The nice part: order doesn't matter and cross-file references just work. &lt;code&gt;main.tf&lt;/code&gt; can say &lt;code&gt;var.region&lt;/code&gt; and Terraform knows to look in &lt;code&gt;variables.tf&lt;/code&gt;. Now let's build it up piece by piece.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the VPC
&lt;/h2&gt;

&lt;p&gt;The VPC is the fence around everything. It owns a private IP range — here a &lt;code&gt;/16&lt;/code&gt;, which gives us 65,536 addresses to carve up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.0.0/16"&lt;/span&gt;
  &lt;span class="nx"&gt;enable_dns_support&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;enable_dns_hostnames&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"devopsec-vpc"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those two DNS flags matter more than they look. Turn them on and your EC2 instance gets a real public DNS name, not just a bare IP. Skip them and you'll wonder later why nothing resolves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two subnets, one important difference
&lt;/h2&gt;

&lt;p&gt;A subnet is a slice of the VPC's address range, pinned to one availability zone. We need two: one public, one private.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.1.0/24"&lt;/span&gt;
  &lt;span class="nx"&gt;availability_zone&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-central-1a"&lt;/span&gt;
  &lt;span class="nx"&gt;map_public_ip_on_launch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;   &lt;span class="c1"&gt;# the key line&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"private"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.2.0/24"&lt;/span&gt;
  &lt;span class="nx"&gt;availability_zone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-central-1a"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only real difference is &lt;code&gt;map_public_ip_on_launch&lt;/code&gt;. Set it to &lt;code&gt;true&lt;/code&gt; and anything you launch into the public subnet automatically gets a public IP. The private subnet doesn't get that flag, so its instances stay unreachable from outside — which is the whole point of calling it private.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A subnet isn't public because of its name. It's public because of a public IP, a gateway, and a route that ties them together.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The three pieces that make "public" true
&lt;/h2&gt;

&lt;p&gt;Here's the part the console hides from you behind friendly defaults. A public subnet needs three things working together: a gateway to the internet, a route table, and a rule pointing all outbound traffic at that gateway.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1. A door to the internet&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_internet_gateway"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# 2. A route table for the public subnet&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table"&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# 3. "Send anything not local out the gateway"&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route"&lt;/span&gt; &lt;span class="s2"&gt;"public_internet_access"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;route_table_id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;destination_cidr_block&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;
  &lt;span class="nx"&gt;gateway_id&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_internet_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you glue the table to the subnet. Without this association, the route table exists but does nothing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table_association"&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;route_table_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the private subnet gets none of this. No gateway, no route, no association. That's on purpose — it costs nothing to leave it isolated, and adding an outbound path would mean a NAT gateway and real hourly charges we don't need here.&lt;/p&gt;

&lt;h2&gt;
  
  
  A firewall in front of the server
&lt;/h2&gt;

&lt;p&gt;A security group is a stateful firewall wrapped around the instance. You describe what's allowed in (ingress) and what's allowed out (egress), and it remembers the connections so replies come back automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"ec2_sg"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"devopsec-ec2-sg"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;ingress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                       &lt;span class="c1"&gt;# SSH in&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ssh_cidr&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;ingress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                       &lt;span class="c1"&gt;# HTTP in&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;egress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                        &lt;span class="c1"&gt;# everything out&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"-1"&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two ways in: SSH on port 22 and HTTP on port 80. Everything is allowed out. I pulled the SSH range into a variable so I can lock it to my own IP later instead of leaving it open to the whole internet.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Habit worth building:&lt;/strong&gt; The risk almost always lives on the ingress side. Wide-open egress is usually fine; wide-open &lt;code&gt;0.0.0.0/0&lt;/code&gt; on SSH is the thing to tighten first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The server itself
&lt;/h2&gt;

&lt;p&gt;Rather than hardcode an AMI ID — which changes constantly and differs per region — I ask AWS for the latest Amazon Linux 2023 image at plan time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_ami"&lt;/span&gt; &lt;span class="s2"&gt;"amazon_linux_2023"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;most_recent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;owners&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"amazon"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"name"&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"al2023-ami-*-x86_64"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_instance"&lt;/span&gt; &lt;span class="s2"&gt;"web_server"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ami&lt;/span&gt;                    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_ami&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amazon_linux_2023&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t3.micro"&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_security_group_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2_sg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The instance lands in the public subnet and wears the security group we just built. Because the subnet auto-assigns public IPs and routes out through the gateway, this server is reachable the moment it boots.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things that actually bit me
&lt;/h2&gt;

&lt;p&gt;The code above reads clean, but I didn't get there on the first try. Two small things cost me a few minutes each, and both are the kind of thing nobody mentions until you hit them.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. "Free tier" is regional
&lt;/h3&gt;

&lt;p&gt;I started with &lt;code&gt;t2.micro&lt;/code&gt; because that's the classic free-tier instance. The apply failed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: creating EC2 Instance: InvalidParameterCombination:
The specified instance type is not eligible for Free Tier.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turns out &lt;code&gt;t2.micro&lt;/code&gt; is only free-tier in older regions. In &lt;code&gt;eu-central-1&lt;/code&gt; the free-tier type is &lt;code&gt;t3.micro&lt;/code&gt;. One word changed, and the apply went through. If you ever see that error, ask AWS what's actually free where you are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 describe-instance-types &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s2"&gt;"Name=free-tier-eligible,Values=true"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"InstanceTypes[].InstanceType"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. The wrong account, silently
&lt;/h3&gt;

&lt;p&gt;My terminal had no &lt;code&gt;AWS_PROFILE&lt;/code&gt; set, so Terraform quietly used the &lt;code&gt;default&lt;/code&gt; profile — which pointed at a different account than the one I meant. Everything "worked," just in the wrong place. Now I pin the profile so it can't drift:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="c"&gt;# at the top of my Makefile
&lt;/span&gt;&lt;span class="k"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_PROFILE&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; spomega
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No more guessing which account an apply is about to touch. That one line saved me from a repeat of the mystery-resources-in-the-wrong-account afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  From nothing to a running network
&lt;/h2&gt;

&lt;p&gt;With all of that in a folder, the entire lifecycle is four commands — and I wrapped them in a Makefile so I never fat-finger a flag.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make init       &lt;span class="c"&gt;# download the AWS provider&lt;/span&gt;
make validate   &lt;span class="c"&gt;# check the config is sound&lt;/span&gt;
make plan       &lt;span class="c"&gt;# preview what will change&lt;/span&gt;
make apply      &lt;span class="c"&gt;# build it for real&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A minute later, Terraform hands back the IDs and the public IP of a live server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;instance_id&lt;/span&gt;        &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"i-02c8a8ab7c98892b2"&lt;/span&gt;
&lt;span class="nx"&gt;instance_public_ip&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"3.120.245.41"&lt;/span&gt;
&lt;span class="nx"&gt;vpc_id&lt;/span&gt;             &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vpc-0d27a957bdf8b8c5f"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And when I'm done, &lt;code&gt;make destroy&lt;/code&gt; removes every last piece, so nothing sits there quietly billing me overnight. That round-trip — build it, prove it, delete it, rebuild it identically — is the thing you can't get by clicking.&lt;/p&gt;




&lt;p&gt;That's the whole project: a network you can read top to bottom, review in a pull request, and recreate anywhere in a minute. Five resources the assignment asked for, a few more to make "public" mean something, and two little gotchas paid forward so you can skip them.&lt;/p&gt;

&lt;p&gt;If you're building your first VPC, steal this shape and change the CIDRs. If you've done this a hundred times, I'd love to hear how you'd tighten it — especially where you draw the line on that SSH rule.&lt;/p&gt;

&lt;p&gt;👏 &lt;em&gt;Clap if this saved you a trip through the console.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; Terraform · AWS · DevOps · Infrastructure as Code · VPC&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>devops</category>
      <category>iac</category>
    </item>
  </channel>
</rss>
