本页是译文,以英文原文为准。 阅读原文
写作一次 terraform apply,开出 20+ 个 AWS 服务 —— 全部按新的 Free Plan 调好
AWS 新的 Free Plan 到处是雷:现在每个 ElastiCache 节点都烧 credits,DynamoDB 按需模式绕开 Always Free。一个模块,约束带校验,覆盖 20+ 个服务。
AWS 新的 free plan 送 $200 credits($100 基础 + $100 奖励)。前提是五项活动都得单独激活。 这一步很容易漏掉,我自己就差点漏了。
第一次给个人项目起 ElastiCache 集群,我选了 cache.t4g.micro。Graviton 机型,
看上去就是那个显而易见的现代选择。问题在后面:旧的 free tier 只覆盖 cache.t3.micro,
新的 Free Plan 干脆两种机型都不给额度,全烧 credits。等我发现,已经白烧了几天 —
— 那个 cache 我根本没在用。这个模块就是为防住这类错误写的。
我写了一个 Terraform 模块。五项赚 credits 活动,它自动满足其中四项;开通 20+ 个服务;
而且在结构上让你很难把自己配出账单来。模块已发布在 Terraform Registry:cloudplz/free-tier/aws。
「直接用 free tier 就行了」的问题
不靠 Terraform 摸 AWS free tier,就是在读脚注。几个容易栽跟头的地方:
- ElastiCache 现在每种节点类型都烧 credits。 旧的 12 个月 free tier 里,
cache.t3.micro有 750 小时/月的额度,cache.t4g.micro没有。新的 Free Plan 下两种都没有额度 —— 都消耗 credits。 旧规则还散落在各种老博客里,我就中招了:选了 Graviton 的t4g,以为旧的那张表还算数。 - DynamoDB 按需计费会吃掉 free tier。 25 RCU/WCU 的 Always Free 额度只认 provisioned capacity 模式。 切到 on-demand,就是按请求付费。
- 公网 IPv4 地址 $0.005/小时(约 $3.65/月),2024 年调价之后就是这个价。每个 IP、 每个资源都算。前 12 个月里每月前 750 小时免费 —— 刚好够一台常开实例—— 但每多一个 IP(第二台实例、 一个 NAT gateway、一个 ALB),立刻开始计费。
- KMS customer-managed key 不免费。 SSE-S3 免费,AWS 托管密钥也不收存储费。 但很多安全指南建议给 S3 桶默认上 customer-managed KMS key —— 那就每个 key 每月 $1, 还要加上超出每月 20K 免费请求之后的按次调用费。
上面这些,模块全部编码成带校验的约束。你没法不小心配出一个不免费的 ElastiCache 节点类型: validation 块在 plan 阶段就拒掉,任何资源都还没创建。
模块开通了什么
模块把资源分成两类:一类始终部署,是核心服务;一类靠 features 对象开关,可选。
核心服务(始终开启):
| 资源 | 配置 | 为什么是这个具体设置 |
|---|---|---|
| VPC | /16 CIDR,public + private 子网 | 不上 NAT gateway —— 省约 $32/月 |
| EC2 | t4g.micro,gp3 30GB | t-family 由 validation 强制 |
| 公网 IPv4 | EC2 上 1 个地址 | 由 750 小时/月的免费额度覆盖(前 12 个月);额外的 IP 按 $0.005/小时计费 |
| Lambda | 128MB,Function URL + API Gateway | 128MB 让免费 GB-秒最大化 |
| DynamoDB | PROVISIONED 25 RCU/WCU | on-demand 会绕过 Always Free |
| SQS | Standard queue + DLQ | FIFO 烧请求更快 |
| SNS | Standard topic | 每月 1M 次发布免费 |
| CloudWatch | 2 个 alarm,保留 7 天 | 保持在 Always Free 的 10 个 alarm 上限之内 |
| EventBridge | Scheduler 按 rate(5 min) | 每月 14M 次 Scheduler 调用免费 |
| Budgets | 零支出告警 | 拿到一项能赚 $20 credits 的活动 |
| Secrets Manager | 已启用数据库的凭证 | $0.40/secret/月 —— 故意算核心 |
| IAM | EC2、Lambda、Step Functions 的角色 | 最小权限访问所必需 |
Secrets Manager 是有意为之。数据库凭证属于密钥库,不该躺在 Terraform state 或环境变量里。 $0.40/secret/月,这笔常驻成本不免费,我主动接受。IPv4 地址不一样:只要就这一个, 就被免费额度覆盖。
可选服务(feature 开关):
| Feature | 创建什么 | 月成本 |
|---|---|---|
rds = true | RDS PostgreSQL db.t4g.micro,20GB | 约 $13.98 |
aurora = true ⚠️ | Aurora Serverless v2,0–4 ACU(自动暂停) | 在 Free Plan 限额内由 credits 覆盖 —— 可能需要 Paid Plan(见上方警告) |
elasticache = true | ElastiCache Valkey cache.t3.micro | 约 $12.41 |
cloudfront = true | CloudFront PriceClass_100 + S3 origin | Always Free |
cognito = true | Cognito User Pool | Always Free(10K MAU) |
step_functions = true | Step Functions STANDARD 状态机 | Always Free(每月 4K 次状态转换) |
bedrock_logging = true | Bedrock 调用日志写入 CloudWatch | 配置免费;盯住每月 5GB 的 CloudWatch Logs 额度 |
2026 年三月那次调整分量不轻:Aurora PostgreSQL Serverless v2 进了 Free Plan。
credits 有效期内,每个集群最多 4 ACU、1 GiB 存储。公告文章略过了两个关键细节。
第一,「covered by the plan」说的是由 credits 出资,不是 Always Free: credits 一过期就开始计费。
第二,自动暂停(auto-pause)只在 min_capacity = 0时才生效。旧默认值是 0.5 ACU,
那个值下集群从不暂停,空闲也要约 $44/月,成本模型整个被悄悄破坏掉。模块现在把 aurora_min_capacity 默认设为 0(自动暂停打开,
空闲时计算 ≈ $0;存储仍按 $0.10/GB-月累计,算在 1 GiB 额度内)。依赖暂停之前,
有两点要知道。恢复时首次连接有几秒冷启动延迟。另外,只要有东西一直占着连接,或者某个不兼容的 feature 在起作用,
暂停就会被静默禁用 —— AWS 不报错,集群一直热着。还有存储,Terraform 根本管不住 —
— Aurora 卷以 10 GiB 为增量增长。所以 1 GiB 这个 plan 上限,要你自己盯用量,不是强制。
五项赚 credits 活动
五项活动,AWS 每项给 $20,在 $100 基础额度之上再补 $100 奖励 credits。四项靠terraform apply 加一点收尾就能拿到,
一项得在控制台上手动操作:
| 活动 | Terraform 开通什么 | 手动步骤 |
|---|---|---|
| EC2 | aws_instance.web (t4g.micro) | 终止实例一次(该活动要求启动并终止) |
| RDS | aws_db_instance.postgres (db.t4g.micro) | 无 |
| Lambda | aws_lambda_function_url.handler | 无 —— URL 本身就是触发器 |
| Budgets | aws_budgets_budget.zero_spend | 无 |
| Bedrock | 调用日志配置 | 在控制台里启用模型访问 + 发 1 条 prompt |
跑一遍 terraform apply,把 EC2 实例终止一次(活动校验启动和终止 —— 之后再 terraform apply 把它拉回来)
,然后进 Bedrock 控制台,启用任意一个模型,发一条 prompt。五项活动就齐了 —— $100 奖励 credits。
一个提醒:这些活动的完成情况,AWS 没提供遥测记录。算之前,先在控制台 Explore AWS 组件里确认每一项都显示已完成。
Credit 预算这笔账
总共 $200($100 基础 + $100 奖励),不同配置下能撑多久,看下面这张表。按模块默认值aurora_min_capacity = 0 跑,
Aurora 空闲时自动暂停,也就不再计入每月消耗 —— 空闲计算 ≈ $0,存储只花几美分,
还在 1 GiB 额度内。整张表有一个硬约束:credits 在账号创建后 12 个月过期。
哪种配置都撑不过这个点。
| 场景 | 每月消耗 | $200 能撑 |
|---|---|---|
| 全默认(RDS + ElastiCache + Secrets Manager) | 约 $39.79 | 约 5 个月 |
| 拿到 $20 credit 后关掉 RDS | 约 $25.41 | 约 7.9 个月 |
| 关掉 RDS + ElastiCache | 约 $12.60 | 约 15.9 个月 → 封顶 12 |
成本最优策略:第一个月全默认跑,拿满五个 $20 credit,然后关掉 RDS 和 ElastiCache。 按约 $12.60/月算,纸面上能撑 15.9 个月 —— 但 12 个月过期才是硬天花板。除非你多留几个服务常开, $200 里大概只用得到 $150。
几个值得理解的设计决定
拿 validation 当成本护栏
跟一份纯 README.md 实例类型推荐清单比,模块的价值就在这里:validation 块让你在结构上很难把自己配出费用:
variable "ec2_instance_type" {
default = "t4g.micro"
validation {
condition = can(regex("^t[0-9]+[a-z]*\\.", var.ec2_instance_type))
error_message = "ec2_instance_type must be a t-family instance type (e.g., t4g.micro, t3.micro)."
}
}
variable "elasticache_node_type" {
default = "cache.t3.micro"
validation {
condition = var.elasticache_node_type == "cache.t3.micro"
error_message = "elasticache_node_type is pinned to cache.t3.micro to keep credit burn minimal (~$12.41/mo)."
}
}
variable "aurora_max_capacity" {
default = 4.0
validation {
condition = var.aurora_max_capacity <= 4.0
error_message = "aurora_max_capacity must be <= 4.0 to stay within the free plan cap."
}
}
ElastiCache 被钉死,是因为现在挑节点类型要算的是「烧多少 credits」,不是「免费还是不免费」
。新的 Free Plan 下每种节点类型都消耗 credits,模块就选合法默认值里最便宜的那个,
让「想用 Graviton」的直觉没法悄悄把消耗翻倍。cache.t4g.micro看着像 cache.t3.micro 的现代等价物,
其实并不更便宜。plan 阶段就拦下来,让你不用靠账单才发现这件事。
用 optional object type 做 feature 开关
features 变量用了 Terraform optional() 类型约束,调用方随便省掉几个 key,
都不会报错:
variable "features" {
type = object({
rds = optional(bool, true)
aurora = optional(bool, true)
elasticache = optional(bool, true)
cloudfront = optional(bool, true)
cognito = optional(bool, true)
step_functions = optional(bool, true)
bedrock_logging = optional(bool, true)
})
default = {}
}
default = {} 意味着整块都能省 —— 所有 feature 默认打开。map(bool) 那种常见写法逼你把想要的每个 key 都列出来,
这个友好得多。用 optional(),只声明你改的那几个:
# Disable only the expensive ones
features = {
rds = false
elasticache = false
}
这里有个设计说明。模块早期版本用一条 plan 时校验,把 ElastiCache 和 RDS/Aurora 绑在一起。
前提假设就错了:以为 ElastiCache 共用 RDS 的 DB subnet group。其实不共用 —— ElastiCache 有自己的 aws_elasticache_subnet_group 资源,
模块本来就在创建。那条耦合校验是过期残留,它让「只开 cache」这种配置根本没法用,
当前版本已经删掉。现在每个服务各管各的:elasticache = true 配 rds = false是合法配置。
不上 NAT gateway
VPC 里有 public 和 private 子网,但没有 NAT gateway。这省下约 $32/月($0.045/小时) 。private 子网里需要出网的资源,走 AWS 服务的 VPC endpoint (S3、Secrets Manager、 SSM),要么根本不需要出网。EC2 放在 public 子网。个人学习环境够用了:可以用 security group 锁住 SSH, 或者干脆不用 SSH,改用 SSM Session Manager。
DynamoDB:PROVISIONED 还是 on-demand
Always Free 给 DynamoDB 的额度是 25 RCU、25 WCU、25 GB。只认 provisioned capacity 模式。 on-demand 定价不参与 Always Free。模块默认建一张 provisioned 表,恰好 25/25。 改成 on-demand,每一次读和写都立刻开始烧 credits。
发布到 Terraform Registry
Terraform Registry 对模块名有严格约定:terraform-<PROVIDER>-<NAME>。 GitHub 仓库 cloudplz/terraform-aws-free-tier 会自动映射成 registry 里那个cloudplz/free-tier/aws。
没有申请流程 —— 把 GitHub 账号接到 registry,选中仓库就行。之后带语义化版本号 tag 的 release(v1.0.0、
v1.1.0 等),registry 会自动收录。
CI 流水线在每次 push 和 PR 时运行:
# .github/workflows/ci.yml (abbreviated)
- terraform fmt -check -recursive
- terraform validate
- tflint --recursive
- trivy config .
- terraform test
terraform test 跑 tests/ 里的单元测试。测试校验变量默认值、feature 开关行为、
安全约束(没有公开的 S3 桶、没有未加密存储),还有跨变量校验 —— 全程不创建任何真实基础设施。
5 行 HCL
模块发布之后,用它就是 Terraform 里最精简的形态了。name 是唯一必填输入,其余全部有安全默认值。
把 70+ 个资源全写进自己那份配置,是 500+ 行 HCL —— AWS 每次调价、每次改服务行为,
你都得跟着维护更新。用发布好的模块,version = "~> 1.0" 一改就自动拿到修复。
module "free_tier" {
source = "cloudplz/free-tier/aws"
version = "~> 1.0"
name = "myproject"
}
- Registry:registry.terraform.io/modules/cloudplz/free-tier/aws
- Source:github.com/cloudplz/terraform-aws-free-tier
发现某个本该 Always Free 却没被正确处理的服务?开个 issue —— AWS 一调限额, 成本表就跟着漂。