你们完成了A轮融资,团队迅速扩充了人手,新功能也很快上线。在第六个月到第十二个月之间的某个时候,有人发给你一张AWS成本管理工具的截图,上面显示的那条曲线一直在持续上升。

这条曲线的变化并非偶然,而是遵循某种固定的规律。在我审计过的几乎所有公司中,都在同样的发展阶段出现了这八种相同的趋势。

本指南列出了这八种常见问题,告诉你在哪里可以找到相关信息,并提供了针对每种问题的解决方法。读完这本指南后,你就能知道哪些因素正在消耗你的资金,以及本周应该采取什么措施来解决问题。

目录

本书适合谁阅读

本指南专为处于A轮融资阶段的公司的工程师、首席技术官以及技术联合创始人编写。这类公司通常拥有15到80名工程师,每月的AWS费用在2万到15万美元之间,而且财务团队也开始关注基础设施相关的开支。

你并不需要一个专门的FinOps团队。只需要一名工程师,每周花一个下午的时间,再加上本指南中提供的八种解决方法就可以了。

开始之前你需要准备以下内容:

  • 拥有启用了成本管理工具的AWS账户

  • 配置好了AWS CLI v2版本(执行命令`aws configure`)

  • 对EC2、RDS、EBS和S3等服务有基本的了解

  • 将AWS成本管理工具添加到书签中——你会经常需要使用它

完成所有修改所需的时间:总共需要8到20个工程师的工作小时,分两个阶段完成。阅读本指南大约需要20分钟,而效果最好的解决方案(模式3)的实现时间约为30分钟。

在开始之前:先确定基准数据

千万不要跳过这一步。如果没有基准数据,进行优化就只是盲目猜测而已。在开始任何操作之前,请先运行以下命令:

# 获取上个月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。在每次进行优化后,你都可以将实际效果与这个基准值进行对比,从而验证是否真的实现了成本节约。

在A轮融资阶段,各项费用的典型分布情况如下:

AWS服务 占账单总额的百分比 潜在的浪费空间
EC2(计算资源) 45–55% 较高
数据传输服务 15–20% 非常高
RDS 10–15% 中等
EBS 8–12% 中等
CloudWatch 3–6% 中等
负载均衡器 3–5% 较低

现在,让我们来逐一分析这些常见的浪费现象。

模式1:新员工的“额外负担”

每招聘一名工程技术人员,都需要为他们配备开发环境。这是理所当然的。但问题在于:在功能上线之后,这些开发环境往往会被完全忽略,从而造成浪费。

这些开发环境会持续运行下去。以m5.xlarge型号的实例为例,每小时的成本为0.192美元,因此如果有10名工程师每个人都忘记了关闭自己的开发环境,那么每月将产生1,380美元的浪费——而这些基础设施其实根本没有任何实际作用。

在A轮融资阶段之后,这种浪费现象会更加严重,因为招聘速度会加快。新员工在周一加入团队后,会立即创建EC2实例、RDS数据库以及在开发集群中分配命名空间,然后在下周五之前完成功能开发,接着就转向下一个项目。由于没有专人负责管理这些资源,它们往往会被完全忽略。

这种浪费的具体表现如下:

Alice的开发环境信息:
  EC2 m5.xlarge实例——最后一次CPU使用记录是在23天前
  RDS db.t3.medium数据库——最后一次连接记录是在19天前
  EKS命名空间中的Pod——最后一次调度任务是在15天前
>每月费用:187美元
>状态:正在运行

如何发现这种浪费:

# 查找过去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标签来覆盖这一自动停止机制。

# 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'

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\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)}个被标记为需要保持运行的实例被跳过。")
    return {'stopped': stopped, 'skipped': skipped}

每月的开支: 根据团队规模以及这种模式持续运行的时间长度,开支金额会在1,000到2,000美元之间。

模式2: staging环境的过度使用

最初只设置一个staging环境。但随后前端团队需要自己的独立环境,因为后端团队不断破坏他们之前的环境;接着机器学习团队也需要独立的计算资源;最后质量保障团队则需要一个稳定的环境来进行集成测试。

在大家还没察觉之前,就已经有四个staging环境全天24小时都在运行了——而其中每一个环境每天都有16个小时处于空闲状态。

真正的浪费并不在于这些环境的存在本身,而在于它们的使用安排。staging环境根本没有必要在凌晨3点还在运行。

这种浪费的具体表现是:

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%的费用

如何发现这种浪费:

