← all posts

The $32 line in my architecture — a Fargate VPC, three-tier security groups, and auditing my own cloud bill

I built a containerised REST API on ECS Fargate, sitting inside a custom multi-AZ VPC, all defined in Terraform and deployed live. Then I wrote a small tool to audit what it actually cost to run — and the single biggest line on the bill wasn't the compute, and it wasn't the database. It was a NAT gateway, quietly charging about $32 a month to do a job I could have done another way. This is how the thing is built, and what that number taught me.

The network came first

It would have been quicker to drop the API into a default VPC and move on. I built a custom one instead — around 46 Terraform resources — with public and private subnets spread across two Availability Zones. The point of the exercise was to design the network deliberately rather than inherit whatever AWS gives you by default, because in real work the VPC is where a lot of the security and cost decisions actually live.

Three tiers, each trusting only the one in front

Security groups are the part I'm most deliberate about. Rather than one permissive group, there are three, arranged as tiers:

  • The load balancer tier accepts traffic from the internet on HTTPS.
  • The application tier (the Fargate tasks) accepts traffic only from the load balancer's security group — nothing else can reach it.
  • The data tier (RDS PostgreSQL) accepts traffic only from the application tier.

Each tier references the security group in front of it rather than an IP range, so the rules stay correct even as things scale. The effect is least-privilege at the network layer: if the app tier were ever compromised, it still can't reach anything the design didn't explicitly allow, and the database is never reachable from the internet at all.

Multi-AZ, because "it works" and "it stays working" are different

The Fargate service and the RDS instance are spread across two Availability Zones. For a personal project that's arguably overkill — but the habit is the point. Designing for the loss of an AZ from the start is cheaper than retrofitting it, and it's the default posture I'd want on anything real.

Then I audited my own bill

Once it was running, I wrote a small Boto3 cost-audit tool that walks the account, lists the resources this stack is running, and estimates the monthly cost of each. I wanted to know where the money actually goes, not guess.

The top line wasn't Fargate and it wasn't RDS. It was the NAT gateway — about $32/month, before a single byte of data-processing charge. NAT gateways bill an hourly rate just for existing, so in a small always-on stack they can quietly out-cost the things doing the actual work.

The decision that came out of it

The private subnets needed outbound access mainly to reach AWS APIs. A NAT gateway is one way to give them that — but for AWS-only traffic, VPC endpoints (interface and gateway endpoints) can replace most of what the NAT was doing, at a lower fixed cost, while keeping the traffic on the AWS network instead of routing it out and back. That's the change I'd make next, and the audit tool is what turned it from an assumption into a measured decision.

The broader lesson stuck: cost is an architectural property, not an afterthought you discover on the invoice. Building the audit tool made it something I can see while I design, which is where it belongs.

What's in the box

FastAPI in a container on ECS Fargate, behind an Application Load Balancer, talking to RDS PostgreSQL, inside the custom VPC — all Terraform, with 18 tests and CI, deployed live and verified. The code and the cost-audit tool are on GitHub.