A VPC is easy to stand up and easy to get subtly wrong - an overlapping CIDR that blocks a future merge, a public route table that exposes a subnet you meant to keep private, a NAT gateway quietly billing for traffic that should have used a free endpoint.
Walk this list once now, then use it as a review checklist before shipping any code that creates or modifies VPC networking.
Plan non-overlapping CIDRs across all VPCs. Overlapping ranges make peering and Transit Gateway routing impossible; pick distinct blocks per environment and region before you need them.
Size the VPC CIDR for growth. You cannot shrink a CIDR, and expanding it is limited. A /16 per VPC leaves room; do not cram everything into a /24.
Use RFC 1918 private ranges deliberately. Carve environments out of 10.0.0.0/8 (or 172.16.0.0/12) with a documented scheme so no two teams collide.
Leave headroom in each subnet. Remember AWS reserves five addresses per subnet, and size subnets so autoscaling and ENIs do not exhaust them.
Create the VPC with a planned block and tag it from the start.
Tier subnets by role. Separate public (load balancers), private (app), and data (database) subnets so routing and security can differ per tier.
Span at least two Availability Zones. A subnet lives in one AZ; create one per tier per AZ so a single AZ failure does not take the workload down.
Tag every network resource. VPCs, subnets, route tables, gateways, and endpoints all take tags; untagged networking is the hardest to audit and clean up.
Enable DNS hostnames when instances need public names. Custom VPCs have enableDnsHostnames off by default; flip it with ModifyVpcAttribute.
Keep private subnets truly private. Never set MapPublicIpOnLaunch on a private-tier subnet.
Keep the main route table private. Never add a 0.0.0.0/0 -> igw route to the main table, or every unassociated subnet silently becomes public.
Use explicit route tables per tier. Associate a dedicated public route table only with public subnets; let private subnets route to a NAT gateway.
Never touch the local route. The implicit VPC-CIDR-to-local route is automatic and immutable; do not try to add or remove it.
Fail routes over with ReplaceRoute. To repoint a default route (say to a different NAT gateway) use ReplaceRoute, not delete-and-recreate, to avoid a gap.
Wire public routing explicitly and leave the main table alone.
Write least-privilege inbound rules. Open only the specific ports and sources a tier needs; never a blanket 0.0.0.0/0 on management ports like SSH or RDP.
Reference security groups, not CIDRs, between tiers. Let the web SG be the source on the database SG's rule so access follows instances, not addresses.
Rely on stateful behavior. Do not add outbound SG rules for return traffic - security groups are stateful and handle responses automatically.
Use NACLs as a coarse backstop only. Add stateless subnet-level deny rules for known-bad CIDRs; remember to allow ephemeral egress (1024-65535) since NACLs are stateless.
Do not leave the default SG wide open. Lock down or avoid the default security group so nothing lands on it by accident.
Reference the source group instead of an IP range.
Deploy one NAT gateway per AZ. It avoids an AZ-level single point of failure and the cross-AZ data-transfer charges of routing through another AZ's gateway.
Offload S3 and DynamoDB to gateway endpoints. Free gateway endpoints keep that traffic off the NAT gateway, cutting per-GB data-processing charges.
Release Elastic IPs you stop using. An allocated but unassociated EIP (for example after deleting a NAT gateway) keeps billing until ReleaseAddress.
Turn on flow logs. Enable at least REJECT flow logs on sensitive subnets for cheap security and troubleshooting visibility.
Prefer VPC endpoints over public paths for AWS APIs. Interface endpoints keep service traffic private and reduce reliance on NAT for AWS calls.
Clean up in reverse dependency order. Detach and delete gateways, disassociate route tables, delete subnets, then the VPC - and sweep for orphaned ENIs, EIPs, and endpoints on a schedule.
Why plan non-overlapping CIDRs before I need to peer?
Because peering and Transit Gateway cannot route between VPCs with overlapping ranges, and you cannot easily re-address a live VPC. Choosing distinct blocks up front keeps future connectivity possible.
How big should my VPC CIDR be?
Large enough to grow into - a /16 per VPC is a common default. You cannot shrink a CIDR and expanding it is limited, so err toward roomy.
Why keep the main route table private?
Any subnet you do not explicitly associate uses the main table. If it has an internet route, every such subnet is silently public. Keep it private and associate an explicit public table where you want it.
Should I reference CIDRs or security groups in rules?
Reference other security groups between tiers so access follows instances rather than IP addresses, and only use CIDRs for external sources you must name.
Do I need outbound security-group rules for responses?
No. Security groups are stateful, so responses to allowed inbound traffic are permitted automatically. Only add egress rules to restrict outbound-initiated traffic.
How do I keep NAT gateway costs down?
Use one gateway per AZ to avoid cross-AZ charges, offload S3 and DynamoDB traffic to free gateway endpoints, and release Elastic IPs you no longer use.
Why one NAT gateway per Availability Zone?
A NAT gateway is AZ-scoped. One shared gateway is a single point of failure and forces cross-AZ traffic that bills extra. Per-AZ gateways are both more resilient and cheaper at scale.
What is the safe order to delete a VPC?
Reverse of creation: disassociate and delete custom route tables, detach and delete gateways, delete NAT gateways and release EIPs, delete subnets, then the VPC - after clearing any lingering ENIs and endpoints.
Should I enable flow logs everywhere?
At least REJECT logs on sensitive subnets are cheap and invaluable for security and debugging. Use ALL to S3 where you need forensics, scoping volume with subnet-level logs.
How do I make a VPC design highly available?
Tier subnets across at least two AZs, put a NAT gateway in each AZ, and spread load balancers and instances across the AZs so one zone's failure does not take the workload down.