Volver al blog
KubernetesFinOpsCost AllocationMulti-TenancyPrometheusPlatform Engineering

Asignación de costes de Kubernetes por equipo: showback, chargeback y clústeres compartidos

Cómo repartir por equipo la factura de Kubernetes: etiquetas de namespace, precio por request o uso, costes ociosos y compartidos, showback antes de chargeback.

P
Rafael Melo
November 4, 202611 min de lectura

Para asignar los costes de Kubernetes por equipo, etiqueta cada namespace con su propietario, convierte tu factura en un precio por CPU-hora y otro por GiB-hora, cobra a cada namespace lo que sea mayor entre lo que solicitó y lo que usó, y reparte la capacidad ociosa y los costes compartidos con una regla acordada antes del primer informe. Después, muestra las cifras durante unos meses antes de que nadie pague por ellas.

La factura cloud no lo hará por ti. Enumera máquinas, discos y balanceadores de carga, mientras que tus equipos comparten esas máquinas pod a pod. Este artículo trata de quién paga qué en un clúster compartido. Reducir el total es otro trabajo, que cubre nuestra guía de optimización de costes en Kubernetes con prácticas SRE. En la práctica, las dos cosas van juntas, porque el equipo que ve su propia cifra es el equipo que la baja.

¿Por qué la factura cloud no muestra el coste por equipo?

Los proveedores cloud facturan por recurso: este nodo, ese disco, ese balanceador de carga, durante tantas horas. Kubernetes programa por pod. En un node pool compartido, un nodo ejecuta pods de cuatro equipos esta hora y de otros dos equipos la siguiente, y el nodo en sí no es de nadie.

  • Los tags de la nube sirven para los recursos que un equipo crea directamente. Un nodo de un pool compartido no puede llevar el tag de un solo equipo.
  • Los pods se mueven. El autoescalado añade y elimina nodos todo el día, así que "qué nodo es de quién" no tiene una respuesta estable.
  • Los datos que necesitas están dentro del clúster: qué namespace solicitó cuánta CPU y memoria, y cuánta usó de verdad, hora a hora.

Así que la asignación es un cálculo que ejecutas tú, no un informe que descargas. Todo lo que sigue se construye con piezas upstream: namespaces y etiquetas, kube-state-metrics, las métricas de contenedor del kubelet y Prometheus.

¿Conviene asignar por namespace o por etiqueta?

Usa el namespace como unidad y registra el propietario como etiquetas del namespace. Las etiquetas de los pods las pone quien escribe cada deployment y se van desalineando con el tiempo. Las etiquetas del namespace se ponen una sola vez, normalmente el equipo de plataforma, y cada pod del namespace hereda el propietario.

  • Da a cada equipo sus propios namespaces, uno por entorno: checkout-prod, checkout-staging.
  • Pon al menos dos etiquetas en cada namespace: team y cost-center. Añade env si staging y producción se pagan con presupuestos distintos.
  • Un namespace compartido por varios equipos es la excepción. Divídelo, o asígnalo por una etiqueta de pod y acepta el mantenimiento extra.

Las etiquetas opcionales acaban faltando. Kubernetes puede rechazar los namespaces nuevos que no las lleven, con una ValidatingAdmissionPolicy integrada y sin ningún controlador adicional:

yaml
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]

El binding empieza en modo Warn y Audit: quien cree un namespace sin las etiquetas recibe un aviso, y el log de auditoría del API server lo registra si la auditoría está activada. Cuando dejen de aparecer avisos, cambia validationActions a [Deny]. La política solo comprueba CREATE, así que los namespaces que ya existen siguen funcionando mientras los etiquetas.

Hay una trampa en la que cae casi todo el mundo. kube-state-metrics v2 no exporta las etiquetas de los namespaces ni de los pods a menos que las incluyas con --metric-labels-allowlist, por ejemplo --metric-labels-allowlist=namespaces=[team,cost-center]. Sin el flag, kube_namespace_labels no devuelve nada y todas las consultas que hacen join con ella salen vacías. Con él, las etiquetas aparecen como label_team y label_cost_center, con los guiones convertidos en guiones bajos.

¿Cómo se convierten la CPU y la memoria en dinero?

Necesitas dos precios: uno por núcleo de CPU y hora, y otro por GiB de memoria y hora. Ninguna métrica te los da. Los calculas a partir de la factura y los mantienes como datos de entrada.

  • Toma el coste mensual de los nodos de un pool, después de descuentos y compromisos de uso.
  • Decide qué parte del precio de un nodo es CPU y qué parte es memoria. Una forma habitual es comparar una instancia de propósito general y una optimizada para memoria con el mismo número de núcleos: la diferencia es aproximadamente el precio de la memoria extra.
  • Divide cada parte entre los núcleos-hora y los GiB-hora que el pool proporcionó ese mes.

Después, cobra a cada contenedor lo que sea mayor entre su request y su uso:

  • El request es capacidad que el scheduler reserva. Nadie más puede usarla, así que le cuesta al clúster tanto si el pod la usa como si no.
  • El uso por encima del request es capacidad prestada del margen libre del nodo. Tampoco es gratis.
  • Un pod sin ningún request parece gratis en un modelo basado solo en requests. Un LimitRange con requests por defecto en cada namespace cierra ese hueco.

