Comprendiendo las Unidades de Systemd: Una Inmersión Profunda en la Configuración de Servicios

Aprende cómo funcionan las unidades de servicio de systemd, incluyendo Unit, Service, Install, anulaciones, reinicios y registros.

Comprendiendo las Unidades de Systemd: Una Inmersión Profunda en la Configuración de Servicios

Los archivos de unidad de systemd son pequeños archivos de texto que deciden cómo se inician los servicios, de qué dependen, bajo qué usuario se ejecutan y qué sucede cuando fallan. Si alguna vez te has preguntado por qué systemctl restart myapp.service funciona para una aplicación pero no para otra, la respuesta suele estar en el archivo de unidad.

Esta guía se centra en las unidades .service porque son las que los administradores y desarrolladores editan con más frecuencia. El mismo sistema también gestiona sockets, temporizadores, montajes, dispositivos, rutas y objetivos, pero los archivos de servicio son donde aparecen la mayoría de los errores operativos.

¿Qué son los Archivos de Unidad de Systemd?

Los archivos de unidad de systemd son archivos de texto simples que contienen directivas de configuración para una unidad específica. Una unidad representa un recurso gestionado por systemd. El tipo más común es la unidad de servicio, que define cómo iniciar, detener, reiniciar y gestionar un proceso en segundo plano o una aplicación.

Los archivos de unidad están organizados en secciones, cada una denotada por corchetes ([]). Las secciones más importantes para las unidades de servicio son:

  • [Unit]: Contiene metadatos sobre la unidad, dependencias y orden.
  • [Service]: Define el comportamiento del servicio en sí, incluyendo cómo ejecutarlo.
  • [Install]: Especifica cómo se debe habilitar o deshabilitar la unidad, típicamente vinculándola a unidades objetivo.

Systemd busca archivos de unidad en varios directorios estándar, siendo los más comunes:

  • /etc/systemd/system/: Para unidades configuradas localmente, anulando las predeterminadas.
  • /usr/lib/systemd/system/: Para unidades instaladas por paquetes en muchas distribuciones.
  • /lib/systemd/system/: Utilizado por algunos sistemas de la familia Debian para unidades proporcionadas por paquetes.

Cuando necesites inspeccionar una unidad, evita adivinar la ruta. Usa:

systemctl cat nginx.service
systemctl show -p FragmentPath nginx.service

systemctl cat es especialmente útil porque muestra la unidad base junto con cualquier anulación adicional. Esa es la versión que systemd está utilizando realmente.

Anatomía de un Archivo de Unidad .service

Desglosemos un archivo de unidad .service típico para entender sus componentes.

La Sección [Unit]

Esta sección proporciona información descriptiva y define las relaciones entre las unidades.

  • Description=: Una descripción legible por humanos del servicio.
  • Documentation=: URLs o rutas a la documentación del servicio.
  • After=: Especifica que esta unidad debe iniciarse después de que las unidades listadas hayan terminado de iniciarse.
  • Requires=: Similar a After=, pero también hace que las unidades listadas sean obligatorias. Si una unidad requerida falla al iniciarse, esta unidad también fallará.
  • Wants=: Una forma más débil de dependencia. Esta unidad intentará iniciar sus unidades deseadas, pero su fallo no impedirá que esta unidad se inicie.
  • Conflicts=: Especifica unidades que no pueden ejecutarse concurrentemente con esta unidad.

Ejemplo de sección [Unit]:

[Unit]
Description=Mi Servidor Web Personalizado
Documentation=https://example.com/docs/mi-servidor-web
After=network.target

Esto indica que nuestro servidor web personalizado debe iniciarse después de que la red esté disponible.

Una trampa común: After= controla el orden, no el requisito. Si escribes After=postgresql.service, systemd inicia tu servicio después de PostgreSQL cuando ambos son parte de la transacción, pero no arrastra automáticamente a PostgreSQL. Si tu aplicación realmente necesita que PostgreSQL sea iniciado por la misma transacción, usa también Wants=postgresql.service o, para una dependencia fuerte, Requires=postgresql.service.

Incluso entonces, las dependencias no son verificaciones de salud. After=network.target no garantiza que DNS funcione, que una API remota sea accesible o que una base de datos acepte conexiones. Tu aplicación aún necesita un comportamiento de reintento sensato.

La Sección [Service]

