Skip to main content
DevOps Insightsguides & answers

Cloud Computing · beginner

AWS EC2 and VPC: Building Your First Cloud Infrastructure

Short answer

A VPC is a private network you own inside AWS. The single idea that unlocks the rest: a subnet is "public" or "private" only because of its route table. There is no checkbox. A subnet whose default route points at an internet gateway is public; one whose default route points at a NAT gateway is private. Everything else follows from that.

N
Neeraj Jha
Updated 8 min read158 views
On this page · 11 sections
Diagram: an AWS VPC with an internet gateway, a public subnet holding a load balancer and NAT gateway, and a private subnet holding EC2 instances and RDS.

Who this is for

You can launch an EC2 instance from the console and you are not sure why it can or cannot reach the internet. No Terraform required — though once this makes sense, describing it in code is the next thing worth doing.

The VPC

VPC  10.0.0.0/16                      65,536 addresses
├─ public-a   10.0.1.0/24   (eu-west-1a)   → route 0.0.0.0/0 to Internet Gateway
├─ public-b   10.0.2.0/24   (eu-west-1b)
├─ private-a  10.0.11.0/24  (eu-west-1a)   → route 0.0.0.0/0 to NAT Gateway
└─ private-b  10.0.12.0/24  (eu-west-1b)

Three properties to get right at creation, because they are awkward later:

The CIDR is close to permanent. You can add secondary ranges, but you cannot shrink or renumber the primary one. Pick something large and non-overlapping with any network you might one day peer with or connect by VPN. 10.0.0.0/16 is a reasonable default precisely because it leaves room.

Subnets are tied to one availability zone. This is how you get fault-tolerance: two subnets in two AZs, not one big one.

AWS reserves five addresses in every subnet — the first four and the last. A /24 gives you 251 usable addresses, not 256. On a /28 that reservation is five out of sixteen, which is the size where people run out unexpectedly.

The CIDR calculator is quicker than doing this twice.

Public and private, properly

An internet gateway is attached to the VPC and gives two-way reachability to instances that have a public IP and a route to it.

A NAT gateway lets private instances make outbound connections without being reachable from outside. The classic (zonal) kind lives in a public subnet in one AZ; the newer regional kind spans AZs by itself and needs no public subnet.

public subnet route table      0.0.0.0/0  →  igw-xxxx
private subnet route table     0.0.0.0/0  →  nat-xxxx

That is the entire distinction. If a "private" instance is reachable from the internet, its subnet's route table points at the internet gateway.

The NAT gateway is the bill people do not expect. It is charged for every hour it exists, whether or not anything uses it, and for every gigabyte it processes. High availability multiplies the hourly part by the number of AZs — one zonal gateway per AZ, or a regional one billed per AZ it runs in. For traffic to S3 and DynamoDB, a gateway VPC endpoint routes privately and costs nothing per gigabyte — which is often most of the data a private subnet was sending through NAT.

Security groups and NACLs

Security groupNetwork ACL
Attached toAn instance's network interfaceA subnet
StateStateful — replies are automaticStateless — you need both directions
RulesAllow onlyAllow and deny
EvaluationAll rules togetherIn number order, first match wins

Use security groups. Reach for NACLs only when you need an explicit deny at the subnet boundary — and remember that stateless means a rule permitting inbound 443 does nothing until the ephemeral port range is allowed outbound too. That asymmetry is responsible for a great deal of wasted time.

The practice worth adopting immediately is referencing a security group instead of a CIDR:

web-sg     inbound 443 from 0.0.0.0/0
app-sg     inbound 8080 from web-sg      ← not from 10.0.1.0/24
db-sg      inbound 5432 from app-sg

The rule now describes intent, keeps working when subnets change, and cannot accidentally include something else that happens to be in that range.

Launching an instance

Choices that matter more than the instance type:

