Docker Engine: Entendiendo como almacenar de forma persistente nuestros datos

Hoy en dia Docker Engine cuenta con la particularidad en el almacenamiento de los datos de la aplicación a ejecutar hace uso de Union Mount en este caso implementa Union FileSystem o UnionFS lo que permite combinar varios sistemas de archivos y directorios (llamados branches) para crear un solo sistema de archivos lógico. Al momento de crear un contenedor Docker Engine crea una serie de directorios dentro de UnionFS donde los datos del contenedor son almacenados. ¿Pero que sucede si el contenedor creado es eliminado? junto con el, los datos de tu aplicación tambien son eliminados, por lo que esto no serviria para cuando por ejemplo, queremos ejecutar con Docker Engine una base de datos utilizando MongoDB. A fin de evitar que se eliminen los datos es necesario crear Volúmenes en Docker Engine que permitirá volver los datos persistentes. 

Haciendo un poco de historia y por si no lo sabían, en las versiones anteriores a la 1.9 la forma de volver los datos persistentes era la de crear un contenedor de datos,  ese contenedor solamente sus directorios montados eran enlazados a los demás contenedores que lo necesiten, aun esta funcionalidad sigue activa en Docker Engine.

En las versiones recientes de Docker Engine, por defecto los volúmenes son creados cuando son declarados en el Dockerfile de una imagen, no se les declara un nombre, Docker Engine se encarga se setear el nombre a partir de un hash, pero esto no aplica para cuando queremos montar directorios del sistema de ficheros del Host en algun contenedor.

A partir de aca y para una mejor explicación, vamos a clasificar las distintas formas de montar directorios y crear volumenes en Docker: 

  1. Volúmenes de Datos.
  2. Volúmenes de Datos desde Contenedores.
  3. Directorios Montados desde Host.

 

Volúmenes de Datos

Los volúmenes de datos en Docker Engine son subdirectorios en el sistema de ficheros del Host que permiten ser montados directamente en un contenedor, pero también permiten ser compartidos y reusados entre múltiples contenedores, una muy importante funcionalidad es la posibilidad de hacer respaldos del directorio en el Host. Para los volúmenes de datos los directorios creados se encuentran en la ruta /var/lib/docker/volumes del Host. Es Importante definir Storage Driver en Docker.

 

STORAGE DRIVER:

Un Storage Driver es la particularidad de como Docker Engine implementa un Union FileSystem. Por ejemplo, para distribuciones como Ubuntu, por defecto, el Storage Driver para los volumenes de datos es Another Union File System o AUFS, pero para las distribuciones basadas en Red Hat como lo son CentOS o Fedora implementan Device Mapper . Docker Engine soporta muchos Union File System y puedes encontrar mas información en el siguiente enlace: Docker: Select a storage driver. 

En nuestro caso y como las pruebas que haremos estará orientada a llevar contenedores a producción utilizaremos CentOS 7 con el Storage Driver de Device Mapper.

Debo aclarar algo muy importante, para el tipo de Union FileSystem: Device Mapper existen los modos Devicemapper Loopback (también conocido como loop-lvm) y Devicemapper (también conocido como direct-lvm). Por defecto, para un instalación de Docker Engine en CentOS 7 viene configurado el modo loop-lvm o Devicemapper Loopback.

DEVICEMAPPER LOOP-LVM:

El Devicemapper hace uso del modulo thin provisioning para implementar snapshot en el modo Copy-on-Write, para los que no conocen el funcionamiento de thin provisioning, el mismo lo que hace es que crea un sistema de archivo lógico mínimo y luego usará el espacio mínimo, esto quiere decir que poco a poco irá creciendo hasta el máximo reservado. Docker Engine usa este modo para guardar las imágenes e iniciar contenedores. En el Devicemapper se crea un thin pool que esta basado en dos bloques de dispositivos (loopback devices), una para los datos y otro para los metadatos. La ruta donde se encuentra los dispositivos /loop0 y loop1 es la siguiente:

/var/lib/docker/devicemapper/devicemapper

