lunes, 6 de junio de 2016

Contruyendo imágenes con un fichero Dockerfile

Actualmente no se recomienda el enfoque del comando “docker commit”. En su lugar, se recomienda construir imágenes utilizando un archivo de definición llamado Dockerfile y el comando “docker build”. El fichero Dockerfile utiliza una síntaxis básica de instrucciones para la construcción de imágenes Docker. El enfoque Dockerfile proporciona un mecanismo más repetible y transparente para crear imágenes.
Una vez tenemos un fichero Dockerfile creado, utilizaremos el comando “docker build” para crear una nueva imagen a partir de las instrucciones del fichero Dockerfile, que permiten al sistema saber qué queremos que se instale (imagen base que se utiliza, ficheros que queremos copiar a la imagen, etc..)

Nuestro primer Dockerfile

Vamos a crear un directorio y un fichero Dockerfile inicial. Vamos a construir una imagen de Docker que contiene un servidor web simple.

$ mkdir web
$ cd web
$ touch Dockerfile

Hemos creado un directorio llamado web para mantener nuestro fichero Dockerfile. Ese directorio será nuestro entorno de compilación, que es lo que Docker llama un contexto o construir un contexto. Docker cargará y construirá el contexto, así como los archivos y directorios contenidos en el mismo y a nuestro demonio Docker cuando se ejecute la construcción. Esto lo proporciona el demonio Docker con acceso directo a cualquier código, archivos u otros datos que deseemos incluir en la imagen.
Nosotros hemos creado un fichero Dockerfile vacío para comentar. Ahora vamos a ver un ejemplo de un Dockerfile para crear una imagen Docker, la cual actuará como un servidor web.

# Version: 0.0.1
FROM Ubuntu:14.04
MAINTAINER Javier Hernandez “jalapuente@example.com
RUN apt-get update && apt-get –y install nginx
RUN echo ‘Hola, yo estoy en un contenedor’ > /usr/share/nginx/html/index.html
EXPOSE 80

El fichero Dockerfile contiene una serie de instrucciones emparejadas con argumento. Cada instrucción, por ejemplo FROM, debe estar en mayúsculas y estar seguido por un argumento: FROM Ubuntu:14.04. Las instrucciones en el fichero Dockerfile se procesan de arriba hacia abajo, por lo que se deben ordenar en consecuencia.

Cada instrucción añade una nueva capa a la imagen y luego graba la imagen.

Docker ejecuta las instrucciones siguiendo el siguiente flujo de trabajo:
  1. Docker ejecuta un contenedor desde la imagen.
  2.  Una instrucción se ejecuta y se hacen los cambios en el contenedor.
  3. Docker ejecuta el equivalente a “docker commit” y graba una nueva capa.
  4. Docker entonces ejecuta un nuevo contenedor desde esta nueva imagen.
  5. La próxima instrucción en el fichero es ejecutada,  y procesada repetidamente hasta que todas las instrucciones han sido ejecutadas.

Esto significa que si un Dockerfile se detiene por alguna razón (por ejemplo si una instrucción falla), nos quedaremos con una imagen que podemos utilizar. Esto nos puede resultar muy útil para tareas de depuración. Se puede ejecutar un contenedor desde esta imagen interactiva y luego depurar, porque la instrucción falló usando la última imagen creada.

Dockerfile también es compatible con comentarios. Cualquier línea que comienza con un símbolo # es considerado un comentario.

La primera instrucción de un Dockerfile debe ser FROM. La instrucción FROM especifica una imagen existente en la que operarán las instrucciones posteriores, esta imagen se llama la imagen base.

En nuestro Dockerfile hemos especificado la imagen de ubuntu:14.04 como nuestra imagen base. Esta especificación construirá una imagen sobre la base de un sistema operativo Ubuntu 14.04. Al igual que con la ejecución de un contenedor, que siempre hay que especificar exactamente cual es la imagen base sobre la que estamos construyendo.

A continuación, hemos especificado la instrucción MAINTAINER, que le describe a Docker quién es el autor de la imagen y cuál es su dirección de correo electrónico. Esto es útil para especificar un propietario y para facilitar el contacto con el creador de una imagen.