Aquí reside la lógica central para ejecutar el servicio.

  • Type=: Define el tipo de inicio del proceso. Los tipos comunes incluyen:
    • simple (predeterminado): El proceso principal es el iniciado por ExecStart=. Systemd considera el servicio iniciado inmediatamente después de que el proceso ExecStart= se bifurca.
    • forking: Utilizado para demonios tradicionales que bifurcan un proceso hijo y salen. Systemd espera a que el proceso padre salga.
    • oneshot: Para tareas que ejecutan un solo comando y luego salen.
    • notify: El servicio envía una notificación a systemd cuando ha terminado de iniciarse.
    • dbus: Para servicios que adquieren un nombre D-Bus.
  • ExecStart=: El comando a ejecutar para iniciar el servicio.
  • ExecStop=: El comando a ejecutar para detener el servicio.
  • ExecReload=: El comando a ejecutar para recargar la configuración del servicio sin reiniciarlo.
  • Restart=: Define cuándo se debe reiniciar el servicio. Las opciones incluyen no (predeterminado), on-success, on-failure, on-abnormal, on-watchdog, on-abort y always.
  • RestartSec=: El tiempo a esperar antes de reiniciar el servicio.
  • User= / Group=: El usuario y grupo bajo los cuales debe ejecutarse el servicio.
  • WorkingDirectory=: El directorio de trabajo para los procesos ejecutados.
  • Environment= / EnvironmentFile=: Establece variables de entorno para el servicio.

Ejemplo de sección [Service]:

[Service]
Type=simple
ExecStart=/usr/local/bin/mi-servidor-web --config /etc/mi-servidor-web.conf
User=www-data
Group=www-data
Restart=on-failure
RestartSec=5

Esta configuración inicia nuestro servidor web, lo ejecuta como usuario y grupo www-data, y lo reinicia automáticamente si falla, con un retraso de 5 segundos.

Type= merece cuidado adicional. Muchas unidades rotas usan Type=forking porque un script de inicio antiguo usaba modo demonio. Para una aplicación moderna que permanece en primer plano, Type=simple suele ser correcto. Si tu proceso se bifurca en segundo plano pero systemd no sabe cómo identificar el proceso principal real, los informes de estado y los reinicios pueden volverse engañosos.

Para un trabajo único, usa Type=oneshot y a menudo RemainAfterExit=yes si la acción completada debe contar como activa. Por ejemplo, una unidad que prepara una regla de firewall o monta un recurso especial puede salir exitosamente pero aún representar un estado que te importa.

La Sección [Install]

Esta sección se utiliza al habilitar o deshabilitar una unidad. Define cómo la unidad se integra con las unidades objetivo de systemd.

  • WantedBy=: Especifica el/los objetivo(s) que deberían "desear" esta unidad cuando esté habilitada. Para servicios que deben iniciarse al arrancar, multi-user.target se usa comúnmente.

Ejemplo de sección [Install]:

[Install]
WantedBy=multi-user.target

Cuando ejecutas systemctl enable mi-servicio-personalizado.service, systemd crea un enlace simbólico desde /etc/systemd/system/multi-user.target.wants/ a tu archivo de servicio, asegurando que se inicie cuando el sistema alcance el nivel de ejecución multiusuario.

Si una unidad no tiene sección [Install], aún puede ser perfectamente válida. Simplemente no se puede habilitar directamente con systemctl enable a menos que exista otro mecanismo de instalación. Algunas unidades están destinadas a ser arrastradas por dependencias, sockets, temporizadores o objetivos en lugar de ser habilitadas manualmente.

Creación y Gestión de Unidades de Servicio Personalizadas

Recorramos el proceso de creación de una unidad de servicio personalizada.

Paso 1: Crear el Archivo de Unidad

Crea un nuevo archivo en /etc/systemd/system/ con extensión .service. Para nuestro ejemplo, creemos /etc/systemd/system/mi-app.service.

[Unit]
Description=Servicio de Aplicación Personalizada
After=network.target

[Service]
Type=simple
ExecStart=/opt/mi-app/bin/ejecutar-app --port 8080
User=appuser
Group=appgroup
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Consideraciones Importantes:

  • Asegúrate de que el comando ExecStart apunte a un script o binario ejecutable que sea accesible y tenga permisos de ejecución.
  • Crea el User y Group especificados si no existen (sudo useradd -r -s /bin/false appuser, sudo groupadd appgroup, sudo usermod -a -G appgroup appuser).
  • Asegúrate de que la aplicación pueda iniciarse y detenerse correctamente usando los comandos especificados.

