ArgoCD: Guía Completa de GitOps para Kubernetes

ArgoCD: Guía Completa de GitOps para Kubernetes

ArgoCD se ha convertido en el estándar de facto para la entrega continua basada en GitOps en Kubernetes. Si ejecutas cargas de trabajo en producción sobre Kubernetes y sigues desplegando con kubectl apply directo o releases de Helm sin trazabilidad, ArgoCD resuelve una categoría de problemas que quizás ni sabes que tienes. Esta guía cubre desde los conceptos fundamentales hasta la configuración lista para producción.

El problema que resuelve ArgoCD

El CI/CD tradicional empuja los despliegues hacia el clúster. El sistema de integración continua ejecuta los tests, construye la imagen y llama a kubectl apply o helm upgrade contra el clúster. Este modelo tiene varios problemas estructurales:

  • El drift pasa desapercibido. Alguien aplica un hotfix directamente en el clúster. El repositorio Git ya no refleja la realidad, y nadie lo sabe.
  • No existe una única fuente de verdad. El estado del clúster es el autoritativo, no Git. El estado deseado y el estado real pueden divergir en silencio.
  • El rollback es doloroso. Revertir un despliegue fallido implica re-ejecutar pipelines de CI antiguos o deshacer cambios manualmente, ninguna de las dos opciones es rápida.
  • La gestión multi-clúster multiplica el problema. Cada clúster se convierte en un copo de nieve con su propio historial de cambios no documentados.

GitOps invierte este modelo. Git es la fuente de verdad. El clúster extrae su estado deseado de Git y reconcilia continuamente hacia él. ArgoCD es el operador GitOps más maduro para Kubernetes, implementando este modelo pull con un conjunto de funcionalidades listo para producción.

Cómo funciona ArgoCD: arquitectura principal

ArgoCD se ejecuta como un conjunto de controladores dentro de tu clúster Kubernetes. Los componentes principales son:

  • Application Controller — Observa tanto el repositorio Git como el estado real del clúster. Calcula el diff y dirige la reconciliación.
  • API Server — Expone la API gRPC/REST que consumen la CLI, la UI y los sistemas externos.
  • Repository Server — Genera los manifiestos de Kubernetes a partir de la fuente (Helm, Kustomize, YAML plano, Jsonnet).
  • Redis — Cachea el estado del clúster y los datos del repositorio para reducir la carga del API server.
  • Dex (opcional) — Proporciona autenticación OIDC para la integración con SSO.

La unidad fundamental en ArgoCD es una Application — un CRD que mapea una fuente (una ruta en un repositorio Git en una revisión concreta) a un destino (un namespace en un clúster). ArgoCD compara continuamente el estado deseado de Git con el estado real del clúster y reporta el estado de sincronización.

Sync Status frente a Health Status

Dos conceptos ortogonales que debes entender desde el primer día:

  • Sync Status — ¿Coincide el estado real con lo que Git dice que debería ser? Valores: Synced, OutOfSync, Unknown.
  • Health Status — ¿Está funcionando la aplicación realmente? Valores: Healthy, Progressing, Degraded, Suspended, Missing, Unknown.

Una aplicación puede estar Synced pero Degraded: los manifiestos se aplicaron correctamente, pero un pod está en crash-loop. Por el contrario, puede estar OutOfSync pero Healthy: alguien aplicó un cambio directamente en el clúster al margen de Git.

Este es el punto clave de ArgoCD: te da visibilidad total sobre ambas dimensiones de cada aplicación, algo que ninguna herramienta de CI/CD clásica puede ofrecerte.

Instalación de ArgoCD

El método de instalación oficial utiliza un único manifiesto. En producción, fija siempre una versión específica:

kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/v2.11.0/manifests/install.yaml

Esto despliega ArgoCD en el namespace argocd con acceso cluster-admin completo. Para una instalación HA en producción, utiliza la variante manifests/ha/install.yaml, que despliega múltiples réplicas del API server y del application controller.

Acceder a la UI y la CLI

La contraseña inicial del administrador se genera automáticamente y se almacena en un secret:

argocd admin initial-password -n argocd

Para acceso local, haz port-forward al API server:

kubectl port-forward svc/argocd-server -n argocd 8080:443

Luego inicia sesión mediante la CLI:

argocd login localhost:8080 --username admin --password <contraseña> --insecure

En producción, expón el servidor de ArgoCD mediante un Ingress o un LoadBalancer con un certificado TLS válido. Si usas NGINX Ingress Controller:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: argocd-server-ingress
  namespace: argocd
  annotations:
    nginx.ingress.kubernetes.io/ssl-passthrough: "true"
    nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
spec:
  ingressClassName: nginx
  rules:
  - host: argocd.tudominio.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: argocd-server
            port:
              number: 443

La anotación ssl-passthrough es importante: el API server de ArgoCD termina TLS él mismo, por lo que el Ingress debe pasar el tráfico sin descifrarlo.

Definir tu primera Application

Las Applications se pueden crear mediante la UI, la CLI o declarativamente con un manifiesto YAML. El enfoque declarativo es el recomendado: significa que la propia configuración de ArgoCD está en Git, cerrando el círculo del paradigma GitOps:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: mi-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/tu-org/tu-app
    targetRevision: HEAD
    path: k8s/overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
    - CreateNamespace=true

Campos clave que debes entender:

  • targetRevision — Puede ser el nombre de una rama, un tag o el SHA de un commit. En producción, fija siempre a un tag en lugar de HEAD.
  • path — El directorio dentro del repositorio que contiene tus manifiestos de Kubernetes.
  • automated.prune — Elimina automáticamente los recursos que ya no están en Git. Necesario para un GitOps real, pero úsalo con cuidado: borrará recursos.
  • automated.selfHeal — Revierte automáticamente los cambios manuales aplicados directamente en el clúster. Esto es lo que hace que Git sea la fuente de verdad y no solo una referencia.

La combinación de prune: true y selfHeal: true es lo que diferencia un despliegue GitOps real de simplemente usar ArgoCD como un mecanismo de despliegue sofisticado. Sin ellos, el drift sigue siendo posible.

Integración con Helm

ArgoCD tiene soporte nativo para Helm. Puede desplegar charts de Helm directamente desde repositorios de charts o desde tu repositorio Git. Puedes sobrescribir valores por entorno:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: prometheus-stack
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://prometheus-community.github.io/helm-charts
    chart: kube-prometheus-stack
    targetRevision: 58.4.0
    helm:
      releaseName: prometheus-stack
      valuesObject:
        grafana:
          adminPassword: "${GRAFANA_PASSWORD}"
        prometheus:
          prometheusSpec:
            retention: 30d
            storageSpec:
              volumeClaimTemplate:
                spec:
                  storageClassName: fast-ssd
                  resources:
                    requests:
                      storage: 50Gi
  destination:
    server: https://kubernetes.default.svc
    namespace: observability

Un matiz importante: ArgoCD renderiza los charts de Helm en el lado del servidor usando su propio motor de plantillas, no helm install. Esto significa que los hooks de Helm (pre-install, post-upgrade, etc.) sí están soportados, pero el release no se registra en el historial de Helm. Ejecutar helm list no mostrará los releases gestionados por ArgoCD a menos que configures ArgoCD para usar el backend de secrets de Helm.

Esta es una fuente frecuente de confusión para equipos que migran de un flujo de trabajo basado exclusivamente en Helm: ArgoCD y Helm no son competidores, pero sí hay matices en cómo interactúan.

Projects: multi-tenancy y control de acceso

Los AppProjects de ArgoCD proporcionan multi-tenancy dentro de una única instancia de ArgoCD. Permiten restringir qué repositorios de fuentes, clústeres de destino y namespaces puede usar cada equipo. Toda Application pertenece a un Project.

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: equipo-plataforma
  namespace: argocd
spec:
  description: Aplicaciones del equipo de plataforma
  sourceRepos:
  - 'https://github.com/tu-org/*'
  destinations:
  - namespace: 'plataforma-*'
    server: https://kubernetes.default.svc
  clusterResourceWhitelist:
  - group: ''
    kind: Namespace
  namespaceResourceBlacklist:
  - group: ''
    kind: ResourceQuota

Los Projects son donde defines los límites de lo que cada equipo puede hacer. El proyecto default no tiene restricciones: nunca lo uses para cargas de trabajo en producción. Crea proyectos dedicados por equipo o por entorno.

El modelo habitual en entornos empresariales es un proyecto por equipo de producto, con restricciones de namespace y de repositorio de fuente que impidan que un equipo accidentalmente despliegue en el entorno de otro.

Configuración RBAC

ArgoCD tiene su propio sistema RBAC superpuesto sobre el RBAC de Kubernetes. Se configura mediante el ConfigMap argocd-rbac-cm. Los roles se definen por proyecto o de forma global:

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-rbac-cm
  namespace: argocd
data:
  policy.default: role:readonly
  policy.csv: |
    # El equipo de plataforma tiene acceso completo a su proyecto
    p, role:platform-admin, applications, *, equipo-plataforma/*, allow
    p, role:platform-admin, projects, get, equipo-plataforma, allow
    p, role:platform-admin, repositories, *, *, allow

    # El equipo de desarrollo puede sincronizar pero no borrar
    p, role:developer, applications, get, */*, allow
    p, role:developer, applications, sync, */*, allow
    p, role:developer, applications, action/*, */*, allow

    # Vincular grupos SSO a roles
    g, tu-org:equipo-plataforma, role:platform-admin
    g, tu-org:developers, role:developer

El policy.default: role:readonly garantiza que cualquier usuario autenticado sin asignación de rol explícita obtenga acceso de solo lectura: un valor por defecto seguro para producción.

La integración con SSO (vía Dex u otro proveedor OIDC) permite que los grupos de tu directorio corporativo (Active Directory, Okta, Google Workspace) se mapeen directamente a roles en ArgoCD, sin necesidad de gestionar usuarios locales.

Gestión multi-clúster

ArgoCD puede gestionar múltiples clústeres Kubernetes desde un único plano de control. Registra clústeres externos con la CLI:

# Primero, asegúrate de que el contexto del clúster destino está en tu kubeconfig
argocd cluster add produccion-eu-west --name produccion-eu-west

# Verifica el registro
argocd cluster list

ArgoCD creará un ServiceAccount en el clúster destino y almacenará sus credenciales como un secret de Kubernetes en el namespace de ArgoCD. Las Applications pueden entonces apuntar a este clúster por nombre en el campo destination.server.

Para setups multi-clúster a gran escala, considera el patrón App of Apps o los ApplicationSets. Los ApplicationSets son un controlador que genera Applications dinámicamente basándose en generadores: listas de clústeres, estructuras de directorios Git o combinaciones matriciales:

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: cluster-addons
  namespace: argocd
spec:
  generators:
  - clusters:
      selector:
        matchLabels:
          environment: production
  template:
    metadata:
      name: '{{name}}-addons'
    spec:
      project: plataforma
      source:
        repoURL: https://github.com/tu-org/cluster-addons
        targetRevision: HEAD
        path: 'addons/{{metadata.labels.region}}'
      destination:
        server: '{{server}}'
        namespace: kube-system

Este único ApplicationSet despliega los addons apropiados en cada clúster etiquetado como environment: production, usando la etiqueta de región de cada clúster para seleccionar la ruta correcta en el repositorio.

La potencia de este enfoque es significativa: añadir un nuevo clúster de producción solo requiere etiquetarlo correctamente; ArgoCD se encarga automáticamente de desplegar todos los addons necesarios.

Estrategias de sync y waves

Al desplegar aplicaciones complejas con dependencias entre recursos, necesitas controlar el orden de despliegue. ArgoCD ofrece dos mecanismos para esto:

Fases de sync

Los recursos se despliegan en tres fases: PreSync, Sync y PostSync. Usa Sync Hooks para recursos que deben completarse antes de que avance el sync principal (migraciones de base de datos, emisión de certificados, etc.):

apiVersion: batch/v1
kind: Job
metadata:
  name: db-migration
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
  template:
    spec:
      containers:
      - name: migrate
        image: tu-app:v1.2.3
        command: ["./migrate.sh"]
      restartPolicy: Never

La anotación HookSucceeded asegura que el Job se limpie una vez completado con éxito, evitando acumulación de recursos en el clúster.

Sync waves

Dentro de la fase Sync, las waves controlan el orden. Los recursos con un número de wave menor se aplican y deben estar sanos antes de que se apliquen los de waves superiores:

# Se aplica primero
metadata:
  annotations:
    argocd.argoproj.io/sync-wave: "1"

# Se aplica después de que la wave 1 esté sana
metadata:
  annotations:
    argocd.argoproj.io/sync-wave: "2"

El caso de uso más habitual es: wave 0 para CRDs, wave 1 para el Namespace y ConfigMaps base, wave 2 para el Deployment, wave 3 para el Ingress. Este orden garantiza que los recursos dependientes no intentan crearse antes de que sus prerequisitos existan.

Notificaciones y alertas

ArgoCD Notifications es un controlador independiente que envía alertas cuando cambia el estado de una Application. Soporta Slack, PagerDuty, estado de commits de GitHub, email y más de una docena de proveedores adicionales. Se configura mediante el ConfigMap argocd-notifications-cm:

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-notifications-cm
  namespace: argocd
data:
  service.slack: |
    token: $slack-token
  template.app-sync-failed: |
    slack:
      attachments: |
        [{
          "title": "{{.app.metadata.name}}",
          "color": "#E96D76",
          "fields": [{
            "title": "Sync Status",
            "value": "{{.app.status.sync.status}}",
            "short": true
          },{
            "title": "Mensaje",
            "value": "{{range .app.status.conditions}}{{.message}}{{end}}",
            "short": false
          }]
        }]
  trigger.on-sync-failed: |
    - when: app.status.sync.status == 'Unknown'
      send: [app-sync-failed]
    - when: app.status.operationState.phase in ['Error', 'Failed']
      send: [app-sync-failed]

En la práctica, configura al menos notificaciones para sync-failed y health-degraded. El resto de eventos (sync iniciado, sync exitoso) generan demasiado ruido para canales de alertas de producción.

Gestión de secrets con ArgoCD

ArgoCD no incluye gestión de secrets de forma intencionada: almacenar secrets en Git como texto plano nunca es aceptable. Los patrones habituales son:

  • Sealed Secrets (Bitnami) — Cifra los secrets con una clave específica del clúster. El secret cifrado se puede commitear en Git; solo el clúster puede descifrarlo.
  • External Secrets Operator — Sincroniza secrets desde Vault, AWS Secrets Manager, GCP Secret Manager, etc. a secrets de Kubernetes. La Application en ArgoCD gestiona el CRD ExternalSecret, no el valor del secret en sí.
  • argocd-vault-plugin — Un plugin que reemplaza valores de marcador de posición en los manifiestos con secrets recuperados de Vault en el momento del sync.

El enfoque de External Secrets Operator es el más flexible para equipos que ya usan un backend centralizado de secrets. La Application en ArgoCD despliega objetos ExternalSecret, que el controlador ESO resuelve en tiempo de ejecución sin que los valores lleguen nunca a Git.

Para equipos que empiezan desde cero en España, Sealed Secrets tiene una curva de aprendizaje más baja y es perfectamente válido para la mayoría de los casos de uso. La complejidad adicional de External Secrets Operator se justifica cuando ya tienes Vault o un servicio cloud de gestión de secrets.

Buenas prácticas para producción

  • Ejecuta ArgoCD en modo HA. Usa manifests/ha/install.yaml con 3 réplicas del API server y múltiples shards del application controller para clústeres grandes (100+ aplicaciones).
  • Fija las versiones de imagen. Nunca uses latest para la propia imagen de ArgoCD. Fija una versión específica y actualiza deliberadamente.
  • Usa el patrón App of Apps para el bootstrapping. Una única Application raíz despliega todas las demás Applications. Esto hace que el bootstrapping del clúster sea idempotente y reproducible.
  • Separa la configuración de ArgoCD de la configuración de aplicación. Almacena los manifiestos de Application de ArgoCD en un repositorio gitops dedicado, separado del código fuente de las aplicaciones.
  • Activa el resource tracking mediante anotaciones. Usa application.resourceTrackingMethod: annotation en argocd-cm en lugar del tracking basado en labels por defecto, que puede entrar en conflicto con los propios labels de Helm.
  • Establece límites de recursos en los controladores de ArgoCD. La CPU y la memoria del application controller escalan con el número de recursos rastreados. Monitoriza y ajusta según sea necesario.
  • Restringe el auto-sync en producción. Considera requerir aprobación manual de sync para entornos de producción incluso cuando usas GitOps, o al menos exige un gate de aprobación de PR antes de que los cambios lleguen a la rama objetivo.

El patrón App of Apps merece un desarrollo adicional: la idea es tener un repositorio gitops-config que contenga una Application raíz. Esa Application raíz apunta a un directorio que contiene los manifiestos de todas las demás Applications. Para añadir una nueva aplicación al clúster, añades su manifiesto Application al repositorio gitops-config. ArgoCD lo detecta y la despliega automáticamente.

ArgoCD frente a Flux

Flux v2 es el otro operador GitOps principal. Ambos son proyectos graduados de CNCF. Las diferencias principales en la práctica:

CaracterísticaArgoCDFlux v2
UIUI web integradaSin UI oficial (usa Weave GitOps)
Multi-clústerUn plano de control gestiona muchos clústeresAgente por clúster, modelo pull
ApplicationSetsNativoKustomization + HelmRelease
Gestión de secretsBasada en pluginsIntegración nativa con SOPS
Curva de aprendizajeMayor (más conceptos)Menor (más Kubernetes-nativo)
Estado CNCFGraduadoGraduado

ArgoCD gana cuando necesitas la UI, gestión multi-clúster desde un plano central, o tienes un equipo de operaciones grande que se beneficia de la vista visual de topología de aplicaciones. Flux gana cuando quieres un enfoque más simple y puramente Kubernetes-nativo con mejor integración de SOPS para la gestión de secrets.

En el contexto del mercado español y europeo, ArgoCD domina en organizaciones con equipos de plataforma dedicados. Flux es más frecuente en equipos pequeños que valoran la simplicidad operativa.

Primeros pasos: de cero a ArgoCD en producción

El camino más rápido desde cero hasta un setup funcional de ArgoCD en un clúster local:

# 1. Crea un clúster local (kind o minikube)
kind create cluster --name argocd-demo

# 2. Instala ArgoCD
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# 3. Espera a que los pods estén listos
kubectl wait --for=condition=Ready pods --all -n argocd --timeout=120s

# 4. Obtén la contraseña inicial del administrador
argocd admin initial-password -n argocd

# 5. Port-forward e inicia sesión
kubectl port-forward svc/argocd-server -n argocd 8080:443 &
argocd login localhost:8080 --username admin --insecure

# 6. Despliega tu primera aplicación
argocd app create guestbook \
  --repo https://github.com/argoproj/argocd-example-apps.git \
  --path guestbook \
  --dest-server https://kubernetes.default.svc \
  --dest-namespace guestbook \
  --sync-policy automated

Desde aquí, los próximos pasos naturales son integrar ArgoCD con tu pipeline de CI existente (el CI construye y publica la imagen, actualiza el tag de imagen en Git, ArgoCD detecta el cambio y sincroniza), configurar SSO mediante Dex y establecer el patrón App of Apps para gestionar múltiples aplicaciones de forma declarativa.

Preguntas frecuentes

¿Puede ArgoCD desplegar en el clúster donde se ejecuta?

Sí. El destino https://kubernetes.default.svc hace referencia al clúster local. ArgoCD puede gestionar simultáneamente su propio clúster y clústeres externos.

¿Soporta ArgoCD repositorios Git privados?

Sí. Configura las credenciales del repositorio mediante argocd repo add con claves SSH, usuario/contraseña HTTPS o credenciales de GitHub App. Las credenciales se almacenan como secrets de Kubernetes en el namespace de ArgoCD.

¿Cómo gestiona ArgoCD la instalación de CRDs?

Los CRDs pueden ser gestionados por ArgoCD, pero hay un problema de dependencia circular: si un CRD no está instalado aún, ArgoCD no puede validar recursos que lo usen. El patrón recomendado es colocar los CRDs en wave 1 y los recursos dependientes en wave 2, o usar una Application separada para los CRDs.

¿Cuál es la diferencia entre una Application y un AppProject?

Una Application es la unidad de despliegue: mapea una fuente Git a un destino en el clúster. Un AppProject es un agrupador y límite de control de acceso: restringe qué fuentes y destinos puede usar una Application dentro del proyecto. Toda Application pertenece exactamente a un AppProject.

¿Cómo hago un rollback de un despliegue con ArgoCD?

La forma GitOps: revertir el commit en Git y dejar que ArgoCD reconcilie. ArgoCD también proporciona un rollback basado en UI a cualquier revisión de sync anterior, pero esto se considera una medida temporal: el historial de Git siempre debe actualizarse para reflejar el estado deseado.

¿Cuántas aplicaciones puede gestionar una instancia de ArgoCD?

Una instancia en modo HA puede gestionar cómodamente miles de Applications. El cuello de botella suele ser el application controller; configurar múltiples shards (mediante ARGOCD_CONTROLLER_REPLICAS) permite escalar horizontalmente para clústeres con cientos de Applications.

Conclusión

ArgoCD no es simplemente otra herramienta de despliegue: es un cambio de paradigma en cómo se gestiona la infraestructura y las aplicaciones en Kubernetes. El modelo pull basado en Git elimina la clase entera de problemas de drift, rollback doloroso y falta de trazabilidad que plagan los enfoques push tradicionales.

La curva de aprendizaje inicial — Projects, Applications, ApplicationSets, sync waves, hooks — puede parecer intimidante. Pero cada concepto resuelve un problema real que encontrarás en producción. Empieza con una Application simple, activa auto-sync con selfHeal, y observa cómo ArgoCD empieza a rechazar el drift. Desde ese momento, la visibilidad y el control que obtienes son difíciles de abandonar.

Para equipos que quieren profundizar más en GitOps y ArgoCD en producción, la guía de patrones de arquitectura de Kubernetes cubre cómo ArgoCD encaja en un stack de platform engineering más amplio junto con service mesh, aplicación de políticas y herramientas de observabilidad.

Seguridad en Kubernetes: Guía de Hardening para Producción

Seguridad en Kubernetes: Guía de Hardening para Producción

Kubernetes no tiene un botón de “activar seguridad”. Es una disciplina en capas que abarca el plano de control, las cargas de trabajo, la red, la cadena de suministro y el runtime. Esta guía cubre los controles que más importan en producción, por qué existe cada uno y cómo implementarlos sin romper el clúster.

Para los equipos que operan en España o la Unión Europea, el hardening de Kubernetes no es solo una buena práctica técnica — es un requisito derivado del RGPD (Reglamento General de Protección de Datos) y, para entidades del sector público o proveedores de servicios esenciales, del ENS (Esquema Nacional de Seguridad). Las medidas descritas aquí son directamente aplicables a las categorías de control técnico que exigen ambos marcos.


La superficie de ataque en Kubernetes

Antes de aplicar ningún hardening, es imprescindible entender qué se está protegiendo. Un clúster de Kubernetes expone varias superficies de ataque diferenciadas:

  • API server — El plano de control centralizado. Cualquier entidad que pueda alcanzarlo con credenciales válidas puede leer el estado del clúster, modificar cargas de trabajo o escalar privilegios.
  • etcd — Almacena todo el estado del clúster en texto plano, incluidos los Secrets. El acceso directo a etcd equivale a tener root en todos los nodos.
  • Nodos — Un nodo comprometido permite acceder a todos los Secrets montados en los pods que ejecuta, al API del kubelet, y potencialmente escapar al hipervisor subyacente.
  • Pods — Los pods privilegiados, los que usan red del host o los que tienen capabilities excesivas pueden romper el aislamiento de contenedor.
  • Cadena de suministro — Imágenes maliciosas, registros comprometidos y artefactos sin firmar pueden introducir código controlado por un atacante directamente en el clúster.
  • RBAC — Los roles excesivamente permisivos permiten movimiento lateral y escalada de privilegios en cuanto un atacante consigue cualquier punto de apoyo inicial.

Los controles que se describen a continuación abordan cada una de estas superficies. Prioriza según tu modelo de amenazas: un clúster multi-tenant expuesto a internet los necesita todos; un clúster de desarrollo interno puede relajar algunos.

Una forma útil de pensar en la seguridad de Kubernetes es en términos de las cuatro fases del ciclo de vida del atacante: reconocimiento inicial, escalada de privilegios, movimiento lateral y exfiltración de datos. Cada sección de esta guía interfiere con una o más de esas fases. El hardening del API server y el RBAC dificultan la escalada inicial. Las network policies bloquean el movimiento lateral. El cifrado de Secrets y el control estricto de RBAC previenen la exfiltración. Falco y los audit logs permiten detectar y responder cuando las capas anteriores fallan.


1. RBAC: mínimo privilegio desde el primer día

El Control de Acceso Basado en Roles (RBAC) es el mecanismo principal de autorización de Kubernetes. La mayoría de los clústeres fallan en RBAC no porque esté mal configurado, sino porque es excesivamente permisivo por defecto y nadie lo revisa de forma sistemática.

Errores habituales en RBAC

  • Asignar cluster-admin por comodidad. Prácticamente ninguna carga de trabajo necesita cluster-admin. Usa roles con scope de namespace siempre que sea posible.
  • Usar * en verbos o recursos. Los permisos con wildcard son casi siempre más amplios de lo que se pretende.
  • No auditar el uso de tokens de ServiceAccount. Cada pod tiene una ServiceAccount. La SA por defecto en la mayoría de los namespaces no tiene permisos, pero las cargas de trabajo personalizadas suelen recibir SAs excesivamente permisivas.
  • Olvidar automountServiceAccountToken: false. Si una carga de trabajo no necesita comunicarse con el API de Kubernetes, deshabilita el montaje del token por completo.

Patrones RBAC prácticos

Para una carga de trabajo que solo necesita leer ConfigMaps en su propio namespace:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: configmap-reader
  namespace: my-app
rules:
- apiGroups: [""]
  resources: ["configmaps"]
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: my-app-configmap-reader
  namespace: my-app
subjects:
- kind: ServiceAccount
  name: my-app
  namespace: my-app
roleRef:
  kind: Role
  name: configmap-reader
  apiGroup: rbac.authorization.k8s.io

Audita el RBAC existente con kubectl-who-can o rbac-tool para localizar bindings excesivamente permisivos antes de que lo haga un atacante.

Nota RGPD/ENS: El principio de mínimo privilegio está recogido explícitamente en el artículo 25 del RGPD (privacidad por diseño) y en las medidas de control de acceso del ENS (categorías Media y Alta). Un RBAC correctamente dimensionado es evidencia directa de cumplimiento ante una auditoría.


2. Pod Security Standards: endurecimiento de cargas de trabajo

PodSecurityPolicy fue deprecada en Kubernetes 1.21 y eliminada en 1.25. Su reemplazo es Pod Security Admission (PSA), un admission controller integrado que aplica uno de tres perfiles de seguridad a nivel de namespace:

  • Privileged — Sin restricciones. Solo para componentes del sistema.
  • Baseline — Previene las escaladas de privilegios más críticas: contenedores privilegiados, hostPID, hostIPC, hostNetwork, capabilities peligrosas.
  • Restricted — Aplica las mejores prácticas actuales de hardening. Requiere ejecutar como non-root, eliminar todas las capabilities y usar un perfil seccomp restringido.

Activa la aplicación a nivel de namespace con labels:

apiVersion: v1
kind: Namespace
metadata:
  name: produccion
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: v1.30
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: v1.30
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: v1.30

Un pod que ejecute como root o solicite host-network en un namespace con perfil restricted será rechazado en el momento de admisión. Los modos warn y audit permiten validar antes de aplicar la restricción.

PSA cubre las escaladas a nivel de pod más críticas, pero es de grano grueso. Para control de políticas más fino, combina Kyverno con PSA.

Una estrategia de adopción recomendada para clústeres existentes es empezar aplicando el nivel Baseline en modo warn en todos los namespaces de producción. Durante uno o dos sprints, el equipo revisa las advertencias y corrige las cargas de trabajo problemáticas — típicamente las que ejecutan como root por inercia histórica o las que montan el socket de Docker para tareas de CI. Una vez que las advertencias desaparecen, se pasa al modo enforce. Solo entonces se evalúa si algún namespace puede subir a Restricted. Este proceso gradual evita las interrupciones de producción que ocurren cuando se aplica Restricted directamente en un clúster heredado.


3. Network Policies: micro-segmentación del tráfico

Por defecto, cualquier pod de un clúster de Kubernetes puede comunicarse con cualquier otro pod en cualquier namespace. Este modelo de red plana otorga a un atacante movimiento lateral sin restricciones en cuanto compromete cualquier carga de trabajo.

Las Network Policies definen reglas de tipo allow en la capa L3/L4 para la comunicación pod-a-pod. Las aplica el plugin CNI (Calico, Cilium, Weave — Flannel no soporta NetworkPolicy).

Patrón de denegación por defecto

Empieza denegando todo el ingress y egress en cada namespace, y luego abre solo lo que sea explícitamente necesario:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: produccion
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress

Después permite el tráfico específico:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-to-db
  namespace: produccion
spec:
  podSelector:
    matchLabels:
      app: postgres
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: api
    ports:
    - protocol: TCP
      port: 5432

No olvides el egress de DNS — la mayoría de cargas de trabajo necesitan resolver nombres mediante kube-dns, lo que requiere egress UDP al puerto 53 hacia el namespace kube-system.

Si usas Cilium como CNI, puedes ir más allá de las Network Policies estándar de Kubernetes y aplicar políticas a nivel L7: restringir qué paths HTTP específicos puede llamar un pod, limitar la comunicación a determinados métodos gRPC, o filtrar por cabeceras HTTP. Cilium Network Policies son especialmente útiles en arquitecturas de microservicios donde diferentes servicios del mismo namespace deberían tener superficies de comunicación distintas.

La micro-segmentación de red es una de las medidas técnicas más efectivas para cumplir con el principio de “necesidad de saber” del RGPD y con los controles de segmentación de red del ENS. En una evaluación de cumplimiento, poder demostrar que la base de datos de producción solo es accesible desde los pods de la aplicación — y no desde cualquier pod del clúster — es evidencia tangible de control técnico implementado.


4. Gestión de Secrets

Los Secrets de Kubernetes están codificados en base64, no cifrados. Se almacenan en etcd en texto plano por defecto. Cualquier usuario con permiso get sobre Secrets puede leerlos. No es una vulnerabilidad — es una decisión de diseño que delega la responsabilidad en el operador:

  • Activa el cifrado en reposo para etcd. Configura EncryptionConfiguration con un proveedor AES-CBC o AES-GCM. Esto cifra los Secrets antes de que se escriban en etcd.
  • Usa almacenes externos de secretos. HashiCorp Vault, AWS Secrets Manager o Azure Key Vault con el External Secrets Operator hace que los valores reales de los secretos nunca residan en Kubernetes.
  • Restringe el RBAC de Secrets de forma agresiva. Nunca otorgues list sobre Secrets a nivel de clúster — devuelve todos los valores. Usa get sobre recursos con nombre donde sea posible.
  • Evita variables de entorno para secretos. Prefiere los volume mounts. Las variables de entorno son visibles en la salida de kubectl describe pod y pueden filtrarse a través del logging de la aplicación.
# Cifrado en reposo de etcd - en la configuración de kube-apiserver
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
  - secrets
  providers:
  - aescbc:
      keys:
      - name: key1
        secret: <clave-de-32-bytes-codificada-en-base64>
  - identity: {}

Contexto RGPD: El cifrado de datos en reposo es una medida técnica explícitamente recomendada por el artículo 32 del RGPD para proteger datos personales. Si los Secrets de tu clúster contienen credenciales de acceso a bases de datos con datos personales, el cifrado en reposo de etcd no es opcional desde la perspectiva del cumplimiento.

Un patrón cada vez más extendido en organizaciones que operan bajo el ENS o en sectores regulados (financiero, sanitario, administración pública) es adoptar el External Secrets Operator junto con HashiCorp Vault o Azure Key Vault. En este modelo, los Kubernetes Secrets actúan únicamente como proyección efímera de secretos que residen y se rotan en un sistema de gestión de secretos dedicado. El beneficio operativo es notable: rotación centralizada de credenciales sin redeployment de pods, auditoría completa de acceso a secretos, y una superficie de ataque mucho menor en caso de compromiso del clúster.


5. Seguridad de imágenes y cadena de suministro

La postura de seguridad en runtime es tan buena como las imágenes que ejecutas. Una imagen comprometida procedente de un registro público evade todos los controles de runtime que hayas implementado.

Escaneo de imágenes en CI/CD

Usa Trivy, Grype o Snyk para escanear imágenes como parte de tu pipeline de CI. Bloquea despliegues de imágenes con CVEs críticos:

# En el pipeline de CI
trivy image --exit-code 1 --severity CRITICAL tu-imagen:tag

Registro privado con control de admisión

Permite solo imágenes de tu registro privado usando un admission webhook (Kyverno, OPA Gatekeeper). Esto evita que los desarrolladores ejecuten imágenes públicas arbitrarias en producción:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: restrict-image-registries
spec:
  validationFailureAction: Enforce
  rules:
  - name: validate-registries
    match:
      any:
      - resources:
          kinds: ["Pod"]
    validate:
      message: "Las imágenes deben proceder de registry.empresa.com"
      pattern:
        spec:
          containers:
          - image: "registry.empresa.com/*"

Imágenes distroless o de base mínima

Las imágenes distroless contienen solo la aplicación y sus dependencias de runtime — sin shell, sin gestor de paquetes, sin herramientas de diagnóstico. Esto reduce drásticamente la superficie de ataque y el número de CVEs. Las imágenes distroless de Google están disponibles para Java, Node.js, Python y Go.

Firma y verificación de imágenes

Cosign (del proyecto Sigstore) permite firmar imágenes de contenedor y verificar firmas en el momento de admisión mediante Kyverno o Connaisseur. Esto previene los ataques de sustitución de imágenes, en los que un atacante reemplaza una imagen legítima en el registro.

En entornos regulados europeos (banca, sanidad, administración pública), la firma de imágenes proporciona trazabilidad de la cadena de suministro de software, un requisito cada vez más presente en las auditorías ENS de categoría Alta y en los marcos de ciberseguridad del sector financiero (DORA).

SBOM: inventario de componentes de software

El Software Bill of Materials (SBOM) se está convirtiendo en un estándar de facto en la seguridad de cadenas de suministro. Herramientas como Syft permiten generar un SBOM en formato SPDX o CycloneDX a partir de cualquier imagen de contenedor. Combinado con Grype para el escaneo de vulnerabilidades contra el SBOM, obtienes trazabilidad completa de los componentes de tus imágenes sin necesidad de reconstruirlas. La directiva europea NIS2, transpuesta en España a través de la Ley de Coordinación y Gobernanza de la Ciberseguridad, incluirá progresivamente requisitos de gestión de riesgos en la cadena de suministro de software que hacen del SBOM una práctica recomendable para las organizaciones afectadas.


6. Seguridad en runtime

La seguridad en runtime detecta y responde a actividad maliciosa una vez que un contenedor ya está en ejecución. La herramienta principal en este ámbito es Falco — un proyecto CNCF que usa eBPF para monitorizar llamadas al sistema y generar alertas cuando los contenedores se comportan de forma inesperada.

Las reglas por defecto de Falco detectan patrones de ataque habituales:

  • Shell lanzada dentro de un contenedor
  • Conexión de red a una IP inesperada
  • Escritura en rutas de fichero sensibles (/etc/passwd, /etc/shadow)
  • Escalada de privilegios mediante binarios setuid
  • Drift del contenedor (nuevos ficheros ejecutables escritos en runtime)

Combina Falco con perfiles seccomp para restringir las llamadas al sistema que puede realizar un contenedor a nivel del kernel. El perfil seccomp RuntimeDefault (disponible como perfil por defecto desde Kubernetes 1.27) bloquea más de 300 llamadas al sistema que los contenedores prácticamente nunca necesitan.

spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: app
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      runAsNonRoot: true
      runAsUser: 65534
      capabilities:
        drop: ["ALL"]

Estas cuatro configuraciones de securityContext juntas (allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, runAsNonRoot: true, capabilities.drop: ALL) hacen que el escape de contenedor sea significativamente más difícil y satisfacen el estándar de seguridad de pods Restricted de Kubernetes.

Desde la perspectiva del ENS, el despliegue de herramientas de detección en tiempo de ejecución corresponde a los controles de monitorización continua (mp.si.4) en la categoría de medidas de protección de sistemas de información.

Un detalle importante sobre Falco: no es suficiente con desplegarlo; hay que gestionar las reglas activamente. Las reglas por defecto son un buen punto de partida, pero en un entorno productivo con muchos microservicios generarán falsos positivos que el equipo ignorará progresivamente — hasta que una alerta real quede enterrada en el ruido. La práctica correcta es ajustar las reglas por namespace o por etiqueta de deployment, silenciando los falsos positivos conocidos de cada servicio y manteniendo alertas nítidas para comportamientos realmente anómalos. Las alertas de Falco se pueden enrutar a sistemas de gestión de eventos de seguridad (SIEM) como OpenSearch Security Analytics, IBM QRadar o Microsoft Sentinel, que son comunes en organizaciones españolas de tamaño medio-grande.


7. Hardening del API Server

El API server es el componente más crítico a endurecer. Configuraciones clave:

  • Deshabilita la autenticación anónima. --anonymous-auth=false garantiza que toda petición esté autenticada.
  • Activa el audit logging. Registra todas las peticiones al API server en un fichero o mediante webhook. Sin audit logs no puedes investigar incidentes ni detectar abuso de RBAC.
  • Restringe los plugins de admisión. Asegúrate de que NodeRestriction está activo — impide que los kubelets de los nodos modifiquen objetos fuera de su propio nodo.
  • No expongas el API server a internet. Usa una VPN, un bastion host o un endpoint privado. Si necesitas exponerlo, restringe el acceso por IP.
# Política de auditoría mínima — registra todas las peticiones a nivel metadata,
# y el body completo para recursos sensibles
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse
  resources:
  - group: ""
    resources: ["secrets", "configmaps"]
- level: Metadata
  omitStages: ["RequestReceived"]

El audit logging del API server es equivalente a los registros de auditoría exigidos por el RGPD (artículo 30, registro de actividades de tratamiento) y por las medidas de trazabilidad del ENS. En una brecha de seguridad, estos logs son la diferencia entre poder demostrar qué ocurrió y no poder hacerlo.

La política de auditoría mínima mostrada arriba es un punto de partida. En producción, necesitarás ajustar los niveles según el volumen de peticiones y el coste de almacenamiento. Un error habitual es activar RequestResponse para todos los recursos — en un clúster activo puede generar cientos de gigabytes de logs al día. Una estrategia más sostenible es RequestResponse solo para recursos sensibles (secrets, configmaps, rolebindings) y Metadata para el resto. Los logs de auditoría deben exportarse a un sistema externo (Elasticsearch, S3, un SIEM) que el propio clúster no pueda modificar, para que sean útiles como evidencia forense.


8. Seguridad de etcd

etcd almacena todo el estado del clúster. Trátalo con la misma sensibilidad que tu base de datos de producción:

  • Activa TLS para toda la comunicación de etcd. Tanto la comunicación entre pares (etcd-a-etcd) como la comunicación con clientes (apiserver-a-etcd) deben usar mutual TLS.
  • Restringe el acceso de red a etcd. etcd solo debe ser alcanzable por el API server. Usa reglas de firewall o security groups para aplicarlo.
  • Activa el cifrado en reposo. Como se describe en la sección de Secrets.
  • Realiza backups de etcd de forma regular. Un snapshot de etcd es una copia completa de todo el estado del clúster, incluidos todos los Secrets. Cifra los backups y almacénalos de forma separada del clúster.

En entornos con certificación ENS, el backup y la restauración verificada de etcd forman parte de los controles de continuidad de operaciones. Los backups sin cifrar de etcd en un bucket de almacenamiento objeto son un vector de ataque frecuentemente ignorado.


9. CIS Kubernetes Benchmark

El CIS Kubernetes Benchmark es una lista de comprobación exhaustiva de controles de seguridad que abarca el plano de control, los nodos y las cargas de trabajo. Ejecutar kube-bench contra tu clúster te da una evaluación puntuada frente a los controles CIS:

kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml
kubectl logs $(kubectl get pods -l app=kube-bench -o name)

kube-bench genera PASS/FAIL/WARN para cada control con guías de remediación. Ejecútalo tras la configuración inicial del clúster y después de cambios de configuración significativos.

El CIS Kubernetes Benchmark tiene un mapeo documentado con los controles del ENS y con los requisitos técnicos de PCI-DSS. Si tu organización opera en el sector financiero español bajo supervisión del Banco de España o la CNMV, este benchmark es un punto de partida reconocido para demostrar diligencia técnica.


10. Postura de seguridad continua con Kubescape y Trivy Operator

Kubescape y herramientas similares (Trivy Operator, KubeScore) proporcionan escaneo continuo del estado real del clúster — no solo una auditoría puntual. Comprueban las cargas de trabajo frente a las guías de hardening NSA/CISA, el framework MITRE ATT&CK y el CIS Benchmark en tiempo real.

Despliega Trivy Operator para escaneo continuo en el clúster:

helm repo add aquasecurity https://aquasecurity.github.io/helm-charts/
helm install trivy-operator aquasecurity/trivy-operator \
  --namespace trivy-system \
  --create-namespace \
  --set="trivy.ignoreUnfixed=true"

Trivy Operator crea recursos personalizados VulnerabilityReport, ConfigAuditReport y RbacAssessmentReport en el mismo namespace que cada carga de trabajo. Estos pueden ser scrapeados por Prometheus y visualizados en Grafana para disponer de un dashboard de seguridad.

Esta capacidad de monitorización y reporte continuo resulta especialmente valiosa en entornos con obligaciones de revisión periódica: cumplimiento ENS, auditorías de seguridad ISO 27001, o revisiones de seguridad técnica previas a una certificación del CCN-CERT.


Lista de comprobación de hardening

  • RBAC revisado — sin roles con wildcard, sin bindings innecesarios de cluster-admin
  • Token automount de ServiceAccount deshabilitado para cargas de trabajo sin acceso al API
  • Pod Security Standards aplicados a nivel de namespace (mínimo Baseline, Restricted donde sea posible)
  • Network policies desplegadas — denegación por defecto con permisos explícitos
  • Secrets cifrados en reposo en etcd
  • Imágenes escaneadas en CI — sin CVEs críticos en producción
  • Registro privado aplicado mediante control de admisión
  • securityContext de contenedores endurecido (non-root, filesystem de solo lectura, sin capabilities)
  • Perfil seccomp RuntimeDefault activado
  • Audit logging del API server activado
  • TLS de etcd y acceso de red restringido
  • kube-bench ejecutado y hallazgos críticos/altos remediados
  • Seguridad en runtime (Falco) desplegada y alertas enrutadas al equipo de guardia
  • Escaneo continuo (Trivy Operator o Kubescape) desplegado

Preguntas frecuentes

¿Por dónde empiezo si mi clúster no tiene controles de seguridad hoy?

Empieza por los controles de mayor impacto y menor esfuerzo: audita tu RBAC (revoca cluster-admin donde no sea necesario), activa Pod Security Admission en modo warn en todos los namespaces, y despliega Trivy Operator. Estos tres pasos te dan visibilidad inmediata y previenen las escaladas de privilegio más comunes sin romper nada.

¿Activar Network Policies rompe la resolución DNS?

Sí, si despliegas una política de egress default-deny sin permitir explícitamente DNS. Añade una regla de egress que permita UDP puerto 53 al servicio kube-dns en kube-system cuando apliques network policies de denegación por defecto.

¿Está Kubernetes certificado para PCI-DSS, ENS o ISO 27001?

Kubernetes en sí no está certificado — tu configuración y los controles que implementas determinan el cumplimiento. El CIS Kubernetes Benchmark se mapea con muchos requisitos de PCI-DSS, ENS e ISO 27001. Las ofertas de Kubernetes gestionado (EKS, GKE, AKS) tienen sus propias certificaciones de cumplimiento para la infraestructura subyacente, pero la configuración del clúster y las cargas de trabajo siguen siendo responsabilidad del equipo.

En el contexto español, el CCN-CERT publica guías técnicas (series CCN-STIC) aplicables a la securización de plataformas de contenedores que complementan el ENS. Para entidades del sector público con sistemas de categoría Media o Alta, conviene consultar estas guías junto con el CIS Benchmark.

¿OPA Gatekeeper o Kyverno?

Ambas aplican políticas de admisión, pero Kyverno es nativo de Kubernetes (las políticas se escriben como YAML) mientras que Gatekeeper usa Rego (un lenguaje de políticas específico de dominio). Para equipos sin experiencia en Rego, Kyverno es significativamente más rápido de adoptar y mantener. Para equipos que ya usan OPA en otras partes de su stack, Gatekeeper ofrece consistencia. Ambos se integran bien con flujos de trabajo GitOps.

¿Con qué frecuencia debo actualizar Kubernetes para los parches de seguridad?

Aplica una release de parche en un plazo de 30 días desde su publicación para CVEs calificados como High o Critical. Las actualizaciones de versión minor (por ejemplo, 1.29 → 1.30) deben realizarse dentro de la ventana de soporte — Kubernetes mantiene las tres últimas versiones minor. Quedarse más de una versión minor por detrás significa ejecutar sin parches de seguridad para un subconjunto creciente de la base de código.


Kubernetes seguro en contexto DevSecOps

La seguridad de Kubernetes no es responsabilidad exclusiva del equipo de plataforma o de seguridad. En una organización que ha adoptado DevSecOps correctamente, los controles descritos en esta guía se integran en el ciclo de desarrollo normal:

  • Los desarrolladores reciben feedback de seguridad en el pull request, antes de que el código llegue a producción. Herramientas como Trivy en CI, Checkov para Helm charts y Kubelinter para manifiestos YAML hacen posible esta detección temprana.
  • Las políticas de Kyverno o Gatekeeper se gestionan como código (GitOps), con la misma revisión de código y proceso de aprobación que cualquier cambio de infraestructura.
  • Los resultados de Trivy Operator y Kubescape se exponen en dashboards de Grafana accesibles para todo el equipo de ingeniería, no solo para el equipo de seguridad. La visibilidad compartida genera responsabilidad compartida.
  • Las alertas de Falco se integran en el sistema de guardia (PagerDuty, OpsGenie) junto con las alertas de disponibilidad. La seguridad en runtime no puede tener un equipo de respuesta separado con tiempos de reacción más lentos que los incidentes de producción.