Para verificar lo anterior, tendremos que ejecutar lo siguiente:

# ls -lahs /var/lib/docker/devicemapper/devicemapper
total 1005M
1002M -rw-------. 1 root root 100G Sep 09 15:35 data
 2.5M -rw-------. 1 root root 2.0G Sep 09 15:29 metadata

# lsblk
NAME                         MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
vda                          253:0    0   35G  0 disk 
└─vda1                       253:1    0   35G  0 part /
loop0                          7:0    0  100G  0 loop 
└─docker-253:1-41961541-pool 252:0    0  100G  0 dm   
loop1                          7:1    0    2G  0 loop 
└─docker-253:1-41961541-pool 252:0    0  100G  0 dm 

Tambien podemos verificar lo anterior antes comentando, ejecutando docker info:

Storage Driver: devicemapper
 Pool Name: docker-253:1-41961541-pool
 Pool Blocksize: 65.54 kB
 Base Device Size: 10.74 GB
 Backing Filesystem: xfs
 Data file: /dev/loop0
 Metadata file: /dev/loop1
 Data Space Used: 1.061 GB
 Data Space Total: 107.4 GB
 Data Space Available: 35.12 GB
 Metadata Space Used: 1.974 MB
 Metadata Space Total: 2.147 GB
 Metadata Space Available: 2.146 GB
 Thin Pool Minimum Free Space: 10.74 GB
 Udev Sync Supported: true
 Deferred Removal Enabled: false
 Deferred Deletion Enabled: false
 Deferred Deleted Device Count: 0
 Data loop file: /var/lib/docker/devicemapper/devicemapper/data
 Metadata loop file: /var/lib/docker/devicemapper/devicemapper/metadata


La anterior ejecución nos indica que para el dispositivo <strong>data </strong>se encuentran usado 1.061 GB de 107.4 GB máximo reservado, pero como el Disco Duro de la instancia donde se está efectuado las pruebas es de 35 GB, el espacio disponible es el mismo mostrado en el output que es de 35.12 GB. Por defecto viene seteado para 100G por lo que si queremos aumentar la capacidad, o para en este caso, disminuir la capacidad maxima reservada, realizaremos los siguientes cambios, en el archivo /usr/lib/systemd/system/docker.service con lo siguiente:

ExecStart=/usr/bin/dockerd --storage-driver=devicemapper --storage-opt dm.loopdatasize=20GB --storage-opt dm.loopmetadatasize=5GB

Luego reiniciamos los servicios ejecutando: systemctl daemon-reload ; systemctl restart docker y volvemos a verificar con los anteriores ejecuciones de comandos. Como nota adicional, los anteriores cambios para disminuir el tamaño total reservado se han realizado en una instalación inicial, por lo que no la recomiendo para instalaciones ya en ejecuacion. Es muy importante mencionar que Docker recomienda que no se use para contenedores en producción esta versión de Devicemapper loop-lvm , solamente se recomienda para hacer pruebas de laboratorio.

DEVICEMAPPER DIRECT-LVM:

Este procedimiento creara un volumen lógico configurado como un thin pool por lo que este modo es igual al anterior pero sin los loops devices. Es generado y creado a partir de particiones y deberemos contar con un nuevo disco en la Host o bien una partición con espacio disponible. Para este modo es necesario instalar los paquetes de LVM2. Lo inicial sera crear un volumen físico con ayuda de los comandos proporcionados por LVM2, en mi caso cuento con otro disco nuevo montado en el Host e identificado como /dev/vdc , ejecutamos lo siguiente:

# pvcreate /dev/vdc
Physical volume "/dev/vdc" successfully created

Luego de lo anterior creamos el grupo para el volumen:

# vgcreate docker-lvm /dev/vdc 
Volume group "docker-lvm" successfully created

Creamos los volúmenes lógicos para data y metadata :

# lvcreate --wipesignatures y -n data docker-lvm -l 95%VG
  Logical volume "data" created.
# lvcreate --wipesignatures y -n metadata docker-lvm -l 5%VG
  Logical volume "metadata" created.

Lo próximo es iniciar el servicio de Docker Engine con los anteriores valores, para ello deberemos editar el archivo ubicado y de nombre /usr/lib/systemd/system/docker.service con lo siguiente:

ExecStart=/usr/bin/dockerd --storage-driver=devicemapper --storage-opt dm.datadev=/dev/docker-lvm/data --storage-opt dm.metadatadev=/dev/docker-lvm/metadata --storage-opt dm.fs=xfs

Realizado el cambio anterior ejecutamos:

# systemctl daemon-reload ; systemctl restart docker

Verificamos que los cambios han sido tomados ejecutando docker info :

Storage Driver: devicemapper
 Pool Name: docker-253:1-92284478-pool
 Pool Blocksize: 65.54 kB
 Base Device Size: 10.74 GB
 Backing Filesystem: xfs
 Data file: /dev/docker-lvm/data
 Metadata file: /dev/docker-lvm/metadata
 Data Space Used: 11.8 MB
 Data Space Total: 51 GB
 Data Space Available: 50.99 GB
 Metadata Space Used: 397.3 kB
 Metadata Space Total: 2.68 GB
 Metadata Space Available: 2.68 GB
 Thin Pool Minimum Free Space: 5.1 GB
 Udev Sync Supported: true
 Deferred Removal Enabled: false
 Deferred Deletion Enabled: false
 Deferred Deleted Device Count: 0
 Library Version: 1.02.107-RHEL7 (2016-06-09)

y luego con el comando lsblk :

​# lsblk
NAME                           MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
vda                            253:0    0   35G  0 disk 
└─vda1                         253:1    0   35G  0 part /
vdc                            253:32   0   50G  0 disk 
├─docker--lvm-data             252:0    0 47.5G  0 lvm  
│ └─docker-253:1-92284478-pool 252:2    0 47.5G  0 dm   
└─docker--lvm-metadata         252:1    0  2.5G  0 lvm  
  └─docker-253:1-92284478-pool 252:2    0 47.5G  0 dm

Es importante destacar que este procedimiento se recomienda para ser usado en ambientes de produccion.

 

CREAR VOLUMENES DE DATOS:

Explicado y comprendido lo anterior sobre Storage Driver en Docker, llego el momento de crear nuestro primeros volúmenes. Docker Engine cuenta con un parámetro que nos permite gestionar volúmenes, por lo que si ejecutamos: docker volume --help , veremos la ayuda de este parámetro. Los comandos para el parámetro volume en Docker son:

  • create : permite crear un volumen.
  • inspect : muestra la información detalla en uno o mas volúmenes creados.
  • ls : permite listar los volúmenes que han sido creados.
  • rm : elimina un volumen.

Sin tanto explicar, entraremos de lleno en como crear un volumen, en la línea de comandos ejecutamos :

$ docker volume create --name vol-001

La opción --name permite darle un nombre al volumen creado. Si queremos inspeccionar que el volumen ha sido creado exitosamente, ejecutamos:

$ docker volume inspect vol-001
[
    {
        "Name": "vol-001",
        "Driver": "local",
        "Mountpoint": "/var/lib/docker/volumes/vol-001/_data",
        "Labels": {},
        "Scope": "local"
    }
]

Una vez que tenemos creado un volumen y le hemos asignado un nombre, crearemos un contenedor que usara el volumen recien creado, para ello al crear el contenedor usaremos el parámetro -v y el formato es el siguiente: docker create --volume=volume:/container_directory

Para la opción volume es el nombre del volumen creado y para la opción /container_directory es el directorio dentro del contenedor donde estara ubicado los datos de ese volumen creado. Ahora para crear el contenedor con el volumen creado ejecutamos:

$ docker create -it --volume=vol-001:/data-container --name container001 -h container001 zokeber/centos

Ahora, verificamos que se ha montado correctamente el volumen, entramos al contenedor y listamos los directorios:

[[email protected] /]# ls -la
total 16
drwxr-xr-x   1 root root  192 Sep 09 15:17 .
drwxr-xr-x   1 root root  192 Sep 09 15:17 ..
-rwxr-xr-x   1 root root    0 Sep 09 15:17 .dockerenv
lrwxrwxrwx   1 root root    7 Apr 15  2015 bin -> usr/bin
drwxr-xr-x   1 root root    0 Sep 09 15:05 data-container
drwxr-xr-x   5 root root  380 Sep 09 15:17 dev

Crearemos en ese directorio algunos archivos:

[[email protected] container-data]# ls
1.txt  2.txt file.txt

La gestión de volúmenes permite que los datos se vuelva persistente y que pueda ser usada por otros contenedores, si eliminamos el anterior contenedor y creamos otro usando ese mismo volumen, encontraremos  los archivos creados, en este caso el volumen sera montado en otro directorio dentro del contenedor:

$ docker create -it --volume=vol-001:/opt --name container002 -h container002 zokeber/centos

Recuerden que el volumen se encuentra montado dentro del contenedor en el directorio /opt , por lo que si verificamos dentro de el:

[[email protected] opt]# pwd
/opt
[[email protected] opt]# ls
1.txt  2.txt file.txt

Encontraremos los archivos y directorios creados anteriormente, si en nuestro Host, verificamos el directorio 
/var/lib/docker/volumes/vol-001/_data nos encontraremos con los mismo archivos creados:

[[email protected] ~]# ls /var/lib/docker/volumes/vol-001/_data
1.txt  2.txt file.txt

También podemos eliminar el volumen creado, pero deberemos verificar que no esta asociado a ningún contenedor:

[[email protected] ~]# docker volume rm vol-001
Error response from daemon: Unable to remove volume, volume still in use: remove vol-001: volume is in use - [968ba4ff9e2bc834d9ee10b9aa4d2d08e94eb54e7f4dce32694634809afa0574]
[[email protected] ~]# docker rm -f container002
container002
[[email protected] ~]# docker volume rm vol-001
vol-001
[[email protected] ~]# docker volume ls
DRIVER              VOLUME NAME

Lo anterior nos indica que al intentar eliminar el volumen Docker Engine te avis que esta en uso por el contenedor, por lo que he eliminado primero el contenedor y luego el volumen. Adicional el parámetro --volume puede ser usado abreviado -v de la siguiente forma:

$ docker create -it -v vol-001:/data-container --name container001 -h container001 zokeber/centos

 

Volumenes de Datos desde Contenedores

Para este caso, la aplicación es bastante sencilla, deberemos crear un volumen de datos que estaría montado en un contenedor, y posterior sera utilizado o compartido por un segundo contenedor, para ello, he creado un volumen al que he llamado shared-vol docker volume create --name shared-vol

Luego creare un contenedor que contendrá ese volumen montado en la ruta /shared-volume :

docker create -it --volume=shared-vol:/shared-volume --name container001 -h container001 zokeber/centos

Verificamos que el volumen ha sido montado en nuestro contenedor:

[[email protected] ~]# docker exec -it container001 bash
[[email protected] ~]# cd /
[[email protected] /]# ls -l  
...
dr-xr-x---   1 root root   90 Apr 15  2015 root
drwxr-xr-x   1 root root  100 Apr 15  2015 run
lrwxrwxrwx   1 root root    8 Apr 15  2015 sbin -> usr/sbin
drwxr-xr-x   1 root root    0 Sep 09 15:32 shared-volume
...

Ahora utilizaremos un nuevo parámetro llamado --volumes-from que nos permitirá que el directorio montado en el primer contenedor pueda ser montado en otro contenedor, para ello crearemos un contenedor llamado container002:

# docker create -it --volumes-from=container001 --name container002 -h container002 zokeber/centos

Una vez creado el nuevo contenedor verificamos que ha sido montado el volumen del contenedor llamado container001:

[[email protected] ~]# docker exec -it container002 bash
[[email protected] ~]# cd /
[[email protected] /]# ls -la
...
lrwxrwxrwx   1 root root    8 Apr 15  2015 sbin -> usr/sbin
drwxr-xr-x   1 root root    0 Sep 09 15:32 shared-volume
drwxr-xr-x   1 root root    0 Jun 10  2014 srv
...

Podemos incluso verificar con el comando inspect de Docker Engine, para el primer contenedor creado al que le adjuntamos el volumen shared-vol la información es la siguiente:

"AutoRemove": false,
"VolumeDriver": "",
"VolumesFrom": null,

"Mounts": [
    {
        "Name": "vol-shared",
        "Source": "/var/lib/docker/volumes/vol-shared/_data",
        "Destination": "/shared-volume",
        "Driver": "local",
        "Mode": "z",
        "RW": true,
        "Propagation": "rprivate"
    }
],
 

Para el segundo contenedor creado y al que le montamos el volumen del primer contenedor creado, la información es la siguiente:

"AutoRemove": false,
"VolumeDriver": "",
"VolumesFrom": [
    "container001"
],

"Mounts": [
    {
        "Name": "vol-shared",
        "Source": "/var/lib/docker/volumes/vol-shared/_data",
        "Destination": "/shared-volume",
        "Driver": "local",
        "Mode": "",
        "RW": true,
        "Propagation": "rprivate"
    }
],

 

Directorios Montados desde Host

En este caso utilizaremos un directorio que se encuentra en el Host, es decir el servidor donde se encuentran corriendo los contenedores Docker. Como ejemplo utilizaremos el directorio /mnt/backup que es un segundo disco montado en este directorio. La ejecución es muy simple, tan solo deberemos crear un contenedor nuevo con los siguientes parámetros:

# docker create -it --volume=/mnt/backups:/opt/backups --name container003 -h container003 zokeber/centos

Lo anterior creara un nuevo contenedor que internamente tiene montado el directorio /mnt/backups del Host pero montado como /opt/backups , para verificar que lo anterior es cierto y funciona correctamente, crearemos en el directorio /mnt/backups del Host, una serie de archivos que posteriormente entrarremos al contenedor  y verificaremos que se encuentran en el directorio /opt/backups :

[[email protected] backups]# touch 1.txt 2.txt 3.txt
[[email protected] backups]# ll
-rw-r--r-- 1 root root 0 Sep 09 16:14 1.txt
-rw-r--r-- 1 root root 0 Sep 09 16:14 2.txt
-rw-r--r-- 1 root root 0 Sep 09 16:14 3.txt
[[email protected] backups]# docker exec -it container003 bash
[[email protected] ~]# cd /opt/
[[email protected] opt]# ll
drwxr-xr-x 2 root root 42 Nov 12 16:14 backups
[[email protected] opt]# cd backups/
[[email protected] backups]# ll
-rw-r--r-- 1 root root 0 Sep 09 16:14 1.txt
-rw-r--r-- 1 root root 0 Sep 09 16:14 2.txt
-rw-r--r-- 1 root root 0 Sep 09 16:14 3.txt

Como podemos observar los archivos creados son mostrados en el directorio /opt/backups del contenedor. Por otro lado, si en el contenedor ejecutamos el comando df -h observaremos que se encuentra montado como si estuviesemos en el Host:

[[email protected]]# df -h
Filesystem               Size  Used Avail Use% Mounted on
/dev/vda1                 80G  439M   78G   1% /
/dev/vdb1                 59G  4.0G   55G   7% /mnt/backups

Por ultimo si queremos montar un directorio del Host como solo lectura en cualquier contenedor tendremos que colocarle la opción de readonly, y se efectua de la siguiente forma:

# docker create -it --volume=/mnt/backups:/opt/backups:ro --name container003 -h container003 zokeber/centos

Lo anterior permite crear un contenedor que contiene el directorio /mnt/backups del Host montado en la ruta /opt/backups en modo de solo escritura, pero del lado del Host se puede escribir mas no del lado del contenedor.

Y hasta aca, este largo pero sencillo post. Si tienen alguna duda, pueden dejarla en la caja de comentarios.