Kubernetes para devs (1/2)

Developer deploying on Kubernetes
Developer deploying on Kubernetes
Helmcode SPONSORED
CTA Image

¿Quieres mejorar los procesos de desarrollo, la eficiencia, la escalabilidad y seguridad en tu Cloud a la vez que ahorras costes?

Contacta con nuestro equipo de expertos en infraestructura Cloud y lleva tus aplicaciones al siguiente nivel.

Visita nuestra web

Kubernetes es una de las herramientas más utilizadas en infraestructura por las empresas y se ha convertido en el estándar cuando se trata de llevar aplicaciones en contenedores a escala en todo el mundo.

Es por esto que es muy importante que no solo las personas que administran Kubernetes tengan conocimientos del mismo, sino que las personas de desarrollo sepan sus bondades y particularidades para poder hacer un desarrollo más enfocado y adaptado a este orquestador de contenedores.

Esta guía no trata de enseñar Kubernetes, aunque dará unos principios básicos. Lo que se intentará cubrir es qué cosas deben saber las personas de desarrollo para poder crear apps más óptimas para esta plataforma. Dicho esto.

Empecemos por el principio...

Antes de empezar a hablar si quiera de Kubernetes debemos tener muy claro el concepto de contenedores.

¿Qué son los contenedores y cómo funcionan?

Podríamos hacer un post entero hablando de esto así que por resumir, un contenedor es una especie de paquete que contiene toda tu aplicación y lo que necesita para funcionar: código, librerías, dependencias, variables de entorno, etc.

Para poder levantar un contenedor, antes debemos crear una imagen, esto no es mas que un elemento que nos ayuda a configurar y especificar con exactitud qué necesita y cómo debe correr nuestro contenedor. La forma más habitual de poder hacer esto es utilizando Docker. Con esta herramienta podrás construir una imagen del contenedor y, si está bien configurada, podrás levantar tu aplicación en contenedores donde quieras.

🚀 Tener esto claro nos lleva al primer concepto importante.

0️⃣ Inmutabilidad

Como hemos dicho, el contenedor debe tener tu código, dependencias, etc. Esto quiere decir que si quieres editar este código y actualizar la versión de tu aplicación deberás construir una nueva imagen del contenedor. Lo mismo si quieres cambiar la versión de una librería o lenguaje que estes utilizando.

Esto en local, antes de subir tus cambios, los podrás probar siempre con Docker.

En Kubernetes, lo ideal es que en el repositorio donde vive tu aplicación tengas Pipelines automatizados que hagan el proceso de construir la nueva imagen de tu aplicación y el posterior despliegue en Kubernetes.

NUNCA debes editar el código dentro del contenedor de Kubernetes. ¿Por qué? Esto mejor lo vemos con el siguiente concepto.

1️⃣ Stateless

Por defecto, un contenedor no guarda ningún tipo de estado ni tiene ningún tipo de persistencia. Esto quiere decir que cualquier cambio que hagas dentro de un contenedor, por defecto, se va a borrar en el momento que muera o se reinicie este contenedor.

Este concepto es sencillo pero es sumamente importante porque condiciona mucho el desarrollo de las aplicaciones con respecto a lo que se suele hacer en servidores tradicionales.

2️⃣ Logs

Antes los logs debían guardarse en ficheros para poder persistirlos. En contenedores debemos olvidarnos de esta práctica y debemos empezar a expulsar todos nuestros logs de aplicación por standard output. Lo que quiere decir que a nivel de aplicación se quitará esta responsabilidad y lo único que debe hacerse es expulsar los logs por la salida misma del proceso. Todos los logs, info, warnings, error, etc.

🤬 ¡Pero no puedo perder logs! No los perderás, en Kubernetes esta responsabilidad debe recaer sobre un sistema de centralizado de logs, los cuales sabrán leer la salida de cada contenedor y la guardarán en alguna base de datos destinada para esto. De esta forma podrás tanto almacenar como consultar logs pasados sin depender de guardar estos logs en ficheros.

Si por el contrario decides igualmente guardar tus logs en fichero lo único que conseguirás es ralentizar tu aplicación por escribir en disco, ocupar almacenamiento adicional de forma innecesaria y encima esos logs en cuanto el contenedor muera o se reinicie se perderá y será irrecuperable.

3️⃣ Almacenamiento de ficheros estáticos

Muchas aplicaciones requieren guardar o procesar PDFs, CSVs, imágenes o cualquier otro tipo de fichero estático. Debido al principio que hemos comentado de Stateless, estos ficheros deben quedar por fuera del contenedor.