Hemos seguido estas instrucciones con dos instrucciones RUN. La instrucción RUN ejecuta comandos en la imagen actual. Los comandos en nuestro ejemplo son: la actualización de los repositorios APT instalados e instalar el paquete nginx y luego crear el archivo /usr/share/nginx/html/index.html que contiene un texto de ejemplo. Como hemos visto anteriormente, cada una de estas instrucciones creará una nueva capa y, si tiene éxito, almacenará dicha capa y luego ejecutará la siguiente instrucción.

De forma predeterminada, la instrucción RUN se ejecuta dentro un Shell usando el comando “/bin/sh -c”. Si está ejecutando la instrucción en una plataforma sin un shell o bien deseamos que no lo tenga, puede especificar la instrucción en formato de ejecución:

RUN [“apt-get”,” install “,”-y”,”nginx”]

Utilizamos este formato para especificar una matriz que contiene el comando a ejecutar y luego cada parámetro que deseemos pasar al comando.

A continuación, hemos especificado la instrucción EXPOSE, que le indica a Docker que la aplicación alojada en este contenedor utilizará este puerto específico dentro del contenedor. Eso no significa que podamos acceder automáticamente a cualquier servicio que se está ejecutando en ese puerto (en este caso, el puerto 80) en el contenedor. Por razones de seguridad, Docker no abre los puertos automáticamente, pero espera que nosotros lo hagamos cuando se ejecute el contenedor utilizando el comando “docker run”.
Podemos especificar varias instrucciones EXPOSE para indicar que varios puertos van a ser expuestos.

Construyendo imágenes desde nuestro fichero Dockerfile

Todas las instrucciones se ejecutarán y se grabará la transacción y una imagen nueva será creada cuando ejecutamos el comando “docker build”. Vamos a construir nuestra imagen ahora.

$ cd web
$ docker build -t="jalapuente/web" .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM ubuntu:14.04
14.04: Pulling from library/ubuntu
Digest: sha256:28bd2edcebe82d41c3494bf6205016fe08e681452f1448acd44d55e2cda7e3c0
Status: Downloaded newer image for ubuntu:14.04
 ---> e9ae3c220b23
Step 2 : MAINTAINER Javier Hernandez “jalapuente@example.com”
 ---> Running in 84f09e04f141
 ---> a711e3c2443f
Removing intermediate container 84f09e04f141
Step 3 : RUN apt-get update && apt-get -y install nginx
 ---> Running in 20f581c46181
Ign http://archive.ubuntu.com trusty InRelease
Get:1 http://archive.ubuntu.com trusty-updates InRelease [64.4 kB]
Get:1 http://archive.ubuntu.com trusty-updates InRelease [64.4 kB]
….
Removing intermediate container 20f581c46181
Step 4 : RUN echo ‘Hola, yo estoy en un contenedor’ > /usr/share/nginx/html/index.html
 ---> Running in d947cc9a30e1
 ---> 9a1e7fb9d967
Removing intermediate container d947cc9a30e1
Step 5 : EXPOSE 80
 ---> Running in a6416af141d9
 ---> 0e7818e84f46
Removing intermediate container a6416af141d9
Successfully built 0e7818e84f46

Hemos utilizado el comando “docker build” para construir nuestra nueva imagen. Hemos especificado la opción -t para etiquetar la imagen resultante con un repositorio y un nombre, aquí el repositorio jalapuente y el nombre de la imagen web. Siempre es recomendable dar un nombre a nuestras imágenes, aunque no sea obligatorio para hacer que sea más fácil seguirles la pista y gestionarlas.

También podemos etiquetar las imágenes durante el proceso de construcción añadiendo un sufijo a la etiqueta, después del nombre de la imagen con dos puntos, por ejemplo:

$ docker build -t="jalapuente/web:v1" .

Si no especificamos ninguna etiqueta, Docker le asignará automáticamente la etiqueta “latest”.

El punto le dice a Docker que mire en el directorio local para buscar el fichero Dockerfile. También podemos especificar un repositorio Git como fuente de origen para el fichero Dockerfile como podemos ver en esta instrucción:

$ docker build -t="jalapuente/web:v1" git@github:jalapuente/docker-web

En este caso Docker asume que el fichero Dockerfile está localizado en la raíz del repositorio Git.

También podríamos especificar la ruta donde está el fichero utilizando el parámetro –f. Por ejemplo:

$ docker build -t="jalapuente/web:v1" –f /opt/docker/proyecto1

