1. 为什么要在 IaC 阶段管成本
一句话总结: 云账单是滞后的,等到月结才发现超支已经无法挽回,而 IaC 是唯一能在「资源创建之前」看到成本影响的位置。
传统成本治理是「事后对账」:月底看账单、发现超支、再去翻谁开了大规格实例。问题在于资源一旦创建就开始计费,追溯成本高、止损慢。
# 账单只能告诉你「花了多少」,不能告诉你「为什么」
aws ce get-cost-and-usage \
--time-period Start=2026-09-01,End=2026-10-01 \
--granularity MONTHLY \
--metrics UnblendedCost
把成本前移到 plan 阶段,逻辑就变成「这个 PR 会让月账单增加多少」。这是 FinOps 里投入产出比最高的一环:
| 阶段 | 成本可见性 | 干预成本 |
|---|---|---|
| 写配置 | 无 | 最低 |
| plan | 可预估 | 低 |
| apply 后 | 有真实数据 | 中 |
| 月结账单 | 滞后 | 高 |
一句话:成本治理的最佳时机不是「账单出来之后」,而是「代码评审的时候」——那时候改一行配置的成本最低。
2. Infracost 与 plan 成本预估
一句话总结: Infracost 读取 plan JSON,结合云厂商价格 API 计算出「本次变更的月度成本增量」,直接贴到 PR 评论里。
Infracost 的输入是 terraform plan 的 JSON 输出,输出是人类可读的成本差异:
# 生成 plan 并交给 Infracost 分析,再只看变更增量
terraform plan -out=plan.tfplan
terraform show -json plan.tfplan > plan.json
infracost breakdown --path plan.json
infracost diff --path plan.json
# 典型输出:逐资源列出增量,最后给出月度总增量
# + aws_instance.web +$146.00/mo
# + aws_db_instance.main +$312.40/mo
# Monthly cost will increase by $458.40
在 CI 里自动评论到 PR:
# .github/workflows/cost.yml
name: cost-estimate
on:
pull_request:
paths: ["**.tf"]
jobs:
infracost:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: infracost/actions/setup@v3
with:
api-key: ${{ secrets.INFRACOST_API_KEY }}
- run: |
terraform init
terraform plan -out=plan.tfplan
terraform show -json plan.tfplan > plan.json
- run: infracost comment github --path plan.json \
--repo $GITHUB_REPOSITORY \
--pull-request ${{ github.event.pull_request.number }} \
--github-token ${{ secrets.GITHUB_TOKEN }} \
--behavior update
2.1 用策略文件固化成本基线
Infracost 支持策略文件,表达「这个资源每月不得超过 X 美元」这类约束,超标即 CI 失败:
# infracost-policy.yml
version: 0.2
policies:
- name: 单实例月成本上限
resource_type: aws_instance
checks:
- type: monthly_cost
max: 200
infracost breakdown --path plan.json --config-file infracost-policy.yml
一句话:Infracost 的价值不在「算得准不准」,而在「让成本在评审时可见」——可见的成本才有机会被讨论。
3. 实例规格与存储权衡
一句话总结: 成本优化的最大杠杆是「选对规格」而非「砍掉资源」,多数团队的浪费来自长期低负载的大规格实例与过高冗余的存储类型。
3.1 实例规格的取舍
| 场景 | 常见浪费 | 优化方向 |
|---|---|---|
| Web 前端 | 长期 CPU 低于 10% | 降规格或改用 Graviton |
| 批处理 | 按峰值选规格 | 改为按需伸缩 + Spot |
| 数据库 | 按峰值选实例 | 读写分离 + 只读副本 |
| 缓存 | 内存按峰值预留 | 降一档并观察命中率 |
# 用变量表达规格,便于统一调整与成本对比
variable "web_instance_type" {
description = "Web 层实例规格"
type = string
default = "t3.medium"
}
resource "aws_instance" "web" {
count = var.web_instance_count
instance_type = var.web_instance_type
ami = data.aws_ami.ubuntu.id
}
# 用 ARM 架构降本(同规格约便宜 20%)
variable "use_graviton" {
type = bool
default = true
}
locals {
instance_type = var.use_graviton ? "t4g.medium" : "t3.medium"
}
3.2 存储类型的权衡
# gp3 比 gp2 便宜且性能可调,新项目默认选 gp3
resource "aws_ebs_volume" "data" {
availability_zone = "us-east-1a"
size = 100
type = "gp3" # 而非 gp2
# 按实际需要配置 IOPS,避免为默认值付溢价
iops = 3000
throughput = 125
}
# 冷数据用 infrequent access 或归档层
resource "aws_s3_bucket_lifecycle_configuration" "logs" {
bucket = aws_s3_bucket.logs.id
rule {
id = "archive-old-logs"
status = "Enabled"
transition {
days = 30
storage_class = "STANDARD_IA"
}
transition {
days = 90
storage_class = "GLACIER"
}
expiration {
days = 365
}
}
}
一句话:规格优化的顺序是「先看清用量,再动配置」——没有监控数据支撑的降配,只是在赌运气。
4. 闲置资源清理
一句话总结: 云账号里常年躺着未挂载的磁盘、未关联的弹性 IP、闲置的负载均衡与快照,它们不产生价值却持续计费,需要定期巡检与自动清理。
# 未挂载的 EBS 卷
aws ec2 describe-volumes \
--filters Name=status,Values=available \
--query 'Volumes[].{ID:VolumeId,Size:Size,Created:CreateTime}' \
--output table
# 未关联的弹性 IP(闲置也要计费)
aws ec2 describe-addresses \
--query 'Addresses[?AssociationId==null].PublicIp'
# 停止超过 30 天的实例
aws ec2 describe-instances \
--filters Name=instance-state-name,Values=stopped \
--query 'Reservations[].Instances[].{ID:InstanceId,Launch:LaunchTime}'
用 Terraform 管理「该被清理的资源」本身就是一种治理:把巡检脚本的输出与 state 对比,找出不在 IaC 管理范围内、又长期闲置的资源。
# 用 data source 找出未使用的卷,纳入告警
data "aws_ebs_volumes" "all" {
filter {
name = "status"
values = ["available"]
}
}
output "orphan_volumes" {
value = [for v in data.aws_ebs_volumes.all.ids : v]
}
| 闲置类型 | 计费 | 清理方式 |
|---|---|---|
| 未挂载 EBS | 是 | 快照后删除 |
| 未关联 EIP | 是 | 直接释放 |
| 空负载均衡 | 是 | 确认后删除 |
| 老快照 | 是 | 生命周期策略 |
| 保留的日志组 | 是 | 设置保留天数 |
# 日志组设置保留天数,避免无限累积
resource "aws_cloudwatch_log_group" "app" {
name = "/app/service"
retention_in_days = 30
}
一句话:闲置资源的可怕之处是「没人认领」——把它写进 IaC 并打上 Owner 标签,它就从「无主账单」变成「有人负责的资产」。
5. 成本护栏与预算告警
一句话总结: 护栏在资源创建前拦截超规格配置,预算告警在成本超阈值时通知,两者一前一后构成成本治理的自动防线。
# AWS Budgets 预算告警
resource "aws_budgets_budget" "monthly" {
name = "monthly-total"
budget_type = "COST"
limit_amount = "5000"
limit_unit = "USD"
time_unit = "MONTHLY"
notification {
comparison_operator = "GREATER_THAN"
threshold = 80
threshold_type = "PERCENTAGE"
notification_type = "ACTUAL"
subscriber_email_addresses = ["finops@example.com"]
}
notification {
comparison_operator = "GREATER_THAN"
threshold = 100
threshold_type = "PERCENTAGE"
notification_type = "FORECASTED"
subscriber_email_addresses = ["finops@example.com"]
}
}
# 在代码层面限制规格:用 validation 拒绝超大实例
variable "instance_type" {
type = string
validation {
condition = contains(
["t3.micro", "t3.small", "t3.medium", "t4g.medium"],
var.instance_type
)
error_message = "仅允许使用审批过的实例规格。"
}
}
5.1 用标签做成本分摊
locals {
cost_tags = {
CostCenter = var.cost_center
Environment = var.environment
Owner = var.team
ManagedBy = "terraform"
}
}
# 激活成本分配标签后可按团队/环境查询
aws ce get-cost-and-usage \
--time-period Start=2026-09-01,End=2026-10-01 \
--granularity MONTHLY \
--metrics UnblendedCost \
--group-by Type=TAG,Key=CostCenter
一句话:护栏的意义是「让超支配置根本进不来」——事后告警只能止损,事前拦截才能省钱。
6. 变更的成本影响评审
一句话总结: 把成本纳入 PR 评审流程:Infracost 评论给出增量、策略文件给出阈值、评审人基于数据判断这笔钱该不该花。
一个完整的成本评审闭环:
# PR 模板中的成本检查项
# ## 成本影响
# - [ ] Infracost 评论已查看
# - [ ] 增量超过 $100/月 已获得审批
# - [ ] 已确认无替代的更省方案
# 对比优化前后的成本
infracost breakdown --path plan-before.json --format json > before.json
infracost breakdown --path plan-after.json --format json > after.json
infracost diff --path after.json --compare-to before.json
| 变更类型 | 成本影响 | 评审重点 |
|---|---|---|
| 新增资源 | 线性增长 | 规格是否最小可用 |
| 扩容 | 倍数增长 | 是否有自动伸缩替代 |
| 存储层调整 | 长期累积 | 生命周期策略是否配置 |
| 架构重构 | 可能双向 | 长期 TCO 而非月费 |
一句话:成本评审不是「审批花钱」,而是「让花钱这件事有据可依」——有数据的讨论远比拍脑袋的争论高效。
7. 成本可见性与持续优化
一句话总结: 一次性优化会随规模增长而失效,需要把成本指标纳入日常看板、定期复查规格与利用率,形成持续优化的节奏。
# 用 Cost Explorer 找出 Top 成本项
aws ce get-cost-and-usage \
--time-period Start=2026-09-01,End=2026-10-01 \
--granularity MONTHLY \
--metrics UnblendedCost \
--group-by Type=DIMENSION,Key=SERVICE \
--query 'ResultsByTime[0].Groups | sort_by(@, &Metrics.UnblendedCost.Amount) | reverse(@)[:10]'
# 利用率与成本对照:低利用率 + 高成本 = 优先优化对象
aws cloudwatch get-metric-statistics \
--namespace AWS/EC2 \
--metric-name CPUUtilization \
--dimensions Name=InstanceId,Value=i-0abc123 \
--start-time 2026-09-01T00:00:00Z \
--end-time 2026-10-01T00:00:00Z \
--period 86400 \
--statistics Average
持续优化的三个节奏:
| 节奏 | 动作 | 负责方 |
|---|---|---|
| 每次 PR | Infracost 增量评审 | 开发者 |
| 每周 | 闲置资源巡检 | 平台团队 |
| 每季度 | 规格与保留实例复查 | FinOps |
一句话:成本优化的终点不是「省到极致」,而是「每一美元都对应一个明确的业务价值」——这需要持续的可见性而非一次性的运动。
8. 总结
成本治理把「账单」翻译成「配置」,让每一笔支出在产生之前就被看见:
| 环节 | 手段 | 关键点 |
|---|---|---|
| 预估 | Infracost + plan JSON | 贴到 PR 评论,评审时可见 |
| 规格 | 变量化 + Graviton | 先看利用率再降配 |
| 存储 | gp3 + 生命周期策略 | 冷数据分层归档 |
| 清理 | 巡检脚本 + Owner 标签 | 无主资源优先处理 |
| 护栏 | validation + Budgets | 事前拦截 + 事后告警 |
| 分摊 | 成本分配标签 | 按团队/环境归集 |
| 评审 | PR 模板 + 阈值 | 有数据支撑决策 |
| 持续 | 看板 + 定期复查 | 周/季度节奏 |
一句话收尾:成本优化不是「省钱运动」,而是「让基础设施的每一分投入都可解释」。从 Infracost 在 plan 阶段亮出价格,到护栏挡住超规格配置,再到标签把账单归集到团队,成本从「财务部门的数字」变成了「工程团队每次提交都能看到的信号」。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。