Cómo limpiar ConfigMaps y Secrets huérfanos en Kubernetes

Cómo limpiar ConfigMaps y Secrets huérfanos en Kubernetes

Kubernetes Housekeeping: Cómo Limpiar ConfigMaps y Secrets Huérfanos

El mantenimiento de un cluster Kubernetes en producción va mucho más allá de desplegar aplicaciones y escalar pods. Con el tiempo, uno de los problemas más silenciosos que acumula cualquier cluster en uso real es el de los recursos huérfanos: ConfigMaps y Secrets que en algún momento sirvieron para algo, pero que hoy llevan semanas o meses sin ser referenciados por ningún workload activo.

Kubernetes no elimina automáticamente ConfigMaps ni Secrets cuando se borra el Deployment, StatefulSet o Job que los utilizaba. El resultado es predecible: acumulación progresiva de basura en etcd, superficie de ataque innecesaria con credenciales obsoletas todavía accesibles, y un ruido operativo que complica cualquier diagnóstico.

Este artículo cubre las causas habituales de este problema, métodos de detección —tanto manuales como automatizados—, estrategias de prevención y, sobre todo, cómo ejecutar la limpieza de forma segura en entornos reales.


Por qué se acumulan ConfigMaps y Secrets huérfanos

Antes de limpiar, conviene entender cómo se llega a esta situación. No es un caso de negligencia puntual: es el resultado natural de varios patrones de trabajo habituales.

Actualizaciones de aplicaciones sin limpieza del historial

Cada vez que se actualiza la configuración de una aplicación —cambio de variables de entorno, rotación de credenciales, nueva URL de base de datos— es habitual crear un nuevo ConfigMap o Secret con nombre versionado. El problema: el anterior no se borra.

Despliegues eliminados que dejan su configuración atrás

Cuando un equipo hace kubectl delete deployment myapp, el Deployment desaparece, pero los ConfigMaps y Secrets asociados permanecen. Kubernetes no establece esa relación de ciclo de vida salvo que se configure explícitamente.

Rollouts fallidos

Un rollout que falla a mitad puede dejar una versión de ConfigMap preparada para una versión de la aplicación que nunca llegó a arrancar. Esa configuración queda flotando en el namespace indefinidamente.

Flujos de trabajo en desarrollo

Los entornos de desarrollo y staging son especialmente propensos: pruebas rápidas, experimentos, recursos creados a mano sin etiquetas ni propietario declarado. Muy pocos equipos tienen disciplina para limpiar detrás de ellos.

Pipelines CI/CD con nombres únicos por hash

Herramientas como Helm y Kustomize pueden generar ConfigMaps con sufijos de hash basados en el contenido (myapp-config-7d4f9c). Cada actualización genera un nuevo recurso. Sin un mecanismo de pruning activo, los anteriores se acumulan indefinidamente.


El impacto real de no hacer housekeeping

No es solo una cuestión de orden. Los recursos huérfanos tienen consecuencias concretas:

Seguridad degradada. Un Secret huérfano puede contener credenciales antiguas, claves de API o certificados que ya no deberían ser accesibles para ningún proceso. Siguen siendo válidos en muchos casos, y siguen ocupando espacio en etcd —que no está cifrado por defecto en todos los clusters.

Consumo de etcd. Cada ConfigMap y Secret ocupa espacio en la base de datos de estado del cluster. etcd tiene límites de tamaño (el valor por defecto es 2 GB), y alcanzarlo en producción es una emergencia operativa real. Los recursos huérfanos aceleran ese límite sin aportar ningún valor.

Ruido operativo. Un namespace con cien ConfigMaps activos y doscientos huérfanos es un namespace difícil de operar. Localizar la configuración correcta, depurar un problema de arranque, revisar qué credenciales están en uso… todo se complica.

Coste de cognitive load para el equipo. No es trivial: los equipos que trabajan en namespaces desordenados cometen más errores, tardan más en diagnosticar incidencias y tienen más resistencia a hacer cambios.


Detección: encontrar los recursos huérfanos

Detección manual con kubectl y jq

El enfoque básico consiste en cruzar la lista de ConfigMaps y Secrets de un namespace contra las referencias activas en los Pods en ejecución.

Script para ConfigMaps:

#!/bin/bash
# detect-orphaned-configmaps.sh
# Identifica ConfigMaps no referenciados por ningún Pod activo

NAMESPACE=${1:-default}

echo "Comprobando ConfigMaps huérfanos en namespace: $NAMESPACE"
echo "---"

# Obtener todos los ConfigMaps del namespace
CONFIGMAPS=$(kubectl get configmaps -n $NAMESPACE -o jsonpath='{.items[*].metadata.name}')

