Primeros pasos con Apisix en Kubernetes
Tags: apisix, kubernetes, apigateway
Published: 2023-04-22 19:14:55

---
title: Primeros pasos con Apisix en Kubernetes
published: true
date: 2023-03-28 00:00:00 UTC
tags: apisix,kubernetes,apigateway
canonical_url: https://jorge.aguilera.soy/blog/2023/apisix-1.html
---

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"

 — [https://www.tibco.com/reference-center/what-is-an-api-gateway](https://www.tibco.com/reference-center/what-is-an-api-gateway)

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

<dl>
<dt>WARNING</dt>
<dd>
<p>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</p>
<div>
<ul>
<li>
<p>docker (a estas alturas quién no tiene instalado docker en su máquina)</p>
</li>
<li>
<p>kubectl, es una aplicacion de consola que se instala facilmente siguiendo <a href="https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/">https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/</a></p>
</li>
<li>
<p>k9s, un kubectl supervitaminado, yo no puedo vivir ya sin él</p>
</li>
<li>
<p>k3d es una implementación muy ligera de kubernetes que se instala siguiendo <a href="https://k3d.io/v5.4.9/">https://k3d.io/v5.4.9/</a></p>
</li>
<li>
<p>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</p>
</li>
</ul>
</div>
</dd>
</dl>

## 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"`

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

```
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/](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`

<dl>
<dt>INFO</dt>
<dd>
<p>Básicamente hemos creado un objeto en el cluster que se encargará de recibir peticiones en un <code>port</code> 80 y los reenviará
al container al <code>targetPort</code> 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</p>
</dd>
</dl>

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/](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)

<dl>
<dt>INFO</dt>
<dd>
<p>voy a usar el namespace <strong>apisix</strong> donde desplegar todos los artefactos relacionados con apisix, pero le puedes
dar otro nombre o incluso usar el <em>default</em>, aunque lo veo un poco "guarro"</p>
</dd>
</dl>

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](https://jorge.aguilera.soy/images/2023/k9s_1.png)

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

<dl>
<dt>INFO</dt>
<dd>
<p>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.</p>
</dd>
</dl>

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
```

<dl>
<dt>INFO</dt>
<dd>
<p>helm nos creó, en el namespace que le dijimos, un servicio llamado <code>apisix-gateway</code> el cual está
escuchando en un puerto llamado <code>apisix-gateway</code></p>
</dd>
</dl>

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