sábado, 30 de junio de 2018

Conceptos avanzados de imágenes Docker - Parte II

Hola, en la entrada anterior vimos como complicarnos un poco a la hora de construir imágenes. Hoy toca complicarnos un poco más y estudiar algunas de las opciones disponibles para la construcción de imágenes.

Hasta ahora hemos construido imágenes que nos permiten crear contenedores que ejecutarán una aplicación dererminada. La pregunta es ¿como le hemos indicado a docker qué debe ejecutar?¿cómo se ejecuta? y más importante todavía ¿que opciones tenemos?

Evidentemente la primera pregunta es la más sencilla. Si recordamos el dockerfile para nuestra imagen de un servidor ldap, teníamos lo siguiente:

Dockerfile de imagen ldap.
Como ya sabemos, mediante la instrucción ENTRYPOINT le estamos indicando a docker que programa o comando de nuestra imagen ejecutar al crear un contenedor basado en ella.

Ahora nos queda responder a las otras dos preguntas, las cuales están muy relacionadas entre si así que, como diría Groucho, respondamos primero a la segunda pregunta.

Ya sabemos que al crear una imagen es necesario que indiquemos en el dockerfile que la describe que se ejecutará dentro del contenedor. Para esto hay disponibles dos instrucciones que podemos usar en nuestros dockerfiles, estas instrucciones son CMD y ENTRYPOINT y en ambos casos el resultado al usar una u otra es muy similar, salvo por los siguientes puntos que debemos tener en cuenta:
  • El objetivo al usar CMD es el de especificar parámetros por defecto para nuestros contenedores. En este caso, siempre es necesario especificar un ENTRYPOINT junto con la instrucción CMD, la cual no contendrá ningún ejecutable.
  • Al usar ENTRYPOINT definiremos siempre un ejecutable que se lanzará al arrancar un contenedor. Esta definición siempre debe contener la ruta al ejecutable que queremos lanzar al crear nuestros contenedores. 
Es decir, aunque con ambas instrucciones podemos especificar el programa a ejecutar, lo ideal es siempre usar ENTRYPOINT para especificar el ejecutable y CMD para establecer los parámetros por defecto del mismo. Al especificarlo de esta manera, permitimos que cualquier parámetro que pasemos al comando docker run sustituya a los definidos en la instrucción CMD. Teniendo todo esto en cuenta, nuestro dockerfile para la imagen ldap quedaría del siguiente modo:

Nuevo dockerfile con parámetros por defecto.

Con esta nueva definición contruimos de nuevo nuestra imagen y ahora, al crear un contenedor, se ejecutará el servidor ldap indicado en ENTRYPOINT con los parámetros por defecto especificados en CMD. Si al crearlo pasamos otros parámetros al comando docker run, estos nuevos parámetros sustituirán a los que hemos especificado en el dockerfile. Veamoslo con un pequeño ejemplo:

Contenedor creado a partir de la imagen ldap.
Como podemos ver. nuestro conetendor está ejecutando el servidor ldap con los parámetros por defecto indicados en la instrucción CMD de nuestro nuevo dockerfile.

Si ahora creo otro contenedor y especifico otros parámetros el resultado será el siguiente:

Cambiando los parámetros en tiempo de creación del contenedor.
Es importante tener en cuenta que también podemos usar CMD para especificar el ejecutable, es decir, podemos usar ENTRYPOINT o CMD para especificar que ejecutar al crear un contenedor, incluso los dos a la vez si queremos y, como siempre, todo dependerá de nuestras necesidades y entorno.

Desde mi punto de vista, creo que lo mejor es usar la combinación de ENTRYPOINT, para el ejecutable y CMD, para los parámetros por defecto, cuando pueda ser necesario cambiar estos últimos al crear un contenedor. Cuando lo que buscamos es asegurarnos de que no se pueden alterar los parámetros por defecto, lo mejor será que nuestro dockerfile solo contenga una entrada ENTRYPOINT que especifique tanto el ejecutable como los parámetros necesarios. En este último caso cualquier parámetro que se añada en el momento de crear el contenedor se añadirá a los parámetros por defecto sin sustituirlos, con lo que nos aseguramos que el servicio arrancará con los parámetros deseados.

Otro punto importante que debemos tener en cuenta al utilizar ENTRYPOINT es el formato que utilizamos. Según la documentación, tenemos dos opciones al especificar la instrucción ENTRYPOINT:
  • Formato shell, que es más o menos: ENTRYPOINT comando param1 param2 ...
  • Formato exec, que es más o menos: ENTRYPOINY ["ejecutable", "param1", "param2", ... ]
Bueno y ¿cual es la diferencia y por qué es importante? La diferencia y por tanto su importancia, viene derivada de como se ejecuta el servicio en un caso y en el otro.

Hasta ahora siempre hemos usado el segundo formato en nuestros dockerfile, el denominado formato exec, en el cual especificamos el ejecutable y sus parámetros como un array JSON. Cuando hemos creado contenedores e inspeccionado los procesos corriendo en los mismos, hemos visto que nuestro ejecutable era siempre el PID número 1. Si usamos el formato shell, ¿que es lo que sucede? Como siempre veamoslo con un ejemplo muy sencillo:

Contenedor creado usando el formato shell de ENTRYPOINT.
Como podemos ver en la salida anterior, nuestro comando no es el PID 1 dentro del contenedor y esto es importante si queremos que nuestros servicios se paren ordenadamente. Cuando paramos un contenedor con el comando docker stop, en estos caso, el contenedor no parará de forma limpia ya que solo la shell usada para arrancar nuestro comando recibirá la señal, pero nuestro servicio no con lo que, en algunos casos, puede que tras un timeout docker deba enviar una señal SIGKILL al proceso, lo que puede provocar que el servicio no se pare adecuadamente. Teniendo esto en cuenta lo mejor será siempre usar la forma exec de ENTRYPOINT, como recomienda la documentación de Docker.

Hasta aquí un poco más de información sobre Docker y como trabajar con imágenes. En la siguiente entrada veremos como personalizar en el momento de crear un contenedor la configuración de nuestro servicio ldap.