Kubecost 成本监控与治理:从集群估算到账单闭环
月度成本会上,平台仪表盘显示某 namespace 花费下降了三成,云账单却几乎没有变化。研发认为优化已经生效,财务认为工具不可信,平台团队最后发现三件事同时发生:空闲节点成本被隐藏,持久卷仍在计费,最近窗口使用的是公开按需价而不是合同后的账单净额。问题不在某个数字算错,而在 Allocation、Assets 与 Cloud Cost 被当成了同一种成本。
另一个现场更危险。团队为了启用自动优化,直接保留了 Kubecost Cluster Controller 的集群写权限;同时把云账单访问密钥写进 Helm values 并提交到仓库。成本平台从只读观测工具变成了既能读取组织账单、又能修改工作负载的高权限控制面,却没有审批、审计与回滚边界。
先把三个成本世界拆开
Kubecost 使用 OpenCost 分配引擎建立 Kubernetes 成本模型,并在其上提供 ETL、Aggregator、云账单、长期数据、多集群与治理能力。OpenCost 是厂商中立的规范和开源实现,Kubecost 是建立在这套模型之上的产品;两者不是简单的“免费版与付费版”。
Allocation 回答工作负载归属问题,按 cluster、namespace、controller、pod、container、label 等维度聚合 CPU、内存、GPU、PV、网络以及共享和空闲成本。Assets 回答底层资产问题,关注 Node、Disk/PV、LoadBalancer 和 Network。Cloud Cost 读取云账单,覆盖集群内外支出、合同折扣、信用与摊销口径。三者的时间水位和主键不同,不能拿一个页面的合计直接替代另一个。
__idle__ 表示已购买但没有归属给工作负载的预留容量,__unallocated__ 表示缺少当前聚合维度的成本,__unmounted__ 表示没有挂载到 Pod 的 PV 成本。它们都是待解释金额,不是可以从报表中静默删除的噪声。
以 3.x 组件链建立可回滚安装
下面以 Kubecost 3.2.x 为运行基线,命令锁定 Chart 3.2.1。操作端需要 Helm、kubectl、可动态供给块存储的 StorageClass,以及安装集群级只读资源的权限。Kubecost 3.x 主 Chart 包含 Aggregator、Cloud Costs、FinOps Agent 和 Frontend;它不是旧 cost-analyzer Chart 的小版本升级。
先确认身份、集群和存储,再渲染清单。global.clusterId 必须全局唯一且长期稳定,不能复用默认值:
export KUBECOST_VERSION=3.2.1
export KUBECOST_NAMESPACE=kubecost
export KUBECOST_CLUSTER_ID='<globally-unique-cluster-id>'
kubectl auth can-i create clusterroles
kubectl get storageclass
helm version
kubectl version
helm template kubecost kubecost \
--repo https://kubecost.github.io/kubecost/ \
--version "${KUBECOST_VERSION}" \
--namespace "${KUBECOST_NAMESPACE}" \
--set "global.clusterId=${KUBECOST_CLUSTER_ID}" \
--set clusterController.enabled=false \
> /tmp/kubecost-rendered.yaml生产先采用只读观测面。关闭 clusterController 后,检查渲染结果中没有对应 Deployment 与写权限;平台禁止 privileged、hostNetwork 或宿主机 /proc 访问时,还要关闭 networkCosts.enabled。Network Costs 是节点级 DaemonSet,不是普通 namespace 采集器。
grep -nE 'kind: (ClusterRole|StatefulSet|DaemonSet)|cluster-controller|privileged|hostNetwork' /tmp/kubecost-rendered.yaml
helm upgrade --install kubecost kubecost \
--repo https://kubecost.github.io/kubecost/ \
--version "${KUBECOST_VERSION}" \
--namespace "${KUBECOST_NAMESPACE}" --create-namespace \
--set "global.clusterId=${KUBECOST_CLUSTER_ID}" \
--set clusterController.enabled=false \
--wait --timeout 15m
kubectl -n "${KUBECOST_NAMESPACE}" get deploy,statefulset,daemonset,pod,pvc
kubectl -n "${KUBECOST_NAMESPACE}" get events --sort-by=.metadata.creationTimestampPod Running 只证明进程启动。Aggregator 依赖本地块存储和摄取水位;NFS/file storage 不受支持。预期证据应包括 StatefulSet Ready、PVC Bound、Agent 没有持续的 RBAC 错误、Aggregator 没有反复重启,Frontend 能查询到非空窗口。
3.x 的部署边界需要按数据路径理解。FinOps Agent 在集群侧读取对象与指标,Aggregator 把短周期样本压实为可查询的成本事实,Frontend 只负责呈现和代理查询,Cloud Costs 则沿独立凭证读取云账单。把 Frontend 扩成多副本不能提升摄取能力;给 Agent 增加内存也不能修复 Aggregator 磁盘抖动。生产 values 应把每个组件的副本、requests/limits、持久卷、调度约束和拓扑分散分别声明,并把安装时渲染出的实际对象纳入变更审查:
global:
clusterId: <globally-unique-cluster-id>
clusterController:
enabled: false
networkCosts:
enabled: false
# 持久卷、资源字段和组件键以锁定版本渲染结果为准,
# 不把旧 cost-analyzer values 直接复制到 3.x。配置审查至少回答四个问题:哪个 Pod 持有本地状态,PVC 删除策略是什么,节点维护时能否重新挂载,资源限制触发 OOM 后数据从哪个水位恢复。若集群只有单可用区块存储,Aggregator 的可用性上限就受该区约束;若 StorageClass 使用 WaitForFirstConsumer,PVC Pending 还可能来自调度拓扑,而不只是容量不足。先用 helm template 和 kubectl explain 确认当前版本字段,再把环境差异放进版本化 values,避免依靠一串不可审计的 --set。
从端口转发完成第一轮数据验证
UI 默认不应裸露公网。先用端口转发建立临时入口:
kubectl -n kubecost port-forward svc/kubecost-frontend 9090:9090在另一个终端检查 Allocation、Assets 和诊断信息。3.x Allocation 使用 /model/allocation,不要沿用已弃用的 /model/allocation/compute:
curl -fsS 'http://127.0.0.1:9090/model/allocation?window=1d&aggregate=namespace' \
-o /tmp/kubecost-allocation.json
curl -fsS 'http://127.0.0.1:9090/model/assets?window=1d&aggregate=type' \
-o /tmp/kubecost-assets.json
jq 'keys' /tmp/kubecost-allocation.json
jq 'keys' /tmp/kubecost-assets.json正向实验选择一个已有的沙箱 namespace,记录其 workload、requests、PV 和节点,再查询同一窗口:
export LAB_NAMESPACE='<sandbox-namespace>'
kubectl -n "${LAB_NAMESPACE}" get deploy,statefulset,pod,pvc -o wide
curl -fsS "http://127.0.0.1:9090/model/allocation?window=1d&aggregate=namespace&filterNamespaces=${LAB_NAMESPACE}" \
| tee /tmp/kubecost-lab-allocation.json \
| jq .预期可以找到该 namespace,并看到 CPU、RAM、PV 等非负成本分量。若 workload 刚创建,窗口内金额很小是合理的;若对象存在很久却完全没有记录,要沿 Agent 采集、ETL 水位、clusterId、时间窗口和 Aggregator 日志逐层排查,不能先修改价格把数字“调出来”。
反向实验:让归属信息缺失,而不是制造昂贵资源
在沙箱创建一个没有成本中心标签的小负载,等待至少一个采集窗口,再按标签聚合。实验使用组织允许的不可变镜像:
apiVersion: apps/v1
kind: Deployment
metadata:
name: cost-unallocated-lab
namespace: <sandbox-namespace>
spec:
replicas: 1
selector:
matchLabels:
app: cost-unallocated-lab
template:
metadata:
labels:
app: cost-unallocated-lab
spec:
containers:
- name: pause
image: <approved-pause-image@sha256:digest>
resources:
requests:
cpu: 20m
memory: 32Mikubectl apply -f cost-unallocated-lab.yaml
curl -fsS 'http://127.0.0.1:9090/model/allocation?window=1d&aggregate=label:cost_center' \
| tee /tmp/kubecost-label-allocation.json \
| jq .预期该成本进入缺失标签对应的 __unallocated__,而不是从总额消失。补上 cost_center=lab 标签后,新时间片应进入明确归属;旧时间片是否回填取决于历史对象与 ETL 数据,不能假设改标签会重写过去。这个反例同时验证标签治理和金额守恒。
完成后删除实验负载,并再次查询一个新窗口,确认沙箱成本停止增长;删除对象不代表历史窗口消失:
kubectl -n "${LAB_NAMESPACE}" delete deployment cost-unallocated-lab
kubectl -n "${LAB_NAMESPACE}" get deployment cost-unallocated-lab
curl -fsS "http://127.0.0.1:9090/model/allocation?window=1h&aggregate=namespace&filterNamespaces=${LAB_NAMESPACE}" \
| jq .预期第一条查询返回 NotFound,新窗口只保留删除前已发生的费用。若 Deployment 仍存在,先检查清理命令的 context 与 namespace;若对象已删除但后续窗口持续增加,保存对象 UID、查询窗口、Agent 与 Aggregator 水位,排查时间边界、缓存或重复 clusterId,不要直接清空 ETL。
再做一次不会破坏既有数据的存储反向实验:在隔离演练集群把故意不存在的 StorageClass 写入专用 release 的 values,然后执行 helm upgrade --install --wait --timeout 5m。预期 Helm 超时,PVC 保持 Pending,事件包含 storageclass.storage.k8s.io ... not found 或卷拓扑不匹配,Frontend 即使启动也不能形成稳定成本窗口。改回真实 StorageClass 后重新部署,并同时验证 PVC Bound、StatefulSet Ready、摄取水位继续前进和同一历史窗口可查询。最后恢复演练 values,删除专用 release 与 PVC;生产集群禁止用删除真实 PVC 的方式验证恢复能力。
云账单接入必须使用独立身份
没有账单集成时,Kubecost 可使用公开按需价格或自定义价格做估算,但不能证明与实际发票一致。Cloud Cost 接入 AWS CUR、Azure Cost Export 或 GCP BigQuery Billing Export 后,才有机会比较 Net、List、Amortized 和 Invoiced 等口径。
生产优先使用 IRSA、EKS Pod Identity、Workload Identity Federation 或受限托管身份。cloudCost.cloudIntegrationSecret 应引用现有 Secret;cloudIntegrationJSON、静态 Access Key、服务账号 JSON、对象存储密钥、产品 Key 和联邦存储配置不能进入 Git、命令历史、截图或诊断包。账单权限通常覆盖整个账户或组织,影响面远大于 Kubernetes namespace。
权限至少拆成三层:Agent 对 Kubernetes 对象 get/list/watch;Cloud Costs 对指定账单报表、Dataset 或 Bucket 的只读查询;Cluster Controller 对工作负载、ResourceQuota 或 Namespace 的写动作。只做观测时关闭第三层。启用 Actions 时,为每类动作单独限定 namespace、审批人、维护窗口和回滚对象,不授予笼统管理员权限。
多集群不是“安装很多套”
多个集群分别安装 Kubecost Free,并不等于获得统一多集群治理。统一视图通常需要 Primary/Secondary、Federated Storage 与相应授权。Secondary Agent 按唯一 clusterId 写入对象存储,Primary Aggregator 摄取并提供统一查询;云账单凭证只放在 Primary,避免复制到每个集群。
联邦链路至少监控每个 clusterId 的最后写入水位、对象数量、Aggregator 摄取延迟、重复 clusterId 和权限拒绝。对象存储解决长期交换,不替代 Primary Aggregator 的本地高 IOPS 块存储。网络分区时各 Agent 可以继续生成数据,但统一报表会变陈旧,平台必须把 freshness 暴露给读者。
多集群验收不能只看 Primary 页面出现多个名称。建立 cluster_registry,记录 clusterId、环境、区域、owner、Agent 版本、最后成功上传与退役状态;每天用注册表与实际摄取集合做双向差集。注册但未摄取代表链路中断,摄取但未注册代表影子集群或复用 ID。故意在沙箱 Secondary 阻断对象存储写入后,预期 Primary 的该集群水位停止、其他集群继续推进,告警只指向受影响 clusterId;恢复权限后应从断点补传且不产生双份金额。若恢复后总额近似翻倍,优先检查 clusterId 复用、对象前缀和重放幂等,而不是调低成本。
Secondary 只需要向约定前缀写入,Primary 只需要读取这些前缀;账单读取和集群写操作不应随联邦关系传播。可以用身份模拟检查权限边界:
kubectl auth can-i --as=system:serviceaccount:kubecost:<agent-service-account> list pods --all-namespaces
kubectl auth can-i --as=system:serviceaccount:kubecost:<agent-service-account> patch deployments --all-namespaces
kubectl auth can-i --as=system:serviceaccount:kubecost:<controller-service-account> patch deployments -n <approved-namespace>只读 Agent 的预期结果是 yes / no;Controller 未启用时第三条也应为 no。若 Agent 可以 patch,失败证据是 RBAC 边界已经扩大,必须定位绑定来源、撤销多余 RoleBinding/ClusterRoleBinding,再重复三条检查。不要通过删除 ServiceAccount 修复,因为这会把权限错误伪装成采集故障。
容量、性能与成本平台自身的成本
Aggregator 默认容量和保留期只是起点,不是规模承诺。磁盘需求取决于集群数、Pod/container 基数、采集粒度、账单行数、保留期和重算并发。用实际增长率规划:
required_disk = daily_growth_p95 × retention_days × compaction_factor × safety_margin
query_headroom = available_memory - ingestion_working_set - largest_reconciliation_window持续观察 PVC 使用率与增长斜率、摄取延迟、查询延迟、重启、OOM、对象存储失败和账单窗口更新时间。成本平台还会产生节点 requests、PV、对象存储、跨区/公网流量和账单查询费用;这些支出应作为平台成本单列,不能藏进共享池后宣称工具“免费”。
按证据分型排障
安装后无数据时,先看 Agent RBAC 与 Kubernetes API,再看 ETL 和 Aggregator;只有 Allocation 正常而 Cloud Cost 缺失时,才进入云身份、报表路径、查询引擎和账单发布水位。金额过高或过低时,按价格源、时间窗口、聚合维度、idle/shared 策略和账单口径分层,而不是调一个乘数。
kubectl -n kubecost get pods -o wide
kubectl -n kubecost logs -l app.kubernetes.io/instance=kubecost --all-containers --since=30m
kubectl -n kubecost describe pvc
kubectl auth can-i --as=system:serviceaccount:kubecost:<service-account> list pods --all-namespaces诊断包只保留组件版本、对象 UID、错误码、相对时间线和脱敏 clusterId。云账号、资源 ID、真实标签、合同折扣、内部 endpoint 与 Secret 必须掩码。401/403 是身份或权限问题,空窗口可能是水位问题,__unallocated__ 是归属问题,三者不能混成“数据不准”。
升级、回滚与退出都要保留数据契约
从 2.x 进入 3.x 优先平行安装:不同 namespace、不同 release name,固定旧新版本,同时查询相同稳定窗口,对比 Allocation、Assets、Cloud Cost、特殊项、clusterId 和权限。1.x 先经过 2.x;旧 cost-analyzer values 不能直接当作 3.x values。
升级前导出 Helm values、RBAC、Secret 名称而非 Secret 值、API 样本、PVC/对象存储水位和对账基线。新版本至少经过一个完整稳定窗口,再切换入口。若金额守恒破坏、摄取持续落后、权限扩大或 API 契约变化,停止切流并保留旧读路径。
平行验证要同时比较“能查到”和“解释一致”。固定一个已稳定的 24 小时窗口,分别记录旧、新版本的总 Allocation、idle、unallocated、PV、网络、资产与 Cloud Cost,再按 cluster/namespace 抽样追到原始对象。允许的差异必须能归因于价格版本、算法修复或水位,而不是用百分比阈值把未知差异吞掉。升级失败的直接证据包括 PVC schema 无法读取、Aggregator CrashLoopBackOff、查询字段消失、clusterId 改写、云账单水位倒退以及新增 ClusterRole 写权限。
回滚时先把查询入口切回旧 release,再恢复旧 values 与凭证引用;不要让两个 Primary 同时消费并发布同一联邦前缀。回滚复验应看到旧 API 延迟恢复、稳定窗口总额与基线一致、新 Agent 不再上传、旧 Agent 水位继续前进。只有这些证据成立,才清理新 release。这样即使 3.x 的组件或数据格式继续演进,退出路径仍由开放导出、金额守恒和最小权限控制,而不是被某个 UI 或私有存储格式锁死。
退出时先冻结 Actions,导出仍需保留的 Allocation/Assets/Cloud Cost 与分摊规则,撤销 Ingress 和云账单身份,再卸载 release。PVC 可能因 keep 策略保留,helm uninstall 不等于数据已销毁:
helm get values kubecost -n kubecost -o yaml > /tmp/kubecost-values-export.yaml
helm uninstall kubecost -n kubecost
kubectl -n kubecost get pvc,secret,serviceaccount
kubectl get clusterrole,clusterrolebinding | grep -i kubecost确认历史数据已归档或按制度销毁后,再删除 PVC、对象存储前缀、静态密钥和云端角色。最终证据应包括旧 ServiceAccount 不能再访问 API、云身份已撤销、对象存储无继续写入、旧入口不可达,并且替代报表能够解释同一稳定窗口的金额差异。
当团队能从一个金额追到 clusterId、工作负载或资产、价格口径、时间水位、分摊规则和账单行,并能说明由谁批准一次优化动作,Kubecost 才从“有图表的成本工具”变成可治理的 FinOps 平台。
