Saltar al contenido

Migrando de Kubernetes Ingress a Gateway API con Envoy Gateway y observabilidad end-to-end

Augusto Mancuso
Published date:

Migrando de Kubernetes Ingress a Gateway API con Envoy Gateway y observabilidad end-to-end

Nota: este artículo muestra una implementación anonimizada y generalizada. Los dominios, IPs, nombres de namespaces, nombres de aplicaciones y secretos fueron reemplazados por valores de ejemplo. La idea es explicar el enfoque técnico sin exponer información sensible de ningún entorno real.

Durante las últimas semanas estuve trabajando en una migración end-to-end desde Kubernetes Ingress hacia Gateway API para tres ambientes: internal, dev y testing.

A primera vista puede parecer un cambio simple: dejar de usar objetos Ingress y empezar a publicar servicios con HTTPRoute. Pero en la práctica la migración fue bastante más amplia.

No se trató solamente de cambiar un recurso de Kubernetes por otro. El objetivo fue diseñar una capa de entrada HTTP más ordenada, observable y operable, separando mejor las responsabilidades entre plataforma e implementación de aplicaciones.

La migración incluyó:

El resultado fue una capa de entrada que no solo enruta tráfico, sino que también permite responder preguntas operativas importantes:


1. Contexto: por qué migrar desde Ingress

Ingress resolvió durante años una necesidad muy concreta: exponer tráfico HTTP/HTTPS hacia servicios dentro de Kubernetes.

El problema es que, a medida que crece la plataforma, aparecen limitaciones prácticas:

Gateway API propone un modelo más explícito:

En esta implementación usé Envoy Gateway como controller, por lo que además aparecen recursos propios de Envoy Gateway:

Una distinción importante:

RecursoAPIUso
GatewayClassGateway API estándarDefine el controller disponible.
GatewayGateway API estándarDefine el punto de entrada y sus listeners.
HTTPRouteGateway API estándarPublica rutas HTTP hacia Services.
ReferenceGrantGateway API estándarAutoriza referencias cross-namespace.
EnvoyProxyExtensión de Envoy GatewayConfigura deployment, service, HPA, PDB, telemetry y tracing de Envoy.
ClientTrafficPolicyExtensión de Envoy GatewayDefine comportamiento del tráfico cliente hacia el Gateway.
BackendTrafficPolicyExtensión de Envoy GatewayDefine timeouts, retries, circuit breaking y load balancing hacia backends.
SecurityPolicyExtensión de Envoy GatewayDefine CORS, auth u otras políticas de seguridad soportadas por Envoy Gateway.

2. Objetivo de la arquitectura

La migración buscó resolver varios puntos al mismo tiempo:

  1. Separar ambientes
    Cada ambiente debía tener su propio Gateway, IP y reglas de publicación.

  2. Separar responsabilidades
    El equipo de plataforma administra Gateway, EnvoyProxy, certificados, políticas globales, observabilidad y permisos.
    Los equipos de desarrollo publican sus aplicaciones mediante HTTPRoute.

  3. Evitar configuraciones manuales por aplicación
    Una aplicación no debería tener que configurar Envoy directamente para obtener logs, métricas y trazas básicas.

  4. Estandarizar la publicación de rutas
    Las aplicaciones exponen servicios usando un patrón común: HTTPRoute -> Service -> Pods.

  5. Mejorar la operación
    La plataforma debe poder observar tráfico, errores, latencia, upstreams, certificados y trazas desde el Gateway.

  6. Preparar una base más cercana a Platform Engineering
    La idea no es solamente exponer aplicaciones, sino construir una capacidad interna reusable.


3. Arquitectura general

La arquitectura simplificada queda así:

flowchart LR
    Client[Cliente / navegador / API client]
    DNS[DNS / Edge / Load Balancer externo]
    MetalLB[MetalLB IP dedicada]
    EG[Envoy Gateway]
    GW[Gateway + Listeners]
    HR[HTTPRoute]
    SVC[Kubernetes Service]
    POD[Pods de aplicación]

    Prom[Prometheus]
    Loki[Loki]
    Tempo[Tempo]
    OTel[OpenTelemetry Collector]

    Client --> DNS --> MetalLB --> EG --> GW --> HR --> SVC --> POD

    EG -->|métricas| Prom
    EG -->|access logs JSON| Loki
    EG -->|trazas OTLP| OTel --> Tempo
    POD -->|spans opcionales| OTel

La ruta de tráfico es:

Cliente
  -> DNS / Edge
  -> IP LoadBalancer asignada por MetalLB
  -> Envoy Gateway
  -> Gateway Listener
  -> HTTPRoute
  -> Service
  -> Pod

La ruta de observabilidad es:

Envoy Gateway
  -> métricas Prometheus
  -> access logs JSON recolectados por Loki
  -> trazas OTLP hacia OpenTelemetry Collector / Tempo

Si la aplicación también está instrumentada con OpenTelemetry, entonces la traza no termina en el Gateway. Continúa dentro del backend y permite ver el flujo completo.


4. Layout lógico de ambientes

La implementación separa tres ambientes:

AmbienteGatewayEnvoyProxyDominio ejemploIP ejemplo
internalinternal-gwinternal-proxy*.apps.example.com192.0.2.10
devdev-gwdev-proxy*.dev.apps.example.com192.0.2.11
testingtesting-gwtesting-proxy*.testing.apps.example.com192.0.2.12

192.0.2.0/24 pertenece al rango reservado para documentación. En un entorno real se reemplaza por IPs de la red interna o del pool correspondiente.


5. Prerrequisitos

Antes de aplicar estos manifiestos asumí lo siguiente:

Validaciones iniciales:

kubectl get gatewayclass
kubectl get crd | grep gateway
kubectl get pods -n envoy-gateway-system
kubectl get pods -n metallb-system
kubectl get pods -n monitoring

6. MetalLB: IPs dedicadas por Gateway

Para que cada Gateway tenga una IP estable, definí pools dedicados en MetalLB.

apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: envoy-gw-l2-internal
  namespace: metallb-system