# 查找被标记为“staging”的EKS节点组及其当前状态
aws eks list-nodegroups --cluster-name your-cluster-name --output table

# 检查被标记为“staging”的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 Instance Scheduler进行定时启动和停止:

# 方案1:利用AWS Instance Scheduler进行基于标签的调度(CloudFormation解决方案)
# 为你的staging EC2实例和RDS集群添加以下标签:
# 调度时间:工作时间内
# 这样就可以在周一至周五的08:00启动实例,并在20:00停止它们;
# 周末则完全停止运行

# 方案2:快速解决方案——利用Lambda函数,在工作日的20:00停止所有staging环境
aws events put-rule \
  --schedule-expression "cron(0 20 ? * MON-FRI *)" \
  --name stop-staging-environments \
  --state ENABLED

# 这个Lambda函数会执行与方案1相同的操作,但它是针对被标记为“staging”的实例进行的;
# 同时还需要在周一至周五的07:30添加相应的启动规则

除了调度之外,还可以采取进一步整合的措施

如果前端团队和后端团队使用的是相同的数据库架构,那么就可以将它们合并到一个共享的staging环境中,并通过命名空间级别的隔离机制来确保数据的安全性。这样做的总成本会比维持两个独立的环境要低。

# 一个带有命名空间隔离功能的共享staging集群
# 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账单中都被最常低估的一项费用。它按每处理1GB数据收取0.045美元的费用——而在EKS集群中,默认情况下会有大量流量通过NAT网关。

任何从ECR获取容器镜像的Pod都会经过NAT网关;任何向S3写入数据的Lambda函数也会经过NAT网关;任何查询SQS、访问DynamoDB或调用Secrets Manager API的服务同样会经过NAT网关——除非你配置了VPC端点。

VPC端点可以在你的VPC与AWS服务之间建立私有连接,这样流量就会直接通过AWS的核心网络传输,而不会经过NAT网关,因此数据传输费用将会为零。

这种浪费的具体表现是什么:

# 运行以下命令查看当前的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美元之间。

解决方法——为流量最大的4项AWS服务配置VPC端点:

# 首先获取你的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端点一起使用,用于获取Docker镜像
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)

在向你的首席财务官解释这一方案时,可以将其称为“NAT税”。他们毕竟了解税收相关的事情。说“我们为内部网络流量支付每GB 0.045美元的税费,而这种费用其实可以在30分钟内就被消除”,这样的表述比“数据处理字节数”更容易被理解。

每月可节省的费用: 根据你使用容器的频率以及S3的使用量,这一数字会在2,000到8,000美元之间变化。

模式4:节约计划的时间安排错误

节约计划意味着你承诺在一段时间内(为一到三年),每小时为AWS计算资源支付固定的费用,以此换取30%到70%的折扣。这个方案从数学角度上看确实很有吸引力,但问题往往出在时间安排上。

当账单金额变得很大时,人们的本能反应就是立即选择这个节约计划,从而减少开支并向首席财务官展示成果。然而问题在于:如果你没有先调整资源使用规模,那么你实际上是在以折扣价为那些被浪费的计算资源付费。而当你后来调整了资源规模后,你的实际支出会低于原先承诺的金额——这时你就会为那些并没有被使用的计算资源支付费用。

错误的操作顺序是怎样的:

步骤1:AWS的每月账单金额为100,000美元。
步骤2:购买每小时70,000美元的节约计划。
步骤3:调整资源使用规模,实际支出降至60,000美元。
步骤4:节约计划可以覆盖70,000美元的费用,但你实际上只使用了60,000美元。
步骤5:你每月要为那些没有被使用的计算资源支付28,000美元,
         (节约计划的折扣仅适用于超出的部分)。

最终结果:你在12个月内一直在为浪费的资源付费。

正确的操作顺序是怎样的:

步骤1:首先调整资源使用规模,使支出从100,000美元降至60,000美元。
步骤2:为临时性任务添加按需使用的计算资源,进一步降低支出至45,000美元。
步骤3:将兼容的工作负载迁移到Graviton平台上,使支出进一步降至36,000美元。
步骤4:此时再购买每小时25,000美元的节约计划。
步骤5:最终的每月费用为:12,500美元(固定费用)+ 11,000美元(按需使用费用)= 23,500美元。

最终结果:与原来的账单相比,每月可节省76,500美元。

如何确定自己应该选择哪种节约计划:

# 查看过去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推荐的节约计划。
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美元之间波动。如果能够正确地运用这种模式,那么它将带来最高的投资回报率。