The key pair is unrecoverable. AWS keeps the public half. Lose the private key and you cannot SSH in — recovery means detaching the root volume and attaching it elsewhere. Better: skip SSH keys and use Session Manager, which needs no key, no open port 22, and no bastion.

Root volume deletion. DeleteOnTermination defaults to true for the root volume. Data on it goes when the instance does. Anything you want to keep belongs on a separate EBS volume, or not on the instance at all.

Require IMDSv2. The instance metadata service hands out the instance's IAM credentials. Under IMDSv1 that is a plain HTTP GET, so any server-side request forgery in your application can fetch them. IMDSv2 requires a PUT to obtain a token first, which a naive SSRF cannot do. Set the metadata option to required rather than optional.

Elastic IPs are charged whether or not they are in use. AWS bills every public IPv4 address by the hour, attached or idle, so an allocated address doing nothing is pure cost. They are also a finite per-region allocation, so stranded ones are worth tidying.

User data

bash
#!/bin/bash
set -euo pipefail
dnf install -y nginx
systemctl enable --now nginx

User data runs once at first boot, as root, and its output goes to /var/log/cloud-init-output.log — which is the first place to look when an instance comes up and the application is not there. It does not re-run on reboot, so it is bootstrap, not configuration management.

Stop, start and reboot are not the same

Added 30 October 2026. The console puts Reboot, Stop and Terminate side by side, and they differ in exactly the ways that break things later. Per the EC2 User Guide:

  • Reboot keeps everything except what was in memory: same host, same public and private IP, instance store data intact.
  • Stop, then start keeps the EBS volumes and the private IP, but the instance usually lands on a new host, gets a new public IPv4 address unless an Elastic IP is associated, and loses anything on instance store disks. Billing for the instance stops while it is stopped; its EBS volumes are still billed.
  • Terminate deletes the instance and, by default, its root volume.

So anything that points at the public address, a DNS record, a firewall rule, somebody's SSH config, breaks after the first stop. Use an Elastic IP or a load balancer for addresses that must not move, and keep data on EBS rather than instance store. Each state, with what survives it, is laid out in the EC2 explorer.

Common mistakes

  1. Expecting a subnet to be private because it is named private. The route table decides. Check it.
  2. Opening 22 or 3389 to 0.0.0.0/0. Automated scanning finds these in minutes. Session Manager removes the need entirely.
  3. One subnet, one AZ. It works right up until that AZ has a bad day.
  4. Paying NAT charges for S3 traffic. A gateway endpoint is free and takes a few minutes to add.
  5. Clicking it together and never writing it down. A console-built VPC has no history and no second copy — Terraform state drift is what happens when you adopt it into code later and someone keeps clicking.

If this did not work

  • Cannot SSH to a public instance. In order: does it have a public IP, does its subnet route 0.0.0.0/0 to an internet gateway, does the security group allow 22 from your address, and is the OS user right (ec2-user, ubuntu, admin — they differ by AMI).
  • A private instance cannot reach the internet. For a zonal NAT gateway, check it is in a public subnet (a zonal NAT gateway in a private subnet routes nowhere); for either kind, check the private route table points at it.
  • Two instances cannot talk. Security groups are the usual answer — add a rule on the destination referencing the source's group.
  • "InsufficientInstanceCapacity". That instance type is unavailable in that AZ right now. Try another AZ or type; it is not a quota error.

Next steps

Build the four-subnet layout above once by hand so the routing is concrete, then throw it away and rebuild it with Terraform. The second time takes twenty minutes and you get to keep it.

Mentioned in this article

Tagged with

Enjoyed this article?

Get more DevOps insights delivered to your inbox.

Get new posts by email

Subscribe to get an email when a new blog post is published. Skip anytime.

No spam, unsubscribe anytime.

N

Written by

Neeraj Jha

Platform administrator and lead writer.

View all posts →

Discussion

0 comments

Sign in to join the conversation.

Be the first to comment

Start a conversation about this post

Share: