T
traeai
登录
返回首页
freeCodeCamp.org

AWS FinOps 指南:产品市场契合后 Series A 初创公司的 8 种成本模式

7.5Score
AWS FinOps 指南:产品市场契合后 Series A 初创公司的 8 种成本模式

TL;DR · AI 摘要

本文为 Series A 初创公司提供 AWS FinOps 指南,揭示产品市场契合后常见的 8 种成本模式,并给出具体优化方案,帮助工程师和 CTO 识别并修复基础设施浪费。

核心要点

  • 产品市场契合后(6-12 个月),初创公司通常出现 8 种可预测的 AWS 成本模式,如开发环境闲置。
  • 通过启用 Cost Explorer 和运行基准命令,可量化当前成本结构,作为优化前后的对比基准。
  • 例如 Pattern 1(新员工实验税)显示,一个 m5.xlarge 开发实例每月成本 $138,若 10 人每人遗忘一个,则月浪费 $1,380。

结构提纲

按章节快速跳转。

  1. 文章指出,在产品市场契合后(6-12 个月),初创公司会普遍遇到可预测的 AWS 成本增长模式,需要系统性优化。

  2. 指南面向拥有 15-80 名工程师、月 AWS 账单 $20k-$150k 的 Series A 公司,要求具备基本 AWS 使用权限和工具配置。

  3. 文章强调在优化前必须运行基准命令获取上月服务成本分布,作为后续优化效果的验证依据。

  4. 描述新员工创建开发环境后未及时关闭导致的持续浪费,举例 m5.xlarge 实例每月 $138 成本。

  5. 指出数据传输占总账单 15-20%,且具有极高浪费潜力,需优化跨区域传输和 CDN 使用。

  6. §Pattern 3:RDS 配置不当

    分析 RDS 实例配置过高或未使用预留实例导致的成本问题,建议根据实际负载调整规格。

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • AWS FinOps 指南
    • 核心挑战
      • 产品市场契合后成本激增
      • 8 种可预测成本模式
    • 解决方案
      • 建立成本基准
      • 识别并修复浪费模式
      • 自动化工具(如 Lambda)
    • 关键模式
      • Pattern 1: 新员工实验税
      • Pattern 2: 数据传输浪费
      • Pattern 3: RDS 配置不当

金句 / Highlights

值得收藏与分享的关键句。

#AWS#FinOps#成本优化#初创公司#DevOps
打开原文

标题:Series A 初创公司 AWS FinOps 指南:产品市场契合后出现的 8 种成本模式

URL 来源:https://www.freecodecamp.org/news/the-aws-finops-guide-for-series-a-startups/

发布时间:2026-06-02T16:27:27.160Z

Markdown 内容:

图 1:Series A 初创公司 AWS FinOps 指南:产品市场契合后出现的 8 种成本模式

你完成了 Series A 融资。工程团队快速扩张,功能发布速度加快。在第六个月到第十二个月之间,某人向你转发了一张 AWS Cost Explorer 的截图,其中有一条只向上增长的线。

这条线并非随机。它遵循一种模式。在我审计过的几乎所有公司中,在相同的增长阶段都会出现同样的八种模式。

本指南列出了这八种模式,告诉你应该在哪里查找,并为每种模式提供了修复方法。读完本指南后,你将知道哪些漏洞正在消耗你的资金储备——以及本周该如何应对这些问题。

目录

本指南适用人群

本指南面向 Series A 公司的工程师、CTO 和技术联合创始人——通常拥有 15 至 80 名工程师,每月 AWS 账单在 $20,000 至 $150,000 之间,并且财务团队最近开始关注基础设施支出。

你不需要专门的 FinOps 团队。你需要一名工程师,每周下午抽出一点时间,以及本指南中的八种模式。

开始之前你应该具备以下条件:

  • 启用了 Cost Explorer 的 AWS 账户访问权限
  • 配置了 AWS CLI v2(运行 aws configure
  • EC2、RDS、EBS 和 S3 有基本熟悉
  • 创建了一个 Cost Explorer 书签——你会频繁使用它

完成所有修复所需的时间估计: 两轮冲刺周期内平均 8–20 个工程小时。阅读大约需要 20 分钟。最高回报率的修复(模式 3)大约需要 30 分钟。

开始前:建立基准数据

不要跳过此步骤。没有基准数据的优化只是猜测。在接触任何内容之前,请运行以下命令:

bash
# 获取上个月按服务划分的 AWS 成本明细
# 这将成为你的基准值——保存到某个地方
aws ce get-cost-and-usage \
  --time-period Start=$(date -d 'last month' +%Y-%m-01),End=$(date +%Y-%m-01) \
  --granularity MONTHLY \
  --group-by Type=DIMENSION,Key=SERVICE \
  --metrics UnblendedCost \
  --query 'ResultsByTime[0].Groups[*].{Service:Keys[0],Cost:Metrics.UnblendedCost.Amount}' \
  --output table | sort -k3 -rn

然后截取输出结果。将文件命名为 aws-baseline-YYYY-MM.png。每次修复后,你将与该基准进行对比,以验证实际节省的成本。

Series A 公司的典型成本构成如下:

| AWS 服务 | 占账单比例 | 浪费潜力 | | --- | --- | --- | | EC2(计算) | 45–55% | 高 | | 数据传输 | 15–20% | 非常高 | | RDS | 10–15% | 中等 | | EBS | 8–12% | 中等 | | CloudWatch | 3–6% | 中等 | | 负载均衡器 | 3–5% | 低 |

现在让我们逐一分析每种模式。

模式 1:新员工实验成本

每个工程团队的新成员都需要一个开发环境。这是预期之内的。但预期之外的是,在功能发布后会发生什么:一切照旧。

开发环境仍在运行。以 m5.xlarge 实例为例,每小时成本为 $0.192,一个被遗忘的开发环境每月成本为 $138。如果十名工程师每人忘记关闭一个开发环境,每月仅基础设施成本就高达 $1,380——而这些资源实际上并未发挥作用。

这种模式在 Series A 轮融资后加速,因为招聘速度非常快。一名新工程师周一入职,启动一个 EC2、一个 RDS 和一个开发集群中的命名空间,周五完成功能发布,然后转向下一个任务。开发环境并未引起任何人的注意。开发资源也没有正式的下线流程。

浪费的表现形式如下:

code
Alice(功能/支付流程)的开发环境:
  EC2 m5.xlarge — 最后一次 CPU 活动:23 天前
  RDS db.t3.medium — 最后一次连接:19 天前
  EKS 命名空间 — 最后一次 Pod 调度:15 天前
  每月成本:$187
  状态:运行中

如何找到它们:

bash
# 查找过去 14 天内平均 CPU 使用率低于 5% 的 EC2 实例
# 这些是空闲实例——可以考虑关闭或终止
aws cloudwatch get-metric-statistics \
  --namespace AWS/EC2 \
  --metric-name CPUUtilization \
  --period 1209600 \
  --statistics Average \
  --start-time $(date -d '14 days ago' --iso-8601=seconds) \
  --end-time $(date --iso-8601=seconds) \
  --dimensions Name=InstanceId,Value=YOUR_INSTANCE_ID \
  --query 'Datapoints[*].{Average:Average}' \
  --output table

解决方案——自动停止空闲实例:

以下 Lambda 函数每天晚上 22:00 运行一次。它检查所有标记为 Environment=dev 的 EC2 实例在过去七天内的 CPU 使用率。任何平均 CPU 使用率低于 5% 的实例将被自动停止。在停止之前,会通过 SNS 向工程师的邮箱发送通知,给予他们添加 KeepAlive=true 标签以阻止停止的机会。

python
# idle_environment_stopper.py
# 部署为由 EventBridge 定时触发的 Lambda 函数:cron(0 22 * * ? *)
# 在夜间和周末运行之前停止空闲的开发环境

import boto3
from datetime import datetime, timedelta, timezone

ec2 = boto3.client('ec2')
cloudwatch = boto3.client('cloudwatch')
sns = boto3.client('sns')

IDLE_CPU_THRESHOLD = 5.0      # 停止 CPU 平均使用率低于此阈值的实例
IDLE_DAYS = 7                  # 查看过去 7 天的 CloudWatch 数据
SNS_TOPIC_ARN = 'arn:aws:sns:us-east-1:YOUR_ACCOUNT:dev-environment-alerts'
markdown
def get_average_cpu(instance_id):
    """返回 EC2 实例的 7 天平均 CPU 利用率。"""
    response = cloudwatch.get_metric_statistics(
        Namespace='AWS/EC2',
        MetricName='CPUUtilization',
        Dimensions=[{'Name': 'InstanceId', 'Value': instance_id}],
        StartTime=datetime.now(timezone.utc) - timedelta(days=IDLE_DAYS),
        EndTime=datetime.now(timezone.utc),
        Period=604800,  # 一个 7 天周期
        Statistics=['Average']
    )
    datapoints = response.get('Datapoints', [])
    return datapoints[0]['Average'] if datapoints else 0.0

def lambda_handler(event, context):
    """停止空闲开发实例并通知其所有者。"""
    
    # 查找所有正在运行的开发实例
    response = ec2.describe_instances(
        Filters=[
            {'Name': 'instance-state-name', 'Values': ['running']},
            {'Name': 'tag:Environment', 'Values': ['dev', 'development']},
        ]
    )

    stopped = []
    skipped = []

    for reservation in response['Reservations']:
        for instance in reservation['Instances']:
            instance_id = instance['InstanceId']
            tags = {t['Key']: t['Value'] for t in instance.get('Tags', [])}

            # 跳过明确标记为保持运行的实例
            if tags.get('KeepAlive', '').lower() == 'true':
                skipped.append(instance_id)
                continue

            avg_cpu = get_average_cpu(instance_id)

            if avg_cpu < IDLE_CPU_THRESHOLD:
                # 停止前通知所有者
                owner = tags.get('Owner', 'unknown')
                sns.publish(
                    TopicArn=SNS_TOPIC_ARN,
                    Subject=f'开发环境已停止:{instance_id}',
                    Message=(
                        f'实例 {instance_id}(所有者:{owner})在过去 {IDLE_DAYS} 天内的平均 CPU 利用率为 {avg_cpu:.1f}%,已被停止。\n\n'
                        f'为防止这种情况,请添加标签:KeepAlive=true\n'
                        f'要重新启动:aws ec2 start-instances --instance-ids {instance_id}'
                    )
                )
                ec2.stop_instances(InstanceIds=[instance_id])
                stopped.append({'id': instance_id, 'owner': owner, 'avg_cpu': avg_cpu})

    print(f"已停止 {len(stopped)} 个空闲实例。跳过了 {len(skipped)} 个 KeepAlive 实例。")
    return {'stopped': stopped, 'skipped': skipped}

每月节省金额: $1,000–$2,000,具体取决于团队规模和该模式运行的时间长度。

模式 2:预发布环境泛滥

预发布环境最初只有一个。然后前端团队需要自己的环境,因为后端团队不断破坏他们的环境。接着机器学习(ML)团队需要独立的计算资源。最后质量保证(QA)团队需要一个稳定的环境用于集成测试。

在大家注意到之前,你已经有四个预发布环境全天候运行——每个环境每天有 16 小时处于空闲状态。

浪费并不在于环境的存在本身。而在于运行时间表。预发布环境不需要在凌晨 3 点运行。

浪费的表现如下:

code
staging-frontend:   $250/月   使用时间:周一至周五 09:00-18:00
staging-backend:    $250/月   使用时间:周一至周五 09:00-18:00
staging-ml:         $250/月   使用时间:周一至周五 10:00-17:00
staging-qa:         $250/月   使用时间:周一至周五 09:00-17:00
总计:           $1,000/月     运行时间:每天 24 小时,每周 7 天
实际使用率:       ~35%        你支付了 100%

如何发现:

code
# 查找标记为预发布的 EKS 节点组及其当前状态
aws eks list-nodegroups --cluster-name your-cluster-name --output table

# 检查标记为预发布的 EC2 实例及其启动时间
# 任何运行超过 30 天且没有周末停止计划的实例都是候选对象
aws ec2 describe-instances \
  --filters "Name=tag:Environment,Values=staging" "Name=instance-state-name,Values=running" \
  --query 'Reservations[*].Instances[*].{ID:InstanceId,Type:InstanceType,Launch:LaunchTime}' \
  --output table

解决方案 —— 使用 AWS 实例调度器进行定时启动和停止:

code
# 方案 1:基于标签的调度(CloudFormation 解决方案)
# 在你的预发布 EC2 实例和 RDS 集群上添加以下标签:
# Schedule: office-hours
# 这将在工作日的 08:00 启动实例并在 20:00 停止它们
# 周末完全关闭

# 方案 2:快速 Lambda 解决方案 —— 工作日每天 20:00 停止所有预发布环境
aws events put-rule \
  --schedule-expression "cron(0 20 ? * MON-FRI *)" \
  --name stop-staging-environments \
  --state ENABLED

# 停止 Lambda —— 与模式 1 的模式相同,但针对预发布标签
# 在工作日的 07:30 添加相应的启动规则

除了调度之外的整合

如果前端和后端共享数据库架构,可以将它们整合到一个共享的预发布环境中,并通过命名空间级别的隔离实现资源复用。整合后的成本低于两个独立环境:

code
# 一个带有命名空间隔离的共享预发布集群
# frontend-staging 和 backend-staging 共享节点,通过 Karpenter 实现
# 但通过命名空间级别的网络策略实现隔离
apiVersion: v1
kind: Namespace
metadata:
  name: staging-frontend
  labels:
    environment: staging
    team: frontend
---
apiVersion: v1
kind: Namespace
metadata:
  name: staging-backend
  labels:
    environment: staging
    team: backend

成本计算:

| 场景 | 每月成本 | | --- | --- | | 之前:4 个环境,全天候运行 | $1,000 | | 之后:2 个整合后的环境,仅在工作日运行 | $290 | | 每月节省 | $710 |

模式 3:NAT 网关费用

NAT 网关是我在审计过的所有 AWS 账单中最常被低估的项目之一。它按每 GB 数据处理量收费 $0.045,而在 EKS 集群中,默认情况下会有大量流量通过 NAT 网关。

每个从 ECR 拉取容器镜像的 Pod 都会经过 NAT 网关。每个向 S3 写入数据的 Lambda 函数都会经过 NAT 网关。每个轮询 SQS、查询 DynamoDB 或调用 Secrets Manager API 的服务都会经过 NAT 网关——除非你已经配置了 VPC 终端节点。

VPC 终端节点在你的 VPC 和 AWS 服务之间创建私有连接。流量通过 AWS 骨干网络路由,而不是 NAT 网关。数据传输变为免费。

浪费的表现如下:

bash
# 运行以下命令查看当前 NAT 网关的数据处理账单
aws ce get-cost-and-usage \
  --time-period Start=\((date -d 'last month' +%Y-%m-01),End=\)(date +%Y-%m-01) \
  --granularity MONTHLY \
  --filter '{
    "Dimensions": {
      "Key": "USAGE_TYPE",
      "Values": ["NatGateway-Bytes", "NatGateway-Hours"]
    }
  }' \
  --metrics UnblendedCost \
  --query 'ResultsByTime[0].Total.UnblendedCost.Amount' \
  --output text

如果这个数字高于 $200,则说明你存在 NAT 网关问题。在运行 EKS 的大多数 A 轮融资公司中,这个数字介于 $800 到 $6,000 之间。

解决方案——为四个最高流量的 AWS 服务配置 VPC 终端节点:

bash
# 首先获取你的 VPC ID 和路由表 ID
VPC_ID=$(aws ec2 describe-vpcs \
  --filters "Name=tag:Name,Values=your-vpc-name" \
  --query 'Vpcs[0].VpcId' --output text)

ROUTE_TABLE_ID=$(aws ec2 describe-route-tables \
  --filters "Name=vpc-id,Values=$VPC_ID" "Name=association.main,Values=true" \
  --query 'RouteTables[0].RouteTableId' --output text)

# S3 网关终端节点——免费创建,消除所有 S3 NAT 费用
aws ec2 create-vpc-endpoint \
  --vpc-id $VPC_ID \
  --service-name com.amazonaws.us-east-1.s3 \
  --route-table-ids $ROUTE_TABLE_ID

# DynamoDB 网关终端节点——同样免费
aws ec2 create-vpc-endpoint \
  --vpc-id $VPC_ID \
  --service-name com.amazonaws.us-east-1.dynamodb \
  --route-table-ids $ROUTE_TABLE_ID

# ECR API 终端节点——消除每次容器拉取的 NAT 费用
aws ec2 create-vpc-endpoint \
  --vpc-id $VPC_ID \
  --vpc-endpoint-type Interface \
  --service-name com.amazonaws.us-east-1.ecr.api \
  --subnet-ids $(aws ec2 describe-subnets \
    --filters "Name=vpc-id,Values=$VPC_ID" "Name=tag:Tier,Values=private" \
    --query 'Subnets[*].SubnetId' --output text)

# ECR Docker 终端节点——与 ECR API 一起用于镜像拉取
aws ec2 create-vpc-endpoint \
  --vpc-id $VPC_ID \
  --vpc-endpoint-type Interface \
  --service-name com.amazonaws.us-east-1.ecr.dkr \
  --subnet-ids $(aws ec2 describe-subnets \
    --filters "Name=vpc-id,Values=$VPC_ID" "Name=tag:Tier,Values=private" \
    --query 'Subnets[*].SubnetId' --output text)

当你向 CFO 解释这个问题时,可以称之为“NAT 税”。他们理解税收的概念。“我们正在为内部网络流量支付每 GB $0.045 的税,而这种流量可以在 30 分钟内消除”,这样的表述比“数据处理字节数”更易于理解。

每月节省金额: 根据你的容器拉取频率和 S3 使用情况,约为 $2,000 到 $8,000。

模式 4: Savings Plan 时间安排错误

Savings Plan 是一种承诺,在未来一到三年内以固定每小时美元金额在 AWS 计算资源上消费,以换取 30% 到 70% 的折扣。数学计算非常吸引人。然而,团队往往在时间安排上出错。

当账单变得很大时,本能反应是进行承诺。购买 Savings Plan,减少账单,然后向 CFO 展示。问题在于:如果你没有先进行实例优化(rightsizing),那么你实际上是在以折扣价为浪费的资源付费。当你稍后进行优化时,实际支出可能会低于你的承诺——而你仍然需要为未使用的计算资源付费。