Este modelo — seguridad integrada en el flujo de trabajo, no añadida como capa posterior — es la diferencia entre un postura de seguridad que mejora continuamente y una que se deteriora con cada sprint.


Contenido relacionado

Para una visión más profunda de cómo la seguridad encaja en la arquitectura global de la plataforma Kubernetes, consulta la guía de patrones de arquitectura Kubernetes y la guía sobre construir una cultura de seguridad en Kubernetes.

Escalabilidad en Prometheus: Alta Cardinalidad y Cómo Solucionarla (2026)

Escalabilidad en Prometheus: Alta Cardinalidad y Cómo Solucionarla (2026)

Escalabilidad en Prometheus: Alta Cardinalidad y Cómo Solucionarla

Tu instancia de Prometheus consume 20 GB de RAM. Las queries en Grafana tienen timeout. Las alertas llegan tarde —o directamente no llegan— justo cuando más las necesitas. Y el culpable, casi siempre, es el mismo: la cardinalidad.

La cardinalidad es el problema de escalabilidad más subestimado de Prometheus. No es un bug, ni una mala configuración del cluster, ni un exporter mal escrito. Es una consecuencia directa de cómo Prometheus modela los datos: cada combinación única de etiquetas genera una time series independiente que debe almacenarse, indexarse y consultarse. Añade un label con miles de valores distintos —un user_id, un pod dinámico, una IP— y tu instancia puede pasar de 200.000 series a 20 millones en cuestión de horas.

Este artículo cubre el problema en profundidad: qué es la cardinalidad, por qué degrada Prometheus, cómo detectarlo antes de que explote, y las opciones que tienes desde arreglos tácticos inmediatos hasta rediseños arquitecturales completos con Thanos, Grafana Mimir y VictoriaMetrics.


El modelo de datos de Prometheus y por qué la cardinalidad importa

Para entender el problema hay que entender el modelo. Prometheus almacena datos en time series: secuencias de pares (timestamp, valor) identificadas de forma única por un nombre de métrica y un conjunto de etiquetas (labels). Una métrica como:

http_requests_total{method="GET", status="200", endpoint="/api/v1/users"}

es una time series. Cambias cualquier label, tienes una nueva serie. Este diseño es extremadamente potente para hacer slicing y dicing de datos —es lo que hace que PromQL sea tan expresivo— pero tiene un coste directo: cada serie consume memoria, espacio en disco y ciclos de CPU para indexación.

La cardinalidad es el número total de combinaciones únicas de etiquetas que existen en todas tus métricas. La fórmula es simple: es el producto cartesiano de todos los valores posibles de cada label en una métrica dada.

Ejemplo concreto. Una métrica http_requests_total con:

  • method: 4 valores (GET, POST, PUT, DELETE)
  • status_code: 6 valores (200, 201, 400, 401, 404, 500)
  • endpoint: 50 rutas distintas

Genera 4 × 6 × 50 = 1.200 series activas. Manejable.

Ahora añades un label customer_id con 10.000 clientes activos. El resultado: 1.200 × 10.000 = 12.000.000 series desde una sola métrica. Eso equivale a entre 36 y 48 GB de RAM solo para el head block, la zona de datos recientes que Prometheus mantiene completamente en memoria.

Una instancia de Prometheus con 1 millón de series activas consume típicamente entre 4 y 6 GB de RAM solo para el head block. A 10 millones de series, estás mirando a 40–60 GB, y el rendimiento de compactación empieza a deteriorarse de forma no lineal.

Labels de alta cardinalidad: los habituales

Estos son los labels que hay que vigilar porque su cardinalidad puede crecer sin límite o con límite muy alto:

  • User IDs y session tokens — tantos valores como usuarios activos tengas
  • Request IDs y trace IDs — cardinalidad infinita por definición
  • Pod names en entornos con autoscaling — cada nueva réplica añade nuevas series; cuando el pod muere, las series quedan como «stale» hasta que expiran
  • Mensajes de error en texto libre — cada variación del mensaje genera una serie nueva
  • Direcciones IP en entornos con alta rotación de clientes
  • Versiones de build si cada deployment tiene su propio hash o timestamp

La cardinalidad no es solo un problema de espacio. También es un problema de series churn: la tasa a la que se crean y eliminan series. Un entorno con 500.000 series totales pero 100.000 nuevas series por minuto puede ser más problemático que uno con 2 millones de series estables.


Síntomas: cómo saber que tienes un problema de cardinalidad

El problema no aparece de golpe. Crece gradualmente y se manifiesta de formas que a veces se confunden con otros problemas.

Crecimiento del head block sin plateau

El indicador más directo es prometheus_tsdb_head_series. En un sistema sano, esta métrica sube y baja siguiendo los patrones de tus aplicaciones —más series durante el día laboral, menos por la noche. Cuando tienes un problema de cardinalidad, sube de forma monótona sin estabilizarse. Eventualmente termina en OOM (Out of Memory).

prometheus_tsdb_head_series

Monitoriza también la tasa de creación:

rate(prometheus_tsdb_head_series_created_total[5m])

Si esta tasa es sostenidamente alta —más de 50.000 series/minuto— tienes un problema estructural, independientemente del total actual.

Query timeouts y dashboards lentos

Las queries de PromQL que antes tardaban 200 ms ahora tardan 10 segundos. Los dashboards de Grafana empiezan a mostrar «query timed out». La evaluación de alertas se retrasa y los AIOps alertan con minutos de lag.

Este síntoma es especialmente peligroso porque ocurre precisamente cuando más lo necesitas: durante un incidente, con alta carga, cuando las métricas son más importantes.

Scrape failures y gaps en los datos

La presión de memoria activa el garbage collector de Go con más frecuencia. Los ciclos largos de GC hacen que Prometheus no pueda completar scrapes dentro del timeout. El resultado: targets marcados como down, huecos en las series temporales, y en el peor caso, alertas de «instancia caída» cuando la instancia está funcionando pero con degradación de rendimiento.

Compactación lenta

El TSDB de Prometheus compacta bloques periódicamente para reducir fragmentación y mejorar el rendimiento de queries. Con millones de series, esta compactación puede tomar 30 segundos o incluso varios minutos. Durante ese tiempo, el rendimiento de escritura se degrada y las queries sobre datos históricos son más lentas.

Monitoriza:

prometheus_tsdb_head_chunks_storage_size_bytes
rate(prometheus_tsdb_compactions_total[1h])

Diagnóstico: encontrar qué genera la cardinalidad

Antes de actuar, necesitas saber qué está causando el problema. Prometheus tiene herramientas integradas para esto.

El endpoint /api/v1/status/tsdb

curl http://localhost:9090/api/v1/status/tsdb | jq '.data.headStats'

Devuelve estadísticas del head block: número de series, chunks, samples. También incluye los 10 label names y los 10 nombres de métricas con mayor número de series. Es el punto de partida para cualquier investigación de cardinalidad.

# Top 10 métricas por número de series
curl -s http://localhost:9090/api/v1/status/tsdb | \
  jq '.data.seriesCountByMetricName[:10]'

# Top 10 label names por número de pares de valores
curl -s http://localhost:9090/api/v1/status/tsdb | \
  jq '.data.labelValueCountByLabelName[:10]'

PromQL para diagnóstico

Cardinalidad total estimada de una métrica específica:

count(http_requests_total)

Distribución de series por label:

count by (job) (http_requests_total)

Identificar labels con alta cardinalidad en una métrica:

count(count by (customer_id) (http_requests_total))

Si este último número es muy alto —decenas de miles o más— ese label es el culpable.


Soluciones tácticas: arreglos inmediatos

Estas soluciones pueden implementarse sin cambios arquitecturales y dan resultados rápidos.

1. Recording rules: pre-computar y agregar

Las recording rules son la herramienta más potente para reducir cardinalidad de forma estructural. Consiste en pre-calcular aggregations costosas y almacenar el resultado como una nueva métrica con menos labels.

La convención de nombres es level:metric:operations:

groups:
  - name: http_request_aggregations
    interval: 30s
    rules:
      - record: job:http_requests_total:rate5m
        expr: |
          sum by (job, namespace, method, status_code) (
            rate(http_requests_total[5m])
          )

      - record: job:http_request_duration_seconds:p99_5m
        expr: |
          histogram_quantile(0.99,
            sum by (job, namespace, le) (
              rate(http_request_duration_seconds_bucket[5m])
            )
          )

En el primer ejemplo, eliminamos el label pod de la aggregation. Si tienes 500 pods, esto reduce la cardinalidad de esa métrica agregada en un factor de 500. Las queries que antes escaneaban millones de series ahora escanean cientos.

Principios clave para las recording rules:

  • Úsalas para métricas que consultas frecuentemente en dashboards y alertas
  • Agrega al nivel más grueso que satisfaga tu caso de uso
  • El resultado debe ser una métrica útil por sí misma, no solo un artefacto de optimización
  • Los dashboards de Grafana deben usar las recorded metrics, no las series originales

2. metric_relabel_configs: filtrar en el scrape

Esta configuración actúa después de que Prometheus hace el scrape pero antes de almacenar los datos. Es el lugar correcto para:

  • Eliminar métricas que no usas
  • Normalizar valores de alta cardinalidad
  • Renombrar o reetiquetas
scrape_configs:
  - job_name: application-pods
    kubernetes_sd_configs:
      - role: pod
    metric_relabel_configs:
      # Eliminar métricas de runtime de Go que no usamos
      - source_labels: [__name__]
        regex: 'go_gc_.*|go_memstats_.*|process_.*'
        action: drop

      # Normalizar endpoints con IDs dinámicos
      - source_labels: [endpoint]
        regex: '/api/v1/users/[0-9]+'
        target_label: endpoint
        replacement: '/api/v1/users/:id'

      # Eliminar label de alta cardinalidad antes de almacenar
      - regex: 'customer_id|session_id|request_id'
        action: labeldrop

La normalización de endpoints es especialmente importante. Una ruta como /api/v1/users/12345 y /api/v1/users/67890 generan series distintas; normalizarlas a /api/v1/users/:id colapsa todas esas series en una sola.

Diferencia entre relabeling y metric_relabel_configs:

  • relabel_configs: actúa durante el descubrimiento de targets, antes del scrape. Afecta a qué targets se scraping.
  • metric_relabel_configs: actúa después del scrape, sobre las métricas recibidas. Es donde filtras series.

3. Límites de cardinalidad por scrape

Prometheus permite imponer límites per-job para evitar que un exporter mal configurado pueda saturar la instancia:

scrape_configs:
  - job_name: application-pods
    sample_limit: 50000       # máximo de samples por scrape
    label_limit: 64           # máximo de labels por sample
    label_name_length_limit: 128
    label_value_length_limit: 1024

Cuando un scrape supera sample_limit, Prometheus rechaza todo el scrape y marca el target como up=0 con la razón en prometheus_target_scrape_pool_exceeded_target_limit_total. Esto protege la instancia global a costa de perder datos de ese target temporalmente.

Configura alertas sobre estas métricas para detectar exporters que están creciendo hacia el límite antes de que lo superen:

# Targets que superaron el límite en las últimas 24h
increase(prometheus_target_scrapes_exceeded_sample_limit_total[24h]) > 0

4. Reducir la retención local

Si el problema es de espacio en disco más que de memoria, ajusta la retención:

# En los argumentos de arranque de Prometheus
--storage.tsdb.retention.time=15d
--storage.tsdb.retention.size=50GB

Esto no reduce la cardinalidad activa (head block), pero evita que el disco se llene con datos históricos si no los necesitas localmente.


Soluciones arquitecturales: cuando el tuning no es suficiente

Las soluciones tácticas tienen límites. Cuando tu instancia supera el millón de series o necesitas alta disponibilidad real, necesitas cambios arquitecturales.

Federation: la opción más simple

La federación jerárquica consiste en que una instancia «global» de Prometheus hace scrape del endpoint /federate de otras instancias «leaf». Las instancias leaf hacen el scrape real de las aplicaciones y pre-agregan sus métricas; la instancia global consume solo los agregados.

# En la instancia global
scrape_configs:
  - job_name: federate
    honor_labels: true
    metrics_path: /federate
    params:
      match[]:
        - '{job="kubernetes-pods"}'
        - 'job:http_requests_total:rate5m'   # Solo recorded metrics
    static_configs:
      - targets:
          - 'prometheus-region-eu:9090'
          - 'prometheus-region-us:9090'

Cuándo usar federation:
– Tienes múltiples instancias de Prometheus por región o cluster y quieres un dashboard global
– Solo necesitas métricas pre-agregadas en la vista global
– No necesitas hacer range queries sobre datos históricos federados

Limitaciones serias:
– No puedes hacer range queries contra datos federados (solo el snapshot actual)
– La instancia global es un single point of failure para dashboards globales
– No escala bien más allá de 4–5 instancias leaf
– No es una solución para alta disponibilidad real

Remote Write: hacer Prometheus stateless

Con remote_write, Prometheus envía todas las samples a un storage externo en tiempo real. La instancia local puede mantener una retención mínima (2–6 horas) solo para evaluar alertas y reglas.

remote_write:
  - url: https://thanos-receive.monitoring.svc:19291/api/v1/receive
    queue_config:
      capacity: 10000
      max_shards: 200
      min_shards: 1
      max_samples_per_send: 500
      batch_send_deadline: 5s
      min_backoff: 30ms
      max_backoff: 5s
    write_relabel_configs:
      # Opcional: filtrar antes de enviar
      - source_labels: [__name__]
        regex: 'debug_.*'
        action: drop

Tuning del queue_config:

El parámetro más importante es max_shards. Cada shard es una goroutine que envía datos al endpoint remoto. Más shards = mayor throughput pero también mayor presión de memoria. En sistemas de alta ingestión, valores entre 50 y 200 son habituales.

Monitoriza el lag de remote write:

# Lag en segundos entre lo que Prometheus tiene y lo que ha enviado
prometheus_remote_storage_highest_timestamp_in_seconds
  - ignoring(remote_name, url) prometheus_remote_storage_queue_highest_sent_timestamp_seconds

Un lag sostenido de más de 60 segundos indica que el endpoint remoto no puede absorber la tasa de ingestión o que hay un problema de red.


Soluciones a largo plazo: Thanos, Mimir y VictoriaMetrics

Estas tres opciones son las más adoptadas en producción para escalar más allá de lo que una instancia de Prometheus puede manejar.

CriterioThanosGrafana MimirVictoriaMetrics
ArquitecturaSidecar + object storeMicroservicios distribuidosBinario único o cluster
StorageS3-compatibleS3-compatibleFormato TSDB propio
Complejidad operacionalMediaAltaBaja
Escalabilidad de ingestiónVia Receive fan-outDistributors + ingestersMillones de samples/s por nodo
Multi-tenancy nativaLimitadaCompletaSolo Enterprise
Compatibilidad PromQLCompletaCompletaMetricsQL (superset)
Mejor caso de usoMigración incremental desde Prometheus existentePlataformas multi-tenant con gobernanzaSimplicidad y rendimiento
LicenciaApache 2.0AGPLv3Community: Apache 2.0 / Enterprise: propietaria

Thanos: la ruta incremental

Thanos es la opción menos disruptiva si ya tienes Prometheus en producción. No reemplaza Prometheus; lo extiende.

Componentes principales:

Thanos Sidecar corre junto a cada instancia de Prometheus, sube los bloques TSDB completados (cada 2 horas) a object storage (S3, GCS, Azure Blob), y expone los datos recientes via gRPC para queries federadas.

# thanos-sidecar en el mismo pod que Prometheus
containers:
  - name: thanos-sidecar
    image: quay.io/thanos/thanos:v0.35.0
    args:
      - sidecar
      - --tsdb.path=/prometheus
      - --prometheus.url=http://localhost:9090
      - --objstore.config-file=/etc/thanos/s3.yaml
      - --grpc-address=0.0.0.0:10901

Thanos Query federa queries contra múltiples sidecars y el Store Gateway (que lee de object storage):

# thanos-query puede consultar datos actuales (sidecar) e históricos (store)
args:
  - query
  - --grpc-address=0.0.0.0:10901
  - --http-address=0.0.0.0:10902
  - --endpoint=thanos-sidecar-eu:10901
  - --endpoint=thanos-sidecar-us:10901
  - --endpoint=thanos-store:10901
  - --query.replica-label=prometheus_replica   # Para deduplicación

Thanos Receive es el componente para escenarios push. En lugar de que el sidecar suba bloques, las instancias de Prometheus envían datos via remote_write a Thanos Receive, que los almacena localmente y los sube a object storage. Permite alta disponibilidad activa-activa:

# Configuración de Thanos Receive con hashring para distribución
hashring.json:
  - endpoints:
      - thanos-receive-0.thanos-receive:10907
      - thanos-receive-1.thanos-receive:10907
      - thanos-receive-2.thanos-receive:10907
    tenants: []  # Acepta todos los tenants

Cuándo elegir Thanos:
– Ya tienes Prometheus en producción y quieres añadir retención a largo plazo sin grandes cambios
– Necesitas queries multi-cluster con deduplicación
– Tu equipo tiene experiencia con Kubernetes y object storage
– Prefieres añadir componentes incrementalmente

Limitaciones:
– El número de componentes crece rápidamente (Sidecar, Query, Store, Compactor, Ruler, Receive, Query Frontend)
– Thanos Query no soporta todos los features de PromQL con el mismo rendimiento que Prometheus nativo
– La latencia de queries sobre datos históricos depende de la velocidad de lectura de object storage

Grafana Mimir: multi-tenancy enterprise

Mimir es la evolución de Cortex, rediseñada por Grafana Labs. Su propuesta de valor es la multi-tenancy nativa: cada tenant tiene sus métricas aisladas, con límites de cardinalidad por tenant, quotas de ingestión y namespaces separados.

La arquitectura es de microservicios completa:

  • Distributor: recibe los writes de remote_write, los valida y los distribuye a los ingesters via consistent hashing
  • Ingester: almacena datos recientes en memoria y los vuelca periódicamente a object storage
  • Querier: ejecuta queries distribuyéndolas entre ingesters (datos recientes) y Store-Gateway (datos históricos)
  • Compactor: compacta bloques en object storage para optimizar almacenamiento y rendimiento
  • Ruler: evalúa recording rules y alertas
  • Query Frontend: cachea queries y las divide en subqueries paralelas
# mimir.yaml - configuración simplificada
multitenancy_enabled: true

ingester:
  ring:
    replication_factor: 3

limits:
  # Por tenant, sobrescribibles via API
  ingestion_rate: 10000        # samples/segundo
  ingestion_burst_size: 200000
  max_global_series_per_user: 1500000
  max_label_names_per_series: 30
  max_label_value_length: 2048

blocks_storage:
  backend: s3
  s3:
    bucket_name: mimir-blocks
    endpoint: s3.eu-west-1.amazonaws.com

Los límites por tenant son la funcionalidad más relevante para problemas de cardinalidad: puedes aislar un tenant problemático sin que afecte al resto de la plataforma.

Cuándo elegir Mimir:
– Gestionas monitorización como servicio interno para múltiples equipos o clientes
– Necesitas garantías de aislamiento entre tenants
– Tienes capacidad operacional para gestionar una arquitectura de microservicios compleja
– Quieres integración nativa con el stack de Grafana (Grafana Cloud usa Mimir internamente)

Consideraciones:
– La complejidad operacional es la más alta de las tres opciones
– Requiere un orquestador (Kubernetes) para funcionar bien
– La licencia AGPLv3 tiene implicaciones si redistribuyes el software

VictoriaMetrics: rendimiento y simplicidad

VictoriaMetrics adopta una filosofía diferente: resolver los mismos problemas con menos componentes y mejor rendimiento. Su motor de almacenamiento propio ofrece compresión 5–10x mejor que el TSDB de Prometheus y rendimiento de queries superior en benchmarks.

Modo single-node (recomendado hasta ~10 millones de series activas):

./victoria-metrics \
  -storageDataPath=/var/lib/victoria-metrics \
  -retentionPeriod=12 \
  -httpListenAddr=:8428 \
  -maxConcurrentInserts=16 \
  -insert.maxQueueDuration=1m

Es un único binario. No hay sidecars, no hay componentes de object storage, no hay distributed systems que operar. El scrape se configura igual que en Prometheus (soporta el formato de configuración de Prometheus) o puedes enviarle datos via remote_write desde Prometheus.

Modo cluster para escala horizontal:

                   ┌─────────────┐
                   │  vminsert   │  ← recibe remote_write
                   └──────┬──────┘
                          │ replica
              ┌───────────┼───────────┐
              ▼           ▼           ▼
        ┌──────────┐ ┌──────────┐ ┌──────────┐
        │ vmstorage│ │ vmstorage│ │ vmstorage│
        └──────────┘ └──────────┘ └──────────┘
              │           │           │
              └───────────┼───────────┘
                          ▼
                   ┌─────────────┐
                   │  vmselect   │  ← responde queries
                   └─────────────┘

MetricsQL: VictoriaMetrics implementa MetricsQL, un superset de PromQL con funciones adicionales como keep_last_value(), range_quantile() y mejoras en el handling de series con gaps. Es compatible hacia atrás con PromQL estándar.

# MetricsQL: interpolación de valores ausentes
keep_last_value(http_requests_total[5m])

# Percentil sobre un rango temporal (no disponible en PromQL estándar)
range_quantile(0.99, rate(http_request_duration_seconds_bucket[1h]))

vmagent es el agente ligero de VictoriaMetrics que reemplaza a Prometheus para el scrape: consume menos memoria, soporta sharding de scrape nativo para distribuir la carga entre múltiples instancias, y envía datos via remote_write a VictoriaMetrics o cualquier otro endpoint compatible.

Cuándo elegir VictoriaMetrics:
– Prioridad en simplicidad operacional y bajo coste de gestión
– Equipos pequeños o medianos sin dedicación a platform engineering
– Altos volúmenes de ingestión con recursos limitados (el ahorro en RAM y disco es real y significativo)
– Migración desde Prometheus sin querer adoptar Kubernetes adicional para el stack de métricas


Guía de decisión por escala

Menos de 1 millón de series activas

Una instancia única de Prometheus con tuning es suficiente. Prioriza:

  1. Identificar y eliminar labels de alta cardinalidad con metric_relabel_configs
  2. Implementar recording rules para las queries frecuentes de dashboards y alertas
  3. Configurar sample_limit por scrape job
  4. Ajustar retención y --storage.tsdb.max-block-duration si es necesario

Hardware recomendado: 16 GB RAM, SSD para el storage del TSDB. No necesitas arquitectura distribuida.

De 1 a 5 millones de series activas

Aquí empieza a ser necesario sharding funcional o una solución de almacenamiento externo.

Opción A: Sharding funcional
Divide las responsabilidades de scrape entre múltiples instancias de Prometheus, cada una responsable de un subconjunto de targets (por namespace, por cluster, por tipo de workload). Cada instancia mantiene su propio TSDB. Las alertas se evalúan localmente; para dashboards globales, usa federation o Thanos Query.

Opción B: VictoriaMetrics single-node
Un nodo de VictoriaMetrics puede manejar varios millones de series activas con mucha menos RAM que Prometheus. Si el cuello de botella es la memoria de Prometheus, esta es la migración más rápida.

Más de 5 millones de series o requisitos globales

Aquí necesitas una arquitectura distribuida. La elección entre Thanos, Mimir y VictoriaMetrics depende de tus prioridades:

  • Migración incremental con mínima disrupción → Thanos Sidecar
  • Multi-tenancy nativa con aislamiento completo → Grafana Mimir
  • Máximo rendimiento con mínima complejidad operacional → VictoriaMetrics Cluster

Monitorizar la salud de Prometheus

Antes de que el problema escale, necesitas alertas sobre las propias métricas de Prometheus.

Métricas clave a monitorizar

# Total de series activas en el head block
prometheus_tsdb_head_series

# Tasa de creación de nuevas series (churn)
rate(prometheus_tsdb_head_series_created_total[5m])

# Tamaño del head block en bytes
prometheus_tsdb_head_chunks_storage_size_bytes

# Lag de remote write (si aplica)
max_over_time(
  (prometheus_remote_storage_highest_timestamp_in_seconds
   - ignoring(remote_name, url) 
   prometheus_remote_storage_queue_highest_sent_timestamp_seconds)[5m:]
)

# Duración de la evaluación de alertas
prometheus_rule_group_last_duration_seconds

# Targets con scrape fallido
up == 0

Alertas recomendadas

groups:
  - name: prometheus-health
    rules:
      - alert: PrometheusHighCardinality
        expr: prometheus_tsdb_head_series > 2000000
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Prometheus con alta cardinalidad ({{ $value }} series)"
          description: "La instancia {{ $labels.instance }} supera 2M de series activas"

      - alert: PrometheusHighSeriesChurn
        expr: rate(prometheus_tsdb_head_series_created_total[5m]) > 50000
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "Alta tasa de creación de series en Prometheus"

      - alert: PrometheusRemoteWriteLag
        expr: |
          (prometheus_remote_storage_highest_timestamp_in_seconds
           - ignoring(remote_name, url)
           prometheus_remote_storage_queue_highest_sent_timestamp_seconds) > 120
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Remote write con lag superior a 2 minutos"

Preguntas frecuentes

¿Cuántas series puede manejar Prometheus de forma segura?

No hay un límite universal. La regla práctica es calcular aproximadamente 3–4 GB de RAM por millón de series en el head block, más un 50% de margen para queries y otras operaciones. Con 16 GB de RAM, estás cómodo hasta 2–3 millones de series activas. Con 32 GB, hasta 5–6 millones.

El número relevante no es solo el total sino también el churn. Una instancia con 1 millón de series estables es mucho más cómoda que una con 500.000 series pero 80.000 nuevas series por minuto (como ocurre en entornos Kubernetes con muchos deployments frecuentes).

¿Cómo implemento alta disponibilidad de Prometheus sin soluciones externas?

La forma estándar es desplegar dos instancias de Prometheus idénticas (misma configuración, mismos scrape targets) y usar Alertmanager con el flag --cluster.* para deduplicar alertas entre ambas instancias. Para queries, necesitas algo que agregue las dos instancias: Thanos Query con deduplicación, VictoriaMetrics con vmselect, o simplemente apuntar Grafana a las dos instancias como datasources con «prefer one, fallback to other».

Esta configuración no elimina el split-brain para datos históricos —cada instancia tiene su propio TSDB— pero cubre el escenario de «una instancia cae y pierdes alertas».

¿Remote_write o Thanos Sidecar?

Depende del caso de uso:

  • Remote_write es mejor si quieres que Prometheus sea stateless desde el principio, enviando datos a medida que llegan. Menor latencia hasta que los datos están disponibles en el store remoto. Mayor presión en el endpoint receptor.
  • Thanos Sidecar sube bloques TSDB completos (cada 2 horas) a object storage. Prometheus sigue siendo el source of truth para datos recientes. Menos presión de escritura pero 2 horas de ventana donde los datos recientes solo están en la instancia local.

Para la mayoría de los casos prácticos, la combinación de remote_write + Thanos Receive es la más flexible.

¿Afecta el número de scrape targets a la cardinalidad?

Indirectamente. Más targets no significa más cardinalidad si cada target expone series con los mismos conjuntos de labels. La cardinalidad sube cuando los targets exponen series con labels de alta cardinalidad. El caso típico son pods de Kubernetes donde el label pod es único por réplica: 500 pods del mismo deployment generan 500 valores distintos del label pod, multiplicando la cardinalidad de cualquier métrica que lo incluya.

¿VictoriaMetrics es compatible con mis dashboards de Grafana existentes?

Sí, en su mayor parte. VictoriaMetrics soporta el API de Prometheus, por lo que Grafana puede usarlo como datasource directamente en modo «Prometheus». Las queries PromQL estándar funcionan sin cambios. Solo las features de MetricsQL no disponibles en PromQL estándar requieren adaptación, y esas no las tendrás en dashboards existentes.


Conclusión

La escalabilidad de Prometheus no es un problema que se resuelve una vez. Es una disciplina continua: monitorizar la cardinalidad, aplicar límites preventivos, pre-agregar con recording rules, y rediseñar la arquitectura cuando los números lo justifican.

El camino habitual es:

  1. Detectar el problema temprano con alertas sobre prometheus_tsdb_head_series y churn rate
  2. Eliminar cardinalidad innecesaria con metric_relabel_configs y labeldrop
  3. Pre-agregar métricas frecuentes con recording rules
  4. Escalar horizontalmente cuando una instancia ya no sea suficiente, eligiendo entre Thanos (migración incremental), Mimir (multi-tenancy) o VictoriaMetrics (simplicidad y rendimiento)

La mayoría de los problemas graves de cardinalidad en producción son evitables. Unos cuantos labels bien elegidos —y sobre todo, la disciplina de no añadir labels que no se van a filtrar en queries— marcan la diferencia entre una instancia de Prometheus que funciona durante años y una que se cae en el peor momento posible.

Alternativas al NGINX Ingress Controller en Kubernetes: Guía de Migración

Alternativas al NGINX Ingress Controller en Kubernetes: Guía de Migración

Después del NGINX Ingress Controller: Alternativas y Guía de Migración

Si gestionas clusters de Kubernetes en producción y llevas tiempo usando un NGINX Ingress Controller, este artículo es para ti. En los últimos meses, dos eventos han puesto el tema de los ingress controllers en primer plano: una serie de vulnerabilidades críticas bajo el nombre colectivo de IngressNightmare y el anuncio de deprecación estratégica por parte de F5/NGINX de su propio proyecto open source. El resultado es que muchos equipos de plataforma están revisando su posición y evaluando alternativas.

Este artículo no es una traducción de tendencias de Silicon Valley — es una guía técnica y operativa escrita desde la práctica de gestionar infraestructura Kubernetes real. Cubriremos qué ha pasado exactamente, qué opciones existen, cómo compararlas y cómo planificar una migración sin interrupciones en producción.


Dos proyectos, un mismo nombre: el problema de partida

Antes de entrar en alternativas, conviene aclarar algo que genera confusión frecuente: bajo el término «NGINX Ingress Controller» conviven dos proyectos completamente distintos.

ingress-nginx (kubernetes/ingress-nginx)

Mantenido por la comunidad bajo el paraguas del proyecto Kubernetes, ingress-nginx es el controlador que aparece recomendado en la documentación oficial de Kubernetes y el que la mayoría de equipos instala por defecto. Usa NGINX open source como proxy de datos, con configuración dinámica implementada mediante scripts Lua y la directiva nginx.conf generada automáticamente.

Es, con diferencia, el controlador de ingress más extendido en clusters autogestionados, instalaciones on-premise y la mayoría de entornos cloud que no usan soluciones propietarias del proveedor.

NGINX Ingress Controller (nginxinc/kubernetes-ingress)

Este es el proyecto comercial de F5/NGINX. Soporta NGINX Plus, usa las APIs nativas de NGINX en lugar del enfoque Lua-heavy, y está orientado a clientes enterprise que necesitan soporte con contrato. A diferencia de ingress-nginx, no usa anotaciones en el mismo formato y tiene su propia Helm chart con valores distintos.

Estos dos controladores no son intercambiables. Las anotaciones de configuración son diferentes, los valores de los Helm charts son diferentes, y el comportamiento ante casos extremos difiere de forma sustancial. Si alguien del equipo habla de «migrar del NGINX Ingress Controller», lo primero es confirmar de cuál de los dos se habla.


Lo que pasó en 2024-2025: vulnerabilidades y deprecación

IngressNightmare: CVE con CVSS 9.8

En marzo de 2024, el proyecto ingress-nginx acumuló un conjunto de vulnerabilidades críticas que los investigadores agruparon bajo el nombre IngressNightmare. La más grave obtuvo una puntuación CVSS de 9.8 y permitía ejecución remota de código sin autenticación a través del admission webhook.

El vector de ataque era el siguiente: el admission webhook de ingress-nginx valida los objetos Ingress cuando se crean o modifican. Un atacante con capacidad de crear o modificar recursos Ingress en el cluster — no necesariamente con privilegios de administrador — podía inyectar directivas de configuración arbitrarias de NGINX que se ejecutaban en el contexto del controlador, el cual corre con permisos elevados. Desde ahí, el salto a comprometer el cluster completo era trivial.

Los investigadores estimaron que aproximadamente el 43% de los entornos cloud analizados tenían instalaciones vulnerables expuestas. La superficie de ataque era particularmente amplia porque muchos equipos no eran conscientes de que el admission webhook escuchaba en una red accesible.

Las versiones que corrigen estas vulnerabilidades son la 1.11.5+ y la 1.12.1+. Si tu instalación está por debajo de esos números, tienes que actualizar o deshabilitar el admission webhook antes de hacer cualquier otra cosa.

F5/NGINX depreca su controlador open source

Paralelamente, F5/NGINX anunció que dejaba de desarrollar activamente el kubernetes-ingress open source y pivotaba hacia NGINX Gateway Fabric, una implementación de la Kubernetes Gateway API con NGINX como plano de datos. Este movimiento es coherente con la dirección general del ecosistema Kubernetes, pero deja a los equipos que usan el controlador open source de F5/NGINX con una decisión clara: migrar a NGINX Gateway Fabric o evaluar otras alternativas.


Impacto real en clusters en producción

Estos dos eventos — las vulnerabilidades y la deprecación — tienen tres consecuencias prácticas que cualquier equipo de plataforma debe gestionar:

1. Riesgo de seguridad inmediato. Una instalación de ingress-nginx sin parchear es una superficie de ataque crítica. Esto no es negociable: la actualización o el hardening del admission webhook es la primera acción, independientemente de cualquier decisión sobre migración.

2. Incertidumbre operativa a medio plazo. Incluso si parchas ahora, la pregunta sigue sobre la mesa: ¿tiene sentido seguir invirtiendo en un controlador con historial de CVEs críticos y una comunidad que tiene que gestionar deuda técnica acumulada? No hay una respuesta universal, pero el debate ya no puede posponerse.

