Introducción a Kubernetes y Minikube

El objetivo de esta entrada es dar unas nociones básicas de cómo podemos usar Kubernetes para desplegar una pequeña aplicación basada en microservicios.

Para ello usaremos un repositorio ya creado con un pequeño proyecto que contiene dos microservicios y todos los scripts de Kubernetes que usaremos en esta entrada: https://github.com/abenitezsan/kubernetesDemo

Así mismo, disponemos de un repositorio en DockerHub con las imágenes de nuestros microservicios (2 pequeñas aplicaciones creadas en Spring boot) preparadas para usar: https://hub.docker.com/r/abenitezsan/

Nota: En esta entrada se asume que el lector tiene ya un conocimiento básico sobre contenedores y Docker, sino los conceptos pueden ser algo complicados de entender.

¿Qué es Kubernetes?

Kubernetes es una herramienta de orquestación de contenedores que requiere al menos 3 nodos, uno actuando como master (o director de la orquesta) y el resto de nodos como repositorios de contenedores:

La interacción con Kubernetes se hace únicamente con el master, al cual le daremos órdenes de despliegue, arranque, parada, etc. de nuestros contenedores.

Podemos desglosar Kubernetes en los siguientes elementos:

  • Pods: La instancia mínima  que usará Kubernetes consistirá de al menos una imagen Docker, aunque pueden ser más. Suele usarse una sola por facilidad de control,  por lo tanto,  un pod puede ser desde una aplicación SpringBoot (sirviendo una REST API),   hasta una base de datos  (sirviendo datos a los demás pods). Los pods por definición son stateless,  Kubernetes los desplegará y destruirá constantemente  en función de las necesidades  actuales. Si los pods deben persistir datos,  deben apoyarse en volumes.
  • Deployments: Un despliegue en Kubernetes no es más que la plantilla de un pod que dará instrucciones a Kubernetes de cómo crear los pods asociados, como arrancar el contenedor Docker, cuantas replicas queremos por defecto, etc. Los despliegues acabarán creando Replication controllers  que por defecto mantendrán el número de réplicas que especificamos en el despliegue, pero nos permitirán cambiar estas a voluntad en el futuro.
  • Services: Los pods no son visibles más allá de  su propio contenedor, sin conocer su ip:puerto que cambia con bastante frecuencia, y no se puede interactuar con ellos desde el exterior. Para solucionar esto,   existen los servicios que  actúan como capa encima de nuestros pods, gestionando el balanceo de carga entre ellos,  y  permitiendo acceso desde el interior (Red de Nodos Kubernetes) o el exterior.