错误顺序的表现如下:

bash
Step 1: AWS 账单为每月 $100,000
Step 2: 购买每月 $70,000 Savings Plan 承诺
Step 3: 优化实例——实际支出降至 $60,000
Step 4: Savings Plan 覆盖 $70,000,但你只使用了 $60,000
Step 5: 你每月为未使用的计算资源支付 $28,000
         (Savings Plan 折扣应用于超额部分)
         
净结果:你在接下来的 12 个月内锁定了浪费

正确顺序的表现如下:

bash
Step 1: 优化实例——支出从 $100,000 降至 $60,000
Step 2: 添加 Spot 实例用于测试环境——支出从 $60,000 降至 $45,000
Step 3: 将兼容的工作负载迁移到 Graviton 实例——支出降至 $36,000
Step 4: 现在购买覆盖每月 $25,000 Savings Plan(稳定状态的 70%)
Step 5: 有效月成本:$12,500(承诺部分)+ $11,000(按需部分)= $23,500

净结果:与原始账单相比,每月节省 $76,500

如何检查你应该承诺多少:

bash
# 查看过去 30 天的 EC2 按需支出
# 这是你的优化基准——优化后的实际使用量
aws ce get-cost-and-usage \
  --time-period Start=\((date -d '30 days ago' +%Y-%m-%d),End=\)(date +%Y-%m-%d) \
  --granularity DAILY \
  --filter '{
    "And": [
      {"Dimensions": {"Key": "SERVICE", "Values": ["Amazon Elastic Compute Cloud - Compute"]}},
      {"Dimensions": {"Key": "PURCHASE_TYPE", "Values": ["On-Demand"]}}
    ]
  }' \
  --metrics UnblendedCost \
  --query 'ResultsByTime[*].{Date:TimePeriod.Start,Cost:Total.UnblendedCost.Amount}' \
  --output table

# 获取 AWS 自身基于你使用情况的 Savings Plan 建议
aws savingsplans get-savings-plans-purchase-recommendation \
  --savings-plans-type COMPUTE_SP \
  --term-in-years ONE_YEAR \
  --payment-option NO_UPFRONT \
  --lookback-period-in-days THIRTY_DAYS

一般来说,承诺优化后的稳定状态按需支出的 60% 到 70%。留出 30% 到 40% 的灵活性。永远不要在未优化的基准上进行承诺。

每月节省金额: 根据计算支出,约为 $5,000 到 $15,000。这是在正确排序时具有最高单次行动 ROI 的模式。

模式 5:跨 AZ 数据传输

当数据跨越一个可用区(Availability Zone)边界时,AWS 会在每个方向收取每 GB $0.01 的费用。$0.01 听起来微不足道,但实际上并非如此——因为在分布式系统中,AZ 边界被频繁跨越,且费用是双向的。

最常见的场景是:你的应用程序Pod被调度到多个可用区(AZ),这是为了提高弹性,但你的数据库被固定在一个可用区内。每个来自不同可用区的Pod发出的数据库查询会产生$0.01/GB的数据传输费用,无论是发送到数据库还是从数据库返回。如果每天有100GB的数据库流量,每月的成本就是$60;如果每天有1TB的流量,则每月成本为$600。

浪费的表现形式如下:

bash
# 检查当前跨可用区数据传输费用
aws ce get-cost-and-usage \
  --time-period Start=$(date -d 'last month' +%Y-%m-01),End=$(date +%Y-%m-01) \
  --granularity MONTHLY \
  --filter '{"Dimensions": {"Key": "USAGE_TYPE", "Values": ["DataTransfer-Regional-Bytes"]}}'  \
  --metrics UnblendedCost \
  --query 'ResultsByTime[0].Total.UnblendedCost.Amount' \
  --output text

如何找出哪些Pod导致了跨可用区流量:

bash
# 检查你的数据库RDS实例所在的可用区
aws rds describe-db-instances \
  --query 'DBInstances[*].{ID:DBInstanceIdentifier,AZ:AvailabilityZone}' \
  --output table

# 检查你的应用程序Pod运行在哪些可用区
kubectl get pods -o wide -n production | awk '{print $7}' | sort | uniq -c

如果你的RDS位于us-east-1a,而60%的Pod位于us-east-1bus-east-1c,那么你就存在跨可用区流量问题。

解决方案——拓扑感知路由:

yaml
# topology-aware-routing.yaml
# 这告诉Kubernetes优先将Pod调度到与请求节点相同的可用区
# 以保持流量本地化