En Prometheus, esto cabe en un puñado de recording rules. Los dos precios son constantes que sustituyes por los tuyos:

yaml
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)

Hay algunos detalles que importan:

  • Los pods de Jobs completados siguen reportando sus requests en kube-state-metrics hasta que se borran. El filtro kube_pod_status_phase cuenta solo los pods pendientes y en ejecución.
  • container!="" y container!="POD" descartan los totales a nivel de pod y el contenedor pause, que de otro modo se contarían dos veces.
  • Las reglas dan euros por hora. Una cifra mensual es la media del mes multiplicada por sus horas: avg_over_time(team:cost_eur_per_hour:allocated[30d]) * 24 * 30.

Si prefieres no mantener estas consultas, OpenCost, un proyecto de la CNCF, implementa el mismo modelo del mayor entre request y uso, y añade los precios de catálogo de los proveedores cloud. Las decisiones de asignación del resto de este artículo se le aplican igual.

¿Qué haces con la capacidad ociosa y los servicios compartidos?

Después de la asignación quedan dos bloques de coste, y cómo los trates decide si los equipos se fían del informe.

La capacidad ociosa es capacidad de los nodos que ningún namespace solicitó ni usó: la diferencia entre lo que proporcionan los nodos y lo que se facturó. La última regla de arriba la mide para la CPU. Hay dos formas honestas de tratarla:

  • Repartirla entre los equipos en proporción a lo que se asignó a cada uno. Cada euro acaba en un equipo, pero la capacidad ociosa queda escondida dentro de la cifra de todos.
  • Mantenerla como línea propia, a cargo del equipo de plataforma. La capacidad ociosa pasa a ser una cifra visible con un responsable cuyo trabajo es reducirla, con bin-packing, nodos más pequeños y autoescalado.

Los servicios compartidos son lo que usan todos los equipos: la tarifa del control plane, kube-system, los ingress controllers, la monitorización y el logging, y los balanceadores de carga delante del ingress compartido. Repártelos en proporción al coste asignado, o según una señal de uso cuando exista, como el volumen de logs por namespace para el stack de logging.

Acuerda el método antes de que salga el primer informe. Cambiarlo cuando los equipos ya han visto sus cifras convierte cada revisión en una discusión sobre el método en lugar de sobre el gasto.

Un ejemplo práctico: tres equipos en un mismo clúster

Las cifras son redondas e inventadas para el ejemplo. Un clúster cuesta 12.000 € al mes:

  • 9.000 € son nodos.
  • 3.000 € son compartidos: el control plane, la monitorización y los balanceadores de carga delante del ingress.

Con las reglas de arriba, el coste de nodos del mes se asigna así:

  • Checkout: 3.600 €
  • Search: 2.700 €
  • Data: 1.350 €
  • Capacidad ociosa, sin asignar a nadie: 1.350 €

La opción A reparte la capacidad ociosa y los costes compartidos en proporción a lo asignado (47%, 35% y 18%):

  • Checkout paga 3.600 € + 2.047 € = 5.647 €
  • Search paga 2.700 € + 1.535 € = 4.235 €
  • Data paga 1.350 € + 768 € = 2.118 €

La opción B reparte solo los 3.000 € compartidos y deja la capacidad ociosa al equipo 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 €
  • El equipo de plataforma muestra 1.350 € de capacidad ociosa como línea propia

Las dos suman 12.000 €. Con la opción A, Data paga 238 € por capacidad ociosa que no causó. Con la opción B, los 1.350 € tienen un responsable. Empieza con la opción B en los primeros informes, porque muestra dónde está el desperdicio. Pasa a la opción A más adelante si finanzas necesita cada euro en el presupuesto de un producto.

El mismo informe muestra otra cosa. Si Search solicita 20 núcleos y usa 6 en un día normal, Search paga los 20. Esa es la conversación que lleva a ajustar el tamaño de los requests, y empieza mucho más fácilmente a partir de una cifra que es del propio equipo. Si quieres que alguien más revise el método antes de que salga tu primer informe, hacemos una evaluación gratuita de costes de Kubernetes.

Showback o chargeback: ¿cuál debe ir primero?

  • Showback: cada equipo recibe un informe de sus costes, y no se mueve dinero.
  • Chargeback: esos costes se cargan al presupuesto o al centro de coste del equipo.

Empieza con showback y mantenlo durante unos cuantos ciclos mensuales. Los primeros informes siempre encuentran namespaces sin etiquetar, cargas de trabajo en el sitio equivocado y desacuerdos sobre los costes compartidos. Corregir todo eso cuando no hay nada en juego es más barato que corregirlo en una disputa de presupuesto.

Pasa a chargeback cuando:

  • cada namespace tenga sus etiquetas y la política esté configurada para denegar;
  • la línea sin asignar sea pequeña y siga siéndolo mes tras mes;
  • el método para la capacidad ociosa y los costes compartidos esté por escrito y finanzas lo haya aprobado;
  • los equipos hayan dejado de cuestionar las cifras y hayan empezado a preguntar cómo bajarlas.