spec:
  ipAddressPools:
    - envoy-gw-pool-internal
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: envoy-gw-pool-internal
  namespace: metallb-system
spec:
  addresses:
    - 192.0.2.10/32
  autoAssign: false
  avoidBuggyIPs: false
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: envoy-gw-l2-dev
  namespace: metallb-system
spec:
  ipAddressPools:
    - envoy-gw-pool-dev
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: envoy-gw-pool-dev
  namespace: metallb-system
spec:
  addresses:
    - 192.0.2.11/32
  autoAssign: false
  avoidBuggyIPs: false
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: envoy-gw-l2-testing
  namespace: metallb-system
spec:
  ipAddressPools:
    - envoy-gw-pool-testing
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: envoy-gw-pool-testing
  namespace: metallb-system
spec:
  addresses:
    - 192.0.2.12/32
  autoAssign: false
  avoidBuggyIPs: false

El punto importante es autoAssign: false. Eso evita que MetalLB asigne estas IPs a cualquier Service LoadBalancer. Las IPs quedan reservadas para los Gateways.


7. EnvoyProxy: configuración del plano de datos

EnvoyProxy es una extensión de Envoy Gateway. Me permitió controlar cómo se despliega Envoy y cómo se comporta a nivel operativo.

En esta capa configuré:

Ejemplo parametrizado para un ambiente:

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
  name: internal-proxy
  namespace: envoy-gateway-system
  labels:
    platform.example.com/gateway-role: internal
    observability.platform.example.com/enabled: "true"
spec:
  logging:
    level:
      default: warn
  telemetry:
    metrics:
      prometheus:
        disable: false
      sinks:
        - type: OpenTelemetry
          openTelemetry:
            host: otel-collector.monitoring.svc.cluster.local
            port: 4317
    accessLog:
      settings:
        - format:
            type: JSON
            json:
              start_time: "%START_TIME%"
              method: "%REQ(:METHOD)%"
              scheme: "%REQ(:SCHEME)%"
              authority: "%REQ(:AUTHORITY)%"
              path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"
              protocol: "%PROTOCOL%"
              response_code: "%RESPONSE_CODE%"
              response_code_details: "%RESPONSE_CODE_DETAILS%"
              response_flags: "%RESPONSE_FLAGS%"
              connection_termination_details: "%CONNECTION_TERMINATION_DETAILS%"
              upstream_transport_failure_reason: "%UPSTREAM_TRANSPORT_FAILURE_REASON%"
              bytes_received: "%BYTES_RECEIVED%"
              bytes_sent: "%BYTES_SENT%"
              duration_ms: "%DURATION%"
              upstream_service_time_ms: "%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%"
              downstream_remote_address: "%DOWNSTREAM_REMOTE_ADDRESS%"
              downstream_direct_remote_address: "%DOWNSTREAM_DIRECT_REMOTE_ADDRESS%"
              x_forwarded_for: "%REQ(X-FORWARDED-FOR)%"
              user_agent: "%REQ(USER-AGENT)%"
              request_id: "%REQ(X-REQUEST-ID)%"
              trace_id: "%TRACE_ID%"
              traceparent: "%REQ(TRACEPARENT)%"
              requested_server_name: "%REQUESTED_SERVER_NAME%"
              upstream_host: "%UPSTREAM_HOST%"
              upstream_cluster: "%UPSTREAM_CLUSTER%"
              route_name: "%ROUTE_NAME%"
              virtual_cluster_name: "%VIRTUAL_CLUSTER_NAME%"
              gateway_name: "internal-gw"
              gateway_namespace: "envoy-gateway-system"
              environment: "internal"
          sinks:
            - type: File
              file:
                path: /dev/stdout
    tracing:
      samplingRate: 100
      provider:
        type: OpenTelemetry
        backendRefs:
          - name: otel-collector
            namespace: monitoring
            port: 4317
      spanName:
        client: "%REQ(:METHOD)% %REQ(:AUTHORITY)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% -> %UPSTREAM_CLUSTER%"
        server: "%REQ(:METHOD)% %REQ(:AUTHORITY)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% -> %UPSTREAM_CLUSTER%"
      tags:
        gateway.name: internal-gw
        gateway.namespace: envoy-gateway-system
        deployment.environment: internal
        http.method: "%REQ(:METHOD)%"
        http.authority: "%REQ(:AUTHORITY)%"
        http.path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"
        http.protocol: "%PROTOCOL%"
        http.status_code: "%RESPONSE_CODE%"
        http.request_id: "%REQ(X-REQUEST-ID)%"
        trace.id: "%TRACE_ID%"
        route.name: "%ROUTE_NAME%"
        upstream.cluster: "%UPSTREAM_CLUSTER%"
        upstream.host: "%UPSTREAM_HOST%"
        upstream.transport_failure_reason: "%UPSTREAM_TRANSPORT_FAILURE_REASON%"
        response.flags: "%RESPONSE_FLAGS%"
        response.code_details: "%RESPONSE_CODE_DETAILS%"
        k8s.pod.name: "%ENVIRONMENT(ENVOY_POD_NAME)%"
        k8s.namespace.name: "%ENVIRONMENT(ENVOY_POD_NAMESPACE)%"
  provider:
    type: Kubernetes
    kubernetes:
      envoyDeployment:
        strategy:
          type: RollingUpdate
          rollingUpdate:
            maxUnavailable: 0
            maxSurge: 1
        pod:
          topologySpreadConstraints:
            - maxSkew: 1
              topologyKey: kubernetes.io/hostname
              whenUnsatisfiable: ScheduleAnyway
              labelSelector:
                matchLabels:
                  gateway.env: internal
                  gateway.type: internal
          labels:
            gateway.env: internal
            gateway.type: internal
            observability.platform.example.com/enabled: "true"
        container:
          resources:
            requests:
              cpu: 100m
              memory: 256Mi
            limits:
              cpu: 1000m
              memory: 1Gi
      envoyHpa:
        minReplicas: 2
        maxReplicas: 5
        behavior:
          scaleDown:
            stabilizationWindowSeconds: 300
            policies:
              - type: Percent
                value: 50
                periodSeconds: 60
          scaleUp:
            stabilizationWindowSeconds: 60
            policies:
              - type: Percent
                value: 100
                periodSeconds: 60
              - type: Pods
                value: 2
                periodSeconds: 60
            selectPolicy: Max
        metrics:
          - type: Resource
            resource:
              name: cpu
              target:
                type: Utilization
                averageUtilization: 70
          - type: Resource
            resource:
              name: memory
              target:
                type: Utilization
                averageUtilization: 80
      envoyPDB:
        minAvailable: 1
      envoyService:
        type: LoadBalancer
        externalTrafficPolicy: Local
        labels:
          gateway.env: internal
          gateway.type: internal
          observability.platform.example.com/enabled: "true"

