Dockerfile y las capas AUFS o Overlay2

Cuando hacemos una build de una nueva imagen de Docker nos basamos en un Dockerfile, fichero que define exactamente los pasos a seguir para conseguir nuestra nueva imagen.

Una imagen de Docker está basada en diferentes “capas“. Digamos que con Docker siempre nos basamos en imagenes ya hechas, y descargadas de DockerHub. Éstas pueden ser imágenes de sistemas operativos como Debian o Ubuntu.

En este artículo explicaré como funciona el sistema de capas, que al principio es un poco confuso.

Imágenes pre-hechas

Hay empresesas o vendors, como por ejemplo Apache, Python, PHP, etc… que usan las imágenes base de un sistema operativo, y encima instalan una versión de su producto. Esta imagen es tagueada, y normalmente se sube a DockerHub para que todo el mundo la pueda descargar.

Veamos las diferentes imágenes que tiene Python (por poner un ejemplo). Normalmente cada tag corresponde a una versión de Python.

¿Y qué tiene que ver ésto con el sistema de capas, si se puede saber?

el lector del post, justo ahora mismo

Veamos un ejemplo. Los chicos de Python se basan en una imagen de Ubuntu 18.04, que para nuestro ejemplo tiene 3 capas.

Ellos instalan Python sobre Ubuntu, creando 2 capas mas. El resultado es el siguiente:

Ejemplo de capas de imagen de Python:3, sobre un Ubuntu 18.04

Cuando te descargues python3 haciendo docker pull python:3 te descargas todas las capas, las de Ubuntu y las de Python.

Reaprovechamiento de Capas

Ahora bien, si ahora instalas MySQL y éste se basa también en Ubuntu 18.04 como imagen base, no hará falta descargar Ubuntu de nuevo, ¡Reusas las “capas” que te habías descargado para Python!

Ejemplo de reaprovechamiento de capas

Esto a mi me parece bastante increíble.

La primera vez que lo ví, flipé. Su mayor ventaja es que se trata cada capa como un elemento individual. Obviamente están enlazadas, puesto que la capa #2 se construye siempre sobre la capa #1, y las #3 sobre la #2. Pero esto nos da ciertas ventajas.

Primero el espacio en disco. En éste ejemplo (y me lo invento) quizás las 3 capas de Ubuntu pesan 600MB, mientras que las de PHP y MySQL pesan 100MB cada una. Si no reaprovechamos las capas, necesitaríamos tener dos veces Ubuntu descargado, y esto ocuparía mucho espacio en disco. No tiene mucho sentido.

Después, imagina que en MySQL encuentran una vulnerabilidad grave y tienen que cambiar el valor por defecto en un fichero de configuración. El cambio ocupa escasos KB de datos. Pues es muy fácil para ellos crear una nueva capa sobre la #2 con solo este fix, y nada mas. Así cuando te quieras descargar la nueva versión, solo descargas lo que te haga falta. Quedaría algo así:

Las actualizaciones se pueden hacer añadiendo una capa nueva

¿Qué utilidad práctica tiene todo ésto?

Para empezar ya hemos visto que tenemos reducción de espacio en disco, por la reutilización de capas.

OK, ¿y qué?

Si, realmente todo esto ya te viene de serie. Pero tú también creas tus propios Dockerfiles, y aquí si que le podemos sacar provecho a éstos conocimientos mas bien teóricos.

Dockerfile de ejemplo

Veamos un ejemplo tonto, para ver si le podemos sacar provecho o no al tema de las capas:

FROM python:3

RUN apt  update
RUN apt install vim -y
RUN apt install nano -y
RUN mkdir /app

COPY ./app.py /app/app.py

CMD python /app/app.py
  1. Nos basamos en la imagen base de python:3.
  2. Instalamos un par de dependencias de sistema:
    • Instalar VIM y NANO son dos ejemplos tontos que nunca instalarías realmente en un container. Aquí normalmente instalaríamos dependencias de sistema que nuestro programa necesite. Para no liar el tema, usamos un nombre de paquete que todos conocemos.
  3. Creamos un directorio /app
  4. Copiamos un programa llamado app.py dentro del container
  5. Definimos el comando que se ejecutará si lanzamos el container. En este caso, ejecutar la aplicación

