9.2.2.2.1.1.1. Contenedores privilegiados

Dedicaremos este epígrafe a la gestión de contenedores privilegiados creados y gestionados por el propio administrador[1] y, conocidos estos, introduciremos en el próximo la dificultad de crear contenedores no privilegiados con un usuario normal.

9.2.2.2.1.1.1.1. Preliminares

Al disponer de paquete en Debian, su instalación es sencilla:

$ apt install lxc

Comencemos aclarando, sin embargo, algunas particularidades que nos ayudarán a entender el desarrollo posterior:

  1. El demonio lxc-net gestiona la creación de interfaces puentes (que se estudiarán bajo el epígrafe dedicado a la red) y, como hace libvirtd, proporciona direcciones IP dinámicas valiéndose de dnsmasq.

  2. Los contenedores contienen un conjunto de archivos y directorios que constituyen el sistema de archivos del sistema huésped. Por tanto, cada contenedor recreará una estructura típica de directorios (/bin, /etc, /usr, etc) que variará según sea la distribución que el huésped imita.

  3. Para construir este árbol de directorios, LXC proporciona una serie de plantillas predefinidas.

  4. Al crear un contenedor con una de ellas, la plantilla se descargará y se ejecutará un script que preparará el contenedor utilizando la configuración local y la información de la plantilla. Las plantillas se almacenan en principio en /var/cache/lxc, aunque pueden borrarse posteriormente sin escrúpulo.

  5. A diferencia de QEmu o Virtualbox, no existe un archivo de disco (QCOW2, VDI) que contenga un sistema de archivos totalmente ajeno al anfitrión, sino que el contenido del contenedor son archivos y directorios incluidos en una parte del árbol de directorios del anfitrión (por defecto, /var/lib/lxc/nombre_contenedor). Esto supone que LXC actúe dependiendo de cuál sea el sistema de archivos sobre el que se encuentra esa parte del árbol, esto es, dependiendo de cuál sea el formato de su almacenamiento (backing store en su terminología):

    «none» (o también «dir»)

    este formato es el que se presenta cuando los archivos del contenedor los trata como archivos sin más consideración. Por ejemplo, si el sistema de archivos del anfitrión es ext4 y /var/lib/lxc forma parte de este sistema de archivos, no habrá más remedio que usar este formato. No es lo más apropiado, desde luego:

    • Las instantáneas consisten en hacer una copia completa de todos los archivos.

    • No hay forma de limitar el tamaño del contenedor y, si /var/lib/lxc no constituye un sistema de archivos aparte, los contenedores pueden crecer hasta ocupar todo el espacio disponible de /.

    «btrfs»/«zfs»

    Si /var/lib/lxc se encuentra en un sistema de archivos BTRFS o ZFS (p.e. porque hayamos dispuesto esa ruta en partición aparte y le hayamos dado formato con alguno de estos dos sistemas), entonces podremos informar a LXC de que puede hacer uso de sus características especiales (p.e. soporte nativo para instantáneas, subvolúmenes). Por sus posibilidades, es la opción más conviente.

    «lvm»

    Podemos optar por incluir cada contenedor en un volumen lógico de LVM. LVM soporta instantáneas y, si utilizamos aprovisionamiento fino, las ventajas se multiplican.

    «loop»

    Este es el formato más cercano al estilo de las dos virtualizaciones completas que hemos citados anteriormente: los archivos del contenedor se guardaran dentro de un archivo regular del anfitrión (/var/lib/lxc/nombre_contenedor/rootdev).

9.2.2.2.1.1.1.2. Creación

Para la creación de un contenedor tenemos que tener presente de inicio dos aspectos: qué sistema Linux queremos incluir y cuál será el formato de almacenamiento. Existe otro más (la limitación de recursos), pero lo trataremos más adelante. Comencemos, pues, por lo más sencillo:

# lxc-create -n test -t download -- -d alpine -r 3.17 -a amd64

donde hay que notar:

  • Somos el administrador.

  • Nuestro contenedor se llamará «test».

  • Hacemos la instalación a través de una plantilla llamada download que facilita la operación y nos permite seleccionar de forma sencilla una de las imágenes disponibles en los servidores. Sin añadir más, la imagen ejecuta un script que nos muestra y, a continuación, nos permite seleccionar cuál es la plantilla que deseamos utilizar, pero…

  • Tras -- se pueden incluir las opciones que permita la plantilla (en este caso, download). Pues bien, si observamos la lista de plantillas veremos que cada una se define por cuatro características: distribución (-d), versión (-r), arquitectura (-a) y variante (-v), que son precisamente las opciones que permite añadir download para restringir la lista de selección que nos muestra. Así, si incluyéramos únicamente -a amd64 aparecerían sólo las plantillas para esta arquitectura. Si añadimos las opciones suficientes como para restringir la lista a una sola plantilla, entonces la instalación se realizará inmediatamente sin más preguntas. Esto es precisamente lo que logra la orden de arriba, porque aunque falta especificar la variante, cuando esta no se indica, se entiende que es «default».

Por tanto, hemos instalado una distribución la variante default de la versión 3.17 de Alpine para arquitectura x64_64[2].