Para dev y testing usé el mismo patrón, cambiando:


8. Gateways por ambiente

Cada ambiente tiene su propio Gateway.

8.1 Gateway interno

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: internal-gw
  namespace: envoy-gateway-system
  labels:
    platform.example.com/gateway-role: internal
    observability.platform.example.com/enabled: "true"
spec:
  gatewayClassName: eg
  infrastructure:
    parametersRef:
      group: gateway.envoyproxy.io
      kind: EnvoyProxy
      name: internal-proxy
  addresses:
    - type: IPAddress
      value: 192.0.2.10
  listeners:
    - name: http-internal
      protocol: HTTP
      port: 80
      hostname: "*.apps.example.com"
      allowedRoutes:
        namespaces:
          from: Same
    - name: https-internal
      protocol: HTTPS
      port: 443
      hostname: "*.apps.example.com"
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: internal-wildcard-tls
      allowedRoutes:
        namespaces:
          from: Selector
          selector:
            matchLabels:
              gateway.platform.example.com/internal: "true"
              platform.example.com/gateway-access: internal

Redirección HTTP a HTTPS:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: internal-http-to-https-redirect
  namespace: envoy-gateway-system
spec:
  parentRefs:
    - name: internal-gw
      sectionName: http-internal
  hostnames:
    - "*.apps.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      filters:
        - type: RequestRedirect
          requestRedirect:
            scheme: https
            port: 443
            statusCode: 301

8.2 Gateway dev

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: dev-gw
  namespace: envoy-gateway-system
  labels:
    platform.example.com/gateway-role: dev
spec:
  gatewayClassName: eg
  infrastructure:
    parametersRef:
      group: gateway.envoyproxy.io
      kind: EnvoyProxy
      name: dev-proxy
  addresses:
    - type: IPAddress
      value: 192.0.2.11
  listeners:
    - name: http-dev
      protocol: HTTP
      port: 80
      hostname: "*.dev.apps.example.com"
      allowedRoutes:
        namespaces:
          from: Selector
          selector:
            matchLabels:
              platform.example.com/gateway-access: dev
    - name: https-dev
      protocol: HTTPS
      port: 443
      hostname: "*.dev.apps.example.com"
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: dev-wildcard-tls
      allowedRoutes:
        namespaces:
          from: Selector
          selector:
            matchLabels:
              platform.example.com/gateway-access: dev

8.3 Gateway testing

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: testing-gw
  namespace: envoy-gateway-system
  labels:
    platform.example.com/gateway-role: testing
spec:
  gatewayClassName: eg
  infrastructure:
    parametersRef:
      group: gateway.envoyproxy.io
      kind: EnvoyProxy
      name: testing-proxy
  addresses:
    - type: IPAddress
      value: 192.0.2.12
  listeners:
    - name: http-testing
      protocol: HTTP
      port: 80
      hostname: "*.testing.apps.example.com"
      allowedRoutes:
        namespaces:
          from: Selector
          selector:
            matchLabels:
              platform.example.com/gateway-access: testing
    - name: https-testing
      protocol: HTTPS
      port: 443
      hostname: "*.testing.apps.example.com"
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: testing-wildcard-tls
      allowedRoutes:
        namespaces:
          from: Selector
          selector:
            matchLabels:
              platform.example.com/gateway-access: testing

9. Control de publicación por namespace

Uno de los puntos más importantes de Gateway API es que permite controlar qué namespaces pueden adjuntar rutas a un Gateway.

Ejemplo:

apiVersion: v1
kind: Namespace
metadata:
  name: app-internal
  labels:
    gateway.platform.example.com/internal: "true"
    platform.example.com/gateway-access: internal
    environment: internal
    owner: platform-team
---
apiVersion: v1
kind: Namespace
metadata:
  name: app-dev
  labels:
    platform.example.com/gateway-access: dev
    environment: dev
    owner: backend-team
---
apiVersion: v1
kind: Namespace
metadata:
  name: app-testing
  labels:
    platform.example.com/gateway-access: testing
    environment: testing
    owner: backend-team

Estas labels funcionan como un contrato de publicación.

Un namespace sin la label correcta puede crear un HTTPRoute, pero ese route no debería ser aceptado por el Gateway correspondiente.

Validación:

kubectl get httproute -A
kubectl describe httproute -n app-dev app-demo
kubectl get gateway -n envoy-gateway-system
kubectl describe gateway -n envoy-gateway-system dev-gw

En el status de HTTPRoute conviene revisar condiciones como:


10. ReferenceGrant para certificados TLS cross-namespace

Si los certificados TLS están en el mismo namespace del Gateway, no hace falta ReferenceGrant.

Pero si el Gateway está en envoy-gateway-system y los Secrets TLS están en namespaces de aplicaciones, entonces Gateway API requiere una autorización explícita con ReferenceGrant.

Ejemplo:

apiVersion: gateway.networking.k8s.io/v1
kind: ReferenceGrant
metadata:
  name: allow-dev-gateway-tls
  namespace: app-dev
spec:
  from:
    - group: gateway.networking.k8s.io
      kind: Gateway
      namespace: envoy-gateway-system
  to:
    - group: ""
      kind: Secret
      name: dev-wildcard-tls
    - group: ""
      kind: Secret
      name: app-specific-tls

Para testing:

apiVersion: gateway.networking.k8s.io/v1
kind: ReferenceGrant
metadata:
  name: allow-testing-gateway-tls
  namespace: app-testing