El nombre de fichero Dockerfile no es necesario especificarlo.
Revisando el proceso de construcción de docker, podemos ver que el contexto construido se ha enviado al demonio Docker.

Sending build context to Docker daemon 2.048 kB

En el caso de existir un archivo llamado .dockerignore en la raíz del directorio, se interpreta como una lista separada por una nueva línea de los patrones de exclusión. Al igual que un archivo .gitignore excluye los archivos de la lista de ser tratados como parte del contexto de construcción y, por lo tanto les impide ser subidos al demonio Docker.

Que sucede si una instrucción falla

Vamos a ver que sucede si una instrucción falla. Veamos un ejemplo: supongamos que en el paso 4 escribimos el nombre del paquete requerido equivocadamente y en su lugar escribimos ngin.

Si ejecutáramos de nuevo la construcción, podremos ver que falla. Una vez que ha fallado podríamos depurar el fallo, para ello utilizaríamos el ID del último paso que nos ofrece “docker build” y crearíamos una sesión para conectarnos:

$ docker run –t –i ID_DE_IMAGEN /bin/bash
ID_DE_IMAGEN:/#

Entonces podría tratar de ejecutar el comando “apt-get install –y ngin” de nuevo con el nombre del paquete correcto o realizar cualquier otra depuración para determinar qué salió mal. Una vez que hemos identificado el problema, podemos salir del contenedor, actualizar mi Dockerfile con el nombre del paquete correcto, y volver a intentar construir.

Dockerfiles y la caché de compilación

Como resultado de cada paso que se ejecuta, se consolida como una imagen, Docker es capaz de ser inteligente sobre la construcción de imágenes. Tratará las capas anteriores como caché. Si, en nuestro ejemplo de depuración, no necesitamos cambiar nada en los pasos 1 a 3, entonces Docker usaría las imágenes previamente construidas como memoria caché y punto de partida. En realidad, se empezaría el proceso de construcción directamente desde el paso 4. Esto puede ahorrarnos mucho tiempo en la construcción de imágenes, en el caso de que un paso anterior no haya cambiado nada. Sin embargo, si ha cambiado algo en los pasos 1 a 3, entonces Docker reiniciaría desde la primera instrucción de cambio.

A veces, sin embargo, queremos asegurarnos de que no se utiliza la memoria caché. Por ejemplo, si hubiera caché en el paso 3 de nuestro ejemplo  “apt-get update”, entonces no se actualizaría la caché de paquetes APT. Es posible que deseemos que se ejecute para conseguir una nueva versión de un paquete. Para omitir el caché, podemos utilizar el parámetro “--no-cache” con el comando “docker build”.

$ docker build –no-cache –t=”jalapuente/web” .
  

El uso de la caché de construcción para plantillas

Como resultado de la caché de construcción, podemos construir nuestros Dockerfiles en forma de plantillas simples (por ejemplo, añadir un repositorio de paquetes o ejecutar paquetes de actualización en la parte superior del archivo para asegurarnos de que la caché es utilizada). Lo más habitual es tener el mismo conjunto de instrucciones en la parte superior de mi Dockerfile, por ejemplo para Ubuntu:

FROM Ubuntu:14.04
MAINTAINER Javier Hernandez “jalapuente@example.com
ENV ACTUALIZADO_FECHA 2015-12-08
RUN apt-get –qq update

Vamos a ver esta nueva Dockerfile. En primer lugar, hemos utilizado la instrucción FROM para especificar una imagen base de ubuntu:14.04. Después hemos añadido la instrucción MAINTAINER para proporcionar los datos de contacto. Entonces he especificado una nueva instrucción, ENV. La instrucción ENV establece las variables de entorno en la imagen. En este caso, he especificado la instrucción ENV para establecer una variable de entorno llamada ACTULIZADO_FECHA, mostrando cuando la plantilla ha sido actualizada. Por último, hemos especificado el comando “apt-get –qq update” en una instrucción RUN. Esto actualiza la caché de paquetes APT cuando se ejecuta, asegurando que disponemos de los últimos paquetes disponibles para instalar.

Con esta plantilla, cuando quiero actualizar la construcción, puedo cambiar la fecha de mi instrucción ENV. Docker entonces restablece la memoria caché cuando llega a esa instrucción ENV y se ejecuta cada instrucción posterior de nuevo sin depender de la memoria caché. Esto significa que la instrucción “RUN apt-get –qq update” se vuelve a ejecutar y la caché de paquetes se actualiza con los últimos contenidos. Podemos extender este ejemplo de plantilla para cualquier plataforma de destino o adaptarla a las distintas necesidades.