Ahora bien, ¿qué pasa con el segundo aspecto, esto es, el formato de almacenamiento? El formato se introduce con la opción -B antes de --, puesto que es algo que nada tiene que ver con la plantilla que se use. Cuando no se especifica nada, lxc-create entiende -B none (o -B dir, que es lo mismo) y, por tanto, LXC entenderá que nuestro contenedor es una mera colección de archivos y directorios sin nada especial de lo que pueda aprovecharse. La traducción de esto es que se habrá creado lo siguiente:

/var/lib/lxc
     +-- test
           +---- config
           +---- rootfs/
                    +-- ...
                    +-- Estructura de directorios de Alpine
                    +-- ...

O sea, un directorio dentro de /var/lib/lxc cuyo nombre coincide con el nombre del contenedor que, a su vez, contiene:

  • Un directorio que incluye la estructura de archivos del contenedor (rootfs). Cuando entremos en el huésped, nos encontraremos enjaulados dentro de él.

  • Un archivo (config) con la configuración del contenedor para la que se toma como referencia la que se encuentra en /etc/lxc/default.conf.

Probemos crear con otro formato:

# lxc-destroy -n test  # Primero borramos el anterior
# lxc-create -n test -t download -B loop --fssize=50M -- -d alpine -r 3.17 -a amd64

La orden es idéntica a la anterior, pero hemos añadido las opciones para que el almacenamiento sea un único archivo (-B loop). LXC crea un archivo, pero ¿de qué tamaño y con qué sistema de archivos lo formatea? Para el tamaño existe la opción --fssize, que sí se ha indicado porque lo predeterminado es 1GiB y nosotros no necesitamos tanto para una minidistribución. El sistema de archivos se especifica con --fstype y su valor predeterminado es ext4. Si gulismeamos nos encontraremos lo siguiente:

/var/lib/lxc
     +-- test
           +---- config
           +---- rootdev  [Aquí dentro está nuestra Alpine]
           +---- rootfs/

La estructura es semejante, pero ahora rootfs está vacío, puesto que es el archivo rootdev el que contiene dentro de sí toda la estructura de directorios. En este caso, cuando arranquemos el contenedor, LXC montará el contenido de rootdev sobre rootfs. También es preciso notar que, si el sistema de archivos lo soporta, el archivo rootdev es un archivo disperso como el que podemos crear nosotros con truncate, por lo que inicialmente no ocupará todo el tamaño que le asignamos, e irá creciendo según añadamos contenido.

Si /var/lib/lxc se encuentra en un sistema de archivos BTRFS, podremos indicarle a LXC que lo tenga en cuenta. Sin embargo, para ello deberemos tener antes instalado:

# apt install btrfs-progs

Ahora sí, procedamos:

# lxc-destroy -n test
# lxc-create -n test -t download -B btrfs -- -d alpine -r 3.17 -a amd64

En principio, no observaremos diferencias respecto a no especificar formato (la estructura de archivos es idéntica), pero existen. De hecho:

# btrfs subvolume list /var/lib/lxc
ID 258 gen 102 top level 5 path test/rootfs

LXC ha definido un subvolumen para la estructura de directorios del contenedor. Esta es la clave que permite luego crear instantáneas o limitar el espacio de disco del contenedor.

Nota

Con ZFS ocurre algo similar.

Por último, podemos escoger como formato de almacenamiento LVM. Para ello es obvio que necesitamos disponer de un grupo de volúmenes (al que llamaremos VGtest) y, aunque no es extrictamente necesario, supondremos que dentro de él también tenemos un pool llamado lxc para aprovisionamiento fino[3]. En estas circunstancias, podemos crear un contenedor así:

# lxc-destroy -n test
# lxc-create -n test -t download -B lvm --vgname VGtest --thinpool lxc --fssize=50M -- -d alpine -r 3.17 -a amd64

Esto supone que dentro del pool VGtest/lxc se cree un volumen lógico de 50 MiB (--fssize) para albergar el contenedor. El nombre del volumen lógico se toma del nombre del contenedor. Por tanto, si consultamos los volúmenes existentes:

# lvs
  LV        VG     Attr       LSize   Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert
  home      VGraid Vwi-aotz-- 500,00m lxc         6,45
  log       VGraid -wi-ao----  64,00m
  lxc       VGraid twi-aotz-- 768,00m             12,59  12,40
  mysql     VGraid Vwi-aotz-- 500,00m lxc         6,41
  raiz      VGraid -wi-ao----   1,75g
  srv       VGraid Vwi-aotz-- 500,00m lxc         6,41
  swap      VGraid -wc-ao----  32,00m
  test      VGraid Vwi-aotz--  50,00m lxc         3,14

Como no hemos especificado sistema de archivos (--fstype), el volumen lógico se formatea en ext4. La orden crea un estructura como la anterior:

/var/lib/lxc
     +-- test
           +---- config
           +---- rootfs/

pero rootfs se encontrará vacío puesto los archivos del contenedor se encuentran en el volumen lógico.

Nota

