Back to blog
Article

A small app VPC without the enterprise diagram

A small app VPC without the enterprise diagram
S

StriveBit

4 min readCloud Solutions

A small app VPC without the enterprise diagram

A client asks us to host their inventory app on AWS. They have one web tier, one API server, one Postgres instance, and no dedicated ops person. The natural temptation is to replicate a diagram from a whitepaper: three Availability Zones, public and private subnets per AZ, a NAT Gateway per AZ, transit gateway, VPC endpoints for S3 and DynamoDB, a bastion host with session manager. Most of that is cost and cognitive overhead for a workload that runs on two EC2 instances and an RDS instance.

The layout we actually use is smaller. One VPC with a /16 CIDR. Two AZs, because RDS multi-AZ wants two and because single-AZ is a single point of failure for the database. Four subnets total: a public /24 and a private /24 in each AZ. The public subnets hold the load balancer. The private subnets hold the EC2 instances and the RDS instance.

A single NAT Gateway in one AZ. This is the tradeoff: if that AZ fails, instances in the other AZ cannot reach the internet for package updates or outbound API calls. We accept that. The alternative — a NAT Gateway per AZ — doubles the cost for a failure scenario that is rare and recoverable. For a small application, the money is better spent on database backups or CloudWatch alarms that actually fire.

The security groups are where most teams overcomplicate. We use four. One for the load balancer, allowing 80 and 443 from the internet. One for the web tier, allowing 80 and 443 only from the load balancer's security group. One for the API tier, allowing its port only from the web tier's security group. One for the database, allowing 5432 only from the API tier's security group. Reference by security group ID, not by CIDR. This means when an instance is replaced or scaled, the rules do not change.

What we do not add: a bastion host. SSM Session Manager reaches private instances through a VPC endpoint and an IAM role on the instance. No SSH keys, no open port 22, no jump box that becomes another thing to patch. The VPC endpoint for SSM costs less than a bastion and does not require maintaining an AMI.

We do not add VPC endpoints for S3 unless the application is moving enough data that the NAT Gateway bandwidth charges become visible. For a small app fetching a few files from S3, the traffic goes through the NAT Gateway and the cost is negligible.

The route tables are straightforward. The public subnets share a route table with a default route to the internet gateway. The private subnets share a route table with a default route to the NAT Gateway.

We put CloudWatch alarms on the NAT Gateway's connection count, the RDS CPU utilization, and the load balancer's 5xx count. The alarm actions notify an SNS topic that sends to our Slack. If the NAT Gateway is saturated, it usually means a runaway process or a misconfigured log forwarder, not a capacity problem.

The entire setup is a Terraform module we reuse across clients. It takes the VPC CIDR, the application name, and the instance types as variables. The state file lives in an S3 bucket with versioning and a DynamoDB lock table. We do not split the state by environment for a small application — one workspace per environment is enough.

This layout has survived three production incidents across different clients, none of them caused by the VPC design. The failures were a bad deployment, a Postgres connection pool exhaustion, and a certificate renewal that the load balancer did not pick up. Each one was visible in the monitoring we already had. None of them would have been prevented by a more complex network topology.

Back to all articles

Ready to build something great?

We help ambitious teams build software that lasts. If you're interested in working with us or want to discuss your project, let's connect.

Get in touch