Visualización de nuestra nueva imagen

Ahora vamos a comprobar nuestra nueva imagen. Podemos hacer esto con el comando “docker images”.

$ docker images jalapuente/web
REPOSITORY           TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
Jalapuente/web       latest              0e7818e84f46        3 hours ago         227.5 MB

Si queremos profundizar sobre como se ha creado nuestra nueva imagen, podemos usar el comando “docker history”.

$ docker history 0e7818e84f46
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
0e7818e84f46        3 hours ago         /bin/sh -c #(nop) EXPOSE 80/tcp                 0 B                
9a1e7fb9d967        3 hours ago         /bin/sh -c echo ‘Hola, yo estoy en un conte     38 B               
56092f24c55b        3 hours ago         /bin/sh -c apt-get update && apt-get -y insta   39.54 MB           
a711e3c2443f        3 hours ago         /bin/sh -c #(nop) MAINTAINER Javier Hernandez   0 B                
e9ae3c220b23        4 weeks ago         /bin/sh -c #(nop) CMD ["/bin/bash"]             0 B                
a6785352b25c        4 weeks ago         /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/   1.895 kB           
0998bf8fb9e9        4 weeks ago         /bin/sh -c echo '#!/bin/sh' > /usr/sbin/polic   194.5 kB           
0a85502c06c9        4 weeks ago         /bin/sh -c #(nop) ADD file:531ac3e55db4293b8f   187.7 MB     

Podemos ver que cada capa de la imagen está dentro de nuestra imagen jalapuente/web y las instrucciones del Dockerfile fueron creadas en el momento de su construcción.

Lanzamiento de un contenedor de nuestra nueva imagen

Podemos ahora poner en marcha un nuevo contenedor utilizando nuestra nueva imagen y comprobar que ha funcionado lo que hemos construido.

$ docker run –d –p 80 --name web jalapuente/web nginx –g “daemon off;”

Hemos lanzado un nuevo contenedor llamado web usando el comando “docker run” y el nombre de la imagen que acabamos de crear. Hemos especificado la opción -d, que le indica a Docker que funcione como servicio (en background). Esto nos permite ejecutar procesos de larga ejecución, como el demonio Nginx. También hemos especificado un comando para ejecutar cuando el contenedor arranque: nginx -g "daemon off;". Esto abrirá Nginx en primer plano para ejecutar nuestro servidor web.

También hemos especificado una nuevo parámetro, -p. El parámetro -p gestiona los puertos de red que Docker publica en tiempo de ejecución. Cuando se ejecuta un contenedor, Docker tiene dos métodos de asignación de puertos en el host Docker:
        Docker puede asignar al azar un puerto alto del rango 32.768-61.000 en el host Docker, que mapea al puerto 80 en el contenedor.
        Podemos especificar un puerto concreto en el host Docker, que se asigna al puerto 80 en el contenedor.

Para ver que puerto ha mapeado, podemos utilizar el comando “docker ps” o bien podemos utilizar el comando “docker port”, indicando el contenedor y puerto del que queremos conocer su mapeo, y nos devolverá el puerto aleatorio que le ha sido asignado.
El parámetro “-p” también nos permite ser flexibles en cuanto a como se mapea un puerto en el host. Por ejemplo, podemos especificar que puerto del host se mapea al puerto del contenedor: ‘docker run –d –p 8080:80 web jalapuente/web nginx –g “daemon off;” ‘, esto mapearía el puerto 80 en el contenedor al puerto 8080 en el host local.
Si ejecutamos varios contenedores, sólo un contenedor puede mapearse a un puerto específico en el host local. Esto podría ser un límite de la flexibilidad de Docker.
También podríamos mapearlo sólo a un interface de red determinado, por ejemplo “-p 127.0.0.1:80:80” mapearía el puerto 80 del contenedor de tal forma que sólamente seria posible acceder desde el host local a través del puerto 80.

También podríamos mapear un puerto UDP añadiendo el sufijo /udp  al final del mapeo del puerto.

Docker también dispone del parámetro “-P”, el cual publica todos los puerto que fueron definidos para exponer con la instrucción EXPOSE del fichero Dockerfile.


No hay comentarios:

Publicar un comentario