Para imputar os custos de Kubernetes por equipa, põe em cada namespace uma label com o seu dono, transforma a tua fatura num preço por núcleo-hora de CPU e por GiB-hora, cobra a cada namespace o maior valor entre o que pediu e o que usou, e divide os custos ociosos e partilhados segundo uma regra acordada antes do primeiro relatório. Depois, mostra esses números durante alguns meses antes de alguém pagar por eles.
A fatura da cloud não faz isto por ti. Lista máquinas, discos e balanceadores de carga, enquanto as tuas equipas partilham essas máquinas pod a pod. Este artigo é sobre quem paga o quê num cluster partilhado. Reduzir o total é outro trabalho, tratado no nosso guia de otimização de custos em Kubernetes com práticas SRE. Na prática, as duas coisas andam juntas, porque a equipa que consegue ver o seu próprio número é a equipa que o baixa.
Porque é que a fatura da cloud não mostra o custo por equipa?
Os fornecedores de cloud faturam por recurso: este nó, aquele disco, aquele balanceador de carga, durante tantas horas. O Kubernetes agenda por pod. Num node pool partilhado, um nó corre pods de quatro equipas nesta hora e de outras duas equipas na hora seguinte, e o próprio nó não pertence a ninguém.
- As tags da cloud funcionam para os recursos que uma equipa cria diretamente. Um nó num pool partilhado não pode ter a tag de uma só equipa.
- Os pods mudam de sítio. O escalamento automático acrescenta e remove nós durante todo o dia, por isso "que nó é de quem" não tem uma resposta estável.
- Os dados de que precisas estão dentro do cluster: que namespace pediu quanta CPU e memória, e quanto usou realmente, hora a hora.
Por isso, a imputação é um cálculo que corres tu, não um relatório que descarregas. Tudo o que se segue é construído com peças upstream: namespaces e labels, kube-state-metrics, as métricas de contentores do kubelet e o Prometheus.
Deves imputar por namespace ou por label?
Usa o namespace como unidade e põe a propriedade no namespace, sob a forma de labels. As labels dos pods são definidas por quem escreve cada deployment e vão divergindo com o tempo. As labels dos namespaces são definidas uma vez, normalmente pela equipa de plataforma, e todos os pods do namespace herdam o dono.
- Dá a cada equipa os seus próprios namespaces, um por ambiente:
checkout-prod,checkout-staging. - Põe pelo menos duas labels em cada namespace:
teamecost-center. Acrescentaenvse staging e produção forem pagos por orçamentos diferentes. - Um namespace partilhado por várias equipas é a exceção. Divide-o, ou imputa-o por uma label de pod e aceita a manutenção extra.
As labels opcionais acabam por faltar. O Kubernetes pode recusar novos namespaces sem elas, com uma ValidatingAdmissionPolicy nativa e sem nenhum controller adicional:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: require-cost-labels
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["namespaces"]
validations:
- expression: >-
has(object.metadata.labels) &&
'team' in object.metadata.labels &&
'cost-center' in object.metadata.labels
message: "New namespaces need the labels team and cost-center."
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: require-cost-labels
spec:
policyName: require-cost-labels
validationActions: [Warn, Audit]O binding começa em modo Warn e Audit: quem criar um namespace sem as labels recebe um aviso, e isso fica no registo de auditoria do API server, se a auditoria estiver ativa. Quando os avisos pararem, muda validationActions para [Deny]. A política só verifica CREATE, por isso os namespaces que já existem continuam a funcionar enquanto lhes pões as labels.
Há uma armadilha que apanha quase toda a gente. O kube-state-metrics v2 não exporta labels de namespaces nem de pods, a não ser que as indiques com --metric-labels-allowlist, por exemplo --metric-labels-allowlist=namespaces=[team,cost-center]. Sem a flag, kube_namespace_labels não devolve nada e todas as consultas que a usam num join vêm vazias. Com ela, as labels aparecem como label_team e label_cost_center, com os hífenes convertidos em underscores.
Como transformar CPU e memória em dinheiro?
Precisas de dois preços: um por núcleo de CPU por hora e outro por GiB de memória por hora. Nenhuma métrica te dá estes valores. Calculas-os a partir da fatura e guardas-os como parâmetros de entrada.
- Pega no custo mensal dos nós de um pool, depois de descontos e compromissos de utilização.
- Decide que parte do preço de um nó é CPU e que parte é memória. Uma forma comum é comparar uma instância de uso geral com uma otimizada para memória, com o mesmo número de núcleos: a diferença é, aproximadamente, o preço da memória extra.
- Divide cada parte pelos núcleos-hora e GiB-hora que o pool forneceu nesse mês.
Depois, cobra a cada contentor o maior valor entre o que pediu (o request) e o que usou:
- O request é capacidade que o scheduler reserva. Mais ninguém a pode usar, por isso custa ao cluster quer o pod a use, quer não.
- A utilização acima do request é capacidade emprestada da folga do nó. Também não é gratuita.
- Um pod sem nenhum request parece gratuito num modelo que só olha para os requests. Um LimitRange com requests por omissão em todos os namespaces fecha essa brecha.
No Prometheus, isto cabe em meia dúzia de recording rules. Os dois preços são constantes que substituis pelos teus:
groups:
- name: cost-allocation
interval: 5m
rules:
# Example prices. Replace them with the numbers from your own bill.
- record: cost:cpu_core_hour:eur
expr: vector(0.031)
- record: cost:memory_gib_hour:eur
expr: vector(0.0042)
# Per namespace: the larger of request and usage, for running and pending pods only.
- record: namespace:cost_cpu_cores:billed
expr: |
sum by (namespace) (
max by (namespace, pod, container) (
kube_pod_container_resource_requests{resource="cpu", unit="core"}
* on (namespace, pod) group_left ()
max by (namespace, pod) (kube_pod_status_phase{phase=~"Pending|Running"})
or
rate(container_cpu_usage_seconds_total{container!="", container!="POD"}[5m])
)
)
- record: namespace:cost_memory_gib:billed
expr: |
sum by (namespace) (
max by (namespace, pod, container) (
kube_pod_container_resource_requests{resource="memory", unit="byte"}
* on (namespace, pod) group_left ()
max by (namespace, pod) (kube_pod_status_phase{phase=~"Pending|Running"})
or
container_memory_working_set_bytes{container!="", container!="POD"}
)
) / 2^30
# Euros per hour, per namespace and per team.
- record: namespace:cost_eur_per_hour:allocated
expr: |
namespace:cost_cpu_cores:billed * scalar(cost:cpu_core_hour:eur)
+ namespace:cost_memory_gib:billed * scalar(cost:memory_gib_hour:eur)
- record: team:cost_eur_per_hour:allocated
expr: |
sum by (label_team) (
namespace:cost_eur_per_hour:allocated
* on (namespace) group_left (label_team) kube_namespace_labels
)
# Capacity nobody was billed for.
- record: cluster:cost_cpu_cores:idle
expr: |
sum(kube_node_status_allocatable{resource="cpu", unit="core"})
- sum(namespace:cost_cpu_cores:billed)Há alguns detalhes que importam:
- Os pods de Jobs concluídos continuam a reportar os seus requests no kube-state-metrics até serem apagados. O filtro
kube_pod_status_phaseconta apenas os pods pendentes e em execução. container!=""econtainer!="POD"excluem os totais ao nível do pod e o contentor pause, que de outra forma seriam contados duas vezes.- As regras dão euros por hora. O valor mensal é a média ao longo do mês vezes as horas que o mês tem:
avg_over_time(team:cost_eur_per_hour:allocated[30d]) * 24 * 30.
Se preferires não manter estas consultas, o OpenCost, um projeto da CNCF, implementa o mesmo modelo do maior entre request e utilização e acrescenta os preços de tabela da cloud. As decisões de imputação no resto deste artigo aplicam-se-lhe da mesma forma.
O que fazer com a capacidade ociosa e os serviços partilhados?
Depois da imputação, sobram dois montes de custos, e a forma como os tratas decide se as equipas confiam no relatório.
A capacidade ociosa é a capacidade dos nós que nenhum namespace pediu nem usou: a diferença entre o que os nós fornecem e o que foi cobrado. A última regra acima mede-a para a CPU. Há duas formas honestas de a tratar:
- Distribui-a pelas equipas, na proporção do que foi imputado a cada uma. Cada euro acaba numa equipa, mas a capacidade ociosa fica escondida dentro do número de todas.
- Mantém-na como uma linha própria, da responsabilidade da equipa de plataforma. A capacidade ociosa passa então a ser um número visível, com um dono cujo trabalho é reduzi-lo, através de bin-packing, nós mais pequenos e escalamento automático.
Os serviços partilhados são o que todas as equipas usam: a taxa do control plane, o kube-system, os ingress controllers, a monitorização e o logging, e os balanceadores de carga à frente do ingress partilhado. Divide-os na proporção do custo imputado, ou por um sinal de utilização quando exista, como o volume de logs por namespace no caso do stack de logging.
Chega a acordo sobre o método antes de sair o primeiro relatório. Mudá-lo depois de as equipas verem os seus números transforma cada revisão numa discussão sobre o método em vez de sobre a despesa.
Um exemplo prático: três equipas num cluster
Os números são redondos e inventados para o exemplo. Um cluster custa 12 000 € por mês:
- 9 000 € correspondem aos nós.
- 3 000 € são partilhados: o control plane, a monitorização e os balanceadores de carga à frente do ingress.
Com as regras acima, o custo dos nós no mês é imputado assim:
- Checkout: 3 600 €
- Search: 2 700 €
- Data: 1 350 €
- Capacidade ociosa, não imputada a ninguém: 1 350 €
A opção A distribui os custos ociosos e partilhados na proporção da imputação (47%, 35% e 18%):
- Checkout paga 3 600 € + 2 047 € = 5 647 €
- Search paga 2 700 € + 1 535 € = 4 235 €
- Data paga 1 350 € + 768 € = 2 118 €
A opção B distribui apenas os 3 000 € partilhados e deixa a capacidade ociosa com a equipa de plataforma:
- Checkout paga 3 600 € + 1 412 € = 5 012 €
- Search paga 2 700 € + 1 059 € = 3 759 €
- Data paga 1 350 € + 529 € = 1 879 €
- A equipa de plataforma apresenta 1 350 € de capacidade ociosa numa linha própria
As duas opções somam 12 000 €. Na opção A, a equipa Data paga 238 € por capacidade ociosa que não causou. Na opção B, os 1 350 € têm dono. Começa pela opção B nos primeiros relatórios, porque mostra onde está o desperdício. Passa para a opção A mais tarde, se o departamento financeiro precisar de cada euro no orçamento de um produto.
O mesmo relatório mostra outra coisa. Se Search pede 20 núcleos e usa 6 num dia típico, Search paga 20. É essa a conversa que leva a fazer o right-sizing dos requests, e começa muito mais facilmente a partir de um número que é da própria equipa. Se quiseres um segundo par de olhos sobre o método antes de sair o teu primeiro relatório, fazemos uma avaliação gratuita de custos de Kubernetes.
Showback ou chargeback: qual deve vir primeiro?
- Showback: cada equipa recebe um relatório dos seus custos e não há movimento de dinheiro.
- Chargeback: esses custos são cobrados ao orçamento ou ao centro de custo da equipa.
Começa pelo showback e mantém-no durante alguns ciclos mensais. Os primeiros relatórios encontram sempre namespaces sem labels, cargas de trabalho no sítio errado e desacordos sobre os custos partilhados. Corrigir isso quando nada está em jogo sai mais barato do que corrigi-lo numa disputa de orçamento.
Passa para o chargeback quando:
- todos os namespaces têm as suas labels e a política está em modo deny;
- a linha não imputada é pequena e continua pequena mês após mês;
- o método para os custos ociosos e partilhados está escrito e o departamento financeiro concordou com ele;
- as equipas deixaram de questionar os números e começaram a perguntar como os baixar.
Um relatório mensal útil por equipa inclui o custo total e a variação face ao mês anterior, os namespaces mais caros, os requests comparados com a utilização, e a parte dos custos ociosos e partilhados que lhe cabe.
Como imputar custos num cluster multi-tenant?
O mesmo modelo funciona quando os tenants são clientes ou unidades de negócio em vez de equipas. Cada tenant tem os seus próprios namespaces com uma label tenant, e alguns controlos mantêm os números honestos:
- Uma ResourceQuota nos namespaces de cada tenant limita o total que pode pedir e, com isso, o que pode custar.
- Um LimitRange dá requests por omissão aos contentores que não declaram nenhum, para que nenhum tenant corra de graça.
- A utilização acima dos requests é cobrada pela regra do maior entre request e utilização, por isso um vizinho ruidoso paga a capacidade que pede emprestada.
- Um tenant com o seu próprio node pool, isolado com taints e tolerations, paga o pool inteiro, incluindo a capacidade ociosa. Reservou esses nós e mais ninguém os pode usar.
Como comparar custos entre vários clusters?
- Usa as mesmas chaves de label em todos os clusters. Uma label
teamnum cluster e uma labelownernoutro não se podem somar. - Dá a cada Prometheus uma external label com o nome do cluster e envia as séries gravadas para um armazenamento central, para que o custo de uma equipa possa ser somado entre clusters.
- Mantém os preços por cluster. As regiões, os tipos de instância e os compromissos de utilização diferem, e um preço global único de CPU deslocaria custos dos clusters caros para os baratos.
- Imputa os custos indiretos de cada cluster, como o control plane, os namespaces de sistema e os agentes por nó, dentro desse cluster antes de somares clusters.
- Um cluster dedicado a uma equipa não precisa de imputação: vai todo para essa equipa. Mesmo assim, apresenta-o no mesmo formato, para que as comparações continuem simples.
E a despesa fora do cluster que ninguém etiquetou?
Os pods são apenas parte da fatura. Há vários custos que são criados pelo Kubernetes mas faturados fora dele:
- Volumes persistentes. Um disco é faturado enquanto existir, mesmo sem nenhum pod a usá-lo. Cada PersistentVolume aponta para o seu claim através de
claimRefe, por aí, para um namespace.kube_persistentvolumeclaim_resource_requests_storage_bytesdá o tamanho pedido por namespace. Muitos drivers CSI também conseguem escrever o namespace do claim no disco da cloud como tag; confirma se o teu o faz. - Balanceadores de carga. Cada Service do tipo LoadBalancer cria um na cloud, e
kube_service_spec_type{type="LoadBalancer"}conta-os por namespace. - Snapshots, tráfego de saída e as bases de dados geridas que uma equipa usa. Põe-lhes tags no teu código de infraestrutura com as mesmas chaves
teamecost-centerque usas nos namespaces, para que o relatório da cloud e o do cluster se cruzem pelos mesmos nomes. As tags por omissão nos teus módulos de Terraform fazem disto a regra e não a exceção.
O que continuar sem tags vai para uma linha não imputada. Publica-a com tudo o resto. Uma linha não imputada visível, com alguém responsável por a reduzir, diminui. Uma escondida cresce.
Por onde continuar
A imputação não reduz a despesa por si só. Mostra onde olhar. Os namespaces com a maior diferença entre requests e utilização são onde o right-sizing compensa primeiro, e as cargas batch que toleram interrupções são candidatas a node pools de spot. As recording rules ficam bem ao lado do resto da tua configuração de Prometheus, e as labels e a política encaixam naturalmente num golden path de engenharia de plataformas.
Perguntas frequentes
As equipas devem pagar pelo que pedem ou pelo que usam? Pelo maior dos dois. Os requests reservam capacidade, seja ela usada ou não, e a utilização acima dos requests pede capacidade emprestada aos outros. Cobrar só a utilização premeia quem pede a mais. Cobrar só os requests torna gratuitos os pods sem requests.
Precisamos de uma ferramenta comercial para isto? Não. O kube-state-metrics, as métricas de contentores do kubelet e as recording rules do Prometheus chegam para um cluster ou para vários. O OpenCost implementa o mesmo modelo, se preferires não manter as consultas.
Com que frequência deve correr o relatório? Mantém um dashboard diário para as equipas, para que vejam o efeito de uma alteração no espaço de um dia, e um relatório mensal para o showback ou o chargeback, alinhado com a fatura da cloud.
E as GPUs? Atribui-lhes um preço por dispositivo-hora e imputa-as com base nos requests. Uma GPU pedida por um pod fica indisponível para todos os outros, usada ou não, por isso o request é a medida justa.
Queres que o teu primeiro relatório de imputação seja verificado antes de chegar ao departamento financeiro? Ajudamos as equipas a configurar as labels, as recording rules e a divisão dos custos ociosos e partilhados, começando por uma avaliação gratuita de custos de Kubernetes.