Conceptos de Almacenamiento
Creo que el estudio y la comprensión perfecta de cómo funcionan los volúmenes en Kubernetes debe hacerse con cuidado, porque al final lo que vale es la información que producimos y no queremos perderla.
Cuando hablamos de almacenamiento en Kubernetes, primero necesitamos entender cómo funciona el almacenamiento con contenedores.
Primero vamos a entender el almacenamiento con Docker, lo que nos dará nociones básicas para que sea más fácil entenderlo en Kubernetes.
En Docker tenemos dos conceptos:
- Storage Drivers
- Volume Drivers
Cuando instalamos Docker en una máquina, crea la carpeta /var/lib/docker con varias subcarpetas donde se guardan los datos. Los datos están relacionados con imágenes, contenedores en ejecución, etc.
sudo tree -L 1 /var/lib/docker
/var/lib/docker
├── buildkit
├── containers
├── engine-id
├── image
├── network
├── overlay2
├── plugins
├── runtimes
├── swarm
├── tmp
└── volumes
Cuando construimos un Dockerfile, cada instrucción crea una nueva capa (layer) que va agregando sus diferencias. Una vez que construimos una imagen, estamos agregando más capas a partir de la imagen que viene del FROM. Cada capa agregada aumentará el tamaño de nuestra imagen.
Sin embargo, si construimos una imagen dos veces, conseguimos aprovechar las capas anteriores por ser iguales, solo generamos las capas diferentes.

Una imagen es completamente read-only y cuando ejecutamos un contenedor basado en esta imagen, se crea una nueva capa para tener permiso de lectura y escritura. Solo los archivos generados por el contenedor se almacenan realmente.

Generalmente son archivos de log, archivos temporales generados por el contenedor o los archivos modificados por el usuario. Esta capa solo existe mientras el contenedor esté ejecutándose o detenido. Cuando destruimos el contenedor, esta capa también será destruida junto con todos los datos.
Una imagen puede ser usada por varios contenedores, entonces no tiene sentido que un contenedor modifique los archivos de una imagen. Para que sea inmutable, es solo lectura.
Si queremos modificar un archivo que vino de la propia imagen, ¿cómo se trata esto?
Es posible modificar un archivo, pero Docker crea una copia de este archivo en la capa read-write y luego lo modifica. Este mecanismo se llama copy on write (COW). Cada vez que se solicita un archivo, primero buscará en la capa COW creada por el contenedor, si no lo encuentra, buscará dentro de los archivos de la imagen. Cuando es necesario abrir un archivo de la imagen para modificación, se crea una copia en su capa COW.

Sabiendo que los datos del contenedor morirán junto con él, ¿cómo persistir los datos?
En Docker podemos montar un directorio del host dentro del contenedor y grabar datos en ese directorio. El directorio se monta en una ruta dentro del contenedor.
docker run -v /ruta/del/host:/ruta/en/contenedor
Los volúmenes Docker son gestionados por el propio Docker y no están directamente vinculados a un directorio específico del host. Se almacenan en una ubicación específica del sistema de archivos de Docker como se mostró anteriormente.
Los volúmenes Docker son más portátiles e independientes del host. Pueden moverse fácilmente entre contenedores e incluso entre hosts. Además, los volúmenes Docker pueden tener configuraciones específicas, como ser solo lectura o tener copias de seguridad automatizadas.
Ya entendemos que es mejor montar un volumen Docker que mapear una carpeta del host dentro del contenedor.
Entonces creando un volumen Docker...
docker volume create data_volume
docker volume ls
DRIVER VOLUME NAME
local 489998434eb6ccc46100905700247d441f9abb565a005592a73617f8f5090cea
local b8af063f368da3ab7deaa0b5e44d645e91d2ffaaf1fa0f3e947f7c62ff6e11c4
local cacf4290059f1ba89f765469941d6712d0e78f94d53557dda530702ec2b2904d
local data_volume
local eb98d6fd1af16b7d964a0a0251e6e2a3485878f7a8a66d2cc866464da9bec471
Antes de usar el volumen, vamos a confirmar cómo funciona el COW en la práctica.
# No pasé el volumen
docker run -it --name ubuntu ubuntu bash
echo "teste" > /root/david
# ¿Dónde veo esto en la capa COW?
docker inspect ubuntu
[
{
# REMOVIDO PARA FACILITAR LA LECTURA
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/3e94271808092cd2f2d0cfacbbf16e13eba84f9dc5529cb85687bb0cc9f3c8c2-init/diff:/var/lib/docker/overlay2/fcf52ba43f52d039928bd1ba9aba777fbf6148cba54e6693a94e4a98f6ce7726/diff",
"MergedDir": "/var/lib/docker/overlay2/3e94271808092cd2f2d0cfacbbf16e13eba84f9dc5529cb85687bb0cc9f3c8c2/merged",
"UpperDir": "/var/lib/docker/overlay2/3e94271808092cd2f2d0cfacbbf16e13eba84f9dc5529cb85687bb0cc9f3c8c2/diff",
"WorkDir": "/var/lib/docker/overlay2/3e94271808092cd2f2d0cfacbbf16e13eba84f9dc5529cb85687bb0cc9f3c8c2/work"
},
"Name": "overlay2"
},
# REMOVIDO PARA FACILITAR LA LECTURA
}
]
La carpeta merged es donde ocurre el COW.
sudo ls -lha /var/lib/docker/overlay2/3e94271808092cd2f2d0cfacbbf16e13eba84f9dc5529cb85687bb0cc9f3c8c2/merged
total 80K
drwxr-xr-x 1 root root 4,0K fev 14 22:13 .
drwx--x--- 5 root root 4,0K fev 14 22:11 ..
lrwxrwxrwx 1 root root 7 jan 25 11:03 bin -> usr/bin
drwxr-xr-x 2 root root 4,0K abr 18 2022 boot
drwxr-xr-x 1 root root 4,0K fev 14 22:06 dev
-rwxr-xr-x 1 root root 0 fev 14 22:06 .dockerenv
drwxr-xr-x 1 root root 4,0K fev 14 22:15 etc
drwxr-xr-x 2 root root 4,0K abr 18 2022 home
lrwxrwxrwx 1 root root 7 jan 25 11:03 lib -> usr/lib
lrwxrwxrwx 1 root root 9 jan 25 11:03 lib32 -> usr/lib32
lrwxrwxrwx 1 root root 9 jan 25 11:03 lib64 -> usr/lib64
lrwxrwxrwx 1 root root 10 jan 25 11:03 libx32 -> usr/libx32
drwxr-xr-x 2 root root 4,0K jan 25 11:03 media
drwxr-xr-x 2 root root 4,0K jan 25 11:03 mnt
drwxr-xr-x 2 root root 4,0K jan 25 11:03 opt
drwxr-xr-x 2 root root 4,0K abr 18 2022 proc
drwx------ 1 root root 4,0K fev 14 22:15 root
drwxr-xr-x 5 root root 4,0K jan 25 11:06 run
lrwxrwxrwx 1 root root 8 jan 25 11:03 sbin -> usr/sbin
drwxr-xr-x 2 root root 4,0K jan 25 11:03 srv
drwxr-xr-x 2 root root 4,0K abr 18 2022 sys
drwxrwxrwt 1 root root 4,0K fev 14 22:15 tmp
drwxr-xr-x 1 root root 4,0K jan 25 11:03 usr
drwxr-xr-x 1 root root 4,0K jan 25 11:06 var
sudo ls -lha /var/lib/docker/overlay2/3e94271808092cd2f2d0cfacbbf16e13eba84f9dc5529cb85687bb0cc9f3c8c2/merged/root
total 28K
drwx------ 1 root root 4,0K fev 14 22:15 .
drwxr-xr-x 1 root root 4,0K fev 14 22:13 ..
-rw------- 1 root root 17 fev 14 22:08 .bash_history
-rw-r--r-- 1 root root 3,1K out 15 2021 .bashrc
-rw-r--r-- 1 root root 6 fev 14 22:15 david # aquí está
-rw-r--r-- 1 root root 161 jul 9 2019 .profile
-rw------- 1 root root 712 fev 14 22:15 .viminfo
Si detenemos ese contenedor o el contenedor termina, ¿qué tendremos en el directorio 3e94271808092cd2f2d0cfacbbf16e13eba84f9dc5529cb85687bb0cc9f3c8c2? Recordando que este directorio solo existe mientras el contenedor exista.
# Ubuntu no está en ejecución, pero no fue destruido
docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c9cd02186069 ubuntu "bash" 3 hours ago Exited (0) 2 minutes ago ubuntu
7ac5423aea7b kindest/node:v1.29.1 "/usr/local/bin/entr…" 6 days ago Up 3 days 127.0.0.1:33959->6443/tcp kind-cluster-control-plane
d719119fd49b kindest/node:v1.29.1 "/usr/local/bin/entr…" 6 days ago Up 3 days kind-cluster-worker
effc6ac77623 kindest/node:v1.29.1 "/usr/local/bin/entr…" 6 days ago Up 3 days kind-cluster-worker3
cac77bb51e52 kindest/node:v1.29.1 "/usr/local/bin/entr…" 6 days ago Up 3 days kind-cluster-worker2
sudo ls -lha /var/lib/docker/overlay2/3e94271808092cd2f2d0cfacbbf16e13eba84f9dc5529cb85687bb0cc9f3c8c2
total 92K
drwx--x--- 4 root root 4,0K fev 15 00:37 .
drwx--x--- 580 root root 68K fev 14 22:06 ..
drwxr-xr-x 7 root root 4,0K fev 14 22:13 diff
-rw-r--r-- 1 root root 26 fev 14 22:06 link
-rw-rw-rw- 1 root root 57 fev 14 22:06 lower
drwx------ 3 root root 4,0K fev 14 22:11 work
La carpeta merged ya no existe, ¿y dónde fue el archivo /root/david?
sudo ls -lha /var/lib/docker/overlay2/3e94271808092cd2f2d0cfacbbf16e13eba84f9dc5529cb85687bb0cc9f3c8c2/diff
total 40K
drwxr-xr-x 7 root root 4,0K fev 14 22:13 .
drwx--x--- 4 root root 4,0K fev 15 00:37 ..
drwxr-xr-x 5 root root 4,0K fev 14 22:15 etc
drwx------ 2 root root 4,0K fev 14 22:15 root
drwxrwxrwt 2 root root 4,0K fev 14 22:15 tmp
drwxr-xr-x 5 root root 4,0K jan 25 11:03 usr
drwxr-xr-x 5 root root 4,0K jan 25 11:06 var
sudo ls -lha /var/lib/docker/overlay2/3e94271808092cd2f2d0cfacbbf16e13eba84f9dc5529cb85687bb0cc9f3c8c2/diff/root/
total 20K
drwx------ 2 root root 4,0K fev 14 22:15 .
drwxr-xr-x 7 root root 4,0K fev 14 22:13 ..
-rw------- 1 root root 121 fev 15 00:37 .bash_history
-rw-r--r-- 1 root root 6 fev 14 22:15 david
-rw------- 1 root root 712 fev 14 22:15 .viminfo
sudo cat /var/lib/docker/overlay2/3e94271808092cd2f2d0cfacbbf16e13eba84f9dc5529cb85687bb0cc9f3c8c2/diff/root/david
teste
Ahora vamos a eliminar el contenedor.
docker container rm ubuntu
# ¿La carpeta está ahí?
sudo ls -lha /var/lib/docker/overlay2/3e94271808092cd2f2d0cfacbbf16e13eba84f9dc5529cb85687bb0cc9f3c8c2
ls: cannot access '/var/lib/docker/overlay2/3e94271808092cd2f2d0cfacbbf16e13eba84f9dc5529cb85687bb0cc9f3c8c2': No such file or directory
# No
Vimos que de hecho necesitamos persistir los datos cuando sea necesario y para eso podemos usar un volumen.
Si fuéramos a levantar ese mismo contenedor pero ahora usando el volumen data_volume.
# Estamos diciendo que el volumen data_volume debe ser el directorio /volume dentro del contenedor
docker run -it --name ubuntu -v data_volume:/volume ubuntu bash
# ¿Existe el volumen?
root@e8b35562c932:/# ls -lha /volume
total 8.0K
drwxr-xr-x 2 root root 4.0K Feb 15 00:34 .
drwxr-xr-x 1 root root 4.0K Feb 15 03:50 ..
# Vamos a crear el archivo david con el contenido teste y salir del contenedor
root@e8b35562c932:/# cd volume/
root@e8b35562c932:/volume# echo "teste" > david
root@e8b35562c932:/volume# ls
david
root@e8b35562c932:/volume# exit
# Eliminamos el contenedor
docker container rm ubuntu
# Verificando los datos
sudo ls -lha /var/lib/docker/volumes/data_volume/_data/
total 12K
drwxr-xr-x 2 root root 4,0K fev 15 00:51 .
drwx-----x 3 root root 4,0K fev 14 21:34 ..
-rw-r--r-- 1 root root 6 fev 15 00:51 david
sudo cat /var/lib/docker/volumes/data_volume/_data/david
teste
Una curiosidad, si no creas el volumen, se creará automáticamente.
docker run -it --name ubuntu -v data_volume2:/volume ubuntu bash
root@178a9408f35f:/# exit
exit
docker volume ls
DRIVER VOLUME NAME
local 489998434eb6ccc46100905700247d441f9abb565a005592a73617f8f5090cea
local b8af063f368da3ab7deaa0b5e44d645e91d2ffaaf1fa0f3e947f7c62ff6e11c4
local cacf4290059f1ba89f765469941d6712d0e78f94d53557dda530702ec2b2904d
local data_volume
local data_volume2
local eb98d6fd1af16b7d964a0a0251e6e2a3485878f7a8a66d2cc866464da9bec471
# Eliminando los volúmenes si quieres
Ahora vamos a hacer otra prueba: 2 contenedores accediendo al mismo volumen.
# En el contenedor 1
docker run -it --name ubuntu1 -v shared_volume:/volume ubuntu bash
root@7f3599bc79de:/#
# En el contenedor 2
docker run -it --name ubuntu2 -v shared_volume:/volume ubuntu bash
root@4ad12aeb8fe5:/# cd /volume/
root@4ad12aeb8fe5:/volume# touch david
# En el contenedor 1
root@7f3599bc79de:/# cd volume/
root@7f3599bc79de:/volume# ls
david
Y comparten el mismo directorio fácilmente. Volume mount es cuando montamos un volumen Docker en un directorio en el contenedor y Volume bind es cuando montamos un directorio del host en el contenedor.
Storage Drivers
¿Quién es responsable de ejecutar todas estas operaciones? ¿Qué operaciones? Mantener esta arquitectura de capas creando una capa en la que es posible escribir, mover los archivos entre las capas, etc. Son los Storage drivers.
Los storage drivers mantienen toda esta arquitectura de capas y archivos entre las capas.
Algunos storage drivers conocidos:
- AUFS
- ZFS
- BTRFS
- Device Mapper
- Overlay
- Overlay2
La selección del storage driver suele hacerse automáticamente dependiendo del sistema operativo del host. Estos drivers tienen diferentes rendimientos.
Overlay2: ahora es el predeterminado en todas las distribuciones Linux con soporte activo. Requiere un sistema de archivos ext4 o xfs. Ofrece un buen equilibrio entre rendimiento y eficiencia para operaciones de copia en escritura. Cuando es necesaria una copia en escritura, el driver busca en las capas de la imagen para encontrar el archivo correcto, comenzando por la capa superior. Los resultados se almacenan en caché para acelerar el proceso la próxima vez. Overlay2 a nivel de archivo en oposición al nivel de bloque. Esto mejora el rendimiento, maximizando la eficiencia del uso de la memoria, pero puede resultar en capas escribibles más grandes cuando se realizan muchos cambios.
AUFS y Overlay: son más antiguos. Ninguno de ellos se recomienda para uso en distribuciones Linux modernas donde se soporta overlay2.
BTRFS y ZFS: Estos dos drivers funcionan a nivel de bloque y son ideales para operaciones con uso intensivo de escritura. Cada uno de ellos requiere su respectivo sistema de archivos de respaldo. El uso de estos drivers hace que su directorio /var/lib/docker se almacene en un volumen btrfs o zfs. Cada capa de imagen obtiene su propio directorio en la carpeta subvolumes. El espacio se asigna para directorios bajo demanda según sea necesario, manteniendo bajo el uso del disco hasta que ocurran operaciones de copia en escritura.
Las capas base de la imagen se almacenan como subvolúmenes en el sistema de archivos. Otras capas se convierten en instantáneas, conteniendo solo las diferencias que introducen. Las modificaciones de la capa escribible se tratan a nivel de bloque, agregando otra instantánea con uso eficiente de espacio.
Puedes crear instantáneas de subvolúmenes y otras instantáneas en cualquier momento. Estas instantáneas continúan compartiendo datos inalterados, minimizando el consumo general de almacenamiento.
Usar uno de estos drivers puede proporcionar una mejor experiencia para contenedores con uso intensivo de escritura. Si estás escribiendo muchos archivos temporales o almacenando en caché muchas operaciones en el disco, btrfs o zfs puede superar el rendimiento de overlay2. Lo que debes usar depende de tu sistema de archivos de respaldo - generalmente zfs es preferido como una alternativa más moderna a btrfs.
Device Mapper: Este fue una vez el driver recomendado para CentOS y RHEL, pero perdió su lugar en overlay2 en versiones más recientes del kernel. Este driver requería un sistema de archivos de respaldo direct-lvm. Ya no debe usarse - está obsoleto y será completamente eliminado en el futuro.
Los drivers de almacenamiento de Docker se utilizan para gestionar capas de imagen y la parte escribible del sistema de archivos de un contenedor. Aunque los cambios en los sistemas de archivos del contenedor se pierden cuando el contenedor se detiene, aún necesitan persistirse mientras el contenedor está en ejecución. Es el driver de almacenamiento el que proporciona este mecanismo.
Cada driver tiene un conjunto diferente de optimizaciones que lo hace más o menos adecuado para diferentes escenarios. Hoy en día overlay2 es el driver predeterminado y la opción recomendada para la mayoría de las cargas de trabajo, aunque opciones alternativas como btrfs, zfs y fuse-overlayfs tienen algunas características más avanzadas y pueden ser necesarias en ciertos casos.
Aún existen más, pero al final es Overlay2 y ZFS, siendo que el último solo debe aplicarse en casos específicos.
Volume Driver
Los volúmenes en Docker no son manipulados por el Storage Driver sino por los Volume Driver Plugins. Storage driver solo es responsable del control entre las capas.
Los volúmenes están dedicados y separados de la arquitectura de capas. Son más como un disco duro externo.
El volume driver plugin predeterminado es Local que ayuda a crear un volumen en el host (/var/lib/docker/volumes) para almacenar los datos como ya vimos.
Existen muchos otros plugins que permiten crear un volumen en soluciones externas como:
- Azure File Storage
- Convoy
- Digital Ocean Block Storage
- Flocker
- gce-docker
- GlusterFS
- NetApp
- RexRay: Puede usarse para provisionar almacenamiento en AWS EBS, S3, Openstack Cinder
- Portworx
- VMware Vsphere Storage
Y muchos otros...
# Ejemplo de uso con RexRay EBS
docker run -it --name ubuntu --volume-driver rexray/ebs --mount src=ebs-vol,target=/volume ubuntu bash