Como lo habitual es que tanto el grupo de volúmenes como el pool sean siempre los mismos, es posible definir un archivo /etc/lxc/lxc.conf donde se definan estos nombres a fin de no tener que repetirlos constantemente al crear contenedores (véase lxc.system.conf para más información).

9.2.2.2.1.1.1.3. Manejo

Una vez usado lxc-create, se creará el contenedor:

# lxc-ls
test

pero se encontrará parado, lo cual puede comprobarse añadiendo la opción -f:

# lxc-ls -f
NAME STATE   AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED
test STOPPED 0         -      -    -    false

o bien, lxc-info:

# lxc-info -n test
Name:           test
State:          STOPPED

Para arrancar el contenedor es preciso:

# lxc-start -n test

lo cual cambia el estado del contenedor:

# lxc-ls -f
NAME STATE   AUTOSTART GROUPS IPV4          IPV6 UNPRIVILEGED
test RUNNING 0         -      10.0.3.108    -    false
# lxc-info -n test
Name:           test
State:          RUNNING
PID:            3156
IP:             10.0.3.108
Link:           vethL6i8cY
 TX bytes:      2.09 KiB
 RX bytes:      2.56 KiB
 Total bytes:   4.65 KiB

Esta orden lo arranca, pero por defecto lo deja en segundo plano[4], por lo que tendremos que conectarnos al contenedor. La manera fetén de hacerlo es utilizando la orden lxc-console que nos presentará un login de acceso. Sin embargo, ¿cuál es la contraseña? Lo primero, pues, es preparar el acceso para que nos sea posible. Para ello disponemos de lxc-attach, que permite ejecutar directamente órdenes dentro del contenedor, así que podemos empezar por ponerle una contraseña al administrador:

# lxc-attach -n test -- passwd

Advertencia

En otras distribuciones bastará con esto, pero en el caso particular de Alpine, seguiremos teniendo problemas de acceso, porque en ella, cuando se accede como administrador, se consulta securetty para comprobar si la consola de acceso es considerada segura y las que usamos con lxc-console (/dev/lxc/tty1, /dev/lxc/tty2, etc.) no están. Podemos optar por crear un usuario sin privilegios (y ya en el contenedor convertirnos en administrador con su):

# lxc-attach -n test -- adduser -s /bin/ash -g "" usuario

o eliminar el archivo /etc/securetty para evitar la comprobación:

# lxc-attach -n test -- mv /etc/securetty /etc/securetty.move

Hecho lo cual, podremos ingresar en el contenedor:

# lxc-console -n test

Connected to tty 1
Type <Ctrl+a q> to exit the console, <Ctrl+a Ctrl+a> to enter Ctrl+a itself

Welcome to Alpine Linux 3.17
Kernel 5.10.0-19-amd64 on an x86_64 (/dev/tty1)

test login: root
Password:

[...]

test:~# _

Obsérvese, cómo se nos advierte de que para salir de la consola (que es la 1) podemos teclear Ctrl-A+q. Esto se debe a que si cerramos la sesión (por ejemplo, con exit), se nos volverá a pedir el login como ocurre en un sistema habitualmente; y. si optamos por apagar (poweroff). lo que lograremos es apagar el sistema huésped, o sea, parar el contenedor. La combinación de teclas nos permite desconectarnos del huésped para volver al sistema anfitrión. Posteriormente, podremos volver a conectarnos con:

# lxc-console -n test -t1

donde especificamos el número de consola a la que queremos conectar para evitar que lxc-console pueda escoger una distinta[5]. También es útil tener presente que es posible cambiar la tecla de control por si Ctrl+A, ya la usamos para algo especial en el anfitrión (p.e. porque usemos screen):

# lxc-console -e '^k' -n test

Nota

El gorrito (^) se ha escrito literalmente.

Ya sólo nos queda saber cómo parar y eliminar el contenedor. Lo primero se logra bien apagando el contenedor desde el propio huésped (p.e. con poweroff) como ya hemos visto, o bien desde el anfitrión utilizando la orden:

# lxc-stop -n test

Una vez que hayamos parado el contenedor, podremos eliminarlo con:

# lxc-destroy -n test

9.2.2.2.1.1.1.4. Limitación

Al tratar de limitar recursos tenemos que distinguir entre limitar el espacio de disco, que es algo que dependerá del formato de almacenamiento, y limitar el resto de recursos (RAM, CPU, etc) que depende de cgroups.

Espacio de disco

Dos de los formatos que hemos revisado (loop y lvm) ya limitan per se el tamaño del contenedor por vía de la opción --fssize. none, por su parte, es incapaz de fijar una limitación, puesto que los archivos irán creciendo mientras haya espacio disponible dentro del sistema de archivos en el que se encuentre /var/lib/lxc. Por tanto, esta exposición se reduce a conocer cómo limitarlo con brtfs y (zfs). En ambos casos, la limitación se fija haciendo uso de las herramientas propias del sistema de archivos y no del propio LXC, así que es más un problema de conocer tal sistema de archivos que de conocer esta tecnología de contenedores. Estudiemos cómo hacerlo con BTRFS.

Ya se adelantó que al crear un contenedor sobre un sistema BTRFS (e indicarle con :B btrfs que así es), se crea automáticamente un subvolumen para el contenido del contenedor:

# btrfs subvolume list /var/lib/lxc
ID 258 gen 102 top level 5 path test/rootfs

Pues bien, para poder limitar el espacio que ocupará este subvolumen, necesitamos habilitar las cuotas en el sistema de archivos:

# btrfs quota enable /var/lib/lxc

y establecer una cuota para el subvolumen:

# btrfs qgroup limit 50m /var/lib/lxc/test/rootfs

con lo cual:

# btrfs qgroup show -r /var/lib/lxc
qgroupid         rfer         excl     max_rfer
--------         ----         ----     --------
0/5          16.00KiB     16.00KiB         none
0/258         9.65MiB      9.65MiB     50.00MiB

Y, efectivamente, si intentamos dentro del huésped escribir 50MiB:

test:~$  dd < /dev/zero > ceros bs=1M count=50
dd: error writing 'standard output': Quota exceeded
41+0 records in
40+0 records out

seremos incapaces de completar la operación, porque ya había más de 9MiB ocupados por la propia Alpine.

Podemos redefinir cualquier otro valor para la cuota y, si decidimos eliminarla, basta con usar la palabra none:

# btrfs qgroup limit none /var/lib/lxc/test/rootfs

Otros recursos

El resto de recursos (p.e. la memoria RAM) se limitan haciendo uso de cgroups. Las modernas versiones de Debian (a partir de Bullseye) usan v2, así que sobre esta segunda versión es sobre la que trabajaremos. En principio, no hay definida ninguna limitación, así que si sobre mi sistema de 512MiB, entro al contenedor y consulto la memoria disponible:

test:~$  grep ^MemT /proc/meminfo
MemTotal:         484704 kB

obtendremos más o menos esa cantidad de memoria disponible. ¿Cómo establecer las limitaciones? Para ello debemos hacer uso de lxc-cgroups (con la contenedor arrancado) y saber qué controlador debemos tocar:

# lxc-groups -n test memory.max 100m
# lxc-groups -n test memory.swap.max 25m
# lxc-cgroup -n test cpuset.cpus 0,2

Estos, por ejemplo, limitan la RAM, la swap y el uso de la CPU a solamente al primero y tercero de los núcleos. Podemos comprobar estos límites consultando:

# cat /sys/fs/cgroup/lxc.payload.test/memory.max
104857600

o sea, 10MiB expresados en bytes. Los límites, sin embargo, son efímeros y se perderán al apagar la máquina. Para hacerlos permanentes, pueden añadirse al archivo de configuración del contenedor (en este caso, /var/lib/lxc/test/config):

lxc.cgroup2.memory.max = 100m
lxc.cgroup2.memory.swap.max = 25m
lxc.cgroup2.cpuset.cpus = 0,2

9.2.2.2.1.1.1.5. Copias

La copia de un contenedor proporciona, en general, un nuevo contenedor independiente. La herramienta para llevarla a cabo es lxc-copy, y su sintaxis no depende de cuál sea el formato de almacenamiento, aunque los detalles de cada uno nos pueden resultar interesante. Por tanto, introduciremos primero su uso y, ya conocido, estudiaremos cómo se comporta con cada uno de los formatos.

La creación de un nuevo contenedor a partir de otro ya existente es bastante sencilla:

# lxc-copy -n test -N copiatest
# lxc-ls -fF name,state
NAME      STATE
copiatest STOPPED
test      STOPPED

Con ello tendremos dos contenedores distintos, aunque exactamente iguales, que podrán usarse simultáneamente y divergir a partir de ahora. Ambos, además, tendrán el mismo formato de almacenamiento.

none

Internamente lo que ocurre es que se hace una copia simple de los contenidos del contenedor, por lo que ocuparemos el doble de espacio. Sin embargo, este formato, permite al hacer una copia la inclusión de la opción -s (de snapshot):

# lxc-copy -n test -s -N copiatest

En este caso, el contenedor resultante depende de la existencia del original:

# ls -1 /var/lib/lxc/copiatest/
config
lxc_rdepends
overlay
rootfs
# grep rootfs /var/lib/lxc/copiatest/config
lxc.rootfs.path = overlay:/var/lib/lxc/test/rootfs:/var/lib/lxc/copiatest/overlay/delta

ya que se utiliza overlayfs (un sistema de archivos de unión) para constituirse. Por ello, el nuevo contenedor toma como base el antiguo y solamente contiene las diferencias respecto a éste. Téngase en cuenta que si se manipula el contenedor original, tales manipulaciones también se reflejan en el nuevo contenedor, por lo que debería evitarse su ejecución.

Nota

Observe que hacer esto es equivalente a crear un disco derivado QCOW2, o en Virtualbox definir un disco VDI como de multiconexión.

loop

La copia del contenedor con este formato:

# lxc-copy -n test -N copiatest

crea un contenedor independiente cuyo formato es también loop:

# lxc-ls -1
copiatest
test
# ls -1 /var/lib/lxc/copiatest/
config
rootdev
rootfs
# grep rootfs /var/lib/lxc/copiatest/config
lxc.rootfs.path = loop:/var/lib/lxc/copiatest/rootdev

Este formato no soporta la opción -s.

Advertencia

lxc-copy es un comando mudito y deja de mostrar errores cuando estos se producen, a menos que modifiquemos el nivel de los mensajes. Es el caso de intentar añadir la opción -s a la copia de un contenedor con este formato:

# lxc-copy -n test -s -N copiatest
$ echo $?
1

y habrá que añadirle la opción -l para que lo haga:

# lxc-copy -l info -n test -s -N copiatest
lxc-copy: test: storage/loop.c: loop_clonepaths: 45 The loop storage driver does not support snapshots
lxc-copy: test: storage/storage.c: storage_copy: 412 Failed creating new paths for clone of "/var/lib/lxc/test/rootdev"
lxc-copy: test: lxccontainer.c: copy_storage: 3586 Error copying storage.
lxc-copy: test: tools/lxc_copy.c: do_clone: 358 Failed to clone
LVM

La copia, simplemente, crea otro volumen lógico. No soporta la opción -s.

BTRFS

Se añade o no la opción -s obra de un mismo modo: crea un nuevo subvolumen para el nuevo contenedor que es una instantánea del contenedor original:

# lxc-copy -n test -N copiatest
# btrfs subvolume list -s /var/lib/lxc  # -s lista sólo subvolúmenes que sean instantáneas
ID 269 gen 314 cgen 313 top level 5 otime 2023-01-13 19:54:20 path copiatest/rootfs

lo cual supone que, de principio, no ocupen apenas espacio adicional de disco.

Nota

Si el subvolumen asociado al contenedor tiene definido un límite de cuota, el subvolumen del nuevo contenedor también tendrá definido ese mismo límite.

9.2.2.2.1.1.1.6. Instantáneas

Las instantáneas (como en el caso de las herramientas de virtualización completa) permiten guardar un estado determinado del contenedor a fin de poder recuperarlo en el futuro. En este caso, la herramienta es lxc-snapshot:

# lxc-snapshot -n test

Esto genera propiamente una instantánea (por eso no tiene nombre propio), no un nuevo contenedor, que se almacena dentro del propio directorio de «test»:

# lxc-ls
test
# lxc-snapshot -n test -L
snap0 (/var/lib/lxc/test/snaps) 2023:01:13 09:29:44

Obviamente, podremos seguir modificando el contenedor con la seguridad de poder regresar al estado en que sehizo la instantánea:

# lxc-start -n test
# lxc-attach -n test -- touch /root/saludo.txt
# lxc-attach -n test -- ls /root/
saludo.txt
# lxc-stop -n test

Y si ahora queremos revertir los cambios:

# lxc-snapshot -n test -r snap0

el contenedor volverá al estado en que se encontraba al realizar la instantánea:

# lxc-start -n test
# lxc-attach -n test -- ls /root/
# lxc-stop -n test

Las instantáneas, por supuesto, pueden borrarse:

# lxc-snapshot -n test -d snap0
# lxc-snapshot -n test -L
No snapshots
none

La instantánea no es más que una copia simple del contenido del contenedor. Por tanto, duplicaremos la ocupación del disco.

loop

Como none, se hace una copia completa del contenido:

# lxc-snapshot -n test
# lxc-snapshot -n test -L
snap0 (/var/lib/lxc/test/snaps) 2023:01:13 11:06:39
# grep rootfs /var/lib/lxc/test/snaps/snap0/config
lxc.rootfs.path = dir:/var/lib/lxc/test/snaps/snap0/rootfs
# ls /var/lib/lxc/test/snaps/snap0/rootfs/
bin  dev  etc  home  lib  lost+found  media  mnt  opt  proc  root  run sbin  srv  sys  tmp  usr  var
LVM

No soporta la creación de instantáneas.

BTRFS

Usa también la técnica de subvolúmenes e instantáneas del sistema de archivos, por lo que permite ahorrar espacio de disco.

9.2.2.2.1.1.1.7. Configuración

Cuando se crea un contenedor, dentro de su directorio de definición (p.e. /var/lib/lxc/test para el contenedor test), hay al menos dos entidades que se crean:

  • un directorio rootfs en el que se encontrarán los archivos que constituyen el contenido del contenedor.

  • Un archivo config con su definición.

Este último archivo se genera a partir de dos fuentes:

  1. La que suministra la propia orden lxc-create. Tal es el caso, por ejemplo, del propio nombre del contenedor. Por ese motivo, el archivo contiene esta línea:

    lxc.uts.name = test
    
  2. La configuración predeterminada suministrada a través del contenido del archivo /etc/lxc/default.conf. El contenido de este archivo en mi sistema es:

    lxc.net.0.type = veth
    lxc.net.0.link = lxcbr0
    lxc.net.0.flags = up
    
    lxc.apparmor.profile = generated
    lxc.apparmor.allow_nesting = 1
    

    razón por la cual todos los contenedores traen en principio una única interfaz de red conectada a la interfaz puente lxcbr0.

    La ruta de este archivo, sin embargo, no es inamovible. Creación tiene una opción -f que permite introducir una ruta alternativa para el contenedor que se esté creando en ese momento y, además, es posible crear un archivo /etc/lxc/lxc.conf en donde se defina otra ruta que se use permanentemente (consúltese la página de manual lxc.system.conf).