apiVersion: v1
kind: Service
metadata:
  name: payment-api
  namespace: production
  annotations:
    # 尽可能将流量路由到与调用者相同的可用区
    service.kubernetes.io/topology-mode: "Auto"
spec:
  selector:
    app: payment-api
  ports:
  - port: 8080
    targetPort: 8080
yaml
# 对于Pod本身——跨可用区分布但优先本地
# topologySpreadConstraints确保均匀分布
# 同时拓扑感知路由保持流量在可用区内

spec:
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        app: payment-api

对于数据库流量,建议从单可用区RDS迁移到Aurora,后者可以内部处理可用区路由。你的应用程序只需连接一个端点,Aurora会自动进行内部路由,从而避免应用程序层面的跨可用区费用。

每月节省金额: 根据数据库查询量和Pod的可用区分布情况,节省金额为$500至$6,000不等。

模式6:gp2卷陷阱

2014年,AWS推出了gp2 EBS卷。2020年,他们推出了gp3——更便宜、更快,并且具有更好的基准性能。然而,到2026年,大多数A轮公司仍在使用gp2。

区别在于:gp2每GB每月收费$0.10,并提供每GB 3 IOPS(最低100 IOPS)。gp3每GB每月收费$0.08,并提供3,000 IOPS的基准性能,无论卷大小如何。gp3在大多数卷大小上比gp2便宜20%,IOPS速度提高了10倍。迁移过程是在线的——卷在挂载并使用时即可运行。

查找所有gp2卷:

bash
# 列出账户中所有的gp2卷及其大小和每月成本
aws ec2 describe-volumes \
  --filters Name=volume-type,Values=gp2 \
  --query 'Volumes[*].{
    ID:VolumeId,
    Size:Size,
    State:State,
    MonthlyCost_USD:Size
  }' \
  --output table

# 计算总数:卷的数量和总GB数
aws ec2 describe-volumes \
  --filters Name=volume-type,Values=gp2 \
  --query 'length(Volumes)' --output text

aws ec2 describe-volumes \
  --filters Name=volume-type,Values=gp2 \
  --query 'sum(Volumes[*].Size)' --output text

解决方案——通过脚本一次性将所有gp2迁移到gp3:

bash
#!/bin/bash
# migrate_gp2_to_gp3.sh
# 将所有gp2卷迁移到gp3。在线操作——无需停机。
# 每次修改异步运行;卷在整个过程中保持可用。

echo "开始gp2到gp3的迁移..."

# 获取所有gp2卷的ID
VOLUMES=$(aws ec2 describe-volumes \
  --filters Name=volume-type,Values=gp2 \
  --query 'Volumes[*].VolumeId' \
  --output text)

COUNT=0
for VOL_ID in $VOLUMES; do
  echo "正在将$VOL_ID迁移到gp3..."
  aws ec2 modify-volume \
    --volume-id $VOL_ID \
    --volume-type gp3 \
    --no-cli-pager
  COUNT=$((COUNT + 1))
done

echo "已启动$COUNT个卷的迁移。"
echo "修改过程在线运行——无需停机。监控进度:"
echo "aws ec2 describe-volumes-modifications --query 'VolumesModifications[*].{ID:VolumeId,State:ModificationState}'"

验证完成:

bash
# 检查是否还有gp2卷
aws ec2 describe-volumes \
  --filters Name=volume-type,Values=gp2 \
  --query 'length(Volumes)' \
  --output text
# 预期结果:0

每月节省金额: 总EBS支出的20%。如果每月EBS支出为$10,000,则只需花30分钟工作即可节省$2,000。

模式7:无限日志陷阱

CloudWatch日志组默认保留策略为“永不过期”。任何未显式设置保留策略的日志组都会无限积累日志。对于一家忙碌的A轮公司来说,这意味着你存储着自2022年以来从未打开过的调试日志,这些日志是在冲刺评审后创建的。

成本悄然累积。CloudWatch对日志存储收费$0.03/GB/月,对日志摄入收费$0.50/GB/月。一个每天生成50GB日志的集群每天摄入$25(即$750/月),并且这些日志会永久存储,每月成本不断增加。

查找没有保留策略的日志组:

bash
# 列出所有日志组及其保留设置
# 任何显示“retentionInDays: null”的组都是无限的——永远不会过期
aws logs describe-log-groups \
  --query 'logGroups[*].{Name:logGroupName,RetentionDays:retentionInDays,StoredBytes:storedBytes}' \
  --output table | grep -E "(None|null)"

# 统计有多少日志组未设置保留策略
aws logs describe-log-groups \
  --query 'length(logGroups[?retentionInDays==`null`])' \
  --output text

解决方案——批量设置保留策略:

不同类型的日志有不同的合规要求。调试日志不需要保留。审计日志可能需要保留365天。以下表格提供了合理的默认值:

| 日志类型 | 推荐保留时间 | 原因 | | --- | --- | --- | | 应用程序调试日志 | 14天 | 仅在活跃调试时有用 | | 应用程序错误日志 | 90天 | 事件后调查窗口 | | 访问日志 | 30天 | 安全审查窗口 | | CloudTrail审计日志 | 365天 | SOC2证据要求 | | VPC流量日志 | 90天 | 安全调查窗口 |

bash
#!/bin/bash
# set_log_retention.sh
# 设置所有未设置策略的日志组的30天保留期
# 根据日志组类型调整保留期

echo "正在为没有过期策略的日志组设置保留策略..."

# 获取所有没有保留期的日志组
aws logs describe-log-groups \
  --query 'logGroups[?retentionInDays==`null`].logGroupName' \
  --output text | tr '\t' '\n' | while read LOG_GROUP; do

  # 跳过CloudTrail日志 — 这些日志需要更长的保留期以满足SOC2要求
  if echo "$LOG_GROUP" | grep -qi "cloudtrail"; then
    echo "跳过CloudTrail日志组: $LOG_GROUP"
    aws logs put-retention-policy \
      --log-group-name "$LOG_GROUP" \
      --retention-in-days 365
    continue
  fi

  # 对所有其他日志组设置30天保留期
  echo "正在为 $LOG_GROUP 设置30天保留期"
  aws logs put-retention-policy \
    --log-group-name "$LOG_GROUP" \
    --retention-in-days 30
done

echo "完成。超过保留期的日志将由CloudWatch自动删除。"

每月节省: 存储成本可节省500–2,000美元。当减少嘈杂的调试日志时,摄取成本的降低会立即生效。随着旧日志到期,存储成本的节省会在30–90天内累积。

模式8:孤儿资源收集器

每位离职工程师都会留下一些痕迹。附加到已终止实例的EBS卷。分配但未关联的弹性IP。前端服务已被废弃(第三季度)的负载均衡器。替换RDS实例后留下的旧快照。这些都不是有意为之,但它们都会产生费用。

解决方法是每周进行一次审计。不是手动调查——而是每周日晚上运行的自动化脚本,查找孤儿资源,并通过Slack消息发送待删除资源列表。

查找孤儿资源:

bash
# 未附加的EBS卷 — 你正在为无内容的存储付费
aws ec2 describe-volumes \
  --filters Name=status,Values=available \
  --query 'Volumes[*].{
    ID:VolumeId,
    Size:Size,
    Created:CreateTime,
    MonthlyCost:Size
  }' \
  --output table

# 未关联的弹性IP — 当未附加到运行实例时,每个弹性IP每月收费3.60美元
aws ec2 describe-addresses \
  --query 'Addresses[?AssociationId==`null`].[PublicIp,AllocationId]' \
  --output table

# 旧快照 — 创建时间超过90天且不再需要
aws ec2 describe-snapshots \
  --owner-ids self \
  --query "Snapshots[?StartTime<='$(date -d '90 days ago' --iso-8601=seconds)'].[SnapshotId,StartTime,VolumeSize]" \
  --output table

# 空闲负载均衡器 — 活跃但路由零流量
aws elbv2 describe-load-balancers \
  --query 'LoadBalancers[*].{ARN:LoadBalancerArn,DNS:DNSName,State:State.Code}' \
  --output table

每周清理Lambda:

python
# orphan_resource_reporter.py
# 每周日20:00通过EventBridge运行
# 向Slack报告孤儿资源 — 不自动删除
# 删除需要人工决策。Lambda显示待删除资源候选。

import boto3
import json
import urllib.request
from datetime import datetime, timedelta, timezone

SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/YOUR/WEBHOOK/URL'