Un informe mensual útil por equipo incluye el coste total y la variación respecto al mes anterior, los namespaces más caros, los requests comparados con el uso y la parte de la capacidad ociosa y de los costes compartidos que le corresponde.

¿Cómo se asignan los costes en un clúster multi-tenant?

El mismo modelo funciona cuando los tenants son clientes o unidades de negocio en lugar de equipos. Cada tenant tiene sus propios namespaces con una etiqueta tenant, y unos pocos controles mantienen las cifras honestas:

  • Una ResourceQuota en los namespaces de cada tenant limita el total que puede solicitar y, con ello, lo que puede costar.
  • Un LimitRange da requests por defecto a los contenedores que no declaran ninguno, para que ningún tenant funcione gratis.
  • El uso por encima de los requests se cobra con la regla del mayor entre request y uso, así que un vecino ruidoso paga la capacidad que toma prestada.
  • Un tenant con su propio node pool, aislado con taints y tolerations, paga el pool entero, capacidad ociosa incluida. Reservó esos nodos y nadie más puede usarlos.

¿Cómo se comparan los costes entre varios clústeres?

  • Usa las mismas claves de etiqueta en todos los clústeres. Una etiqueta team en un clúster y una etiqueta owner en otro no se pueden sumar.
  • Da a cada Prometheus una etiqueta externa con el nombre del clúster y envía las series de las recording rules a un almacén central, para que el coste de un equipo se pueda sumar entre clústeres.
  • Mantén los precios por clúster. Las regiones, los tipos de instancia y los compromisos de uso son distintos, y un único precio global de CPU movería coste de los clústeres caros a los baratos.
  • Asigna los costes generales de cada clúster, como su control plane, los namespaces de sistema y los agentes por nodo, dentro de ese clúster antes de sumar clústeres.
  • Un clúster dedicado a un solo equipo no necesita asignación: todo va a ese equipo. Inclúyelo igualmente en el informe con el mismo formato, para que las comparaciones sigan siendo sencillas.

¿Y el gasto fuera del clúster que nadie etiquetó?

Los pods son solo una parte de la factura. Varios costes los crea Kubernetes, pero se facturan fuera de él:

  • Volúmenes persistentes. Un disco se factura mientras existe, aunque ningún pod lo use. Cada PersistentVolume apunta a su claim a través de claimRef y, por tanto, a un namespace. kube_persistentvolumeclaim_resource_requests_storage_bytes da el tamaño solicitado por namespace. Muchos drivers CSI también pueden escribir el namespace del claim en el disco de la nube como tag; comprueba si el tuyo lo hace.
  • Balanceadores de carga. Cada Service de tipo LoadBalancer crea uno en la nube, y kube_service_spec_type{type="LoadBalancer"} los cuenta por namespace.
  • Snapshots, tráfico de red saliente y las bases de datos gestionadas que usa un equipo. Ponles tags en tu código de infraestructura con las mismas claves team y cost-center que usas en los namespaces, para que el informe de la nube y el del clúster se crucen por los mismos nombres. Los tags por defecto en tus módulos de Terraform hacen que esto sea la regla y no la excepción.

Lo que siga sin tags va a una línea sin asignar. Publícala junto con todo lo demás. Una línea sin asignar visible, con alguien responsable de reducirla, se hace más pequeña. Una oculta crece.

Por dónde seguir

La asignación no reduce el gasto por sí sola. Muestra dónde mirar. Los namespaces con mayor diferencia entre requests y uso son donde el right-sizing da resultados antes, y las cargas batch que toleran interrupciones son candidatas a node pools de spot. Las recording rules van junto al resto de tu configuración de Prometheus, y las etiquetas y la política encajan de forma natural en un golden path de platform engineering.

Preguntas frecuentes

¿Los equipos deben pagar por lo que solicitan o por lo que usan? Por el mayor de los dos. Los requests reservan capacidad se use o no, y el uso por encima de los requests toma prestada capacidad de otros. Cobrar solo el uso premia solicitar de más. Cobrar solo los requests hace gratis a los pods sin requests.

¿Necesitamos una herramienta comercial para esto? No. kube-state-metrics, las métricas de contenedor del kubelet y las recording rules de Prometheus bastan para un clúster o para varios. OpenCost implementa el mismo modelo si prefieres no mantener las consultas.

¿Con qué frecuencia debe ejecutarse el informe? Mantén un dashboard diario para los equipos, para que vean el efecto de un cambio en un día, y un informe mensual para showback o chargeback, alineado con la factura cloud.

¿Y las GPU? Ponles precio por dispositivo-hora y asígnalas según los requests. Una GPU solicitada por un pod no está disponible para nadie más, se use o no, así que el request es la medida justa.

¿Quieres que revisemos tu primer informe de asignación antes de que llegue a finanzas? Ayudamos a los equipos a configurar las etiquetas, las recording rules y el reparto de la capacidad ociosa y de los costes compartidos, empezando por una evaluación gratuita de costes de Kubernetes.