Los servicios se sirven de servidores DNS,  instalados en la red (cómo https://github.com/kubernetes/dns)  para registrarse en esta y permitir el acceso por nombres de servicio a sus pods, facilitando el descubrimiento de servicios.

Por ejemplo, para acceder a nuestros pods, que contienen una aplicación  de gestión de clientes desplegada en un tomcat, solo necesitaríamos esta url: http://customerService:8080

 

  • Volumes & Persistent volumes: Como comentamos al hablar de los pods, necesitamos volúmenes para gestionar el guardado/acceso de datos a los discos físicos de nuestros pods. En aplicaciones que necesiten guardado permanentes de datos  usaremos persistent volumes,   los cuales pueden existir en muchos sistemas de almacenamiento. Puedes consultar en cuales en este enlace: https://kubernetes.io/docs/concepts/storage/persistent-volumes/.  En esta entrada solo usaremos hostPath al tener que trabajar con Minikube.
  • Ingress Controllers: Usaremos los ingress  para redirigir nuestro tráfico a los servicios expuestos, agrupando todas sus rutas bajo un solo dominio. Ingress es realmente una convención sobre la que diversos tipos de controladores (como nginx o traefik)  trabajan para redirigir las llamadas del exterior a nuestros servicios.

 

¿Qué es Minikube?

Cómo comentamos antes, Kubernetes necesita al menos 3 nodos para funcionar, lo cual es un engorro  para poder hacer pruebas locales. Para eso se creó Minikube, el cual es una versión reducida de Kubernetes, corriendo en una única máquina virtual, la cual  actúa de maestro y esclavos a la vez.

Para instrucciones de instalación y más información de Minikube podéis consultar  la página del proyecto: https://github.com/kubernetes/minikube

Así mismo, se necesitará instalar kubectl para poder comunicarse con el servidor Kubernetes.  Podéis encontrar instrucciones de instalación en el siguiente enlace: https://kubernetes.io/docs/tasks/tools/install-kubectl/

Nota para usuarios Windows: Aunque en la documentación se comenta  que Minikube funciona correctamente con hyper-v es muy posible que encontréis multitud de  problemas, en ese caso es recomendable usar VirtualBox.

Arrancando Minikube

Una vez tengamos Minikube instalado en nuestra máquina virtual, lo arrancaremos usando línea de comandos:

minikube start

Si todo ha ido bien veremos un texto como el siguiente:

 

 

 

 

 

Podemos comprobar que minikube (y kubectl) se han instalado correctamente, usando kubectl para obtener la lista de servicios desplegados:

kubectl get services

Actualmente solo deberíamos ver el servicio kubernetes  en la lista.

Kubernetes es manejado  normalmente por comandos, aunque puede usarse algún dashboard para  manejarlo. Minikube viene instalado con uno por defecto  que podemos arrancar con el siguiente comando:

Minikube dashboard

Al acabar una ventana del navegador se abrirá automáticamente:

Desplegando nuestra aplicación

Desplegando MYSQL

Comenzaremos desplegando un servidor Mysql, para el cual necesitaremos antes crear un volumen persistente, ya que necesita almacenar sus datos entre arranques.
Para ello usaremos el siguiente script: https://github.com/abenitezsan/kubernetesDemo/blob/master/scripts/persistentVolume.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
name: mysql-pv
spec:
capacity:
storage: 20Gi
accessModes:
- ReadWriteOnce
hostPath:
path: /c/Users/adbe/minikubeSD

¿Qué hemos hecho aquí?

  • apiVersion: Versión del api que vamos a usar.
  • kind:   Tipo de componente de Kubernetes a crear, en este caso un volumen persistente.
  • metadata: Listado de metadatos de este elemento, tenemos libertad de poner lo que queramos, más tarde podremos usarlos para decirles a otros elementos de kubernetes como encontrar este volume.
  • storage: Especificamos que queremos 20Gb reservados para nuestro volumen persistente.
  • accessModes: ReadWriteOnce  significa que será leído y escrito por un solo nodo, hay otros modos disponibles para permitir lectura y/o escritura por multiples nodos.
  • HostPath: Especificamos el tipo de volume persitente que usaremos, una ruta en la máquina host de la máquina virtual.
  • path:  Ruta física de nuestra máquina ( Advertencia: /c/<username> es la única ruta permitida en Minikube para usuarios Windows). 

Y ejecutamos el script usando el siguiente comando: 

kubectl create -f persistentVolume.yaml

Y luego verificamos que se ha creado correctamente:

kubectl get pv
NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM STORAGECLASS REASON AGEMysql-pv 20Gi RWO Retain Bound 0d

Con nuestro volumen persistente creado estamos listos para desplegar Mysql, usando el siguiente script : https://github.com/abenitezsan/kubernetesDemo/blob/master/scripts/mysqlDeploy.yaml

apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
ports:
- port: 3306
selector:
app: mysql
type: NodePort
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pv-claim
spec:
accessModes:
- ReadWriteOnce
storageClassName: ""
resources:
requests:
storage: 20Gi
---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: mysql
spec:
strategy:
type: Recreate
template:
metadata:
labels:
app: mysql
spec:
containers:
- image: mysql:5.6
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
value: password
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- mountPath: /var/lib/mysql
name: mysql-persistent-storage
volumes:
- name: mysql-persistent-storage
persistentVolumeClaim:
claimName: mysql-pv-claim

En este script tenemos  realmente 3 scripts diferentes de Kubernetes, separados por —-

Echemos un vistazo a cada uno de ellos  distinguiéndolos por su parámetro “kind”:

  • PersistentVolumeClaim : Creando una reclamación de volumen persistente  le pedimos a kubernetes que busque una cantidad de espacio en disco disponible en cualquier volumen persistente y la mantenga asignada a esta reclamación. Kubernetes mantendrá siempre  la organización del espacio disponible en todos los volúmenes persistentes y gestionará su distribución.
  • metadata: Usamos el metadata para darle un nombre a nuestra reclamación.
  • spec : En esta sección especificamos a Kubernetes cuanto espacio queremos reclamar  y si necesitamos alguna clase especial que se encargue del manejo de este ( no en nuestro caso, pero si es común en sistemas de almacenamiento más complejos como Amazon S3).
  • Deployment: Aquí estamos creando tanto  plantilla de despliegue de nuestros pods Mysql como la configuración del Controlador de replicación.
  • strategy => type: Tipo de estrategia de creación de nuestros pods. Usando recreate especificamos que queremos que el pod se cree de nuevo en cada reinicio, update de la imagen, etc.
  • template: En esta sección especificamos la plantilla del pod.
  • metadata: Categorización de nuestro pod, usamos un nombre identificativo para ser luego encontrado  por los servicios.
  • image: Imagen de Docker en  el repositorio de DockerHub  con la versión asociada. Con esta configuración,  Kubernetes va  ir a buscar la imagen a: https://hub.docker.com/_/mysql/.
  • env: Configuración de la imagen Docker, en nuestro caso solo configuramos la contraseña root de mysql.
  • port: Especificación del puerto que nuestro pod abrirá para comunicarse.  En este caso usamos el de por defecto,  en el que levanta mysql (3406) para facilitar la configuración. Este puerto es solo alcanzable desde el interior.
  • volumeMounts: Aquí estamos especificando a mysql que monte un volumen  con el nombre especificado, indicando la ruta interna del contenedor que usará (ruta de almacenamiento por defecto de mysql, de nuevo intentamos facilitar la configuración).
  • Volumes: Aquí especificamos el volumen montado en volumeMounts  a nuestro claim, creado justo antes  (claimName).

No hemos especificado ningún valor para replicas,  por lo que por defecto,  Kubernetes creará una sola replica de Mysql.

  • Service: Creamos  un componente de servicio “Mysql” y le decimos que exponga nuestros pods de Mysql.
  • port: Con 3306 establecemos que ese será el puerto usado siempre por los pods asociados a este servicio.
  • selector: Selector usado para encontrar los pods asociados, en este caso estamos diciéndole a Kubernetes que para este servicio queremos todos los pods que tengan app: mysql  en sus metadatos.
  • type: Por defecto los servicios son solo visibles dentro de la red de Kubernetes, pero podemos exponerlos usando algunos tipos que lo permiten como NodePort .

Más sobre tipos de servicios en  https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services—service-types. Este es el único disponible usando Minikube.

Aunque estemos usando targetPort,  al no especificar un targetPort,  no estamos exponiendo el servicio con un puerto determinado, sino que se generará automáticamente.

Cuando hayamos terminado de revisar la configuración podemos  crear los nuevos elementos usando:

kubectl create -f persistentVolume.yaml

Y podemos ver si ahora hay algún claim sobre nuestro almacenamiento persistente:

kubectl get pv

NAME   CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS   CLAIM                 STORAGECLASS   REASON    AGE                

mysql-pv   20Gi       RWO       Retain          Bound     default/mysql-pv-claim                 0d                        

 

Podemos ver también si nuestros pods han arrancado correctamente:

kubectl get pods

NAME                         READY      STATUS             RESTARTS   AGE

mysql-2703443597-wr2t3        1/1       running                 0     27s

 

Y también el servicio:

kubectl get services

NAME                 CLUSTER-IP   EXTERNAL-IP   PORT(S)          AGE

kubernetes           10.0.0.1     <none>        443/TCP          15d

mysql                10.0.0.54    <nodes>       3306:32077/TCP   45s

 

Que tal como vemos, está registrado internamente en el puerto 3306, pero expuesto en el 32077. En este punto, podemos usar algún gestor de Mysql para conectar a nuestra Base de datos y administrarla. Podemos obtener la IP expuesta por Minikube ejecutando:

minikube status

minikube: Running

localkube: Running

kubectl: Correctly Configured: pointing to minikube-vm at 192.168.99.100

 

Para que los siguientes servicios funcionen correctamente necesitamos crear el esquema “kubernetesDemo” en nuestra base de datos.
Así mismo, en este punto es interesante reiniciar Minikube para ver que el almacenamiento persistente funciona correctamente y nuestro esquema se mantiene tras reinicios.

Desplegando MicroServicios

Una vez que tenemos nuestra base de datos funcionando con el esquema creado, estamos listos para desplegar nuestros Microservicios (aplicaciones SpringBoot).
Es interesante observar la configuración de acceso a base de datos de ellas:

spring.jpa.hibernate.ddl-auto=create

spring.datasource.url=jdbc:mysql://mysql:3306/kubernetesDemo

spring.datasource.username=root

spring.datasource.password=password

Como podemos ver, estamos estableciendo como única cadena de conexión “mysql”, que es el nombre del servicio creado en el paso anterior, a través del servicio de DNS Kubernetes, encontrar nuestro servicio y nos redirigirá al pod correspondiente.
Podemos ver ahora los scripts preparados para crear nuestros dos servicios, en ambos estamos creando un despliegue y un servicio asociado a él.

https://github.com/abenitezsan/kubernetesDemo/blob/master/scripts/CustomerDeployment.yaml 

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

  name: customerws

spec:

  replicas: 2

  template:

    metadata:

      labels:

        app: customerws

    spec:

      containers:

      - name: customerws

        image: abenitezsan/customerservice

        imagePullPolicy: "Always"

        ports:

        - containerPort: 8080

---

kind: Service

apiVersion: v1

metadata:

  name: customerws-service

spec:

  selector:

    app: customerws

  ports:

  - protocol: TCP

    port: 8080

    targetPort: 8080

  type: NodePort

https://github.com/abenitezsan/kubernetesDemo/blob/master/scripts/productDeployment.yaml

apiVersion: extensions/v1beta1 # for versions before 1.6.0 use extensions/v1beta1

kind: Deployment

metadata:

  name: productws

spec:

  replicas: 2

  template:

    metadata:

      labels:

        app: productws

    spec:

      containers:

      - name: productws

        image: abenitezsan/productservice

        imagePullPolicy: "Always"

        ports:

        - containerPort: 8080

---

kind: Service

apiVersion: v1

metadata:

  name: productws-service

spec:

  selector:

    app: productws

  ports:

  - protocol: TCP

    port: 8080

    targetPort: 8080

  type: NodePort

Ambos son muy similares y su especificación debería ser bastante clara con lo comentando durante la descripción de Mysql, sin embargo, un par de apartados merecen una mención:

  • image (abenitezsan/productservice): Como en este caso estamos usando repositorio propio de DockerHub, hemos de especificarlo.
  • imagePullPolicy: “Always” sirve para decir a Kubernetes que cada vez que cree un pod verifique si hay una versión de la imagen Docker que actualizar.
  • replicas: En este caso especificamos que queremos 2 réplicas de cada servicio activas siempre.

Ejecutamos los dos scripts:

kubectl create -f productDeployment.yaml

kubectl create -f CustomerDeployment.yaml

Y miramos el estado de nuestros nuevos pods:

kubectl get pods

NAME                          READY     STATUS    RESTARTS   AGE

customerws-3577278556-30m4l   1/1       Running   18         8d

customerws-3577278556-xx239   1/1       Running   13         8d

mysql-2703443597-kss92        1/1       Running   4          47m

productws-1360159399-655rq    1/1       Running   8          8d

productws-1360159399-w0t4p    1/1       Running   18         8d

 

Podemos ver cuatro nuevos pods, dos por cada uno de los servicios que hemos creado.
Y nuestros nuevos servicios:

kubectl get services

NAME                 CLUSTER-IP   EXTERNAL-IP   PORT(S)          AGE

customerws-service   10.0.0.215   <nodes>       8080:30471/TCP   10d

kubernetes           10.0.0.1     <none>        443/TCP          15d

mysql                10.0.0.54    <nodes>       3306:32077/TCP   8d

productws-service    10.0.0.158   <nodes>       8080:31400/TCP   8d

 

En este punto es interesante acceder a nuestros nuevos servicios y comprobar que están funcionando correctamente, usando la IP obtenida con Minikube status y el puerto obtenido en el listado anterior.

Enrutando nuestra aplicación-Ingress

En este punto tenemos nuestra aplicación expuesta al exterior, pero son algo difíciles de alcanzar, al tener que ir manteniendo los puertos de cada. Para solventar esto usaremos Ingress, creando reglas para enrutar las peticiones a nuestra aplicación directamente desde los puertos 80/443.

Echemos un vistazo a la siguiente regla Ingress: https://github.com/abenitezsan/kubernetesDemo/blob/master/scripts/ingress-rule.yaml

>apiVersion: extensions/v1beta1

kind: Ingress

metadata:

 name: customer-ingress

 annotations:

  ingress.kubernetes.io/rewrite-target: /

spec:

 rules:

   - host:

     http:

       paths:

         - path: /customer

           backend:

             serviceName: customerws-service

             servicePort: 8080

         - path: /product

           backend:

             serviceName: productws-service

             servicePort: 8080

Aspectos a destacar del script:

  • ingress.kubernetes.io/rewrite-target: / : Aquí estamos haciendo una reescritura interna del enrutamiento. Por ejemplo, si llamamos a <ip>/customer , en lugar de mapearlo por defecto a customerServiceIP:port/customer, el mapeo será a customerServiceIP:port.
  • host: Virtual host donde aplica la regla Ingress. En nuestro caso lo dejamos vacío, porque queremos que la regla aplique a todas las peticiones.
  • paths: Kubernetes usará la configuración enrutara a nuestras peticiones con la ruta configurada al servicio y Puerto que le digamos.

NOTA: En este caso usamos el puerto configurado como interno del servicio.

Ejecutamos nuestro script:

kubectl create -f ingress-rule.yaml

Y vemos si se ha desplegado correctamente:

kubectl get ing

NAME               HOSTS     ADDRESS          PORTS     AGE

customer-ingress   *         192.168.99.100   80        8d

Esto por sí solo no bastará, además de reglas Ingress, necesitamos algún controlador que las gestione en entornos de producción. Uno bastante popular es Traefik : https://github.com/containous/traefik .

En Minikube en cambio podemos usar su controlador, incluido por defecto (nginx), que necesitamos activar:

minikube addons enable ingress

Y ya está, ahora nuestros servicios deberían estar expuestos en nuestras rutas, usando la ip obtenida con Minikube status. Podemos acceder a nuestros servicios:

http://192.168.99.100/customer

http://192.168.99.100/product

 

Deja un comentario

Menú de cierre