def get_orphaned_resources():
    """收集所有孤儿AWS资源及其估算的月度成本。"""
    ec2 = boto3.client('ec2')
    elbv2 = boto3.client('elbv2')
    report = {'total_monthly_waste': 0, 'resources': []}

    # 未附加的EBS卷 ($0.08/GB/月,gp3)
    volumes = ec2.describe_volumes(
        Filters=[{'Name': 'status', 'Values': ['available']}]
    )['Volumes']
    for vol in volumes:
        monthly_cost = round(vol['Size'] * 0.08, 2)
        report['resources'].append({
            'type': '未附加的EBS卷',
            'id': vol['VolumeId'],
            'detail': f"{vol['Size']}GB {vol['VolumeType']}",
            'monthly_cost': monthly_cost
        })
        report['total_monthly_waste'] += monthly_cost

    # 未关联的弹性IP ($3.60/月)
    addresses = ec2.describe_addresses()['Addresses']
    for addr in addresses:
        if 'AssociationId' not in addr:
            report['resources'].append({
                'type': '未关联的弹性IP',
                'id': addr['AllocationId'],
                'detail': addr['PublicIp'],
                'monthly_cost': 3.60
            })
            report['total_monthly_waste'] += 3.60

    # 创建时间超过90天的快照
    cutoff = (datetime.now(timezone.utc) - timedelta(days=90)).isoformat()
    snapshots = ec2.describe_snapshots(OwnerIds=['self'])['Snapshots']
    old_snapshots = [s for s in snapshots if s['StartTime'].isoformat() < cutoff]
    for snap in old_snapshots:
        monthly_cost = round(snap.get('VolumeSize', 0) * 0.05, 2)
        report['resources'].append({
            'type': '旧快照 (90+天)',
            'id': snap['SnapshotId'],
            'detail': f"创建于 {snap['StartTime'].strftime('%Y-%m-%d')}",
            'monthly_cost': monthly_cost
        })
        report['total_monthly_waste'] += monthly_cost

    return report

def post_to_slack(report):
    """将孤儿资源报告发送到Slack。"""
    resource_lines = '\n'.join([
        f"• {r['type']} `{r['id']}` — {r['detail']} — *${r['monthly_cost']}/月*"
        for r in report['resources']
    ])
markdown
message = {
    'text': (
        f":money_with_wings: *Weekly Orphaned Resource Report*\n\n"
        f"Found *{len(report['resources'])} orphaned resources* "
        f"costing *${report['total_monthly_waste']:.2f}/month*\n\n"
        f"{resource_lines}\n\n"
        f"Review and delete resources that are no longer needed."
    )
}

req = urllib.request.Request(
    SLACK_WEBHOOK_URL,
    data=json.dumps(message).encode(),
    headers={'Content-Type': 'application/json'}
)
urllib.request.urlopen(req)

def lambda_handler(event, context):
    report = get_orphaned_resources()
    post_to_slack(report)
    return {
        'resources_found': len(report['resources']),
        'monthly_waste': report['total_monthly_waste']
    }

Monthly savings: $500–$2,000. Each departing engineer typically leaves behind $50–$200 in orphaned resources. With a team of 30 and a 30% annual turnover rate, these costs can accumulate quickly.

The Full Savings Summary

| Pattern | Monthly Saving | Time to Fix | Difficulty | | --- | --- | --- | --- | | 1. New hire experiment tax | $1,000–$2,000 | 2 hours (Lambda) | Medium | | 2. Staging proliferation | $600–$800 | 3 hours (scheduling) | Low | | 3. NAT Gateway tax | $2,000–$8,000 | 30 minutes | Low | | 4. Savings Plan timing | $5,000–$15,000 | One decision | Low | | 5. Cross-AZ data transfer | $500–$6,000 | 2 hours | Medium | | 6. gp2 volume trap | $1,000–$5,000 | 30 minutes (script) | Low | | 7. Infinite log trap | $500–$2,000 | 1 hour (script) | Low | | 8. Orphaned resources | $500–$2,000 | 2 hours (Lambda) | Low | | Total potential | $11,100–$40,800/month | | |

What to Do This Week

Don't try to fix all eight patterns this week. Prioritize based on ROI per hour of engineering time:

Day 1 (30 minutes): Pattern 3 — NAT Gateway endpoints. This has the highest ROI per minute of any fix in this guide. One command creates the S3 endpoint. Done.

Day 2 (30 minutes): Pattern 6 — gp2 to gp3 migration. Run the script. Check the output. Done.

Day 3 (1 hour): Pattern 7 — log retention policies. Run the bulk retention script. Done.

Day 4 (2 hours): Patterns 1 and 8 — deploy both Lambdas. They will run automatically from here.

Next sprint: Pattern 2 (staging schedule), Pattern 5 (topology-aware routing), and Pattern 4 (run the rightsizing cycle first, then evaluate Savings Plans).

After each fix, open Cost Explorer and compare against your baseline screenshot from the start of this guide. The line should start trending downward.

Resources

  • **FinOps Foundation Framework** — The practitioner framework this guide contributes to, covering the Inform, Optimize, and Operate phases of cloud cost management
  • **AWS Compute Optimizer** — AWS's own rightsizing recommendation service, used alongside the patterns in this guide for EC2 and EBS recommendations

_Ayobami Adejumo__is a senior platform engineer and FinOps specialist. He has audited AWS infrastructure for over 30 Series A companies and contributes practical tooling to the FinOps Foundation Asset Library._


Learn to code for free. freeCodeCamp's open source curriculum has helped more than 40,000 people get jobs as developers. Get started

code

AI 可能会生成不准确的信息,请核实重要内容