spec:
  from:
    - group: gateway.networking.k8s.io
      kind: Gateway
      namespace: envoy-gateway-system
  to:
    - group: ""
      kind: Secret
      name: testing-wildcard-tls

Esto es sano desde el punto de vista operativo: el namespace dueño del recurso referenciado decide quién puede consumirlo.


11. Políticas de tráfico

Separé dos tipos de políticas:

11.1 ClientTrafficPolicy

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: ClientTrafficPolicy
metadata:
  name: internal-gw-client-policy
  namespace: envoy-gateway-system
  labels:
    platform.example.com/gateway-role: internal
    platform.example.com/review-required: "true"
spec:
  targetRefs:
    - group: gateway.networking.k8s.io
      kind: Gateway
      name: internal-gw
  tls:
    minVersion: "1.2"
    maxVersion: "1.3"
    alpnProtocols:
      - h2
      - http/1.1
  headers:
    requestID: PreserveOrGenerate
    enableEnvoyHeaders: false
    withUnderscoresAction: RejectRequest
  timeout:
    http:
      requestReceivedTimeout: 30s
      streamIdleTimeout: 5m
      idleTimeout: 1h
  connection:
    bufferLimit: 1Mi
    maxAcceptPerSocketEvent: 32
  tcpKeepalive:
    idleTime: 300s
    interval: 30s
    probes: 3

Puntos interesantes:

11.2 BackendTrafficPolicy global del Gateway

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: internal-gw-backend-policy
  namespace: envoy-gateway-system
  labels:
    platform.example.com/gateway-role: internal
    platform.example.com/review-required: "true"
spec:
  targetRefs:
    - group: gateway.networking.k8s.io
      kind: Gateway
      name: internal-gw
  timeout:
    http:
      requestTimeout: 60s
      streamIdleTimeout: 5m
      connectionIdleTimeout: 1h
  retry:
    numRetries: 2
    perRetry:
      timeout: 15s
    retryOn:
      triggers:
        - connect-failure
        - refused-stream
        - unavailable
        - cancelled
        - retriable-status-codes
      httpStatusCodes:
        - 503
  circuitBreaker:
    maxConnections: 4096
    maxPendingRequests: 1024
    maxParallelRequests: 8192
    maxParallelRetries: 256
    maxRequestsPerConnection: 0
  loadBalancer:
    type: LeastRequest

Esta política define valores conservadores para el Gateway.

Para rutas específicas, un equipo puede proponer una política más puntual sobre su HTTPRoute, por ejemplo:

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: app-demo-backend-policy
  namespace: app-dev
spec:
  targetRefs:
    - group: gateway.networking.k8s.io
      kind: HTTPRoute
      name: app-demo
  timeout:
    http:
      requestTimeout: 60s
      streamIdleTimeout: 5m
      connectionIdleTimeout: 1h
  retry:
    numRetries: 2
    perRetry:
      timeout: 15s
    retryOn:
      triggers:
        - connect-failure
        - refused-stream
        - unavailable
        - cancelled
        - retriable-status-codes
      httpStatusCodes:
        - 503
  circuitBreaker:
    maxConnections: 1024
    maxPendingRequests: 256
    maxParallelRequests: 2048
    maxParallelRetries: 64
  loadBalancer:
    type: LeastRequest

Para aplicaciones con WebSocket, SSE, streaming o uploads grandes, estos valores deben revisarse con cuidado. No conviene aplicar timeouts o buffering de forma global sin entender el patrón de tráfico.


12. Publicación de aplicaciones con HTTPRoute

El contrato para los equipos de desarrollo es simple:

  1. La aplicación tiene un Deployment.
  2. La aplicación expone un Service.
  3. El equipo publica un HTTPRoute hacia el Gateway del ambiente.
  4. El namespace debe tener la label que habilita adjuntar rutas a ese Gateway.

12.1 Ejemplo dev

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-demo
  namespace: app-dev
  labels:
    app: app-demo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: app-demo
  template:
    metadata:
      labels:
        app: app-demo
    spec:
      containers:
        - name: app
          image: nginx:alpine
          ports:
            - name: http
              containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: app-demo
  namespace: app-dev
  labels:
    app: app-demo
spec:
  selector:
    app: app-demo
  ports:
    - name: http
      port: 80
      targetPort: 80
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-demo
  namespace: app-dev
  labels:
    app.kubernetes.io/name: app-demo
    platform.example.com/gateway-role: dev
spec:
  parentRefs:
    - name: dev-gw
      namespace: envoy-gateway-system
      sectionName: https-dev
  hostnames:
    - app-demo.dev.apps.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: app-demo
          port: 80

12.2 Ejemplo testing

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-demo
  namespace: app-testing
  labels:
    app.kubernetes.io/name: app-demo
    platform.example.com/gateway-role: testing
spec:
  parentRefs:
    - name: testing-gw
      namespace: envoy-gateway-system
      sectionName: https-testing
  hostnames:
    - app-demo.testing.apps.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: app-demo
          port: 80

12.3 Ejemplo internal

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-demo
  namespace: app-internal
  labels:
    app.kubernetes.io/name: app-demo
    platform.example.com/gateway-role: internal
spec:
  parentRefs:
    - name: internal-gw
      namespace: envoy-gateway-system
      sectionName: https-internal
  hostnames:
    - app-demo.apps.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: app-demo
          port: 80

Con esta definición, Envoy ya puede generar información útil como:


13. SecurityPolicy por ruta

Para algunos casos puede ser útil aplicar una SecurityPolicy propia de Envoy Gateway.

Ejemplo con CORS:

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
  name: app-demo-security-policy
  namespace: app-dev
  labels:
    app.kubernetes.io/name: app-demo
    platform.example.com/gateway-role: dev
spec:
  targetRefs:
    - group: gateway.networking.k8s.io
      kind: HTTPRoute
      name: app-demo
  cors:
    allowMethods:
      - GET
      - POST
      - PUT
      - PATCH
      - DELETE
      - OPTIONS
    allowHeaders:
      - Authorization
      - Content-Type
      - X-Requested-With
    exposeHeaders:
      - X-Request-Id
    maxAge: 24h
    allowCredentials: true

