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/
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 # forversions before 1.6.0use 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/1Running 188d
customerws-3577278556-xx239 1/1Running 138d
mysql-2703443597-kss92 1/1Running 447m
productws-1360159399-655rq 1/1Running 88d
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.
- Customer Service: http://192.168.99.100:30471
- Product Service: http://192.168.99.100:31400
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
/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: