AWS Private EC2 Operations Guide Part 5: Cost Analysis and Optimization — NAT, ALB, EC2, and Data Transfer
Introduction
By the end of Part 1 through Part 4 you have a full set of infrastructure. A month later the bill arrives, and the first reaction is almost always the same — “why is NAT Gateway so high?”
This final post tears that bill apart. What costs what, where it leaks, and the levers that cut a month’s spend roughly in half without giving up security or availability.
- Part 1 — Why Private Subnet?
- Part 2 — Building VPC infrastructure with Terraform
- Part 3 — Connecting without Bastion via SSM Session Manager
- Part 4 — CI/CD pipeline with GitHub Actions + SSM/CodeDeploy
- Part 5 — Cost analysis and optimization strategies (this post)
This post targets juniors who got their first surprising AWS bill. After reading you should know where each dollar went, which one-line change sweeps $5–$30 off this month immediately, and which scenario your environment fits into.
TL;DR
- NAT Gateway is half the bill. The Parts 1–4 baseline is ~$110/month and ~$65 of that is NAT — the first place to look.
- The fastest free win: an S3 Gateway Endpoint. Keep EC2’s S3 traffic off NAT — $0/hour, $5–$30/month off instantly.
- For EC2, go Right-size → Graviton → Savings Plans, in that order. 30–50% off with little to no code change. The 1-year No-Upfront Compute Savings Plan is the sweet spot.
- ALB is about consolidation. Five services × five ALBs is $80/month leaking. Combine into one with Listener Rules → ~$25.
- Memorize budgets by scenario: side-project ~$40, startup ~$110, enterprise $300+. Knowing which tier you’re in makes every next decision easier.
1. Decomposing the Monthly Bill of the Parts 1–4 Environment
1.1 The breakdown table
Baseline scenario: Part 2’s layout (Multi-AZ HA, two NATs, ALB, two EC2s) + Part 4’s pipeline + light user traffic (10 GB/month egress). Seoul region, 2026 prices.
| Item | Unit | Monthly | Share |
|---|---|---|---|
| 2× NAT Gateway (per AZ) | $32.40 × 2 | $64.80 | 58% |
| ALB (hours + LCU) | $20 (low traffic) | $20.00 | 18% |
| 2× EC2 t3.micro (on-demand) | $10.50 × 2 | $21.00 | 19% |
| 2× EBS gp3 8GB | $0.80 × 2 | $1.60 | 1% |
| S3 (1GB artifacts + requests) | - | $0.50 | 0.5% |
| Data Transfer Out (10GB) | $0.114/GB | $1.14 | 1% |
| CloudWatch Logs (1GB) | $0.80 | $0.80 | 1% |
| Total | ~$110 | 100% |
1.2 Free items vs billed items
A first read of the bill produces two simultaneous reactions: “why so much?” and “why is this $0?” The pattern is simple.
| Free (no per-hour charge) | Billed (per-hour or per-GB) |
|---|---|
| VPC, Subnet, Route Table, IGW | NAT Gateway, ALB, EC2 |
| Security Group, NACL | EBS Volume, EBS Snapshot |
| IAM Role, OIDC Provider | Interface VPC Endpoint, EIP / public IPv4 |
| S3 Gateway Endpoint | S3 Storage, S3 Request |
| SSM Session Manager (managed EC2) | CloudWatch Logs Ingest/Storage |
| CodeDeploy (EC2/Lambda) | Data Transfer (Out, Cross-AZ, NAT processing) |
The trick is identifying resources that bill per hour even when idle. NAT Gateway, ALB, EC2 instances, EBS, EIP — these five make 90% of the bill.
1.3 Where the money leaks
That table as a distribution:
pie title Parts 1–4 monthly cost distribution
"NAT Gateway" : 65
"ALB" : 20
"EC2" : 21
"Other (S3/EBS/Logs/Transfer)" : 4
NAT alone is half the spend — true for almost every variation of this series environment. Hence §2 is the single biggest line.
2. NAT Gateway — The Biggest Lever
2.1 NAT bills two ways
- Hourly: $0.045/hour × 720h ≈ $32.40/month — billed even at zero traffic.
- Data processing: $0.045/GB — every GB that flows through.
- On top of that, anything that exits to the internet pays Data Transfer Out separately.
In other words, 1 GB through NAT costs ~$0.045 (NAT processing) + ~$0.114 (egress) ≈ $0.16/GB. The same 1 GB pulled from S3 in the same region is $0 (via S3 Gateway Endpoint).
2.2 Single NAT vs Multi-AZ NAT
| Environment | NAT layout | Monthly (hourly only) |
|---|---|---|
| Learning / dev / staging | Single AZ | $32.40 |
| Standard production (HA recommended) | One per AZ (2 total) | $64.80 |
| High-traffic production | One per AZ + heavy data processing | $100+ |
Multi-AZ NAT preserves the AZ-failure isolation we discussed in Part 2 §3.3. If cost is painful, the standard split is single NAT for dev/stage, Multi-AZ only for prod.
2.3 S3 Gateway Endpoint — the fastest free cut
Keeping EC2-to-S3 traffic off NAT is the single biggest one-liner you can apply.
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.ap-northeast-2.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = [
aws_route_table.private_a.id,
aws_route_table.private_c.id,
]
}
- Gateway Endpoints cost $0/hour — no extra charge.
- The Private Route Tables get an S3 prefix-list route added automatically.
- Part 4’s artifact downloads, CloudWatch Logs S3 export, and every other S3 hit bypass NAT.
If artifact traffic is 100 GB/month, that’s $4.50 NAT processing + $11.40 egress = ~$16/month off. Even tiny environments save $5+/month from a five-minute change.
2.4 SSM Interface Endpoint — when is it worth it?
Same comparison as Part 3 §4.2:
- If NAT already exists and you also need general internet egress: reuse NAT — cheaper.
- If you want NAT off entirely and only SSM/S3/core AWS reachable: 3 Interface Endpoints + S3 Gateway Endpoint can replace NAT. 3 × 2 AZ × $0.011/h × 720h ≈ ~$48/month vs Multi-AZ NAT $64/month — slightly cheaper, plus the security upside that Private EC2 has no internet path at all.
2.5 Aside: NAT Instance?
A “NAT Instance” pattern (run NAT yourself on a t4g.nano with iptables) costs ~$5/month — dramatically cheaper. Drawbacks:
- You build HA and automatic failover yourself.
- Throughput caps to the instance size.
- You own the security patches and operations.
Recommended only for learning and personal projects. For real workloads, NAT Gateway wins — operator hours are also money.
3. EC2 — Right-size, Graviton, Savings Plans
3.1 Right-size first
The fastest one-line cut to EC2 cost is dropping a size. From CloudWatch:
- Average CPU < 20% → candidate to size down.
- Memory > 70% → cut CPU but switch to a memory-rich family (e.g.
r).
aws ce get-rightsizing-recommendation produces automatic suggestions — usually one or two 30–40% candidates show up.
3.2 Graviton (t4g/m7g) — ~20% off with little effort
ARM Graviton instances are ~20% cheaper than the equivalent x86, and JVM, Node, Python, Go all run as-is. Switching Part 2’s t3.micro → t4g.micro:
- $10.50/month → $8.40/month (≈ $4/month off across two instances)
- Just point at the ARM AL2023 AMI.
Note: Native dependencies (some Python libs, old .NET) need a rebuild. The exception is x86-only commercial software with binding licenses.
3.3 Savings Plans / Reserved Instances / Spot
For always-on EC2, the next step is committed-use discount.
| Option | Term | Discount | Flexibility |
|---|---|---|---|
| Spot Instance | None | Up to 90% | Reclaimed on 2-min notice — non-interactive / batch only |
| Reserved Instance | 1 / 3 yr | Up to 72% | Fixed family |
| Compute Savings Plans | 1 / 3 yr | Up to 66% | Free across family/region/OS |
| EC2 Instance Savings Plans | 1 / 3 yr | Up to 72% | Fixed family/region, free across size |
For most workloads, a 1-year No-Upfront Compute Savings Plan is the sweet spot — low commit risk, and it survives migrations to Graviton or different families.
3.4 Quick decision
flowchart TB
Q1{Instance runs 24×7?}
Q2{Workload interruptible?}
SP[Compute Savings Plans 1y]
Spot[Spot Instance]
OD[Stay On-Demand]
Q1 -->|yes| SP
Q1 -->|no| Q2
Q2 -->|"yes (batch / CI)"| Spot
Q2 -->|no| OD
4. Data Transfer — Invisible but Often a Top Line
4.1 Which traffic actually costs
| Path | Rate | Note |
|---|---|---|
| EC2 → internet (out) | $0.114/GB | First 100 GB/month free |
| EC2 → EC2 same AZ (private IP) | Free | |
| EC2 → EC2 different AZ | $0.01/GB × 2 | $0.02/GB combined |
| ALB → EC2 (cross-AZ) | $0.01/GB | ALB to backend across AZ |
| EC2 → S3 same region (Gateway Endpoint) | Free | The key |
| EC2 → S3 same region (via NAT) | $0.045/GB (NAT processing) | When no Endpoint |
| EC2 → another region | $0.02/GB | Most cross-region |
4.2 ALB cost — hours + LCU
ALB bills two ways:
- Hourly: $0.0225/hour × 720h ≈ $16.20/month — billed at zero traffic.
- LCU (Load Balancer Capacity Unit): $0.008/LCU-hour — the max of four dimensions (new connections, active connections, processed bytes, rule evaluations).
Light traffic: ~$20/month. As traffic grows, LCU is the only thing that scales. Instead of running 5 ALBs for 5 small services, the usual answer is 1 ALB + Listener Rules for host- and path-based routing — $80/month → $25/month.
4.3 ALB consolidation — group with Listener Rules
resource "aws_lb_listener_rule" "service_a" {
listener_arn = aws_lb_listener.https.arn
priority = 10
action {
type = "forward"
target_group_arn = aws_lb_target_group.service_a.arn
}
condition {
host_header {
values = ["a.example.com"]
}
}
}
Five services → one ALB + five Listener Rules. One of the highest-impact single changes you can make. Caveat: stuffing too many rules into one ALB makes the rule-evaluation LCU dimension expensive — re-split once you pass ~50 rules.
5. Cost Visibility — Tags, Budgets, Anomaly Detection
5.1 Tag policy first
To see cost you need cost to carry labels. Tag every resource with at least these four:
| Tag key | Example | Use |
|---|---|---|
Env | dev, staging, prod | Cost per environment |
App | myapp, internal-tool | Cost per service |
Owner | team-platform | Who’s responsible |
CostCenter | R&D, Marketing | Accounting category |
You can enforce via Tag Policies, and you must activate the tag in Cost Explorer (Settings → Cost Allocation Tags → Activate) before it shows up as a cost dimension.
5.2 AWS Budgets — proactive alerts
The simplest way to be warned before the budget breaks.
resource "aws_budgets_budget" "monthly" {
name = "monthly-cap"
budget_type = "COST"
limit_amount = "150"
limit_unit = "USD"
time_unit = "MONTHLY"
notification {
comparison_operator = "GREATER_THAN"
threshold = 80
threshold_type = "PERCENTAGE"
notification_type = "FORECASTED"
subscriber_email_addresses = ["ops@example.com"]
}
}
FORECASTED fires when “current trajectory exceeds the cap by month-end” — no more bill-day surprises.
5.3 Cost Anomaly Detection — automatic outlier alerts
AWS uses ML to learn your normal pattern and alerts when costs spike unexpectedly (five accidental NATs, an unstopped t3.xlarge). Once enabled, monitoring runs itself.
In the console: Cost Management → Cost Anomaly Detection → create a Monitor. If you’ve ever had a runaway-cost incident, this is non-negotiable.
6. Budgets by Scenario
6.1 Side project — ~$40/month
No HA, no Multi-AZ. One NAT, one EC2.
| Item | Monthly |
|---|---|
| 1× NAT Gateway | $32.40 |
| 1× t4g.nano (Graviton) | $4.20 |
| 8 GB EBS | $0.80 |
| Public IP / EIP | $3.60 |
| Total | ~$41 |
Drop the ALB; point a domain at the EC2 directly. Skip Multi-AZ. To go lower, move to Public Subnet and remove NAT entirely — down to ~$15/month (the “side project on Public Subnet + SG is enough” path from Part 1).
6.2 Startup / MVP — ~$110/month
The Parts 1–4 baseline, unchanged. Multi-AZ HA + ALB + 2 EC2s + pipeline.
| Item | Monthly |
|---|---|
| 2× NAT Gateway | $64.80 |
| ALB | $20.00 |
| 2× t4g.micro (Graviton) | $16.80 |
| EBS, S3, Logs, Transfer | ~$5 |
| Total | ~$107 |
Apply a 1-year Compute Savings Plan and the EC2 line drops ~30%; add an S3 Gateway Endpoint and NAT data processing falls — all the way to ~$95/month.
6.3 Enterprise / compliance — $300+/month
Add Interface VPC Endpoints, larger EC2, RDS Multi-AZ, KMS, WAF, 1-year CloudWatch Logs retention.
| Item | Monthly |
|---|---|
| 2× NAT Gateway | $64.80 |
| 3× Interface VPC Endpoint × 2 AZ | $48 |
| ALB + WAF | $35 |
| 2× t4g.medium + Savings Plans | $50 |
| RDS Multi-AZ (db.t4g.medium) | $80+ |
| CloudWatch Logs (10 GB/month) | $10 |
| KMS, Secrets Manager | $5 |
| Total | $300+ |
At this tier the conversation shifts from line-by-line trimming to cost visibility and governance. The tags / Budgets / Anomaly trio in §5 becomes mandatory infrastructure.
7. Common Traps — Checklist
When the bill jumps unexpectedly, run through this list in order:
- ☐ Unattached EIP / unused public IPv4 — $0.005/hour ≈ $3.60/month each. Pile up fast.
- ☐ EBS on stopped EC2 — stopped instances still pay for their volumes.
- ☐ Old EBS snapshots — automated backups accumulate at GB × $0.05/month.
- ☐ S3 traffic going via NAT — without a Gateway Endpoint you pay both NAT processing and egress.
- ☐ CloudWatch Logs with no retention — default is “Never expire”. Set 30 or 90 days.
- ☐ S3 Versioning without Lifecycle — versions stack up explosively. With SHA-keyed artifacts, lifecycle to Glacier IR or Delete after 90 days.
- ☐ Cross-AZ chatty traffic — two components of the same service across AZs, talking heavily, racks up inter-AZ costs.
- ☐ Forgotten dev/stage — dev kept 24×7 spends about a third of prod. Use Instance Scheduler to stop nights/weekends.
- ☐ Five+ ALBs — check whether host/path routing can collapse them (§4.3).
- ☐ Cross-region traffic — confirm you haven’t accidentally been hitting S3 or DynamoDB in another region.
Recap
What to take away:
- NAT Gateway is half the bill. Top candidate, top weapon — S3 Gateway Endpoint (free) earns you $5+/month with five minutes of work.
- Multi-AZ HA is a value-vs-cost trade-off. Single NAT for dev/stage, Multi-AZ for prod — the standard split.
- EC2 cost order: Right-size → Graviton → Savings Plans. 30–50% off with little to no code change is normal.
- ALB is about consolidation. Per-service ALBs leak tens of dollars a month; Listener Rules collapse them to a quarter of the cost.
- Visibility precedes savings. Without tags / Budgets / Anomaly, you don’t even know where to cut.
- Memorize budgets by scenario. Side ~$40, Startup ~$110, Enterprise $300+ — knowing your tier makes the next decision easier.
The single goal of Part 5 was this — read the bill from the Parts 1–4 environment and make confident savings decisions. From here, a single NAT, one EC2 size, one ALB swap each move $30–$50/month at will.
That closes the series. From Part 1’s “why Private Subnet?” to Part 5’s “decompose the bill,” the start and end of operating Private EC2 are both in your hands. When you stand up a new service, when a friend forwards their AWS bill, when you sketch a VPC at an interview — if you can pull a one-line conclusion from each part on demand, the series has done its job.
Appendix
A. Five commands to read your bill quickly
# 1. Cost so far this month
aws ce get-cost-and-usage \
--time-period Start=$(date -u +%Y-%m-01),End=$(date -u +%Y-%m-%d) \
--granularity MONTHLY \
--metrics UnblendedCost \
--query "ResultsByTime[0].Total.UnblendedCost"
# 2. Cost by service
aws ce get-cost-and-usage \
--time-period Start=$(date -u +%Y-%m-01),End=$(date -u +%Y-%m-%d) \
--granularity MONTHLY \
--metrics UnblendedCost \
--group-by Type=DIMENSION,Key=SERVICE
# 3. Find unattached EIPs
aws ec2 describe-addresses \
--query "Addresses[?AssociationId==null].[PublicIp,AllocationId]" \
--output table
# 4. CloudWatch log groups with no retention
aws logs describe-log-groups \
--query "logGroups[?retentionInDays==null].logGroupName" \
--output table
# 5. Right-sizing recommendations
aws ce get-rightsizing-recommendation \
--service AmazonEC2 \
--query "RightsizingRecommendations[].[CurrentInstance.ResourceId,RightsizingType]"
B. Free Tier quick reference — first 12 months
| Service | Free quota |
|---|---|
| EC2 | t2.micro / t3.micro 750h/month |
| EBS | 30 GB gp2/gp3 |
| S3 | 5 GB Standard |
| Data Transfer Out | 100 GB/month (across all services) |
| CloudWatch | 10 metrics + 5 GB logs |
| ELB / ALB | 750h |
If you stand up Part 1’s environment for learning, almost everything except NAT Gateway lands inside the Free Tier. NAT alone is ~$32/month and the single biggest line.
C. The next single command
The first thing to do after closing this post is to find where last month’s spend actually went.
aws ce get-cost-and-usage \
--time-period Start=$(date -u -v-1m +%Y-%m-01),End=$(date -u +%Y-%m-01) \
--granularity MONTHLY \
--metrics UnblendedCost \
--group-by Type=DIMENSION,Key=SERVICE
The top three services tell you where to start. Apply §2–§4 in order from there — the entire series, in a sense, has been preamble to that one line.