Analicemos algunos aspectos relacionados con la configuración. La configuración de la red, que es más enjundiosa, la trataremos bajo epígrafe aparte.

Grupos

Los contenedores cuya ejecución esté relacionada entre sí, pueden incluirse dentro de un mismo grupo con solo añadir lxc.group a la configuración. Supongamos, por ejemplo, estos dos contenedores:

# lxc-create -n test -t download -- -d alpine -r 3.17 -a amd64
# lxc-copy -n test -N copiatest

que hemos configurado de manera que se complementan y ambos deben estar arrancados. Para ello podemos incluirlos dentro de un mismo grupo:

# cat >> /var/lib/lxc/test/config

# Grupo de pruebas
lxc.group = pruebas
lxc.start.order = 1
lxc.start.delay = 3

# cat >> /var/lib/lxc/copiatest/config

# Grupo de pruebas
lx.group = pruebas
lxc.start.order = 2

Si hemos hecho esta configuración, entonces podremos arrancar el conjunto con:

# lxc-autostart -A -g pruebas

en vez de tener que ir arrancando individualmente cada una de las máquinas. Gracias a lxc.start.order podremos definir el orden de arranque de los contenedores y, lxc.start.delay permite introducir unos segundos de espera para el arranque del siguiente contenedor.

Para apagar el conjunto en una sola orden podemos hacer:

# lxc-autostart -A -k -g pruebas

Autoarranque

Podemos configurar LXC para que un contenedor arranque automáticamente al iniciarse en anfitrión. Para ello, debemos añadir las siguientes lineas a su archivo de configuración:

# cat >> /var/lib/lxc/test/config

# Autoarranque
lxc.start.auto = 1
lxc.start.delay = 5

Todo esto provocará que el contenedor aparezca como listado entre aquellos que arrancan automáticamente:

# lxc-autostart -L
test 5

donde el 5 es el valor de lxc.start.delay. Ahora bien, si tuviéramos varios contenedores con arranque automático, ¿cuál es su orden de arranque? Primero arrancará los contenedores con lxc.start.auto puesto a 1 que además pertenezcan al grupo «onboot», a continuación arrancará los contenedores que pertenezcan a tal grupo y finalmente aquellos con lxc.start.auto puesto a 1 que no pertenezcan a él. Para determinar el orden en contenedores que cumplan las mismas condiciones de arranque se atiende el valor de lxc.start.order.

Directorios compartidos

Si se quiere compartir un directorio entre anfitrión y huésped puede hacerse lo siguiente:

lxc.mount.entry = /tmp mnt none bind,optional 0 0

que es una línea que imita la sintaxis del archivo /etc/fstab. En este caso, montaremos el directorio temporal del anfitrión sobre el directorio /mnt/ del huésped, ya que cuando se utilizan rutas relativas, éstas se toman respecto a la raíz del contenedor. (en nuestro ejemplo, /var/lib/lxc/test/rootfs).

Como alternativa, si los puntos de montaje son varios, puede utilizarse otra variable:

lxc.mount.fstab = /var/lib/lxc/test/fstab

Ganchos

Es posible definir scripts que se ejecuten ante determinados eventos del contenedor (arranque, parada, clonado, etc.) a través de de la directiva lxc.hook.nombre_evento. Basta echarle un vistazo a lxc.container.conf.

Configuración modular

Podemos separar la configuración en archivos independientes utilizando la directiva:

lxc.include = /etc/lxc/net-bridge.conf
#lxc.include = /etc/lxc/net-macvlan.conf

Ver también

La explicación de todas las directivas que pueden incluirse en el archivo de configuración se encuentra en la página de manual lxc.container.conf.

9.2.2.2.1.1.1.8. Red

Para lograr conexión de red los contenedores usan interfaces VETH, puesto que este tipo de interfaces se crean por parejas y permiten la comunicación de dos espacios de nombres de red distintos (el del contenedor y el del anfitrión) al encontrarse cada miembro de la pareja en uno de ellos.

../../../../../../_images/veth.png

Sobre la base de este tipo de interfaces, podemos hacer la configuración de red:

  1. Si queremos que el contenedor comparta red con la interfaz de red de la máquina, podemos incluir la interfaz VETH en una interfaz puente que comparta con la interfaz real del anfitrión. Este es el caso ilustrado en la figura anterior y que se corresponde con el caso de adaptador puente mediante puente en QEmu.

  2. Como alternativa a lo anterior, para lograr el mismo efecto podemos utilizar interfaces macvlan, de modo semejante al caso de adaptador puente mediante interfaz macvtap en QEmu

  3. Si queremos crear redes independientes a la del anfitrión, podemos crear interfaces puente, una por cada una de esas redes, del mismo modo que hacíamos en QEmu al crear interfaces puente.

Nuestro estudio analizará primero cómo está configurada la red de forma predeterminada para después poder profundizar en los tres clases planteados.

9.2.2.2.1.1.1.8.1. Red predefinida

