Ya os hablamos hace unas semanas de Kubernetes Gateway API: Reimaginando Ingress. Como lo prometido es deuda, este post es la segunda parte y abordaremos Gateway API desde un punto de vista más realista, dejando a un lado la teoría, para adentrarnos en la realidad de la implementación. Para ello, utilizaremos un entorno de Google Cloud con los siguientes componentes desplegados y los permisos necesarios:

Antes de meternos en harina, simplemente recordar que Gateway API no propone una implementación concreta, solo la definición y el comportamiento que debe seguir la especificación. Por ello, cuando utilizamos una implementación, como es el caso de GKE, debemos consultar previamente qué características han implementado y cuáles no.

No es bonito encontrar que necesitamos una funcionalidad que Gateway API define como opcional y que Google no ha implementado cuando ya tenemos todo configurado y listo para desplegar. En el caso de GKE podemos revisar los siguientes enlaces:

A partir de aquí, juguemos un pequeño juego de rol.

Somos la persona a cargo del clúster

Kubernetes es nuestro día a día, estamos acostumbrados a trabajar con él y hemos decidido que es buena idea probar esto de Gateway API.

Solemos trabajar con Ingress + Ingress Controller, pero cada vez es más complejo, los equipos de desarrollo publican nuevos endpoints cada vez más rápido y ahora resulta que negocio plantea aplicaciones que no se podrán invocar por HTTP sino por TCP/UDP e incluso gRPC (disclaimer: protocolos distintos a HTTP/S aún se encuentran en canales de experimentación, pero se espera que estén disponibles en poco tiempo).

Queremos estar preparados para todo esto y vemos que Google ya ha liberado una implementación en GA. Nos proponemos entonces usar esta oportunidad para un pequeño proyecto de una web sencilla con un blog.

Tanto la web como el blog serán gestionados por equipos de desarrollo diferentes y, al ser un proyecto pequeño, la parte de operación del clúster debe tener el mínimo mantenimiento posible. Manos a la obra.

Dado que la aplicación estará publicada en Internet, hay que reservar la IP que usaremos como punto de entrada. Además, es necesario provisionar un certificado para poder acceder por HTTPs hasta el clúster. No es necesario TLS end-to-end.

# Reserva de la IP pública externa global
$ gcloud compute addresses create gateway-external-ip \
  --global \
  --ip-version IPV4

En un caso real, el certificado probablemente nos sería provisto o nos hubieran dado acceso a una CA para poder generarlo. Para efectos de esta demo, usaremos un certificado autofirmado cuya creación no cubriremos (podéis usar cualquier guía para ello).

Una vez creado, es necesario subir el certificado a Google para poder gestionarlo desde ahí.

# Subida del certificado autofirmado a Google Cloud
$ gcloud compute ssl-certificates create demo-certificate \
    --certificate=server.crt \
    --private-key=server.key \
    --global

Con esto ya están hechos los preparativos para poder jugar dentro del clúster. Recapitulemos lo que tenemos hasta ahora:

Instalamos el CRD de Gateway si no lo hemos hecho en la creación del clúster, acto seguido, nos conectamos al mismo:

# Activamos Gateway API, lo que instalará el CRD
$ gcloud container clusters update <cluster-name> \
    --gateway-api=standard \
    --location=<zone>

# Generamos un kubeconfig válido con nuestro usuario IAM
$ gcloud container clusters get-credentials <cluster-name> --zone <cluster-zone> --project <project-id>

Uno de los requisitos es disponer de múltiples equipos, por lo que crearemos Namespaces para todos ellos (todos los objetos se generan con kubectl apply -f <filename>):

# namespaces.yaml
———
apiVersion: v1
kind: Namespace
metadata:
  name: infrateam
labels:
  name: infrateam
———
apiVersion: v1
kind: Namespace
metadata:
  name: webteam
labels:
  name: webteam
———
apiVersion: v1
kind: Namespace
metadata:
  name: blogteam
labels:
  name: blogteam

Asumiremos que, posteriormente, cada equipo de desarrollo solo tendrá acceso a su namespace correspondiente.

Generamos entonces el objeto Gateway en el namespace del equipo de infraestructura:

# gateway.yaml
———
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1beta1
metadata:
  name: demo—gateway
  namespace: infrateam
spec:
  # La GatewayClassName define qué tipo de balanceador queremos
  gatewayClassName: gke—l7—global-external-managed
  # Google nos permite pasarle nombre de la IP reservada para asignarla
  addresses:
  - type: NamedAddress
    value: gateway-external-ip
  # Definimos dos listener: HTTP en el puerto 80 y HTTPS en el 443
  listeners:
  - name: http
    protocol: HTTP
    port: 80
    # Nadie, salvo el equipo de infra, puede usar este listener
    allowedRoutes:
      kinds:
      # Cuando lo implementen, aquí podremos definir otro tipo de rutas:
      # TCP, UDP o gRPC.
      - kind: HTTPRoute
      namespaces:
        from: same
  - name: https
    protocol: HTTPS
    port: 443
    # Podemos definir qué modo TLS utilizar
    # Una opción propia de GKE nos permite usar certificados gestionados
    tls:
      mode: Terminate
      options: 
        networking.gke.io/pre-shared-certs: demo-certificate
    # Esta vez permitimos a todo el mundo usar este listener
    # aunque podríamos restringirlo si queremos controlarlo todo.
    allowedRoutes:
      kinds:
      - kind: HTTPRoute
      namespaces:
        from: All

La creación del Gateway desencadena la generación de un balanceador por el lado de Google, podemos monitorizar su estado directamente desde el Gateway:

# Vemos su estado en Unknown
$ kubectl get Gateway demo-gateway -n infrateam
NAME           CLASS                            ADDRESS   PROGRAMMED   AGE
demo-gateway   gke-l7-global-external-managed             Unknown      32s

# Un par de minutos después ya podemos verlo desplegado
$ kubectl get Gateway demo-gateway -n infrateam
NAME           CLASS                            ADDRESS         PROGRAMMED   AGE
demo-gateway   gke-l7-global-external-managed   34.36.153.219   True         2m11s

En la consola de Google Cloud podremos verlo igualmente configurado (ojo, que al declarar dos listener, nos ha creado dos balanceadores distintos con diferente protocolo pero misma IP):

Consola de Google Cloud

Como los encargados de gestionar el clúster, nos interesa mantener la seguridad al máximo, por lo que crearemos una redirección automática de HTTP a HTTPs. Para ello usamos un HTTPRoute de Gateway API y lo asociaremos al listener HTTP.

# http2https.yaml
———
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1beta1
metadata:
  name: demo-gateway-http2https
  # Debe usar este namespace para poder usar el listener HTTP
  namespace: infrateam
spec:
  # Es el HTTPRoute el que se asocia a un Gateway, no al revés.
  parentRefs:
  - namespace: infrateam
    name: demo-gateway
    # Indicamos el listener en el que queremos escuchar
    sectionName: http
  rules:
  - filters:
    # Hacemos una redirección con cambio de esquema, devolvemos un 301
    - type: RequestRedirect
      requestRedirect:
        scheme: https
        statusCode: 301

Para comprobar su estado, podemos hacer un describe del objeto. Si el Gateway acepta la ruta, veremos en los eventos SYNC success y en el estado un Status: True, Reason: Accepted.

$ kubectl describe HTTPRoute demo-gateway-http2https -n infrateam
[...]
Status:
  Parents:
    Conditions:
      Last Transition Time:  2023-09-15T10:43:33Z
      Message:               
      Observed Generation:   1
      Reason:                Accepted
      Status:                True
      Type:                  Accepted
      Last Transition Time:  2023-09-15T10:43:33Z
      Message:               
      Observed Generation:   1
      Reason:                ReconciliationSucceeded
      Status:                True
      Type:                  Reconciled
    Controller Name:         networking.gke.io/gateway
    Parent Ref:
      Group:         gateway.networking.k8s.io
      Kind:          Gateway
      Name:          demo-gateway
      Namespace:     infrateam
      Section Name:  http
Events:
  Type    Reason  Age   From                   Message
  ————    ——————  ————  ————                   ———————
  Normal  ADD     56s   sc-gateway-controller  infrateam/demo-gateway-http2https
  Normal  SYNC    11s   sc-gateway-controller  Bind of HTTPRoute "infrateam/demo-gateway-http2https" to ParentRef {Group:       "gateway.networking.k8s.io",
 Kind:        "Gateway",
 Namespace:   "infrateam",
 Name:        "demo-gateway",
 SectionName: "http",
 Port:        nil} was a success
  Normal  SYNC  11s  sc-gateway-controller  Reconciliation of HTTPRoute "infrateam/demo-gateway-http2https" bound to ParentRef {Group:       "gateway.networking.k8s.io",
 Kind:        "Gateway",
 Namespace:   "infrateam",
 Name:        "demo-gateway",
 SectionName: "http",
 Port:        nil} was a success