Antes de poner un comando en ExecStart=, ejecútalo manualmente como el mismo usuario si es posible:

sudo -u appuser /opt/mi-app/bin/ejecutar-app --port 8080

Esto detecta bits de ejecución faltantes, directorios faltantes, rutas relativas incorrectas y problemas de permisos antes de que systemd esté involucrado. Una vez que funcione manualmente, muévelo a la unidad y deja que systemd maneje la supervisión.

Paso 2: Recargar la Configuración de Systemd

Después de crear o modificar un archivo de unidad, debes indicarle a systemd que recargue su configuración.

sudo systemctl daemon-reload

Este comando escanea archivos de unidad nuevos o modificados y actualiza el estado interno de systemd.

Paso 3: Habilitar e Iniciar el Servicio

Para iniciar el servicio inmediatamente y configurarlo para que se inicie al arrancar:

sudo systemctl enable mi-app.service  # Crea enlaces simbólicos para inicio al arrancar
sudo systemctl start mi-app.service   # Inicia el servicio ahora

Paso 4: Gestionar el Servicio

Usa comandos systemctl para gestionar tu servicio:

  • Ver estado:

    sudo systemctl status mi-app.service
    

    Esto mostrará si el servicio está activo, su ID de proceso, entradas de registro recientes y más.

  • Detener el servicio:

    sudo systemctl stop mi-app.service
    
  • Reiniciar el servicio:

    sudo systemctl restart mi-app.service
    
  • Recargar el servicio (si ExecReload= está definido):

    sudo systemctl reload mi-app.service
    
  • Deshabilitar el servicio (evitar que se inicie al arrancar):

    sudo systemctl disable mi-app.service
    

Paso 5: Ver Registros con journalctl

Systemd se integra estrechamente con journald para el registro. Puedes ver los registros de tu servicio usando journalctl:

  • Ver registros de un servicio específico:

    sudo journalctl -u mi-app.service
    
  • Seguir registros en tiempo real:

    sudo journalctl -f -u mi-app.service
    
  • Ver registros desde el último arranque:

    sudo journalctl -b -u mi-app.service
    

Mejores Prácticas y Consejos

  • Usa Type=notify para aplicaciones modernas: Si tu aplicación lo soporta, Type=notify proporciona una mejor integración con systemd, permitiéndole rastrear con precisión la preparación del servicio.
  • Ejecuta servicios como usuarios no root: Siempre especifica User= y Group= en la sección [Service] para minimizar los riesgos de seguridad.
  • Define las dependencias cuidadosamente: Usa After=, Requires= y Wants= para asegurar que los servicios se inicien en el orden correcto y que se cumplan las dependencias críticas.
  • Aprovecha Restart=: Configura políticas de reinicio apropiadas para asegurar la disponibilidad del servicio.
  • Mantén los archivos de unidad simples: Para secuencias de inicio complejas, considera usar scripts contenedores invocados por ExecStart= en lugar de comandos complejos directamente en el archivo de unidad.
  • Usa systemctl cat <unidad>: Para ver el contenido completo de un archivo de unidad tal como lo ve systemd, incluyendo cualquier anulación.
  • Usa systemctl edit <unidad>: Este comando abre un editor para crear un archivo de anulación para una unidad existente, lo cual es una forma más limpia de modificar archivos de unidad predeterminados que editarlos directamente.

Editando Unidades Existentes de Forma Segura

No edites unidades propiedad de paquetes en /usr/lib/systemd/system/ o /lib/systemd/system/ a menos que estés depurando una máquina desechable. Las actualizaciones de paquetes pueden reemplazar esos archivos. Usa una anulación en su lugar:

sudo systemctl edit nginx.service

Eso crea un drop-in en /etc/systemd/system/nginx.service.d/. Por ejemplo, para agregar una política de reinicio:

[Service]
Restart=on-failure
RestartSec=5s

Algunas directivas pueden especificarse más de una vez. Otras necesitan ser limpiadas antes del reemplazo. ExecStart= es el ejemplo clásico:

[Service]
ExecStart=
ExecStart=/usr/local/bin/mi-wrapper-nginx

La línea ExecStart= en blanco restablece el valor anterior. Sin ella, systemd puede rechazar la unidad o mantener más comandos de los que pretendías.

Después de cualquier cambio en la unidad o drop-in, usa el mismo bucle de revisión:

sudo systemctl daemon-reload
systemctl cat mi-app.service
sudo systemctl restart mi-app.service
journalctl -u mi-app.service -n 50 --no-pager

Los archivos de unidad no son difíciles una vez que separas los tres trabajos: [Unit] describe relaciones, [Service] describe el comportamiento del proceso y [Install] describe la habilitación. La mayor parte de la depuración en el mundo real consiste en encontrar cuál de esos trabajos fue configurado con la suposición incorrecta.

Un Recorrido Realista por un Archivo de Servicio

Aquí hay un servicio pequeño pero realista para una aplicación web Python:

[Unit]
Description=API de Inventario
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
Type=simple
User=inventory
Group=inventory
WorkingDirectory=/srv/inventory-api
EnvironmentFile=/etc/inventory-api/env
ExecStart=/srv/inventory-api/.venv/bin/gunicorn app:app --bind 127.0.0.1:9000
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

Hay varias decisiones silenciosas en ese archivo. El servicio se ejecuta como inventory, no como root. El comando usa una ruta absoluta al gunicorn del entorno virtual, por lo que no depende del PATH de un shell interactivo. La aplicación se vincula a localhost porque un proxy inverso la expondrá públicamente. El archivo de entorno vive fuera de la unidad para que el despliegue pueda actualizar la configuración sin reescribir los metadatos del servicio propiedad del paquete.

Las líneas de dependencia son intencionalmente modestas. After=postgresql.service controla el orden si PostgreSQL es parte de la misma transacción de inicio. No prueba que la base de datos esté lista para conexiones y no reemplaza la lógica de reintento de la aplicación. network-online.target puede ayudar en sistemas que implementan correctamente la preparación de la red, pero no es una garantía universal de que cada dependencia remota sea alcanzable.

Si este servicio falla, las primeras comprobaciones son predecibles:

systemctl status inventory-api.service
journalctl -u inventory-api.service -b --no-pager
systemctl cat inventory-api.service
sudo -u inventory /srv/inventory-api/.venv/bin/gunicorn app:app --bind 127.0.0.1:9000

El último comando no es algo que dejes ejecutando en producción. Es una comprobación de diagnóstico que pregunta: "¿Puede el usuario configurado ejecutar este comando en absoluto?" Si no puede importar la aplicación, leer el archivo de entorno o escribir en su directorio de registro, systemd no lo arreglará por ti.

Directivas de Recursos y Seguridad que Verás a Menudo

Muchas unidades de producción incluyen endurecimiento o controles de recursos. Algunos ejemplos comunes:

[Service]
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
MemoryMax=512M
CPUQuota=80%

Estas directivas pueden ser muy útiles, pero también pueden romper suposiciones. PrivateTmp=true le da al servicio un /tmp privado, por lo que otro proceso puede no ver los archivos que escribe allí. ProtectHome=true puede bloquear el acceso a /home, /root y /run/user. ProtectSystem=full hace que gran parte del sistema sea de solo lectura desde el punto de vista del servicio. Si una aplicación de repente no puede escribir donde solía hacerlo, inspecciona la configuración de endurecimiento antes de culpar a la aplicación.

Los límites de recursos tienen la misma compensación. MemoryMax= puede evitar que un servicio consuma toda la máquina, pero si el valor es demasiado bajo, el servicio puede ser eliminado bajo carga normal. Revisa el registro en busca de mensajes de falta de memoria y compara el límite con el uso real antes de aumentarlo o eliminarlo.

Los Comandos de Depuración Más Útiles

Ten estos cerca cuando trabajes con unidades de servicio:

systemctl status mi-app.service
systemctl cat mi-app.service
systemctl show mi-app.service
systemd-analyze verify /etc/systemd/system/mi-app.service
journalctl -u mi-app.service -b --no-pager

systemctl show es verboso, pero expone las propiedades que systemd calculó después de analizar la unidad. Eso puede revelar un valor sorprendente heredado de un valor predeterminado, un drop-in o una directiva de reinicio. systemd-analyze verify detecta algunos errores de sintaxis y dependencia antes de que reinicies un servicio. No es un reemplazo para probar la aplicación, pero detecta suficientes errores como para que valga la pena ejecutarlo.