Al arrancar LXC (en Debian al menos) se crea automáticamente una interfaz puente lxcbr0 con dirección IP 10.0.3.1/24:

# ip addr show dev lxcbr0
3: lxcbr0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN
group default qlen 1000
    link/ether 00:16:3e:00:00:00 brd ff:ff:ff:ff:ff:ff
    inet 10.0.3.1/24 brd 10.0.3.255 scope global lxcbr0
       valid_lft forever preferred_lft forever

Los contenedores creados añaden una única interfaz a este puente y reciben automáticamente una dirección IP dentro de la red ya dicha. Además, se añaden algunas reglas en el cortafuegos para asegurar la conectividad. Este es el comportamiento observable, pero ¿a qué se debe?

Para entender qué está ocurriendo es conveniente acudir a /etc/default/lxc que a su vez remite a /etc/default/lxc-net. Ahí se encuentra la línea:

USE_LXC_BRIDGE="true"

que, obviamente, es la responsable de que se cree la interfaz puente. En realidad, este línea no la lee el servicio lxc sino otro llamado lxc-net. De hecho, si ponemos la línea anterior a false y reiniciamos el servicio:

# invoke.rc.d lxc-net restart

observaremos cómo desaparece la interfaz. Este red predeterminada es del tercer tipo que enumeramos al comienzo del epígrafe: una red independiente que, sin embargo, tiene comunicación con el anfitrión ya que a lxcbr0 se le define dirección IP.

Sea como sea, ¿por qué la interfaz se llama lxcbr0 y por qué se usa la la red 10.0.3.0/24? La respuesta está en el script /usr/libexec/lxc/lxc-net en cuyo comienzo se definen esos valores, pero de modo que pueden sobrescribirse redefiniéndolos en el archivo de configuración /etc/default/lxc-net. Por tanto, mirando cuál es el nombre de la variable en el script, podremos cambiar esos valores en /etc/default/lxc-net. Por ejemplo:

USE_LXC_BRIDGE="true"
LXC_BRIDGE="lxcnet0"
LXC_ADDR="10.0.10.1"
LXC_NETMASK="255.255.255.0"
LXC_NETWORK="10.0.10.0/24"
LXC_DHCP_RANGE="10.0.10.100,10.0.10.150"

Como puede verse hay una variable que define cuál es el rango de direcciones que se facilitan por DHCP a los contenedores. ¿Esto significa que el software LXC hace de servidor DHCP? La respuesta es no. El script ya citado se encarga de levantar dnsmasq para que escuche exclusivamente en la interfaz definida por LXC_BRIDGE. Por esa razón el paquete lxc tiene como dependencia dnsmasq-base, que incluye los ejecutables de dnsmasq, pero no los archivos que permiten usarlo como servicio, que se incluyen en el paquete dnsmasq.

Por último, ¿por qué los contenedores, si no se manipula su configuración, tiene una interfaz en esta red? La razón se encuentra en /etc/lxc/default.conf, que incluye las siguientes líneas:

lxc.net.0.type = veth
lxc.net.0.link = lxcbr0
lxc.net.0.flags = up

las cuales determinan que todo contenedor las copie en su configuración adicional y, en consecuencia, presenten una único interfaz de tipo VETH cuya pareja se encuentra incluida en la interfaz puente lxcbr0 que crea LXC. Con esta configuración la interfaz creada dentro del contenedor tiene una dirección MAC cualquiera y, además, cambia cada vez que arranquemos el contenedor. Si queremos que sea fija, podemos añadir al archivo default.conf la siguiente línea:

#lxc.network.hwaddr = 00:16:3e:xx:xx:xx
lxc.network.hwaddr = de:ad:be:ef:xx:xx

Esta línea, a diferencia de las anteriores no se copiará exactamente igual en el archivo de configuración del contenedor, sino que las xx:xx se sustituirán por números hexadecimales con lo que cada contenedor acabará teniendo una MAC fija.

Ahora que hemos tratado la dirección MAC del contenedor, es bueno momento para discutir qué ocurre con su dirección IP. En principio no tiene, pero si el contenedor está preparado para pedir por defecto una por DHCP (como es el caso del de Alpine), la obtendrá. Si, en cambio, se quiere una dirección fija puede establecerse añadiendo al archivo de configuración del contenedor (p.e. /var/lib/lxc/test/config) lo siguiente:

lxc.network.ipv4.address = 10.0.10.5/24

Advertencia

Podría ocurrir que la propia configuración del contenedor eliminara esta dirección proporcionada por LXC a la interfaz. Es el caso del contenedor de Alpine, que desecha la IP fija al pedir una dirección dinámica. En su caso particular, para evitarlo tendrá que modificar tal configuración:

# lxc-attach -n test -- sed -i '/^iface/s:dhcp$:manual:' /etc/network/interfaces