3. Complejidad de migración de configuraciones. Los equipos que llevan años usando ingress-nginx tienen acumuladas configuraciones complejas basadas en anotaciones nginx.ingress.kubernetes.io/*. Migrar eso a otro controlador — o a Gateway API — requiere trabajo real. No es un cambio de Helm chart.


Alternativas al NGINX Ingress Controller

A continuación analizamos las principales opciones. No todas son equivalentes en madurez, complejidad operativa ni casos de uso ideales.

Traefik

Traefik es la alternativa más popular entre los equipos que salen de ingress-nginx. Escrito en Go, soporta simultáneamente la Ingress API estándar, sus propios CRDs IngressRoute, y la Gateway API de Kubernetes. Tiene automatización TLS integrada (Let’s Encrypt y otros proveedores ACME), un dashboard visual, y un modelo de configuración que muchos equipos encuentran más comprensible que NGINX.

Ventajas clave:
– Curva de aprendizaje baja para equipos que vienen de ingress-nginx
– TLS automation sin configuración adicional
– Soporte activo de Gateway API
– Muy buena integración con Prometheus y Grafana
– Helm chart mantenida y bien documentada

Limitaciones a considerar:
– El modelo de configuración dinámica puede generar comportamientos inesperados en migraciones complejas
– El dashboard, aunque útil, no sustituye a un sistema de observabilidad propio
– En cargas muy altas, el overhead de Go frente a implementaciones eBPF puede ser medible

Traefik es la opción de menor fricción para migraciones desde ingress-nginx. Si el objetivo principal es salir de NGINX sin comprometer semanas de trabajo, Traefik es probablemente el punto de partida más sensato.

Envoy Gateway

Envoy Gateway es un proyecto CNCF que implementa la Kubernetes Gateway API de forma nativa, usando Envoy como plano de datos. Envoy lleva años siendo el proxy subyacente de Istio y de los load balancers de grandes plataformas cloud, con lo que tiene una base técnica muy sólida.

Ventajas clave:
– Implementación de referencia de Gateway API — las features llegan antes que en otros controladores
– Plano de datos battle-tested a escala
– Separación de responsabilidades clara entre infraestructura y aplicaciones (ver sección Gateway API más abajo)
– Soporte robusto para routing avanzado: header-based, weight-based, retries, timeouts

Limitaciones a considerar:
– Curva de aprendizaje más pronunciada, especialmente si el equipo no tiene experiencia con Envoy
– La Gateway API tiene una abstracción diferente a Ingress — la migración requiere reescribir configuraciones, no solo adaptar anotaciones
– Relativamente más joven como proyecto autónomo (aunque el plano de datos es maduro)

Envoy Gateway es la elección estratégicamente más sólida si el equipo está dispuesto a invertir en aprender el modelo de Gateway API desde cero. A 18 meses vista, es probablemente donde está el ecosistema.

Cilium Gateway API

Si tu cluster ya usa Cilium como CNI, la Cilium Gateway API es una opción que merece evaluación seria. Cilium usa eBPF para procesar tráfico a nivel de kernel, eliminando el overhead del proxy userspace. El resultado es una reducción medible de latencia en percentiles altos (p99, p999) que en cargas intensas puede ser significativa.

Ventajas clave:
– Rendimiento superior gracias a eBPF — menos saltos de red, menos contexto switch
– Integración nativa con network policy de Cilium y con Hubble (observabilidad)
– Un único componente gestionando CNI, network policy e ingress — menos piezas en el cluster
– Implementación de Gateway API bien mantenida

Limitaciones a considerar:
– Solo tiene sentido si ya usas Cilium como CNI — añadir Cilium solo para el ingress no es justificable en la mayoría de casos
– La Gateway API de Cilium tiene gaps de features frente a Envoy Gateway o Traefik en algunos escenarios avanzados
– Cilium en sí tiene una curva de aprendizaje operativa no trivial

Si ya tienes Cilium, considera seriamente consolidar en Cilium Gateway API. Si no lo tienes, no es el punto de entrada.

HAProxy Ingress

HAProxy tiene reputación sólida en rendimiento bruto y control preciso del tráfico. El HAProxy Ingress Controller es una opción válida para equipos con experiencia previa en HAProxy — su modelo de configuración y sus capacidades de balanceo tienen décadas de madurez detrás.

Cuándo tiene sentido:
– El equipo tiene expertise en HAProxy y quiere mantener esa consistencia
– Se requieren capacidades de balanceo muy específicas que otros controladores no exponen fácilmente
– Migración desde un entorno que ya usaba HAProxy como load balancer externo

Cuándo no es la opción ideal:
– El equipo no conoce HAProxy — la curva de aprendizaje no se justifica frente a Traefik
– Se busca una implementación de Gateway API madura — HAProxy Ingress está más orientada a Ingress API estándar

Kong Ingress Controller

Kong ocupa un espacio propio: es más que un ingress controller, es una plataforma de API gateway con un sistema de plugins para autenticación, rate limiting, transformación de peticiones, logging avanzado, y más. Esto lo hace muy potente en contextos donde el ingress controller también tiene que cumplir funciones de API gateway.

Ventajas clave:
– Sistema de plugins muy extenso (autenticación OAuth/OIDC, JWT, rate limiting, transformaciones)
– Buena opción para organizaciones que necesitan API gateway y control de tráfico en un único componente
– Soporte de Gateway API y Ingress API

Limitaciones a considerar:
– Overhead operativo — en configuración stateful requiere una base de datos PostgreSQL; en modo declarativo (DB-less) tiene limitaciones
– No es la elección correcta si solo necesitas routing HTTP básico — hay herramientas más simples para eso
– Las licencias y el soporte enterprise tienen coste

Kong tiene sentido cuando los requisitos van más allá del routing: cuando necesitas un punto centralizado de gestión de políticas para APIs internas o externas.

Istio Gateway

Istio Gateway no es un ingress controller en el sentido tradicional — es la puerta de entrada del mesh de Istio. Si tu organización está planificando o ya operando un service mesh, Istio Gateway ofrece un plano de datos unificado con observabilidad consistente (métricas, trazas distribuidas, mTLS automático) en todos los servicios.

Cuándo tiene sentido:
– Ya tienes Istio desplegado o hay un plan firme de adoptarlo
– Necesitas observabilidad de nivel L7 consistente en todo el cluster
– mTLS entre servicios es un requisito no negociable

Cuándo no tiene sentido:
– Solo necesitas routing de entrada — Istio es overhead masivo para ese único caso
– El equipo no tiene experiencia con service meshes — la carga operativa es significativa
– Clusters pequeños o de desarrollo donde la complejidad no se justifica

NGINX Gateway Fabric

NGINX Gateway Fabric es la apuesta estratégica de F5/NGINX de cara al futuro: una implementación de Kubernetes Gateway API con NGINX como plano de datos. Es la ruta de migración natural para equipos fuertemente invertidos en el ecosistema NGINX.

Ventajas clave:
– Plano de datos NGINX familiar para equipos con expertise en él
– Implementación progresiva de Gateway API activamente desarrollada
– Ruta de migración oficial para usuarios de kubernetes-ingress (F5)

Limitaciones a considerar:
– Proyecto más joven que las alternativas consolidadas
– La implementación de Gateway API todavía cubre menos features que Envoy Gateway
– Si el objetivo es alejarse del ecosistema NGINX, este no es el camino


Tabla comparativa de controladores

ControladorIngress APIGateway APIMadurezCaso de uso idealComplejidad operativa
ingress-nginxSíParcialAltaMigración desde NGINX, herenciaBaja
TraefikSíSíAltaMigración desde ingress-nginxBaja
Envoy GatewayParcialSí (nativa)Media-AltaNuevos clusters, plataformasMedia
Cilium GatewayNoSíMediaClusters con Cilium CNIMedia
HAProxy IngressSíLimitadaAltaEquipos con expertise HAProxyMedia
Kong IngressSíSíAltaAPI gateway + ingressAlta
Istio GatewaySíSíAltaService mesh + ingressMuy alta
NGINX Gateway FabricNoSíMediaEcosistema NGINX, migración F5Media

Gateway API: la dirección estratégica del ecosistema

Para entender por qué tiene sentido evaluar ahora la migración a Gateway API — independientemente del controlador que elijas — hay que entender qué es y por qué representa una evolución genuina.

Limitaciones del recurso Ingress

El recurso Ingress de Kubernetes lleva en el ecosistema desde prácticamente los orígenes, y eso tiene un coste: su modelo es básico y las diferencias entre controladores se resuelven con anotaciones propietarias (nginx.ingress.kubernetes.io/*, traefik.ingress.kubernetes.io/*, etc.). El resultado es configuración que no es portable entre controladores y que mezcla responsabilidades que deberían estar separadas.

El modelo de Gateway API

La Gateway API introduce una jerarquía de recursos con separación de responsabilidades explícita:

GatewayClass — Define los tipos de gateway disponibles en el cluster. Lo gestiona el equipo de infraestructura o el proveedor de la plataforma. Es el punto de configuración de qué controlador se usa y con qué capacidades.

Gateway — Una instancia específica de un listener, con configuración de puertos, protocolos y certificados TLS. Lo gestiona el equipo de plataforma. Múltiples equipos de aplicación pueden compartir un mismo Gateway.

HTTPRoute, TCPRoute, GRPCRoute — Los recursos de routing gestionados por los equipos de aplicación. Definen cómo el tráfico que llega al Gateway se dirige a los servicios del backend.

Esta separación mapea directamente en los roles organizativos típicos: el equipo de plataforma controla el gateway, los equipos de aplicación controlan sus reglas de routing. Ya no hay que dar permisos de administrador de cluster para que un equipo de desarrollo configure su routing.

Estado actual de adopción

Gateway API alcanzó la versión v1.0 en octubre de 2023 y la v1.1 en 2024. Los recursos core (HTTPRoute, GatewayClass, Gateway) están en GA. La API networking.k8s.io/v1 de Ingress no está deprecada — sigue funcionando y seguirá haciéndolo indefinidamente — pero el desarrollo de nuevas features se hace exclusivamente en Gateway API.

La conclusión práctica es esta: si construyes plataformas de Kubernetes hoy, Gateway API es donde debe ir la inversión técnica. Los controladores que implementan Gateway API de forma nativa (Envoy Gateway, Cilium, NGINX Gateway Fabric) son la dirección correcta a medio plazo.


Framework de decisión: ¿qué hacer con tu situación actual?

Mantente en ingress-nginx si…

  • Has parcheado a la versión 1.11.5+ o 1.12.1+ y has deshabilitado o hardened el admission webhook
  • Tu configuración tiene alta densidad de anotaciones nginx.ingress.kubernetes.io/* y el coste de migración no está justificado ahora mismo
  • Tienes expertise interno en NGINX que es difícil de reemplazar
  • El horizonte del proyecto es corto (menos de 12 meses) y no hay presupuesto para infraestructura

En este caso, la acción inmediata es parchear y planificar la migración como trabajo planificado, no como crisis.

Migra ahora si…

  • Estás usando el kubernetes-ingress open source de F5/NGINX (el deprecado) — no el ingress-nginx comunitario
  • Tienes complejidad de anotaciones moderada y cycles de ingeniería disponibles
  • Estás planificando una actualización mayor de Kubernetes de todas formas — es el momento de cambiar el controlador en el mismo proceso
  • El equipo de seguridad ha marcado el historial de CVEs de ingress-nginx como inaceptable para tu perfil de riesgo
  • Estás construyendo clusters nuevos o refactorizando la plataforma — aquí no hay deuda histórica que arrastrar

Evalúa antes de comprometerte si…

  • Tienes requisitos de tráfico complejos (routing multi-tenant, políticas de seguridad avanzadas, service mesh parcial) — vale la pena verificar que el controlador destino los cubre antes de comprometerse
  • La implementación de Gateway API de tu controlador candidato tiene gaps relevantes para tus casos de uso
  • Tienes requisitos multi-cluster o multi-tenant que pueden cambiar las prioridades de la evaluación
  • Necesitas un análisis completo de coste total (licencias, soporte, formación, horas de migración)

Checklist de migración paso a paso

Una migración de ingress controller mal planificada puede provocar downtime en producción. Este proceso en cuatro fases minimiza el riesgo.

Fase 1: Inventario y análisis

Antes de tocar nada en producción:

  • Enumera todos los recursos Ingress del cluster por namespace: kubectl get ingress -A
  • Documenta todas las anotaciones usadas — especialmente las no estándar
  • Identifica configuraciones que podrían no tener equivalente directo en el controlador destino (custom snippets de NGINX, configuraciones de auth externo, etc.)
  • Mapea todos los certificados TLS: origen (cert-manager, secretos manuales, wildcard), referencias en los Ingress, y cómo se gestionan las renovaciones
  • Documenta snippets de configuración NGINX custom (nginx.ingress.kubernetes.io/server-snippet, configuration-snippet) — estos son los que más trabajo dan en la migración

Fase 2: Validación en entorno no productivo

  • Despliega el controlador candidato en un cluster de staging con recursos Ingress idénticos a producción
  • Valida TLS end-to-end para todos los dominios
  • Prueba con carga representativa y compara métricas (latencia, throughput, error rate)
  • Verifica la integración con tu stack de observabilidad (Prometheus, Grafana, alertas)
  • Prueba los escenarios de fallo: ¿qué pasa si el controlador se reinicia? ¿si un backend no responde?
  • Incluye los paths de autenticación externa si los tienes (OAuth2 Proxy, autenticación básica, etc.)

Fase 3: Migración a producción en fases

  • Despliega el nuevo controlador junto al existente usando IngressClasses distintas. Esto es clave: no sustituyas el controlador actual hasta que el nuevo esté validado con tráfico real.
  • Migra primero los recursos de menor riesgo: servicios internos, herramientas de desarrollo, endpoints de baja criticidad
  • Usa DNS como mecanismo de canary: cambia el CNAME o el A record del dominio para apuntar al nuevo controlador, con TTL bajo para poder revertir rápido
  • Monitoriza métricas durante 24-48 horas por cada batch de recursos migrados antes de continuar
  • Mantén el controlador antiguo activo hasta que el 100% del tráfico esté en el nuevo y hayas validado estabilidad durante al menos una semana

Fase 4: Adopción de Gateway API (si aplica)

Si el plan es migrar a Gateway API (recomendado a medio plazo):

  • Instala los CRDs de Gateway API: kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/...
  • Define los recursos GatewayClass según el controlador (Envoy Gateway, Traefik, Cilium…)
  • Migra rutas de forma progresiva, empezando por las más simples (routing básico sin transformaciones ni auth compleja)
  • Actualiza pipelines de CI/CD para generar recursos HTTPRoute/GRPCRoute en lugar de Ingress
  • Verifica que cert-manager funciona con Gateway API — soportado desde la versión 1.14 para HTTP-01 challenges

Preguntas frecuentes

¿Está deprecada la Ingress API de Kubernetes?

No. El recurso networking.k8s.io/v1/Ingress no está deprecado. Sigue funcionando y seguirá haciéndolo indefinidamente. Lo que ha cambiado es que las nuevas features solo se desarrollan en Gateway API. Los Ingress existentes continúan funcionando sin ningún cambio necesario.

¿Puedo ejecutar dos ingress controllers en paralelo durante la migración?

Sí, y es la forma recomendada de migrar. Kubernetes soporta múltiples recursos IngressClass, y cada recurso Ingress puede especificar qué controlador lo gestiona mediante spec.ingressClassName. Esto permite tener el controlador antiguo y el nuevo activos simultáneamente, migrando recursos de forma gradual y reversible.

¿cert-manager sigue funcionando si cambio de controlador?

Sí. cert-manager opera de forma independiente al ingress controller y funciona con cualquiera de las alternativas analizadas. Desde la versión 1.14, cert-manager también soporta Gateway API para los challenges HTTP-01, lo que permite usarlo tanto con Ingress API como con Gateway API.

¿Hay diferencias de rendimiento relevantes entre controladores?

Para la mayoría de workloads, la diferencia entre controladores maduros (ingress-nginx, Traefik, HAProxy, Envoy) es marginal. La excepción notable es Cilium con eBPF: al procesar el tráfico a nivel de kernel elimina el overhead del proxy userspace, lo que se traduce en mejoras medibles en percentiles altos (p99, p999) bajo carga intensa. Si el rendimiento en percentiles altos es crítico para tu aplicación, Cilium Gateway API merece una evaluación seria.

¿Qué pasa si usamos los load balancers nativos del proveedor cloud?

Usar los load balancers cloud nativos (AWS ALB Ingress Controller, GKE Gateway, Azure Application Gateway Ingress Controller) elimina la carga operativa de gestionar un ingress controller en el cluster. A cambio, introduces dependencia del proveedor y costes directos por recurso de load balancer. Es una opción válida para organizaciones completamente cloud-native que no tienen estrategia multi-cloud ni on-premise.


Recomendaciones concretas

Acciones inmediatas (próximos 30 días)

Si usas ingress-nginx:
1. Actualiza a la versión 1.11.5+ o 1.12.1+
2. Evalúa deshabilitar el admission webhook si no lo necesitas (--enable-admission-webhook=false) o restringe su acceso a red
3. Revisa los permisos RBAC del serviceaccount del controlador — aplica el principio de mínimo privilegio

Si usas kubernetes-ingress de F5/NGINX (open source):
1. Contacta con el equipo de F5/NGINX para obtener el timeline de deprecación concreto
2. Empieza la evaluación de NGINX Gateway Fabric en paralelo

Medio plazo (3-6 meses)

  • Evalúa Traefik si buscas la migración de menor fricción desde ingress-nginx: la curva de aprendizaje es baja y la compatibilidad con configuraciones existentes es alta
  • Evalúa Envoy Gateway si buscas la elección estratégicamente más sólida y el equipo tiene capacidad de invertir en aprender Gateway API desde cero
  • Despliega el candidato en staging con carga real antes de comprometerte

Largo plazo (6-18 meses)

Independientemente del controlador de datos que elijas, planifica la migración de los recursos Ingress a recursos de Gateway API (HTTPRoute, GRPCRoute, etc.). La feature parity nunca llegará al recurso Ingress. Los equipos que construyan sobre Gateway API ahora tendrán las herramientas del ecosistema trabajando a favor en los próximos años.


Conclusión

La situación actual no es una crisis, pero sí un punto de inflexión. ingress-nginx parcheado es seguro a corto plazo. Pero la combinación de historial de CVEs, deuda técnica acumulada y la dirección clara del ecosistema hacia Gateway API hace que el debate sobre migración sea inevitable — solo se trata de cuándo y con cuánto control tienes sobre el proceso.

La recomendación práctica: parchea ahora, evalúa Traefik o Envoy Gateway en staging en los próximos meses, y planifica Gateway API como destino a 12-18 meses. No es trabajo de un sprint, pero tampoco es una reescritura desde cero — con el proceso por fases descrito aquí, es perfectamente manejable sin comprometer la estabilidad de producción.

Helm Values JSON Schema: Valida tu values.yaml Antes de Romper Producción

Helm Values JSON Schema: Valida tu values.yaml Antes de Romper Producción

Helm es el gestor de paquetes de facto para Kubernetes, y values.yaml es su interfaz principal de configuración. Durante años, sin embargo, esa interfaz ha estado completamente sin validar por defecto — un fichero YAML de forma libre donde cualquier clave puede ser cualquier cosa, donde los errores tipográficos pasan silenciosamente, y donde las implantaciones mal configuradas solo se revelan cuando los pods fallan al arrancar en producción. El fichero values.schema.json cambia esa ecuación por completo. Este artículo explica por qué importa la validación de schemas, cómo implementarla correctamente y cómo integrarla en un pipeline de CI/CD moderno.

El Problema: Fallos Silenciosos en Producción

Imaginad un equipo de plataforma gestionando docenas de releases de Helm en múltiples clústeres. Un desarrollador envía un fichero de valores con replicaCount: "3" en lugar de replicaCount: 3 — una cadena de texto donde se espera un entero. O escribe image.pullPolicy: Allways con una errata. O no incluye una referencia a un secret obligatorio que la aplicación necesita para arrancar. En los tres casos, Helm sin validación de schema renderizará felizmente los templates, producirá manifiestos de Kubernetes y los aplicará al clúster. El fallo aparece después — a veces mucho después — como un CrashLoopBackOff, un ImagePullBackOff o un error sutil en tiempo de ejecución que lleva horas depurar.

No es un escenario hipotético. Es la realidad diaria de los equipos que operan a escala sin validación de valores. La causa raíz es arquitectónica: los templates de Helm usan el motor text/template de Go, que es débilmente tipado y permisivo por diseño. Un template que hace {{ .Values.replicaCount }} renderizará tanto si el valor es un entero como si es una cadena o incluso un booleano. El manifiesto de Kubernetes resultante puede ser inválido, pero ese error solo sale a la superficie cuando el API server de Kubernetes lo rechaza — o peor, lo acepta pero lo interpreta de manera diferente a la prevista.

Las consecuencias se amplifican a escala. Cuando un chart lo utilizan varios equipos, la ausencia de un contrato formal para los valores aceptables obliga a cada consumidor a leer los ficheros de template y los comentarios para entender qué entradas son válidas. No hay especificación legible por máquina. No hay soporte en el IDE. No hay ningún guardarraíl. La única documentación es lo que el autor del chart tuvo a bien escribir en los comentarios de values.yaml — y los comentarios no detienen a un pipeline de CI de desplegar una implantación rota.

Qué es values.schema.json

Desde Helm 3.0.0, publicado en noviembre de 2019, Helm soporta un fichero opcional values.schema.json en la raíz del directorio del chart — al mismo nivel que Chart.yaml y values.yaml. Este fichero es un documento JSON Schema draft-07 que describe formalmente la estructura, los tipos, las restricciones y los campos obligatorios de los valores del chart.

Cuando este fichero está presente, Helm valida automáticamente los valores combinados (los valores por defecto de values.yaml fusionados con cualquier sobreescritura del usuario) contra el schema en varios puntos: durante helm install, helm upgrade, helm template y helm lint. Si la validación falla, Helm se niega a continuar e imprime un mensaje de error legible que identifica exactamente qué valor ha fallado y por qué. Esto transforma una clase de fallos en tiempo de ejecución en fallos en tiempo de construcción — la dirección correcta para cualquier sistema en producción.

Vale la pena destacar la elección específica de JSON Schema draft-07. El draft-07 tiene un soporte muy amplio en herramientas, incluyendo la extensión YAML de Red Hat para VS Code, los IDEs de JetBrains y la mayoría de los validadores de JSON Schema. Introdujo las palabras clave condicionales if/then/else que son especialmente útiles para los Helm charts. Las versiones más recientes (2019-09, 2020-12) ofrecen funcionalidades adicionales, pero tienen menos soporte universal en herramientas, lo que hace que draft-07 sea la elección pragmática para los autores de charts hoy en día.

Estructura del Directorio del Chart

my-app/
├── Chart.yaml
├── values.yaml
├── values.schema.json      ← va aquí
├── charts/
└── templates/
    ├── deployment.yaml
    ├── service.yaml
    ├── ingress.yaml
    └── _helpers.tpl

El fichero de schema se incluye cuando un chart se empaqueta con helm package y se distribuye a través de repositorios de charts. Los consumidores del chart obtienen la validación del schema automáticamente sin ninguna configuración adicional — los guardarraíles se distribuyen con el propio chart.

Cómo Utiliza Helm el Schema

El comportamiento de validación de Helm es sencillo, pero tiene algunos matices que merece la pena entender. Cuando Helm procesa un release, primero combina todas las fuentes de valores en orden de precedencia creciente: los valores por defecto del chart (values.yaml), los valores del chart padre, los ficheros de valores pasados con -f y, por último, los flags --set. El resultado combinado se valida entonces contra el schema en una única operación.

Esto significa que el schema valida los valores efectivos, no cada fuente de forma aislada. Un campo obligatorio que tiene un valor por defecto en values.yaml pasará la validación aunque el usuario no lo especifique, porque el resultado combinado incluye el valor por defecto. Este es el comportamiento correcto: valida lo que se usará realmente durante el renderizado.

La validación ocurre antes del renderizado de los templates. Si la validación del schema falla, Helm termina con un código de estado no cero e imprime todos los errores de validación. La salida de error es estructurada y accionable:

$ helm install my-release ./my-app --set replicaCount=abc

Error: values don't meet the specifications of the schema(s) in the following chart(s):
my-app:
- replicaCount: Invalid type. Expected: integer, given: string

Para helm lint, que se usa típicamente en pipelines de CI sin instalar en un clúster, la validación del schema también se ejecuta. Esto convierte a helm lint en una potente puerta de pre-despliegue cuando hay ficheros de schema presentes.

Ventajas en el IDE: Autocompletado y Validación Inline

Más allá de la propia validación de Helm, values.schema.json desbloquea el soporte en el IDE que mejora significativamente la experiencia del desarrollador al trabajar con ficheros de valores. La extensión YAML de Red Hat para VS Code puede referenciar un fichero JSON Schema para proporcionar autocompletado, comprobación de tipos y resaltado de errores inline en ficheros YAML.

Para habilitarlo, añadid una configuración yaml.schemas a los ajustes del workspace de VS Code o al fichero de ajustes del usuario:

// .vscode/settings.json
{
  "yaml.schemas": {
    "./my-app/values.schema.json": "./my-app/values.yaml"
  }
}

Con esta configuración, al editar values.yaml en VS Code se mostrará autocompletado para las claves definidas, errores inline para los errores de tipo y documentación en hover extraída de los campos description de vuestro schema. Para los equipos de plataforma que mantienen Helm charts internos, esto transforma el chart en una interfaz de configuración autodocumentada y preparada para el IDE — sin ninguna inversión adicional en herramientas.

Los IDEs de JetBrains (IntelliJ IDEA, GoLand, etc.) soportan las asociaciones de JSON Schema a través del panel de ajustes Languages & Frameworks > Schemas and DTDs > JSON Schema Mappings, proporcionando una funcionalidad equivalente para los equipos que usan esas herramientas.

Construyendo el Schema: Guía Práctica

Construyamos un ejemplo completo y realista. Partimos de un values.yaml típico para un chart de aplicación web:

# values.yaml
replicaCount: 2

image:
  repository: myorg/my-app
  tag: "1.0.0"
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80

ingress:
  enabled: false
  hostname: ""
  tls: false

resources:
  requests:
    cpu: "100m"
    memory: "128Mi"
  limits:
    cpu: "500m"
    memory: "512Mi"

autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80

config:
  logLevel: info
  databaseUrl: ""

nodeSelector: {}
tolerations: []
affinity: {}

Ahora el values.schema.json completo que valida esta estructura:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "my-app Helm Chart Values",
  "description": "Valores de configuración para el Helm chart my-app",
  "type": "object",
  "additionalProperties": false,
  "required": ["image", "service"],
  "$defs": {
    "resourceQuantity": {
      "type": "string",
      "pattern": "^[0-9]+(\\.[0-9]+)?(m|Ki|Mi|Gi|Ti|Pi|Ei|k|M|G|T|P|E)?$",
      "description": "Una cantidad de recurso de Kubernetes (p. ej.: 100m, 128Mi, 1Gi)"
    }
  },
  "properties": {
    "replicaCount": {
      "type": "integer",
      "minimum": 0,
      "maximum": 50,
      "default": 2,
      "description": "Número de réplicas del pod. Estableced a 0 para escalar a cero."
    },
    "image": {
      "type": "object",
      "additionalProperties": false,
      "required": ["repository", "tag"],
      "description": "Configuración de la imagen del contenedor",
      "properties": {
        "repository": {
          "type": "string",
          "minLength": 1,
          "description": "Repositorio de la imagen del contenedor"
        },
        "tag": {
          "type": "string",
          "pattern": "^[a-zA-Z0-9._-]+$",
          "minLength": 1,
          "description": "Tag de la imagen. Evitad usar 'latest' en producción."
        },
        "pullPolicy": {
          "type": "string",
          "enum": ["Always", "IfNotPresent", "Never"],
          "default": "IfNotPresent",
          "description": "imagePullPolicy de Kubernetes"
        }
      }
    },
    "service": {
      "type": "object",
      "additionalProperties": false,
      "required": ["type", "port"],
      "properties": {
        "type": {
          "type": "string",
          "enum": ["ClusterIP", "NodePort", "LoadBalancer", "ExternalName"],
          "description": "Tipo de Service de Kubernetes"
        },
        "port": {
          "type": "integer",
          "minimum": 1,
          "maximum": 65535,
          "description": "Puerto del Service"
        }
      }
    },
    "ingress": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "enabled": {
          "type": "boolean",
          "default": false
        },
        "hostname": {
          "type": "string"
        },
        "tls": {
          "type": "boolean",
          "default": false
        }
      },
      "if": {
        "properties": {
          "enabled": { "const": true }
        },
        "required": ["enabled"]
      },
      "then": {
        "required": ["hostname"],
        "properties": {
          "hostname": {
            "type": "string",
            "minLength": 1,
            "pattern": "^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
            "description": "Nombre de host para el Ingress. Obligatorio cuando ingress está habilitado."
          }
        }
      }
    },
    "resources": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "requests": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "cpu": { "$ref": "#/$defs/resourceQuantity" },
            "memory": { "$ref": "#/$defs/resourceQuantity" }
          }
        },
        "limits": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "cpu": { "$ref": "#/$defs/resourceQuantity" },
            "memory": { "$ref": "#/$defs/resourceQuantity" }
          }
        }
      }
    },
    "autoscaling": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "enabled": { "type": "boolean", "default": false },
        "minReplicas": { "type": "integer", "minimum": 1 },
        "maxReplicas": { "type": "integer", "minimum": 1 },
        "targetCPUUtilizationPercentage": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100
        }
      }
    },
    "config": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "logLevel": {
          "type": "string",
          "enum": ["debug", "info", "warn", "error"],
          "default": "info"
        },
        "databaseUrl": {
          "type": ["string", "null"],
          "description": "URL de conexión a la base de datos."
        }
      }
    },
    "nodeSelector": {
      "type": "object",
      "description": "Selectores de nodo de Kubernetes"
    },
    "tolerations": {
      "type": "array",
      "description": "Tolerations de Kubernetes"
    },
    "affinity": {
      "type": "object",
      "description": "Reglas de afinidad de Kubernetes"
    }
  }
}

Patrones Clave del Schema Explicados

additionalProperties: false

Este es, posiblemente, el patrón más importante de un Helm schema. Sin él, las claves desconocidas pasan la validación silenciosamente — lo que anula gran parte del propósito. Con "additionalProperties": false, cualquier clave no listada en properties provoca un error de validación. Esto detecta erratas como repicaCount en lugar de replicaCount, que de otro modo usarían silenciosamente el valor por defecto y dejarían al desarrollador preguntándose por qué su sobreescritura no tuvo efecto.

Aplicadlo en cada nivel de objeto anidado, no solo en la raíz. Una errata dentro de image: o resources: es tan peligrosa como una en el nivel superior.

$defs para Definiciones Reutilizables

La palabra clave $defs (llamada definitions en versiones anteriores del draft, aunque draft-07 soporta ambas) proporciona un espacio de nombres para fragmentos de schema reutilizables. En el ejemplo anterior, resourceQuantity se define una vez y se referencia mediante $ref tanto en requests como en limits. Esto evita la duplicación y garantiza una lógica de validación consistente en campos relacionados.

Para charts más grandes, $defs se vuelve imprescindible. Los patrones habituales incluyen schemas reutilizables para configuraciones de imagen, requisitos de recursos, configuraciones de probe y mapas de variables de entorno.

Validación Condicional con if/then/else

La construcción if/then/else en JSON Schema draft-07 es especialmente potente para los Helm charts, donde muchos valores son condicionales según un flag de activación de funcionalidad. El ejemplo del ingress lo demuestra: cuando ingress.enabled es true, el campo hostname se vuelve obligatorio y debe coincidir con un patrón de nombre de host válido. Cuando el ingress está deshabilitado, el hostname puede estar vacío u omitirse por completo.

Este patrón puede extenderse para escenarios más complejos. Por ejemplo, para que cuando autoscaling.enabled sea true, el replicaCount independiente no deba establecerse (ya que el HPA controla el número de réplicas):

{
  "if": {
    "properties": {
      "autoscaling": {
        "properties": {
          "enabled": { "const": true }
        },
        "required": ["enabled"]
      }
    }
  },
  "then": {
    "properties": {
      "replicaCount": {
        "description": "replicaCount se ignora cuando autoscaling está habilitado"
      }
    }
  }
}

Validación de Patrones para Tags de Imagen

El campo del tag de imagen es una fuente habitual de problemas en producción. Los equipos despliegan accidentalmente con latest, que es no determinista y hace que los rollbacks sean poco fiables. Una restricción de patrón puede forzar la versión semántica o, como mínimo, prohibir el tag latest en los charts de producción:

"tag": {
  "type": "string",
  "not": {
    "enum": ["latest", ""]
  },
  "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+",
  "description": "Se requiere un tag de versión semántica. 'latest' no está permitido."
}

Esto fuerza que los tags de imagen comiencen con un número de versión semántica, rechazando inmediatamente latest, cadenas vacías o nombres de rama arbitrarios que producirían implantaciones no reproducibles.

Enum para Vocabularios Controlados

Los campos con un conjunto fijo de valores válidos — tipos de Service de Kubernetes, políticas de pull de imágenes, niveles de log — deben usar enum. Esto es más preciso que un patrón y produce mensajes de error más claros. También permite que el autocompletado del IDE muestre exactamente las opciones válidas como una lista de selección, en lugar de requerir que el desarrollador recuerde o busque los valores aceptables.

Integración en CI/CD

GitHub Actions

El punto de integración más directo es helm lint, que ejecuta la validación del schema como parte de sus comprobaciones. Un workflow mínimo de GitHub Actions que valida un chart en cada pull request es el siguiente:

# .github/workflows/helm-lint.yaml
name: Helm Lint

on:
  pull_request:
    paths:
      - 'charts/**'

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Helm
        uses: azure/setup-helm@v4
        with:
          version: '3.14.0'

      - name: Lint chart con valores por defecto
        run: helm lint charts/my-app

      - name: Lint chart con valores de staging
        run: helm lint charts/my-app -f charts/my-app/ci/staging-values.yaml

      - name: Lint chart con valores de producción
        run: helm lint charts/my-app -f charts/my-app/ci/production-values.yaml

      - name: Validar renderizado de templates
        run: |
          helm template my-app charts/my-app \
            -f charts/my-app/ci/production-values.yaml \
            --debug > /dev/null

La convención del directorio ci/ (ficheros de valores específicos para pruebas en CI) es un patrón de la herramienta chart-testing y funciona bien para validar múltiples combinaciones de valores realistas, no solo los valores por defecto.

Para los equipos que usan la herramienta CLI ct (chart-testing) del proyecto Helm, la validación del schema se incluye automáticamente en el comando ct lint, que también gestiona las comprobaciones de versiones del chart y el linting de YAML:

      - name: Chart Testing lint
        uses: helm/chart-testing-action@v2.6.1

      - name: Ejecutar chart-testing lint
        run: ct lint --target-branch ${{ github.event.repository.default_branch }}

Hooks de Pre-commit

Para el desarrollo local, los hooks de pre-commit detectan los problemas antes de que el código se suba al repositorio. El framework pre-commit hace esto sencillo:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gruntwork-io/pre-commit
    rev: v0.1.23
    hooks:
      - id: helmlint

  - repo: local
    hooks:
      - id: helm-schema-validate
        name: Helm Schema Validation
        language: script
        entry: scripts/validate-helm-schemas.sh
        files: ^charts/.*values.*\.yaml$
#!/usr/bin/env bash
# scripts/validate-helm-schemas.sh
set -euo pipefail

for chart_dir in charts/*/; do
  if [[ -f "${chart_dir}/values.schema.json" ]]; then
    echo "Linting ${chart_dir}..."
    helm lint "${chart_dir}" --strict
  fi
done

Integración con ArgoCD y Flux

