Mapping the Billing Structure and a Safe Ending: Cost Overview and Teardown Procedure
In the previous chapter, Tasuku's infrastructure grew into a structure that handles multiple environments with modules/ + envs/dev,prod. In this final chapter, we shift perspective and do two things. First, we consolidate the cost discussions scattered across each chapter into a single map. Second, we show the procedure for safely ending the learning infrastructure we've built throughout this series. Finally, we organize the topics this series placed out of scope as next steps.
12.1. Billing structure map
Ever since Chapter 1, Overview: Requirements and Architecture for the Task Management SaaS "Tasuku", this series has held to a policy of "never writing absolute prices, only showing billing dimensions and relative comparisons, and referring you to the official pricing page for details." Cost cautions for individual resources have come up in each chapter's :::warning, but we haven't yet drawn a map of the whole picture. Let's put it together here in one place.
Fargate, ECR, CloudWatch Logs, and Secrets Manager were each covered mainly in terms of configuration in their individual chapters, without an explicit cost caveat. This is the first time we mention them as billing dimensions. The ACM certificate and the S3 Gateway endpoint are listed as "free" rows for contrast. Being free isn't always self-evident, so the aim is to let you check them side by side with the other paid resources.
12.2. Cost-reduction patterns
The env diff in Standardizing Deployment and Separating Environments: From Connectivity Checks to a module + envs Structure §11.5 already doubles as a cost reduction for the dev environment. RDS's multi_az = false limits billing to a single instance, and Auto Scaling's min_capacity = 1 keeps the number of continuously running tasks to a minimum. Here, we'll show a few more cost-reduction patterns you can layer on top of that.
Temporarily stopping RDS. You can stop the RDS instance during periods when it's not in use. A stop is valid for up to 7 days; after 7 days it automatically restarts.1 However, note that some charges continue even while it's stopped. Billing for provisioned storage (including Provisioned IOPS) and backup storage continues during the stop.2 The only charge that drops to zero entirely is the instance's uptime. This feature doesn't apply to Aurora MySQL, Aurora PostgreSQL, or RDS Custom.3 Since Tasuku uses ordinary PostgreSQL (not Aurora), it isn't subject to this restriction and can use this feature.
aws rds stop-db-instance --db-instance-identifier tasuku-dev-db
Reconsidering the NAT Gateway setup doesn't quite make the cut as a cost-reduction pattern. VPC: CIDR Design and Cost Trade-offs for a 2-AZ, 3-Tier Network §4.5 touched on a mode called "Regional NAT Gateway," which covers multiple Availability Zones with a single NAT Gateway. There, we noted a caveat: "whether the code ends up looking like a single gateway is a separate question from whether the actual billing ends up cheaper than provisioning two, one per AZ — we haven't confirmed the latter."4 This fact-check confirmed that the mode does in fact exist and can be used simply by specifying availability_mode = "regional",5 and that it's available in ap-northeast-1 as well.6 However, the core question Chapter 4 left open — whether it's actually cheaper than provisioning two per AZ — remains unconfirmed. That's because a Regional NAT Gateway, too, actually reserves bandwidth per Availability Zone under the hood,4 so the visible count of gateways doesn't necessarily scale proportionally with cost. We don't recommend adopting this option here either, and we're making no code changes.
The Auto Scaling minimum is already set to dev=1 in the env diff in Standardizing Deployment and Separating Environments: From Connectivity Checks to a module + envs Structure. Outside of situations that temporarily need higher performance, such as load testing, there's no reason to raise this value.
All of this is a matter of structure — "what you cut back to reduce what" — not absolute amounts. Check the actual savings against the official pricing page and your own usage.
12.3. Safe teardown
This section covers the envs/dev and envs/prod created in Standardizing Deployment and Separating Environments: From Connectivity Checks to a module + envs Structure (cleaning up the old main/ was already handled in the previous chapter, §11.8). If you naively run terraform destroy, several resources will refuse to be deleted. Let's take stock of what to check before tearing things down.
| Resource | Protection setting | Work required before destroy |
|---|---|---|
| RDS instance | deletion_protection (true in the prod environment) | Change it to deletion_protection = false, re-apply, then destroy |
| S3 buckets (attachments, ALB logs) | force_destroy not set | Empty the bucket, or temporarily set force_destroy = true and re-apply |
| ECR repository | force_delete not set | Delete the images, or temporarily set force_delete = true and re-apply |
| Route 53 hosted zone | Everything except the default SOA/NS records must be deleted | Delete the apex and api Alias records, and the ACM validation records (deleting the hosted zone itself automatically deletes the SOA/NS records) 7 |
| ACM certificate | Referenced by the listener | The dependency on the port 443 listener is resolved automatically by Terraform's dependency graph (no extra work needed) |
Destroy the tfstate bucket created by bootstrap/ last. If you delete it first, you lose the very place where the state referenced by the envs/dev and envs/prod backends is stored. This is the same chicken-and-egg relationship from S3: Bucket Design and Best Practices for Managing tfstate §5.3 — "you can't configure the backend without the bucket, and you can't put the state that creates the bucket without the backend" — showing up in reverse at destroy time. Since this bucket also doesn't have force_destroy set, just like the attachment and ALB-log buckets in the table above, you need to either empty the bucket before destroying it or temporarily set force_destroy = true and re-apply.
Let's also sort out the relationship with the moved block covered in Standardizing Deployment and Separating Environments: From Connectivity Checks to a module + envs Structure §11.6. moved was a device for reassigning an existing resource to a new address without destroying it. Destroy, by contrast, is an operation that intentionally destroys a resource. The purposes are exact opposites, but both share one thing in common: you build the Terraform operation only after accurately grasping the current state of the resources that exist now.
12.4. Looking ahead
Here are some of the items this series placed out of scope that are easy to pick up next.
In RDS: Design Decisions for Managed PostgreSQL and Completing the Security Group Chain §9.4, we noted that while Fargate's Auto Scaling in ECS Part 2: Publishing the web/api Services and Auto Scaling dynamically adjusts the task count, the cpu/memory values themselves are never revisited — and that this series likewise had no chapter that revisited the RDS instance class (db.t4g.micro). The answer to that question is: reviewing instance sizing is a concern for the operations phase. The practical order of operations is to monitor CloudWatch metrics (CPU utilization, free memory, connection count, etc.) and revisit sizing once you can see the actual load trend. There's no reason to preemptively resize at build time.
There are also other directions you could take this further:
- CI/CD pipeline: Automating the standard deployment procedure from Standardizing Deployment and Separating Environments: From Connectivity Checks to a module + envs Structure §11.2 with GitHub Actions or similar
- CloudFront: A setup that places a CDN in front of the ALB, which Overview: Requirements and Architecture for the Task Management SaaS "Tasuku" placed out of scope
- Multi-region / DR: Moving from a single-region setup to a multi-region deployment built with disaster recovery in mind
- IAM permissions boundary: A structural countermeasure for the Terraform executor's self privilege-escalation risk, left open in IAM: Policy JSON Syntax and a Practical Approach to Terraform Execution Permissions §3.6
Summary
- We brought together the cost discussions scattered across each chapter into a single resource × billing-dimension table. Fargate, ECR, CloudWatch Logs, and Secrets Manager were mentioned as billing dimensions for the first time in this chapter
- As cost-reduction patterns, we showed two: temporarily stopping RDS (note that storage billing continues) and the Auto Scaling minimum value. We confirmed that Regional NAT Gateway exists as an option, but the cost-reduction effect Chapter 4 left open still couldn't be resolved this time either, so we passed on adopting it
- We organized the procedure for safely destroying
envs/devandenvs/prodby resource — RDS, S3, ECR, and Route 53. The tfstate bucket gets destroyed last - We folded the RDS instance-sizing asymmetry left open in Chapter 9 into the operations-phase review items
- We listed CI/CD, CloudFront, multi-region, and permissions boundary as the next steps this series placed out of scope
This series has covered the construction of everything from the VPC through RDS and the ECS services, environment separation, and finally how to safely bring it all to an end. From here, you'll dig deeper into whatever areas you need, guided by Tasuku's actual feature development and the issues that surface as you operate it.
Next steps
- Chapter 11: Standardizing Deployment and Separating Environments: End-to-end connectivity checks, the standard ECS deployment procedure, and the refactor to a module + envs structure
- Series table of contents: The structure of all 12 chapters and how to read through them