模式5:跨区域数据传输

当数据跨越不同的可用区边界时,AWS会按每GB 0.01美元的标准收取费用。这个费率看起来似乎微不足道,但实际上并非如此——因为在分布式系统中,数据跨越可用区边界的操作非常常见,而且这种收费是双向计算的。

最常见的情况是:你的应用程序容器被部署在多个可用区内(这样做确实有助于提高系统的容错能力),但你的数据库却固定位于某个特定的可用区内。因此,当来自不同可用区的容器向该数据库发起查询时,数据传输的费用为每GB 0.01美元(去程和回程均需支付这笔费用)。如果每天有100GB的数据流量需要传输,那么每月的总费用就是60美元;而如果每天有1TB的数据流量需要传输,那么每月的总费用就会达到600美元。

这种资源浪费的具体表现是什么?:

# 检查当前的跨区域数据传输费用
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

如何找出哪些容器是导致跨区域数据传输现象的原因?:

# 查看你的RDS数据库实例位于哪个可用区内
aws rds describe-db-instances \
  --query 'DBInstances[*].{ID:DBInstanceIdentifier,AZ:AvailabilityZone}' \
  --output table

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

如果你的RDS数据库位于us-east-1a可用区,而60%的应用程序容器却分布在us-east-1bus-east-1c可用区内,那么你就遇到了跨区域数据传输的问题。

解决方法——基于拓扑结构的路由机制

# topology-aware-routing.yaml
# 这段配置告诉Kubernetes优先将容器调度到与发起请求的节点位于同一可用区内,从而确保数据传输在本地完成
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
# 对于容器本身而言,虽然应该将它们分散部署在多个可用区内,但仍然应优先选择在本地进行数据传输
# topologySpreadConstraints可以确保容器的均匀分布,
# 而 topology-aware routing则能保证数据流量始终在同一个可用区内流动
spec:
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        app: payment-api

对于数据库流量而言,建议将原本使用单可用区的RDS系统迁移到Aurora平台上。Aurora会内部处理跨可用区的路由逻辑,因此你的应用程序无需承担任何跨区域数据传输的费用。

每月可节省的费用: 根据数据库查询量以及容器在不同区域的分布情况,每月可节省500至6,000美元。

模式6:gp2存储卷的陷阱

2014年,AWS推出了gp2类型的EBS存储卷;2020年,他们又推出了gp3类型——这种存储卷价格更低、速度更快,且基础性能也更好。然而到了2026年,大多数处于发展阶段的公司仍然在使用gp2存储卷。

两者之间的区别在于:gp2存储卷的费用为0.10美元/GB/月,每GB提供3 IOPS的吞吐量(最低为100 IOPS);而gp3存储卷的费用为0.08美元/GB/月,无论容量大小如何,都能提供3,000 IOPS的吞吐量。对于大多数容量的情况来说,gp3存储卷的价格便宜20%,IOPS性能也快了10倍。进行迁移操作时,系统可以在线运行——也就是说,在存储卷仍然被使用的情况下,就可以完成迁移过程。

查找你所有的gp2存储卷:

# 列出账户中所有gp2存储卷的信息,包括其容量和每月费用
aws ec2 describe-volumes \
  --filters Name=volume-type,Values=gp2 \
  --query 'Volumes[*].{
    ID:VolumeId,
    Size:Size,
    State:State,
    MonthlyCost_USD:Size
  }' \
  --output table

# 统计总共有多少个gp2存储卷以及它们的总容量
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类型

#!/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}'"

验证迁移是否完成:

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

每月可节省的费用: 相当于你总EBS使用费用的20%。如果你的EBS月费用为10,000美元,那么通过这次迁移,你可以节省2,000美元——这些钱相当于可以让你的业务多运行30分钟。

模式7:无限量的日志堆积问题

CloudWatch中的日志组默认会永久保留日志数据。因此,如果没有为某个日志组设置明确的保留期限,那么这些日志就会被无限期地保存下来。对于那些业务量较大的公司来说,这意味着他们可能会保存从2022年以来产生的各种调试日志,而这些日志可能自创建以来就再没人看过。

这些成本会悄然累积。CloudWatch对日志存储收取0.03美元/GB/月的费用,而对日志导入则收取0.50美元/GB的费用。如果一个集群每天产生50GB的日志,那么每天需要导入25GB的日志,每月共计750GB的日志需要被存储起来,而随之而来的存储成本也会逐月增加。

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

