{"id":4698,"date":"2026-08-28T01:07:24","date_gmt":"2026-08-28T04:07:24","guid":{"rendered":"https:\/\/tucumandevelopers.com\/index.php\/2026\/08\/28\/aws-vpc-networking-fundamentals-vpcs-subnets-cidr-route-tables-igw-and-nat-gateways\/"},"modified":"2026-08-28T01:07:24","modified_gmt":"2026-08-28T04:07:24","slug":"aws-vpc-networking-fundamentals-vpcs-subnets-cidr-route-tables-igw-and-nat-gateways","status":"publish","type":"post","link":"https:\/\/tucumandevelopers.com\/index.php\/2026\/08\/28\/aws-vpc-networking-fundamentals-vpcs-subnets-cidr-route-tables-igw-and-nat-gateways\/","title":{"rendered":"AWS VPC Networking Fundamentals: VPCs, Subnets, CIDR, Route Tables, IGW, and NAT Gateways"},"content":{"rendered":"<div>\n<div><\/div>\n<div data-article-id=\"4501738\" id=\"article-body\">\n<p>If you&#8217;ve provisioned a VPC from a Terraform module without fully internalising what each piece is doing, that&#8217;s fine \u2014 right up until something breaks. An instance that should be reachable isn&#8217;t. A private instance can&#8217;t pull a package update. And you&#8217;re left checking five different resources with no clear mental model of how they connect.<\/p>\n<p>This post builds that mental model from the ground up. Not just definitions \u2014 the <em>why<\/em> behind each piece, so troubleshooting becomes deduction instead of guesswork.<\/p>\n<hr>\n<h2> <a name=\"cidr-math-you-actually-need\" href=\"#cidr-math-you-actually-need\"> <\/a> CIDR math you actually need <\/h2>\n<p>A CIDR block is <code>IP address \/ prefix length<\/code>. The prefix length fixes the network portion; the remaining bits are your host space.<\/p>\n<p>Formula: <code>2^(32 - prefix) = total addresses<\/code>. AWS reserves 5 per subnet (network address, VPC router, DNS, reserved, broadcast).<\/p>\n<div>\n<table>\n<thead>\n<tr>\n<th>CIDR<\/th>\n<th>Total addresses<\/th>\n<th>Usable<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>\/16<\/td>\n<td>65,536<\/td>\n<td>65,531<\/td>\n<\/tr>\n<tr>\n<td>\/20<\/td>\n<td>4,096<\/td>\n<td>4,091<\/td>\n<\/tr>\n<tr>\n<td>\/24<\/td>\n<td>256<\/td>\n<td>251<\/td>\n<\/tr>\n<tr>\n<td>\/28<\/td>\n<td>16<\/td>\n<td>11<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<p>To reverse-engineer a prefix from a required host count: round up to the next power of two, subtract the exponent from 32. Need 300 hosts? Next power of two is 512 (2\u2079), so prefix = 32 &#8211; 9 = <code>\/23<\/code>. Run this before sizing any subnet that will host an autoscaling group or EKS node group.<\/p>\n<p>Start with <code>\/16<\/code> for the VPC itself. VPC CIDR is difficult to resize after the fact \u2014 once you have subnets, peering connections, or Transit Gateway attachments built against it, renumbering becomes a migration project. <code>\/16<\/code> costs nothing up front and avoids that corner.<\/p>\n<hr>\n<h2> <a name=\"subnet-allocation-carving-up-the-vpc\" href=\"#subnet-allocation-carving-up-the-vpc\"> <\/a> Subnet allocation: carving up the VPC <\/h2>\n<p>A practical three-AZ production layout from <code>10.0.0.0\/16<\/code>:<\/p>\n<div>\n<table>\n<thead>\n<tr>\n<th>Tier<\/th>\n<th>AZ-a<\/th>\n<th>AZ-b<\/th>\n<th>AZ-c<\/th>\n<th>Size<\/th>\n<th>Typical use<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Public<\/td>\n<td>10.0.0.0\/24<\/td>\n<td>10.0.1.0\/24<\/td>\n<td>10.0.2.0\/24<\/td>\n<td>\/24<\/td>\n<td>ALB, NAT gateway, bastion<\/td>\n<\/tr>\n<tr>\n<td>Private\/app<\/td>\n<td>10.0.16.0\/20<\/td>\n<td>10.0.32.0\/20<\/td>\n<td>10.0.48.0\/20<\/td>\n<td>\/20<\/td>\n<td>EKS nodes, ECS, EC2<\/td>\n<\/tr>\n<tr>\n<td>Data<\/td>\n<td>10.0.64.0\/24<\/td>\n<td>10.0.65.0\/24<\/td>\n<td>10.0.66.0\/24<\/td>\n<td>\/24<\/td>\n<td>RDS, ElastiCache<\/td>\n<\/tr>\n<tr>\n<td>Reserved<\/td>\n<td>10.0.128.0\/17<\/td>\n<td><\/td>\n<td><\/td>\n<td>\/17<\/td>\n<td>Future tiers, Transit Gateway, VPN<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<p>The jump from <code>\/24<\/code> in the public tier to <code>\/20<\/code> in the app tier is intentional. ALBs and NAT gateways consume very few IPs; the app tier is where consumption scales with autoscaling groups, rolling deployments, and pod density.<\/p>\n<p>For EKS specifically: with the VPC CNI, every pod can consume an ENI-backed IP. IP exhaustion is one of the most common EKS production incidents. <code>\/20<\/code> per AZ for worker subnets is the standard starting point.<\/p>\n<p>The deliberate gaps between tiers (0\u20132, then 16\u201348, then 64\u201366) leave room to insert new tiers later without renumbering anything already deployed.<\/p>\n<hr>\n<h2> <a name=\"route-tables-the-actual-decision-maker\" href=\"#route-tables-the-actual-decision-maker\"> <\/a> Route tables: the actual decision maker <\/h2>\n<p>A subnet is &#8220;public&#8221; or &#8220;private&#8221; because of its route table \u2014 not any inherent property of the subnet itself. The table is a list of <code>destination \u2192 target<\/code> rules evaluated by <strong>most specific match<\/strong>.<\/p>\n<p>Every route table gets an implicit <code>local<\/code> route for the full VPC CIDR \u2014 this can&#8217;t be removed, and it&#8217;s what lets every subnet reach every other subnet inside the VPC by default.<\/p>\n<p>A public subnet route table in Terraform: <\/p>\n<div>\n<pre><code><span>resource<\/span> <span>\"aws_route_table\"<\/span> <span>\"public\"<\/span> <span>{<\/span> <span>vpc_id<\/span> <span>=<\/span> <span>aws_vpc<\/span><span>.<\/span><span>main<\/span><span>.<\/span><span>id<\/span> <span>tags<\/span> <span>=<\/span> <span>{<\/span> <span>Name<\/span> <span>=<\/span> <span>\"rtb-public\"<\/span><span>,<\/span> <span>Tier<\/span> <span>=<\/span> <span>\"public\"<\/span> <span>}<\/span> <span>}<\/span> <span>resource<\/span> <span>\"aws_route\"<\/span> <span>\"public_internet\"<\/span> <span>{<\/span> <span>route_table_id<\/span> <span>=<\/span> <span>aws_route_table<\/span><span>.<\/span><span>public<\/span><span>.<\/span><span>id<\/span> <span>destination_cidr_block<\/span> <span>=<\/span> <span>\"0.0.0.0\/0\"<\/span> <span>gateway_id<\/span> <span>=<\/span> <span>aws_internet_gateway<\/span><span>.<\/span><span>main<\/span><span>.<\/span><span>id<\/span> <span>}<\/span> <span>resource<\/span> <span>\"aws_route_table_association\"<\/span> <span>\"public_a\"<\/span> <span>{<\/span> <span>subnet_id<\/span> <span>=<\/span> <span>aws_subnet<\/span><span>.<\/span><span>public_az_a<\/span><span>.<\/span><span>id<\/span> <span>route_table_id<\/span> <span>=<\/span> <span>aws_route_table<\/span><span>.<\/span><span>public<\/span><span>.<\/span><span>id<\/span> <span>}<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Creating the route table does nothing on its own \u2014 the association step is what binds it to a subnet and makes routing take effect.<\/p>\n<hr>\n<h2> <a name=\"internet-gateway-and-the-three-conditions-for-inbound-access\" href=\"#internet-gateway-and-the-three-conditions-for-inbound-access\"> <\/a> Internet Gateway and the three conditions for inbound access <\/h2>\n<p>An IGW is horizontally scaled, redundant, and AZ-agnostic \u2014 one per VPC, no capacity to configure. It does two things: 1:1 NAT between public and private IPs (the public IP mapping lives at the IGW, not on the instance \u2014 which is why <code>ip addr<\/code> on an EC2 instance never shows its public IP), and serves as a route table target.<\/p>\n<p>All three of these must be true simultaneously for inbound internet access to work:<\/p>\n<ol>\n<li>The instance has a public or Elastic IP on its ENI.<\/li>\n<li>The subnet&#8217;s route table has <code>0.0.0.0\/0 \u2192 IGW<\/code>.<\/li>\n<li>Both the security group and the NACL allow the inbound traffic on that port.<\/li>\n<\/ol>\n<p>Any one missing produces the same symptom: a silent timeout with no obvious pointer to the actual cause. This is where most &#8220;why can&#8217;t I reach my instance&#8221; tickets originate.<\/p>\n<hr>\n<h2> <a name=\"nat-gateway-outbound-only\" href=\"#nat-gateway-outbound-only\"> <\/a> NAT Gateway: outbound only <\/h2>\n<p>A NAT gateway lives in a specific subnet in a specific AZ, performs source NAT for private instances, and has real hourly and per-GB cost. The packet walk for a private instance at <code>10.0.2.15<\/code> requesting a public registry:<\/p>\n<ol>\n<li>Private subnet route table: <code>0.0.0.0\/0 \u2192 nat-0abc...<\/code> <\/li>\n<li>NAT gateway rewrites source to its own Elastic IP + ephemeral port<\/li>\n<li>NAT gateway&#8217;s public subnet route table: <code>0.0.0.0\/0 \u2192 IGW<\/code> <\/li>\n<li>IGW performs its own separate 1:1 NAT translation<\/li>\n<\/ol>\n<p>Two distinct NAT translations \u2014 easy to collapse into one mental step, but they&#8217;re separate resources doing separate jobs.<\/p>\n<p><strong>Why it lives in the public subnet:<\/strong> the NAT gateway needs its own route to the IGW, so it must sit in a subnet whose route table already points to the IGW. The private subnet&#8217;s route table then points <code>0.0.0.0\/0<\/code> at the NAT gateway. Two different route tables, two different subnets, one resource bridging them.<\/p>\n<p><strong>HA pattern:<\/strong> one NAT gateway per AZ, each AZ&#8217;s private subnet routing to the NAT gateway in its own AZ. One NAT gateway for the whole VPC is cheaper but creates a single point of failure \u2014 if that AZ has an outage, every private subnet in every other AZ loses outbound internet access.<\/p>\n<p><strong>Cost trap worth auditing:<\/strong> traffic to S3 and DynamoDB from private subnets doesn&#8217;t need to go through NAT at all if you use VPC Gateway Endpoints. Routing S3 traffic through NAT is billed per GB with no benefit over a free Gateway Endpoint.<\/p>\n<hr>\n<h2> <a name=\"read-the-full-article\" href=\"#read-the-full-article\"> <\/a> Read the Full Article <\/h2>\n<p>The summary covers the core mental model. The full article goes deeper on:<\/p>\n<ul>\n<li>The reverse-engineering formula for subnet sizing applied to autoscaling groups and EKS node groups, with the specific IP exhaustion failure mode explained<\/li>\n<li>The full route table example with VPC peering and S3 Gateway Endpoint entries, and why most-specific-match matters for overlapping routes<\/li>\n<li>IGW statelessness and why NACLs require explicit ephemeral port rules (<code>1024\u201365535<\/code>) that security groups handle automatically<\/li>\n<li>NAT Gateway connection tracking limits: 55,000 concurrent connections per unique destination, <code>PortAllocationErrors<\/code> in CloudWatch as the signal, and when to reconsider architecture vs. adding more NAT gateways<\/li>\n<li>The full security group vs. NACL comparison \u2014 stateful vs. stateless evaluation, allow-only vs. allow-and-deny, and why the default NACL and default security group behave differently out of the box<\/li>\n<\/ul>\n<p><strong>\ud83d\udc49 <a href=\"https:\/\/aloknecessary.in\/blogs\/aws-vpc-networking-fundamentals\/?utm_source=devto&amp;utm_medium=referral&amp;utm_campaign=blog_syndication&amp;utm_content=aws-vpc-networking-fundamentals\" target=\"_blank\" rel=\"noopener noreferrer\">AWS VPC Networking Fundamentals \u2014 Full Article<\/a><\/strong><\/p>\n<\/p><\/div>\n<\/div>\n<\/div>\n<\/div>\n<p>Fuente: <a href=\"https:\/\/dev.to\/aloknecessary\/aws-vpc-networking-fundamentals-vpcs-subnets-cidr-route-tables-igw-and-nat-gateways-19h1\">Art\u00edculo original<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>If you&#8217;ve provisioned a VPC from a Terraform module without fully internalising what each piece is doing, that&#8217;s fine \u2014 right up until something breaks. An instance that should be reachable isn&#8217;t. A private instance can&#8217;t pull a package update. And you&#8217;re left checking five different resources with no clear mental model of how they [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":4697,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2},"webixso_pending_account_ids":""},"categories":[41],"tags":[],"class_list":["post-4698","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devto"],"jetpack_publicize_connections":[],"_links":{"self":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts\/4698","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/comments?post=4698"}],"version-history":[{"count":0,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts\/4698\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/media\/4697"}],"wp:attachment":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/media?parent=4698"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/categories?post=4698"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/tags?post=4698"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}