Hoy vamos a jugar un poco con la tecnología Kubernetes. En su día ya hablamos del modo de contener-izar aplicaciones en contenedores Docker. Ahora vamos a dar el siguiente paso que consiste en la orquestación de contenedores. Cuando hablamos de orquestación nos referimos a la capacidad de un entorno cloud de mantener una aplicación multicontenedor, permitiendo una escalabilidad hacia arriba y hacia abajo, con un garantía de la disponibilidad. Monitorización de los contenedores y auto-reparación de los contenedores comprometidos. Expulsión de contenedores obsoletos para la introducción de unos nuevos con un nuevo despliegue sin perdida de servicio. Una gran característica de Kubernetes es que la configuración se gestiona de forma declarativa, facilitando de ese modo, lo que se pretende, y comparándolo con lo que se tiene, y de esa manera poder mantener el down-time bajito. Como en otras ocasiones todo esto lo probaremos después en un entorno de pruebas que montaremos desde cero.
Kubernetes fue desarrollado por Google a principios de la decada de 2000. En aquellos años, Google usaba para orquestar y administrar su nube dos sistmas llamados Borg y Omega. Borg fue el primero, permitía gestionar los recursos, orquestando las aplicaciones y servicios encapsuladas en contenedores. Esto permitía un escalado de forma dinámica para garantizar alta disponibilidad y reubicar contenedores en caso de fallo. Posteriormente Omega se introdujo, como una mejora a Borg, mejorando la escalabilidad con un enfoque más declarativo. Fue Brendan Burns quien llevó los primeros trabajos con Borg y que posteriormente co-fundó Kubernetes. En 2014 se liberó en un proyecto open source con el nombre definitivo que quiere decir timonel en Griego. Más tarde, fue donado a la CNCF por sus siglas del ingles, Cloud Native Computing Foundation, una organización de código abierto que promueve tecnologías de la nube. Hoy día la plataforma es tremendamente activa, con innumerables herramientas, dashboards, y variantes.
Entonces, ¿que es Kubernetes?
Básicamente dos cosas:
Un cluster para correr aplicaciones.
Un orquestador de aplicaciones.
Como cluster, Kubernetes se compone de un conjunto de máquinas fisicas que corren las aplicaciones. Cada una de estas máquinas denominadas nodos, pueden ser servidores fisicos o virtuales, instancias en la nube, o incluso Raspberri Pi’s. Los nodos es donde las aplicaciones corren. Como todos los los clusters, tiene un controlador denominado Control Plane. Este Control Plane expone una API, asigna trabajo a los nodos y registra el estado de las aplicaciones.
Como orquestador, se encarga de la tarea de desplegar y gestionar la aplicaciones. Estas aplicaciones empaquetadas en contenedores nos ofrecerán escalabilidad automática, indicando de forma declarativa el numero de instancias de una aplicación especifica, llamadas Pods, en funcion de la carga y asegurando los recursos suficientes para atender la demanda, escalando los dichos pods.
Como hemos dicho, el cluster se compone del Control Plane y los nodos. A estos nodos se los denomina workers. EL flujo es el siguiente:
En cada cluster, un Control Plane, CP es una colección de sistemas que comprenden una base de datos, el scheduler, un cron, etc que mantienen de acuerdo a la declaración, el comportamiento de todo el cluster. Para un sistema de pruebas, un CP será suficiente, para un sistema que busca alta disponibilidad, 3 o 5 es lo recomendado.
La piezas para llevar a cabo estas tareas son el API Server, el Cluster Store, el Controler Manager, el Scheduler y el Cloud controller Manager.
El cluster Store es la unica pieza statefull encargada en persistir la configuración completa del cluster y su estado. Está basado en la base de datos Etcd, una conocida base de datos distribuida.
El controler manager, encargado de implementar los controladores en background que monitorizan el resto de componentes del cluster. Se encarga de comparar el estado actual y el estado deseado, comprobar las diferencias y reconciliar las diferencias.
El Scheduler, encargado de repartir nuevas tareas en los nodos workers adecuados.
El Cloud Controller Manager. Si se está corriendo un cluster kubernetes en una nube soportada, tipo AWSo Google, puede interactuar directamente sobre la propia nube facilitando el escalado instanciando nuevos nodos.
Hasta ahora hemos visto el control Plane. Por otro lado tenemos los nodos o workers que desarrollan prpiamente la tarea del Cluster. A vista de pájaro cumplen con las siguientes funciones:
Llamar la API server para nuevas asignaciones de trabajo.
Ejecutar la asignación de tareas.
Reportar resultados al Control Plane.
Dentro de cada nodo podemos encontrar el Kubelet, el Kube-proxy y el container Runtime.
Kubelet: es el agente que gestiona los contenedores. Ademas se comunica con el controlador del cluster y se asegura de que los contenedores están en un estado correcto.
Kube-proxy: Responsable de mantener las reglas de red en los nodos.
Ayuda a las comunicaciones de red entre los servicios del cluster
Container Runtime: Es el responsable de ejecutar los contenedores. Docker es la opción por defecto pero varias opciones más están disponibles.
En el mundo Kubernetes la unidad indivisible es el Pod, es equivalente a como podría ser el contenedor para el mundo Docker. Y son las encargados de contener la ejecución de los contenedores, al menos uno. Aunque se pueden encontrar escenarios en múltiples contenedores corran en el mismo pod.
El escalado se realiza por medio de pods, no a través de añadir más contenedores a un pod. Los pods no son eternos, se crean, viven y mueren. En caso de fallo, se destruye y se crea uno nuevo, con id e IP nueva. Los Pods son inmutables, es decir nunca se modifican y si hay que cambiar o actualizar algo se reemplaza con uno nuevo con la nueva configuración.
Otro objeto de vital importancia en Kubernetes es el Deployment y éste a su vez se envuelve sobre un pod y le añade funciones como auto-reparación o escalado. Un Deployment define el estado que queremos para una aplicación, y el controlador de Deployment se encarga de mantener dicho estado en la aplicación. Las funciones de un Deployment son despliegue, escalado y actualizado:
Desplegado de una aplicación: Un Deployment puede utilizarse para desplegar una aplicación nueva o actualizar una aplicación existente.
Escalado de una aplicación: Un Deployment puede utilizarse para escalar una aplicación aumentando o disminuyendo el número de réplicas de la aplicación.
Actualización de una aplicación: Un Deployment puede utilizarse para actualizar una aplicación cambiando la imagen de contenedor que se utiliza para crear los contenedores de la aplicación. Para actualizar una aplicación, se puede modificar la declaración del Deployment para especificar la nueva imagen de contenedor.
Por ultimo tenemos el objeto Servicio, que su función es proporcionar una interfaz de red para un conjunto de pods. Un servicio asigna una dirección IP y un nombre DNS a un conjunto de pods, y proporciona enrutamiento entre los pods. Una vez declarado un servicio podemos empezar a consumir nuestra aplicación a través de éste.
Y sin más vamos a meternos en harina. Para esta prueba vamos a usar Minikube que nos va a propocionar todo lo necesario para poder hacer unas pruebas. Minikube nos permite ejecutar un clúster de Kubernetes en una sola máquina. Esto hace que sea una opción ideal para el desarrollo local y la experimentación con Kubernetes.
Pues bien primero vamos a instalarlo, siguiendo la documentación que es muy sencillo:
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube
Y listo, vamos a inicializar el cluster con un simple:
daniel@docker-home ~ minikube start
Dependiendo de tu plataforma, puede requerir revisar https://minikube.sigs.k8s.io/docs/drivers/ para ver cual se adecua mas a tus necesidades. En mi Linux por defecto me usó qemu, pero tuve problemas para acceder al servicio. puedes usar drivers especificos para Mac o incluso Windows . Finalmente opte por kvm2 y todo de perlas. Este comando me confirmó que mi plataforma estaba preparada para usar kvm
daniel@docker-home ~ kvm-ok
INFO: /dev/kvm exists
KVM acceleration can be used
Para seleccionar el tipo de maquina que se va a descargar puedes hacerlo especificando el parametro driver:
minikube start --driver=kvm2
😄 minikube v1.32.0 en Ubuntu 22.04
✨ Using the kvm2 driver based on user configuration
💾 Descargando el controlador docker-machine-driver-kvm2:
> docker-machine-driver-kvm2-...: 65 B / 65 B [---------] 100.00% ? p/s 0s
> docker-machine-driver-kvm2-...: 13.01 MiB / 13.01 MiB 100.00% 8.13 MiB
👍 Starting control plane node minikube in cluster minikube
🔥 Creando kvm2 VM (CPUs=2, Memory=3900MB, Disk=20000MB) ...
🐳 Preparando Kubernetes v1.28.3 en Docker 24.0.7...
▪ Generando certificados y llaves
▪ Iniciando plano de control
▪ Configurando reglas RBAC...
🔗 Configurando CNI bridge CNI ...
▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🔎 Verifying Kubernetes components...
🌟 Complementos habilitados: storage-provisioner, default-storageclass
🏄 Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
Si os fiajis vereis que se descarga una maquina virtual que contiene el cluster. Y como tal se puede entrar en ella, averiguamos su ip:
daniel@docker-home ~ minikube ip
192.168.39.218
y esta en pie, luego veremos como acceder:
daniel@docker-home ~ ping 192.168.39.218
PING 192.168.39.218 (192.168.39.218) 56(84) bytes of data.
64 bytes from 192.168.39.218: icmp_seq=1 ttl=64 time=0.523 ms
64 bytes from 192.168.39.218: icmp_seq=2 ttl=64 time=0.413 ms
Lo primero que necesitamos es nuestra declaracion de un despliegue, de nuestra app, umero de replicas, etc:
daniel@docker-home ~ cat hello-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-deployment
labels:
app: hello
spec:
replicas: 3
selector:
matchLabels:
app: hello
template:
metadata:
labels:
app: hello
spec:
containers:
- name: hello
image: gcr.io/google-samples/hello-app:1.0
ports:
- containerPort: 8080
Es en la spec donde le decimos cuantas replicas y que use nuestra imagen de nuestra app. En este caso una app muy sencilla, una especie de hello world. Y le pedimos 3 replicas, vamos a aplicarlo. Pero antes es precisdo que instalemos otro paquete, (el ultimo) que es kubectl, para hablar con nuestro cluster:
daniel@docker-home ~ sudo apt-get update
daniel@docker-home ~ sudo apt-get install -y apt-transport-https ca-certificates curl
daniel@docker-home ~ curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
daniel@docker-home ~ echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.29/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
daniel@docker-home ~ sudo apt-get update
daniel@docker-home ~ sudo apt-get install -y kubectl
Y ahora ya si, lo aplicamos:
daniel@docker-home ~ kubectl apply -f hello-deployment.yaml
deployment.apps/hello-deployment created
Nuestro cluster estará desplegando los contenedores en sus respectivos pods. Pero me ovidé, un lapsus, que 3 son pocos asi que voy a escalar a 5, pido 5 replicas de mi hello-deployment:
daniel@docker-home ~ kubectl scale --replicas=5 deployment/hello-deployment
deployment.apps/hello-deployment scaled
Puedo solicitar los nodos, y comprobar que solo tengo uno:
daniel@docker-home ~ kubectl get nodes
NAME STATUS ROLES AGE VERSION
minikube Ready control-plane 8m v1.28.3
Y comprobar que los pods han subido a 5, nótese como dos de ellos solo tienen unos segundos de vida:
daniel@docker-home ~ kubectl get pods
NAME READY STATUS RESTARTS AGE
hello-deployment-8ccf68948-8w2tw 1/1 Running 0 27s
hello-deployment-8ccf68948-p9pl5 1/1 Running 0 2m47s
hello-deployment-8ccf68948-wbql9 1/1 Running 0 27s
hello-deployment-8ccf68948-wjjfr 1/1 Running 0 2m47s
hello-deployment-8ccf68948-zqcnz 1/1 Running 0 2m47s
Especialmete util el comando describe que permite describir cualquier tipo de objeto, o todo:
daniel@docker-home ~ kubectl describe all
Name: hello-deployment-8ccf68948-8w2tw
Namespace: default
Priority: 0
Service Account: default
Node: minikube/192.168.39.218
Start Time: Thu, 04 Jan 2024 21:32:03 +0100
Labels: app=hello
pod-template-hash=8ccf68948
Annotations:
Status: Running
IP: 10.244.0.7
[....]
Podemos entrar en nodo del cluster y comprobar la existencia de los contenedores:
daniel@docker-home ~ minikube ssh
_ _
_ ( ) ( )
___ ___ (_) ___ (_)| |/') _ _ | |_ __
/' _ ` _ `\| |/' _ `\| || , < ( ) ( )| '_`\ /'__`\
| ( ) ( ) || || ( ) || || |\`\ | (_) || |_) )( ___/
(_) (_) (_)(_)(_) (_)(_)(_) (_)`\___/'(_,__/'`\____)
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8f6fa6899455 dd1b12fcb609 "/hello-app" About an hour ago Up About an hour k8s_hello_hello-deployment-8ccf68948-8w2tw_default_71dd2449-f2df-458c-aee6-f48ae06f173f_0
9b2d0783b1dc dd1b12fcb609 "/hello-app" About an hour ago Up About an hour k8s_hello_hello-deployment-8ccf68948-wbql9_default_51f6b2d2-37de-40b6-bcf6-9573b4be08f4_0
21203b2c06d2 registry.k8s.io/pause:3.9 "/pause" About an hour ago Up About an hour k8s_POD_hello-deployment-8ccf68948-8w2tw_default_71dd2449-f2df-458c-aee6-f48ae06f173f_0
c6ef7451eb28 registry.k8s.io/pause:3.9 "/pause" About an hour ago Up About an hour k8s_POD_hello-deployment-8ccf68948-wbql9_default_51f6b2d2-37de-40b6-bcf6-9573b4be08f4_0
6287411e3ab4 gcr.io/google-samples/hello-app "/hello-app" About an hour ago Up About an hour k8s_hello_hello-deployment-8ccf68948-zqcnz_default_c94529ab-9054-4ab3-bf67-29e7508b96ef_0
a2d7594214e2 gcr.io/google-samples/hello-app "/hello-app" About an hour ago Up About an hour k8s_hello_hello-deployment-8ccf68948-wjjfr_default_7006facd-c203-4808-8981-09f280af6a35_0
d9e96bda0aca gcr.io/google-samples/hello-app "/hello-app" About an hour ago Up About an hour k8s_hello_hello-deployment-8ccf68948
[...]
Finalmente exponemos un servicio para nuestro despliegue y a continuación podemos ver la descripcion de este:
daniel@docker-home ~ kubectl expose deployment hello-deployment --type=LoadBalancer --name=my-service-hw
service/my-service-hw exposed
daniel@docker-home ~ kubectl describe services
Name: kubernetes
Namespace: default
Labels: component=apiserver
provider=kubernetes
Annotations:
Selector:
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.96.0.1
IPs: 10.96.0.1
Port: https 443/TCP
TargetPort: 8443/TCP
Endpoints: 192.168.39.218:8443
Session Affinity: None
Events:
Name: my-service-hw
Namespace: default
Labels: app=hello
Annotations:
Selector: app=hello
Type: LoadBalancer
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.99.56.83
IPs: 10.99.56.83
Port: 8080/TCP
TargetPort: 8080/TCP
NodePort: 30864/TCP
Endpoints: 10.244.0.3:8080,10.244.0.4:8080,10.244.0.5:8080 + 2 more...
Session Affinity: None
External Traffic Policy: Cluster
Events:
Finalmente podemos invocar nuestro servicio en el cluster que hemos ccreado en modo loadbalancer, y comprobaremos como cada vez que se le llama con un curl contesta uno de los contenedores en cada pod de nuestro cluster:
daniel@docker-home ~ curl http://192.168.39.218:30864
Hello, world!
Version: 1.0.0
Hostname: hello-deployment-8ccf68948-zqcnz
daniel@docker-home ~ curl http://192.168.39.218:30864
Hello, world!
Version: 1.0.0
Hostname: hello-deployment-8ccf68948-8w2tw
daniel@docker-home ~ curl http://192.168.39.218:30864
Hello, world!
Version: 1.0.0
Hostname: hello-deployment-8ccf68948-wjjfr
daniel@docker-home ~ curl http://192.168.39.218:30864
Hello, world!
Version: 1.0.0
Hostname: hello-deployment-8ccf68948-zqcnz
daniel@docker-home ~ curl http://192.168.39.218:30864
Hello, world!
Version: 1.0.0
Hostname: hello-deployment-8ccf68948-wbql9
daniel@docker-home ~ curl http://192.168.39.218:30864
Hello, world!
Version: 1.0.0
Hostname: hello-deployment-8ccf68948-zqcnz
Ahora se pueden hacer alghunas pruebas de resiliencia, es curioso comprobar como al entrar en el nodo y hacer un "docker remove" de un contenedor, el controlador de despliegue vuelve a restablecer los 5.
Pues eso ha sido todo, espero que os haya parecido tan interesante como a mi, un saludo, Daniel