Introducción
Hoy en día, y cada vez más, es común que las empresas migren algunas partes de su infraestructura (o incluso la compañía entera) al cloud. Hay dos principales enfoques: Permanecer lo mas cerca posible de la antigua arquitectura usando VMs, o apostar por flexibilidad/escalabilidad/disponibilidad y buscar una nueva perspectiva mediante el uso de un orquestador de contenedores como Kubernetes. Centrándose en esta última opción (Kubernetes), lo que anteriormente se ejecutaba directamente sobre un Sistema Operativo (y que por tanto tenía acceso a la mayoría de aplicaciones que provee dicho Sistema) ahora se ejecuta dentro dentro de contenedores. Además, estos contenedores comúnmente no se ejecutan en un destino (máquina/nodo) específico, sino en un clúster. Todo funciona bien y todo es fantástico, hasta que uno se da cuenta de que el entorno ya no es tan amigable y conocido como cabría esperar. Herramientas de Sistema Operativo, como las tareas programadas o Crones, no encajan en las buenas prácticas de los contenedores, ya que son trasversales al Sistema (lo que es lo opuesto a la filosofía que se predica para los contenedores, consistente en aislar cada aplicación). Incrustar esas herramientas (o sus comportamientos) en la aplicación, al mismo tiempo que se aísla la misma, puede llegar a convertirse en una ardua tarea plagada de parches temporales y prácticas no tan buenas.
Como ecosistema completo grande, Kubernetes provee por su cuenta algunas de las funcionalidades anteriormente mencionadas, de modo que los requerimientos del día a día puedan ser atendidos y gestionados. Sin embargo, aparecen nuevas operativas y posibilidades cuando se habla de clústeres y contenedores. A lo largo de las siguientes líneas se describirá cómo Kubernetes ha solucionado la funcionalidad Cron, así como la mayoría de sus nuevas características y detalles ocultos.
1. Cómo funcionan los CronJobs en Kubernetes realmente
Cuando se crea un recurso CronJob, lo que hace realmente Kubernetes es registrar una programación en el tiempo. Cada 10 segundos el Controlador de CronJobs comprueba si hay programaciones que coincidan y de las que deba ocuparse. Cuando llega el momento señalado se crea un recurso Job, que deberá hacerse cargo de la tarea para esa ejecución específica. Finalmente cada Job crea un recurso Pod, que será el ejecutor final.
Como probablemente se haya podido apreciar ya, esta aproximación difiere significativamente del enfoque que tiene un Sistema Operativo. Lo que se observa aquí es un desacoplamiento entre el sistema de programación en el tiempo (Cron) y la gestión de las ejecuciones (Jobs). Esto permite al clúster (y por tanto al usuario) gestionar tareas efímeras sin perder el control sobre ellas.
Cabe además mencionar que los Jobs pueden crear uno o más Pods (permitiendo concurrencia/paralelismo) a la vez que se aseguran que las tareas se completan satisfactoriamente. Sin embargo, este último comportamiento podría crear complejidades adicionales ya que los contenedores ya incorporan su propia funcionalidad de gestión de fallos y reinicios. Este tema será abordado en la siguiente sección.
2. Cómo configurar los detalles avanzados
Debe tenerse en cuenta que para configurar un CronJob, cada recurso subyacente debe configurarse también. Esto significa que la configuración de un CronJob agrega sus propios parámetros además de las propiedades del Job y de las especificaciones del Pod/contenedor. Teniendo en cuenta que la mayoría de las operativas comunes se pueden entender y conseguir leyendo la documentación, el propósito de esta sección es mostrar cómo obtener ciertas funcionalidades avanzadas mediante la configuración.
Gestionar los errores
Cuando un contenedor detiene su ejecución (ya sea por un fallo o después de una ejecución satisfactoria) existen una serie de acciones que se pueden tomar justo después, las cuales son definidas (como de costumbre) por las directivas del propio recurso. Las acciones típicas son reiniciar el contenedor (siempre o sólo cuando ha sucedido algún error) o bien no hacer nada. Pero, además, los Jobs añaden una capa de complejidad adicional que se encarga de que la tarea se termine adecuadamente. Esto quiere decir que la política de reinicios se garantiza desde dos capas diferentes, que a su vez deben gestionarse desde dos partes diferentes.
Desde el lado de los contenedores, la directiva se llama restartPolicy. En el lado de los Jobs, la política se gestiona mediante la directiva backoffLimit, la cual especifica el número de fallos permitidos antes dejar de reiniciar la tarea. Teniendo ambas en cuenta, obtener la configuración para un CronJob que pueda fallar sin reinicios se aproxima a lo siguiente.
apiVersion: batch/v1beta1 kind: CronJob ... spec: ... jobTemplate: spec: backoffLimit: 0 template: spec: containers: ... restartPolicy: Never
Ejecuciones Solapadas vs. Secuenciales
Cuando se habla de un CronJob en específico, podrían coexistir varias ejecuciones. Dependiendo del tipo de operativa que caracterice a la tarea, la concurrencia podría ser una forma de proceder para acelerar el procesamiento. Existen tres vías disponibles para gestionar cómo se ejecutan los Jobs, las cuales son controladas mediante la directiva concurrencyPolicy.
- Allow: Permite ejecuciones solapadas.
- Replace: Las nuevas ejecuciones fuerzan, antes de comenzar, que se terminen las ejecuciones previas.
- Forbid: Las nuevas ejecuciones se descartan si una ejecución previa todavía está en ejecución.
Mientras que la primera permite la concurrencia, las dos últimas apuestan por ejecuciones secuenciales. Una vez más, para seguir el estilo de los crones de la vieja escuela, la opción que más se aproxima es Allow. Por otra parte, las ejecuciones concurrentes pueden desembocar en efectos no deseados si no se tratan adecuadamente, y es algo que se debe tener en cuenta y tratarse con cautela.
Tiempo mínimo de ejecución
Como ya se ha visto anteriormente, las nuevas ejecuciones de tareas pueden llegar a interferir con las anteriores, dependiendo de cómo se establezcan las directivas de concurrencia. Existe una propiedad en la especificación de los contenedores (terminationGracePeriodSeconds) que se puede emplear para sobrellevar las consecuencias de interrumpir ejecuciones. Mediante su uso se puede definir un tiempo mínimo de ejecución, de modo que aunque una nueva tarea fuerce la terminación de una previa, se garantiza una terminación grácil.
3. Operar con los CronJobs como un Maestro
Una vez la configuración pone de manifiesto lo que se pretende que haga el CronJob, los comandos básicos pueden ser utilizados para recuperar su estado (información acerca de la programación, el estado de ejecución, logs, …). Al igual que en la sección anterior, las siguientes operativas cubren cómo obtener algunas funcionalidades menos comunes.
Habilitar/Deshabilitar los CronJobs
La especificación de los CronJobs contiene una propiedad llamada suspend que permite deshabilitarlos. Ya sea de manera temporal o permanente, los CronJobs se pueden definir sin que ello implique que vayan a ser ejecutados (a pesar de lo que indiquen sus programaciones en el tiempo).
# Deshabilitar un CronJob NOMBRE_CRONJOB=mi-cronjob-1 kubectl patch cronjobs $NOMBRE_CRONJOB -p '{"spec" : {"suspend" : true }}' # Deshabilitar TODOS los CronJobs kubectl get cronjobs | grep False | cut -d' ' -f 1 | xargs kubectl patch cronjobs -p '{"spec" : {"suspend" : true }}'
Echa un vistazo a la siguiente sección para descubrir más detalles sobre los efectos colaterales que esto podría causar.
Ejecutar CronJobs manualmente
Es ampliamente reconocido que las pruebas (comunmente llamados tests, del Inglés) son útiles para detectar efectos no deseados. Los CronJobs se pueden ejecutar manualmente incluso cuando están desactivados, por lo que mantenerlos en ese estado y poder ejecutarlos bajo circunstancias que sirvan para probarlos puede ayudar a validar que todo está correctamente configurado.
NOMBRE_CRONJOB=mi-cronjob-1 kubectl create job --from=cronjobs/$NOMBRE_CRONJOB $NOMBRE_CRONJOB-ejec-manual-01
4. Sacando a la luz los casos poco frecuentes
Después de todo, no existiría la variedad si todo funcionase igual. Cada situación tiene sus propias particularidades y características especiales, por lo que cuando se trata de los CronJobs la cosa no es diferente. A lo largo de las siguientes líneas se presentarán y tratarán algunos casos poco frecuentes, de modo que las soluciones aportadas puedan ser reutilizadas (o, al menos, tenidas en consideración).
Tiempo máximo de ejecución
En secciones anteriores se trató el tema del tiempo de ejecución, de modo que se garantizase un tiempo mínimo antes de forzar la terminación. Pero, ¡eh! ¿Qué sucede con las ejecuciones que se alarguen en el tiempo más allá de lo previsto? La motivación (desde un punto de vista agnóstico) de los contenedores (y también la de Kubernetes) es la de ejecutar las tareas hasta el infinito y más allá. Los contenedores pueden fluir o pueden crashear (fallar), pero no se deben terminar como una práctica habitual. Siguiendo este principio, no existe ninguna forma de gestionar la expiración del tiempo previsto desde la especificación de los CronJobs/Jobs/Contenedores. ¿Entonces, es imposible implantar un tiempo máximo de ejecución? — ¡No, no lo es! En este punto es donde el abanico de herramientas de Linux aparece al rescate. Existe un comando llamado timeout que puede ser usado para ejecutar otro comando durante una cantidad específica de tiempo.
Aunque la utilidad anterior puede limitar el tiempo, el código de salida de la ejecución cuando lo hace, no es un código exitoso; por lo que el contenedor entrará en un estado de fallo (que puede escalar en un reinicio si así está especificado). Por otra parte, el estado del comando que se quiere ejecutar se puede preservar, pero entonces no se puede saber si fue terminado o no. Entonces, ¿cómo tratar ambas situaciones? En el siguiente extracto de código se puede hallar una propuesta que consigue terminar siempre de manera satisfactorio, a la vez que se devuelve retroalimentación acerca de lo ocurrido en realidad.
containers: - name: "mi-container-limitado-a-10s" ... command: ["bash", "-c"] args: - /usr/bin/timeout 10 bash -c 'bash -c "com arg1" && echo OK || echo KO-COM' || echo KO-TMP
CronJobs que no se ejecutan después de haber sido desactivados y activados de nuevo
Se produce un efecto colateral cuando se desactiva un CronJob, que consiste en que después de haberse perdido 100 programaciones en el tiempo, el CronJob no se volverá a ejecutar. Esta información ya se señala en la documentación, pero se menciona de pasada y se trata como algo poco relevante. La solución en este caso es volver a crear el recurso.
CronJobs que se ejecutan fuera de su programación después de haber sido reactivados
Se puede encontrar otro efecto no esperado cuando se trata con CronJobs, que consiste en que al reactivar uno de ellos (y a pesar del hecho de que el momento en el que se haga no coincida con el que tiene programado) se ejecuta inmediatamente. Esto sucede porque la ejecución no es simplemente un evento aislado, sino una ventana de tiempo con una fecha límite. Esto implica que cada ejecución que se pierde (por la política de concurrencia, o por estar el CronJob deshabilitado) incrementa un contador (hasta cierta cantidad, lo que probablemente está relacionado con el caso poco frecuente anterior). Entonces, llegado el momento, cuando el CronJob se vuelve a activar y se permite de nuevo su ejecución, el controlador tiene en cuenta que existen programaciones pendientes. Si la ventana de tiempo no se ha cerrado aún, el CronJob comienza a ejecutarse.
Este comportamiento puede gestionarse estableciendo la directiva startingDeadlineSeconds en un valor pequeño, de modo que la ventana de ejecución no coincida con el momento de la reactivación.
CronJobs que no se ejecutan
Cabe tener en cuenta que la anterior directiva startingDeadlineSeconds se puede establecer en cualquier valor, pero no todos ellos van a causar el efecto deseado. Como ya se ha mencionado anteriormente el controlador de CronJobs se ejecuta cada 10 segundos, por lo que todo valor por debajo de diez segundos hará que los CronJobs nunca se ejecuten. Se ha enviado una incidencia al proyecto de website de Kubernetes, avisándoles de este caso. Seguramente en siguientes versiones se podrá encontrar que ya se ha documentado, aunque no sea así por el momento.
Así que no se debe olvidar establecer startingDeadlineSeconds en un valor mayor que 10.
Conclusión
Como habrás podido comprobar, la diferencia entre los crones de Sistema Operativo y los de Kubernetes es mayor de la esperable desde un primer momento. Existen diferentes escenarios en un clúster y numerosas situaciones gestionar, y por eso se tratan. En algunos casos estaremos buscando un comportamiento similar al que podemos obtener en un Sistema Operativo, y en otros casos no, pero probablemente todos ellos pueden ser abordados. Por otra parte, que algo se incline mucho al multi-propósito también puede derivar (como los CronJobs) en una experiencia de configuración con una mayor dificultad.
Afortunadamente siempre puedes contar con el equipo de Geko (un equipo de ingenieros altamente cualificados), quienes profundizarán en el tema hasta conseguir hacerlo sencillo para ti. ¡No olvides volver por el blog de Geko para comprobar qué hay nuevo por aquí! El equipo de Geko siempre estará más que contento de verte por aquí, y por supuesto ¡Ponte en contacto con nosotros para más información!
Lectura adicional
https://kubernetes.io/docs/tasks/job/automated-tasks-with-cron-jobs/
https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/
https://kubernetes.io/docs/concepts/workloads/controllers/job/
https://www.magalix.com/blog/kubernetes-patterns-the-cron-job-pattern
https://medium.com/@bambash/kubernetes-docker-and-cron-8e92e3b5640f
https://medium.com/cloud-native-the-gathering/how-to-write-and-use-kubernetes-cronjobs-3fbb891f88b8
https://medium.com/@hengfeng/what-does-kubernetes-cronjobs-startingdeadlineseconds-exactly-mean-cc2117f9795f