# 列出所有带有保留设置的日志组
# 任何显示“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天 用于安全调查
#!/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 "跳过日志组:$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卷;某个已分配但未被关联到任何实例的Elastic IP;某个在第三季度就被弃用的服务前仍然配置着负载均衡器;还有那些已被替换的RDS实例的旧快照……这些情况都不是有意造成的,但它们都会被计入费用。

为了解决这个问题,我们采用了每周进行一次自动审计的方法。而不是人工检查——而是编写一个自动化脚本,在每个周日晚上运行,该脚本会找出那些“无处可去”的资源,并通过Slack发送消息,列出需要删除的资源清单。

如何找到这些“无主资源”:


# 未关联的EBS卷——你正在为这些空荡荡的存储空间支付费用
aws ec2 describe-volumes \
  --filters Name='status', Values='available' \
  --query 'Volumes[*].{ID:VolumeId, Size:Size, Created:CreateTime, MonthlyCost:Size}' \
  --output table

# 未关联的Elastic 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函数:


# 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卷(gp3类型卷的每月费用为0.08美元/GB)
    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': 'Unattached EBS Volume',
            'id': vol['VolumeId'],
            'detail': f"{vol['Size']}GB {vol['VolumeType']}",
            'monthly_cost': monthly_cost
        })
        report['total_monthly_waste'] += monthly_cost

    # 未关联的Elastic IP(每个的成本为3.60美元/月)
    addresses = ec2.describe_addresses]['Addresses']
    for addr in addresses:
        if 'AssociationId' not in addr:
            report['resources'].append({
                'type': 'Unassociated Elastic 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': 'Old Snapshot (90+ days)',
            'id': snap['SnapshotId'],
            'detail': f"Created {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']}/month*"
        for r in report['resources']
    ])

    message = {
        'text': (
            f":money_with_wings: *每周“无主资源”报告*\n\n"
            f"共找到了*{len(report['resources'])}个“无主资源”,\n\n"
            f>它们的总费用为*${report['total_monthly_waste']:.2f}/月*\n\n"
            f"{resource_lines}\n\n"
            f>请查看这些列表,然后删除那些不再需要的资源吧。」
        )
    }
    
    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']
    }

每月可节省的费用: 500至2,000美元。每位离职的工程师通常会留下50至200美元的“闲置资源”。如果一个团队有30名成员,且人员流动率为30%,那么这些闲置资源的数量会迅速增加。

全部节省措施汇总

>

>

>

>

>

>

>

>

>

节省措施 每月可节省的费用 所需处理时间 难度等级
1. 新员工培训相关开销 1,000至2,000美元 2小时(使用Lambda函数即可解决) 中等难度
2>测试环境资源占用问题 600至800美元 3小时(需要安排调度任务) 低难度
NAT网关相关开销 2,000至8,000美元 30分钟 低难度
节省计划安排问题 5,000至15,000美元 只需做一个决策即可解决 低难度
跨区域数据传输开销 500至6,000美元 2小时 中等难度
gp2存储类型带来的浪费 1,000至5,000美元 30分钟(通过脚本即可解决) 低难度
无限量日志存储问题 500至2,000美元 1小时(通过脚本即可解决) 低难度
“闲置资源”造成的浪费 500至2,000美元 2小时(使用Lambda函数即可解决) 低难度
总潜在节省金额: 每月11,100至40,800美元

参考资源

  • FinOps基础框架 —— 本指南所依据的实践框架,涵盖了云成本管理的规划、优化和运营三个阶段。

  • AWS Cost Explorer API参考手册 —— 本指南中使用的get-cost-and-usage命令的完整使用说明。

  • AWS计算优化服务 —— AWS提供的资源调整推荐服务,可结合本指南中的建议来优化EC2和EBS资源的配置。

  • AWS VPC端点文档 —— 用于推荐措施3的各类VPC端点的详细信息列表。

  • AWS实例调度解决方案 —— AWS提供的CloudFormation模板,可用于推荐措施2中涉及的环境调度任务。

  • Karpenter工具文档 —— 适用于那些希望进一步探索动态节点配置和灵活使用Spot实例的团队的资源。

  • FinOps基础资源库 —— 一个由业界专家共同维护的资源库,其中包含了许多实用脚本和最佳实践资料。

Ayobami Adejumo 是一位资深平台工程师兼FinOps领域的专家。他曾经为30多家处于A轮融资阶段的公司审核过他们的AWS基础设施架构,并为FinOps Foundation的资产库贡献了多种实用工具。

Comments are closed.