En cualquier caso, nuestro propósito es desechar la configuración de red predeterminada y hacerla toda manualmente para tener mayor control sobre ella. En consecuencia, debemos hacer tres cosas:

  1. Evitar la creación de la interfaz puente editando /etc/default/lxc-net:

    USE_LXC_BRIDGE="false"
    

    o como alternativa, deshabilitar el servicio lxc-net.service.

  2. Instalar el paquete dnsmasq para que actúe como servicio:

    # apt install dnsmasq
    
  3. Reorganizaremos la configuración de /etc/lxc/ del siguiente modo:

    /etc/lxc
          +---- lxc.conf
          +---- common.conf
          +---- lxe-net.conf
          +---- [.. otras configuraciones de red ..]
    

    donde lxc.conf es:

    lxc.default_config = /etc/lxc/lxe-net.conf
    

    o otro archivo de red que se decida, tal como se definirán a continuación. common.conf por su parte contiene la parte de /etc/lxc/default.conf que no refiere configuración de red:

    lxc.apparmor.profile = generated
    lxc.apparmor.allow_nesting = 1
    

    Y, finalmente, lxe-net.conf la configuración de red que espera el servicio lxc-net que acabamos de deshabilitar:

    lxc.net.0.type = veth
    lxc.net.0.link = lxcbr0
    lxc.net.0.flags = up
    lxc.net.0.hwaddr = de:ad:be:ef:xx:xx
    
    lxc.include = /etc/lxc/common.conf
    

9.2.2.2.1.1.1.8.2. Interfaz puente con puente

En este caso se supone que el anfitrión está configurado de modo que hay definida una interfaz puente (br0) dentro de la cual se encuentra la interfaz física (eth0) con lo que su file:/etc/network/interfaces tendrá el aspecto referido en QEmu para este caso.

Si es así podemos definir el siguiente archivo /etc/lxc/bridge.conf:

lxc.net.0.type = veth
lxc.net.0.link = br0
lxc.net.0.flags = up
lxc.net.0.hwaddr = de:ad:be:ef:xx:xx

lxc.include = /etc/lxc/common.conf

Con esto debería ser suficiente, aunque si queremos que esta sea la configuración predeterminada, aún tendremos que editar lxc.conf para que lxc.default_config señale este archivo.

Nota

Hemos supuesto que en la red del anfitrión ya existe un servidor DHCP, así que no nos hemos preocupado por definir uno.

9.2.2.2.1.1.1.8.3. Interfaz puente con macvlan

De nuevo, vuelve a ser pertinente lo expuesto sobre QEmu para interfaces macvtap, aunque en este caso la interfaz sea de tipo macvlan. Como con macvtap, la configuración en el anfitrión puede ser nula, aunque de ser así no habrá conectividad de red entre anfitrión y huésped. Lo mejor, pues, es alterar la configuración del anfitrión para permitirlo:

# The primary network interface
allow-hotplug eth0
iface eth0 inet manual
        up   ip link set $IFACE up
        down ip link set $IFACE down

auto macvlan0
iface macvlan0 inet dhcp
        pre-up    ip link add link eth0 name $IFACE type macvlan mode bridge
        post-down ip link del dev $IFACE

Sea como sea, deberemos crear un archivo /etc/lxc/macvlan.conf con la siguiente configuración:

lxc.net.0.type = macvlan
lxc.net.0.macvlan.mode = bridge
lxc.net.0.link = eth0
lxc.net.0.flags = up
lxc.net.0.hwaddr = de:ad:be:ef:xx:xx

lxc.include = /etc/lxc/common.conf

Nota

Es de notar que esta configuración no provocará la aparición de ninguna interfaz en el anfitrión, como sí ocurre en el caso de usar interfaces VETH. Para una explicación de ello, puede consultar este interesante artículo de RedHat sobre interfaces virtuales en Linux.

9.2.2.2.1.1.1.8.4. Red independiente

No es más que la configuración manual de la estrategia que usa la red predeterminada. Para ello necesitamos definir una interfaz puente en /etc/network/interfaces.d/lxc:

auto lxcnet0
iface lxcnet0 inet manual
   bridge_ports    none
   bridge_maxwait  2
   hwaddress ether de:ad:be:ef:00:00

Y a esta interfaz asociaremos las interfaces VETH que vayamos creando para los contenedores. Para ello podemos crear /etc/lxc/lxcnet0.conf:

lxc.net.0.type = veth
lxc.net.0.link = lxcnet0
lxc.net.0.flags = up
lxc.net.0.hwaddr = de:ad:be:ef:xx:xx

El problema de esta configuración es que cualquier contenedor que se cree con ella, estará totalmente aislado y, además, ni siquiera tendrá una IP. Incluso definiéndole una dirección, seguirá aislado. A partir de aquí tebemos varias soluciones:

  1. Si no nos importa que la red no esté aislada, podemos asignar una dirección IP a lxcnet0 y configurar dnsmasq para que sirva direcciones a través de ella. De este modo, todos los contenedores en la red se configurarán dinámicamente.

  2. Si queremos que la red esté aislada a toda costa, podemos dejar sin configurar lxcnet0 y crear un contenedor que sirva de puerta de enlace con dos interfaces de red: una puesta como adaptador puente (véanse los dos epígrafes anteriores) y otra en esta red independiente. Si a este contenedor le instalamos, además, dnsmasq podremos configurarlo para que proporcione direcciones IP al resto.

Notas al pie