Aplicación de ejemplo

Veréis que estoy copiando un fichero llamado app.py . Es un programa que no hace nada; es solo de ejemplo para que el Dockerfile se vea completo. Podría ser cualquier programa con cualquier lenguaje, y lo extenso que haga falta.

Creando la build

Ejecutamos el comando para hacer la build, y etiquetar nuestra primera imagen:

docker build -t example ./
Sending build context to Docker daemon  15.87kB
Step 1/8 : FROM python:3
 ---> 29d2f3226daf
Step 2/8 : RUN apt  update
 ---> Running in 6a922ae2dd88

// Long output. Removed for clarity.

Removing intermediate container 6a922ae2dd88
 ---> 13050909d024
Step 3/8 : RUN apt install vim -y
 ---> Running in 57e14bdb4066

// Long output. Removed for clarity.

Removing intermediate container 57e14bdb4066
 ---> c59a54778aa0
Step 4/8 : RUN apt install nano -y
 ---> Running in 4690372a350c

// Long output. Removed for clarity.

Removing intermediate container 4690372a350c
 ---> 21bdd3d3b5ed
Step 5/8 : RUN mkdir /app
 ---> Running in 6a9d9e5da1ff
Removing intermediate container 6a9d9e5da1ff
 ---> d5916b3ffc4d
Step 6/8 : COPY . /app
 ---> a905576648e9
Step 7/8 : WORKDIR "/app"
 ---> Running in d5251181734a
Removing intermediate container d5251181734a
 ---> 539c2ab1c4c8
Step 8/8 : CMD python /app/app.py
 ---> Running in 3f825d44e9c7
Removing intermediate container 3f825d44e9c7
 ---> c4aaa6b6b227
Successfully built c4aaa6b6b227
Successfully tagged example:latest

Como podemos ver, por cada línea del Dockerfile, se crea una capa nueva. En los últimos pasos se puede ver claramente como añade el tag a la última capa creada para ponerle un nombre al último cambio:

Successfully built c4aaa6b6b227
Successfully tagged example:latest

La capa c4aaa6b6b227 queda enlazada con la 539c2ab1c4c8, ésta con la a905576648e9, ésta con la d5916b3ffc4d y así succesivamente, hasta llegar a la capa de Python 29d2f3226daf que yo ya tenía descargada en mi sistema. Si no la tuviese descargada, Docker iría a Dockerhub para descargar ésta capa, que a su vez estaría enlazada con otras llegando a descargar Ubuntu 16.04 peeeero como la capa de Python ya la tenía descargada, la puedo reaprovechar para construïr encima de ella.

Recreando la build

Si ahora lanzo la build exactamente igual como antes, ¿qué ocurre? Pues veámoslo en un ejemplo:

docker build -t example ./
Sending build context to Docker daemon  15.87kB
Step 1/8 : FROM python:3
 ---> 29d2f3226daf
Step 2/8 : RUN apt  update
 ---> Using cache
 ---> 13050909d024
Step 3/8 : RUN apt install vim -y
 ---> Using cache
 ---> c59a54778aa0
Step 4/8 : RUN apt install nano -y
 ---> Using cache
 ---> 21bdd3d3b5ed
Step 5/8 : RUN mkdir /app
 ---> Using cache
 ---> d5916b3ffc4d
Step 6/8 : COPY . /app
 ---> Using cache
 ---> a905576648e9
Step 7/8 : WORKDIR "/app"
 ---> Using cache
 ---> 539c2ab1c4c8
Step 8/8 : CMD python /app/app.py
 ---> Using cache
 ---> c4aaa6b6b227
Successfully built c4aaa6b6b227
Successfully tagged example:latest

¡Docker usa la cache!