Tanto ArgoCD como Flux (Helm Controller) invocan helm template internamente al reconciliar los Helm releases. Dado que helm template ejecuta la validación del schema cuando hay un fichero de schema presente, cualquier valor inválido en un manifiesto HelmRelease de Flux o en una Application de ArgoCD hará que la reconciliación falle con un mensaje de error claro — visible en los logs del controlador y que aparece como un estado degradado del recurso. No se requiere configuración adicional; la validación del schema es automática.

Generando Schemas a Partir de Charts Existentes

Para charts que ya tienen un values.yaml bien estructurado, escribir un schema desde cero es laborioso pero no se parte de cero. Varias herramientas pueden generar un schema borrador que luego se refina:

  • helm-values-schema-json — un plugin de Helm (helm plugin install https://github.com/losisin/helm-values-schema-json) que inspecciona values.yaml y genera un schema borrador con tipos inferidos. Ejecutadlo con helm schema-gen values.yaml.
  • Herramientas online de generación de JSON Schema — pegad vuestros valores como JSON (convertid primero el YAML a JSON) y obtendréis un schema borrador.
  • Manualmente desde cero — para charts nuevos, escribir el schema junto con el fichero de valores desde el principio es el enfoque más preciso y no requiere herramientas adicionales.

Los schemas generados siempre son puntos de partida. Infieren tipos a partir de los valores existentes, pero no pueden conocer las restricciones previstas, los enums, los patrones, los campos obligatorios en casos condicionales ni additionalProperties: false en los niveles anidados. La revisión y refinamiento manual es siempre necesaria.

Errores Comunes y Cómo Evitarlos

ErrorSíntomaSolución
Sin additionalProperties: falseLas erratas en nombres de claves pasan la validación silenciosamenteAñadidlo en cada nivel de objeto, incluyendo los anidados
Schema solo en la raízLas erratas en niveles anidados no se detectanAplicad additionalProperties: false recursivamente
Sin valores por defecto en el schemaEl IDE muestra campos como obligatorios cuando son opcionalesAñadid default a todos los campos opcionales
Patrones demasiado estrictos que bloquean valores válidosImplantaciones legítimas fallan la validación del schemaProbad los patrones contra vuestro espacio de valores real antes de publicar
Usar definitions en lugar de $defsFunciona en la mayoría de herramientas pero es terminología de draft-2019-09+Usad $defs para cumplimiento con draft-07; ambos funcionan en la práctica
Schema no comprometido en el repositorio del chartLos consumidores no obtienen validación al obtener el chart del repositorioIncluir siempre values.schema.json junto al chart
Validar valores de subcharts desde el schema del chart padreErrores de schema para valores de subcharts que el padre no poseeNo intentéis validar valores de subcharts en el schema del padre; cada chart es dueño de su propio schema

El Problema de los Valores Nulos

Un problema sutil pero común: en YAML, una clave no establecida sin valor (clave:) se resuelve como null, no como una cadena vacía o un cero. Si vuestro schema define un campo como "type": "string", un valor nulo fallará la validación. Para gestionar campos opcionales que los usuarios podrían dejar en blanco, utilizad una unión de tipos:

"databaseUrl": {
  "type": ["string", "null"],
  "description": "URL de conexión a la base de datos. Dejad null para usar el valor por defecto."
}

Como alternativa, aseguraos de que los valores por defecto en values.yaml usen cadenas vacías ("") en lugar de claves sin valor, y documentad esa convención para los consumidores del chart.

Desviación del Schema

A medida que los charts evolucionan, se añaden nuevos valores a values.yaml sin las correspondientes actualizaciones en values.schema.json. Con el tiempo, el schema queda obsoleto y proporciona una cobertura parcial. La solución es procedimental: tratad las actualizaciones del schema como parte de la definición de hecho para cualquier PR que modifique los valores. La revisión de código debe incluir la comprobación de que los valores nuevos o modificados tienen entradas correspondientes en el schema.

Un patrón útil es incluir la validación del schema como paso obligatorio en la checklist de revisión del chart:

  • ¿Se ha añadido el nuevo campo a values.yaml con un valor por defecto sensato?
  • ¿Se ha añadido la entrada correspondiente en values.schema.json con el tipo correcto?
  • ¿Se ha ejecutado helm lint localmente con los valores de CI para verificar que pasa la validación?

Preguntas Frecuentes

¿Valida values.schema.json los valores de los subcharts?

No. Cada chart en una relación de dependencia valida únicamente sus propios valores contra su propio schema. Si el chart A depende del chart B, y el chart B tiene un schema, el schema del chart B valida los valores bajo la clave b: en el values.yaml del chart A — pero solo cuando se procesa en el contexto del chart B. El schema del chart A no debe intentar describir la estructura de valores del chart B. Esto es por diseño: mantiene el acoplamiento flexible entre charts y permite que los subcharts evolucionen sus schemas de forma independiente.

¿Puedo usar JSON Schema draft-2020-12 en lugar de draft-07?

Técnicamente, Helm no fuerza estrictamente qué versión del draft usáis — utiliza la librería Go github.com/xeipuuv/gojsonschema, que soporta draft-04 hasta draft-07. El uso de palabras clave de drafts más nuevos que no están soportadas por esta librería puede provocar que se ignoren silenciosamente en lugar de lanzar un error. Para el soporte en el IDE, draft-07 tiene la compatibilidad más amplia. Si necesitáis características de drafts más nuevos (como unevaluatedProperties de 2020-12), probad cuidadosamente para confirmar que las hace cumplir el validador de Helm y no se omiten silenciosamente.

¿Cómo gestiono valores que difieren entre entornos sin conflictos de schema?

El schema debe describir todos los valores válidos en todos los entornos. Usad enum para enumerar todos los valores válidos para un campo, y usad if/then/else para restricciones que solo aplican en ciertas configuraciones. El schema es un contrato de lo que acepta el chart, no una política de lo que debe usar un entorno específico. Las políticas específicas de entorno (como “producción debe usar un mínimo de 3 réplicas”) se aplican mejor en un nivel superior — mediante controladores de admisión como OPA Gatekeeper o Kyverno — en lugar de en el schema del chart.

¿Se ejecuta la validación del schema cuando se usa helm template para dry runs?

Sí. helm template ejecuta la validación del schema antes de renderizar los templates. Esto lo hace útil como paso de validación en pipelines de CI incluso sin un clúster activo: helm template release-name ./chart -f values-override.yaml fallará con errores de schema si los valores son inválidos, y emitirá los manifiestos renderizados si son válidos. Pasar la salida a kubectl apply --dry-run=client -f - añade una capa adicional de validación de la API de Kubernetes para una comprobación offline exhaustiva.

¿Debo añadir values.schema.json a los charts que no mantengo (charts upstream)?

Para los charts upstream que consumís pero no mantenéis (como los charts de Bitnami, ingress-nginx, cert-manager), el enfoque recomendado es mantener un fichero JSON Schema separado en vuestro propio repositorio GitOps que valide vuestros ficheros de sobreescritura de valores específicos. Herramientas como jsonschema (Python) o ajv (Node.js) pueden validar un fichero de valores YAML/JSON contra un schema en CI sin que Helm esté involucrado. Esto os proporciona validación del schema para vuestras sobreescrituras específicas del entorno sin necesidad de modificar las fuentes de charts upstream.

Conclusión

values.schema.json es una de las funcionalidades más infrautilizadas de Helm. Está disponible desde Helm 3.0.0, no requiere dependencias externas, y transforma los errores de configuración de fallos silenciosos en tiempo de ejecución en errores explícitos en tiempo de construcción.

El coste de adoptarlo es bajo: un fichero JSON por chart, mantenido junto al código del chart. El beneficio es sustancial: validación de tipos, campos obligatorios forzados, vocabularios controlados, autocompletado en el IDE, y una puerta de CI que rechaza valores incorrectos antes de que lleguen a un clúster.

Para los equipos de plataforma que mantienen charts internos consumidos por múltiples equipos, el schema actúa como documentación ejecutable — una especificación formal que no puede desincronizarse con el comportamiento real porque es el comportamiento real. Los comentarios en values.yaml se ignoran durante el despliegue; el schema de JSON no.

Si vuestros charts de Helm no tienen todavía un values.schema.json, el siguiente paso es claro: elegid el chart más crítico, escribid un schema que cubra sus campos más usados con additionalProperties: false en cada nivel, añadid helm lint a vuestro pipeline de CI, y ejecutad el lint contra el conjunto de valores de CI que usáis hoy. Los primeros errores que encontréis serán reales — problemas existentes que llevan tiempo latentes y que el schema acaba de hacer visibles.

Prometheus 3.0 y OpenTelemetry: Soporte Nativo OTLP Explicado

Prometheus 3.0 y OpenTelemetry: Soporte Nativo OTLP Explicado

Siete años es mucho tiempo en el mundo de la observabilidad. Desde que Prometheus 2.0 apareció en 2017, el ecosistema ha sido transformado por la adopción cloud-native, el auge del distributed tracing y la consolidación de OpenTelemetry como estándar de facto para la instrumentación. Prometheus 3.0, publicado en noviembre de 2024, es la respuesta del proyecto a esa transformación — y su cambio más significativo es la capacidad nativa de ingerir métricas OpenTelemetry directamente, sin ningún componente intermediario de por medio.

Este artículo entra en detalle en lo que Prometheus 3.0 cambia realmente para ingenieros de plataforma y arquitectos cloud que gestionan — o planean gestionar — cargas de trabajo instrumentadas con OTel junto a stacks de monitorización basados en Prometheus. Cubriremos el endpoint nativo de ingesta OTLP, el soporte de nombres de métricas UTF-8, Remote Write 2.0, las consideraciones de migración y los patrones arquitectónicos que siguen teniendo sentido incluso cuando OTLP nativo está disponible.

Qué Cambió en Prometheus 3.0: Lo Relevante para OTel

Prometheus 3.0 incluye un conjunto sustancial de cambios. No todos son igualmente relevantes para la integración con OpenTelemetry, así que veamos qué mueve realmente la aguja para los usuarios de OTel antes de entrar en detalle en cada área.

Ingesta Nativa OTLP

La funcionalidad estrella: Prometheus 3.0 incorpora un receptor OTLP integrado que expone un endpoint HTTP que acepta métricas en formato OpenTelemetry Protocol. Las aplicaciones instrumentadas con cualquier SDK OTel pueden ahora enviar métricas directamente a Prometheus sin pasar por un OpenTelemetry Collector. No es un sidecar, no es un plugin, no es un adaptador externo — es un endpoint de primera clase en el propio binario de Prometheus.

Nombres de Métricas UTF-8

Prometheus restringía históricamente los nombres de métricas a [a-zA-Z_:][a-zA-Z0-9_:]*. OpenTelemetry utiliza puntos y barras en los nombres de métricas por convención — http.server.request.duration es un nombre canónico de métrica OTel. Prometheus 3.0 elimina esta restricción y admite caracteres UTF-8 arbitrarios en nombres de métricas y etiquetas, que es el cambio de compatibilidad más importante para la interoperabilidad con OTel.

Remote Write 2.0

Remote Write 2.0 sustituye el protocolo original con una codificación más eficiente basada en protobuf, añade soporte nativo de histogramas en el formato de wire y reduce significativamente el consumo de ancho de banda en despliegues a gran escala. Si estáis federando métricas hacia Thanos, Mimir o Cortex, esto tiene impacto en el coste operativo.

Nueva Interfaz Web

La interfaz web de Prometheus ha sido completamente reescrita. La nueva UI usa React, soporta exploración de metadata de métricas y proporciona una experiencia de construcción de queries notablemente mejorada. Es una mejora de calidad de vida más que un cambio arquitectónico, pero reduce la dependencia de herramientas externas como Grafana para la investigación ad-hoc.

Resumen de Cambios Incompatibles

Prometheus 3.0 elimina varias funcionalidades que estaban deprecadas en 2.x. Las más significativas operacionalmente son: eliminación de la ruta de flag deprecated --web.enable-admin-api, eliminación de ciertas opciones de formato de almacenamiento legacy, cambios en los timeouts de scrape por defecto, y validación más estricta de configuraciones que anteriormente se aceptaban en silencio. Más adelante en este artículo incluimos un checklist de migración.

El Receptor OTLP: Cómo Funciona y Qué Acepta

El receptor OTLP en Prometheus 3.0 está implementado como una funcionalidad opcional que debe activarse explícitamente. Una vez habilitado, expone un endpoint HTTP en /api/v1/otlp/v1/metrics que acepta payloads OTLP ExportMetricsServiceRequest codificados en protobuf — el mismo formato de wire que utiliza el exportador OTLP del OpenTelemetry Collector.

Qué Acepta (y Qué No)

Esto es crítico entenderlo antes de diseñar vuestra arquitectura en torno a la ingesta OTLP nativa: el soporte OTLP de Prometheus 3.0 es exclusivamente para métricas. No acepta trazas ni logs. OTLP es un protocolo unificado que cubre las tres señales, pero Prometheus es un almacén de métricas — el receptor gestiona únicamente la parte de métricas de la especificación OTLP.

Tipos de métricas soportados en el receptor OTLP:

  • Gauge — se mapea directamente a un Gauge de Prometheus
  • Sum (monotónico) — se mapea a un Counter de Prometheus
  • Sum (no monotónico) — se mapea a un Gauge de Prometheus
  • Histogram (explicit bucket) — se mapea a un Histogram de Prometheus
  • ExponentialHistogram — se mapea a Prometheus Native Histograms (funcionalidad de 3.0)
  • Summary — se mapea a un Summary de Prometheus

Los resource attributes del payload OTLP — cosas como service.name, k8s.pod.name, cloud.region — se convierten en labels de Prometheus. Esta conversión es configurable, y por defecto Prometheus aplica una estrategia de promoción que convierte los resource attributes más comunes a labels descartando los que crearían cardinalidad extremadamente alta.

Activar el Receptor OTLP

Activar la ingesta OTLP nativa requiere dos cosas: un feature flag y un bloque de configuración en prometheus.yml.

Arrancad el binario de Prometheus con el feature flag:

prometheus \
  --config.file=/etc/prometheus/prometheus.yml \
  --enable-feature=otlp-write-receiver

Luego añadid la configuración del receptor OTLP a vuestro prometheus.yml:

# prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

otlp:
  # Promover estos resource attributes de OTLP a labels de Prometheus
  promote_resource_attributes:
    - service.name
    - service.namespace
    - service.instance.id
    - k8s.namespace.name
    - k8s.pod.name
    - k8s.node.name
    - cloud.region
    - deployment.environment

Con esta configuración, Prometheus escuchará en el puerto 9090 (por defecto) y aceptará métricas OTLP en http://<prometheus-host>:9090/api/v1/otlp/v1/metrics.

Estrategia de Promoción de Resource Attributes

La lista promote_resource_attributes merece una reflexión cuidadosa. OTLP transporta un contexto de nivel de recurso muy rico — cada payload de métricas incluye un objeto ResourceMetrics con atributos que describen el origen: nombre del servicio, versión, entorno, pod de Kubernetes, nodo, cluster, detalles del proveedor cloud y más. Las labels de Prometheus son pares clave-valor planos en cada serie temporal. Promover demasiados resource attributes dispara la cardinalidad; promover demasiado pocos pierde contexto importante.

Una lista pragmática de partida para despliegues en Kubernetes:

otlp:
  promote_resource_attributes:
    - service.name          # Crítico: identifica el servicio
    - service.namespace     # Agrupación lógica
    - deployment.environment  # prod/staging/dev
    - k8s.namespace.name    # Namespace de Kubernetes
    - k8s.pod.name          # Cardinalidad a nivel de pod — valorad omitirlo a gran escala
    - k8s.node.name         # Útil para correlación con infraestructura

Evitad promover k8s.pod.name indiscriminadamente a escala — en un cluster con miles de pods de vida corta, esto genera una presión de cardinalidad significativa. Prefered service.name y service.namespace para la mayoría de los casos de uso de alertas, reservando las labels a nivel de pod para dashboards de depuración.

Nombres de Métricas UTF-8: El Cambio que Realmente Importa

Para apreciar por qué el soporte de nombres de métricas UTF-8 importa tanto, necesitáis entender la fricción que elimina. Las convenciones semánticas de OpenTelemetry definen nombres de métricas usando puntos como separadores de namespace. La métrica canónica de duración de servidor HTTP es http.server.request.duration. La duración canónica de consulta a base de datos es db.client.operation.duration. Estos nombres están estandarizados entre lenguajes y frameworks — vuestro servicio en Go, vuestro servicio en Java y vuestro servicio en Python emiten el mismo nombre de métrica cuando se instrumentan con OTel.

Prometheus 2.x no podía almacenar estos nombres. Los puntos son caracteres ilegales en la nomenclatura de métricas de Prometheus. Todos los puentes OTel-a-Prometheus — el exportador Prometheus del OpenTelemetry Collector, las capas de compatibilidad de prom-client, el antiguo exportador prometheusremotewrite — tenían que traducir estos nombres, típicamente reemplazando los puntos por guiones bajos: http_server_request_duration.

Esta traducción es imprecisa y crea múltiples problemas:

  • Colisiones de nombres: http.server.request_duration y http.server.request.duration se convierten ambas en http_server_request_duration
  • Rotura de dashboards: Los dashboards de Grafana construidos contra las convenciones semánticas de OTel no funcionan contra métricas Prometheus traducidas sin modificación
  • Correlación entre señales: Los atributos de trazas usan notación de puntos; cuando los nombres de métricas difieren, las herramientas de correlación automatizada pierden el hilo
  • Presión hacia el vendor lock-in: Los equipos acaban con convenciones de nomenclatura separadas para “métricas Prometheus” y “métricas OTel” y mantienen ambas

Prometheus 3.0 con soporte UTF-8 almacena http.server.request.duration de forma nativa. Sin traducción. Sin colisiones. El nombre de métrica con el que instrumentáis es el nombre de métrica que consultáis.

Activar Nombres de Métricas UTF-8

Los nombres de métricas UTF-8 requieren el feature flag utf8-names:

prometheus \
  --config.file=/etc/prometheus/prometheus.yml \
  --enable-feature=utf8-names \
  --enable-feature=otlp-write-receiver

Una vez habilitado, las queries PromQL deben usar nombres de métrica entrecomillados cuando el nombre contiene caracteres fuera del conjunto de caracteres legacy:

# Nombre de métrica legacy — sin comillas funciona bien
http_server_requests_total

# Nombre de métrica OTel con puntos — requiere entrecomillado en PromQL
{"__name__"="http.server.request.duration"}

# O usando la nueva sintaxis PromQL de Prometheus 3.0
http.server.request.duration{service_name="api-gateway"}

El parser de PromQL en Prometheus 3.0 se ha actualizado para gestionar nombres de métricas entrecomillados como construcción de primera clase. El motor PromQL de Grafana también se ha actualizado para gestionar esta sintaxis — verificad vuestra versión de Grafana (10.3+ tiene soporte completo) antes de desplegar.

Del SDK OTel Directamente a Prometheus 3.0: Sin Collector

Para equipos que solo necesitan introducir métricas de aplicación en Prometheus, la ingesta OTLP nativa permite una arquitectura dramáticamente más simple. Así es como se ve con diferentes SDKs OTel.

Go (OpenTelemetry SDK)

package main

import (
    "context"
    "time"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
    "go.opentelemetry.io/otel/sdk/metric"
    "go.opentelemetry.io/otel/sdk/resource"
    semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)

func initMetrics(ctx context.Context) (*metric.MeterProvider, error) {
    res, err := resource.New(ctx,
        resource.WithAttributes(
            semconv.ServiceName("mi-api"),
            semconv.ServiceNamespace("plataforma"),
            semconv.DeploymentEnvironment("produccion"),
        ),
    )
    if err != nil {
        return nil, err
    }

    // Apuntar directamente al endpoint OTLP de Prometheus 3.0
    exporter, err := otlpmetrichttp.New(ctx,
        otlpmetrichttp.WithEndpoint("prometheus:9090"),
        otlpmetrichttp.WithURLPath("/api/v1/otlp/v1/metrics"),
        otlpmetrichttp.WithInsecure(), // Usar WithTLSClientConfig en producción
    )
    if err != nil {
        return nil, err
    }

    provider := metric.NewMeterProvider(
        metric.WithResource(res),
        metric.WithReader(
            metric.NewPeriodicReader(exporter,
                metric.WithInterval(30*time.Second),
            ),
        ),
    )

    otel.SetMeterProvider(provider)
    return provider, nil
}

Python (OpenTelemetry SDK)

from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.sdk.resources import Resource, SERVICE_NAME, SERVICE_NAMESPACE

resource = Resource.create({
    SERVICE_NAME: "mi-api",
    SERVICE_NAMESPACE: "plataforma",
    "deployment.environment": "produccion",
})

exporter = OTLPMetricExporter(
    endpoint="http://prometheus:9090/api/v1/otlp/v1/metrics",
)

reader = PeriodicExportingMetricReader(
    exporter,
    export_interval_millis=30_000,
)

provider = MeterProvider(resource=resource, metric_readers=[reader])
metrics.set_meter_provider(provider)

# Usar el meter
meter = metrics.get_meter("mi-api")
request_counter = meter.create_counter(
    name="http.server.request.count",
    description="Total de peticiones HTTP al servidor",
    unit="1",
)
request_duration = meter.create_histogram(
    name="http.server.request.duration",
    description="Duración de peticiones HTTP al servidor",
    unit="s",
)

Java (OpenTelemetry SDK con Spring Boot)

# application.properties (Spring Boot con auto-instrumentación OTel)
otel.service.name=mi-api
otel.resource.attributes=service.namespace=plataforma,deployment.environment=produccion

# Configurar el exportador OTLP para enviar directamente a Prometheus
otel.metrics.exporter=otlp
otel.exporter.otlp.metrics.endpoint=http://prometheus:9090/api/v1/otlp/v1/metrics
otel.exporter.otlp.metrics.protocol=http/protobuf

# Intervalo de exportación
otel.metric.export.interval=30000

Con Spring Boot y el agente Java de OTel, no se necesitan cambios de código más allá de la configuración — el agente instrumenta vuestro servidor HTTP, clientes de base de datos y sistemas de mensajería automáticamente y envía métricas usando los nombres definidos en las convenciones semánticas de OTel.

OTel Collector hacia Prometheus 3.0: Cuándo Necesitáis el Intermediario

La ingesta OTLP nativa es atractiva, pero el OpenTelemetry Collector sigue siendo relevante para un conjunto significativo de casos de uso. Entender cuándo es apropiado cada patrón es la decisión arquitectónica central que afrontaréis al adoptar Prometheus 3.0 en un entorno OTel.

Patrón 1: OTel Collector como Gateway de Fan-Out

Cuando necesitáis enviar métricas a múltiples backends simultáneamente — Prometheus para alertas, un almacén de larga duración como Thanos para análisis histórico, y una plataforma de observabilidad comercial para correlación full-stack — el OTel Collector gestiona el fan-out eficientemente. Las aplicaciones envían una sola vez al Collector; el Collector distribuye a todos los backends.

# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 10s
    send_batch_size: 1000
  memory_limiter:
    check_interval: 1s
    limit_mib: 512

exporters:
  # Enviar a Prometheus 3.0 vía OTLP
  otlphttp/prometheus:
    endpoint: http://prometheus:9090/api/v1/otlp
    tls:
      insecure: true

  # Fan-out a Thanos vía remote_write
  prometheusremotewrite/thanos:
    endpoint: http://thanos-receive:10908/api/v1/receive
    resource_to_telemetry_conversion:
      enabled: true

  # Fan-out a backend comercial
  otlp/datadog:
    endpoint: https://otel-intake.datadoghq.com
    headers:
      DD-API-KEY: "${DD_API_KEY}"

service:
  pipelines:
    metrics:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlphttp/prometheus, prometheusremotewrite/thanos, otlp/datadog]