Para probar el acceso al clúster, generamos el registro DNS en nuestro fichero /etc/hosts (si no disponemos de zona DNS propia) y lanzamos una petición pasando la custom CA como parámetro:

#/etc/hosts
[...]
34.36.153.219 web.paradigmademo.com

$ curl -IL web.paradigmademo.com --cacert certificate/rootCA.crt
HTTP/1.1 301 Moved Permanently
Cache-Control: private
Location: https://web.paradigmademo.com:443/
Content-Length: 0
Date: Fri, 15 Sep 2023 11:24:34 GMT
Content-Type: text/html; charset=UTF-8

HTTP/2 404 
content-length: 18
content-type: text/plain
via: 1.1 google
date: Fri, 15 Sep 2023 11:24:34 GMT
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

Como podemos observar, llegamos sin ningún problema al listener HTTP y Gateway nos redirige al listener HTTPs. Podemos ver la misma configuración en la consola de Google:

Listener HTTP y Gateway

Una vez hecho todo esto, ya dejamos toda la configuración del Gateway correctamente montada. Como personas encargadas de gestionar el clúster, ya podemos dormir tranquilamente sabiendo en qué todo el tráfico de acceso al clúster es HTTPs con el certificado que nosotros hemos provisionado.

Ahora somos del equipo de desarrollo

Un equipo de desarrollo feliz no necesita saber qué hay montado en el clúster; los certificados no es algo que ellos gestionen, ni los balanceadores. Un equipo de desarrollo feliz sí quiere poder desplegar nuevas funcionalidades, añadiendo nuevas URI a su aplicación, sin tener que pelearse con nadie ni tener que pasar por tediosos procedimientos de aprobación.

Jugaremos a dos bandas, primero desplegamos un webserver nginx que simulará la web y posteriormente nos cambiaremos de gorra para desplegar un wordpress. Nuestro objetivo es el siguiente:

Desplegando la web

Definimos el despliegue de la aplicación:

# web/deployment.yaml
———
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
——
# Ojo: Estamos creando un ClusterIP, NO está siendo publicado
apiVersion: v1
kind: Service
metadata:
  name: web
  labels:
    app: nginx
spec:
  ports:
    - port: 80
  selector:
    app: nginx

A continuación, simplemente definimos la ruta que queremos para nuestra aplicación:

# web/httpRoute.yaml
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1beta1
metadata:
  name: web
spec:
  parentRefs:
  - name: demo-gateway
    namespace: infrateam
    sectionName: https
  hostnames:
  - web.paradigmademo.com
  rules:
  - matches:
    # Podemos definir el path en el que sirve la aplicación
    - path:
        value: /
    # Referenciamos el servicio anteriormente desplegado
    backendRefs:
    - name: web
      port: 80

A efectos de la demo, probamos en primer lugar a desplegarlo en el listener http:

$ kubectl describe HttpRoute -n webteam
Name:         web
Namespace:    webteam
Labels:       <none>
Annotations:  <none>
API Version:  gateway.networking.k8s.io/v1beta1
Kind:         HTTPRoute
Metadata:
  Creation Timestamp:  2023-09-15T11:38:14Z
  Generation:          1
  Resource Version:    6642136
  UID:                 7b8bfdc2-347d-4f25-8b36-34748aff4d1d
Spec:
  Hostnames:
    web.paradigmademo.com
  Parent Refs:
    Group:         gateway.networking.k8s.io
    Kind:          Gateway
    Name:          demo-gateway
    Namespace:     infrateam
    Section Name:  http
  Rules:
    Backend Refs:
      Group:   
      Kind:    Service
      Name:    web
      Port:    80
      Weight:  1
    Matches:
      Path:
        Type:   PathPrefix
        Value:  /
Events:
  Type    Reason  Age    From                   Message
  ————    ——————  ————   ————                   ———————
  Normal  ADD     3m18s  sc—gateway-controller  webteam/web

Podemos comprobar que, pasado un tiempo prudencial (3 minutos y medio), nunca llega a sincronizarse como en el caso anterior. Al cambiar al listener HTTPS podemos ver que Kubernetes detecta el update y se sincroniza en unos 2 minutos.