for cm in $CONFIGMAPS; do
    # Omitir kube-root-ca.crt — gestionado por el sistema
    if [[ "$cm" == "kube-root-ca.crt" ]]; then
        continue
    fi

    # Comprobar si algún Pod referencia este ConfigMap
    REFERENCED=$(kubectl get pods -n $NAMESPACE -o json | \
        jq -r --arg cm "$cm" '.items[] |
        select(
            (.spec.volumes[]?.configMap.name == $cm) or
            (.spec.containers[].env[]?.valueFrom.configMapKeyRef.name == $cm) or
            (.spec.containers[].envFrom[]?.configMapRef.name == $cm)
        ) | .metadata.name' | head -1)

    if [[ -z "$REFERENCED" ]]; then
        echo "Huérfano: $cm"
    fi
done

Script para Secrets:

#!/bin/bash
# detect-orphaned-secrets.sh

NAMESPACE=${1:-default}

echo "Comprobando Secrets huérfanos en namespace: $NAMESPACE"
echo "---"

SECRETS=$(kubectl get secrets -n $NAMESPACE -o jsonpath='{.items[*].metadata.name}')

for secret in $SECRETS; do
    # Omitir tokens de service account y Secrets del sistema
    SECRET_TYPE=$(kubectl get secret $secret -n $NAMESPACE -o jsonpath='{.type}')
    if [[ "$SECRET_TYPE" == "kubernetes.io/service-account-token" ]]; then
        continue
    fi

    # Comprobar si algún Pod referencia este Secret
    REFERENCED=$(kubectl get pods -n $NAMESPACE -o json | \
        jq -r --arg secret "$secret" '.items[] |
        select(
            (.spec.volumes[]?.secret.secretName == $secret) or
            (.spec.containers[].env[]?.valueFrom.secretKeyRef.name == $secret) or
            (.spec.containers[].envFrom[]?.secretRef.name == $secret) or
            (.spec.imagePullSecrets[]?.name == $secret)
        ) | .metadata.name' | head -1)

    if [[ -z "$REFERENCED" ]]; then
        echo "Huérfano: $secret"
    fi
done

Importante: Estos scripts comprueban únicamente Pods en ejecución. Un ConfigMap puede estar referenciado en un Deployment cuyo Pod está temporalmente caído, en un CronJob que no ha disparado recientemente, o en un template de Job. Antes de borrar cualquier recurso, es imprescindible cruzar también contra Deployments, StatefulSets, DaemonSets, CronJobs y Jobs.

Herramientas automatizadas

Para clusters con muchos namespaces o workflows de auditoría recurrente, los scripts manuales no escalan. Hay herramientas específicas para esta tarea.

Kor

Kor es una herramienta de código abierto diseñada específicamente para detectar recursos sin usar en Kubernetes. Tiene soporte nativo para ConfigMaps, Secrets, ServiceAccounts, Services y varios recursos más.

# Instalación en macOS
brew install kor

# Escanear ConfigMaps y Secrets sin usar en un namespace
kor all --namespace production --output json

# Solo ConfigMaps
kor configmap --namespace production

# Solo Secrets, excluyendo namespaces de sistema
kor secret --namespace production --exclude-namespaces kube-system,kube-public

La salida en formato JSON es especialmente útil para integrar el análisis en pipelines de CI/CD o dashboards de monitorización.

Popeye

Popeye genera informes completos de salud del cluster, incluyendo recursos huérfanos, configuraciones incorrectas, y buenas prácticas no seguidas.

# Instalación
brew install derailed/popeye/popeye

# Escaneo completo con salida en JSON
popeye --output json --save

# Escaneo centrado en un namespace
popeye --namespace production

Popeye es especialmente valioso para una primera auditoría de un cluster heredado, porque da una visión global del estado de salud en múltiples dimensiones, no solo recursos huérfanos.


Prevención: evitar que se acumulen en primer lugar

Detectar y limpiar es necesario, pero el objetivo real es que los recursos tengan un ciclo de vida gestionado desde el principio. Hay varias estrategias complementarias.

Owner References: limpieza automática en cascada

El mecanismo más potente de Kubernetes para gestión de ciclo de vida es ownerReferences. Si un ConfigMap tiene como propietario un Deployment, cuando ese Deployment se borra, el garbage collector de Kubernetes elimina automáticamente el ConfigMap.

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: production
  ownerReferences:
    - apiVersion: apps/v1
      kind: Deployment
      name: myapp
      uid: d9607e19-f88f-11e6-a518-42010a800195
      controller: true
      blockOwnerDeletion: true
data:
  app.properties: |
    database.url=postgres://db:5432

Las herramientas de gestión de paquetes como Helm y Kustomize gestionan esto automáticamente para los recursos que crean. Si estás aplicando manifiestos directamente con kubectl apply, es responsabilidad del equipo añadir estas referencias.

Etiquetado consistente: visibilidad y control

Las etiquetas son la base de cualquier operación de gestión masiva en Kubernetes. Sin etiquetas consistentes, no hay forma de hacer consultas selectivas ni de atribuir un recurso a su propietario.

apiVersion: v1
kind: ConfigMap
metadata:
  name: api-gateway-config-v2
  labels:
    app: api-gateway
    component: configuration
    version: v2
    managed-by: argocd
    owner: platform-team
data:
  config.yaml: |
    # configuración aquí

Con etiquetas bien definidas, las operaciones de limpieza se simplifican enormemente:

# Todos los ConfigMaps de una aplicación
kubectl get configmaps -l app=api-gateway

# Eliminar versiones antiguas
kubectl delete configmaps -l app=api-gateway,version=v1

Conviene definir un estándar de etiquetado a nivel de organización y hacerlo cumplir con políticas de admisión (ver sección de OPA Gatekeeper más adelante).

GitOps con pruning habilitado

Las herramientas GitOps como ArgoCD mantienen el estado del cluster sincronizado con el repositorio Git. Cuando se habilita el pruning, cualquier recurso que desaparezca del repositorio también se elimina del cluster automáticamente.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp
spec:
  syncPolicy:
    automated:
      prune: true      # Eliminar recursos que no estén en Git
      selfHeal: true

Esta es probablemente la estrategia de prevención más eficaz para equipos que trabajan con GitOps: el cluster siempre refleja exactamente lo que está en Git, sin residuos.

Kustomize configMapGenerator con pruning

Kustomize tiene un generador nativo de ConfigMaps que añade un sufijo de hash basado en el contenido. Esto garantiza que cualquier cambio en la configuración genere un nuevo ConfigMap (y que los Pods hagan rolling update automáticamente). Combinado con --prune, las versiones antiguas se eliminan al aplicar la nueva.

kubectl apply --prune -k ./overlays/production \
  -l app=myapp

Este patrón es especialmente útil en pipelines CI/CD donde la configuración cambia frecuentemente.

Resource Quotas como mecanismo de presión

Establecer cuotas de recursos por namespace crea una presión natural para que los equipos mantengan el orden:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: config-quota
  namespace: production
spec:
  hard:
    configmaps: "50"
    secrets: "50"

Cuando un equipo alcanza el límite, se ve obligado a revisar qué hay en el namespace y eliminar lo que ya no sirve. No es la solución más elegante, pero funciona como red de seguridad.


Ejecución de la limpieza: de lo manual a lo automatizado

Limpieza manual puntual

Para una primera limpieza de un cluster heredado, el proceso recomendado es:

# 1. Generar listado de candidatos a eliminar
./detect-orphaned-configmaps.sh production > orphaned-cms.txt
cat orphaned-cms.txt

# 2. Revisar el listado manualmente antes de actuar

# 3. Eliminar los confirmados
for cm in $(cat orphaned-cms.txt | grep "Huérfano:" | awk '{print $2}'); do
    kubectl delete configmap $cm -n production
done

La revisión manual es imprescindible en la primera ejecución. Los scripts de detección basados en Pods activos pueden tener falsos positivos para recursos referenciados por workloads caídos temporalmente.

Backup antes de cualquier borrado

Antes de eliminar nada en producción, siempre:

# Backup de todos los ConfigMaps del namespace
kubectl get configmap -n production -o yaml > cm-backup-$(date +%Y%m%d).yaml

# Solo eliminar ConfigMaps con más de 30 días de antigüedad
kubectl get configmap -n production -o json | \
  jq -r --arg date "$(date -d '30 days ago' -u +%Y-%m-%dT%H:%M:%SZ)" \
  '.items[] | select(.metadata.creationTimestamp < $date) | .metadata.name' | \
  while read cm; do
    echo "Candidato a eliminar: $cm (creado: $(kubectl get cm $cm -n production -o jsonpath='{.metadata.creationTimestamp}'))"
    # Descomentar para eliminar realmente:
    # kubectl delete configmap $cm -n production
  done

El filtro por antigüedad es una capa de seguridad adicional: un ConfigMap reciente tiene más probabilidades de estar en uso aunque no aparezca referenciado en ningún Pod activo en ese momento.

CronJob para limpieza periódica automatizada

Una vez que el proceso manual está validado, automatizarlo con un CronJob es el paso natural:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: configmap-cleanup
  namespace: kube-system
spec:
  schedule: "0 2 * * 0"  # Domingos a las 2:00 AM
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: cleanup-sa
          containers:
          - name: cleanup
            image: bitnami/kubectl:latest
            command:
            - /bin/bash
            - -c
            - |
              echo "Iniciando limpieza de ConfigMaps..."

              for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
                echo "Revisando namespace: $ns"

                # ConfigMaps referenciados por workloads activos
                REFERENCED_CMS=$(kubectl get deploy,sts,ds -n $ns -o json | \
                  jq -r '.items[].spec.template.spec |
                  [.volumes[]?.configMap.name,
                   .containers[].env[]?.valueFrom.configMapKeyRef.name,
                   .containers[].envFrom[]?.configMapRef.name] |
                  .[] | select(. != null)' | sort -u)

                ALL_CMS=$(kubectl get cm -n $ns -o jsonpath='{.items[*].metadata.name}')

                for cm in $ALL_CMS; do
                  if [[ "$cm" == "kube-root-ca.crt" ]]; then
                    continue
                  fi

                  if ! echo "$REFERENCED_CMS" | grep -q "^$cm$"; then
                    echo "Eliminando ConfigMap huérfano: $cm en namespace: $ns"
                    kubectl delete cm $cm -n $ns
                  fi
                done
              done
          restartPolicy: OnFailure

El CronJob necesita los permisos adecuados para listar y borrar recursos en todos los namespaces:

# ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
  name: cleanup-sa
  namespace: kube-system
---
# ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: cleanup-role
rules:
- apiGroups: [""]
  resources: ["configmaps", "secrets", "namespaces"]
  verbs: ["get", "list", "delete"]
- apiGroups: ["apps"]
  resources: ["deployments", "statefulsets", "daemonsets"]
  verbs: ["get", "list"]
---
# ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: cleanup-binding
subjects:
- kind: ServiceAccount
  name: cleanup-sa
  namespace: kube-system
roleRef:
  kind: ClusterRole
  name: cleanup-role
  apiGroup: rbac.authorization.k8s.io

Integración en CI/CD

Para pipelines que despliegan con versionado explícito (etiquetas Git, versiones semánticas), la limpieza se puede integrar directamente como paso post-deploy:

# Ejemplo en GitLab CI
cleanup_old_configs:
  stage: post-deploy
  image: bitnami/kubectl:latest
  script:
    - |
      # Eliminar ConfigMaps de versiones antiguas tras despliegue exitoso
      kubectl delete configmap -n production \
        -l app=myapp,version!=${CI_COMMIT_TAG}

    - |
      # Conservar solo las últimas 3 versiones por timestamp
      kubectl get configmap -n production \
        -l app=myapp \
        --sort-by=.metadata.creationTimestamp \
        -o name | head -n -3 | xargs -r kubectl delete -n production
  only:
    - tags
  when: on_success

Este enfoque es muy limpio: cada despliegue exitoso elimina las versiones anteriores, manteniendo un historial mínimo pero suficiente para un rollback rápido.


Patrones avanzados para clusters en producción

OPA Gatekeeper: forzar etiquetado desde admisión

La forma más efectiva de garantizar que todos los ConfigMaps tengan etiquetas de ciclo de vida es hacerlo obligatorio en el momento de su creación, a nivel de admission webhook. OPA Gatekeeper permite definir estas políticas como código:

apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: configmaprequiredlabels
spec:
  crd:
    spec:
      names:
        kind: ConfigMapRequiredLabels
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package configmaprequiredlabels

        violation[{"msg": msg}] {
          input.review.kind.kind == "ConfigMap"
          not input.review.object.metadata.labels["app"]
          msg := "Los ConfigMaps deben tener la etiqueta 'app' para gestión de ciclo de vida"
        }

        violation[{"msg": msg}] {
          input.review.kind.kind == "ConfigMap"
          not input.review.object.metadata.labels["owner"]
          msg := "Los ConfigMaps deben tener la etiqueta 'owner' para gestión de ciclo de vida"
        }

Con esta política activa, ningún ConfigMap sin las etiquetas app y owner puede crearse en el cluster. Esto resuelve el problema en origen.

Monitorización con Prometheus

Para clusters grandes, la visibilidad continua del estado de recursos huérfanos es fundamental. Un exporter personalizado puede exponer métricas para Prometheus:

apiVersion: v1
kind: ConfigMap
metadata:
  name: orphan-detection-exporter
data:
  script.sh: |
    #!/bin/bash
    # Exponer métricas para scraping de Prometheus
    while true; do
      echo "# HELP k8s_orphaned_configmaps Número de ConfigMaps huérfanos"
      echo "# TYPE k8s_orphaned_configmaps gauge"

      for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
        count=$(./detect-orphaned-configmaps.sh $ns | grep -c "Huérfano:")
        echo "k8s_orphaned_configmaps{namespace=\"$ns\"} $count"
      done

      sleep 300  # Actualizar cada 5 minutos
    done

Con las métricas disponibles en Prometheus, se puede configurar una alerta que notifique cuando el número de ConfigMaps huérfanos supera un umbral:

groups:
- name: kubernetes-housekeeping
  rules:
  - alert: ConfigMapsHuerfanosExcesivos
    expr: k8s_orphaned_configmaps > 20
    for: 24h
    labels:
      severity: warning
    annotations:
      summary: "Alto número de ConfigMaps huérfanos en {{ $labels.namespace }}"
      description: "El namespace {{ $labels.namespace }} tiene {{ $value }} ConfigMaps huérfanos"

Políticas de limpieza en flota multi-cluster

En entornos con múltiples clusters, gestionar las políticas de limpieza de forma centralizada es un requisito. Crossplane permite definir estas políticas como recursos declarativos que se aplican a toda la flota:

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: cluster-cleanup-policy
spec:
  compositeTypeRef:
    apiVersion: platform.example.com/v1
    kind: ClusterCleanupPolicy
  resources:
    - name: cleanup-cronjob
      base:
        apiVersion: kubernetes.crossplane.io/v1alpha1
        kind: Object
        spec:
          forProvider:
            manifest:
              apiVersion: batch/v1
              kind: CronJob

Este patrón es especialmente valioso cuando el equipo de plataforma necesita garantizar que todos los clusters de la organización siguen las mismas políticas de mantenimiento, sin depender de que cada equipo recuerde configurarlo manualmente.


Plan de implementación

Acciones inmediatas (hoy)

  1. Auditar el estado actual. Ejecutar los scripts de detección en todos los namespaces relevantes. Guardar el resultado.
  2. Hacer backup. Exportar todos los ConfigMaps y Secrets a YAML antes de tocar nada.
  3. Eliminar los obvios. Recursos con más de 90 días de antigüedad sin ninguna referencia en ningún workload son candidatos seguros. Eliminar con revisión manual previa.

Corto plazo (1-4 semanas)

  1. Definir estándar de etiquetado. Acordar con los equipos qué etiquetas son obligatorias (app, owner, version, managed-by).
  2. Añadir owner references a los manifiestos existentes que no las tengan.
  3. Desplegar CronJobs de detección. Empezar solo con detección (sin borrado), para generar visibilidad.
  4. Integrar limpieza en CI/CD para los pipelines con versionado explícito.

Medio plazo (1-3 meses)

  1. Adoptar GitOps con pruning habilitado en ArgoCD o Flux.
  2. Implementar OPA Gatekeeper para forzar etiquetado en admisión.
  3. Configurar alertas en Prometheus sobre métricas de recursos huérfanos.
  4. Establecer quotas por namespace como mecanismo de presión secundario.

Ongoing

  • Revisar semanalmente los informes de Popeye o Kor en los namespaces clave.
  • Incluir revisión de recursos huérfanos en la planificación de sprints del equipo de plataforma.
  • Incorporar la gestión de ciclo de vida de ConfigMaps y Secrets en el onboarding de nuevos desarrolladores.

Conclusión

Los ConfigMaps y Secrets huérfanos son uno de esos problemas de Kubernetes que parecen menores hasta que no lo son: un cluster con años de historial puede tener miles de recursos obsoletos, consumiendo etcd, exponiendo credenciales que deberían estar rotadas, y generando confusión operativa en cada incidencia.

La buena noticia es que el problema tiene solución clara. No se trata de una operación de limpieza masiva única —eso solo resuelve el síntoma— sino de un enfoque por capas que combina:

  • Prevención estructural mediante owner references, etiquetado consistente y GitOps con pruning.
  • Detección continua con herramientas como Kor, Popeye o un exporter Prometheus personalizado.
  • Limpieza controlada con scripts auditados, backups previos, y criterios de antigüedad antes de borrar.

Un cluster bien mantenido no es el que nunca acumula residuos —es imposible evitarlo en entornos dinámicos— sino el que tiene mecanismos para detectarlos, contenerlos y eliminarlos de forma sistemática y segura.

La gestión del ciclo de vida de los recursos de configuración es una práctica de ingeniería de plataforma, no una tarea de limpieza puntual. Cuanto antes se integre en los procesos del equipo, menos deuda técnica operativa se acumula.

Gateway API Kubernetes 2026: Evaluación crítica del soporte por proveedor

Gateway API Kubernetes 2026: Evaluación crítica del soporte por proveedor

El Gateway API de Kubernetes ha dejado de ser una promesa para convertirse en el estándar presente para la gestión de tráfico. Con las APIs estables de Ingress NGINX ya deprecadas —señal inequívoca del cambio de paradigma—, los equipos de plataforma deben decidir qué proveedor de Gateway API adoptar. La página oficial de implementaciones lista numerosas opciones, pero la realidad muestra soporte fragmentado, estabilidad variable y lagunas significativas que afectan especialmente a estrategias multi-cluster.

Esta evaluación va más allá de los checklists de marketing para analizar el soporte práctico de Gateway API en los principales proveedores cloud, ingress controllers y service meshes. Se examina qué versiones son realmente production-ready, los problemas de interoperabilidad que nadie menciona en los keynotes, y qué deberías valorar antes de estandarizar tu infraestructura.


El espectro de madurez del Gateway API: de Experimental a Standard

No todos los recursos del Gateway API tienen el mismo nivel de madurez. El modelo de versionado único de esta API —con funcionalidades que progresan por los tracks Experimental, Standard y Extended— implica que el soporte por proveedor es inherentemente desigual. Una implementación puede tener soporte completo para los recursos estables Gateway y HTTPRoute mientras ofrece respaldo solo parcial o experimental para GRPCRoute o TCPRoute.

Esto genera un dilema fundamental: diseñar para el mínimo común denominador o aceptar restricciones específicas de cada proveedor. La decisión depende de mapear con precisión los requisitos de gestión de tráfico —HTTP, terminación TLS, gRPC, balanceo TCP/UDP— contra lo que cada proveedor ofrece en forma estable.

Soporte de la API core: los cimientos

La mayoría de los proveedores soportan ya las versiones v1 (GA) de los recursos fundamentales:

  • GatewayClass y Gateway: Soporte prácticamente universal para v1. Son los recursos del plano de control para provisionar y configurar load balancers.
  • HTTPRoute: Soporte universal para v1. Es el caballo de batalla del enrutamiento HTTP/HTTPS y el recurso más estable del ecosistema.

El soporte para otros tipos de rutas revela la fragmentación real:

  • GRPCRoute: Con frecuencia en beta o experimental. Crítico para arquitecturas de microservicios modernas, pero aún sin fiabilidad universal.
  • TCPRoute y UDPRoute: Soporte irregular. Algunos proveedores los implementan como beta; otros los ignoran directamente, forzando a usar anotaciones propias o recursos custom.
  • TLSRoute: Habitualmente vinculado a integraciones específicas de gestión de certificados, como cert-manager.

Esta heterogeneidad no es un bug —es consecuencia lógica de una especificación que avanza por capas—, pero tiene implicaciones prácticas enormes cuando planificas una migración desde Ingress o cuando diseñas una plataforma multi-tenant.


Análisis en profundidad por proveedor: la realidad de las implementaciones

AWS Elastic Kubernetes Service (EKS)

AWS dispone de un controlador oficial de Gateway API para EKS. El soporte es pragmático pero actualmente limitado en alcance.

Recursos soportados: GatewayClass, Gateway, HTTPRoute y GRPCRoute. GRPCRoute usa v1beta1, lo que indica que no ha alcanzado estabilidad GA. Este detalle importa: adoptar recursos en beta en producción es una decisión de riesgo que conviene documentar y monitorizar.

Infraestructura subyacente: El controlador mapea directamente a AWS Application Load Balancer (ALB) y Network Load Balancer (NLB). Es a la vez una fortaleza —aprovechas servicios AWS gestionados con todos sus SLAs— y una restricción, ya que heredas los límites de ALB/NLB en cuanto a funcionalidades y configuración.

Laguna crítica: No hay soporte para TCPRoute ni UDPRoute. Las cargas de trabajo que requieren balanceo TCP/UDP puro deben recurrir al tipo LoadBalancer de Kubernetes Service, o a un ingress controller adicional junto al controlador de Gateway API. El resultado es un modelo de gestión fragmentado: parte del tráfico bajo Gateway API, parte bajo primitivas legacy. Para equipos que buscan coherencia operativa, esto es un obstáculo real.

Observabilidad: La integración con CloudWatch y AWS X-Ray existe, pero la correlación entre recursos del Gateway API y trazas/métricas granulares requiere configuración adicional. No es plug-and-play.

Google Kubernetes Engine (GKE)

GKE ha integrado el soporte del Gateway API directamente en su oferta de Kubernetes gestionado, con foco en la infraestructura de load balancing global de Google Cloud.

El controlador GKE Gateway soporta recursos v1 y provisionará Google Cloud Global External Load Balancers. La integración con la gestión de certificados de Google y con Cloud CDN es una ventaja diferencial real —si ya vives en el ecosistema GCP. Para funcionalidades de enrutamiento avanzado, puede que necesites backend configs específicos de GCP, lo que introduce acoplamiento con la plataforma.

Punto fuerte: La capacidad de usar load balancers globales con anycast significa que un único Gateway puede servir tráfico optimizado por latencia a nivel mundial. Para aplicaciones con usuarios distribuidos geográficamente, esto tiene valor operativo considerable.

Punto débil: La portabilidad sufre. Las configuraciones que aprovechan integraciones específicas de GCP son difícilmente trasladables a otro proveedor o a on-premises sin reescribir la capa de política.

Azure Kubernetes Service (AKS)

AKS proporciona el Application Gateway Ingress Controller (AGIC) con soporte para Gateway API, mapeando sobre Azure Application Gateway. El soporte para tipos de ruta más recientes, como GRPCRoute, ha ido históricamente por detrás de otros proveedores.

La experiencia en AKS con Gateway API tiene una característica particular: la integración con Azure AD Workload Identity y el modelo de permisos de Azure puede simplificar ciertos flujos de autenticación, pero también introduce complejidad en el modelo de delegación del Gateway. GatewayClass, cuando apunta a recursos de Azure Application Gateway, hereda las limitaciones de configuración del WAF de Azure —positivo si ya tienes WAF habilitado, problemático si necesitas políticas que Application Gateway no soporta nativamente.

NGINX Ingress Controller

Con las APIs estables de Ingress deprecadas en favor de Gateway API, la implementación de Gateway API de NGINX es ya el camino principal hacia adelante para este controlador. Y aquí es donde se diferencia fundamentalmente de los proveedores cloud:

Al no estar limitado por el producto de load balancing propietario de ningún proveedor cloud, NGINX tiene soporte generalmente excelente para el rango completo de recursos tanto experimentales como estándar. Esto lo convierte en una opción sólida para despliegues híbridos o multi-cloud que requieren paridad de funcionalidades entre entornos.

Para plataformas on-premises o híbridas, NGINX es actualmente el candidato más maduro. La consistencia entre un clúster en AWS, otro en Azure y tus propios nodos on-prem es algo que los controladores cloud nativos simplemente no pueden ofrecer.

La contrapartida: asumes tú la carga operativa del controlador. No hay un equipo de SREs de cloud gestionándolo; eres tú quien gestiona las actualizaciones, los incidentes y la capacidad.

Kong Ingress Controller

Kong ha sido un adoptador temprano y comprensivo del Gateway API, con frecuencia implementando funcionalidades con rapidez. Aprovecha el extenso ecosistema de plugins de Kong Gateway, lo que puede ser un gran atractivo —pero también introduce vendor lock-in.

El modelo de extensión de Kong mediante plugins es potente: rate limiting, autenticación JWT, transformaciones de request/response, logging a sistemas externos. Todo esto es accesible desde el Gateway API mediante KongPlugin CRDs. El problema es que esas KongPlugin CRDs son específicas de Kong; si en el futuro necesitas migrar a otro controlador, esa capa de política no viene contigo.

Para organizaciones que ya tienen inversión en Kong Gateway y quieren unificar la gestión bajo Gateway API, es una elección natural. Para los demás, la dependencia del ecosistema Kong es un factor de riesgo a ponderar.


Lagunas críticas para arquitectos enterprise

Más allá de verificar qué recursos están soportados, existen lagunas más profundas que impactan los despliegues en producción, especialmente en entornos complejos.

1. Soporte multi-cluster y entornos híbridos

La especificación del Gateway API incluye conceptos como ReferenceGrant para enrutamiento cross-namespace y, en el horizonte, cross-cluster. En la práctica, muy pocos proveedores tienen una historia multi-cluster robusta y production-ready. La mayoría de las implementaciones asumen un único clúster como unidad de operación.

Para arquitecturas que abarcan múltiples clústeres —aislamiento por equipo, distribución geográfica, dominios de fallo—, la realidad actual obliga a:

  • Gestionar recursos Gateway separados por clúster.
  • Usar un load balancer global externo (DNS/GSLB) para distribuir tráfico entre los gateways de cada clúster.
  • Mantener configuraciones de política potencialmente divergentes entre clústeres.

Esto niega parte de la promesa de la API de proporcionar configuración unificada y abstraída. El mapa de ruta existe —el KEP de multi-cluster Gateway está en progreso—, pero en 2026 todavía no es algo en lo que puedas apoyarte en producción sin trabajo de integración significativo.

2. Policy Attachment y consistencia de extensiones

El Gateway API está diseñado para extensión mediante policy attachment: rate limiting, reglas WAF, autenticación. No existe ningún estándar sobre cómo se implementan estas políticas. Un proveedor puede usar un CRD RateLimitPolicy custom; otro confía en anotaciones; un tercero usa un motor de políticas separado.

El resultado es deriva de configuración masiva y vendor lock-in, rompiendo el objetivo de portabilidad que es uno de los argumentos centrales a favor del Gateway API. Si tienes diez equipos desplegando aplicaciones en tres proveedores distintos, mantener coherencia en las políticas de seguridad se convierte en un problema de gobernanza que el Gateway API en su estado actual no resuelve por sí solo.

La solución pragmática: define una capa de abstracción interna —plantillas Helm, módulos Terraform, o una plataforma interna— que traduzca políticas de negocio a configuración específica de proveedor. Más trabajo, pero te da control sobre la deriva.

3. Interfaces de observabilidad y depuración

Aunque la API define campos status, la riqueza de datos operacionales varía enormemente: logs de error detallados, métricas granulares vinculadas a recursos de la API, integración de trazas distribuidas. Algunos proveedores exponen integración profunda con su stack de monitorización; otros ofrecen visibilidad mínima.

Esta asimetría tiene consecuencias reales para los equipos SRE. En un incidente de producción, la diferencia entre un proveedor que expone métricas por HTTPRoute —latencia, tasa de error, distribución de códigos de respuesta— y uno que solo expone métricas agregadas del load balancer puede significar horas de diferencia en el tiempo de resolución.

Antes de comprometerte con un proveedor, valida específicamente:

  • ¿Hay métricas por recurso HTTPRoute o solo por gateway?
  • ¿Los logs incluyen el nombre del HTTPRoute que procesó la request?
  • ¿Hay integración nativa con OpenTelemetry o necesitas sidecars adicionales?
  • ¿Las alertas se pueden definir contra condiciones del Gateway API o solo contra métricas de infraestructura?

Framework de evaluación: preguntas para tu equipo

Antes de seleccionar un proveedor, trabaja este checklist técnico. No es exhaustivo, pero cubre los vectores de decisión que más frecuentemente se ignoran:

1. Requisitos de ruta
¿Necesitamos soporte estable solo para HTTP, o también para gRPC, TCP, UDP? ¿Es aceptable soporte en beta para rutas no-HTTP? ¿Con qué frecuencia añadiremos nuevos tipos de workload?

2. Modelo de infraestructura
¿Queremos un load balancer gestionado por el cloud (más sencillo, menos control) o un controlador basado en clúster (más portable, mayor carga operativa)? ¿Quién en el equipo va a operar y actualizar este controlador?

3. Futuro multi-cluster
¿Nuestra arquitectura es hoy de un único clúster pero probablemente crecerá? ¿El proveedor tiene un roadmap creíble para Gateway API multi-cluster? ¿Cómo de dolorosa sería la migración si necesitamos cambiar en 18 meses?

4. Necesidades de política
¿Qué políticas avanzadas son necesarias: autenticación, WAF, rate limiting? ¿Cómo las implementa el proveedor? ¿Podemos convivir con CRDs de política específicas del proveedor, o eso rompe nuestra estrategia de portabilidad?

5. Observabilidad y depuración
¿Qué logging, métricas y trazas se exponen para los recursos del Gateway API? ¿Se integran con nuestra plataforma de observabilidad existente (Grafana, Datadog, Dynatrace)?

6. Ruta de actualización
¿Cuál es el historial del proveedor en adoptar nuevas versiones del Gateway API? ¿Cuánto tardó en soportar v1 de HTTPRoute tras su GA? ¿Hay breaking changes entre versiones menores?


Recomendaciones estratégicas

Basadas en el panorama actual, estas son las rutas pragmáticas según contexto:

Para despliegues en un único cloud: empieza con el controlador nativo de tu proveedor cloud (AWS, GKE, AKS). Es el camino de menor resistencia y ofrece la mejor integración con el resto de servicios cloud (IAM, certificados, monitorización). Sé consciente de sus limitaciones específicas respecto a tipos de ruta no soportados y documenta los workarounds desde el principio.

Para híbrido, multi-cloud o on-premises: estandariza sobre un controlador portable basado en clúster, como Ingress-NGINX o Kong. La consistencia entre entornos te ahorrará complejidad operativa significativa, aunque implique renunciar a ciertas integraciones cloud-native. En un entorno donde tienes tres clouds y dos datacenters propios, la coherencia de configuración vale más que la integración óptima con cualquier proveedor individual.

Para proyectos greenfield: diseña tus aplicaciones y configuraciones solo contra los recursos estables v1 (Gateway, HTTPRoute). Trata cualquier uso de recursos beta/experimental como riesgo conocido que puede requerir refactoring posterior. Esta disciplina es especialmente importante si anticipas que el proyecto durará más de dos años —el panorama del Gateway API seguirá evolucionando.

Siempre ten un plan de salida: aísla los YAMLs de configuración del Gateway API de las políticas y anotaciones específicas del proveedor. Usa namespaces o directorios separados para la configuración portable vs. la específica. Esta modularidad hará que cualquier migración futura sea considerablemente menos dolorosa. La historia del Ingress API nos enseñó que las abstracciones de Kubernetes evolucionan —planifica para ello.


Conclusión

La evolución del Gateway API es un avance neto para el ecosistema Kubernetes, ofreciendo un modelo de expresividad muy superior al Ingress original. Sin embargo, en 2026 el panorama de proveedores sigue madurando. El soporte es amplio pero no profundo, y las lagunas críticas en gestión multi-cluster y portabilidad de políticas persisten.

El arquitecto que tenga éxito será el que elija proveedor basándose en cómo sus restricciones y capacidades específicas se alinean con los patrones de tráfico inmediatos de su organización y la estrategia de plataforma a largo plazo. La era de la configuración Gateway API universal —write-once-run-anywhere— no ha llegado todavía. Pero con una selección informada y deliberada de proveedor, y con la disciplina de diseñar contra los recursos estables del core, puedes construir una base sólida para cuando esa promesa se materialice.

La migración desde Ingress no es un evento puntual sino un proceso que se extiende durante meses o años en organizaciones grandes. Cuanto antes elijas deliberadamente en lugar de dejarte llevar por el camino de menor resistencia, más control tendrás sobre los trade-offs. Y en infraestructura de plataforma, el control sobre los trade-offs es exactamente lo que distingue una arquitectura robusta de una que genera deuda técnica silenciosa.


¿Estás evaluando Gateway API para tu plataforma Kubernetes? ¿Qué proveedor has elegido y por qué? Cuéntamelo en los comentarios o en LinkedIn.

Prometheus Alertmanager vs Grafana Alerting (2026): Arquitectura, diferencias y cuándo usar cada uno

Prometheus Alertmanager vs Grafana Alerting (2026): Arquitectura, diferencias y cuándo usar cada uno

En entornos de observabilidad en producción es habitual acabar con dos sistemas de alertas corriendo en paralelo: Prometheus Alertmanager gestionando las alertas basadas en métricas, y Grafana Alerting cubriendo el resto de necesidades de notificación. El resultado es el problema clásico de consolidación de alertas: páginas duplicadas al equipo de guardia, reglas de silencio dispersas entre dos plataformas, y ningún sistema con autoridad clara sobre el ciclo de vida de las alertas.

La elección entre ambas herramientas depende de tu ecosistema de datasources, la madurez de tu flujo GitOps y el enfoque de gestión de guardias. Los dos son productos maduros en producción, pero abordan el problema desde ángulos fundamentalmente distintos.

Visión general de la arquitectura

Prometheus Alertmanager: el componente independiente

Alertmanager opera como un componente dedicado y desacoplado. No evalúa reglas de alerta — esa responsabilidad recae en sistemas externos (Prometheus, Thanos Ruler, Cortex, Mimir Ruler), que evalúan expresiones PromQL y envían las alertas activas a la API de Alertmanager mediante HTTP POST.

Modelo de configuración: un único fichero YAML (alertmanager.yml) que contiene el árbol de routing, las definiciones de receivers, las reglas de inhibition y las plantillas de silencio. Sin base de datos, sin estado gestionado por interfaz — configuración puramente declarativa, controlable por versiones, que facilita la reproducibilidad total y los flujos GitOps.

Modelo de alta disponibilidad: múltiples instancias de Alertmanager forman un clúster basado en gossip usando el protocolo mesh, compartiendo el estado de silencios y notificaciones entre nodos. El failover ocurre sin notificaciones duplicadas ni pérdidas.

Grafana Alerting: la plataforma integrada

Grafana Alerting embebe el ciclo de vida completo de las alertas — evaluación de reglas, gestión de estado, routing y notificación — dentro del propio proceso de Grafana. Internamente utiliza un fork del Alertmanager de Prometheus para el routing y la notificación, aunque este detalle de implementación resulta invisible para el usuario.

Capacidad diferencial: evalúa reglas de alerta directamente, consultando cualquier datasource compatible — Prometheus, Loki, Elasticsearch, CloudWatch, PostgreSQL y más de 100 plugins de datasource. Las reglas, contact points, políticas de notificación y mute timings se almacenan en la base de datos de Grafana o se provisionan mediante YAML o API.

Alta disponibilidad: en entornos self-hosted se utiliza base de datos compartida y peer-discovery entre instancias de Grafana. Grafana Cloud proporciona HA totalmente gestionada.


Comparativa de características

CaracterísticaPrometheus AlertmanagerGrafana Alerting
DatasourcesSolo compatibles con PrometheusCualquier datasource de Grafana
Evaluación de reglasExterna (Prometheus/Ruler)Integrada
Árbol de routingYAML jerárquicoPolíticas de notificación con label matchers
Agrupacióngroup_by, group_wait, group_intervalControles equivalentes mediante políticas
InhibitionReglas nativas de inhibitionSoportado desde v10.3, menos flexible
SilenciosBasados en labels, limitados en el tiempoMute timings + silencios ad hoc
Canales de notificaciónEmail, Slack, PagerDuty, OpsGenie, webhook, etc.Los anteriores más Teams, Discord, Google Chat, LINE y más
PlantillasGo templatesGo templates + variables de Grafana
Multi-tenancyNo integradoNativo mediante organizaciones + RBAC
Alta disponibilidadClúster basado en gossipRespaldado por base de datos con peer discovery
Modelo de configuraciónFichero YAML únicoUI + API + YAML de provisionamiento
Compatibilidad GitOpsExcelenteRequiere ficheros de provisionamiento o Terraform
Servicio gestionadoGrafana Cloud (Mimir), Amazon Managed PrometheusGrafana Cloud

Puntos fuertes de Alertmanager

Configuración declarativa, nativa para GitOps

Toda la configuración reside en un único fichero YAML sin estado oculto en ninguna base de datos. Los cambios pasan por pull requests, siguen el mismo proceso de revisión de código que el resto de la infraestructura, y se despliegan mediante pipelines CI/CD exactamente igual que cualquier otro componente de infraestructura como código.

# alertmanager.yml — todo en un único fichero
global:
  resolve_timeout: 5m
  slack_api_url: "https://hooks.slack.com/services/T00/B00/XXX"

route:
  receiver: platform-team
  group_by: [alertname, cluster, namespace]
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
    - match:
        severity: critical
      receiver: pagerduty-oncall
      group_wait: 10s
    - match_re:
        team: "^(payments|checkout)$"
      receiver: payments-slack
      continue: true

receivers:
  - name: platform-team
    slack_configs:
      - channel: "#platform-alerts"
  - name: pagerduty-oncall
    pagerduty_configs:
      - service_key: ""
  - name: payments-slack
    slack_configs:
      - channel: "#payments-oncall"

inhibit_rules:
  - source_match:
      severity: critical
    target_match:
      severity: warning
    equal: [alertname, cluster]

Cada cambio es completamente auditable. Los rollbacks se reducen a un git revert. La herramienta amtool permite validar la configuración en los pipelines de CI antes de desplegar.

Ligero y con una única responsabilidad

Alertmanager hace una sola cosa: enrutar y entregar notificaciones. Sin dashboards, sin motor de consultas, sin plugins de datasource. Este diseño de responsabilidad única garantiza un consumo de recursos mínimo — una instancia modesta gestiona miles de alertas activas con pocos cientos de megabytes de memoria. El tiempo de arranque se mide en milisegundos; la carga operativa es prácticamente inexistente.

Inhibition y routing maduros

Las reglas de inhibition son elementos de primera clase en la configuración de Alertmanager. Suprimen warnings secundarios cuando ya hay alertas críticas activas, evitando las tormentas de alertas que tanto daño hacen a los equipos de guardia. El árbol de routing jerárquico con flags continue permite entregas matizadas: enviar simultáneamente al canal del equipo Y escalar a PagerDuty con agrupaciones distintas en cada nivel.

Alta disponibilidad probada en producción

El clúster HA basado en gossip lleva años demostrando su estabilidad. Tres réplicas detrás de un load balancer (o con service discovery de Kubernetes) proporcionan notificaciones fiables sin almacenamiento compartido. El protocolo gestiona la deduplicación automáticamente, sin configuración adicional.


Puntos fuertes de Grafana Alerting

Reglas de alerta multi-datasource

Este es el diferenciador más potente de Grafana Alerting. Las reglas pueden consultar Loki para detectar picos de errores en logs, CloudWatch para la utilización de recursos AWS, Elasticsearch para errores de aplicación, o PostgreSQL para métricas de negocio — todo desde un único sistema de alertas centralizado.

# Ejemplo de provisionamiento de regla de alerta en Grafana
# Alerta basada en tasa de errores en Loki
apiVersion: 1
groups:
  - orgId: 1
    name: application-errors
    folder: Production
    interval: 1m
    rules:
      - uid: loki-error-spike
        title: "Alta tasa de errores en el servicio de pagos"
        condition: C
        data:
          - refId: A
            datasourceUid: loki-prod
            model:
              expr: 'sum(rate({app="payment-service"} |= "ERROR" [5m]))'
          - refId: B
            datasourceUid: "__expr__"
            model:
              type: reduce
              expression: A
              reducer: last
          - refId: C
            datasourceUid: "__expr__"
            model:
              type: threshold
              expression: B
              conditions:
                - evaluator:
                    type: gt
                    params: [10]
        for: 5m
        labels:
          severity: warning
          team: payments

Alertmanager no puede hacer esto — recibe únicamente alertas pre-evaluadas y no tiene concepto de datasource.

Interfaz unificada para la gestión de alertas

Una sola interfaz gestiona la creación de reglas de alerta, su visualización, la configuración de políticas de notificación, los contact points y la gestión de silencios. Para equipos que prefieren la configuración visual frente a editar YAML, esto reduce significativamente la barrera de entrada. Los ingenieros pueden ver el estado de las alertas activas, el historial de evaluación y los caminos de routing de notificaciones directamente en el navegador, sin necesidad de conocer la sintaxis de configuración.

Multi-tenancy nativo y RBAC

El modelo de organizaciones de Grafana y el control de acceso basado en roles se extienden de forma natural al sistema de alertas. Distintos equipos gestionan sus propias reglas de alerta, contact points y políticas de notificación dentro del ámbito de su organización o carpeta, con aislamiento completo y sin visibilidad cruzada entre equipos. Conseguir esto con Alertmanager autónomo requeriría instancias separadas por tenant o el Alertmanager multi-tenant de Mimir.

Mute timings y planificación más rica

Mientras que Alertmanager soporta silencios (supresiones ad hoc, limitadas en el tiempo), Grafana Alerting añade los mute timings: ventanas temporales recurrentes que suprimen notificaciones. Esto permite mantenimientos programados, alertas solo en horario laboral, o la supresión de alertas no críticas los fines de semana. Con Alertmanager, gestionar estos escenarios requiere herramientas externas o la creación manual de silencios recurrentes.

Grafana Cloud como opción gestionada

Para equipos que quieren evitar la gestión de infraestructura de alertas self-hosted, Grafana Cloud proporciona Grafana Alerting completamente gestionado con HA, persistencia de estado y entrega de notificaciones. El stack de Grafana Cloud incluye también Mimir Alertmanager gestionado, que permite alertas nativas de Prometheus mientras se aprovecha la infraestructura administrada.


Cuándo usar Prometheus Alertmanager

Alertmanager es la opción óptima cuando:

  • Tu stack de métricas es nativo de Prometheus. Todas las reglas de alerta usan expresiones PromQL evaluadas por Prometheus, Thanos Ruler o Mimir Ruler. No existe ningún valor añadido en enrutar las notificaciones a través de Grafana.
  • GitOps es innegociable. Cada cambio de infraestructura requiere revisión mediante pull request y configuración completamente declarativa. El modelo de fichero único de Alertmanager simplifica enormemente la gestión frente al estado respaldado por base de datos de Grafana. Herramientas como amtool permiten validar la configuración en los pipelines de CI.
  • Necesitas routing granular con inhibition. Árboles de routing complejos con múltiples niveles de agrupación, reglas de inhibition y flags continue se expresan de forma más natural en el formato YAML de Alertmanager. Esta lógica lleva años estable y documentada.
  • Ejecutas microservicios con routing por equipo. Cuando cada equipo es propietario de su subárbol de routing con lógica compleja, el modelo jerárquico de Alertmanager escala mejor que la configuración guiada por interfaz gráfica. Los equipos gestionan su sección de configuración mediante CODEOWNERS en Git.
  • Quieres mínima carga operativa. Alertmanager es un binario único con requisitos de recursos mínimos — sin backups de base de datos, migraciones ni actualizaciones de frameworks de interfaz.

Cuándo usar Grafana Alerting

Grafana Alerting es la opción óptima cuando:

  • Alertas sobre más que métricas Prometheus. Reglas de alerta basadas en logs de Loki, queries de Elasticsearch, métricas de CloudWatch o consultas a bases de datos requieren Grafana Alerting. La alternativa — ejecutar herramientas de alertas separadas por datasource — es peor en todos los sentidos.
  • Tu equipo prefiere configuración visual. No todos los ingenieros quieren editar árboles de routing en YAML. Si tu organización valora las interfaces visuales para la gestión de alertas y contact points, la interfaz de Grafana proporciona ventajas de productividad significativas.
  • Usas Grafana Cloud. Si ya estás en Grafana Cloud, usar el sistema de alertas integrado es el camino de menor resistencia: HA, entrega gestionada de notificaciones y experiencia unificada sin infraestructura adicional.
  • El multi-tenancy es un requisito. Varios equipos necesitan configuraciones de alertas aisladas con RBAC. El modelo nativo de organizaciones y carpetas de Grafana es significativamente más sencillo que ejecutar instancias Alertmanager por tenant.
  • Necesitas mute timings para ventanas de mantenimiento recurrentes. Equipos que suprimen alertas regularmente durante ventanas programadas (despliegues, procesamiento por lotes, supresión de no-críticos en fin de semana) encontrarán los mute timings de Grafana mucho más ergonómicos que gestionar silencios recurrentes manualmente.

Ejecutar ambos a la vez: el patrón híbrido

Muchos entornos de producción ejecutan deliberadamente ambos sistemas con límites de responsabilidad claramente definidos.

Arquitectura híbrida habitual

  • Prometheus Alertmanager gestiona todas las alertas basadas en métricas. Reglas PromQL evaluadas por Prometheus o rulers de almacenamiento a largo plazo (Thanos, Mimir). Alertmanager es propietario del routing, la agrupación y la notificación.
  • Grafana Alerting gestiona las alertas no-Prometheus: alertas basadas en logs de Loki, métricas de negocio desde datasources SQL, reglas de correlación multi-datasource.

Los límites de propiedad deben estar documentados y ser explícitos:

# Límites de propiedad para alertas híbridas

# Prometheus Alertmanager gestiona:
#   - Todas las reglas de alerta basadas en PromQL
#   - Alertas de infraestructura (node, kubelet, etcd, CoreDNS)
#   - Alertas SLO/SLI de aplicaciones basadas en métricas

# Grafana Alerting gestiona:
#   - Reglas de alerta basadas en logs (Loki, Elasticsearch)
#   - Alertas de métricas de negocio (datasources SQL)
#   - Reglas de correlación multi-datasource
#   - Alertas para equipos que prefieren gestión por interfaz

# Compartido:
#   - Contact points / receivers usan los mismos canales Slack y servicios PagerDuty
#   - Las rotaciones de guardia se gestionan externamente (PagerDuty, Grafana OnCall)

Ambos sistemas entregan a los mismos canales de notificación. La disciplina crítica: asegurarse de que los silencios y las ventanas de mantenimiento se aplican en los dos sistemas cuando sea necesario. Este es el principal coste operativo del enfoque híbrido — si silencias en uno, no olvides silenciar en el otro.

Grafana como visualizador de Alertmanager

Incluso usando Alertmanager en exclusiva para el routing, Grafana puede actuar como visualizador de solo lectura. Grafana soporta nativamente datasources externos de Alertmanager, lo que permite ver las alertas activas, los silencios vigentes y los grupos de alertas dentro de la interfaz de Grafana — visibilidad operativa completa sin mover la lógica de alertas.

# Provisionamiento de datasource de Grafana para Alertmanager externo
apiVersion: 1
datasources:
  - name: Alertmanager
    type: alertmanager
    url: http://alertmanager.monitoring.svc:9093
    access: proxy
    jsonData:
      implementation: prometheus

Este patrón es especialmente útil en equipos donde los SREs gestionan Alertmanager mediante GitOps, pero los desarrolladores prefieren consumir el estado de las alertas desde la interfaz de Grafana sin necesidad de acceso directo a Alertmanager.


Consideraciones de migración

Migrar de Alertmanager a Grafana Alerting

  • Conversión de reglas: las reglas de alerta y recording basadas en PromQL en los ficheros de reglas de Prometheus deben recrearse como reglas de alerta de Grafana. Grafana proporciona herramientas de migración para importar reglas en formato Prometheus, aunque las expresiones complejas requieren ajuste manual.
  • Traducción del árbol de routing: el árbol de routing jerárquico de Alertmanager se mapea a las políticas de notificación de Grafana, pero la semántica difiere. El comportamiento del flag continue y las rutas por defecto pueden comportarse de forma distinta — prueba el routing exhaustivamente antes de dar por buena la migración.
  • Migración de silencios e inhibition: los silencios activos son efímeros y no requieren migración. Las reglas de inhibition deben recrearse en el formato de Grafana. Las ventanas de mantenimiento recurrentes se convierten en mute timings.
  • Ejecuta ambos en paralelo primero: la estrategia de migración más segura consiste en ejecutar ambos sistemas durante dos a cuatro semanas, enviando notificaciones desde los dos, y cortando cuando haya confianza suficiente en Grafana. Acepta el ruido temporal de alertas duplicadas — es mucho menos costoso que perder páginas críticas durante la migración.

Migrar de Grafana Alerting a Alertmanager

  • Limitación de datasources: solo migran las alertas con datasources compatibles con Prometheus. Las alertas que consultan Loki, Elasticsearch o datasources SQL no tienen equivalente en Alertmanager — requieren soluciones alternativas o simplemente quedan fuera del alcance de la migración.
  • Exportación de reglas: exporta las reglas de alerta de Grafana y conviértelas a ficheros de reglas en formato Prometheus. La API de Grafana (GET /api/v1/provisioning/alert-rules) proporciona salida estructurada apta para transformación mediante scripts.
  • Mapeo de contact points: mapea los contact points de Grafana a receivers de Alertmanager — formatos distintos, conceptos equivalentes.
  • Pérdida de estado: Alertmanager no hereda el historial de evaluación de Grafana. Empiezas desde cero. Planifica un periodo breve en el que algunas alertas pueden re-dispararse mientras Prometheus evalúa reglas que antes gestionaba Grafana.

Marco de decisión

Árbol de decisión rápido:

Punto de partida:
│
├── ¿Alertas sobre datasources no-Prometheus (Loki, ES, SQL, CloudWatch)?
│   ├── SÍ → Grafana Alerting (al menos para esos datasources)
│   └── NO ↓
│
├── ¿GitOps / configuración declarativa es un requisito innegociable?
│   ├── SÍ → Alertmanager
│   └── NO ↓
│
├── ¿Necesitas multi-tenancy con RBAC?
│   ├── SÍ → Grafana Alerting (o Mimir Alertmanager)
│   └── NO ↓
│
├── ¿Estás en Grafana Cloud?
│   ├── SÍ → Grafana Alerting (camino de menor resistencia)
│   └── NO ↓
│
└── Por defecto → Alertmanager (más simple, más ligero, más probado)

La respuesta honesta de muchos equipos es “ambos”: Alertmanager para las métricas nativas de Prometheus, Grafana Alerting para todo lo demás. Es una arquitectura perfectamente válida siempre que los límites de propiedad estén documentados y el equipo de guardia sepa exactamente dónde buscar y, sobre todo, dónde silenciar.


Diferencias clave que a menudo se pasan por alto

Más allá de las características que aparecen en cualquier comparativa, hay diferencias operativas que solo se descubren trabajando con ambas herramientas en producción.

El estado de evaluación es radicalmente distinto

Con Alertmanager, el estado de evaluación de las reglas vive en Prometheus (o en Thanos/Mimir Ruler). Alertmanager solo ve alertas que ya han disparado — nunca las reglas en sí ni su historial de evaluación. Grafana Alerting, en cambio, expone el historial completo de evaluación de cada regla: cuántas veces ha disparado, cuándo ha entrado en estado pending, cuándo ha resuelto. Esta visibilidad es enormemente útil para depurar reglas que disparan de forma intermitente o con latencia.

El debugging del routing tiene complejidades distintas

Alertmanager proporciona amtool routing test para probar cómo se enrutaría una alerta hipotética. Es una herramienta poderosa para validar cambios de configuración antes de desplegarlos. Grafana Alerting ofrece una vista similar en la interfaz, pero la falta de herramienta CLI equivalente puede ser un obstáculo para equipos con workflows automatizados de validación.

La gestión de silencio en producción es distinta

Con Alertmanager, crear un silencio en producción durante un incidente requiere acceso a la interfaz web o a la API de Alertmanager. En entornos con acceso restringido, esto puede ser un cuello de botella. Grafana Alerting expone la gestión de silencios en la misma interfaz que los dashboards, lo que suele estar más accesible para desarrolladores que no tienen acceso directo a la infraestructura de monitorización.

Los templates de notificación tienen alcances distintos

Ambas herramientas usan Go templates, pero el contexto de datos disponible difiere. Alertmanager expone el conjunto completo de labels y annotations de la alerta, pero nada más. Grafana Alerting permite incluir imágenes de paneles en las notificaciones — un captura de pantalla del panel relevante adjunta a la alerta en Slack o PagerDuty. Esta funcionalidad, aunque secundaria, puede tener un impacto real en la velocidad de diagnosis durante incidentes.


Alertas en Kubernetes: el caso específico

Para monitoring de Kubernetes específicamente, el kube-prometheus-stack (que incluye Prometheus, Alertmanager y un conjunto comprehensivo de reglas de alerta pre-construidas) sigue siendo el estándar de la industria. Estas reglas usan PromQL y están diseñadas para Alertmanager.

Desplegar kube-prometheus-stack hace de Alertmanager la elección directa para alertas basadas en métricas. Las reglas del stack cubren nodos, kubelet, etcd, CoreDNS, recursos de Kubernetes y métricas de workloads. Recrear estas reglas en Grafana Alerting manualmente es trabajo sin valor añadido claro.

Si además necesitas alertas sobre logs (via Loki) o métricas de negocio desde otras fuentes, la arquitectura recomendada es:

  1. Mantener kube-prometheus-stack con Alertmanager para todas las alertas de infraestructura y SLO/SLI basados en métricas.
  2. Añadir Grafana Alerting únicamente para los datasources no-Prometheus.
  3. Documentar explícitamente qué sistema es propietario de qué tipo de alerta.

Esta separación evita duplicaciones y mantiene la base de reglas de kube-prometheus-stack actualizada con el mínimo esfuerzo.


Preguntas frecuentes

¿Cuál es la diferencia entre Alertmanager y Grafana Alerting?

Prometheus Alertmanager es un motor independiente de routing de notificaciones que recibe alertas ya evaluadas desde Prometheus y las entrega a Slack, PagerDuty, email u otros receivers. No evalúa reglas de alerta. Grafana Alerting es una plataforma de alertas integrada dentro de Grafana que tanto evalúa reglas de alerta (consultando cualquier datasource compatible) como gestiona el routing de notificaciones. Alertmanager usa exclusivamente configuración YAML; Grafana Alerting ofrece UI, API y provisionamiento por ficheros. La diferencia fundamental: Alertmanager gestiona únicamente las fases de routing y notificación, mientras que Grafana Alerting gestiona el ciclo de vida completo desde la evaluación de la query hasta la notificación.

¿Puede Grafana Alerting reemplazar a Prometheus Alertmanager?

Sí, en muchos casos. Grafana Alerting evalúa reglas PromQL directamente contra datasources Prometheus, por lo que no es estrictamente necesaria una instancia separada de Alertmanager. Sin embargo, Alertmanager sigue siendo superior en ciertos escenarios: entornos muy orientados a GitOps, equipos que necesitan las reglas de inhibition maduras de Alertmanager, o arquitecturas donde la evaluación de reglas Prometheus ocurre externamente (Thanos Ruler, Mimir Ruler) con Alertmanager ya en el pipeline. Si tu único datasource es Prometheus y priorizas la configuración declarativa, Alertmanager sigue siendo más simple y ligero.

¿Es el Grafana Alertmanager lo mismo que el Prometheus Alertmanager?

No exactamente. Grafana Alerting usa internamente un fork del código de Prometheus Alertmanager para el routing de notificaciones, pero no son el mismo producto. El “Alertmanager” de Grafana visible en la interfaz es un componente embebido y gestionado con una interfaz de configuración diferente (políticas de notificación, contact points, mute timings) frente al Prometheus Alertmanager autónomo (árbol de routing, receivers, reglas de inhibition en YAML). Grafana también puede conectarse a un Prometheus Alertmanager externo como datasource, lo que crea confusión terminológica adicional. “Grafana Alertmanager” suele referirse al motor de routing embebido dentro de Grafana Alerting.

¿Cuáles son las mejores alternativas a Prometheus Alertmanager?

La alternativa más directa es Grafana Alerting, que recibe y enruta alertas de Prometheus mientras soporta otros datasources. Otras opciones: Grafana OnCall para gestión de guardias y escalación (usado habitualmente junto a Alertmanager, no en sustitución), PagerDuty u Opsgenie como plataformas gestionadas de respuesta a incidentes que reciben alertas directamente, Keep como plataforma open-source de gestión AIOps de alertas, y Mimir Alertmanager para entornos multi-tenant que ejecutan Grafana Mimir. La elección depende de si necesitas un reemplazo de Alertmanager (routing/notificación) o herramientas complementarias para escalación y respuesta a incidentes.

¿Debo usar alertas de Prometheus o de Grafana para monitoring de Kubernetes?

Para monitoring de Kubernetes específicamente, el kube-prometheus-stack (incluyendo Prometheus, Alertmanager y reglas de alerta pre-construidas y comprehensivas) sigue siendo el estándar de la industria. Estas reglas usan PromQL y están diseñadas para Alertmanager. Desplegar kube-prometheus-stack hace de Alertmanager la elección directa para alertas basadas en métricas. Añade Grafana Alerting si también necesitas alertas sobre logs (via Loki) o datasources no-métricos. Para monitoring de Kubernetes, combinar reglas Prometheus con routing Alertmanager es el enfoque más maduro y mejor soportado por la comunidad.

¿Qué ocurre si configuro mal el routing en Alertmanager y nadie recibe las páginas críticas?

Este es el riesgo real de la flexibilidad de Alertmanager. Un árbol de routing mal configurado puede hacer que alertas críticas caigan en un receiver que no notifica a nadie. La mitigación: usar amtool routing test en los pipelines de CI para validar que las alertas conocidas se enrutan correctamente antes de cada merge. Define un receiver de fallback explícito en la ruta por defecto que siempre notifique al canal correcto. Y prueba con alertas reales en staging antes de cortar a producción.


Reflexión final

El debate Alertmanager vs Grafana Alerting no es sobre qué herramienta es superior en abstracto — es sobre qué encaja mejor en tu contexto operativo concreto. Alertmanager es más simple, más ligero y más amigable para GitOps. Grafana Alerting es más versátil, más accesible para equipos orientados a la interfaz, y la única opción viable para alertas multi-datasource. Ejecutar ambos es perfectamente válido cuando los límites están claros.

El peor escenario no es elegir la herramienta “equivocada” — es acabar ejecutando ambas accidentalmente con cobertura solapada, notificaciones duplicadas y ninguna autoridad clara de propiedad. Sea cual sea tu elección: documenta la decisión, define los límites de propiedad, y asegúrate de que tu equipo de guardia sabe exactamente dónde silenciar alertas a las 3 de la madrugada. Eso es lo que marca la diferencia entre un sistema de alertas que funciona y uno que desgasta.

Istio ServiceEntry explicado: servicios externos, DNS y control de tráfico

Istio ServiceEntry explicado: servicios externos, DNS y control de tráfico

Todo clúster Kubernetes en producción se comunica con el exterior. Tus servicios llaman a APIs de pago, se conectan a bases de datos gestionadas, envían eventos a plataformas SaaS y contactan con sistemas legacy que nunca van a correr dentro del mesh. Por defecto, Istio permite que todo el tráfico de salida fluya libremente, o bien lo bloquea completamente si configuras outboundTrafficPolicy a REGISTRY_ONLY. Ninguno de los dos extremos te da lo que realmente necesitas: acceso selectivo, observable y controlado por políticas a los servicios externos.

Eso es exactamente lo que resuelve Istio ServiceEntry. Registra endpoints externos en el registro interno del mesh para que los sidecars de Envoy puedan aplicar las mismas capacidades de gestión de tráfico, seguridad y observabilidad a las llamadas salientes que ya disfrutas para el tráfico east-west. Sin proxies adicionales, sin egress gateways para el caso básico: solo un recurso YAML que le dice al mesh «este servicio externo existe, y así es como llegar a él».

En esta guía recorro todos los campos del spec de ServiceEntry, explico los cuatro modos de resolución DNS con casos de uso reales, y muestro patrones listos para producción con APIs externas, bases de datos, servicios TCP y cargas de trabajo legacy. También cubrimos cómo combinar ServiceEntry con DestinationRule y VirtualService para obtener circuit breaking, reintentos, connection pooling e incluso sticky sessions para dependencias externas.

Qué es un ServiceEntry

Istio mantiene un registro interno de servicios que fusiona los Kubernetes Services con cualquier entrada adicional que declares. Cuando un sidecar proxy necesita decidir cómo enrutar una petición, consulta ese registro. Los servicios dentro del mesh se registran automáticamente. Los servicios fuera del mesh no lo hacen, a menos que crees un ServiceEntry.

Un ServiceEntry es un recurso personalizado que añade una entrada al registro de servicios del mesh. Una vez registrado, el servicio externo se convierte en un ciudadano de primera clase: Envoy genera clusters, routes y listeners para él, lo que significa que obtienes métricas (istio_requests_total), access logs, trazas distribuidas, originación mTLS, reintentos, timeouts y circuit breaking. El conjunto completo de funcionalidades de Istio.

Sin un ServiceEntry, el tráfico de salida hacia un host externo pasa como una conexión TCP en bruto (en modo ALLOW_ANY) sin telemetría alguna, o se descarta con un 502/503 (en modo REGISTRY_ONLY). Ambos resultados son indeseables en producción. El ServiceEntry cierra esa brecha.

Anatomía de ServiceEntry: todos los campos explicados

Veamos un ServiceEntry completo y desglosemos cada campo.

apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: external-api
  namespace: production
spec:
  hosts:
    - api.stripe.com
  location: MESH_EXTERNAL
  ports:
    - number: 443
      name: https
      protocol: TLS
  resolution: DNS
  exportTo:
    - "."
    - "istio-system"

hosts

Lista de hostnames asociados al servicio. Para servicios externos, suele ser el nombre DNS que utiliza tu aplicación (p. ej., api.stripe.com). Para servicios con protocolos HTTP, el campo hosts se compara con la cabecera HTTP Host. Para protocolos no HTTP o servicios sin nombre DNS, puedes usar un hostname sintético y combinarlo con addresses o endpoints estáticos.

addresses

Direcciones IP virtuales opcionales asociadas al servicio. Son útiles para servicios TCP en los que quieres asignar una VIP que el sidecar interceptará. No son necesarias para servicios HTTP/HTTPS que usan enrutamiento basado en hostname.

ports

Los puertos en los que el servicio externo está expuesto. Cada puerto requiere number, name y protocol. El protocolo importa: configurarlo como TLS le dice a Envoy que haga enrutamiento basado en SNI sin terminar TLS. Configurarlo como HTTPS implica HTTP sobre TLS. Para bases de datos, usarás típicamente TCP.

location

MESH_EXTERNAL o MESH_INTERNAL. Usa MESH_EXTERNAL para servicios fuera de tu clúster (APIs de terceros, bases de datos gestionadas). Usa MESH_INTERNAL para servicios dentro de tu infraestructura que no forman parte del mesh: por ejemplo, VMs corriendo en el mismo VPC que no tienen sidecar, o un Kubernetes Service en un namespace sin inyección de sidecar habilitada. La location afecta a cómo se aplica mTLS y cómo se etiquetan las métricas.

resolution

Determina cómo el sidecar resuelve las direcciones de los endpoints. Es el campo más crítico y le dedico la siguiente sección en su totalidad. Opciones: NONE, STATIC, DNS, DNS_ROUND_ROBIN.

endpoints

Lista explícita de endpoints de red. Obligatorio cuando resolution es STATIC. Opcional con resolución DNS para proporcionar labels o información de localidad. Cada endpoint puede tener address, ports, labels, network, locality y weight.

exportTo

Controla la visibilidad de este ServiceEntry entre namespaces. Usa "." para el namespace actual únicamente, "*" para todos los namespaces. En clústeres multi-equipo, restringe los exports para evitar la contaminación de namespaces.

Modos de resolución DNS: NONE vs STATIC vs DNS vs DNS_ROUND_ROBIN

El campo resolution determina cómo Envoy descubre las direcciones IP detrás del servicio. Equivocarse con este campo es la causa número uno de errores de configuración en ServiceEntry. A continuación, una explicación clara.

ResoluciónFuncionamientoMejor para
NONEEnvoy usa la IP de destino original de la conexión. Sin consulta DNS por parte del proxy.Entradas wildcard, escenarios pass-through, servicios donde la aplicación ya resolvió la IP.
STATICEnvoy enruta a las IPs listadas en el campo endpoints. Sin DNS.Servicios con IPs estables y conocidas (p. ej., bases de datos on-prem, VMs con IPs fijas).
DNSEnvoy resuelve el hostname en el momento de la conexión y crea un endpoint por IP devuelta. Usa DNS asíncrono con health checking por IP.APIs externas detrás de load balancers, bases de datos gestionadas con endpoints DNS (RDS, CloudSQL).
DNS_ROUND_ROBINEnvoy resuelve el hostname y usa un único endpoint lógico, rotando entre las IPs devueltas. Sin health checking por IP.Servicios externos simples, servicios donde no necesitas circuit breaking por endpoint.

Cuándo usar NONE

Usa NONE cuando quieras registrar un rango de IPs externas o hosts wildcard sin que Envoy realice ninguna resolución de direcciones. Esto es habitual para políticas de egress amplias: «permitir tráfico a *.googleapis.com en el puerto 443». Envoy simplemente reenvía el tráfico a la IP que la aplicación resolvió via kube-dns. El inconveniente: Envoy tiene capacidad limitada para aplicar políticas por endpoint.

Cuándo usar STATIC

Usa STATIC cuando el servicio externo tiene direcciones IP conocidas y estables que raramente cambian. Esto evita dependencias DNS por completo. Defines las IPs en la lista de endpoints. Caso de uso clásico: una base de datos Oracle legacy en una IP fija en tu centro de datos.

Cuándo usar DNS

Usa DNS para la mayoría de integraciones con APIs externas. Envoy realiza resolución DNS asíncrona y crea un endpoint en el cluster para cada IP devuelta. Esto habilita health checking y circuit breaking por endpoint, algo crítico para la fiabilidad en producción. Este es el modo que quieres para servicios como api.stripe.com o el endpoint de tu instancia RDS.

Cuándo usar DNS_ROUND_ROBIN

Usa DNS_ROUND_ROBIN cuando el hostname externo devuelve muchas IPs y no necesitas circuit breaking por IP. Envoy trata todas las IPs resueltas como un único endpoint lógico y hace round-robin entre ellas. Es más ligero que el modo DNS y evita crear un número elevado de endpoints en la configuración de clusters de Envoy.

Patrones prácticos

Patrón 1: API HTTP externa (api.stripe.com)

El patrón de ServiceEntry más habitual. Tu aplicación llama a una API HTTPS de terceros. Quieres telemetría de Istio y, opcionalmente, reintentos y timeouts.

apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: stripe-api
  namespace: payments
spec:
  hosts:
    - api.stripe.com
  location: MESH_EXTERNAL
  ports:
    - number: 443
      name: tls
      protocol: TLS
  resolution: DNS

El protocolo es TLS, no HTTPS. Como tu aplicación inicia el handshake TLS directamente, Envoy gestiona esto como TLS opaco mediante enrutamiento basado en SNI. Si estuvieras terminando TLS en el sidecar y haciendo originación TLS via un DestinationRule, configurarías el protocolo como HTTP y gestionarías el upgrade por separado. Pero para la mayoría de APIs externas, deja que la aplicación gestione su propio TLS.

Patrón 2: Base de datos gestionada externa (RDS / CloudSQL)

Las bases de datos gestionadas exponen un endpoint DNS que resuelve a una o varias IPs. Durante un failover, el registro DNS cambia. Necesitas que Envoy respete los TTLs DNS y enrute al primary actual.

apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: orders-database
  namespace: orders
spec:
  hosts:
    - orders-db.abc123.us-east-1.rds.amazonaws.com
  location: MESH_EXTERNAL
  ports:
    - number: 5432
      name: postgres
      protocol: TCP
  resolution: DNS

Para servicios TCP, Envoy no puede usar cabeceras HTTP para enrutar, por lo que depende de coincidencia basada en IP. El modo de resolución DNS garantiza que Envoy resuelva periódicamente el hostname y actualice su lista de endpoints. Esto es crítico para escenarios de failover RDS Multi-AZ, donde el endpoint DNS apunta a una nueva IP.

Patrón 3: Servicio interno legacy no incluido en el mesh

Tienes un servicio de monitorización corriendo en un conjunto de VMs con IPs conocidas dentro de tu VPC. No forma parte del mesh, pero tus servicios dentro del mesh necesitan comunicarse con él.

apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: legacy-monitoring
  namespace: observability
spec:
  hosts:
    - legacy-monitoring.internal
  location: MESH_INTERNAL
  ports:
    - number: 8080
      name: http
      protocol: HTTP
  resolution: STATIC
  endpoints:
    - address: 10.0.5.10
    - address: 10.0.5.11
    - address: 10.0.5.12

Diferencias clave: location es MESH_INTERNAL porque el servicio vive dentro de tu red, y resolution es STATIC porque conocemos las IPs. El hostname legacy-monitoring.internal es sintético: tu aplicación lo usa, y el DNS proxy de Istio (o una entrada de CoreDNS) lo resuelve a uno de los endpoints listados.

Patrón 4: Servicios TCP con múltiples puertos

Algunos servicios externos exponen múltiples puertos TCP: por ejemplo, un clúster Elasticsearch con puertos de datos (9200) y de transporte (9300).

apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: external-elasticsearch
  namespace: search
spec:
  hosts:
    - es.example.com
  location: MESH_EXTERNAL
  ports:
    - number: 9200
      name: http
      protocol: HTTP
    - number: 9300
      name: transport
      protocol: TCP
  resolution: DNS

Cada puerto obtiene su propia configuración de listener en Envoy. El puerto HTTP se beneficia de telemetría completa de capa 7 y gestión de tráfico. El puerto TCP obtiene métricas de capa 4 y políticas a nivel de conexión.

Combinando ServiceEntry con DestinationRule

Un ServiceEntry por sí solo registra el servicio externo. Para aplicar políticas de tráfico —connection pooling, circuit breaking, originación TLS, load balancing— lo combinas con un DestinationRule. Aquí es donde la cosa se pone interesante.

Connection pooling y circuit breaking

Las APIs externas tienen rate limits. Tu base de datos gestionada tiene un número máximo de conexiones. Proteger estas dependencias a nivel del mesh previene fallos en cascada.

apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: stripe-api
  namespace: payments
spec:
  hosts:
    - api.stripe.com
  location: MESH_EXTERNAL
  ports:
    - number: 443
      name: tls
      protocol: TLS
  resolution: DNS
---
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: stripe-api-dr
  namespace: payments
spec:
  host: api.stripe.com
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 50
        connectTimeout: 5s
      http:
        h2UpgradePolicy: DO_NOT_UPGRADE
        maxRequestsPerConnection: 100
    outlierDetection:
      consecutive5xxErrors: 3
      interval: 30s
      baseEjectionTime: 60s
      maxEjectionPercent: 100

Esta configuración limita las conexiones de salida a Stripe a 50, establece un timeout de conexión de 5 segundos y expulsa los endpoints que devuelvan 3 errores 5xx consecutivos. En producción, esto evita que una API de terceros degradada consuma todos tus slots de conexión y provoque un efecto dominó en tus servicios.

Originación TLS

A veces tu aplicación habla HTTP en claro, pero el servicio externo requiere HTTPS. En lugar de modificar el código de la aplicación, puedes delegar la originación TLS al sidecar.

apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: external-api
  namespace: default
spec:
  hosts:
    - api.external-service.com
  location: MESH_EXTERNAL
  ports:
    - number: 80
      name: http
      protocol: HTTP
    - number: 443
      name: https
      protocol: TLS
  resolution: DNS
---
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: external-api-tls
  namespace: default
spec:
  host: api.external-service.com
  trafficPolicy:
    portLevelSettings:
      - port:
          number: 443
        tls:
          mode: SIMPLE

Tu aplicación envía HTTP al puerto 80. Un VirtualService (que se muestra en la siguiente sección) redirige eso al puerto 443. El DestinationRule inicia TLS hacia el endpoint externo. La aplicación nunca sabe que TLS ocurrió.

Combinando ServiceEntry con VirtualService

VirtualService te da gestión de tráfico de capa 7 para servicios externos: reintentos, timeouts, inyección de fallos, enrutamiento basado en cabeceras y traffic shifting. Esto es invaluable cuando migras entre proveedores de API o necesitas políticas de resiliencia para dependencias externas poco fiables.

Reintentos y timeouts

apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: stripe-api-vs
  namespace: payments
spec:
  hosts:
    - api.stripe.com
  http:
    - route:
        - destination:
            host: api.stripe.com
            port:
              number: 443
      timeout: 10s
      retries:
        attempts: 3
        perTryTimeout: 3s
        retryOn: connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes
        retryRemoteLocalities: true

Esto aplica un timeout global de 10 segundos con hasta 3 intentos de reintento (3 segundos cada uno) para condiciones de fallo específicas. Ten en cuenta que esto solo funciona para ServiceEntries con protocolo HTTP. Para entradas con protocolo TLS, donde Envoy no puede ver la capa HTTP, estás limitado a reintentos de conexión TCP configurados via DestinationRule.

Traffic shifting entre proveedores externos

¿Migrando de una API externa a otra? Usa weighted routing para desplazar el tráfico de forma gradual.

apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: geocoding-primary
  namespace: geo
spec:
  hosts:
    - geocoding.internal
  location: MESH_EXTERNAL
  ports:
    - number: 443
      name: tls
      protocol: TLS
  resolution: STATIC
  endpoints:
    - address: api.old-geocoding-provider.com
      labels:
        provider: old
    - address: api.new-geocoding-provider.com
      labels:
        provider: new
---
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: geocoding-dr
  namespace: geo
spec:
  host: geocoding.internal
  trafficPolicy:
    tls:
      mode: SIMPLE
  subsets:
    - name: old-provider
      labels:
        provider: old
    - name: new-provider
      labels:
        provider: new
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: geocoding-vs
  namespace: geo
spec:
  hosts:
    - geocoding.internal
  http:
    - route:
        - destination:
            host: geocoding.internal
            subset: old-provider
          weight: 80
        - destination:
            host: geocoding.internal
            subset: new-provider
          weight: 20

Esto envía el 80% del tráfico de geocodificación al proveedor antiguo y el 20% al nuevo. Ajusta los pesos a medida que ganas confianza. Completamente reversible: basta con devolver el proveedor antiguo al 100%.

Patrones de resolución DNS: Istio DNS Proxy vs kube-dns

La resolución DNS de Istio para servicios externos implica dos capas: cómo tu aplicación resuelve el hostname (kube-dns / CoreDNS) y cómo el sidecar resuelve el hostname (DNS asíncrono de Envoy o el DNS proxy de Istio). Entender la interacción entre ambas capas es crucial para un comportamiento DNS fiable en Istio.

Flujo por defecto (sin Istio DNS Proxy)

Tu aplicación llama a api.stripe.com. kube-dns lo resuelve a una IP. La aplicación abre una conexión a esa IP. El sidecar intercepta la conexión y —si el ServiceEntry usa resolución DNS— Envoy resuelve api.stripe.com de forma independiente para determinar su lista de endpoints. Se producen dos consultas DNS separadas, lo que puede generar inconsistencias si los registros DNS cambian entre ambas resoluciones.

Con el DNS Proxy de Istio (dns.istio.io)

El sidecar de Istio incluye un DNS proxy que intercepta las consultas DNS de la aplicación. Cuando está habilitado (mediante meshConfig.defaultConfig.proxyMetadata.ISTIO_META_DNS_CAPTURE e ISTIO_META_DNS_AUTO_ALLOCATE), el proxy puede:

  • Auto-asignar IPs virtuales para hosts de ServiceEntry que no tienen addresses definidas, lo cual es crítico para ServiceEntries TCP que necesitan coincidencia basada en IP.
  • Resolver hosts de ServiceEntry directamente, evitando el viaje de ida y vuelta a kube-dns para servicios conocidos del mesh.
  • Garantizar consistencia entre la resolución DNS de la aplicación y la resolución de endpoints del sidecar.

En instalaciones modernas de Istio (1.18+), la captura DNS está habilitada por defecto. Verifica con:

istioctl proxy-config bootstrap <pod-name> -n <namespace> | grep -A2 "ISTIO_META_DNS"

Cuándo importa más el DNS Proxy

El DNS proxy es especialmente importante para ServiceEntries TCP sin un campo addresses explícito. Sin una VIP, Envoy no puede asociar una conexión TCP entrante al ServiceEntry correcto porque no hay una cabecera HTTP Host que inspeccionar. El DNS proxy resuelve esto auto-asignando una VIP del rango 240.240.0.0/16 y devolviéndola cuando la aplicación resuelve el hostname. El sidecar entonces intercepta el tráfico a esa VIP y lo enruta al endpoint externo correcto.

Sticky sessions con ServiceEntry

Algunos servicios externos requieren afinidad de sesión: por ejemplo, un servicio legacy que almacena el estado de sesión en memoria, o un endpoint WebSocket que debe mantener una conexión persistente al mismo backend. Istio soporta sticky sessions para servicios externos a través de consistent hashing en un DestinationRule.

apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: legacy-session-service
  namespace: default
spec:
  hosts:
    - legacy-session.internal
  location: MESH_INTERNAL
  ports:
    - number: 8080
      name: http
      protocol: HTTP
  resolution: STATIC
  endpoints:
    - address: 10.0.1.10
    - address: 10.0.1.11
    - address: 10.0.1.12
---
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: legacy-session-dr
  namespace: default
spec:
  host: legacy-session.internal
  trafficPolicy:
    loadBalancer:
      consistentHash:
        httpCookie:
          name: SERVERID
          ttl: 3600s

Esta configuración hace hash sobre una cookie HTTP llamada SERVERID. Si la cookie no existe, Envoy genera una y la establece en la respuesta para que las peticiones posteriores del mismo cliente vayan al mismo endpoint.

También puedes hacer hash sobre:

  • Cabecera HTTP: consistentHash.httpHeaderName: "x-user-id" — útil cuando tu aplicación envía un identificador de usuario en cada petición.
  • IP de origen: consistentHash.useSourceIp: true — opción más sencilla, pero se rompe en entornos con NAT o IPs de egress compartidas.
  • Parámetro de query: consistentHash.httpQueryParameterName: "session_id" — para REST APIs que incluyen un identificador de sesión en la URL.

Las sticky sessions con ServiceEntry funcionan de forma idéntica a las sticky sessions dentro del mesh. El requisito clave es que el ServiceEntry debe usar resolución STATIC o DNS (no NONE) para que Envoy tenga múltiples endpoints entre los que aplicar el hash. Con DNS_ROUND_ROBIN, solo hay un endpoint lógico, por lo que el consistent hashing no tiene efecto.

Resolución de problemas comunes

Errores 503 al llamar a servicios externos

El problema más habitual con ServiceEntry. Empieza con esta secuencia de diagnóstico:

# Comprueba si el ServiceEntry está aplicado y visible para el proxy
istioctl proxy-config cluster <pod-name> -n <namespace> | grep <external-host>

# Comprueba los listeners
istioctl proxy-config listener <pod-name> -n <namespace> --port <port>

# Revisa los access logs de Envoy para la petición específica
kubectl logs <pod-name> -n <namespace> -c istio-proxy | grep <external-host>

Causas habituales de errores 503:

  • Protocolo incorrecto: establecer protocol: HTTPS cuando tu aplicación inicia TLS. Usa TLS para pass-through; usa HTTP solo si el sidecar hace la originación TLS.
  • ServiceEntry ausente en modo REGISTRY_ONLY: si outboundTrafficPolicy es REGISTRY_ONLY, cualquier host sin ServiceEntry queda bloqueado.
  • Restricción de exportTo: el ServiceEntry está en el namespace A, exportado solo a ".", y el pod que llama está en el namespace B.
  • Fallo de resolución DNS: Envoy no puede resolver el hostname. Comprueba que los servidores DNS sean accesibles desde el pod.

Fallos de resolución DNS

Cuando el resolvedor DNS asíncrono de Envoy falla, verás flags UH (upstream unhealthy) o UF (upstream connection failure) en los access logs.

# Verifica que DNS funciona desde dentro del sidecar
kubectl exec <pod-name> -n <namespace> -c istio-proxy -- \
  pilot-agent request GET /dns_resolve?proxyID=<pod-name>.<namespace>&host=api.stripe.com

# Comprueba el estado del cluster Envoy
istioctl proxy-config endpoint <pod-name> -n <namespace> | grep <external-host>

Si el endpoint aparece como UNHEALTHY, Envoy resolvió DNS pero la detección de anomalías expulsó el host. Si no aparece ningún endpoint, la resolución DNS está fallando. Solución habitual: asegúrate de que tus pods pueden llegar a un servidor DNS externo, o que CoreDNS está configurado para reenviar consultas del dominio externo.

La originación TLS no funciona

Si configuraste originación TLS via un DestinationRule pero el tráfico sigue fallando:

  1. Asegúrate de que el protocolo del puerto en el ServiceEntry es HTTP, no TLS. Si lo estableces como TLS, Envoy trata la conexión como TLS opaco pass-through y no aplicará la configuración TLS del DestinationRule.
  2. Verifica que el campo host del DestinationRule coincide exactamente con la entrada hosts del ServiceEntry.
  3. Comprueba que el VirtualService (si se usa) enruta al número de puerto correcto.

El ServiceEntry TCP no intercepta tráfico

Para ServiceEntries con protocolo TCP sin el DNS proxy, Envoy no puede asociar el tráfico por hostname. Debes hacer una de estas cosas:

  • Establecer un campo addresses explícito con una VIP a la que tu aplicación se dirija.
  • Habilitar el DNS proxy de Istio para que auto-asigne VIPs.
  • Asegurarte de que la IP de destino coincide con lo que resuelve el ServiceEntry.

Sin una de estas opciones, el tráfico TCP pasa por el PassthroughCluster y evita tu ServiceEntry por completo.

Preguntas frecuentes

¿Necesito un ServiceEntry si outboundTrafficPolicy está en ALLOW_ANY?

No lo necesitas para la conectividad: tus servicios pueden alcanzar hosts externos sin él. Pero deberías crearlo de todos modos. Sin ServiceEntry, el tráfico de salida pasa por el PassthroughCluster, lo que significa: sin métricas detalladas por destino, sin access logging con el hostname externo, sin circuit breaking, sin reintentos ni políticas de timeout. Un ServiceEntry es la diferencia entre «funciona» y «funciona de forma fiable con observabilidad».

¿Cuál es la diferencia entre protocol TLS y HTTPS en un puerto de ServiceEntry?

TLS le dice a Envoy que trate la conexión como TLS opaco. Envoy lee la cabecera SNI para determinar el enrutamiento, pero no descifra el payload. Úsalo cuando tu aplicación inicia TLS directamente. HTTPS le dice a Envoy que el protocolo es HTTP sobre TLS, lo que implica que Envoy debe gestionar TLS. En la práctica, para servicios externos donde la aplicación gestiona su propio TLS, usa TLS. Usa HTTP con originación TLS en un DestinationRule cuando quieras que el sidecar gestione TLS.

¿Puedo usar wildcards en los hosts de ServiceEntry?

Sí, pero con limitaciones. Puedes usar *.example.com para coincidir con cualquier subdominio de example.com. Sin embargo, las entradas wildcard solo funcionan con resolution: NONE porque Envoy no puede hacer consultas DNS para hostnames wildcard. Esto significa que pierdes la capacidad de aplicar políticas de tráfico por endpoint. Los ServiceEntries wildcard son más adecuados para control de acceso de egress amplio que para gestión de tráfico granular.

¿Cómo configuro sticky sessions para un servicio externo detrás de un ServiceEntry?

Crea un ServiceEntry con resolución STATIC o DNS (para que Envoy tenga múltiples endpoints) y combínalo con un DestinationRule que configure consistentHash bajo trafficPolicy.loadBalancer. Puedes hacer hash sobre una cookie HTTP, una cabecera, la IP de origen o un parámetro de query. El ServiceEntry debe exponer múltiples endpoints para que el consistent hashing tenga algún efecto. Consulta la sección «Sticky sessions con ServiceEntry» más arriba para un ejemplo YAML completo.

¿Cómo interactúa ServiceEntry con NetworkPolicy y Istio AuthorizationPolicy?

Un ServiceEntry no evita las Kubernetes NetworkPolicy. Si una NetworkPolicy bloquea el egress a la IP externa, el tráfico se descartará a nivel CNI antes de que Envoy pueda enrutarlo. Istio AuthorizationPolicy también puede restringir qué workloads tienen permitido llamar a hosts específicos de ServiceEntry. Para una defensa en profundidad: usa ServiceEntry para la gestión de tráfico y la observabilidad, AuthorizationPolicy para el control de acceso a nivel de workload y NetworkPolicy para la aplicación a nivel de red.

Conclusión

ServiceEntry es uno de los recursos de Istio más prácticos que usarás en producción. Transforma conexiones de salida opacas en tráfico gestionado, observable y controlado por políticas, y lo hace sin requerir cambios en el código de tu aplicación.

Empieza con lo básico: crea un ServiceEntry para cada dependencia externa, establece el tipo de resolución correcto y combínalo con un DestinationRule para límites de conexión y circuit breaking. A medida que madures, añade VirtualServices para reintentos y timeouts, configura sticky sessions donde sea necesario y habilita el DNS proxy para una integración perfecta de servicios TCP.

El patrón es siempre el mismo: registra el servicio, aplica políticas, observa el tráfico. Cada dependencia externa que formalices con un ServiceEntry es un punto ciego menos en tu mesh de producción.

HPA Kubernetes: por qué CPU funciona y memoria casi nunca

HPA Kubernetes: por qué CPU funciona y memoria casi nunca

Hay una configuración que aparece en prácticamente todos los clústeres de Kubernetes: un HorizontalPodAutoscaler con objetivo de 70% de CPU y 70% de memoria. Parece razonable. Sigue los ejemplos de la documentación oficial. Y en muchos casos, genera más problemas de los que resuelve — en silencio.

Los síntomas son predecibles: workloads que no hacen nada se escalan hacia arriba porque su footprint de memoria es naturalmente alto. APIs sensibles a la latencia escalan demasiado tarde porque el pico de CPU ya ha pasado cuando los nuevos pods están listos. Jobs batch oscilan entre escalar hacia arriba y hacia abajo durante operación normal. Y los equipos dedican horas a depurar comportamientos del autoescalado que deberían ser sencillos.

Este artículo explica por qué la configuración por defecto del HPA falla, en qué condiciones exactas el HPA basado en memoria tiene sentido (y cuándo no), y qué métricas alternativas — métricas custom, triggers basados en eventos y señales externas — producen un autoescalado que realmente refleja la demanda del workload.


Cómo decide el HPA escalar: la mecánica real

Antes de diagnosticar los problemas, conviene entender con precisión la mecánica. El HPA calcula el número deseado de réplicas con esta fórmula:

desiredReplicas = ceil(currentReplicas × (currentMetricValue / desiredMetricValue))

Para un objetivo de CPU del 70%, con 2 réplicas consumiendo actualmente una media del 140% de su CPU request, el HPA calcula ceil(2 × (140 / 70)) = 4 réplicas. Conceptualmente simple, pero con una dependencia crítica que la mayoría de configuraciones ignora: el valor de la métrica se expresa en relación al resource request, no al limit.

Esta distinción es fundamental para entender todos los modos de fallo que siguen. Si un contenedor tiene un CPU request de 100m y un limit de 2000m, y actualmente consume 80m, el HPA ve un 80% de utilización — aunque el contenedor esté usando solo el 4% de su techo permitido. Pon un threshold de HPA del 70% sobre un contenedor con CPU request de 100m y cualquier carga no trivial disparará el escalado de inmediato.

El controlador HPA sondea métricas cada 15 segundos por defecto (--horizontal-pod-autoscaler-sync-period). El escalado hacia arriba ocurre rápido — en uno a tres ciclos de sondeo cuando el threshold se supera de forma consistente. El escalado hacia abajo es deliberadamente lento: por defecto el controlador espera 5 minutos (--horizontal-pod-autoscaler-downscale-stabilization) antes de reducir réplicas, para evitar el thrashing. Esta asimetría importa cuando se depura oscilación.


HPA basado en CPU: cuándo funciona y cuándo no

CPU es un recurso compresible. Cuando un contenedor alcanza su limit de CPU, el kernel lo throttlea — el proceso se ralentiza pero no muere ni es desalojado. Esta propiedad convierte a CPU en un proxy razonable para la carga en muchos escenarios, pero no en todos.

Dónde funciona bien el HPA por CPU

Los workloads stateless de procesamiento de peticiones son el punto ideal para el HPA basado en CPU. Si tu servicio hace trabajo CPU-bound por petición — APIs REST que transforman datos, lógica de negocio computacionalmente intensiva, procesamiento de imágenes — entonces la utilización de CPU correlaciona bien con el volumen de peticiones. Más peticiones implica más CPU consumida, lo que hace que el HPA añada réplicas, distribuyendo la carga.

Los prerrequisitos clave para que el HPA por CPU funcione correctamente son:

CPU requests precisos. Ajusta los requests al consumo sostenido real del workload bajo carga normal, no a un placeholder bajo. Usa VPA en modo recomendación o datos históricos de Prometheus para dimensionar bien los requests antes de activar el HPA.

Ratio request/limit razonable. Un ratio de 1:4 o menos mantiene los thresholds en porcentaje significativos. Un contenedor con request 100m y limit 4000m convierte los thresholds porcentuales en algo casi inútil.

Consumo de CPU que sigue la carga de usuarios de forma lineal. Si tu servicio realiza trabajo pesado en background independiente de las peticiones entrantes, la utilización de CPU disparará el escalado independientemente de la demanda real.

Dónde falla el HPA por CPU

Servicios sensibles a la latencia con picos bruscos de tráfico. El HPA reacciona a la utilización media de CPU medida en la ventana de sondeo. Para un servicio que gestiona ráfagas de tráfico — una venta flash, un batch de llamadas API disparado por cron, un broadcast de notificaciones — cuando el controlador HPA detecta el pico, encola nuevos pods y esos pods superan los readiness checks, el pico puede haber terminado ya. El resultado: réplicas añadidas después de que el daño está hecho, con el coste añadido de un ciclo de scale-down posterior.

Workloads I/O-bound. Un servicio que pasa la mayor parte del tiempo esperando a consultas de base de datos, llamadas a APIs externas o lecturas de colas de mensajes mostrará CPU baja incluso bajo carga elevada. El HPA no añadirá réplicas mientras el servicio está degradado — ve CPUs ociosas mientras goroutines o threads están bloqueados esperando I/O.

Workloads con costes de cold-start elevados. Si una nueva réplica tarda 30-60 segundos en arrancar (cargando modelos de ML, estableciendo pools de conexiones, populando caches), las decisiones de escalado deben ocurrir antes — no en reacción al pico de CPU.


HPA basado en memoria: por qué casi siempre se rompe

Memory es un recurso incompresible. A diferencia de CPU — que puede throttlearse sin matar un proceso — cuando un contenedor agota su memory limit, el OOM killer lo termina. Esta propiedad única desencadena un conjunto de problemas fundamentales en el uso de memoria como trigger del HPA.

El problema central: la memoria no correlaciona naturalmente con la carga

Para la mayoría de servicios bien diseñados, el consumo de memoria es relativamente estable. Un servicio en Go asigna memoria al arrancar para sus estructuras de runtime, pools de conexiones y caches — y después mantiene aproximadamente ese footprint independientemente del tráfico. Una aplicación JVM asigna heap al arrancar y usa el garbage collector para gestionarlo. En ambos casos, el uso de memoria bajo 10 peticiones por segundo y bajo 10.000 peticiones por segundo puede ser prácticamente idéntico.

Esto significa que un HPA basado en memoria con un threshold del 70% hará una de dos cosas:

  • Nunca dispararse, porque el consumo de memoria del workload es estable y siempre está por debajo del threshold — dejando el HPA inútil.
  • Dispararse siempre, porque el consumo de memoria base del workload supera naturalmente el threshold — causando que el workload escale de forma permanente y nunca vuelva a bajar.

Ninguno de estos resultados corresponde a una necesidad real de escalado.

La trampa de la mala configuración de requests

Este es el modo de fallo más común y la causa más frecuente del “mi workload se escala sin motivo aparente”. Considera un servicio Java que necesita 512Mi de heap para funcionar con normalidad. El equipo establece el memory request en 256Mi — demasiado conservador, ya sea para ahorrar costes o porque la estimación inicial era incorrecta. El servicio consume inmediatamente el 200% de su memory request con solo estar en ejecución. Un HPA con un objetivo de memoria del 70% escalará este workload hasta el máximo de réplicas a los pocos minutos del despliegue, y se quedará ahí para siempre.

La solución nunca es “ajustar el threshold del HPA”. La solución es dimensionar correctamente el memory request. Pero esto revela el problema más profundo: el HPA basado en memoria es extremadamente sensible a la precisión de tus resource requests, y la mayoría de equipos no tienen requests precisos — especialmente en workloads nuevos o tras cambios de código que alteran el footprint de memoria.

Comportamiento de memoria en JVM y Go

Los workloads JVM son especialmente problemáticos. Por defecto, la JVM asigna heap hasta un máximo (-Xmx) y después lo retiene — no libera heap de vuelta al SO de forma agresiva, ni siquiera después del garbage collection. Un servicio JVM que gestiona una petición por hora mostrará prácticamente el mismo footprint de memoria que uno gestionando miles de peticiones por minuto. Además, el garbage collector de la JVM introduce picos de memoria durante los ciclos de recolección que no están relacionados con la carga.

En entornos JVM contenerizados, también hay que tener en cuenta el flag de awareness del memory limit del contenedor (-XX:+UseContainerSupport, activado por defecto desde JDK 11), que afecta a cómo la JVM calcula su techo de heap en relación con el container limit. Sin el tuning adecuado, la JVM puede asignar un heap que ocupa el 80-90% del memory limit del contenedor — disparando de inmediato cualquier HPA basado en memoria.

Los workloads en Go se comportan de forma diferente pero también mal con el HPA de memoria. El garbage collector de Go está diseñado para mantener baja latencia más que uso mínimo de memoria. El runtime puede retener memoria por encima de lo estrictamente necesario, y el footprint de memoria puede variar según los parámetros de tuning del GC (GOGC, GOMEMLIMIT) de maneras que no correlacionan con la carga de peticiones entrantes.

Cuándo el HPA de memoria sí tiene sentido

Hay casos concretos donde el HPA basado en memoria tiene justificación:

Workloads donde el consumo de memoria realmente sigue la carga de forma lineal. Algunos pipelines de procesamiento de datos, caches en memoria que crecen con el volumen de peticiones, o aplicaciones de streaming que buferizan datos proporcionalmente al throughput. Si puedes demostrar mediante métricas que memoria y carga tienen una correlación lineal fuerte, el HPA de memoria es defendible.

Como válvula de seguridad junto al HPA por CPU. Usar memoria como métrica secundaria (no primaria) para protegerse ante memory leaks o allocaciones descontroladas en un servicio que normalmente escala por CPU. En este caso, establece el threshold de memoria alto — 85-90% — de forma que solo se active en escenarios de sobreconsumo real.

Servicios de caché donde no es deseable el desalojo. Si un servicio usa memoria como caché de rendimiento y quieres escalar antes de que la presión de memoria cause desalojo de caché, la utilización de memoria puede ser un trigger útil — siempre que los requests estén correctamente dimensionados.

Fuera de estos casos específicos, eliminar memoria del spec del HPA y apoyarse en las señales que se describen más adelante producirá mejor comportamiento en prácticamente todos los escenarios.


Dimensionar correctamente los requests antes de añadir HPA

Ninguna estrategia de HPA funciona correctamente sin resource requests precisos. Antes de añadir cualquier autoescalador — CPU, memoria o métricas custom — ejecuta tu workload bajo carga representativa y mide el consumo real. La forma más sencilla es usar VPA en modo recomendación:

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: my-service-vpa
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-service
  updatePolicy:
    updateMode: "Off"   # Solo recomendación — no aplicar automáticamente

Tras 24-48 horas de tráfico, comprueba las recomendaciones del VPA:

kubectl describe vpa my-service-vpa

Los valores lowerBound, target y upperBound te dan una base fundamentada en datos para establecer los requests. Establece tus requests en o cerca del valor target del VPA antes de configurar el HPA. Este único paso elimina la causa más común de mal comportamiento del HPA.

Restricción crítica: VPA y HPA no pueden gestionar la misma métrica de recurso simultáneamente. Si el VPA está configurado para actualizar automáticamente CPU o memoria, y el HPA también escala sobre esas métricas, los dos controladores se pelearán entre sí. La combinación segura es: HPA sobre CPU/memoria con VPA en modo solo-recomendación, o HPA sobre métricas custom con VPA sobre CPU/memoria en modo automático.


Mejores señales: qué usar en lugar de memoria (y cuándo ir más allá de CPU)

El cambio fundamental es pasar de métricas de consumo de recursos (que describen el pasado) a métricas de demanda (que describen lo que el workload está siendo pedido hacer ahora mismo o será pedido en segundos).

Peticiones por segundo (RPS)

Para servicios HTTP, las peticiones por segundo por réplica suelen ser el proxy más preciso para la carga. A diferencia de CPU, mide la demanda directamente — no un efecto secundario de la demanda. Un HPA que mantiene 500 RPS por réplica escalará de forma predecible a medida que el tráfico crece, independientemente de si el servicio es CPU-bound, memory-bound o I/O-bound.

RPS está disponible como métrica custom desde tu service mesh (Istio lo expone como istio_requests_total), desde tu ingress controller (NGINX expone tasas de peticiones vía Prometheus), o desde las propias métricas Prometheus de tu aplicación. Configurar el HPA sobre métricas custom requiere el Prometheus Adapter o una implementación compatible de la custom metrics API.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-service
  minReplicas: 2
  maxReplicas: 20
  metrics:
  - type: Pods
    pods:
      metric:
        name: http_requests_per_second
      target:
        type: AverageValue
        averageValue: "500"

Profundidad de cola y consumer lag

Los consumidores de colas de mensajes deben escalar en función del consumer lag — el backlog esperando ser procesado. Esto mide directamente si los consumidores están al día o si la cola crece más rápido de lo que se puede procesar. KEDA proporciona scalers integrados para Kafka, RabbitMQ, SQS y muchos otros sistemas.

Este enfoque es superior a CPU para consumers de mensajería porque un consumer puede estar idle (CPU baja) mientras la cola acumula millones de mensajes. Sin el lag como señal de escalado, el HPA no haría nada — exactamente lo contrario de lo que necesitas.

Latencia (P99)

La latencia P99 por réplica señala eficazmente la sobrecarga cuando se aproxima a los umbrales de SLO, especialmente para APIs de cara al usuario. Si la latencia sube, hay un problema — independientemente de lo que diga la CPU. Usar latencia como señal de escalado es especialmente útil cuando el workload tiene comportamientos de rendimiento no lineales bajo carga.

Escalado planificado y predictivo

Para patrones predecibles (horario laboral, ciclos semanales, eventos programados), el Cron scaler de KEDA permite pre-escalar de forma proactiva antes de que la carga aumente. Esto elimina por completo el problema del cold-start en los picos de tráfico esperados: en lugar de reaccionar a la carga, te anticipas a ella.


Configuración de HPA para producción: lo que realmente importa

Mínimo de réplicas

Establece siempre minReplicas: 2. Un único pod es un single point of failure; si el HPA escala hacia abajo a 1 réplica y ese pod es terminado durante un scale-down, tu servicio tiene un instante de downtime. Dos réplicas mínimas también garantizan que el HPA tenga datos de consumo medio sobre los que calcular.

Ventanas de estabilización

La ventana de scale-down por defecto de 5 minutos suele provocar oscilación en muchos workloads. Auméntala para que coincida con los ciclos del workload. Esta configuración es clave en producción:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-service
  minReplicas: 2
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 60
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 600
      policies:
      - type: Percent
        value: 25
        periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
      - type: Percent
        value: 100
        periodSeconds: 30

Aquí se definen dos comportamientos distintos:

  • Scale-down conservador: ventana de estabilización de 10 minutos, máximo 25% de reducción por minuto. Evita que una caída temporal de carga destruya réplicas que volverán a necesitarse.
  • Scale-up agresivo: sin ventana de estabilización, hasta el doble de réplicas cada 30 segundos. El coste de escalar de más es menor que el de responder tarde.

Ajuste del threshold de CPU

Considera el tiempo de reacción al escalar. Un threshold del 70% suena razonable, pero implica que ya estás sobrecargado cuando el HPA comienza a reaccionar. Establece objetivos al 50-60% para mantener margen mientras arrancan los nuevos pods — especialmente si tu tiempo de cold-start es de más de unos pocos segundos.

PodDisruptionBudgets

Empareja siempre el HPA con un PodDisruptionBudget para evitar que el scale-down termine múltiples pods simultáneamente:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: my-service-pdb
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: my-service

Sin PDB, Kubernetes puede terminar varios pods al mismo tiempo durante el scale-down, causando degradación de servicio en el proceso de optimización de costes.


KEDA: escalado basado en eventos sin reemplazar el HPA

KEDA (Kubernetes Event-Driven Autoscaling) extiende el HPA sin reemplazarlo. En lugar de competir con él, KEDA implementa una custom metrics API que el HPA consume. No añades un nuevo controlador de escalado — añades una fuente de métricas más rica al sistema existente.

KEDA gestiona más de 60 scalers integrados para sistemas como Kafka, RabbitMQ, Azure Service Bus, AWS SQS, y también permite escalado basado en consultas Prometheus arbitrarias. Esto lo convierte en la herramienta de elección para:

  • Consumers de colas de mensajes: escala a cero cuando no hay mensajes, escala agresivamente cuando el lag crece.
  • Workloads event-driven: triggers basados en métricas externas que el HPA estándar no puede alcanzar.
  • Patrones de tráfico predecibles: el Cron scaler permite programar réplicas mínimas por franja horaria.

Un ejemplo de ScaledObject de KEDA para un consumer de Kafka:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: kafka-consumer-scaler
spec:
  scaleTargetRef:
    name: my-kafka-consumer
  minReplicaCount: 1
  maxReplicaCount: 30
  triggers:
  - type: kafka
    metadata:
      bootstrapServers: kafka-broker:9092
      consumerGroup: my-consumer-group
      topic: my-topic
      lagThreshold: "100"

Esto escala el consumer manteniendo un lag máximo de 100 mensajes por réplica. Cuando la cola crece, KEDA añade réplicas. Cuando está vacía, puede bajar a cero — algo imposible con el HPA estándar basado solo en CPU.


Tabla de decisión: qué señal usar según el workload

Tipo de workloadSeñal recomendadaHerramienta
API HTTP stateless (CPU-bound)CPU al 50-60%HPA
API HTTP stateless (I/O-bound)RPS por réplica o latencia P99HPA + custom metrics
Consumer de cola de mensajesConsumer lag / profundidad de colaKEDA
Event-driven (Kafka/SQS/RabbitMQ)Tasa de eventos o lagKEDA
Patrón de tráfico predeciblePlanificación horariaKEDA Cron
Riesgo de memory leakCPU primario + memoria al 85% secundarioHPA v2 multi-metric
Dimensionado previo al HPARecomendaciones históricas CPU/memoriaVPA en modo recomendación

Preguntas frecuentes sobre el autoescalado horizontal en Kubernetes

¿Qué ocurre si defino múltiples métricas en un HPA?

El HPA evalúa todas las métricas definidas y escala para satisfacer la más exigente. Si CPU dice 3 réplicas y RPS dice 5, el HPA usará 5. Nunca se toma el mínimo — siempre el máximo — lo que puede causar sorpresas si no se entiende esta semántica.

Mi workload se escala hacia arriba inmediatamente después del despliegue. ¿Por qué?

Esto casi siempre indica una mala configuración de requests. Usa kubectl top pods para ver el consumo real y compáralo con los requests definidos. Si el consumo supera el request, el HPA verá utilización superior al 100% y escalará agresivamente. La solución es ajustar los requests, no el threshold del HPA.

El HPA escala hacia abajo demasiado rápido y luego vuelve a escalar. ¿Cómo lo soluciono?

Aumenta stabilizationWindowSeconds en la sección scaleDown del behavior. También considera añadir una política de porcentaje (type: Percent) para limitar cuántas réplicas se eliminan por ciclo. El objetivo es que el scale-down sea conservador y el scale-up sea ágil.

¿Puedo usar HPA con StatefulSets?

Técnicamente sí, pero generalmente no deberías. Los StatefulSets implican identidad estable de pod, almacenamiento persistente y orden de escala — propiedades que chocan con el escalado dinámico del HPA. Para workloads stateful, el escalado manual suele ser más seguro.

Mi CPU request es de 50m. ¿Tiene sentido usar HPA por CPU?

Con requests muy bajos, las métricas en porcentaje se vuelven muy granulares — un cambio de 10m en consumo supone un 20% de cambio en utilización. El escalado se vuelve ruidoso. Considera usar métricas custom como RPS en su lugar, o al menos aumentar el request a un valor más representativo.

¿Cómo depuro el HPA cuando no se comporta como espero?

kubectl describe hpa <nombre>

Este comando muestra las métricas actuales, los eventos de escalado recientes y la razón por la que el HPA está o no está escalando. Es el primer sitio donde mirar. Para más detalle, los logs del controlador kube-controller-manager contienen información de depuración sobre las decisiones del HPA.


Conclusión

La configuración por defecto del HPA — CPU al 70%, memoria al 70% — falla de formas predecibles en la mayoría de workloads de producción. La memoria casi nunca es una señal de escalado útil porque no correlaciona con la carga en la mayoría de arquitecturas. CPU funciona bien para servicios CPU-bound con requests bien dimensionados, pero falla en escenarios I/O-bound, picos bruscos y cold-starts lentos.

El autoescalado horizontal en Kubernetes que realmente funciona en producción requiere tres cosas:

  1. Resource requests precisos — sin esto, cualquier estrategia de HPA es aleatoria.
  2. La señal correcta para el tipo de workload — CPU para servicios CPU-bound, RPS/latencia para I/O-bound, consumer lag para colas.
  3. Configuración de comportamiento explícita — ventanas de estabilización, políticas de porcentaje, PDBs.

El resto es ruido. Empieza por los requests, elige la señal adecuada para tu patrón de carga y configura el comportamiento de scale-down de forma conservadora. El autoescalado eficaz no es magia — es ingeniería aplicada con las métricas correctas.

Depurar Contenedores Distroless en Kubernetes: kubectl debug, Ephemeral Containers y Cuándo Usar Cada Técnica

Developer inspecting a distroless container with magnifying glass

El contenedor funciona perfectamente en CI. Se despliega sin problemas en staging. Luego algo falla en producción y escribes el comando de siempre: kubectl exec -it my-pod -- /bin/bash. La respuesta es inmediata: OCI runtime exec failed: exec failed: unable to start container process: exec: "/bin/bash": stat /bin/bash: no such file or directory.

Pruebas con /bin/sh. Mismo error. Pruebas con ls. Mismo error. La imagen del contenedor es distroless — contiene únicamente el binario de tu aplicación y sus dependencias de runtime, sin shell, sin gestor de paquetes, sin ningún tipo de herramienta de diagnóstico. Esto es intencionado y correcto desde el punto de vista de seguridad. También es un desafío operativo significativo la primera vez que te lo encuentras en producción.

Este artículo cubre todas las técnicas prácticas para depurar contenedores distroless en Kubernetes: kubectl debug con ephemeral containers (el enfoque estándar), estrategia pod copy (para versiones de Kubernetes sin soporte de ephemeral containers, o cuando necesitas modificar el spec del pod en ejecución), variantes de imagen debug (el atajo pragmático para desarrolladores), cdebug (una herramienta específica que simplifica el proceso), y depuración a nivel de nodo (el último recurso, con mayor poder). Para cada técnica explicaré qué puede y qué no puede hacer, qué versión de Kubernetes o permisos RBAC requiere, y en qué escenario — desarrollador en local, platform engineer en staging, ops en producción — es la opción más adecuada.

Por Qué los Contenedores Distroless Rompen el Flujo Normal de Depuración

La depuración tradicional de contenedores asume que puedes hacer exec dentro del contenedor y usar herramientas de shell: ps, netstat, strace, curl, un editor de texto. Las imágenes distroless eliminan todo esto por diseño. El proyecto Google distroless, las imágenes basadas en Wolfi de Chainguard, y el ecosistema de imágenes minimalistas en general excluyen deliberadamente todo lo que no es necesario para ejecutar la aplicación. El resultado es una superficie de ataque drásticamente reducida: sin shell no hay RCE mediante shell injection, sin gestor de paquetes no hay vía de escalada fácil, con menos binarios hay menos CVEs en el escaneo de la imagen.

El tradeoff es operativo: cuando algo falla, no puedes usar las herramientas que el propio proceso no tiene permitido ejecutar. Una aplicación Java en gcr.io/distroless/java17-debian12 tiene el JRE y nada más. Un binario de Go compilado con CGO deshabilitado y empaquetado en gcr.io/distroless/static-debian12 tiene literalmente solo el binario y los certificados CA y datos de zona horaria necesarios. No hay wget para descargar un binario de depuración, no hay apt para instalar uno, no hay bash para ejecutar un script.

Kubernetes resuelve esto a nivel de plataforma con los ephemeral containers, añadidos como estables en Kubernetes 1.25. El principio es que un contenedor de depuración — que puede tener una shell completa y cualquier herramienta que necesites — puede inyectarse en un pod en ejecución y compartir su process namespace, network namespace y montajes del sistema de ficheros sin modificar el contenedor original ni reiniciar el pod.

Opción 1: kubectl debug con Ephemeral Containers

Los ephemeral containers son la solución canónica. Desde Kubernetes 1.25 (estable), kubectl debug puede inyectar un contenedor temporal en un pod en ejecución. El contenedor comparte el network namespace del pod destino por defecto, y con --target también puede compartir el process namespace de un contenedor específico, lo que permite inspeccionar sus procesos en ejecución y file descriptors abiertos.

La invocación básica es:

kubectl debug -it my-pod \
  --image=busybox:latest \
  --target=my-container

El flag --target es la pieza crítica. Sin él, el ephemeral container tiene su propio process namespace. Con él, comparte el process namespace del contenedor especificado — lo que significa que puedes ejecutar ps aux y ver los procesos de la aplicación, usar ls -la /proc/<pid>/fd para inspeccionar file descriptors abiertos, y leer el entorno de la aplicación mediante cat /proc/<pid>/environ.

Para un entorno de depuración más potente, reemplaza busybox con una imagen más completa:

kubectl debug -it my-pod \
  --image=nicolaka/netshoot \
  --target=my-container

nicolaka/netshoot incluye tcpdump, curl, dig, nmap, ss, iperf3 y docenas de otras herramientas de diagnóstico de red, lo que la convierte en la opción estándar para escenarios de depuración de red.

Qué Puedes y Qué No Puedes Hacer

Los ephemeral containers comparten el network namespace del pod y, cuando se usa --target, el process namespace. Esto te da:

  • Visibilidad completa sobre el tráfico de red de la aplicación desde dentro del pod (tcpdump, ss, netstat)
  • Inspección de procesos mediante /proc/<pid> — ficheros abiertos, memory maps, variables de entorno, uso de CPU y memoria
  • Acceso al contexto de resolución DNS del pod — exactamente el mismo /etc/resolv.conf que ve la aplicación
  • Capacidad de realizar llamadas de red salientes desde el mismo network namespace (probar endpoints de servicios, resolución DNS)

Lo que no obtienes con ephemeral containers:

  • Acceso al sistema de ficheros del contenedor de aplicación. El ephemeral container tiene su propio root filesystem. No puedes hacer cat /app/config.yaml del sistema de ficheros del contenedor de aplicación a menos que accedas a través de /proc/<pid>/root/.
  • Capacidad de eliminar el contenedor una vez añadido. Los ephemeral containers son permanentes hasta que se elimina el pod. Esto es por diseño — la API de Kubernetes no permite eliminarlos tras su creación.
  • Modificaciones de montajes de volúmenes mediante CLI. No puedes añadir montajes de volúmenes a un ephemeral container mediante kubectl debug (aunque el spec de la API lo soporta, la CLI no expone esta opción).
  • Límites de recursos. Los ephemeral containers no soportan resource requests y limits en la CLI de kubectl debug, aunque esto está evolucionando.

Acceder al Sistema de Ficheros de la Aplicación

La sorpresa más común para los desarrolladores que se inician con ephemeral containers es que no pueden explorar directamente el sistema de ficheros del contenedor de aplicación. La solución es el sistema de ficheros /proc:

# Encontrar el PID de la aplicación
ps aux

# Explorar su sistema de ficheros mediante /proc
ls /proc/1/root/app/
cat /proc/1/root/etc/config.yaml

# O establecer el root al del sistema de ficheros de la aplicación
chroot /proc/1/root /bin/sh  # solo si /bin/sh existe en la imagen de la aplicación

La ruta /proc/<pid>/root es un enlace simbólico al root filesystem del contenedor tal como lo ve el process namespace. Dado que el ephemeral container comparte el process namespace con --target, el PID de la aplicación es normalmente 1, y /proc/1/root te da acceso completo de lectura a su sistema de ficheros.

Requisitos RBAC

Los ephemeral containers requieren el permiso de subrecurso pods/ephemeralcontainers. Esto es independiente de pods/exec, que controla kubectl exec. Un error frecuente es conceder pods/exec para propósitos de depuración sin darse cuenta de que los ephemeral containers requieren un permiso adicional:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: ephemeral-debugger
rules:
- apiGroups: [""]
  resources: ["pods/ephemeralcontainers"]
  verbs: ["update", "patch"]
- apiGroups: [""]
  resources: ["pods/attach"]
  verbs: ["create", "get"]
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list"]

En entornos de producción, este permiso debe estar estrictamente acotado: con límite de tiempo mediante RoleBinding en lugar de ClusterRoleBinding permanente, restringido a namespaces específicos, e idealmente protegido por un flujo de aprobación. El contenedor de depuración se ejecuta como root por defecto, lo que puede crear vías de escalada de privilegios si el contenedor de aplicación se ejecuta como un usuario no-root con process namespace compartido — el contenedor de debug puede adjuntarse a los procesos de la aplicación con privilegios más elevados.

Opción 2: kubectl debug –copy-to (Estrategia Pod Copy)

Cuando necesitas modificar el spec del contenedor del pod — reemplazar la imagen, cambiar variables de entorno, añadir un sidecar con un sistema de ficheros compartido — el flag --copy-to crea una copia completa del pod con tus modificaciones aplicadas:

kubectl debug my-pod \
  -it \
  --copy-to=my-pod-debug \
  --image=my-app:debug \
  --share-processes

Esto crea un nuevo pod llamado my-pod-debug que es una copia de my-pod pero con la imagen del contenedor reemplazada por my-app:debug. Si my-app:debug es tu imagen de aplicación construida con herramientas de depuración incluidas (o una variante debug de tu registro), esto te permite interactuar con el mismo binario exacto en la misma configuración que el pod original.

Un uso más habitual de --copy-to es adjuntar un contenedor de debug junto al contenedor de aplicación existente manteniendo la imagen original sin cambios:

kubectl debug my-pod \
  -it \
  --copy-to=my-pod-debug \
  --image=busybox \
  --share-processes \
  --container=debugger

Esto crea el pod copia con los contenedores originales más un nuevo contenedor debugger compartiendo el process namespace. A diferencia de los ephemeral containers, este enfoque soporta montajes de volúmenes y límites de recursos, y el pod de debug puede eliminarse de forma limpia cuando termines.

Limitaciones de la Estrategia Copy

La estrategia de copia de pods tiene una limitación crítica: no estás depurando el pod original. Crea un nuevo pod que puede comportarse de manera diferente porque:

  • No comparte el estado en memoria del pod original — si el problema es una goroutine leak o corrupción de heap que ha estado acumulándose durante horas, la copia reciente no lo exhibirá inmediatamente
  • Crea un nuevo Pod UID, lo que significa que cualquier admission webhook, network policy, o security context a nivel de pod que dependa de la identidad del pod puede aplicarse de forma diferente
  • Si el pod original está crasheando (CrashLoopBackOff), la copia también lo hará — esta técnica no sirve para depurar crashes a menos que también cambies el entrypoint

Para depurar crashes específicamente, combina --copy-to con un entrypoint modificado para mantener el contenedor activo:

kubectl debug my-crashing-pod \
  -it \
  --copy-to=my-pod-debug \
  --image=busybox \
  --share-processes \
  -- sleep 3600

Opción 3: Variantes de Imagen Debug

El enfoque más pragmático — y el más adecuado para flujos de trabajo de desarrolladores — es mantener una variante debug de tu imagen de aplicación que incluya herramientas de shell. Tanto el proyecto Google distroless como Chainguard ofrecen este patrón de forma oficial.

Las imágenes Google distroless tienen un tag :debug que añade BusyBox a la imagen:

# Imagen de producción
FROM gcr.io/distroless/java17-debian12

# Variante debug — idéntica pero con shell BusyBox
FROM gcr.io/distroless/java17-debian12:debug

Las imágenes de Chainguard siguen una convención similar con variantes :latest-dev que incluyen apk, una shell y utilidades comunes:

# Producción (sin shell, footprint mínimo)
FROM cgr.dev/chainguard/go:latest

# Variante de desarrollo/debug
FROM cgr.dev/chainguard/go:latest-dev

Si construyes tus propias imágenes base, el enfoque recomendado es usar multi-stage builds y mantener build targets separados:

FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp .

# Producción: imagen estática distroless
FROM gcr.io/distroless/static-debian12 AS production
COPY --from=builder /app/myapp /myapp
ENTRYPOINT ["/myapp"]

# Variante debug: mismo binario, con herramientas de shell
FROM gcr.io/distroless/static-debian12:debug AS debug
COPY --from=builder /app/myapp /myapp
ENTRYPOINT ["/myapp"]

En tu pipeline de CI/CD, construye ambos targets y publica my-app:${VERSION} (producción) y my-app:${VERSION}-debug (variante debug) en tu registro. La imagen debug nunca se despliega en producción por defecto, pero existe y está lista para usarse con kubectl debug --copy-to cuando sea necesario.

Consideraciones de Seguridad para las Variantes Debug

Las variantes de imagen debug anulan gran parte del beneficio de seguridad de distroless si se usan en producción, aunque sea temporalmente. Lleva un control del uso: registra cuándo se despliegan imágenes debug, requiere aprobación explícita y asegúrate de que se eliminan tras la sesión de depuración. En entornos regulados, considera si desplegar una variante debug en namespaces de producción está permitido por tu política de seguridad — en muchos casos no lo está, y debes usar ephemeral containers (que añaden un proceso de debug al pod sin modificar la imagen de la aplicación) en su lugar.

Opción 4: cdebug

cdebug es una herramienta CLI de código abierto que simplifica la depuración de contenedores distroless envolviendo kubectl debug con defaults más ergonómicos y capacidades adicionales. Su valor principal está en hacer que la depuración con ephemeral containers se sienta como una experiencia nativa de shell:

# Instalación
brew install cdebug
# o: go install github.com/iximiuz/cdebug@latest

# Depurar un pod en ejecución
cdebug exec -it my-pod

# Especificar un namespace y contenedor
cdebug exec -it -n production my-pod -c my-container

# Usar una imagen de debug específica
cdebug exec -it my-pod --image=nicolaka/netshoot

Lo que cdebug añade sobre el raw kubectl debug:

  • Chroot automático del sistema de ficheros. cdebug exec establece automáticamente el root del sistema de ficheros del contenedor de debug al del contenedor destino, de modo que navegas por / y ves los ficheros de la aplicación — no los del debug image. Esto resuelve el principal punto de fricción con kubectl debug.
  • Integración con Docker. cdebug exec funciona de forma idéntica para contenedores Docker (cdebug exec -it <container-id>), haciendo que sea el mismo flujo de trabajo para depuración local y en cluster.
  • Sin complicaciones RBAC para desarrollo local basado en Docker — útil para flujos de trabajo de desarrolladores antes de que el código llegue a Kubernetes.

El tradeoff: cdebug es una dependencia de terceros y requiere instalación. En entornos con políticas estrictas de tooling (industrias reguladas, clusters air-gapped), puede que no sea una opción. En esos casos, el flujo de trabajo raw de kubectl debug con navegación del sistema de ficheros mediante /proc/1/root es la línea base.

Opción 5: Depuración a Nivel de Nodo

Cuando todo lo demás falla — el pod está en CrashLoopBackOff demasiado rápido para adjuntarse, el problema es a nivel de kernel, o necesitas herramientas como strace que requieren privilegios elevados — la depuración a nivel de nodo te da acceso directo a los procesos del contenedor desde el nodo host.

kubectl debug node/ crea un pod privilegiado en el nodo destino que monta el root filesystem del nodo bajo /host:

kubectl debug node/my-node-name \
  -it \
  --image=nicolaka/netshoot

Desde este pod privilegiado, puedes usar nsenter para entrar en los namespaces de cualquier contenedor que se ejecute en el nodo:

# Encontrar el PID del contenedor en el nodo
# (desde dentro del pod de debug del nodo)
crictl ps | grep my-container
crictl inspect <container-id> | grep pid

# Entrar en los namespaces del contenedor
nsenter -t <pid> -m -u -i -n -p -- /bin/sh

# O solo el network namespace (para depuración de red)
nsenter -t <pid> -n -- ip a

El enfoque con nsenter te permite ejecutar herramientas del toolset del nodo o del contenedor de debug mientras operas en los namespaces del contenedor destino. Así es como ejecutas strace contra un proceso distroless: strace no está en el contenedor de aplicación, pero puedes ejecutarlo desde el nivel de nodo apuntando al PID de la aplicación:

# Trazar todas las syscalls del proceso de la aplicación
nsenter -t <pid> -- strace -p <pid> -f -e trace=network

RBAC y Seguridad para la Depuración a Nivel de Nodo

La depuración a nivel de nodo requiere nodes/proxy y la capacidad de crear pods privilegiados, lo que en la mayoría de clusters de producción está restringido a administradores del cluster. El pod de debug se ejecuta con hostPID: true y hostNetwork: true, dándole visibilidad sobre todos los procesos y el tráfico de red del nodo — no solo del contenedor destino. Esto es significativo: todos los procesos que se ejecutan en el nodo, incluidos los de namespaces de otros tenants, son visibles.

Esta técnica debe tratarse como un procedimiento de break-glass: registra el acceso, requiere doble aprobación en entornos de producción, y limpia inmediatamente tras la sesión de depuración con kubectl delete pod --selector=app=node-debugger.

Cómo Elegir el Enfoque Correcto: Matriz de Perfil de Acceso y Entorno

La técnica que debes usar depende de dos ejes: quién eres (desarrollador, platform engineer, ops/SRE) y dónde está el problema (desarrollo local, staging, producción). Los requisitos y restricciones difieren significativamente entre estas combinaciones.

Desarrollador — Cluster Local o de Desarrollo

Objetivo: Reproducir y entender un bug, inspeccionar la configuración, verificar la conectividad de red a los servicios.
Restricciones: Ninguna significativa — admin completo del cluster en el entorno local o en el namespace de desarrollo personal.
Enfoque recomendado: Variantes de imagen debug o cdebug.

En desarrollo local (Minikube, Kind, Docker Desktop), el camino más rápido es construir la variante debug de tu imagen y desplegarla directamente. Si trabajas con el servicio de otro equipo, cdebug exec te da una shell en el contenedor con el root del sistema de ficheros automático sin necesidad de RBAC especial. El objetivo es velocidad e iteración — reserva los enfoques más estructurados para entornos superiores.

Desarrollador — Cluster de Staging

Objetivo: Depurar problemas de integración, inspeccionar configuración en vivo, verificar el comportamiento específico del entorno.
Restricciones: Cluster compartido — no se pueden desplegar workloads arbitrarios en los namespaces de otros equipos, pero se tiene pods/ephemeralcontainers en el propio namespace.
Enfoque recomendado: kubectl debug con ephemeral containers (--target), acotado al propio namespace.

Staging es donde los ephemeral containers demuestran su valor. Puedes adjuntarte a un pod en ejecución sin reiniciarlo, sin modificar el spec del deployment, y sin afectar a otros usuarios del mismo cluster. Concede a los desarrolladores pods/ephemeralcontainers en los namespaces de su equipo y podrán depurar de forma autónoma sin necesitar intervención de ops.

Platform Engineer / SRE — Producción

Objetivo: Diagnosticar un incidente de producción en vivo. El pod se comporta de forma inesperada — alta latencia, crecimiento de memoria, conexiones inesperadas, respuestas incorrectas.
Restricciones: Los cambios en pods en ejecución son de alto riesgo. Cualquier despliegue de imagen debug debe estar controlado. El problema está activo y afectando a usuarios.
Enfoque recomendado: kubectl debug con ephemeral containers (los ephemeral containers no reinician el pod, no modifican el deployment y son auditables mediante los audit logs de la API).

Los requisitos clave en producción son la auditabilidad y el blast radius mínimo. Los ephemeral containers satisfacen ambos: quedan registrados en el audit log de la API de Kubernetes (quién se adjuntó, cuándo, a qué pod), no modifican el contenedor de aplicación en ejecución, y están limitados al network y process namespaces del propio pod. Documenta la sesión de depuración en tu ticket de incidencia: nombre del pod, hora, qué se observó, quién ejecutó el contenedor de debug.

La estrategia --copy-to generalmente no es apropiada para respuesta a incidentes de producción: crea un nuevo pod que puede o no exhibir el problema, añade carga al cluster durante un incidente, y si está conectado a los mismos servicios (bases de datos, APIs downstream), produce tráfico adicional que complica la investigación forense.

Platform Engineer — Producción, Problema a Nivel de Nodo

Objetivo: Diagnosticar un problema a nivel de kernel, un problema del container runtime, un problema de red que abarca múltiples pods, o una situación en la que el pod crashea demasiado rápido para adjuntarse.
Restricciones: Se requiere el máximo privilegio. Alto riesgo operativo.
Enfoque recomendado: Pod de debug a nivel de nodo con nsenter. Tratar como break-glass.

Para este escenario, crea un rol RBAC dedicado que conceda acceso a nodes/proxy y la capacidad de crear pods con hostPID: true en un namespace de debug dedicado. Vincúlalo solo a usuarios específicos, requiere un paso de autenticación separado (por ejemplo, comprobar kubectl auth can-i contra un binding de tiempo limitado), y registra todos los accesos. Este nivel de acceso debe generar una alerta estilo PagerDuty para que el equipo de seguridad sepa que hay una sesión de debug privilegiada activa en producción.

Errores Comunes y Soluciones

ErrorCausaSolución
“ephemeral containers are disabled for this cluster”Kubernetes < 1.25 o feature gate deshabilitadoActualiza el cluster o habilita el feature gate EphemeralContainers
“cannot update ephemeralcontainers”Falta permiso RBACConcede el rol pods/ephemeralcontainers descrito arriba
“container not found” con --targetNombre de contenedor incorrectoVerifica con kubectl get pod -o jsonpath='{.spec.containers[*].name}'
No se puede leer /proc/1/rootFalta CAP_SYS_PTRACEUsa el perfil PSS Baseline o añade explícitamente la capability
tcpdump no muestra tráficoCapturando en la interfaz equivocadaUsa tcpdump -i any para capturar en todas las interfaces

“ephemeral containers are disabled for this cluster”

Los ephemeral containers requieren Kubernetes 1.16+ (alpha, tras feature gate) y son estables desde la versión 1.25. Si estás en la versión 1.16–1.22, necesitas habilitar el feature gate EphemeralContainers en el API server y en kubelet. Desde la versión 1.23 era beta y estaba habilitado por defecto. Desde la versión 1.25 es estable y siempre está activo. En servicios de Kubernetes gestionados (EKS, GKE, AKS), comprueba la versión del cluster — las versiones anteriores a la 1.25 pueden tenerlo deshabilitado según tu configuración.

“cannot update ephemeralcontainers” (RBAC)

Tienes pods/exec pero no pods/ephemeralcontainers. Añade el permiso mostrado en la sección RBAC anterior. Ten en cuenta que pods/exec y pods/ephemeralcontainers son subrecursos separados — tener uno no implica el otro.

“container not found” con –target

El nombre de contenedor en --target debe coincidir exactamente con el nombre del contenedor tal como está definido en el spec del Pod — no el nombre de la imagen. Compruébalo con kubectl get pod my-pod -o jsonpath='{.spec.containers[*].name}' para obtener los nombres exactos de los contenedores.

Puedo ver procesos pero no puedo leer /proc/1/root

El contenedor de aplicación se ejecuta como usuario no-root (por ejemplo, UID 1000) y el ephemeral container se ejecuta como root. El sistema de ficheros de la aplicación puede tener ficheros propiedad de UID 1000 que no son legibles por otros UIDs según los permisos. La propia ruta /proc/<pid>/root requiere la capability CAP_SYS_PTRACE. Si los PodSecurityStandards (PSS) de tu cluster están configurados como restricted, el contenedor de debug puede no tener esta capability. Usa el perfil PSS Baseline para namespaces de debug o añade explícitamente SYS_PTRACE al securityContext del ephemeral container.

tcpdump no muestra tráfico

Cuando uses nicolaka/netshoot para depuración de red, asegúrate de que el ephemeral container se crea sin --target si tu objetivo es capturar todo el tráfico en la interfaz de red del pod (no solo el del proceso del contenedor específico). Con --target, compartes el process namespace pero el network namespace se comparte a nivel de pod independientemente. Ejecuta tcpdump -i any para capturar en todas las interfaces incluyendo loopback, que es por donde viaja el tráfico entre contenedores dentro de un pod.

Marco de Decisión

Usa esto como punto de partida para seleccionar la técnica adecuada a tu situación:

EscenarioTécnicaRequisito
Incidente de producción activo, pod en ejecuciónkubectl debug + ephemeral containerRBAC pods/ephemeralcontainers, k8s 1.25+
Pod crasheando demasiado rápido para adjuntarsekubectl debug --copy-to + entrypoint modificadoCapacidad de crear pods en el namespace
Desarrollador depurando en dev/stagingcdebug exec o kubectl debugpods/ephemeralcontainers o pod create
Necesitas acceso completo al sistema de ficheroskubectl debug --copy-to + variante debugImagen debug en registro, pod create
Necesitas strace o kernel tracingDebug a nivel de nodo con nsenternodes/proxy, equivalente a cluster admin
Captura de paquetes de redkubectl debug + nicolaka/netshootpods/ephemeralcontainers
Depuración local en Dockercdebug exec <container-id>Acceso al Docker socket
Entorno de debug reproducible en CIVariante debug en build target separadoTag de imagen separado en registro

Diseño RBAC para Producción

Un diseño RBAC limpio para la depuración de contenedores distroless en producción separa tres roles con diferentes niveles de privilegio:

# Tier 1: Autoservicio del desarrollador en namespaces de equipo
# Permite adjuntar ephemeral containers, sin acceso a nodo
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: distroless-debugger
  namespace: team-namespace
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list"]
- apiGroups: [""]
  resources: ["pods/ephemeralcontainers"]
  verbs: ["update", "patch"]
- apiGroups: [""]
  resources: ["pods/attach"]
  verbs: ["create", "get"]
---
# Tier 2: Acceso SRE a incidentes de producción
# Ephemeral containers en todos los namespaces
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: sre-distroless-debugger
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list"]
- apiGroups: [""]
  resources: ["pods/ephemeralcontainers"]
  verbs: ["update", "patch"]
- apiGroups: [""]
  resources: ["pods/attach"]
  verbs: ["create", "get"]
---
# Tier 3: Acceso break-glass a nodo
# Solo para el equipo de plataforma, binding de tiempo limitado recomendado
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: node-debugger
rules:
- apiGroups: [""]
  resources: ["nodes/proxy"]
  verbs: ["get"]
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["create", "get", "list", "delete"]
  # Restringir al namespace de debug mediante RoleBinding, no ClusterRoleBinding

Vincula el Tier 1 permanentemente a tus desarrolladores. Vincula el Tier 2 a los SREs de forma permanente pero con alertas de auditoría sobre su uso. Vincula el Tier 3 solo bajo demanda (mediante un operador de Kubernetes que cree RoleBinding de tiempo limitado) y nunca como ClusterRoleBinding permanente.

La clave de este modelo de tres niveles es que cada escalada de privilegios es visible y tiene coste operativo. Un desarrollador puede depurar su propio namespace sin coordinación. Un SRE que necesita acceso a producción usa el Tier 2, que queda registrado en los audit logs. Un platform engineer que necesita acceso a nivel de nodo activa el Tier 3, que genera alertas y requiere un binding temporal — haciendo que el acceso privilegiado sea auditable y difícil de mantener silenciosamente durante más tiempo del necesario.

Conclusión

Los contenedores distroless son la elección correcta para workloads de producción. Reducen la superficie de ataque, eliminan CVEs innecesarios y obligan a una separación más limpia entre aplicación y tooling. El coste operativo es que tu flujo de trabajo habitual de depuración — exec dentro del contenedor, ejecutar algunos comandos — ya no funciona por defecto.

Kubernetes ofrece una respuesta clara con los ephemeral containers y kubectl debug: inyecta un contenedor de debug con las herramientas que necesites en el pod en ejecución, compartiendo sus namespaces de red y de procesos, sin reiniciar ni modificar la aplicación. Para escenarios donde los ephemeral containers son insuficientes — acceso al sistema de ficheros, depuración de crashes, investigación a nivel de kernel — la estrategia copy y el debug a nivel de nodo cubren los huecos restantes.

La clave para que esto funcione a escala no es la técnica en sí, sino el modelo de acceso: los desarrolladores tienen acceso de autoservicio a ephemeral containers en sus propios namespaces, los SREs tienen acceso a ephemeral containers en todo el cluster para incidentes de producción, y el acceso a nivel de nodo es un procedimiento de break-glass con trail de auditoría y límites de tiempo. Con ese modelo en su lugar, los contenedores distroless se convierten en una cuestión operativa resuelta, no en un obstáculo.

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.) 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-nginxParcialAltaMigración desde NGINX, herenciaBaja
TraefikAltaMigración desde ingress-nginxBaja
Envoy GatewayParcialSí (nativa)Media-AltaNuevos clusters, plataformasMedia
Cilium GatewayNoMediaClusters con Cilium CNIMedia
HAProxy IngressLimitadaAltaEquipos con expertise HAProxyMedia
Kong IngressAltaAPI gateway + ingressAlta
Istio GatewayAltaService mesh + ingressMuy alta
NGINX Gateway FabricNoMediaEcosistema 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.