Liquibase en Kubernetes

Compartir esta publicación

Compartir en facebook
Compartir en linkedin
Compartir en twitter
Compartir en email

Contexto, motivación y teoría

Tenemos un entorno de microservicios con Kubernetes(k8s), en el cual se desarrolla un servicio que requiere de una base de datos. La gestión de migraciones de la base de datos, decidimos realizarla mediante Liquibase.

Eventualmente, nos dimos cuenta que algunos deploys, dejaban la bd bloqueada. Tras una investigación, dimos con la clave. El propio k8s, si un proceso de despliegue tarda más de cierto tiempo, si se excede el failureThreshold combinado con el initialDelaySeconds en el startupProbe, en nuestro ejemplo, mientras se aplican migraciones, asume que el Pod ha quedado en un estado corrupto y/o inconsistente y mata el proceso. Liquibase tiene un sistema de migraciones en el cual tiene un fichero de configuración llamado changelog, en el cual se indican los ficheros de migraciones y en el orden en el que se han aplicado o se tienen que aplicar. Para evitar conflictos durante el proceso de migración, el primer pod bloquea el campo LOCKED de la tabla DATABASECHANGELOGLOCK en la db. Este sistema se utiliza para asegurar que solo un proceso de Liquibase se está ejecutando a la vez. Una vez acabado, libera ese campo de la db, y el siguiente lo bloquea, y así sucesivamente. Si k8s mata este proceso en el momento de despliegue ya sea porque está tardando más de lo configurado en su propio deployment, o porque la CPU del nodo está muy saturada en ese momento y tarda más de lo esperado, la db se queda con ese campo LOCKED a 1, y ningún otro Pod puede tomar el proceso de migraciones.

Investigando un poco, encontramos que en la propia documentación oficial de Liquibase habla de la posibilidad de utilizar Init Containers. En el enlace a la documentación puedes ver más en detalle qué son exactamente los Init Containers, pero en resumen, son unos contenedores que se levantan dentro de un Pod con dos principales particularidades:

  • Estos contenedores se ejecutan siempre hasta su finalización. Es decir, Kubernetes no los matará en mitad del proceso.
  • Cada contenedor de inicialización debe completarse correctamente antes de que comience el siguiente. En nuestro caso, esto evitará que los procesos de migración intenten ejecutarse en paralelo de forma concurrente.

Ahora que ya tenemos claro el contexto y la posible solución de nuestro problema, vamos a ver el paso a paso de cómo implementamos y llevamos a cabo esta solución hasta producción.

Caso práctico

Tal y como comenta la propia documentación, Liquibase tiene sus propias imágenes para utilizar en kubernetes, así que a priori, parece que utilizando esa imagen, especificando qué fichero se quiere ejecutar, debería ser suficiente.

Este caso práctico acabaría aquí para la mayoría de personas, pero nuestro caso es un tanto peculiar. La comunicación con nuestra db es mediante secrets, por lo tanto, necesitamos poder instalar una serie de dependencias que no tiene la imagen base de Liquibase, tales como aws command line interface(awscli). Y aquí, es donde empiezan los problemas. La imagen de liquibase te da unos permisos muy limitados, de manera que no se nos permitía poder instalar awscli.

El siguiente paso fue tomar la decisión de utilizar una imagen base de Ubuntu 20.04 e instalar todo lo que nos fuera necesario. Bien, con esto ya podríamos realmente conseguir nuestro propósito, pero nos dimos cuenta de que Liquibase necesita de Java para poder ejecutarse, cosa que implica tener que gestionar más dependencias de las esperadas. Así que, decidimos utilizar una imagen JRE, concretamente jdk:11. Así ya teníamos java con la versión necesaria, y pudimos instalar tanto liquibase como awscli sin la mayor dificultad.

sK6GVFKl4kqIr91A oNwypURDoyMTHCaqx8FUPZTmhaLbujwOYyW5 ggx5t 34kEF73LpvkLjsfIWdWYhyZU Io1MmnrjlqhM5UeoRBRtKM4ZW4160tGN03q6VR8SuOePJ6oL2OmuD7Kl9dt14ze A

Una vez creado nuestro dockerfile independiente para el init container, necesitamos crear el fichero, bash en nuestro caso, que se ejecutará de principio a fin en nuestro Init Container. En nuestro ejemplo, para poder ejecutar el comando de liquibase que hace update de la bd, necesitamos extraer toda la información necesaria mediante el secret arn.

sRAhO743VEogFb5MAS

Aquí hay un apunte a realizar muy importante. Si actualmente ya tenemos nuestro servicio en producción y funcionando, y lo que vamos a hacer es simplemente cambiar el sistema de migraciones para usarse mediante este método, hay que tener en cuenta lo siguiente: el valor del parámetro changeLogFile debe ser exactamente el mismo que aparece en el campo FILENAME de la tabla DATABASECHANGELOG.

De cualquier otra manera, identificaría el changelog como otro distinto y procedería a aplicarlo desde el principio, con todos los problemas que eso conlleva (ya sea desde un simple error de crear una tabla ya existente, hasta una sobreescritura de nuestros datos en producción).

Ahora, debemos configurar los ficheros yaml de k8s para el uso de nuestro nuevo dockerfile como Init Container. Partiendo de la base de que tenemos nuestro servicio configurado con el nombre “app”. El nombre que hemos decidido poner para nuestro init container es “migrations”, y la configuración, vendría a ser la que se puede observar a continuación:

kTa2PqUIQQi62tcxVzUSixhO2EUQDdk6x4WdVqUR5G3g3pJM3W c 2g29FAxQamsdf7Nbhzpz p6C8bUy4e987BlzhRHRjqVrNEfC65GIjuGd7YHGRIOl 1caxa765HoY8NX3fNLlLnJ5YTf9o 4EQ

Con esto, ya deberíamos poder desplegar con Init Containers. Para nuestro caso, nos hizo falta deshabilitar las migraciones al arrancar la app en Spring, para evitar que los containers de tipo “app” intenten lanzar las migraciones de nuevo, tras el init Container. En este ejemplo, es tan sencillo como indicarle en el application.yml que liquibase no está activado:

DfrjPYQcu2jXPPJjf2lf5XzmJGZ 3T inM0166Eg7e6O6i6w 1XmaXjkiCzla4mRdDYgjQIi ycYavBeUMOUmV sNvocjksKjca5MQMPX pnY708u3OajBQze7vV7pjEyUfeg5zHFELXv i98cd70g

Y con esto, deberíamos tener funcionando nuestro init container para lanzar las migraciones antes de levantar los containers con nuestra aplicación. Como apunte, me gustaría comentar que para ejecutar los tests e2e, podemos configurar nuestro application.yaml para que en el profile de test si que ejecute las migraciones de liquibase al arrancar la app para la base de datos que utilicemos en este entorno, H2 en nuestro caso.

Conclusiones

He dejado un tiempo desde que redacté este artículo antes de escribir las conclusiones, para verificar nuestra hipótesis. Tras un tiempo con esto funcionando, hemos visto que a veces también puede fallar el init container, por razones diversas, ajenas al proceso de despliegue en sí. Pero sí que es cierto que esta solución ha minimizado los problemas de bloqueo de la base de datos en el momento de aplicar las migraciones de manera muy significativa. Por lo tanto, pese a no ser un método infalible, mejora muchísimo respecto al estado previo de nuestro microservicio en Kubernetes. Así que sí tu microservicio gestiona las migraciones con Liquibase, recomiendo utilizar Init Containers para realizar las mismas. Seguimos trabajando para encontrar la solución a esos casos excepcionales en los que se produce un bloqueo de la base de datos al ejecutar las migraciones.

Lo bueno de tener abstraída la gestión de migraciones en un fichero bash que se ejecuta en el init container, es que tenemos control sobre Liquibase. Hemos encontrado unos comandos que nos permiten listar (list-locks) y desbloquear (release-locks) los bloqueos. Así que de momento, hemos creado una regla que lee los locks y si hace más de 15 minutos del último lock activo, asumimos que se ha quedado corrupto el último init container y procedemos a liberar el bloqueo mediante el comando release-locks.

De momento, este es nuestro approach, y nos está funcionando mejor que la situación inicial. Espero que os sirva de ayuda este artículo, y no dudéis en dejar feedback y/o soluciones alternativas a la misma problemática en los comentarios de este post.

Deja una respuesta

Tu dirección de correo electrónico no será publicada.

Suscríbete a nuestro boletín de noticias

Recibe actualizaciones de los últimos descubrimientos tecnológicos

Acerca de Apiumhub

Apiumhub reúne a una comunidad de desarrolladores y arquitectos de software para ayudarte a transformar tu idea en un producto potente y escalable. Nuestro Tech Hub se especializa en Arquitectura de Software, Desarrollo Web & Desarrollo de Aplicaciones Móviles. Aquí compartimos con usted consejos de la industria & mejores prácticas, basadas en nuestra experiencia.

Posts populares
Obtén nuestro Libro: Software Architecture Metrics

Global Software Architecture Summit '22

Reserva tu plaza!

Reserva

¿Tienes un proyecto desafiante?

Podemos trabajar juntos

Secured By miniOrange