OpenCost 成本分摊与 API:让 Kubernetes 费用可解释
月度成本复盘会上,平台团队给出了三个都“有依据”的数字:Prometheus 显示订单命名空间 CPU 平均使用率不到请求量的一半,OpenCost Allocation 显示它承担了大部分集群费用,云账单又比 Allocation 总额高出一截。业务负责人于是质疑平台在重复收费,财务要求直接按云发票拆到 Pod,研发则想把 request 全部砍半。
这不是计算器精度问题。三组数字分别在回答资源使用、Kubernetes 分配和云端结算问题,它们的数据源、时间窗口与修订节奏不同。若没有保留价格源、聚合维度、空闲规则和数据水位,任何漂亮图表都无法成为可复核的成本证据。
先把三个成本事实面拆开
OpenCost 的价值不只是把金额显示在 UI。它把 Kubernetes 对象、资源请求与用量、基础设施价格和持续时间连接成可查询的分配模型。理解模型时先守住三个边界:
Allocation 把工作负载成本聚合到 cluster、node、namespace、controller、pod、container、label 或 annotation。它适合回答“谁消费了多少可分配成本”。Assets 观察 Node、PV、负载均衡器等底层资产。它适合回答“哪些资产产生了成本,哪些没有被工作负载有效承接”。Cloud Cost 读取云厂商成本与用量报告。它适合回答“云账单实际出现了哪些服务、账户和发票实体费用”。
三者能互相核对,却不能相互冒充。Allocation 常以按需目录价或自定义价格构建近实时模型;Cloud Cost 受云账单发布、折扣、承诺、credit、税费和退款影响;Assets 与工作负载的归属还要经过节点、卷、Service 和 provider ID 映射。OpenCost 规范 给出了成本对象关系,实际 API 能否产生非零数据仍取决于集群指标与云集成。
CPU、内存和 GPU 的工作负载成本也不是简单的 usage × 单价。OpenCost 规范以 request 与 usage 的较大值表达资源分配成本,因为调度器为 request 保留容量,而超出 request 的实际使用同样消耗资源。PV 主要依据 PVC 请求容量,网络和负载均衡器还依赖流量指标、资源映射与云侧价格。因而 request 很大、usage 很低的服务仍可能承担较高 Allocation,这通常是在暴露容量承诺,而不是在证明计费程序出错。
安装前先固定运行链和版本
下面以 OpenCost 1.120.4 与官方 Helm Chart 2.5.26 为操作基线。当前官方安装主路径是 Helm;旧的独立 Kubernetes manifests 已移除,也没有可证实的官方 Operator 安装入口。Kubernetes Allocation 依赖 Prometheus 保存的指标历史,脱离 Kubernetes 的 Docker 方式只能查询 Cloud Costs,不能替代集群成本分配。
准备一个可销毁的 Kubernetes 命名空间、Helm、kubectl、curl、jq,以及一个能被 OpenCost 查询的 Prometheus。先确认目标集群和现有监控 owner,避免再次安装 kube-state-metrics 或第二套 Prometheus:
kubectl config current-context
kubectl version
helm version
kubectl get ns opencost
kubectl get deploy,statefulset -A | grep -E 'prometheus|kube-state-metrics'
kubectl auth can-i create clusterroles
kubectl auth can-i create clusterrolebindings安装身份需要创建 namespace、Deployment、ServiceAccount、ClusterRole 和 ClusterRoleBinding;运行时主要读取 Node、Pod、Namespace、controller、Service、PV/PVC、StorageClass、HPA、PDB 等对象。部署身份与运行身份应分离,不能因为 Helm 需要安装权限,就让 OpenCost Pod 永久持有集群管理员权限。
把版本和 values 纳入仓库评审:
helm repo add opencost https://opencost.github.io/opencost-helm-chart
helm repo update
helm show chart opencost/opencost --version 2.5.26
helm show values opencost/opencost --version 2.5.26 > opencost-values-reference.yaml生产流水线应继续保存 Chart 包摘要、渲染后的镜像 digest 和 values revision。只固定应用镜像会遗漏 RBAC、Service、探针与 Secret 挂载变化;只固定 Chart 版本而不检查镜像身份,也不足以证明供应链输入没有漂移。
接入现有 Prometheus
下面的 values.yaml 展示最小的既有 Prometheus 路径。endpoint、TLS 与认证字段要按组织实际后端填写,Secret 只通过引用注入,不把 bearer token 或 Basic Auth 密码写进文件:
opencost:
exporter:
defaultClusterId: lab-cluster-a
prometheus:
internal:
enabled: false
external:
enabled: true
url: http://prometheus-operated.monitoring.svc:9090
metrics:
kubeStateMetrics:
emitKsmV1Metrics: false
emitKsmV1MetricsOnly: false
ui:
enabled: true
service:
type: ClusterIPdefaultClusterId 是历史连续性和多集群区分键,集群重建后随意改名会把同一集群切成两段,也可能把不同集群合并。external.url 决定查询数据源;填错时 OpenCost Pod 仍可能 Ready,但 Allocation 会为空或持续报 Prometheus 错误。两个 kube-state-metrics 兼容开关在已有 KSM 时保持关闭,避免重复时间序列和错误聚合。
先渲染,再安装:
helm template opencost opencost/opencost \
--namespace opencost \
--version 2.5.26 \
-f values.yaml > rendered-opencost.yaml
grep -nE 'image:|ClusterRole|prometheus|clusterId' rendered-opencost.yaml
helm upgrade --install opencost opencost/opencost \
--namespace opencost \
--create-namespace \
--version 2.5.26 \
-f values.yaml \
--waitPod Ready 只证明进程与探针通过。继续检查对象、日志、Prometheus 可达性和 API:
kubectl -n opencost get deploy,pod,svc,sa
kubectl -n opencost rollout status deployment/opencost --timeout=300s
kubectl -n opencost logs deployment/opencost --all-containers --tail=200
kubectl -n opencost port-forward deployment/opencost 9003:9003 9090:9090另开终端执行:
curl -fsS http://127.0.0.1:9003/healthz
curl -fsS 'http://127.0.0.1:9003/allocation?window=60m&aggregate=namespace' \
| tee allocation-namespace.json \
| jq -e '.code == 200 and (.data | length > 0)'预期证据不是某个固定金额,而是 HTTP 成功、响应带有非空 data、目标 cluster ID 正确、至少一个已运行命名空间出现,并且日志没有持续的 Prometheus query、认证或重复序列错误。新装后短窗口为空时,先等待 Prometheus 完成采样,而不是立刻修改价格。
正向实验:从一个工作负载追到 Allocation
在隔离 namespace 创建一个带稳定归属标签的 Deployment。镜像标签用于实验复现,生产交付应替换为组织批准的不可变 digest:
apiVersion: v1
kind: Namespace
metadata:
name: opencost-lab
labels:
finops.example.io/cost-center: platform-lab
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: allocation-probe
namespace: opencost-lab
spec:
replicas: 2
selector:
matchLabels:
app: allocation-probe
template:
metadata:
labels:
app: allocation-probe
finops.example.io/cost-center: platform-lab
finops.example.io/owner: platform
spec:
containers:
- name: pause
image: registry.k8s.io/pause:3.10
resources:
requests:
cpu: 200m
memory: 128Mi
limits:
cpu: 400m
memory: 256Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: truekubectl apply -f allocation-probe.yaml
kubectl -n opencost-lab rollout status deployment/allocation-probe --timeout=180s
kubectl -n opencost-lab get pod -o wide --show-labels
curl -fsS 'http://127.0.0.1:9003/allocation?window=60m&aggregate=namespace&filter=namespace:%22opencost-lab%22' \
| tee allocation-lab.json | jq
curl -fsS 'http://127.0.0.1:9003/allocation?window=60m&aggregate=label:finops.example.io/cost-center' \
| tee allocation-cost-center.json | jq随着采样窗口形成,预期能在 namespace 聚合中找到 opencost-lab,在标签聚合中找到 platform-lab,CPU、RAM 与 total cost 不再全部为零。金额会受节点价格、持续时间和采样水位影响,不能把示例金额写成验收阈值。更可靠的判断是对象存在、归属维度正确、窗口增长时累计成本不倒退,并且总额变化能由工作负载生命周期解释。
排查某个金额时保留四类身份:查询参数、OpenCost revision、Prometheus 最大样本时间、Pod UID/Node provider ID。只保存截图会丢掉窗口、idle 参数和价格版本,无法复算。
反向实验:让标签缺失稳定暴露为 Unallocated
创建同样消耗资源、但故意不带成本中心标签的 Pod:
apiVersion: v1
kind: Pod
metadata:
name: missing-cost-center
namespace: opencost-lab
labels:
app: missing-cost-center
spec:
restartPolicy: Never
containers:
- name: pause
image: registry.k8s.io/pause:3.10
resources:
requests:
cpu: 100m
memory: 64Mi
securityContext:
allowPrivilegeEscalation: falsekubectl apply -f missing-cost-center.yaml
kubectl -n opencost-lab get pod missing-cost-center --show-labels
curl -fsS 'http://127.0.0.1:9003/allocation?window=60m&aggregate=label:finops.example.io/cost-center' \
| tee allocation-missing-label.json | jq预期该资源不会归入 platform-lab,而进入标签聚合的未分配部分。这里的 Unallocated 表示所选聚合维度缺值,不等于节点空闲,也不等于云账单无法映射。若查询里完全没有该 Pod 的任何成本,再检查采样时间、Prometheus 指标、Pod 生命周期和过滤器,而不是把所有零值都归因于缺标签。
修复时改 Deployment/Job 模板或准入策略,让未来 Pod 天然携带标签;只给运行中的 Pod 临时打标签会在重建后再次丢失。实验中先给当前 Pod 补标签,等待新的指标样本出现,再用相同窗口和聚合条件观察归属变化:
kubectl -n opencost-lab label pod missing-cost-center \
finops.example.io/cost-center=platform-lab \
finops.example.io/owner=platform \
--overwrite
kubectl -n opencost-lab get pod missing-cost-center --show-labels
curl -fsS 'http://127.0.0.1:9003/allocation?window=60m&aggregate=label:finops.example.io/cost-center' | jq该实验把治理问题变成了可观察差异:同一查询规则下,缺标签对象进入未分配 bucket,补齐模板后新对象进入 owner bucket。标签值会进入指标和 API,禁止放邮箱、客户名、工单正文、Token 或其他敏感信息;高基数值还会扩大 Prometheus 存储与查询成本。
Idle、Shared 与 Unallocated 不能合并成“其他”
Idle 是资产成本减去已分配工作负载成本后的容量差额,反映 request、usage、节点规格与调度碎片之间的空间。includeIdle 可以保留 __idle__,shareIdle 可以按非 idle 成本比例重新分摊。Unallocated 是聚合维度缺失。Shared 则是团队明确选定的平台、系统或公共服务成本,并按规则分给消费者。
报表若直接隐藏这些 bucket,表面上的归属率会上升,但守恒被破坏。一个可审计的分摊结果至少满足:
可解释总额 = 直接工作负载成本 + Idle + Shared + Unallocated + 明确的 Overhead若选择再分摊,必须保留分摊前总额、规则版本、分母、目标 bucket 和舍入差。平台成本按请求量、资源成本还是固定比例分摊,会改变团队行为,应由工程与财务共同批准,而不是藏在 Dashboard 查询参数里。
Assets 与 Cloud Cost 如何接上同一条证据链
用 Assets API 检查节点、卷和负载均衡资产:
curl -fsS 'http://127.0.0.1:9003/assets?window=24h&aggregate=type' \
| tee assets-24h.json | jqPV 成本为零时,先检查 PVC 请求容量、StorageClass、PV/provider ID 和价格源;Load Balancer 成本为零时,继续检查 Service 到云资源的映射、实例费与流量费。零值只说明当前管线没有得出金额,不证明资源免费。
Cloud Costs 需要单独启用并挂载 cloud-integration.json Secret:
opencost:
cloudIntegrationSecret: cloud-costs
cloudCost:
enabled: true云身份优先使用 AWS IRSA/EKS Pod Identity、GCP Workload Identity 或等价短期身份。Azure Service Principal、静态 service account key 等路径只在平台约束无法使用工作负载身份时采用,并记录 owner、轮换与撤销。读取资源价格的权限和读取账单明细的权限不是同一个角色;账单数据可能包含账号、资源 ID、合同折扣和组织结构,访问面应比普通集群指标更窄。
curl -fsS 'http://127.0.0.1:9003/cloudCost?window=7d&aggregate=service' \
| tee cloud-cost-7d.json | jq云账单通常存在数小时级发布延迟,最近窗口还可能被修订。/allocation 与 /cloudCost 的差异应按时间窗口、币种、目录价/有效价、折扣、credit、税费、集群外费用和资源映射逐层解释,不能用一次总额相减宣布“OpenCost 算错了”。
把 API 接进项目而不制造第二套账本
项目接入应保存原始响应和查询清单,再进入数据仓库或报表层。下面的脚本用固定 UTC 窗口采集三类事实,并为每个文件保存哈希:
#!/usr/bin/env bash
set -euo pipefail
: "${OPENCOST_URL:=http://127.0.0.1:9003}"
: "${WINDOW:=24h}"
out="opencost-export/${CLUSTER_ID:?set CLUSTER_ID}/${WINDOW}"
mkdir -p "$out"
curl -fsS "$OPENCOST_URL/allocation?window=$WINDOW&aggregate=namespace" > "$out/allocation.json"
curl -fsS "$OPENCOST_URL/assets?window=$WINDOW&aggregate=type" > "$out/assets.json"
curl -fsS "$OPENCOST_URL/cloudCost?window=$WINDOW&aggregate=service" > "$out/cloud-cost.json"
jq -e '.code == 200 and (.data | length > 0)' "$out/allocation.json"
jq -e '.code == 200' "$out/assets.json"
sha256sum "$out"/*.json > "$out/SHA256SUMS"生产任务还应记录精确起止时间、时区、currency、accumulate/resolution、idle/shared 参数、过滤器、集群 ID、价格配置 revision、API 版本和数据最大水位。Cloud Cost 未启用时不应让整个 Kubernetes Allocation 导出失败,可把它记录为独立状态;但也不能生成空文件后冒充成功。
OpenCost API 会暴露命名空间、工作负载、标签、资源规格、价格和云账户信息。默认通过 port-forward 或 ClusterIP 使用;需要 Ingress 时增加认证代理、TLS、NetworkPolicy、最小调用方和访问审计。不要把 9003 或 UI 9090 直接暴露到公网。
从现象反推故障层
API 可用但 Allocation 为空
先检查 Prometheus endpoint、认证、抓取目标和样本水位,再看集群 ID 与查询窗口。Pod Ready 与 API 200 都不能证明 Prometheus 查询成功。OpenCost 日志中的 timeout、context deadline exceeded、no data 与认证失败对应不同层次。
金额突然翻倍
优先寻找重复 kube-state-metrics、重复 scrape、两个 OpenCost 实例写入同一数据源、cluster ID 冲突和重叠导出任务。用 Prometheus series 数量、target 列表和相同 labelset 的重复证据定位,不要先除以二。
PV、网络或 LB 长期为零
逐项验证指标、provider ID、云资源映射和价格输入。网络成本还需要方向与流量数据,不能由 CPU/RAM 推算;未挂载卷可能只出现在 Assets,不会自然归入某个 Pod。
查询随窗口增长而超时
resolution、窗口长度、聚合维度和标签基数共同决定 Prometheus 查询负载与 API 响应体。短任务需要较细分辨率,长期报表应使用预聚合、recording rules 或集中查询层。无界 label 聚合和一次返回全部明细会把成本平台本身变成监控平台的高成本租户。
容量、保留和多集群
Allocation 历史首先受 Prometheus 或兼容后端的 retention 约束;OpenCost 内部 collector retention 与 Cloud Cost 查询窗口是另外两组配置。卸载 OpenCost 不会自动删除独立 Prometheus 历史,删除 Prometheus PVC 则可能让成本历史永久消失。
共享 Thanos、Cortex 或 Mimir 时,每个集群仍部署独立 OpenCost 实例,并明确配置:
CURRENT_CLUSTER_ID_FILTER_ENABLED=true
PROM_CLUSTER_ID_LABEL=<stable-cluster-label>
CLUSTER_ID=<stable-cluster-id>缺少当前集群过滤会把多个集群的时间序列混入一次分配。集群 ID 与 external label 必须唯一、稳定;迁移前保留旧新映射。容量验收观察 Prometheus query latency、API p95、超时率、扫描序列量、内存峰值、导出文件大小和数据水位,而不是只看 OpenCost Pod CPU 平均值。
升级、回滚与退出
升级前保存运行证据,而不是只备份 values:
helm get values opencost -n opencost --all > before-values.yaml
helm get manifest opencost -n opencost > before-manifest.yaml
helm history opencost -n opencost
kubectl -n opencost get sa,role,rolebinding,secret,networkpolicy,ingress同时保存同一窗口下的 Allocation、Assets、Cloud Cost、Idle、Unallocated、PV/LB/network 非零情况、Prometheus targets 和查询延迟。新 Chart 先执行 helm template,比较 ServiceAccount、ClusterRole、Secret 挂载、endpoint、探针、资源限制与镜像 digest。升级后用相同窗口和参数复查;Pod Ready 不能替代数据连续性。
helm upgrade opencost opencost/opencost \
--namespace opencost \
--version 2.5.26 \
-f values.yaml \
--wait
helm rollback opencost <revision> -n opencost回滚后仍要检查 API schema、Prometheus 查询、历史窗口和云账单 ingestion。账单源有固有延迟,短时间没有新行不能单独作为回滚失败判据。
退出前导出仍需保留的原始数据与规则版本,停止 API consumer,撤销 IAM/Workload Identity/Service Principal,删除云集成 Secret 和 Ingress,再卸载 release:
helm uninstall opencost -n opencost
kubectl delete namespace opencost
kubectl delete namespace opencost-lab最后核对独立 Prometheus、PVC、云角色、对象存储、负载均衡器和 DNS 是否仍有 owner 与费用。真正的退出证明是查询入口关闭、凭证撤销、保留数据有 owner、应删除资源已核销,而不是 Helm 显示 release 不存在。
架构取舍
OpenCost 适合需要开放成本模型、可脚本化 API、可自管 Prometheus 和厂商中立 Kubernetes 分配语义的团队。它不是发票总账、预算审批平台或自动资源优化控制器。需要统一多集群产品体验、长期 ETL、团队 RBAC、报告、告警、治理动作与商业支持时,应评估 Kubecost 或其他平台;主要诉求是云发票、合同折扣和集群外支出时,云厂商账单导出仍是最终对账入口。
成熟落地不会寻找一个“绝对正确的成本数字”,而会让每个数字都能回答:来自哪一层、使用什么窗口和价格、怎样归属、何时修订、谁批准规则、怎样复算与退出。OpenCost 的工程价值,就在于把这条证据链开放出来。