La ejecución tarda 1 segundo, y realmente no ha hecho…. nada. El ‘hash’ que identifica cada capa es el mismo que en la ejecución anterior y podéis ver que no ha hecho nada por las indiciaciones de:

 ---> Using cache

¿Cómo se explica ésto? ¿Cómo sabe Docker que no tiene que hacer nada?

Como no hemos cambiado ninguna línea del Dockerfile original, cuando Docker intenta ejecutar “RUN apt  update” primero de todo mira si éste mismo paso ya lo había ejecutado en alguna capa. ¿Cómo hace esto? Pues cada capa tiene sus metadatos, y entre ellos está el comando ejecutado:

Inspeccionando la capa para ver el comando que se ejecutó para crearla

Reptie el proceso por cada línea del Dockerfile, y Docker acabará detectando que todo lo tiene en cache.

¿Y si modificamos el Dockerfile original?

Si cambiamos una línea, la del apt install y lo hacemos todo en una sola línea:

FROM python:3

RUN apt  update
RUN apt install vim nano -y # Hemos unido dos líneas en una
RUN mkdir /app
COPY . /app

WORKDIR "/app"
CMD python /app/app.py

Cuando ejecutamos el comando de build, el output es el siguiente:

Sending build context to Docker daemon  3.072kB
Step 1/7 : FROM python:3
 ---> 29d2f3226daf
Step 2/7 : RUN apt  update
 ---> Using cache
 ---> 13050909d024
Step 3/7 : RUN apt install vim nano -y
 ---> Running in b7c7034ecea3

// Long output. Removed for clarity. It is installing both vim and nano.

Removing intermediate container b7c7034ecea3
 ---> cd2af8558be6
Step 4/7 : RUN mkdir /app
 ---> Running in 2f2f087db4ea
Removing intermediate container 2f2f087db4ea
 ---> 9092e9fcbabc
Step 5/7 : COPY . /app
 ---> 8bc2eabfd64b
Step 6/7 : WORKDIR "/app"
 ---> Running in f08320904559
Removing intermediate container f08320904559
 ---> 342e69ca12e3
Step 7/7 : CMD python /app/app.py
 ---> Running in 4a7f7a3b9c0f
Removing intermediate container 4a7f7a3b9c0f
 ---> 467aae794f4a
Successfully built 467aae794f4a
Successfully tagged example:latest

El paso 1 y 2 vienen de cache, mientras que el paso 3 ha cambiado, entonces Docker reconstruye dicha capa, ya que no puede usar la que tenían en cache y necesita calcularla de nuevo.

Si os fijáis, las capas 4 a 7 también son recreadas. Recordemos que una capa siempre hace referencia a la capa anterior.

Si la capa anterior ha cambiado, tiene lógica que tengamos que recrear toooodas las capas subsiguientes.

Conclusión

Bueno, en este artículo quería explicar la teoría detrás de los Dockerfiles y las capas AUFS y Overlay2. Espero que con esta explicación ahora te sientas mas cómodo al crear tus Dockerfiles sabiendo como funcionan.

Pequeños truquitos que ahora que sabemos la teoría parecen lógicos:

  • Reaprovecha Imágenes Base, así ahorras tamaño y tiempo de descarga
  • Pon al inicio del Dockerfile siempre las cosas que menos vayan a cambiar
  • La invalidación de una capa invalida todas las siguientes; los procesos que mas tarden al inicio
  • Si tienes una línea RUN que instala 22 cosas diferentes, si quieres instalar una dependencia más hazlo en una línea diferente del Dockerfile para no tener que esperar. Haz las pruebas que consideres para validar que la instalación es correcta, y si todo ha ido bien, ya si puedes modificar la línea larga y recrear la imagen. Esto te ahorra tiempos de desarrollo.
  • Cuando usas COPY Docker sabrá si el fichero se ha modificado. En caso de que haya cambiado, recreará la capa. Si no se ha modificado, usará la cache como ya sabemos.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *