Introducción
CoreOS es una edición mínima del sistema operativo Fedora, inmutable, con actualizaciones automáticas, y enfocada a ejecutar servicios en contenedores. Aunque puede correr en máquinas físicas, está pensada para el despliegue automatizado de máquinas virtuales, tanto en local como en todo tipo de nubes.
Por todo ello, su instalación y configuración inicial, difieren bastante de cualquier distribución GNU/Linux habitual. Pero el resultado es un sistema robusto y optimizado, que merece la pena explorar.
Objetivo
En este artículo veremos como crear una máquina virtual con Fedora CoreOS, en la que desplegaremos un contenedor con el software Omada Controller que nos permitirá gestionar una infraestructura de red TP-Link.
Lo llevaremos a cabo en un entorno ya existente, que consta de un host físico Debian 13 (Trixie) con KVM/libvirt para virtualización, y una máquina de escritorio con Fedora Workstation 43.
Preparación del Ignition
La configuración de una nueva máquina con CoreOS se basa en el sistema Ignition, con lo que debemos proporcionarle un archivo JSON con un formato específico. Para generarlo, crearemos primero un archivo YAML que veremos a continuación, y utilizaremos la herramienta Butane para convertirlo al formato de Ignition.
Este sería el fichero omadactrl.bu que utilizaríamos en nuestro caso:
variant: fcos
version: 1.6.0
passwd:
users:
- name: core
ssh_authorized_keys:
- ssh-rsa XXXXXX # Clave pública que utilizaremos para acceder vía SSH con el usuario "core"
storage:
disks:
- device: /dev/vdb
wipe_table: true
partitions:
- number: 1
label: containers # Crea una partición en el disco /dev/vdb, que utilizaremos para albergar los contenedores que se ejecutarán en la máquina virtual
filesystems:
- path: /var/lib/containers
device: /dev/vdb1
format: xfs
with_mount_unit: true # Formatea la partición y habilita el montaje automático de la misma mediante un servicio de systemd
files:
- path: /etc/hostname
mode: 0644
contents:
inline: omadactrl.home # Establece el hostname
- path: /etc/sysctl.d/10-disable-ipv6.conf
contents:
inline: | # Deshabilita el protocolo IPv6 para todas las conexiones
net.ipv6.conf.all.disable_ipv6=1
net.ipv6.conf.default.disable_ipv6=1
- path: /etc/NetworkManager/system-connections/enp1s0.nmconnection
mode: 0600
contents:
inline: |
[connection]
id=enp1s0
type=ethernet
interface-name=enp1s0
[ipv4]
address1=192.168.0.100/24,192.168.0.1
dns=192.168.0.1;
dns-search=home
may-fail=false
method=manual
[ipv6]
method=disabled
- path: /etc/hosts
overwrite: false
append:
- inline: | # Añade la entrada con la IP de la máquina en el /etc/hosts
192.168.0.100 omadactrl.home omadactrl
- path: /etc/vconsole.conf
mode: 0644
contents:
inline: KEYMAP=es # Establece el idioma del teclado
- path: /etc/zincati/config.d/55-updates-strategy.toml
contents:
inline: | # Define la ventana en la que se reiniciará de forma automática el servidor para aplicar las actualizaciones del sistema operativo, cuando las hubiere
[updates]
strategy = "periodic"
[[updates.periodic.window]]
days = [ "Sun" ]
start_time = "04:00"
length_minutes = 60
links:
- path: /etc/localtime
target: ../usr/share/zoneinfo/Europe/Madrid # Establece la zona horariaDespués realizamos la conversión ejecutando Butane en nuestra Workstation con Podman:
podman run --interactive --rm quay.io/coreos/butane:release --pretty --strict < omadactrl.bu > omadactrl.ignCon esto ya tenemos el omadactrl.ign con el formato Ignition que necesitamos.
A continuación creamos una carpeta en nuestro host, para albergar el archivo .ign que acabamos de crear, y cualquier otro que vayamos a utilizar en un futuro.
sudo mkdir /var/lib/libvirt/ignition
sudo chown libvirt-qemu:libvirt-qemu /var/lib/libvirt/ignitionUna vez creado el omadactrl.ign, le cambiamos también el propietario.
sudo chown libvirt-qemu:libvirt-qemu /var/lib/libvirt/ignition/omadactrl.ignEn el caso de Debian, AppArmor no le va a permitir a libvirt abrir los archivos .ign en su ubicación, así que debemos indicarle esta excepción añadiendo la siguiente línea al final del archivo /etc/apparmor.d/local/abstractions/libvirt-qemu:
/var/lib/libvirt/ignition/*.ign rk,Y después reiniciamos el servicio AppArmor.
sudo systemctl restart apparmor.serviceDescarga de la imagen de CoreOS
Vamos a la web de descargas de CoreOS y localizamos la URL de la imagen estable para QEMU. Cuando la tengamos, la descargamos en nuestro host y la colocamos en su ubicación definitiva:
wget https://builds.coreos.fedoraproject.org/prod/streams/stable/builds/43.20251024.3.0/x86_64/fedora-coreos-43.20251024.3.0-qemu.x86_64.qcow2.xz
unxz fedora-coreos-43.20251024.3.0-qemu.x86_64.qcow2.xz
sudo mv fedora-coreos-43.20251024.3.0-qemu.x86_64.qcow2 /var/lib/libvirt/images/omadactrl-vda.qcow2
sudo chown libvirt-qemu:libvirt-qemu /var/lib/libvirt/images/omadactrl-vda.qcow2
sudo chmod 600 /var/lib/libvirt/images/omadactrl-vda.qcow2Arranque de la nueva máquina virtual
En los comandos indicados en la guía oficial de CoreOS, se utiliza la opción backing_store para dejar el qcow2 que hemos descargado como base, y hace un overlay sobre dicha imagen para la máquina virtual que estamos creando. Puede ser útil en escenarios en los que vayamos a desplegar muchas máquinas virtuales con la misma base, para lo que supone un ahorro importante de espacio, al compartir todas una única imagen de disco.
Pero en nuestro caso lo vamos a hacer de forma diferente, nuestra máquina virtual atacará directamente a la imagen de disco que hemos descargado, y quedará para uso exclusivo de la misma.
En primer lugar definiremos una serie de variables con los parámetros que le pasaremos al comando virt-install:
export IGNITION_CONFIG="/var/lib/libvirt/ignition/omadactrl.ign"
export DISK_A="/var/lib/libvirt/images/omadactrl-vda.qcow2"
export VM_NAME="omadactrl"
export VCPUS="2"
export RAM_MB="4096"
export DISK_B="/var/lib/libvirt/images/omadactrl-vdb.qcow2"
export DISK_B_GB="20"
export BRIDGE="br2"
export IGNITION_DEVICE_ARG=(--qemu-commandline="-fw_cfg name=opt/com.coreos/config,file=${IGNITION_CONFIG}")Y luego lanzamos la creación de la máquina virtual:
sudo virt-install --connect="qemu:///system" --name="${VM_NAME}" --vcpus="${VCPUS}" --memory="${RAM_MB}" \
--os-variant="fedora-coreos-stable" --import --graphics=none \
--disk="${DISK_A}" \
--disk="${DISK_B},size=${DISK_B_GB}" \
--network bridge=${BRIDGE} "${IGNITION_DEVICE_ARG[@]}"Cuando veamos que ha terminado el proceso, podemos salir de la consola de la máquina virtual pulsando Ctrl+5.
Por último, configuramos la máquina para que arranque automáticamente:
sudo virsh autostart omadactrlInstalación del firewall
Ya nos podemos conectar a la nueva máquina con el usuario «core» e instalar firewalld:
sudo rpm-ostree install firewalldDebemos tener en cuenta que estamos en un sistema inmutable, así que lo reiniciamos para que se aplique el cambio:
sudo systemctl rebootDespués podemos comprobar que efectivamente está ya activo el firewall:
sudo firewall-cmd --list-allConfiguramos los puertos que el controlador Omada va a necesitar abiertos en el firewall. Para ello creamos el archivo /etc/firewalld/services/omada.xml:
<?xml version="1.0" encoding="utf-8"?>
<service>
<short>omada</short>
<description>Omada Controller</description>
<port protocol="tcp" port="8088" />
<port protocol="tcp" port="8043" />
<port protocol="tcp" port="8843" />
<port protocol="tcp" port="29811-29816" />
<port protocol="udp" port="29810" />
<port protocol="udp" port="27001" />
</service>Luego añadimos el nuevo servicio al firewall y comprobamos que queda activado:
sudo firewall-cmd --permanent --add-service=omada
sudo firewall-cmd --reload
sudo firewall-cmd --list-all
sudo firewall-cmd --info-service=omadaPreparación del entorno que ejecutará el contenedor
Vamos a ejecutar el contenedor siguiendo la filosofía «rootless», es decir, con un usuario básico sin privilegios, en lugar de que corra con el usuario «root».
En primer lugar creamos el nuevo usuario, y le habilitamos el lingering para que pueda seguir corriendo servicios a pesar de que no haya ninguna sesión iniciada con el mismo:
sudo useradd -u 1001 box
sudo loginctl enable-linger boxLuego creamos las carpetas que contendrán los datos del contenedor, y hacemos que el nuevo usuario sea su propietario:
sudo mkdir -p /var/lib/containers/omada/data
sudo mkdir -p /var/lib/containers/omada/logs
sudo chown -R box:box /var/lib/containers/omadaCambiamos al nuevo usuario. Hacerlo de esta forma en lugar de utilizando el comando «su» nos facilitará las cosas a la hora de trabajar con servicios de systemd:
sudo machinectl shell box@.hostHabilitamos el servicio que se encargará de arrancar automáticamente los contenedores cuando se reinicie la máquina virtual:
systemctl --user enable podman-restart.service
systemctl --user start podman-restart.service
systemctl --user status podman-restart.serviceHabilitamos el temporizador que comprobará periódicamente si existen nuevas versiones de las imágenes utilizadas en los contenedores, para proceder a su actualización de forma automática, si así lo establecemos:
systemctl --user enable podman-auto-update.timer
systemctl --user list-timers --allDefinición del contenedor
Esta es la parte que puede resultar más «chocante» para los que estamos acostumbrados a trabajar con contenedores Docker. Lo habitual cuando queremos desplegar una aplicación es encontrarnos con un docker-compose.yml de ejemplo, que adaptamos a nuestras necesidades.
Por un lado, CoreOS viene con Podman para la gestión de contenedores en lugar de Docker, y por otro, lo ideal es utilizar lo que llaman Quadlets, que es una manera de integrar la ejecución de contenedores como servicios de systemd.
En nuestro caso, utilizaríamos esta configuración si fuésemos a trabajar con Docker Compose:
services:
omada-controller:
container_name: omada
image: mbentley/omada-controller:5.15
restart: always
ulimits:
nofile:
soft: 4096
hard: 8192
stop_grace_period: 60s
network_mode: host
environment:
- PUID=1001
- PGID=1001
- TZ=Europe/Madrid
volumes:
- /var/lib/containers/omada/data:/opt/tplink/EAPController/data
- /var/lib/containers/omada/logs:/opt/tplink/EAPController/logsPero como vamos a definir un quadlet, lo haremos creando el archivo ~/.config/containers/systemd/omada.container con el siguiente contenido (importante, seguimos con el usuario «box»):
[Unit]
Description=Omada Controller Container
Wants=network-online.target
After=network-online.target
[Container]
ContainerName=omada
Image=docker.io/mbentley/omada-controller:5.15
AutoUpdate=registry
User=1001
Group=1001
Environment=ROOTLESS=true
Environment=TZ=Europe/Madrid
Volume=/var/lib/containers/omada/data:/opt/tplink/EAPController/data:Z,U
Volume=/var/lib/containers/omada/logs:/opt/tplink/EAPController/logs:Z,U
Network=host
Ulimit=nofile=4096:8192
PodmanArgs=--stop-timeout 60
[Service]
Restart=always
[Install]
WantedBy=default.targetEn la documentación de podman-systemd.unit tenemos mucha información para realizar la adaptación de Docker Compose a este tipo de servicios.
Recargamos la configuración de systemd y arrancamos el nuevo servicio:
systemctl --user daemon-reload
systemctl --user start omada.serviceCon esto arrancará el contenedor y ya habríamos terminado.
Referencias
https://docs.fedoraproject.org/en-US/fedora-coreos
https://github.com/coreos/docs/issues/343
https://thelinuxitguy.wordpress.com/2025/01/21/the-ultimate-fedora-coreos-guide/
https://github.com/dave-atx/omada-podman-quadlet/blob/main/.config/containers/systemd/omada/omada.container
https://github.com/mbentley/docker-omada-controller?tab=readme-ov-file#quickstart-guide
https://github.com/containers/quadlet/blob/main/docs/ContainerSetup.md
https://mo8it.com/blog/quadlet/#updating-images
https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html
https://www.tp-link.com/us/support/faq/3281/
https://stackoverflow.com/questions/79173758/podman-volume-mount-permissions-issues
https://www.redhat.com/en/blog/debug-rootless-podman-mounted-volumes
https://www.nanosector.nl/posts/podman-rootless-autostart/