Events:
  Type    Reason  Age    From                   Message
  ————    ——————  ————   ————                   ———————
  Normal  ADD     6m47s  sc-gateway-controller  webteam/web
  Normal  UPDATE  119s   sc-gateway-controller  webteam/web
  Normal  SYNC    5s     sc-gateway-controller  Bind of HTTPRoute "webteam/web" to ParentRef {Group:       "gateway.networking.k8s.io",
 Kind:        "Gateway",
 Namespace:   "infrateam",
 Name:        "demo-gateway",
 SectionName: "https",
 Port:        nil} was a success
  Normal  SYNC  5s  sc-gateway-controller  Reconciliation of HTTPRoute "webteam/web" bound to ParentRef {Group:       "gateway.networking.k8s.io",
 Kind:        "Gateway",
 Namespace:   "infrateam",
 Name:        "demo-gateway",
 SectionName: "https",
 Port:        nil} was a success

En la consola podemos ver el network endpoint group ya registrado.

Network endpoint group ya registrado.

Y repitiendo la query anterior lo que nos devolvía antes un 404, nos devuelve ahora un 200 (el NGINX nos responde):

$ curl -IL web.paradigmademo.com --cacert certificate/rootCA.crt
HTTP/1.1 301 Moved Permanently
Cache-Control: private
Location: https://web.paradigmademo.com:443/
Content-Length: 0
Date: Fri, 15 Sep 2023 11:49:50 GMT
Content-Type: text/html; charset=UTF-8

HTTP/2 200 
server: nginx/1.14.2
date: Fri, 15 Sep 2023 11:49:50 GMT
content-type: text/html
content-length: 612
last-modified: Tue, 04 Dec 2018 14:44:49 GMT
etag: "5c0692e1-264"
accept-ranges: bytes
via: 1.1 google
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

Es importante mencionar que, por defecto, Google configura el balanceador como Endpoint Network Group, de forma que balancea directamente la carga entre pods, no a los nodos. Si configuramos nuestro local para apuntar a la url, podremos comprobar que también es perfectamente accesible vía navegador.

Desplegando el blog

Nos cambiamos ahora la gorra para trabajar en el equipo que gestiona el blog. Usaremos el propio ejemplo de Kubernetes accesible en su repositorio de ejemplos. Seguimos los pasos indicados en la página dedicada al despliegue de wordpress haciendo los siguientes cambios:

Y publicamos la aplicación con un nuevo httpRoute:

# blog/httpRoute.yaml
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: blog
spec:
  parentRefs:
  - name: demo-gateway
    namespace: infrateam
    sectionName: https
  hostnames:
  - web.paradigmademo.com
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /blog
    backendRefs:
    - name: wordpress
      port: 80

Al desplegar Wordpress en el clúster, vemos que todo funciona correctamente con los pods, pero cuando atacamos al endpoint nos devuelve el siguiente error: no healthy upstreams.

Esto es debido a una peculiaridad de Wordpress. Google espera siempre un código 200 para todos los health checks siendo un parámetro no configurable.

Debemos, por tanto, modificar el health check que apunta a nuestra aplicación para que use otra uri que no es la predeterminada, en el caso de Wordpress suele utilizarse el script de instalación. Para ello utilizamos el CRD customizado de Google para generar un HealthCheck propio:

# blog/healthcheck.yaml
———
apiVersion: networking.gke.io/v1
kind: HealthCheckPolicy
metadata:
  name: wordpress
  namespace: blogteam
spec:
  default:
    config:
      type: HTTP
      httpHealthCheck:
        requestPath: /wp-admin/install.php
  targetRef:
    group: ""
    kind: Service
    name: wordpress

Una vez aplicado, esperamos un tiempo prudencial y ya podremos ver en la consola que todo está correcto:

Todo el proceso está correcto.

Mientras que desde el navegador ya podremos ver el script de instalación de wordpress (si hemos configurado de forma acorde el /etc/hosts).

Script de instalación de wordpress

Conclusiones

Como todo en la vida, este artículo también termina :’(, pero no antes de analizar tranquilamente lo que acabamos de hacer:

Esto es una simple demo de cómo podríamos usar Gateway. Pero existen muchas más funcionalidades, como el utilizar pesos para A/B testing, despliegues Blue/Green, redirecciones, rewrites, referencias a endpoints externos al cluster (buckets con contenido estático, funciones serverless), etc.

Y sí, como habéis podido observar, publicar una aplicación es solo cuestión de configurar correctamente el HttpRoute, lo cual hace sencillísima su integración en un chart de Helm o flujo de CI/CD. Con todo ello, espero que os haya sido de utilidad y os ayude a simplificar la gestión de vuestras estrategias de Ingress. ¡Hasta más ver!

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.