Primeros pasos con Apisix en Kubernetes

A poco que la arquitectura de tu producto (no hablo de la de tu aplicación, sino de la del producto) se base en microservicios, bien porque así la diseñasteis desde el principio o bien porque estáis en proceso de migración, seguramente alguien de tu equipo se estará peleando con un Api Gateway

"An API Gateway is the traffic manager that interfaces with the actual backend service or data, and applies policies, authentication, and general access control for API calls to protect valuable data. An API gateway is the way you control access to your back-end systems and services, and it was designed to optimize communication between external clients and your backend services, giving your clients a seamless experience"

Un apigw es algo más que un simple proxy hacia los servicios de backend, pues también lo podemos usar para agregar varias llamadas a los servicios en una única respuesta, así como también se encarga de la autentificación, cache, etc.

Por otro lado, Kubernetes se ha convertido en el orquestador por excelencia en las soluciones de hoy en día sin importar el tamaño de la misma (aunque algunos verán una barbaridad usar kubernetes en una aplicación pequeña yo personalmente sí lo veo porque hoy en día existen implementaciones muy ligeras que te permitirán correr la aplicación y podrás aprovechar un montón de herramientas disponibles para este entorno)

Así pues en este post vamos a ver cómo "desplegar" una aplicación con microservicios en kubernetes en nuestro local, es decir, lo que pretendo es jugar con las herramientas disponibles para comprender cómo funcionan los diferentes componentes de la solución sin necesidad de invertir mucho tiempo (ni dinero). Más adelante, según disponibilidad, iré ampliando esta serie de post usando otras plataformas y explorando otras herramientas, siempre desde el punto de vista de aprender

Requisitos

WARNING

Ignoro si todo lo que vamos a ver funcionaría en un Windows. En un Linux debería funcionar sin problema y probablemente en un Mac también

  • docker (a estas alturas quién no tiene instalado docker en su máquina)

  • kubectl, es una aplicacion de consola que se instala facilmente siguiendo https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/

  • k9s, un kubectl supervitaminado, yo no puedo vivir ya sin él

  • k3d es una implementación muy ligera de kubernetes que se instala siguiendo https://k3d.io/v5.4.9/

  • helm, es una herramienta que "recubre" kubectl y mediante plantillas podemos personalizar y desplegar en el cluster aplicaciones muy complejas con un sólo comando

Crear el cluster

K3d (hay otras opciones igual de ligeras) nos va a servir para crear nuestro cluster en local. Sería como tener el gestor de contenedores de Amazon de EKS en tu local así que lo primero va a ser crear un cluster:

k3d cluster create example -p "8881:80@loadbalancer"

INFO

Voy a usar el puerto 8881 de mi maquina como puerto de acceso al balanceador que se encuentra dentro del cluster y acceder así a los servicios que despliegue en él. Si usas ese puerto para tus cosas usa otro que esté libre

INFO[0000] portmapping '8881:80' targets the loadbalancer: defaulting to [servers:*:proxy agents:*:proxy]
INFO[0000] Prep: Network
INFO[0000] Created network 'k3d-example'
INFO[0000] Created image volume k3d-example-images
INFO[0000] Starting new tools node...
INFO[0000] Starting Node 'k3d-example-tools'
INFO[0001] Creating node 'k3d-example-server-0'
INFO[0001] Creating LoadBalancer 'k3d-example-serverlb'
INFO[0001] Using the k3d-tools node to gather environment information
INFO[0001] HostIP: using network gateway 172.20.0.1 address
INFO[0001] Starting cluster 'example'
INFO[0001] Starting servers...
INFO[0001] Starting Node 'k3d-example-server-0'
INFO[0005] All agents already running.
INFO[0005] Starting helpers...
INFO[0005] Starting Node 'k3d-example-serverlb'
INFO[0011] Injecting records for hostAliases (incl. host.k3d.internal) and for 2 network members into CoreDNS configmap...
INFO[0013] Cluster 'example' created successfully!
INFO[0013] You can now use it like this:
kubectl cluster-info

Comprobamos que el cluster efectivamente se ha creado y vemos el puerto que nos ha asignado (que seguramente NO coincidirá con el tuyo)

kubectl cluster-info

Kubernetes control plane is running at https://0.0.0.0:39137
CoreDNS is running at https://0.0.0.0:39137/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
Metrics-server is running at https://0.0.0.0:39137/api/v1/namespaces/kube-system/services/https:metrics-server:https/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

Vamos a comprobar que accedemos al cluster:

curl -v localhost:8881

*   Trying 127.0.0.1:8881...
* Connected to localhost (127.0.0.1) port 8881 (#0)
> GET / HTTP/1.1
> Host: localhost:8881
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 404 Not Found
< Content-Type: text/plain; charset=utf-8
< X-Content-Type-Options: nosniff
< Date: Tue, 28 Mar 2023 14:49:46 GMT
< Content-Length: 19
<
404 page not found
* Connection #0 to host localhost left intact

Digamos que si has llegado hasta aquí es como si te hubieras creado una cuenta en AWS y le hubieras dado al botón de crear un EKS

Repositorio

El código de este post lo puedes encontrar en https://github.com/jagedn/apisix-example/ aunque también puedes ir copiando y pegando desde el post

Monolito

Supongamos que tenemos nuestro monolito en una imagen docker tal que podemos desplegarla en nuestro cluster. Para este ejemplo nuestro monolito va a ser una imagen pública que simplemente nos devuelve información sobre el container donde se está ejecutando.

Creamos un fichero con la definición de nuestra aplicacion:

whoami-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: whoami-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: whoami
  template:
    metadata:
      labels:
        app: whoami
    spec:
      containers:
      - name: whoami-container
        image: containous/whoami

y la desplegamos en nuestro cluster:

kubectl apply -f whoami-deployment.yml

deployment.apps/whoami-deployment created

Comprobamos que esta desplegada

kubectl get pods

NAME                                 READY   STATUS    RESTARTS   AGE
whoami-deployment-5d4fc76b57-2h8pl   1/1     Running   0          45s

Nuestra aplicación está "viva" y ejecutandose en el cluster pero NO tenemos forma de pedirle que haga nada (sin tener que hacer trucos) así que tenemos que crear y desplegar un servicio

whoami-service.yml
apiVersion: v1
kind: Service
metadata:
  name: whoami-service
spec:
  ports:
  - name: http
    targetPort: 80
    port: 80
  selector:
    app: whoami

kubectl apply -f whoami-service.yml

INFO

Básicamente hemos creado un objeto en el cluster que se encargará de recibir peticiones en un port 80 y los reenviará al container al targetPort 80. Ahora mismo tenemos 1 pod y 1 service pero k8s nos permite que creemos más réplicas del deployment por lo que podriamos tener n pod y 1 service de tal forma que el service se encargaría de ir repartiendo las peticiones a cada pod

Sin embargo, seguimos sin poder acceder a nuestra aplicación "desde fuera". Para ello necesitamos desplegar un Ingress

whoami-ingress.yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: whoami-ingress
spec:
  rules:
  - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: whoami-service
            port:
              name: http

kubectl apply -f whoami-ingress.yml

Y ahora por fín ya podemos acceder a nuestro monolito

curl  localhost:8881/mira/mama/un/monolito

Hostname: whoami-deployment-5d4fc76b57-2h8pl
IP: 127.0.0.1
IP: ::1
IP: 10.42.0.9
IP: fe80::6cc5:9aff:fe51:9a19
RemoteAddr: 10.42.0.8:40276
GET /mira/mama/un/monolito HTTP/1.1
Host: localhost:8881
User-Agent: curl/7.81.0
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 10.42.0.1
X-Forwarded-Host: localhost:8881
X-Forwarded-Port: 8881
X-Forwarded-Proto: http
X-Forwarded-Server: traefik-7cd4fcff68-9ksw7
X-Real-Ip: 10.42.0.1

APISIX

Perfecto, ya tenemos nuestro producto en producción pero el nuevo arquitecto viene con otras ideas y preconiza que el monolito nos va a ralentizar por lo que debemos evolucionar a una arquitectura microservicios. Perfecto (estoy de acuerdo con él) así que mientras los programadores empiezan a crear nuevos servicios nosotros vamos a ir implementando un API Gateway que sirva para consumir sus endpoints (como ya hemos dicho sabemos que un simple proxy a los servicios no será suficiente)

Existen varias formas de instalar Apisix en nuestro cluster pero nosotros vamos a usar la más fácil (no por ello incompleta) usando el chart de Helm disponible

Si seguimos la página oficial, https://apisix.apache.org/docs/apisix/installation-guide/, veremos que consiste en ejecutar 3 comandos, pero primero vamos a preparar un fichero de configuración adecuado para nuestro caso

values.yml
gateway:
  type: NodePort

ingress-controller:
  enabled: true
  config:
    apisix:
      serviceNamespace: apisix

Con este fichero le vamos a decir a Helm que nos instale Apisix "en modo NodePort" (necesario para que k3d luego pueda acceder a él sin problemas) y que también nos instale su controller para ingress. Este será el encargado de que podamos crear rutas y reglas en apisix usando ficheros de kubernetes de forma fácil (y versionada)

INFO

voy a usar el namespace apisix donde desplegar todos los artefactos relacionados con apisix, pero le puedes dar otro nombre o incluso usar el default, aunque lo veo un poco "guarro"

Básicamente ejecutaremos estos 3 comandos

helm repo add apisix https://charts.apiseven.com
helm repo update
helm install apisix apisix/apisix --create-namespace  --namespace apisix -f values.yml

(Fíjate que indico el fichero "-f values.yml" que es como he llamado al fichero de configuración de helm para ajustar la ínstalación de Apisix a mi medida)

Si todo va bien, Apisix va a necesitar un minuto aprox para inicializar todas sus cosas. Vamos a utilizar k9s para comprobarlo. Ejecutamos en un terminal k9s y deberíamos ver todos los pods del cluster:

k9s 1

Según vayan terminando de inicializarse cada componente iremos viendo que pasan de rojo a azul (en mi terminal al menos)

Una vez comprobado que todos han inicializado bien saldremos con …​. síiiiii "dos puntos q", al más puro estilo de vi

INFO

Lo que ha hecho helm ha sido crear un montón de recursos nuevos en nuestro cluster, además de crearnos el namespace y desplegar en él los diferentes componentes.

A grandes rasgos la arquitectura de Apisix se divide:

  • componente stateful que guarda la configuración (etcd)

  • componente admin que gestiona esta configuración

  • componente gateway el encargado de gestionar las peticiones

  • ingress controller, un operador que "convierte" nuestros recursos k8s en configuración apisix

Lo bueno de Apisix es que con esta arquitectura NO necesitas ninguna base de datos

Comprobamos que nuestro "monolito" sigue funcionando (pues en principio no hemos hecho nada que le afecte)

curl  localhost:8881/sigo/vivo

Hostname: whoami-deployment-5d4fc76b57-2h8pl
IP: 127.0.0.1
IP: ::1
IP: 10.42.0.9
IP: fe80::6cc5:9aff:fe51:9a19
RemoteAddr: 10.42.0.8:40276
GET /sigo/vivo HTTP/1.1
Host: localhost:8881
User-Agent: curl/7.81.0
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 10.42.0.1
X-Forwarded-Host: localhost:8881
X-Forwarded-Port: 8881
X-Forwarded-Proto: http
X-Forwarded-Server: traefik-7cd4fcff68-9ksw7
X-Real-Ip: 10.42.0.1

Desplegando rutas en nuestro Apisix

Vamos a indicarle a Apisix que queremos enrutar todas las peticiones que lleguen a una determinada ruta (por ejemplo todas /*) hasta nuestro monolito:

apisix-whoami.yml
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
  name: apisix-whoami-route
  namespace: default
spec:
  http:
    - name: route-1
      match:
        paths:
          - /*
      backends:
        - serviceName: whoami-service
          servicePort: http

kubectl apply -f apisix-whoami.yml

Este recurso es detectado por el controller de Apisix (al ser un kind ApisixRoute) y lo aplica en su configuración de tal forma que todas las peticiones que cumplan la condición "match" se envíen al servicio whoami-service

Como Apisix todavía no está recibiendo peticiones del exterior podemos aplicar esta configuración sin ningún problema.

Activando nuestro APIGW

Ha llegado el momento de "intercambiar" nuestro monolito por nuestro ApiGW. Como estamos en un cluster de ejemplo y nos podemos permitir un pequeño downtime simplemente quitaremos un ingress y lo sustituiremos por otros

apisix-ingress.yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: apisix-ingress
  namespace: apisix
spec:
  rules:
  - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: apisix-gateway
            port:
              name: apisix-gateway
INFO

helm nos creó, en el namespace que le dijimos, un servicio llamado apisix-gateway el cual está escuchando en un puerto llamado apisix-gateway

Quitamos el enrutado hacia el monolito

kubectl delete -f whoami-ingress.yml

Comprobamos que lo "hemos roto"

curl  localhost:8881/sigo/vivo
404 page not found

y corremos a activar el del apigw

kubectl apply -f apisix-ingress.yml

Comprobamos que lo "hemos arreglado"

curl  localhost:8881/sigo/vivo
Hostname: whoami-deployment-5d4fc76b57-2h8pl
IP: 127.0.0.1
IP: ::1
IP: 10.42.0.9
IP: fe80::6cc5:9aff:fe51:9a19
RemoteAddr: 10.42.0.19:58848
GET /sigo/vivo HTTP/1.1
Host: localhost:8881
User-Agent: curl/7.81.0
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 10.42.0.1, 10.42.0.8
X-Forwarded-Host: localhost:8881
X-Forwarded-Port: 8881
X-Forwarded-Proto: http
X-Forwarded-Server: traefik-7cd4fcff68-9ksw7
X-Real-Ip: 10.42.0.8

Si todo ha ido bien hemos "insertado" por delante del monolito una nueva pieza que será la que se encargue ahora de las peticiones desde el exterior y decidirá en base a su configuración cómo responder

Configurando rutas

¿Cómo podemos estar seguros de que todo esto es cierto? Supongamos que en la nueva arquitectura queremos que la vieja aplicación siga ofreciendo servicios pero bajo una nueva ruta /v1 por ejemplo

Reconfiguramos el ApisixRoute

apisix-whoami.yml
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
  name: apisix-whoami-route
  namespace: default
spec:
  http:
    - name: route-1
      match:
        paths:
          - /v1/*
      backends:
        - serviceName: whoami-service
          servicePort: http

(Fijate que el paths lo hemos cambiado a /v1)

y lo aplicamos kubectl apply -f apisix-whoami.yml

Si ahora intentamos acceder a la aplicación veremos que no está disponible

curl  localhost:8881/sigo/vivo
{"error_msg":"404 Route Not Found"}

(Fíjate que el body de respuesta ha cambiado porque ahora es el Apisix el que lo está devolviendo)

Pero sin embargo tenemos a monolito escuchando en /v1

curl  localhost:8881/v1/sigo/vivo
Hostname: whoami-deployment-5d4fc76b57-2h8pl
IP: 127.0.0.1
IP: ::1
IP: 10.42.0.9
IP: fe80::6cc5:9aff:fe51:9a19
RemoteAddr: 10.42.0.19:42206
GET /v1/sigo/vivo HTTP/1.1
Host: localhost:8881
User-Agent: curl/7.81.0
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 10.42.0.1, 10.42.0.8
X-Forwarded-Host: localhost:8881
X-Forwarded-Port: 8881
X-Forwarded-Proto: http
X-Forwarded-Server: traefik-7cd4fcff68-9ksw7
X-Real-Ip: 10.42.0.8

Disclaimer

Obviamente este post es a modo de primeros pasos con Apisix y no pretende cubrir todos los aspectos del mismo. De hecho me faltan todavía muchos conocimientos y cosas a probar

Sin embargo, a diferencia de otros post que encontrarás en Internet, está enfocado totalmente a kubernetes mientras que casi todos los que he encontrado lo hacen usando Docker

2019 - 2024 | Mixed with Bootstrap | Baked with JBake v2.6.7