Patrón 2: Collector para Transformación de Métricas

Los procesadores transform y metricstransform del OTel Collector permiten remodelar las métricas antes de que lleguen a Prometheus: renombrar labels, añadir atributos estáticos, filtrar series de alta cardinalidad, agregar métricas para reducir el coste de almacenamiento, o aplicar conversiones de unidades. Estas operaciones no están disponibles en el receptor OTLP nativo de Prometheus.

processors:
  transform/metrics:
    metric_statements:
      - context: metric
        statements:
          # Descartar métricas internas de depuración
          - delete_matching_keys(attributes, "internal.*")
          # Normalizar valores del label de entorno
          - set(attributes["deployment.environment"], "prod")
            where attributes["deployment.environment"] == "production"

  filter/drop_debug:
    metrics:
      exclude:
        match_type: regexp
        metric_names:
          - ".*\.debug\..*"
          - "runtime\.go\.internal\..*"

  metricstransform:
    transforms:
      # Renombrar una métrica para ajustarse a vuestra convención de nomenclatura Prometheus
      - include: http.server.request.duration
        action: update
        new_name: http_server_request_duration_seconds

Patrón 3: Collector para Trazas y Logs (Siempre Necesario)

Si vuestra arquitectura incluye trazas y logs junto a métricas — y en 2025 casi con toda seguridad lo hace — necesitáis un OTel Collector independientemente de lo que hagáis con las métricas. Prometheus no acepta trazas ni logs. Jaeger, Tempo y Loki tienen sus propios protocolos de ingesta. El Collector es la capa de enrutamiento universal para los tres pilares de la observabilidad.

En esta arquitectura, suele ser más sencillo enrutar las tres señales a través del Collector y dejar que envíe las métricas a Prometheus vía OTLP o remote_write, en lugar de dividir las métricas para que vayan directamente y todo lo demás a través del Collector.

Cuándo Usar OTLP Nativo vs. OTel Collector: Marco de Decisión

EscenarioOTLP NativoOTel Collector
Un solo backend de métricas (solo Prometheus)PreferidoInnecesariamente complejo
Múltiples backends de métricasInsuficienteNecesario
Trazas + Logs en scopeNo aplicableNecesario
Transformación/filtrado de métricas necesarioNo soportadoNecesario
Despliegue Kubernetes simplePreferidoComplejidad adicional
Entornos air-gapped / restringidosPreferido (menos componentes)Valorar con cuidado
Mezcla de targets OTel + Prometheus legacyFunciona junto al scrapingPuede normalizar nomenclatura
Alto volumen, necesidad de bufferingControl limitadoPreferido

La recomendación pragmática para la mayoría de los equipos de ingeniería de plataforma: si ya estáis ejecutando el OTel Collector (y deberíais si las trazas están en scope), continuad enrutando las métricas a través de él. Utilizad el exportador otlphttp del Collector para enviar a Prometheus 3.0. Reservad el patrón de SDK directo a Prometheus para servicios simples donde el Collector sería la única razón para añadir complejidad.

Remote Write 2.0: Qué Cambia para Vuestros Setups Existentes

Remote Write 2.0 es una actualización significativa del protocolo con implicaciones operativas reales para equipos que usan Prometheus como fuente de métricas para sistemas de almacenamiento a largo plazo como Thanos, Mimir, VictoriaMetrics o Cortex.

Cambios Clave en el Protocolo

  • Codificación protobuf con compresión snappy — sustituye al formato anterior basado en texto. Típicamente supone una reducción del 50-70% en el tamaño del wire para grandes lotes de métricas
  • Soporte nativo de histogramas en el formato de wire — los histogramas exponenciales pueden ahora enviarse sin convertirlos a histogramas clásicos, preservando la resolución completa
  • Reenvío de metadata — la información de tipo y unidad de métricas se transmite ahora junto a las muestras, habilitando un procesamiento downstream mejor
  • Timestamps de creación — el timestamp en que se creó un counter se reenvía, habilitando cálculos de tasa más precisos tras reinicios

Configurar Remote Write 2.0

# prometheus.yml
remote_write:
  - url: "http://thanos-receive:10908/api/v1/receive"
    # Remote Write 2.0 se negocia automáticamente con receptores compatibles
    # Forzar RW2.0 explícitamente si es necesario:
    send_native_histograms: true
    metadata_config:
      send: true
      send_interval: 1m
    queue_config:
      capacity: 10000
      max_shards: 200
      max_samples_per_send: 2000
      batch_send_deadline: 5s

Remote Write 2.0 usa negociación de protocolo por content — Prometheus 3.0 intentará RW2.0 primero y volverá a RW1.0 si el receptor no lo soporta. Esto significa que las actualizaciones son generalmente retrocompatibles. Verificad que vuestro sistema receptor (Thanos Receive 0.35+, Mimir 2.12+, VictoriaMetrics 1.98+) soporta RW2.0 antes de esperar los beneficios.

Migración desde Prometheus 2.x: Checklist de Cambios Incompatibles

La actualización de Prometheus 2.x a 3.0 requiere atención a varios cambios incompatibles. Este checklist cubre los más significativos operacionalmente para equipos que ejecutan despliegues Prometheus en producción.

Cambios de Configuración

  • Eliminado: cambio del valor por defecto de query.lookback-delta — el valor por defecto cambió de 5 minutos a coincidir con el scrape interval. Las queries que dependían del valor por defecto de 5m pueden devolver resultados diferentes. Auditad las reglas de alerta que usen instant queries sobre counters.
  • Eliminadas: opciones remote_write deprecadas — la semántica de remote_write[].queue_config.capacity cambió. Revisad y actualizad las configuraciones de cola.
  • Eliminado: flag storage.tsdb.allow-overlapping-blocks — el manejo de bloques solapados ahora es automático. Eliminad este flag de vuestros scripts de arranque.
  • Cambio en protocolos de scrape por defecto — Prometheus 3.0 usa por defecto el formato OpenMetrics para el scraping cuando los targets lo soportan. Esto habilita histogramas nativos pero puede revelar diferencias de parsing. Probad el comportamiento si dependíais del comportamiento anterior.
  • Cambios en Agent mode — si usáis Prometheus en modo Agent, revisad las opciones de configuración actualizadas para la gestión del WAL.

Cambios en PromQL

  • Parsing más estricto — algunas expresiones PromQL anteriormente aceptadas pero técnicamente inválidas ahora fallan. Pasad vuestras reglas de alerta por promtool check rules contra un binario de Prometheus 3.0 antes del cutover.
  • Funciones de histograma nativo — nuevas funciones como histogram_fraction() y histogram_quantile() tienen comportamiento actualizado con histogramas nativos. Las queries existentes de dashboards que usen histogram_quantile() sobre histogramas clásicos siguen funcionando sin cambios.

Compatibilidad de Almacenamiento

Prometheus 3.0 puede leer datos TSDB existentes de 2.x. La ruta de actualización no requiere migración de datos. Sin embargo, Prometheus 2.x no puede leer bloques de datos escritos por 3.0 (el downgrade no está soportado sin pérdida de datos una vez que se han producido escrituras). Tomad un snapshot antes de actualizar si necesitáis capacidad de rollback:

# Tomar un snapshot de TSDB antes de actualizar
curl -X POST http://prometheus:9090/api/v1/admin/tsdb/snapshot

# Verificar que el snapshot existe
ls /prometheus/snapshots/

Pasos de Validación Previos a la Actualización

# 1. Validar la configuración contra Prometheus 3.0
docker run --rm -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml \
  prom/prometheus:v3.0.0 \
  promtool check config /etc/prometheus/prometheus.yml

