sábado, 26 de mayo de 2018

Docker - Construcción de imágenes básicas

Hola de nuevo, hasta ahora hemos explorado muchas funcionalidades de Docker y en todos los casos, hemos trabajado con un elemento común, la imagen del software con el que queremos construir nuestro contenedor.

En un post anterior ya vimos de manera muy simple que es una imagen, pero ya llega el momento de preguntarnos ¿cómo puedo construir las mías?

Como ya vimos, el repositorio de imágenes oficial de Docker contiene muchísimas imágenes que podemos usar libremente para montar nuestros servicios, con lo que puede que en muchos casos no necesitemos construir nuestras imágenes, pero algunos motivos para querer construirlas son:
  • Por seguridad, si queremos saber que es lo que hay exactamente dentro de la imagen que estamos usando.
  • Uso de una versión de software distinta a la de las imágenes disponibles.
  • Nuestro servicio utiliza aplicaciones que son un desarrollo interno.
  • Para activar ciertas funcionalidades o módulos de la aplicación no disponibles en las imágenes publicadas.
Estos son algunos de los motivos que se me ocurren, pero estoy seguro que hay muchos más que en cada caso, harán necesario que construyamos nuestras propias imágenes.

Antes de empezar, vamos a enumerar las recomendaciones fundamentales a tener en cuenta:
  • Idealmente un contenedor debería ejecutar solamente un proceso, por tanto debemos hacer imágenes que solo contengan todo lo necesario para ejecutar dicho proceso.
  • No debemos añadir software adicional sin necesidad. Por ejemplo, si nuestro contenedor no va a conectarse con otros sistemas por ssh, no es necesario incluir dicho comando ni las librerias necesarias.
Siguiendo las recomendaciones anteriores, conseguiremos imágenes muy ligeras que contendrán solo lo necesario para nuestro servicio.

Con todo esto en mente, vamos a construir una imagen base sobre la cual añadiremos software adicional.

Voy a seguir un método de construcción de imágenes totalmente manual, en el cual voy a comenzar creando un chroot que luego usaré para construir la imagen. Este método puede ser un poco lento, pero es automatizable mediante scripts muy simples y una vez creada la imagen, será rapidamente reproducible.

Por la relación entre un chroot y los contenedores, esta forma de construcción nos permite entender mejor como funcionan ambos y aprender un poco mejor como funciona un sistema Linux. Para empezar, vamos a crear una estructura de directorios en la que vamos copiar nuestra shell preferida, junto con las bibliotecas de las que depende para su correcto funcionamiento. Para esto y durante todo el procedimiento, usaremos el comando ldd con el cual comprobaremos las bibliotecas dinámicas de las que depende un binario y que necesitaremos copiar a nuestro chroot.

Empezamos creando una estructura de directorios y copiando unos cuantos comandos, entre ellos nuestra shell preferida (la imagen siguiente está resumida):

Creación de un chroot básico.
Con esto tenemos una shell básica y solamente los comandos cd y ls. Ahora, para probar nuestro chroot y comprobar si nos falta alguna biblioteca solo necesitamos ejecutar el comando chroot especificando la ruta donde tenemos nuestro contexto de construcción:
Prueba de nuestro contexto de construcción mediante chroot.

Tenemos un entorno muy básico en el cual solo disponemos de dos comandos, el comando cd y el comando ls. Como necesitaremos más comandos, siempre teniendo en cuenta que debemos hacer una imagen base sencilla y solo con lo que vayamos a necesitar, tendremos que copiarlos a las rutas correspondientes, así como las bibliotecas necesarias para el correcto funcionamiento de los mismos. Tras añadir todos los comandos que vayamos a necesitar, el entorno de construcción que vamos a usar queda del siguiente modo:

Entorno de construcción básico definitivo.
Una vez que tenemos nuestro entorno chroot básico y hemos comprobado que los comandos que hemos copiado funcionan, podemos construir nuestra primera imagen base, a partir de la cual construiremos imágenes más complejas.

Para poder construir una imagen, necesitamos decirle a docker que es lo que vamos a añadir a la misma y cual va a ser el primer comando que se va a ejecutar dentro de la imagen cuando creemos un contenedor. Para esto, necesitamos crear un fichero de configuración dockerfile que contiene las instrucciones necesarias para el subcomando build de docker. En el caso de nuestra imagen base, el contenido de nuestro dockerfile será tan simple como:

Dockerfile para imagen base.

Como vemos, este fichero de texto contiene tres instrucciones solamentecada una de las cuales indica a docker lo siguiente:
  • FROM scratch. La directiva FROM indica cual es la imagen padre a partir de la cual vamos a construir la nuestra. En nuestro caso, al tratarse de una imagen base, no usaremos  ninguna otra imagen como origen.
  • ADD ./base_image /. Con esta directiva indicamos que el contenido de nuestro directorio base_image debe copiarse al raíz de nuestra imagen.
  • ENTRYPOINT ["/bin/bash"]. Con esta directiva le indicamos a docker cual es el comando que debe ejecutarse al arrancar un contenedor basado en esta imagen.
Os recomiendo que consultéis la referencia oficial de Docker sobre los dockerfile aqui ya que, además de explicarlo mucho mejor que yo, os darán información sobre muchas otras directivas a utilizar para construir imágenes.

Con nuestro dockerfile ya listo, solo necesitamos ejecutar el comando docker build como se mustra a continuación:

Construcción de la imagen base.
Como podemos ver el comando es muy simple y solo necesitamos indicarle la ruta y nombre de nuestro dockerfile, el tag o nombre de nuestra imagen y la ruta a nuestro contexto de construcción. Ahora, al consultar las imágenes disponibles en el repositorio local de imágenes, podemos ver nuestra imagen lista para usarse:

Nuestra nueva imagen base lista para usarse.
Ya solo tenemos que crear un contenedor con nuestra imagen y comprobar que todo funciona correctamente:

Contenedor a partir de imagen base.
Teniendo en cuenta el tipo de imagen que hemos construido y que nuestro ENTRYPOINT apunta a una shell, es importante tener en cuenta que si no creamos un contenedor interactivo este se parará inmediatamente tras ejecutar dicha shell.

Con lo expuesto hasta aquí, ya podemos construir una imagen base, a partir de la cual generaremos imágenes más complejas incluyendo más software.

En las siguientes entradas intentaré crear una imagen con un servidor OpenLDAP a partir de esta y además, veremos opciones de personalización de imágenes, así como la importancia de la directiva ENTRYPOINT y sus alternativas.