¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
Conoce nuestra marca.¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
Conoce nuestra marca.dev
Juan Mas Aguilar 07/02/2024 Cargando comentarios…
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.
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):
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:
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.
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:
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.
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.
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:
Mientras que desde el navegador ya podremos ver el script de instalación de wordpress (si hemos configurado de forma acorde el /etc/hosts).
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!
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.
Cuéntanos qué te parece.