En mi caso, la idea no fue mover toda la lógica de seguridad a las rutas, sino dejar preparada la capacidad para aplicar políticas por aplicación cuando tenga sentido.


14. Observabilidad: métricas, logs y trazas

La observabilidad fue una parte central de la migración.

El Gateway entrega visibilidad base para todo el tráfico que pasa por Envoy:

La aplicación no necesita configurar Envoy para obtener estos datos básicos. Solo necesita publicar su servicio con HTTPRoute.

Si además queremos ver spans internos del backend, entonces la aplicación sí debe estar instrumentada con OpenTelemetry y propagar el contexto.


15. Prometheus dedicado para Envoy Gateway

Una opción que usé fue tener un Prometheus dedicado para Envoy Gateway.

Esto permite separar métricas de Gateway de otras métricas generales del cluster, al menos en una primera etapa.

15.1 RBAC

apiVersion: v1
kind: ServiceAccount
metadata:
  name: prometheus-envoy
  namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: prometheus-envoy
rules:
  - apiGroups: [""]
    resources:
      - nodes
      - nodes/proxy
      - services
      - endpoints
      - pods
    verbs:
      - get
      - list
      - watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: prometheus-envoy
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: prometheus-envoy
subjects:
  - kind: ServiceAccount
    name: prometheus-envoy
    namespace: monitoring

15.2 Configuración de scrape

apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-envoy-config
  namespace: monitoring
data:
  prometheus.yml: |
    global:
      scrape_interval: 15s
      evaluation_interval: 15s

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

    alerting:
      alertmanagers:
        - static_configs:
            - targets:
                - alertmanager-envoy.monitoring.svc.cluster.local:9093

    scrape_configs:
      - job_name: envoy-gateway-internal
        kubernetes_sd_configs:
          - role: pod
            namespaces:
              names:
                - envoy-gateway-system
        relabel_configs:
          - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
            action: keep
            regex: "true"
          - source_labels: [__meta_kubernetes_pod_label_gateway_env]
            action: keep
            regex: internal
          - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
            action: replace
            target_label: __metrics_path__
            regex: (.+)
          - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
            action: replace
            target_label: __address__
            regex: ([^:]+)(?::\d+)?;(\d+)
            replacement: $1:$2
          - source_labels: [__meta_kubernetes_namespace]
            target_label: kubernetes_namespace
          - source_labels: [__meta_kubernetes_pod_name]
            target_label: kubernetes_pod_name
          - source_labels: [__meta_kubernetes_pod_label_gateway_envoyproxy_io_owning_gateway_name]
            target_label: gateway
          - source_labels: [__meta_kubernetes_pod_label_gateway_type]
            target_label: gateway_type

      - job_name: envoy-gateway-dev
        kubernetes_sd_configs:
          - role: pod
            namespaces:
              names:
                - envoy-gateway-system
        relabel_configs:
          - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
            action: keep
            regex: "true"
          - source_labels: [__meta_kubernetes_pod_label_gateway_env]
            action: keep
            regex: dev
          - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
            action: replace
            target_label: __metrics_path__
            regex: (.+)
          - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
            action: replace
            target_label: __address__
            regex: ([^:]+)(?::\d+)?;(\d+)
            replacement: $1:$2
          - source_labels: [__meta_kubernetes_namespace]
            target_label: kubernetes_namespace
          - source_labels: [__meta_kubernetes_pod_name]
            target_label: kubernetes_pod_name
          - source_labels: [__meta_kubernetes_pod_label_gateway_envoyproxy_io_owning_gateway_name]
            target_label: gateway
          - source_labels: [__meta_kubernetes_pod_label_gateway_type]
            target_label: gateway_type

      - job_name: envoy-gateway-testing
        kubernetes_sd_configs:
          - role: pod
            namespaces:
              names:
                - envoy-gateway-system
        relabel_configs:
          - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
            action: keep
            regex: "true"
          - source_labels: [__meta_kubernetes_pod_label_gateway_env]
            action: keep
            regex: testing
          - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
            action: replace
            target_label: __metrics_path__
            regex: (.+)
          - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
            action: replace
            target_label: __address__
            regex: ([^:]+)(?::\d+)?;(\d+)
            replacement: $1:$2
          - source_labels: [__meta_kubernetes_namespace]
            target_label: kubernetes_namespace
          - source_labels: [__meta_kubernetes_pod_name]
            target_label: kubernetes_pod_name
          - source_labels: [__meta_kubernetes_pod_label_gateway_envoyproxy_io_owning_gateway_name]
            target_label: gateway
          - source_labels: [__meta_kubernetes_pod_label_gateway_type]
            target_label: gateway_type

      - job_name: otel-servicegraph
        metrics_path: /metrics
        static_configs:
          - targets:
              - otel-collector.monitoring.svc.cluster.local:8889
            labels:
              kubernetes_namespace: monitoring
              app: otel-servicegraph

15.3 Deployment y Service de Prometheus

apiVersion: apps/v1
kind: Deployment
metadata:
  name: prometheus-envoy
  namespace: monitoring
  labels:
    app: prometheus-envoy
spec:
  replicas: 1
  selector:
    matchLabels:
      app: prometheus-envoy
  template:
    metadata:
      labels:
        app: prometheus-envoy
    spec:
      serviceAccountName: prometheus-envoy
      containers:
        - name: prometheus
          image: quay.io/prometheus/prometheus:v3.5.3
          args:
            - --config.file=/etc/prometheus/prometheus.yml
            - --storage.tsdb.path=/prometheus
            - --storage.tsdb.retention.time=15d
            - --web.enable-lifecycle
          ports:
            - name: web
              containerPort: 9090
          volumeMounts:
            - name: config
              mountPath: /etc/prometheus
            - name: rules
              mountPath: /etc/prometheus/rules
            - name: data
              mountPath: /prometheus
          resources:
            requests:
              cpu: 100m
              memory: 256Mi
            limits:
              cpu: 1000m
              memory: 1Gi
      volumes:
        - name: config
          configMap:
            name: prometheus-envoy-config
        - name: rules
          configMap:
            name: prometheus-envoy-rules
        - name: data
          emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
  name: prometheus-envoy
  namespace: monitoring
  labels:
    app: prometheus-envoy
spec:
  selector:
    app: prometheus-envoy
  ports:
    - name: web
      port: 9090
      targetPort: web

16. Alertas operativas

Algunas alertas útiles para operar Envoy Gateway:

apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-envoy-rules
  namespace: monitoring
data:
  envoy-gateway-alerts.yml: |
    groups:
      - name: envoy-gateway-internal
        rules:
          - alert: EnvoyGatewayInternalTargetDown
            expr: up{job="envoy-gateway-internal"} == 0
            for: 2m
            labels:
              severity: critical
              team: platform
            annotations:
              summary: "Envoy Gateway internal target down"
              description: "Prometheus cannot scrape {{ $labels.kubernetes_pod_name }} in {{ $labels.kubernetes_namespace }}."

          - alert: EnvoyGatewayInternalNoScrapeTargets
            expr: absent(up{job="envoy-gateway-internal"})
            for: 5m
            labels:
              severity: critical
              team: platform
            annotations:
              summary: "No Envoy Gateway internal scrape targets"
              description: "Prometheus is not discovering internal Envoy Gateway pods."

          - alert: EnvoyGatewayInternalHigh5xx
            expr: |
              (
                sum(rate(envoy_http_downstream_rq_xx{job="envoy-gateway-internal",envoy_response_code_class="5"}[5m]))
                /
                clamp_min(sum(rate(envoy_http_downstream_rq_total{job="envoy-gateway-internal"}[5m])), 1)
              ) > 0.05
            for: 5m
            labels:
              severity: warning
              team: platform
            annotations:
              summary: "High 5xx rate on internal Envoy Gateway"
              description: "More than 5% of downstream requests are 5xx for 5 minutes."

          - alert: EnvoyGatewayInternalHigh4xx
            expr: |
              (
                sum(rate(envoy_http_downstream_rq_xx{job="envoy-gateway-internal",envoy_response_code_class="4"}[5m]))
                /
                clamp_min(sum(rate(envoy_http_downstream_rq_total{job="envoy-gateway-internal"}[5m])), 1)
              ) > 0.20
            for: 10m
            labels:
              severity: warning
              team: platform
            annotations:
              summary: "High 4xx rate on internal Envoy Gateway"
              description: "More than 20% of downstream requests are 4xx for 10 minutes."

          - alert: EnvoyGatewayInternalHighP95Latency
            expr: |
              histogram_quantile(
                0.95,
                sum by (le) (
                  rate(envoy_http_downstream_rq_time_bucket{job="envoy-gateway-internal"}[5m])
                )
              ) > 1000
            for: 10m
            labels:
              severity: warning
              team: platform
            annotations:
              summary: "High p95 latency on internal Envoy Gateway"
              description: "Downstream request p95 latency is above 1000ms for 10 minutes."

          - alert: EnvoyGatewayInternalNoHealthyUpstream
            expr: |
              sum(rate(envoy_cluster_upstream_rq_503{job="envoy-gateway-internal"}[5m])) > 0
            for: 5m
            labels:
              severity: critical
              team: platform
            annotations:
              summary: "Envoy Gateway internal has upstream 503s"
              description: "Envoy is returning upstream 503 responses. Check backend endpoints and HTTPRoute backendRefs."

          - alert: EnvoyGatewayInternalUpstreamConnectionFailures
            expr: |
              sum(rate(envoy_cluster_upstream_cx_connect_fail{job="envoy-gateway-internal"}[5m])) > 0
            for: 5m
            labels:
              severity: warning
              team: platform
            annotations:
              summary: "Envoy Gateway internal upstream connection failures"
              description: "Envoy is failing to connect to one or more upstream clusters."

          - alert: EnvoyGatewayInternalCertificateExpiringSoon
            expr: |
              min(envoy_server_days_until_first_cert_expiring{job="envoy-gateway-internal"}) < 14
            for: 1h
            labels:
              severity: warning
              team: platform
            annotations:
              summary: "Envoy Gateway internal certificate expires soon"
              description: "The first certificate loaded by Envoy expires in less than 14 days."

          - alert: EnvoyGatewayInternalTracingDrops
            expr: |
              sum(rate(envoy_tracing_opentelemetry_spans_dropped{job="envoy-gateway-internal"}[5m])) > 0
            for: 5m
            labels:
              severity: warning
              team: platform
            annotations:
              summary: "Envoy Gateway internal is dropping traces"
              description: "Envoy is dropping OpenTelemetry spans. Check the collector and Tempo pipeline."

          - alert: EnvoyGatewayInternalNoRoute
            expr: |
              sum(rate(envoy_http_no_route{job="envoy-gateway-internal"}[5m])) > 1
            for: 10m
            labels:
              severity: warning
              team: platform
            annotations:
              summary: "Envoy Gateway internal requests without route"
              description: "Envoy is receiving traffic that does not match any configured route."

17. Alertmanager mínimo

apiVersion: apps/v1
kind: Deployment
metadata:
  name: alertmanager-envoy
  namespace: monitoring
  labels:
    app: alertmanager-envoy
spec:
  replicas: 1
  selector:
    matchLabels:
      app: alertmanager-envoy
  template:
    metadata:
      labels:
        app: alertmanager-envoy
    spec:
      containers:
        - name: alertmanager
          image: quay.io/prometheus/alertmanager:v0.32.1
          args:
            - --config.file=/etc/alertmanager/alertmanager.yml
            - --storage.path=/alertmanager
          ports:
            - name: web
              containerPort: 9093
          volumeMounts:
            - name: config
              mountPath: /etc/alertmanager
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 200m
              memory: 256Mi
      volumes:
        - name: config
          configMap:
            name: alertmanager-envoy-config
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: alertmanager-envoy-config
  namespace: monitoring
data:
  alertmanager.yml: |
    global:
      resolve_timeout: 5m

    route:
      receiver: default
      group_by:
        - alertname
        - gateway
      group_wait: 30s
      group_interval: 5m
      repeat_interval: 4h

    receivers:
      - name: default
---
apiVersion: v1
kind: Service
metadata:
  name: alertmanager-envoy
  namespace: monitoring
  labels:
    app: alertmanager-envoy
spec:
  selector:
    app: alertmanager-envoy
  ports:
    - name: web
      port: 9093
      targetPort: web

18. Queries PromQL útiles

Requests por segundo:

sum(rate(envoy_http_downstream_rq_total{job="envoy-gateway-internal"}[5m]))

5xx rate:

sum(rate(envoy_http_downstream_rq_xx{job="envoy-gateway-internal",envoy_response_code_class="5"}[5m]))
/
clamp_min(sum(rate(envoy_http_downstream_rq_total{job="envoy-gateway-internal"}[5m])), 1)

4xx rate:

sum(rate(envoy_http_downstream_rq_xx{job="envoy-gateway-internal",envoy_response_code_class="4"}[5m]))
/
clamp_min(sum(rate(envoy_http_downstream_rq_total{job="envoy-gateway-internal"}[5m])), 1)

Latencia p95:

histogram_quantile(
  0.95,
  sum by (le) (
    rate(envoy_http_downstream_rq_time_bucket{job="envoy-gateway-internal"}[5m])
  )
)

Conexiones activas:

sum(envoy_http_downstream_cx_active{job="envoy-gateway-internal"})

Memoria Envoy:

sum(envoy_server_memory_allocated{job="envoy-gateway-internal"})

Upstream 503:

sum(rate(envoy_cluster_upstream_rq_503{job="envoy-gateway-internal"}[5m]))

Fallas de conexión a upstream:

sum(rate(envoy_cluster_upstream_cx_connect_fail{job="envoy-gateway-internal"}[5m]))

Targets scrapeados:

up{job="envoy-gateway-internal"}

19. Queries LogQL útiles

Logs por hostname:

{k8s_namespace_name="envoy-gateway-system", k8s_container_name="envoy"}
| json
| authority="app-demo.apps.example.com"

Logs por route:

{k8s_namespace_name="envoy-gateway-system", k8s_container_name="envoy"}
| json
| route_name=~".*app-demo.*"

Logs por trace id:

{k8s_namespace_name="envoy-gateway-system", k8s_container_name="envoy"}
| json
| trace_id="TRACE_ID"

Errores 5xx:

{k8s_namespace_name="envoy-gateway-system", k8s_container_name="envoy"}
| json
| response_code >= 500

Rutas sin match:

{k8s_namespace_name="envoy-gateway-system", k8s_container_name="envoy"}
| json
| response_code_details="route_not_found"

Upstream elegido por Envoy:

{k8s_namespace_name="envoy-gateway-system", k8s_container_name="envoy"}
| json
| line_format "host={{.authority}} path={{.path}} route={{.route_name}} cluster={{.upstream_cluster}} upstream={{.upstream_host}} code={{.response_code}} trace={{.trace_id}}"

20. OpenTelemetry en las aplicaciones

Envoy puede generar el span del Gateway. Pero para ver la traza completa, la aplicación también debe instrumentarse.

Variables típicas:

env:
  - name: OTEL_SERVICE_NAME
    value: app-demo
  - name: OTEL_EXPORTER_OTLP_ENDPOINT
    value: http://otel-collector.monitoring.svc.cluster.local:4317
  - name: OTEL_EXPORTER_OTLP_PROTOCOL
    value: grpc
  - name: OTEL_RESOURCE_ATTRIBUTES
    value: deployment.environment=dev,k8s.namespace.name=app-dev,service.namespace=app-dev
  - name: OTEL_TRACES_SAMPLER
    value: parentbased_traceidratio
  - name: OTEL_TRACES_SAMPLER_ARG
    value: "0.10"

Headers que la aplicación debería preservar si llama a otros servicios:

Si la aplicación no propaga estos headers, Tempo va a mostrar el span del Gateway, pero no necesariamente la continuidad interna del backend.


21. Pruebas rápidas

21.1 Validar Gateway

kubectl get gateway -n envoy-gateway-system
kubectl describe gateway -n envoy-gateway-system internal-gw
kubectl get svc -n envoy-gateway-system

21.2 Validar HTTPRoute

kubectl get httproute -A
kubectl describe httproute -n app-dev app-demo

Revisar condiciones:

Accepted=True
ResolvedRefs=True

21.3 Validar Service y Endpoints

kubectl get svc -n app-dev app-demo
kubectl get endpoints -n app-dev app-demo
kubectl get pods -n app-dev -l app=app-demo

21.4 Probar sin tocar DNS usando curl —resolve

curl -k -sS -o /tmp/gateway-test.out -w '%{http_code} %{time_total}\n' \
  --resolve app-demo.dev.apps.example.com:443:192.0.2.11 \
  -H 'traceparent: 00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01' \
  -H 'x-envoy-force-trace: true' \
  https://app-demo.dev.apps.example.com/

21.5 Buscar el access log por trace id

{k8s_namespace_name="envoy-gateway-system", k8s_container_name="envoy"}
| json
| trace_id="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

21.6 Buscar traza en Tempo

Buscar por:

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

22. Plan de migración desde Ingress

El enfoque que usé fue migrar en paralelo, no hacer un corte grande de una sola vez.

Paso 1: Inventariar Ingress actuales

kubectl get ingress -A
kubectl get ingress -A -o wide

Relevar:

Paso 2: Clasificar por ambiente

Ejemplo:

HostNamespaceAmbienteGateway destino
app1.apps.example.comapp-internalinternalinternal-gw
app1.dev.apps.example.comapp-devdevdev-gw
app1.testing.apps.example.comapp-testingtestingtesting-gw

Paso 3: Preparar Gateways en paralelo

Antes de mover tráfico real:

kubectl apply -f 01-metallb-pools.yaml
kubectl apply -f 03-envoyproxy.yaml
kubectl apply -f 04-gateways.yaml
kubectl apply -f 05-namespace-access.yaml
kubectl apply -f 06-reference-grants.yaml
kubectl apply -f 08-prometheus-alerts.yaml

Paso 4: Crear HTTPRoutes equivalentes

Por cada Ingress:

Ingress host/path/backend
  -> HTTPRoute host/path/backendRef

Paso 5: Validar sin cambiar DNS

Usar curl --resolve apuntando directo a la IP del Gateway.

Paso 6: Revisar observabilidad

Antes de hacer el corte, validar:

Paso 7: Cambiar DNS o edge routing

Una vez validado, cambiar la resolución del host al nuevo Gateway o modificar la regla del balanceador/edge correspondiente.

Paso 8: Mantener rollback simple

Durante una ventana de convivencia:

Paso 9: Retirar Ingress

Cuando la ruta ya está estable:

kubectl delete ingress -n app-dev app-demo

23. Troubleshooting checklist

Cuando una app no responde a través de Gateway API, reviso la cadena completa.

23.1 ¿El DNS o edge apunta al Gateway correcto?

dig app-demo.dev.apps.example.com

O probar directo:

curl -vk --resolve app-demo.dev.apps.example.com:443:192.0.2.11 https://app-demo.dev.apps.example.com/

23.2 ¿El Gateway está programado?

kubectl get gateway -n envoy-gateway-system
kubectl describe gateway -n envoy-gateway-system dev-gw

23.3 ¿El HTTPRoute fue aceptado?

kubectl describe httproute -n app-dev app-demo

Buscar:

Accepted=True
ResolvedRefs=True

Si ResolvedRefs=False, revisar:

23.4 ¿El namespace tiene las labels correctas?

kubectl get ns app-dev --show-labels

23.5 ¿El Service tiene endpoints?

kubectl get svc -n app-dev app-demo
kubectl get endpoints -n app-dev app-demo

Si no hay endpoints, el problema probablemente está en selectors, pods o readiness probes.

23.6 ¿Envoy está devolviendo 404 o route_not_found?

Buscar en Loki:

{k8s_namespace_name="envoy-gateway-system", k8s_container_name="envoy"}
| json
| response_code_details="route_not_found"

23.7 ¿Envoy está devolviendo 503?

Revisar Prometheus:

sum(rate(envoy_cluster_upstream_rq_503{job="envoy-gateway-internal"}[5m]))

Y logs:

{k8s_namespace_name="envoy-gateway-system", k8s_container_name="envoy"}
| json
| response_code = 503

23.8 ¿Hay fallas de conexión al upstream?

sum(rate(envoy_cluster_upstream_cx_connect_fail{job="envoy-gateway-internal"}[5m]))

23.9 ¿El certificado está cargado correctamente?

openssl s_client -connect app-demo.dev.apps.example.com:443 -servername app-demo.dev.apps.example.com

O directo a la IP:

openssl s_client -connect 192.0.2.11:443 -servername app-demo.dev.apps.example.com

24. Guía mínima para equipos de desarrollo

Para publicar una aplicación, un equipo necesita:

  1. Namespace aprobado con labels del Gateway.
  2. Service creado y con endpoints listos.
  3. HTTPRoute apuntando al Gateway y listener correcto.
  4. Host DNS apuntando al LB correspondiente.
  5. Aplicación preservando traceparent y x-request-id.
  6. Aplicación instrumentada con OpenTelemetry si se necesitan spans internos.
  7. Validación en Loki, Prometheus y Tempo con una request de prueba.

Ejemplo mínimo de HTTPRoute:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-demo
  namespace: app-dev
  labels:
    platform.example.com/gateway-role: dev
    app.kubernetes.io/name: app-demo
spec:
  parentRefs:
    - name: dev-gw
      namespace: envoy-gateway-system
      sectionName: https-dev
  hostnames:
    - app-demo.dev.apps.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: app-demo
          port: 80

25. Aprendizajes de la migración

25.1 Gateway API no es solo un reemplazo de Ingress

El error sería pensar:

Ingress -> HTTPRoute

La migración real se parece más a:

Ingress Controller + annotations + configuración distribuida
  -> Gateway API + Envoy Gateway + políticas + observabilidad + contrato de plataforma

25.2 La observabilidad tiene que formar parte del diseño

Agregar observabilidad después suele dejar huecos.

En este caso, la decisión fue que cada request que entra por el Gateway pueda generar datos útiles desde el inicio:

25.3 El control por namespace es clave

Permitir que cualquier namespace publique rutas hacia cualquier Gateway puede volverse peligroso.

Las labels de namespace y allowedRoutes permiten tener un modelo más claro:

Este namespace puede publicar en dev.
Este namespace puede publicar en testing.
Este namespace puede publicar en internal.

25.4 ReferenceGrant obliga a ser explícito

Cuando hay referencias cross-namespace, Gateway API obliga a declarar permisos.

Eso agrega algo de YAML, pero mejora el modelo de seguridad.

25.5 Los runbooks importan tanto como los manifiestos

La implementación no termina cuando kubectl apply funciona.

También hace falta documentar:

25.6 La migración tiene que ser gradual

Gateway API puede convivir con Ingress durante la transición.

Eso permite migrar host por host, ambiente por ambiente y aplicación por aplicación, manteniendo rollback simple.


26. Conclusión

Migrar desde Kubernetes Ingress hacia Gateway API fue mucho más que cambiar la forma de declarar rutas.

Fue una oportunidad para ordenar la capa de entrada HTTP de la plataforma:

La parte más valiosa fue pasar de una visión centrada en “exponer servicios” a una visión más cercana a Platform Engineering:

La plataforma no solo debe permitir desplegar aplicaciones.
También debe hacerlas publicables, observables, trazables y operables.

Gateway API, combinado con Envoy Gateway, Prometheus, Loki, Tempo y OpenTelemetry, permite construir una base bastante sólida para eso.

No porque agregue más YAML, sino porque obliga a pensar mejor las responsabilidades:

Y para mí ese fue el mayor aprendizaje de la migración.

Next
EnvoyBrokerLab: laboratorio local para entender Envoy, control planes y aprovisionamiento asíncrono