# 2. Validar las reglas de alerta
docker run --rm -v $(pwd)/rules:/etc/prometheus/rules \
  prom/prometheus:v3.0.0 \
  promtool check rules /etc/prometheus/rules/*.yml

# 3. Ejecutar en paralelo (modo shadow) antes del cutover completo
# Desplegar Prometheus 3.0 junto a 2.x, scrapeando los mismos targets
# Comparar resultados de queries entre versiones usando promtool query range

Despliegue Práctico en Kubernetes

A continuación se muestra un despliegue Kubernetes de Prometheus 3.0 preparado para producción con ingesta OTLP habilitada, adecuado como punto de partida para equipos de ingeniería de plataforma.

ConfigMap de Prometheus 3.0

apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-config
  namespace: monitoring
data:
  prometheus.yml: |
    global:
      scrape_interval: 15s
      evaluation_interval: 15s
      external_labels:
        cluster: produccion
        region: eu-west-1

    otlp:
      promote_resource_attributes:
        - service.name
        - service.namespace
        - deployment.environment
        - k8s.namespace.name
        - k8s.pod.name

    rule_files:
      - /etc/prometheus/rules/*.yml

    scrape_configs:
      - job_name: kubernetes-pods
        kubernetes_sd_configs:
          - role: pod
        relabel_configs:
          - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
            action: keep
            regex: "true"
          - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
            action: replace
            target_label: __metrics_path__
            regex: (.+)

    remote_write:
      - url: http://thanos-receive.monitoring.svc.cluster.local:10908/api/v1/receive
        send_native_histograms: true
        metadata_config:
          send: true

Fijaos en el uso de region: eu-west-1 en external_labels. Para equipos en Europa, esta etiqueta es útil no solo para correlación operativa sino también para trazabilidad de datos: saber en qué región reside vuestra telemetría facilita la demostración de cumplimiento del Reglamento General de Protección de Datos (RGPD). En organizaciones bajo supervisión de la Agencia Española de Protección de Datos (AEPD) o cualquier otra autoridad de control europea, poder demostrar que los datos de monitorización no salen de la UE es un requisito real. Etiquetas de región en todas vuestras métricas os dan esa trazabilidad sin esfuerzo adicional.

Deployment de Prometheus 3.0

apiVersion: apps/v1
kind: Deployment
metadata:
  name: prometheus
  namespace: monitoring
spec:
  replicas: 1
  selector:
    matchLabels:
      app: prometheus
  template:
    metadata:
      labels:
        app: prometheus
    spec:
      serviceAccountName: prometheus
      containers:
        - name: prometheus
          image: prom/prometheus:v3.0.0
          args:
            - --config.file=/etc/prometheus/prometheus.yml
            - --storage.tsdb.path=/prometheus/data
            - --storage.tsdb.retention.time=15d
            - --web.enable-lifecycle
            - --web.enable-admin-api
            - --enable-feature=otlp-write-receiver
            - --enable-feature=utf8-names
            - --enable-feature=native-histograms
          ports:
            - name: http
              containerPort: 9090
              protocol: TCP
          volumeMounts:
            - name: config
              mountPath: /etc/prometheus
            - name: data
              mountPath: /prometheus/data
          resources:
            requests:
              cpu: 500m
              memory: 2Gi
            limits:
              cpu: 2000m
              memory: 8Gi
          livenessProbe:
            httpGet:
              path: /-/healthy
              port: http
            initialDelaySeconds: 30
            periodSeconds: 15
          readinessProbe:
            httpGet:
              path: /-/ready
              port: http
            initialDelaySeconds: 5
            periodSeconds: 5
      volumes:
        - name: config
          configMap:
            name: prometheus-config
        - name: data
          persistentVolumeClaim:
            claimName: prometheus-data
---
apiVersion: v1
kind: Service
metadata:
  name: prometheus
  namespace: monitoring
spec:
  selector:
    app: prometheus
  ports:
    - name: http
      port: 9090
      targetPort: http
  type: ClusterIP

Configurar las Aplicaciones para Enviar OTLP

Con este despliegue, cualquier aplicación en el cluster puede enviar métricas OTLP configurando las siguientes variables de entorno (funciona con cualquier SDK OTel que soporte OTLP HTTP):

env:
  - name: OTEL_SERVICE_NAME
    valueFrom:
      fieldRef:
        fieldPath: metadata.labels['app']
  - name: OTEL_SERVICE_NAMESPACE
    valueFrom:
      fieldRef:
        fieldPath: metadata.namespace
  - name: OTEL_METRICS_EXPORTER
    value: "otlp"
  - name: OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
    value: "http://prometheus.monitoring.svc.cluster.local:9090/api/v1/otlp/v1/metrics"
  - name: OTEL_EXPORTER_OTLP_METRICS_PROTOCOL
    value: "http/protobuf"
  - name: OTEL_METRIC_EXPORT_INTERVAL
    value: "30000"
  - name: OTEL_RESOURCE_ATTRIBUTES
    value: "deployment.environment=produccion,k8s.namespace.name=$(NAMESPACE)"

Este enfoque funciona especialmente bien en entornos que usan el OTel Operator para Kubernetes, donde el CRD Instrumentation puede inyectar estas variables de entorno automáticamente en los pods basándose en selectores de namespace o de etiquetas de pod — instrumentación zero-touch con almacenamiento nativo en Prometheus.

Prometheus 3.0 y la Soberanía de Datos en Europa

Un aspecto que suele pasarse por alto en los análisis técnicos de Prometheus 3.0, pero que resulta especialmente relevante para empresas europeas, es cómo la arquitectura de observabilidad interactúa con los requisitos del RGPD.

Cuando las métricas contienen datos que pueden considerarse personales — identificadores de sesión en labels, IPs de usuarios en atributos de recursos, nombres de usuario en trazas correlacionadas — la elección del backend de almacenamiento y su ubicación geográfica tiene implicaciones legales directas. En España y en la UE en general, el RGPD exige que los datos personales no se transfieran a países terceros sin las salvaguardas adecuadas.

Prometheus, al ser autoalojado, ofrece control total sobre dónde residen los datos. Para equipos que trabajan con proveedores cloud europeos — Hetzner, OVHcloud, IONOS, Exoscale — o que gestionan infraestructura propia como un homelab o un cluster on-premises, Prometheus 3.0 con almacenamiento local garantiza que los datos de observabilidad no salen de la jurisdicción europea.

Algunas recomendaciones prácticas para entornos con requisitos de cumplimiento:

  • Usad external_labels con region: eu-west-X en todas las instancias de Prometheus para tener trazabilidad geográfica completa
  • Evitad promover resource attributes que contengan datos personales (IPs de usuario, identificadores de sesión) mediante la lista promote_resource_attributes — filtradlos en el OTel Collector antes de que lleguen a Prometheus
  • Si usáis Thanos o Mimir para almacenamiento a largo plazo, verificad que los buckets de object storage (S3-compatible) están en regiones europeas
  • Documentad vuestros flujos de datos de telemetría en el registro de actividades de tratamiento que exige el RGPD

Preguntas Frecuentes

¿Puedo usar la ingesta OTLP de Prometheus 3.0 para trazas y logs?

No. El receptor OTLP de Prometheus 3.0 gestiona únicamente métricas. Prometheus es un almacén de métricas — no tiene modelo de datos para trazas ni logs. Para trazas necesitáis un backend como Jaeger o Grafana Tempo. Para logs necesitáis Loki, Elasticsearch o un sistema similar. El OTel Collector es la capa de enrutamiento apropiada cuando necesitáis enviar las tres señales a sus respectivos backends desde un único endpoint de push en el lado de la aplicación.

¿El chart Helm de kube-prometheus-stack soporta Prometheus 3.0?

Sí, con matices. El chart kube-prometheus-stack actualizó su imagen de Prometheus a 3.0 a partir de la versión 66.0.0 del chart. Sin embargo, algunas recording rules y alerting rules incluidas pueden necesitar ajuste por los cambios en PromQL y las diferencias de comportamiento por defecto. El propio Prometheus Operator (versión 0.78+) se ha actualizado para soportar las nuevas opciones de configuración incluyendo el bloque de configuración otlp. Si gestionáis Prometheus vía Operator, configuraréis los ajustes OTLP a través de spec.additionalArgs del CRD Prometheus y un recurso PrometheusConfiguration personalizado.

¿Qué ocurre con los nombres de métricas de Prometheus 2.x existentes cuando activo el soporte UTF-8?

Las métricas existentes con nombres basados en guiones bajos siguen funcionando exactamente igual que antes. Activar el soporte UTF-8 es puramente aditivo — permite el almacenamiento y consulta de nombres de métricas que contienen puntos y otros caracteres UTF-8, pero no renombra ni modifica las métricas existentes. Vuestros dashboards, reglas de alerta y recording rules existentes siguen funcionando sin modificación. Solo las métricas ingeridas vía OTLP (o expuestas por exportadores que usen convenciones de nomenclatura OTel) usarán nombres separados por puntos.

¿Cómo afecta la ingesta OTLP nativa al modelo pull de Prometheus?

Coexiste con él. Prometheus 3.0 sigue scrapeando targets vía el modelo pull en el mismo puerto 9090. El endpoint OTLP es una ruta de ingesta adicional, no un reemplazo del scraping. Podéis tener una instancia de Prometheus scrapeando simultáneamente pods de Kubernetes vía service discovery y recibiendo métricas OTLP push de aplicaciones — ambas se almacenan en el mismo TSDB y son consultables vía la misma interfaz PromQL. Este enfoque híbrido es habitual durante las migraciones, donde los componentes legacy se scrapean y los nuevos servicios instrumentados con OTel envían vía OTLP.

¿Es el receptor OTLP de Prometheus 3.0 adecuado para cargas de trabajo de producción de alto volumen?

Para volúmenes moderados, sí. El receptor OTLP es síncrono — la petición HTTP se completa solo después de que las muestras se escriben en el WAL. Bajo tasas de ingesta muy altas (cientos de miles de muestras por segundo), esto puede crear back-pressure que afecta a la latencia de las aplicaciones. El OTel Collector gestiona esto mejor mediante buffering interno, colas de reintentos y procesamiento por lotes. Para escenarios de alto volumen, el patrón recomendado es: las aplicaciones envían al OTel Collector (que confirma inmediatamente y hace buffer), el Collector envía a Prometheus vía OTLP o remote_write en lotes optimizados. Para la mayoría de las cargas de trabajo en Kubernetes — decenas o centenares de servicios con cardinalidad típica de métricas — el receptor OTLP nativo rinde bien sin intermediario.

¿Funciona esto con el Prometheus que viene incluido en kube-prometheus-stack?

Sí. Si ya tenéis kube-prometheus-stack desplegado y queréis añadir ingesta OTLP, la forma más limpia es actualizar el chart a la versión 66.0.0 o posterior y añadir los additionalArgs correspondientes al CRD Prometheus:

# values.yaml para kube-prometheus-stack
prometheus:
  prometheusSpec:
    additionalArgs:
      - name: enable-feature
        value: otlp-write-receiver
      - name: enable-feature
        value: utf8-names
    additionalScrapeConfigs: []

Combinad esto con la configuración otlp.promote_resource_attributes en un PrometheusConfiguration adicional o directamente en el additionalConfig del spec.

¿Cómo migrar de la configuración basada en OpenTelemetry Collector Prometheus exporter a OTLP nativo?

La migración es incremental. Podéis ejecutar ambas configuraciones en paralelo durante la transición:

  1. Habilitad el receptor OTLP en Prometheus 3.0
  2. Configurad vuestras aplicaciones para enviar a ambos endpoints simultáneamente (Collector y Prometheus directo)
  3. Verificad que las métricas aparecen correctamente en Prometheus con los nombres UTF-8 originales
  4. Comparad dashboards y alertas entre ambas fuentes
  5. Una vez validado, eliminad el exportador Prometheus del Collector y simplificad la configuración

La ventaja de esta migración gradual es que podéis verificar que las queries PromQL existentes funcionan con los nuevos nombres (o adaptarlas si fuera necesario) antes de eliminar el path anterior.

Construyendo un Framework de Migración para Kubernetes: Lecciones de Ingress-NGINX

Construyendo un Framework de Migración para Kubernetes: Lecciones de Ingress-NGINX

Si gestionas infraestructura Kubernetes en producción, el anuncio de la depreciación de Ingress-NGINX no fue una sorpresa, sino un recordatorio crudo de la realidad del ecosistema: la obsolescencia es una constante. Lo que hoy es un componente crítico, mañana puede ser un legacy que amenaza la estabilidad y seguridad de tu plataforma.

La depreciación de Ingress-NGINX, un controlador que ha sido la columna vertebral del tráfico entrante para miles de clusters, no es un evento aislado. Es un patrón que se repetirá con otros componentes a medida que Kubernetes evoluciona. La pregunta crítica para los equipos de plataforma no es «¿cómo migramos este componente?», sino «¿cómo construimos un marco de trabajo reutilizable para gestionar estas migraciones de forma sistemática, segura y con mínima interrupción?».

En este artículo, no solo analizaremos las lecciones específicas de Ingress-NGINX, sino que las abstraeremos en un framework de migración para Kubernetes que tu equipo puede aplicar a cualquier cambio mayor: desde la transición a Gateway API, hasta la migración de CNIs, cambios de versión de control-plane, o la adopción de nuevos CRDs. Este framework está diseñado para ingenieros senior, arquitectos y SREs que necesitan un enfoque práctico y metódico para el cambio en entornos empresariales.

Por qué necesitas un framework, no solo un plan ad-hoc

Migrar un componente como el controlador de ingress en un cluster de producción es una operación de alto riesgo. Impacta el tráfico de red, la seguridad (TLS), la observabilidad y, en última instancia, la disponibilidad de tus aplicaciones. Un enfoque ad-hoc, basado en scripts sueltos y manuales, introduce puntos de fallo, hace imposible la validación consistente y dificulta el rollback en caso de problemas.

Un framework proporciona:

  • Consistencia: El mismo proceso para diferentes migraciones.
  • Visibilidad: Estado claro en cada fase.
  • Automatización: Reducción de errores humanos.
  • Seguridad: Puntos de validación y compuertas (gates) definidos.
  • Reversibilidad: Un camino claro para deshacer los cambios.

La documentación oficial de Kubernetes sobre gestión de despliegues enfatiza estrategias como el despliegue gradual y el rollback, principios que son la base de nuestro framework.

Fase 1: Evaluación y Análisis de Impacto

Antes de escribir una línea de código o un manifiesto YAML, debes entender completamente el alcance. Esta fase responde a la pregunta: «¿Qué estamos tocando y qué puede romperse?».

1.1. Inventario de Recursos y Dependencias

Para una migración de ingress, no solo son los objetos Ingress. Debes catalogar:

  • Recursos Directos: Todos los objetos Ingress en todos los namespaces, con sus anotaciones específicas (nginx.ingress.kubernetes.io/*).
  • Configuración Global: ConfigMaps que contienen la configuración del controlador (e.g., nginx-configuration).
  • Dependencias de Infraestructura: Services de tipo LoadBalancer, certificados TLS (manejados por cert-manager o similares), políticas de red (NetworkPolicies).
  • Integraciones de Observabilidad: Dashboards de Grafana, alertas de Prometheus, logs estructurados que dependen de etiquetas específicas de NGINX.

Un comando inicial como kubectl get ingress -A es el punto de partida, pero se necesita un análisis más profundo para capturar anotaciones personalizadas y configuraciones complejas.

1.2. Análisis de Brechas (Gap Analysis)

Compara las capacidades del componente antiguo con las del nuevo. En el caso de migrar de Ingress-NGINX a Gateway API (usando, por ejemplo, un controlador como Gateway API con Envoy o NGINX Kubernetes Gateway), identifica:

  • Funcionalidades equivalentes: ¿El nuevo componente soporta rewrite rules, auth annotations, canary deployments de la misma forma?
  • Paradigmas diferentes: Gateway API separa los roles (Infraestructura vs. Aplicación) mediante GatewayClass, Gateway, y HTTPRoute. Esto es un cambio arquitectónico, no solo una sustitución.
  • Características no soportadas: Algunas anotaciones muy específicas de NGINX pueden no tener un equivalente directo y requerir una solución alternativa.

Este análisis te permitirá categorizar los recursos: aquellos con una migración trivial, aquellos que requieren reconfiguración, y aquellos que pueden ser un bloqueador (show-stopper).

Fase 2: Diseño y Estrategia de Despliegue

Con el alcance definido, diseñamos el cómo. La clave aquí es la coexistencia pacífica y el despliegue incremental.

2.1. Patrón de Despliegue Dual (Dual-Homing)

El patrón más seguro para migrar tráfico de red es ejecutar ambos controladores (el antiguo y el nuevo) en paralelo. Esto permite:

  • Migración por servicio/dominio: Enrutar el tráfico de un host específico (app1.example.com) al nuevo controlador, mientras el resto sigue por el antiguo.
  • Pruebas en producción con tráfico real controlado: Usar un subconjunto de usuarios (por ejemplo, mediante un header HTTP) para enviar tráfico al nuevo ingress.
  • Rollback instantáneo: Basta con reconfigurar el DNS o el balanceador de capa 4 para volver al controlador antiguo.

Como señala el framework de Google para esta migración, esta fase requiere una cuidadosa planificación de la IP del balanceador de carga y la gestión de DNS.

2.2. Automatización de la Conversión de Manifiestos

Convertir manualmente cientos de objetos Ingress es inviable y propenso a errores. Debes crear o utilizar herramientas de conversión. El objetivo no es una conversión 1:1 perfecta (el análisis de brechas ya habrá identificado diferencias), sino generar un artefacto base que luego será revisado y ajustado.

Un script simple en Python o Go puede leer los objetos Ingress existentes y generar los correspondientes HTTPRoute y referencias a Gateway. Este es un ejemplo conceptual de la lógica:

# Pseudocódigo para ilustrar el flujo de conversión
ingress_resources = kubectl_get_all_ingresses()
for ingress in ingress_resources:
    http_route = HTTPRoute()
    http_route.metadata.name = ingress.metadata.name
    http_route.metadata.namespace = ingress.metadata.namespace
    
    for rule in ingress.spec.rules:
        host_rule = HTTPRouteRule()
        host_rule.hostnames = [rule.host]
        # Mapear paths y backends...
        # **Aquí se aplicarían las reglas del Gap Analysis**
        http_route.spec.rules.append(host_rule)
    
    # Gestionar TLS: mapeo a Gateway, no a Secret en HTTPRoute
    if ingress.spec.tls:
        # Nota: En Gateway API, TLS se configura en el recurso Gateway.
        # Esto requiere una estrategia de agrupación por host/Secret.
        pass

La salida de esta automatización debe ser revisada por un ingeniero para los casos complejos antes de aplicarse al cluster.

Fase 3: Implementación y Validación Gradual

Esta es la fase de ejecución, donde el framework impone compuertas (gates) de validación entre cada paso.

3.1. Fase 1: Despliegue del Nuevo Controlador en Modo Pasivo

Instala el nuevo controlador (ej: un operator de Gateway API) pero no lo expongas al tráfico de producción. Configúralo para que escuche en un Service interno o en una IP de LoadBalancer diferente. Valida:

  • Los pods del controlador están Ready.
  • Los CRDs necesarios están instalados.
  • Puedes crear recursos de prueba (HTTPRoute) y el controlador los sincroniza (revisa los logs o el estado del recurso).

3.2. Fase 2: Migración de un Servicio No Crítico (Piloto)

Selecciona un servicio de bajo riesgo, preferiblemente interno o con poco tráfico. Aplica los manifiestos convertidos y configura el DNS o el balanceador de capa 4 para enviar el tráfico de ese host específico al nuevo controlador.

Validaciones post-migración (Checklist):

  • Conectividad básica: ¿Responde el servicio con un código 2xx/3xx?
  • Funcionalidad específica: ¿Funcionan las reglas de rewrite, redirecciones, autenticación?
  • TLS: ¿Los certificados se presentan correctamente? ¿La cadena de confianza es válida?
  • Observabilidad: ¿Aparecen logs en el nuevo formato? ¿Las métricas (peticiones/segundo, latencia) se exponen a Prometheus?
  • Prueba de rollback: Vuelve a enrutar el tráfico al controlador antiguo y confirma que todo sigue funcionando.

3.3. Fase 3: Expansión y Automatización del Pipeline

Tras el éxito del piloto, migra grupos de servicios siguiendo un criterio (por namespace, por equipo dueño, por criticidad). Idealmente, integra este proceso en tu pipeline de GitOps:

  1. El ingeniero modifica la ruta (Kustomize overlay, Helm value) para apuntar al nuevo Gateway.
  2. El cambio se fusiona (merge) y es aplicado por el operador GitOps (ArgoCD, Flux).
  3. Un job de validación post-despliegue ejecuta smoke tests contra el servicio.
  4. Una alerta monitoriza el error rate del servicio migrado; si supera un umbral, puede disparar un rollback automático.

Fase 4: Rollback y Lecciones Aprendidas

Un framework de migración robusto trata el rollback no como un fracaso, sino como una característica esencial del proceso.

4.1. Estrategias de Rollback Granular

Debes poder revertir a diferentes niveles:

  • Rollback por servicio: Reconfigurar el enrutamiento de un host específico al controlador antiguo.
  • Rollback masivo: Tener un script o playbook listo para reenrutar todo el tráfico al controlador antiguo en minutos. Esto implica mantener el controlador antiguo operativo y con capacidad suficiente hasta el final de la ventana de migración.
  • Rollback de configuración: Si el problema está en un manifiesto HTTPRoute erróneo, debes poder revertir el commit en Git y dejar que GitOps sincronice el estado anterior.

4.2. Documentación y Conclusión

Una vez completada la migración (o incluso después de cada fase), documenta:

  • Problemas encontrados y su solución: Este es el conocimiento tribal más valioso.
  • Métricas de éxito: Tiempo total, tasa de error durante la migración, recursos consumidos.
  • Mejoras para el framework: ¿Faltó un punto de validación? ¿Podría automatizarse más un paso?

Esta documentación no es un reporte para la gerencia; es la entrada para refinar el framework y prepararlo para la próxima migración inevitable.

Conclusión: Más allá de Ingress-NGINX

La depreciación de Ingress-NGINX es un caso de estudio perfecto, pero el framework descrito aquí es aplicable a cualquier cambio disruptivo en tu plataforma Kubernetes: la migración a un nuevo CNI, el cambio de una versión mayor del control-plane con APIs obsoletas, la adopción de un nuevo modelo de service mesh, o la consolidación de múltiples clusters.

La constante en Kubernetes es el cambio. La ventaja competitiva de un equipo de plataforma no reside en conocer el componente de moda, sino en poseer un proceso sistemático, automatizado y seguro para adoptar, operar y eventualmente retirar cualquier componente. Invertir en construir y refinar este framework de migración es invertir en la resiliencia y velocidad a largo plazo de tu plataforma. La próxima depreciación importante no será una crisis, sino solo otra ejecución en el playbook de tu equipo.

Helm 4.0 explicado: novedades clave, cambios importantes y cómo prepararte para la migración

Helm 4.0 explicado: novedades clave, cambios importantes y cómo prepararte para la migración

Helm es uno de las principales utilidades dentro del ecosistema de Kubernetes y por tanto la release de una nueva version major, como es Helm 4.0 es algo a tener en consideración porqué sin dudas es algo que se va a tener que analizar, valorar y gestionar en los próximos meses.

Debido a eso, veremos muchos comentarios y artículos alrededor de este tema, por lo que vamos a intentar mostrar algo de luz.

Principales novedades de Helm 4.0

Según el propio proyecto en su anuncio, Helm 4 introduce tres grandes bloques de cambios: nuevo sistema de plugins, mejor integración con Kubernetes ** y modernización interna de SDK y rendimiento**.

Nuevo sistema de plugins (incluye WebAssembly)

Se ha rediseñado por completo del sistema de plugins, haciendo un enfoque especial en la seguridad mediante la introducción de un nuevo runtime WebAssembly que si bien es opcional, es recomendado dado que se ejecutan en un modo «sandbox» que ofrece limites y garantías desde el punto de vista de la seguridad

De todos modos, no hay que preocuparse en exceso, ya que los plugins “clásicos” siguen funcionando, pero el mensaje es claro: para seguridad y extensibilidad, la dirección es Wasm.

Server-Side Apply y mejor integración con otros controladores

Desde esta versión Helm 4 a través del flag --server-side se soporta Server-Side Apply (SSA) que ya se ha convertido en estable desde la versión v1.22 de Kubernetes y permite llevar al lado del servidor las actualizaciones sobre los objetos para evitar conflictos entre diferentes controladores que gestionan los mismos recursos.

También incorporar integración con kstatus para asegurar el estado de un componente de na una manera más fiable que lo que ocurre actualmente con el uso del parámetro --wait

Otras Mejoras Adicionales

Adicionalmente hay otro listado de mejores que siendo de menor calado son saltos cualitativos de importancia como puede ser los siguiente:

  • Instalación por digest en registries OCI: (helm install myapp oci://...@sha256:<digest>)
  • Valores multi-document: puedes pasar varios YAML de values en un único fichero multi-doc, facilitando entornos / overlays complejos.
  • Nuevo argumento --set-json que permite de una manera sencilla pasar estructuras complejas comparado con la solución actual mediante uso del parámetro --set

¿Por qué una major (v4) y no otra minor de la 3.x?

Tal y como explican en la entrada oficial de lanzamiento había features que el equipo no podía introducir en v3 sin romper APIs públicas del SDK y arquitectura interna:

  • Cambio fuerte en el sistema de plugins (WebAssembly, nuevos tipos, integración profunda con el core).
  • Reestructuración de paquetes Go y establecimiento de un SDK estable en helm.sh/helm/v4, incompatible a nivel de código con v3.
  • Introducción y futura evolución de Charts v3, que requieren que el SDK soporte múltiples versiones de API de charts.

Con todo esto, seguir en la rama 3.x habría violado SemVer: el cambio de número mayor está básicamente “pagando” la deuda técnica acumulada para poder avanzar.

Adicionalmente se espera en el futuro una nueva evolución de las charts, pasando de una v2 a una futura v3 que aun no esta completamente definida y actualmente las charts v2 se ejecutan correctamente en esta nueva versión.

¿Tengo que migrar a esta nueva versión?

La respuesta corta es: si. Y posiblemente la respuesta larga sea: sí, y rápido. En el anuncio oficial de Helm 4 especifican el calendario de soporte de Helm 3:

  • Bugfixes de Helm 3 hasta el 8 de julio de 2026.
  • Security fixes de Helm 3 hasta el 11 de noviembre de 2026.
  • No se backportearán nuevas features a Helm 3 durante ese periodo; únicamente se actualizarán librerías de cliente de Kubernetes para soportar nuevas versiones de K8s.

Traducción práctica:

  • Tienes ~1 año para planificar migración tranquila a Helm 4 con soporte de bugs.
  • Después de noviembre de 2026, seguir usando Helm 3 será cada vez más arriesgado desde el punto de vista de seguridad y compatibilidad.

Buenas prácticas para migración

Para realizar la migración hay que recordar que es perfectamente posible y factible tener instaladas ambas versiones en un misma máquina o agente, por lo que se puede hacer una migración «paulatina» para asegurar que se llega al final del soporte de la versión v3 con todo migrado correctamente, y para eso se recomiendan lo siguientes pasos:

  • Realizar un análisis de todos los comandos y uso de Helm tanto desde el punto de vista de pipelines de integración, scripts de upgrade o incluso la importación de librerías cliente de helm en desarrollos basados en Helm.
  • Especialmente revisar con cautela todos los usos de --post-renderer, helm registry login, --atomic, --force.
  • Posteriormente al análisis empezar a probar Helm 4 primero en entornos no productivos, reutilizando los mismos charts y values, revirtiendo a Helm 3 en caso de que se detecte un problema hasta que este sea sub
  • Si tienes plugins críticos, probarlos explícitamente con Helm 4 antes de hacer el cambio global.

Problemas con los Ingress en Kubernetes: limitaciones reales y por qué Gateway API es el futuro

Problemas con los Ingress en Kubernetes: limitaciones reales y por qué Gateway API es el futuro

Introducción

Los Ingress han sido, desde las primeras versiones de Kubernetes, la forma más habitual de exponer aplicaciones al exterior. Aunque su diseño inicial era sencillo y elegante, el éxito de Kubernetes y la creciente complejidad de los casos de uso han convertido al Ingress en una pieza problemática: limitado, inconsistente entre vendors y difícil de gobernar en entornos empresariales.

En este artículo analizamos por qué los Ingress se han convertido en una fuente constante de fricción, cómo han influido los distintos Ingress Controllers en esta situación y por qué cada vez más organizaciones están considerando alternativas como Gateway API.

Qué son los Ingress y por qué fueron diseñados así

El ecosistema de Ingress gira en torno a dos recursos principales:

🏷️ IngressClass

Define qué controlador gestionará los Ingress asociados. Su alcance es global al clúster, por lo que suele ser administrado por el equipo de plataforma.

🌐 Ingress

Es el recurso que los desarrolladores usan para exponer un servicio. Permite definir rutas, dominios, certificados TLS y poco más.

Su especificación es mínima por diseño, lo que permitió una adopción rápida, pero también sembró las bases de los problemas actuales.

El problema: un estándar demasiado simple para necesidades complejas

A medida que Kubernetes se convirtió en estándar empresarial, los usuarios querían replicar configuraciones avanzadas de proxies tradicionales: re-escrituras, timeouts, cabeceras personalizadas, CORS, etc.
Pero Ingress no daba soporte nativo a todo esto.

Los vendors reaccionaron… y ahí nació el caos.

Anotaciones vs CRDs: dos caminos incompatibles

Los distintos Ingress Controllers han tomado caminos muy diferentes para añadir capacidades avanzadas:

📝 Anotaciones (NGINX, HAProxy…)

Ventajas:

  • Flexibles y fáciles de usar
  • Directamente en el recurso Ingress

Desventajas:

  • Cientos de anotaciones propietarias
  • Documentación fragmentada
  • Configuraciones no portables entre vendors

📦 CRDs personalizados (Traefik, Kong…)

Ventajas:

  • Más estructurado y potente
  • Mejor validación y control

Desventajas:

  • Añade nuevos objetos no estándar
  • Requiere instalarlos y gestionarlos
  • Menor interoperabilidad

¿Resultado?
Infraestructuras profundamente acopladas a un vendor, lo que complica migraciones, auditorías y automatización.

La complejidad para los equipos de desarrollo

El diseño de Ingress implica dos responsabilidades muy diferenciadas:

  • Plataforma: define IngressClass
  • Aplicación: define Ingress

Pero la realidad es que el desarrollador acaba tomando decisiones que deberían ser responsabilidad del área de plataforma:

  • Certificados
  • Políticas de seguridad
  • Reglas de reescritura
  • CORS
  • Timeouts
  • Prácticas corporativas de naming

Esto provoca:

  • Configuraciones inconsistentes
  • Cuellos de botella en revisiones
  • Dependencia constante entre equipos
  • Falta de estandarización efectiva

En empresas grandes, donde la seguridad y la gobernanza son críticas, esto es especialmente problemático.

NGINX Ingress: la descomisión que reavivó el debate

La reciente descontinuación del NGINX Ingress Controller ha puesto en evidencia lo frágil del ecosistema:

  • Miles de clústeres dependen de él
  • Múltiples proyectos usan sus anotaciones
  • Migrar implica reescribir configuraciones enteras

Esto ha reactivado la conversación sobre la necesidad de un estándar real… y ahí aparece Gateway API.

Gateway API: una alternativa prometedora (pero no perfecta)

Gateway API nace para resolver muchas de las limitaciones de Ingress:

  • Separación clara entre responsabilidades (infraestructura vs aplicación)
  • Extensibilidad estandarizada
  • Más tipos de rutas (HTTPRoute, TCPRoute…)
  • Mayor expresividad sin depender de anotaciones propietarias

Pero también trae desafíos:

  • Requiere adopción gradual
  • No todos los vendors implementan lo mismo
  • La migración no es trivial

Aun así, se perfila como el futuro de la gestión de tráfico en Kubernetes.

Conclusión

Los Ingress han sido fundamentales para el éxito de Kubernetes, pero su propia simplicidad los ha llevado a convertirse en un cuello de botella. La falta de interoperabilidad, las diferencias entre vendors y la compleja gobernanza en entornos empresariales hacen evidente que es momento de adoptar modelos más maduros.

Gateway API no es perfecto, pero avanza en la dirección correcta.
Las organizaciones que quieran estabilidad a futuro deberían empezar a planificar su transición.

📚 Want to dive deeper into Kubernetes? This article is part of our comprehensive Kubernetes Architecture Patterns guide, where you’ll find all fundamental and advanced concepts explained step by step.

Ingress vs Routes en OpenShift: Diferencias, Traducción y Cuándo Usar Cada Uno

Ingress vs Routes en OpenShift: Diferencias, Traducción y Cuándo Usar Cada Uno

Introducción: OpenShift, la plataforma de Kubernetes empresarial de Red Hat, tiene su propia forma de exponer servicios a clientes externos mediante Routes. Comprender la relación entre Ingress de Kubernetes y Routes de OpenShift es crucial para arquitectos y SREs que gestionan cargas de trabajo en esta plataforma. En Kubernetes estándar, normalmente se usa un recurso Ingress junto con un controlador de Ingress para enrutar el tráfico externo a los servicios. Sin embargo, OpenShift introdujo el concepto de Route y un router integrado (basado en HAProxy) mucho antes de que existiera Ingress en Kubernetes. Hoy OpenShift admite tanto Routes como objetos Ingress estándar, lo que a veces puede generar confusión sobre cuándo usar cada uno y cómo se relacionan. Este artículo explora cómo OpenShift maneja los recursos Ingress, cómo se traducen en Routes, las limitaciones de este enfoque y ofrece orientación sobre cuándo utilizar Ingress frente a Routes.

Routes de OpenShift: Capacidades Avanzadas del Router HAProxy Las Routes son recursos específicos de OpenShift diseñados para exponer servicios externamente. Son atendidas por el router de OpenShift, un proxy basado en HAProxy que se ejecuta dentro del clúster. Las Routes admiten funciones avanzadas como:

  • Balanceo de carga con backends ponderados para dividir el tráfico.
  • Sesiones persistentes (afinidad de sesión).
  • Varios modos de terminación TLS (edge, passthrough, re‑encrypt).
  • Subdominios comodín.
  • Certificados personalizados y SNI.
  • Enrutamiento basado en rutas. Debido a que las Routes son nativas de OpenShift, el router comprende estas funciones de forma nativa y puede configurarse en consecuencia. Esta integración estrecha permite capacidades de enrutamiento potentes y flexibles adaptadas a entornos de OpenShift.

OpenShift admite los recursos Ingress estándar de Kubernetes. Cuando se crea un Ingress, la plataforma lo traduce internamente en una Route equivalente. Cuando creas un Ingress, OpenShift lo traduce automáticamente en una Route equivalente tras bastidores. Esto significa que puedes usar manifiestos Ingress estándar y OpenShift se encargará de exponer tus servicios externamente creando Routes en consecuencia. Esta traducción automática simplifica la migración y admite casos de uso básicos sin necesidad de manifestos específicos de Route. El comportamiento se puede ajustar con anotaciones compatibles con el router de OpenShift (por ejemplo, route.openshift.io/termination, haproxy.router.openshift.io/balance, etc.).

Sin embargo, usar Ingress para generar Routes tiene limitaciones clave: muchas funciones avanzadas del router de OpenShift, como los backends ponderados y las sesiones persistentes (sticky sessions), requieren anotaciones específicas de Route (route.openshift.io) y no son compatibles a través de la especificación estándar de Ingress. Otros modos de TLS, como passthrough y re‑encrypt, también requieren anotaciones específicas de Route. Los Ingress sin host no crean una Route; las Routes requieren un nombre de host. Los hosts comodín solo son compatibles mediante Routes. Además, algunas anotaciones de Route no tienen equivalentes en Ingress. El soporte de protocolos también es más limitado en Ingress, que solo admite HTTP/HTTPS, mientras que las Routes pueden manejar protocolos no HTTP con TLS de paso.

La elección entre Ingress y Routes depende de tus requisitos: utiliza Ingress si buscas portabilidad entre plataformas Kubernetes, ya tienes manifiestos de Ingress existentes o tu aplicación usa solo enrutamiento HTTP/HTTPS básico. Utiliza Routes si necesitas funciones avanzadas como backends ponderados, sesiones persistentes o múltiples modos de terminación TLS; si tu despliegue es específico de OpenShift y puede aprovechar funciones nativas de la plataforma; si necesitas exponer protocolos no HTTP o usar hosts comodín o anotaciones personalizadas no compatibles con Ingress. Muchas veces, los equipos usan una combinación: Ingress para portabilidad y Routes para funciones avanzadas o específicas de OpenShift.

Conclusión: En OpenShift, los recursos Ingress de Kubernetes se convierten automáticamente en Routes, lo que permite exponer servicios externos con poco esfuerzo. Esto permite a los usuarios aprovechar manifiestos de Kubernetes existentes y mantener la portabilidad. Sin embargo, para escenarios de enrutamiento avanzados y para aprovechar plenamente las potentes funciones del router de OpenShift, se recomienda utilizar Routes directamente. Ambas opciones coexisten sin problemas en OpenShift, lo que te permite elegir la herramienta adecuada según los requisitos de tu aplicación.

Frequently Asked Questions

¿OpenShift crea automáticamente una Route por cada Ingress de Kubernetes?

Sí, OpenShift traduce automáticamente un recurso Ingress de Kubernetes estándar en una Route equivalente. Sin embargo, esto solo ocurre si el Ingress especifica un host. Los Ingress sin host no generarán una Route en la plataforma.

¿Cuándo debo usar Routes nativas en lugar de Ingress en OpenShift?

Debes usar Routes directamente cuando necesites: terminación TLS en modo passthrough o re-encrypt, balanceo de carga con backends ponderados, sesiones persistentes (sticky sessions), hosts comodín, o exponer protocolos no HTTP (como TCP). Estas son funciones avanzadas del router HAProxy de OpenShift.

¿Puedo usar anotaciones de Route en un objeto Ingress?

Algunas anotaciones del router de OpenShift (prefijadas con haproxy.router.openshift.io/ o route.openshift.io/) pueden funcionar si se añaden a un recurso Ingress, ya que este se convertirá en una Route. Sin embargo, no todas las funcionalidades están garantizadas. Para un control total y predecible, se recomienda usar el recurso Route directamente.

¿Es mejor usar Ingress para mantener la portabilidad entre Kubernetes y OpenShift?

Usar Ingress es una buena estrategia para mantener la portabilidad de manifiestos entre distribuciones de Kubernetes estándar y OpenShift, siempre que tus necesidades se limiten a enrutamiento HTTP/HTTPS básico. Si necesitas funciones específicas de OpenShift, deberás usar Routes, lo que reduce la portabilidad pero maximiza las capacidades de la plataforma.