Lo ideal aquí es contar con algún tipo de almacenamiento de objetos como pueden ser buckets de S3 en AWS, Blob Storage en Azure o Cloud Storage en GCP. Este tipo de almacenamiento además de ser mucho más barato que el almacenamiento tradicional en disco nos permite poder interactuar desde múltiples contenedores con el mismo objeto, por ejemplo, con el mismo PDF. Esto nos lleva al siguiente concepto fundamental.

4️⃣ Replicas

En Kubernetes rara vez se corre únicamente un contenedor de nuestra aplicación. Sino que se corren múltiples contenedores, es decir, la misma aplicación pero corriendo varias veces en diferentes contenedores en paralelo al mismo tiempo.

🚨 Esto es MUY importante tenerlo en cuenta porque en desarrollo pocas veces pensamos en esto y es otro condicionante de cara a tomar decisiones de desarrollo. Al igual que comentamos en el punto anterior, que varios contenedores de mi aplicación podían interactuar con el mismo PDF. Este concepto lo debemos trasladar a otras partes como:

  • Caché: existen frameworks que, por defecto, guardan la caché de la aplicación en ficheros en local. Esto es muy importante que lo externalicemos a algún sistema externo al cual podamos consultar desde las diferentes replicas que tenemos de nuestra aplicación. Además según el principio de Stateless que hemos visto, si lo guardamos en local, se perderá tarde o temprano esta cache.
  • Sesiones: esto también solía guardarse en local. Este además es un problema grave ya que debido a que tenemos varias réplicas de nuestra aplicación puede darse el caso de que la sesión de un usuario pueda existir en un contenedor pero no en otro por tanto va a provocar problemas en el uso de nuestra aplicación.

Estos dos son los más habituales pero esto al final dependerá de qué haga tu aplicación, lo importante es que tengas en cuenta que van a existir varias replicas de tu aplicación, por tanto si algo debe compartirse debe estar de forma externa al contenedor.

Por cierto, para ambos casos, caché y sesiones, un sistema habitual donde externalizar esto es Redis. Aunque desde su cambio de licencia muchos proyectos están optando por utilizar Valkey.

Minions celebrating
Minions celebrating

Si has llegado hasta aquí, felicidades, ya controlas los conceptos básicos y estas listo para desarrollar aplicaciones que se desplegarán en Kubernetes. Solo con esto posiblemente puedas construir aplicaciones muchos más optimas para Kubernetes que la mayoría de devs.

Conceptos intermedios

Antes de entrar en esta parte debemos hacer un pequeño repaso de teoría con algunos conceptos de Kubernetes:

  • Pods: los pods es la unidad más pequeña que puede desplegarse en Kubernetes y consta de 1 o N contenedores. Aunque lo habitual y más común es correr 1 Pod con 1 contenedor.
  • Workloads: es la forma en que gestiona y corre aplicaciones Kubernetes. Existen varios tipos, el más habitual y el que utilizan la mayoría de aplicaciones es el deployment.
  • Deployment: de forma resumida, es un workload que nos permite gestionar y desplegar nuestra aplicación y gestionar su configuración (puerto donde escucha el contendor, número de replicas, imagen que utiliza la versión de nuestra aplicación, etc)
  • Probes: Kubernetes tiene 3 formas de evaluar el estado de una aplicación:
    • Readiness Probes: esta probe nos sirve para detectar cuándo nuestra aplicación ya está lista para recibir tráfico. En caso de que esta probe falle, nuestra app no recibirá trafico. Únicamente se ejecuta cuando arranca el contenedor.
    • Liveness Probe: para revisar de forma periódica el estado de la aplicación, Kubernetes utiliza esta probe la cual ataca a un endpoint (http) o a un puerto (tcp) según corresponda y si la respuesta es la esperada considera a la aplicación como correcta. Si responde algo que no espera, reinicia la aplicación de forma automática.
    • Startup Probes: existen aplicaciones que tardan mucho en arrancar, para estos casos Kubernetes tiene esta probe que permite esperar más tiempo hasta que la aplicación este lista antes de evaluar su estado con las otras dos probes anteriores que hemos visto.

🏃 Bien, ahora si continuemos con los conceptos.

5️⃣ Health checks

Es muy importante que a nivel de desarrollo proporcionemos una forma de detectar que nuestra app esta funcionando y está lista para recibir tráfico, a esto se le conoce como health checks. Estos se configurarán en la aplicación para que Kubernetes con sus diferentes probes pueda evaluar su estado automáticamente siempre.

Esto podemos hacerlo de dos formas dependiendo del funcionamiento de nuestra app:

  • Si es algo HTTP, podemos configurar un endpoint (Ejm: /health) al cuál las probes estarán preguntando cada cierto tiempo el estado de la app.
  • Si es algo TCP, podemos levantar un puerto y que este no se levante hasta que la App no haya arrancado correctamente por completo y esté funcional.

6️⃣ Variables de entorno

Sobre todo si vienes de entornos de servidores tradicionales donde entrabas a un server a cambiar a mano algo esto suele ser una de las cosas "más chocantes".

En Kubernetes los Pods consumen variables de entorno de ConfigMaps o Secrets, no entraré en detalle en estos componentes pero a nivel de desarrollo lo importante es que entiendas que las variables son "embebidas" desde estos componentes en el arranque de la aplicación a los contendores (Pods).

Esto último es muy importante tenerlo en cuenta sobre todo dependiendo de cómo consume sus variables tu aplicación. Pueden darse al menos dos casos:

  • Hay aplicaciones que requieren que las variables de entorno existan en el proceso de build. Hablando de contenedores esto quiere decir que estas deben existir cuando se construye la imagen de tu aplicación. Esto es muy típico en Apps de JS (React, Next, etc) o Golang por ejemplo. Para actualizar en este caso las variables de entorno de una app se requerirá una nueva imagen y no requerirán de Secrets ni ConfigMaps.
  • Por otra parte, hay aplicaciones que leen las variables de entorno en tiempo de runtime, es decir, cuando la aplicación está arrancando. Esto en desarrollo es importante añadir una validación que controle que las variables de entorno existen y en caso de no existir controlar y expulsar un error. Esto evitará algunos errores que luego son un "poco difíciles" de detectar. Para actualizar las variables de entorno se requiere un restart (rollout -veremos este concepto en el siguiente punto-) del Workload para que arranquen de nuevo los contenedores y lean las nuevas variables de entorno.
⚠️ Lo importante de aquí es identificar si necesitas las variables de entorno en el proceso de build o en runtime. Esto cambiará la forma de desplegar sobre Kubernetes después.

Recomendaciones:

  • Si bien podéis utilizar secrets o configmaps de Kubernetes directamente para gestionar esto. No es recomendable ya que su gestión no es demasiado amigable. Lo mejor es utilizar alguna herramienta que os provea de una UI y que se integre con Kubernetes para hacer la gestión de los secretos más cómoda y segura.
  • Existen muchas herramientas para hacer lo del punto anterior pero una herramienta que nos ha funcionado muy bien a nosotros independientemente de cómo sea la aplicación y en el Cloud en el que esté, es Vault. Tenemos un post hablando más en profundidad cómo lo usamos nosotros con los diferentes equipos de desarrollo con los que trabajamos.
  • A ser posible, utiliza siempre variables de entorno del propio sistema en lugar de consumir de ficheros (Los típicos .env), esto facilita cualquier implementación posterior en Kubernetes y tu equipo de infra lo agradecerá.

7️⃣ Rollouts y Rollbacks

Por defecto, Kubernetes cuando despliega una nueva versión de la aplicación hace rollout, es decir, sustituye progresivamente los contenedores de la versión antigua por la versión nueva. Si todo va bien terminará con todos los contenedores con versión antigua y quedarán vivos solo los de la nueva versión.

Kubernetes Rolling Update
Kubernetes Rolling Update

Si algo falla, dejará con vida algunos contenedores de la versión antigua para no perder servicio y los contenedores de la versión nueva quedarán en fallo o se estarán reiniciando constantemente.

🚨 Para que esto suceda, es muy importante haber configurado Health checks correctamente.

Como ves, esto es un gran beneficio de Kubernetes, el cuál si configuramos y desarrollamos todo adaptado correctamente, por defecto, nos protege de perdidas de servicio a pesar de que hayamos podido meter la pata en una nueva release. Otra ventaja es la facilidad de hacer Rollbacks, esto básicamente es la capacidad de volver a una versión anterior de la aplicación.

Esto nos permite dar marcha atrás enseguida en caso de que detectemos que la nueva versión desplegada tiene algún tipo de anomalía o error y debemos volver a una versión estable de forma inmediata. Esto se puede hacer con cualquier cliente de Kubernetes como kubectl aunque nuestra recomendación para desarrolladores es ArgoCD, el cual veremos en la siguiente parte de este post, junto con los conceptos avanzados de Kubernetes para desarrolladores.

¿Conocías ya todos estos conceptos? Déjanos un comentario si alguna vez te tocó pelearte con algo de esto. Si te ha parecido interesante te invitamos a compartirlo en redes sociales y a suscribirte a nuestro blog para ver más contenido relacionado con infraestructura. ¡Nos vemos en la parte 2 del post!