La factura de Kubernetes de una empresa SaaS europea había pasado de 15.000 a 38.000 dólares al mes en 18 meses, mientras su base de usuarios solo se duplicaba. En cinco semanas la bajamos a 19.610 dólares, un recorte del 48 %, sin cambios en el código de la aplicación, sin reducir réplicas en producción y sin caídas. La capacidad spot fue lo que más ahorró, 7.400 dólares al mes, pero llegó en segundo lugar: primero el right-sizing y, al final, los compromisos de uso.
A continuación están las cinco fases en orden, cada una con el manifiesto, la consulta o el comando para repetirla. Nuestra guía de optimización de costes en Kubernetes con prácticas SRE explica cómo medir el desperdicio en general. Este artículo profundiza en lo que esa guía solo esboza: ejecutar cargas en capacidad spot sin que los usuarios lo noten.
El punto de partida: tres clústeres al 18 % de CPU
La empresa usaba Kubernetes gestionado en una gran nube pública, con tres clústeres: producción, staging y desarrollo. Todos funcionaban día y noche sobre instancias on-demand de uso general con 8 vCPU y 32 GiB. Nuestra auditoría encontró:
- Uso medio de los nodos: 18 % de CPU y 34 % de memoria.
- No producción encendida 24/7: staging y desarrollo solo se usaban en horario laboral, de 09:00 a 19:00 CET.
- Sin autoescalado: el número de nodos era fijo.
- Requests inflados: la mayoría de los pods pedía entre dos y cuatro veces lo que usaba.
- Almacenamiento olvidado: 23 volúmenes huérfanos de PVC antiguos, que costaban 420 dólares al mes.
Juntos, estos datos dicen que los nodos estaban dimensionados para los requests, no para la carga. Eso fijó el orden: reducir los requests, dejar que menos nodos los soporten y, después, abaratar cada nodo.
Fase 1: right-sizing de los requests antes que nada (semanas 1-2)
Ejecutamos el Vertical Pod Autoscaler en modo recomendación en todos los namespaces durante 14 días. VPA necesita un objeto por carga; este bucle crea uno para cada Deployment de un namespace:
NS=orders
for d in $(kubectl -n "$NS" get deployments -o jsonpath='{.items[*].metadata.name}'); do
kubectl apply -f - <<EOF
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: $d
namespace: $NS
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: $d
updatePolicy:
updateMode: "Off"
EOF
done
kubectl -n "$NS" get vpaFija updateMode: "Off" de forma explícita: el valor por defecto de la API, Recreate, desaloja pods para aplicar los nuevos valores.
Después, los requests y los limits se fijaron a partir del P95 de uso de cada contenedor más un margen del 30 %. El target por defecto de VPA es el percentil 90 más un 15 %; los flags del recommender --target-cpu-percentile=0.95, --target-memory-percentile=0.95 y --recommendation-margin-fraction=0.3 hacen que aplique la regla del P95 más el 30 %. Las consultas de Prometheus de nuestra guía de optimización de costes hacen lo mismo a partir de las métricas en bruto.
Ejemplo práctico. Una API en Java pedía 2 CPU y 4Gi por réplica, pero usaba unos 400m y 800Mi. Tenía 6 réplicas. Tomando esas cifras de uso como el P95:
- Request de CPU: 400m × 1,3 = 520m, frente a 2.000m.
- Request de memoria: 800Mi × 1,3 = 1.040Mi, frente a 4.096Mi.
- En las 6 réplicas: 3,12 núcleos en lugar de 12, y unos 6,1Gi en lugar de 24Gi.
Son casi 9 núcleos liberados por un solo servicio. En los 47 servicios, producción pasó de 24 nodos worker a 14. Ahorro: 5.200 dólares al mes.
Fase 2: decidir qué cargas pueden ir a spot (semana 3)
La capacidad spot es capacidad sobrante de un proveedor, vendida con descuento y recuperada cuando el proveedor la necesita. El aviso antes de una recuperación va de dos minutos a 30 segundos o menos, según el proveedor y su configuración. Diseña para el aviso más corto que dé el tuyo.
Una carga solo es candidata a spot si se cumplen todas estas condiciones:
- Sin estado: sin PersistentVolumeClaim y sin datos locales que deban sobrevivir al pod.
- Dos o más réplicas, repartidas entre zonas: una recuperación se lleva una parte de la capacidad, no toda.
- Se apaga dentro del aviso: deja de aceptar trabajo, termina o devuelve lo que tiene en curso y sale antes de que el nodo desaparezca.
- Trabajo idempotente: un consumidor o un job soporta un mensaje entregado de nuevo o se reanuda desde un checkpoint.
- Arranque rápido: una réplica nueva está lista antes de que las demás se queden sin margen.
Esto lista las réplicas, los volume claims y el grace period de cada carga:
kubectl get deployments,statefulsets -A -o json | jq -r '
.items[] | [
.kind,
.metadata.namespace + "/" + .metadata.name,
"replicas=\(.spec.replicas)",
"pvcs=\(([.spec.template.spec.volumes[]? | select(.persistentVolumeClaim)] | length) + ((.spec.volumeClaimTemplates // []) | length))",
"grace=\(.spec.template.spec.terminationGracePeriodSeconds // 30)s"
] | @tsv'Una sola réplica, un volume claim o un grace period más largo que el aviso dejan la carga en on-demand. Aquí, el reparto fue:
- En spot: las API sin estado (12 servicios, 34 pods), los frontends web y los workers en segundo plano y consumidores de colas.
- En on-demand: las bases de datos y los servicios con estado, es decir, PostgreSQL, Redis y el clúster de búsqueda.
Un nodo de base de datos recuperado significa un failover y una réplica reconstruida desde cero. Además, los volúmenes de almacenamiento en bloque son zonales, así que el pod solo puede reiniciarse en la zona de su volumen, que quizá es la que se acaba de quedar sin capacidad spot.
Dos NodePools: spot primero para lo que no tiene estado, on-demand para lo que sí
Karpenter sustituyó al cluster autoscaler y podía elegir entre 15 o más tipos de instancia en 3 zonas de disponibilidad. Dos NodePools de Karpenter v1 que implementan este reparto tienen este aspecto:
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: stateless
spec:
template:
metadata:
labels:
workload-class: stateless
spec:
nodeClassRef:
group: PROVIDER_GROUP
kind: PROVIDER_NODECLASS
name: default
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: node.kubernetes.io/instance-type
operator: Exists
minValues: 15
expireAfter: 720h
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 5m
budgets:
- nodes: "10%"
limits:
cpu: "400"
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: stateful
spec:
template:
metadata:
labels:
workload-class: stateful
spec:
nodeClassRef:
group: PROVIDER_GROUP
kind: PROVIDER_NODECLASS
name: default
taints:
- key: workload-class
value: stateful
effect: NoSchedule
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
expireAfter: Never
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 30m
budgets:
- nodes: "1"
limits:
cpu: "96"Qué hace cada parte:
nodeClassRefapunta a una NodeClass, que es específica de tu nube. Usa el group y el kind que instala la versión de Karpenter de tu proveedor.- Los dos tipos de capacidad en el pool sin estado. Karpenter lanza spot y solo recurre a on-demand cuando no hay ninguna oferta spot disponible. Un pool solo de spot dejaría los pods en Pending.
minValues: 15mantiene al menos 15 tipos de instancia en cada decisión de lanzamiento, de modo que el pool nunca depende de un solo tipo en una sola zona. El campo es alpha y, con la políticaStrictpor defecto, el pool no lanza nada si los demás requisitos dejan menos de 15 tipos. Acota tamaños o familias con las etiquetas de instancia de tu proveedor yGteoLte, no con una lista de nombres de tipos.- Consolidación y spot. Por defecto, Karpenter solo elimina nodos spot infrautilizados. Sustituir uno por un nodo spot más barato requiere la feature gate
SpotToSpotConsolidation(alpha, desactivada por defecto), y una sustitución de un solo nodo exige esa misma flexibilidad de 15 tipos. - El pool con estado tiene un taint para que nada más aterrice en él. Solo consolida nodos vacíos y sus nodos nunca caducan por tiempo. Se sustituyen cuando algo cambia, como una nueva imagen de nodo, de uno en uno.
limitslimitan lo que cada pool puede lanzar. La fase 3 usa ese límite como interruptor.
Programar los pods sin estado para que perder un nodo no sea noticia
Los NodePools deciden qué se puede lanzar; la spec del pod decide dónde cae cada réplica y cómo se va. Un Deployment sin estado para spot, con su PodDisruptionBudget:
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-api
namespace: orders
spec:
replicas: 6
selector:
matchLabels:
app: orders-api
template:
metadata:
labels:
app: orders-api
spec:
terminationGracePeriodSeconds: 45
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: orders-api
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: orders-api
containers:
- name: api
image: registry.example.com/orders-api:1.4.2
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5
failureThreshold: 2
lifecycle:
preStop:
sleep:
seconds: 10
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
memory: 1Gi
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: orders-api
namespace: orders
spec:
maxUnavailable: 1
selector:
matchLabels:
app: orders-apiCómo encajan las piezas:
- Afinidad preferente por spot. El scheduler favorece los nodos spot y Karpenter intenta primero esa preferencia, pero el servicio sigue funcionando cuando se acaba el spot.
- Reparto por zonas con
DoNotSchedule. Seis réplicas quedan dos por zona, así que una recuperación en una zona se lleva como mucho un tercio del servicio. El reparto por hostname evita, cuando es posible, que las réplicas compartan nodo. - Reparto entre tipos de capacidad. Karpenter también acepta
karpenter.sh/capacity-typecomo clave de topología. ConmaxSkew: 1, cerca de la mitad de las réplicas corre en on-demand, lo que resiste una recuperación masiva pero devuelve la mitad del descuento. Resérvalo para servicios en los que perder todas las réplicas spot a la vez rompería el SLO. - El sleep del preStop. El endpoint de un pod que termina se marca como no listo de inmediato, pero los proxies y los balanceadores tardan unos segundos en dejar de enviarle tráfico. El sleep mantiene el proceso atendiendo durante esa ventana; el SIGTERM llega después. La acción
sleepla ejecuta el kubelet, así que la imagen no necesita shell (activada por defecto desde Kubernetes 1.30 y estable desde la 1.34). - El grace period cubre el hook preStop y el apagado posterior. Ejemplo práctico: un sleep de 10 segundos más hasta 20 segundos para terminar las peticiones en curso caben en 45 segundos. Eso cabe en un aviso de dos minutos aunque el drenado empiece tarde, pero no en uno de 30 segundos. La readiness probe funciona al revés: un pod nuevo solo recibe tráfico cuando responde.
- El PodDisruptionBudget no puede impedir una recuperación; el nodo desaparece al llegar el plazo, pase lo que pase. Rige las interrupciones voluntarias, que son frecuentes en un pool spot: consolidación, caducidad y drift.
Los pods con estado reciben lo contrario, un selector obligatorio y una toleration, en la plantilla de pod de cada StatefulSet:
nodeSelector:
karpenter.sh/capacity-type: on-demand
workload-class: stateful
tolerations:
- key: workload-class
operator: Equal
value: stateful
effect: NoSchedulePara la combinación de instancias que usaba esta empresa, los precios spot quedaron de media un 68 % por debajo de on-demand. Ahorro: 7.400 dólares al mes.
¿Planeas mover cargas a spot? Solicita una evaluación gratuita de costes de Kubernetes: clasificamos tus cargas según estos criterios, revisamos tus NodePools y las specs de tus pods y te entregamos una lista de lo que puede pasar a spot y de lo que necesita antes.
Gestión de interrupciones y qué vigilar después
La gestión de interrupciones de Karpenter está desactivada hasta que los avisos de tu proveedor le llegan. En el proveedor donde nació Karpenter, eso significa una cola de mensajes alimentada por las reglas de eventos de la nube, cuyo nombre se indica en la configuración de Karpenter; consulta en la versión de Karpenter de tu proveedor cómo los recibe. Ante un aviso, Karpenter aplica un taint al nodo, lo drena a través de la Eviction API y lanza un sustituto en paralelo. Sin esto, Karpenter solo se entera de la recuperación cuando la instancia ya no existe: los pods mueren sin drenado y los sustitutos arrancan tarde.
Después del cambio, vigila la rotación de nodos por tipo de capacidad. Las métricas de Karpenter llevan una etiqueta capacity_type:
sum by (nodepool, capacity_type, reason) (increase(karpenter_nodeclaims_disrupted_total[1d]))
sum by (nodepool, capacity_type) (increase(karpenter_pods_disruption_initiated_total[1d]))
sum by (nodepool, capacity_type, zone) (increase(karpenter_nodeclaims_terminated_total[1d]))La primera muestra por qué se fueron los nodos (interrupción, consolidación, caducidad, drift; los nombres de los motivos de interrupción varían según el proveedor). La segunda cuenta los pods movidos, y la tercera qué zonas pierden más. Un pool spot que pierde muchos nodos al día en una zona necesita requisitos de instancia más amplios.
Desde la línea de comandos, la antigüedad de los nodos y los desalojos recientes:
kubectl get nodes -L karpenter.sh/capacity-type,topology.kubernetes.io/zone --sort-by=.metadata.creationTimestamp
kubectl get events -A --field-selector reason=EvictedLos eventos se guardan una hora por defecto, así que conserva el histórico en las métricas, junto a la latencia y las tasas de error de cada servicio en tu instalación de Prometheus. El objetivo: que la rotación se mueva mientras las gráficas del SLO siguen planas.
Fase 3: poner la no producción en horario (semana 3)
Desarrollo bajaba a cero nodos fuera del horario de 08:00 a 20:00 CET, de lunes a viernes. Staging mantenía un mínimo de 2 nodos fuera del horario laboral, con capacidad completa durante las ejecuciones de CI/CD. Se hizo con el escalado de nodos de Karpenter más CronJobs que aplican cordon y drain.
Drenar no basta: Karpenter lanza nodos nuevos para los pods desalojados. Por eso, un job que implementa esto pone primero a cero el límite de CPU del pool, lo que bloquea los lanzamientos pero nunca elimina nodos. Después drena los nodos y borra sus NodeClaims, y Karpenter termina las instancias. El número de réplicas no se toca; los pods esperan en Pending hasta que se levanta el límite.
apiVersion: v1
kind: ServiceAccount
metadata:
name: night-shift
namespace: ops
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: night-shift
rules:
- apiGroups: ["karpenter.sh"]
resources: ["nodepools"]
verbs: ["get", "patch"]
- apiGroups: ["karpenter.sh"]
resources: ["nodeclaims"]
verbs: ["list", "delete"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "list", "patch"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["pods/eviction"]
verbs: ["create"]
- apiGroups: ["apps"]
resources: ["daemonsets"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: night-shift
subjects:
- kind: ServiceAccount
name: night-shift
namespace: ops
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: night-shift
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: dev-nodes-off
namespace: ops
spec:
schedule: "0 20 * * 1-5"
timeZone: Europe/Paris
concurrencyPolicy: Forbid
jobTemplate:
spec:
backoffLimit: 1
activeDeadlineSeconds: 1800
template:
spec:
serviceAccountName: night-shift
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 65534
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: karpenter.sh/nodepool
operator: DoesNotExist
initContainers:
- name: freeze
image: registry.k8s.io/kubectl:v1.37.2
args: ["patch", "nodepool", "dev", "--type=merge", "-p", '{"spec":{"limits":{"cpu":"0"}}}']
- name: drain
image: registry.k8s.io/kubectl:v1.37.2
args: ["drain", "--selector=karpenter.sh/nodepool=dev", "--ignore-daemonsets", "--delete-emptydir-data", "--timeout=15m"]
containers:
- name: remove
image: registry.k8s.io/kubectl:v1.37.2
args: ["delete", "nodeclaims", "--selector=karpenter.sh/nodepool=dev", "--wait=false"]Detalles que importan:
- Los init containers se ejecutan en orden y se detienen en el primer fallo, así que no se drena nada si la congelación no funcionó. (La imagen de kubectl no tiene shell).
- El job corre fuera del pool que vacía: la afinidad
DoesNotExistlo mantiene en nodos que Karpenter no gestiona. drainrechaza los pods sin controlador a menos que añadas--force, que los borra para siempre. Un drain fallido deja los nodos acordonados: quítales el cordon o borra sus NodeClaims a mano.- Por la mañana: un segundo CronJob a las
0 8 * * 1-5ejecuta solo el patch, con tu límite diurno, por ejemplo{"spec":{"limits":{"cpu":"64"}}}. Karpenter lanza entonces nodos para los pods en espera. - Staging usa el mismo job con la CPU de dos nodos en lugar de cero: 16 para dos nodos de 8 vCPU. Karpenter recupera como máximo dos nodos. Un pipeline de CI que necesite el entorno completo sube el límite durante la ejecución y lo restaura después; la consolidación elimina los nodos sobrantes cuando se vacían.
El cómputo de no producción bajó un 65 %. Ahorro: 3.800 dólares al mes. Nuestra guía de optimización de costes incluye un CronJob que escala cargas en lugar de nodos; los dos se combinan bien.
Fase 4: el almacenamiento que nadie usaba (semana 4)
Cuatro cambios:
- Eliminamos los 23 volúmenes huérfanos: 420 dólares al mes.
- Pasamos el almacenamiento de logs de volúmenes de almacenamiento en bloque a almacenamiento de objetos con reglas de ciclo de vida: 280 dólares al mes.
- Pasamos las bases de datos de desarrollo de volúmenes con IOPS aprovisionadas a volúmenes SSD de uso general: 190 dólares al mes.
- Nos aseguramos de que los PVC se limpian cuando se borra un namespace.
Los volúmenes huérfanos son de dos tipos. El primero es visible para Kubernetes: un PersistentVolume en la fase Released, cuyo claim ya no existe pero cuyo disco, con una reclaim policy Retain, se sigue facturando. Esto los lista con el ID del disco:
kubectl get pv -o json | jq -r '.items[] | select(.status.phase == "Released") |
[.metadata.name, .spec.capacity.storage, .spec.persistentVolumeReclaimPolicy,
"\(.spec.claimRef.namespace)/\(.spec.claimRef.name)", (.spec.csi.volumeHandle // "-")] | @tsv'Borrar un PersistentVolume en estado Released no borra su disco. Haz un snapshot si alguien puede necesitar los datos y, después, elimina también el disco a través de tu proveedor.
El segundo tipo es invisible para Kubernetes: discos cuyo objeto PersistentVolume se borró mientras el disco seguía existiendo. Compara los volume handles que conoce el clúster con la lista de discos sin adjuntar de tu proveedor, un ID por línea:
kubectl get pv -o jsonpath='{range .items[*]}{.spec.csi.volumeHandle}{"\n"}{end}' | sort -u > in-cluster.txt
sort -u provider-unattached.txt > provider-sorted.txt
comm -13 in-cluster.txt provider-sorted.txtCada ID que aparece es un disco que ningún PersistentVolume reclama. El formato de los handles varía según el driver CSI, así que comprueba que coincide con la exportación de tu proveedor.
La limpieza al borrar un namespace requiere dos ajustes. Borrar un namespace borra sus PVC, pero los discos solo se eliminan con la reclaim policy Delete, la que viene por defecto en la StorageClass, así que cuestiona cualquier clase Retain en no producción. Además, los StatefulSets conservan sus claims por defecto; persistentVolumeClaimRetentionPolicy con whenDeleted: Delete, estable desde Kubernetes 1.32, los elimina junto con el StatefulSet. Ahorro: 890 dólares al mes.
Fase 5: comprometerse al final (semana 5)
Un compromiso de uso abarata todo lo que ejecutas, desperdicio incluido. Comprado antes del right-sizing, fija unos requests inflados; comprado antes de spot, te compromete con capacidad on-demand que estás a punto de abandonar. Por eso llegó tras cuatro semanas de uso optimizado, cuando la base permanente ya era visible: las cargas con estado de producción y la capacidad mínima de staging. Un compromiso de cómputo a un año cubrió el 70 % de la base on-demand restante. Ahorro: 1.100 dólares al mes.
Para encontrar esa base en tus clústeres, toma el mínimo de vCPU on-demand de cuatro semanas. Un compromiso paga instancias completas, así que mide la capacidad de los nodos, no la allocatable:
min_over_time(
sum(
kube_node_status_capacity{resource="cpu", unit="core"}
* on (node) group_left ()
kube_node_labels{label_karpenter_sh_capacity_type="on-demand"}
)[28d:1h]
)kube_node_labels solo lleva el tipo de capacidad si kube-state-metrics se ejecuta con --metric-labels-allowlist=nodes=[karpenter.sh/capacity-type]. Convierte el resultado en gasto por hora con tus precios on-demand antes de comparar ofertas. Cubrir menos que todo el mínimo deja margen para que baje a medida que más servicios pasan por el right-sizing o se mueven a spot.
El resultado y lo que no hicimos
Las cinco fases, por ahorro mensual:
- Right-sizing: 5.200 dólares.
- Capacidad spot para cargas sin estado: 7.400 dólares.
- Horarios para no producción: 3.800 dólares.
- Limpieza de almacenamiento: 890 dólares.
- Compromiso de cómputo a un año: 1.100 dólares.
El total es de 18.390 dólares al mes. La factura pasó de 38.000 a 19.610 dólares, una reducción del 48 %. La aplicación atendió el mismo tráfico con la misma latencia.
Lo que no hicimos:
- Cambiar código de la aplicación.
- Reducir el número de réplicas de ningún servicio de producción.
- Comprometer la disponibilidad ni la recuperación ante desastres.
- Introducir un nuevo punto único de fallo.
- Provocar ninguna caída durante el trabajo.
Para ver adónde va tu gasto antes de empezar, nuestra guía de asignación de costes de Kubernetes por equipo convierte las mismas métricas en un coste mensual por equipo, para que cada equipo vea lo que cuestan sus requests.
Evaluación gratuita de costes de Kubernetes
Si tu factura crece más deprisa que tu tráfico, nuestro equipo de consultoría Kubernetes hará este análisis en tus clústeres sin coste. La evaluación cubre:
- Requests frente a uso: por contenedor, el request actual, su P95 de 14 días y el request que fijaríamos.
- Preparación para spot: qué cargas cumplen los criterios anteriores, qué bloquea al resto y un diseño de NodePools y de scheduling para tu proveedor.
- Capacidad ociosa y restos: horas de no producción, volúmenes Released y sin adjuntar, y el mínimo on-demand que podría cubrir un compromiso.
Recibes una lista escrita de cambios ordenados por ahorro y por riesgo, sin ningún compromiso.
Solicita tu evaluación gratuita de costes de Kubernetes o descubre cómo la consultoría SRE y el platform engineering evitan que el ahorro se vuelva a escapar.