Guía de Temporizadores de Systemd: Reemplazando Trabajos Cron para una Programación Confiable

Usa temporizadores de systemd en lugar de cron cuando necesites registros de journal, manejo de ejecuciones perdidas, dependencias y controles de recursos.

Guía de Temporizadores de Systemd: Reemplazando Trabajos Cron para una Programación Confiable

cron sigue siendo adecuado para muchos trabajos. Si necesitas ejecutar un script de shell cada noche y ya tienes redirección de registros funcionando, no hay premio por reemplazarlo. La razón por la que muchos equipos migran trabajos programados de Linux a temporizadores de systemd no es por moda. Es porque los temporizadores de systemd le dan al trabajo una unidad de servicio real, registros predecibles, manejo de dependencias, comportamiento de ejecuciones perdidas y límites de recursos.

Eso importa cuando la tarea no es solo "ejecutar un comando". Un trabajo de respaldo puede necesitar un disco montado. Un calentador de caché puede necesitar que la red esté utilizable. Un exportador de informes puede necesitar ejecutarse como una cuenta de servicio restringida y dejar registros legibles para la próxima persona de guardia. Un temporizador de systemd te permite describir esas necesidades en el mismo lugar donde gestionas el resto del ciclo de vida del servicio.

Entendiendo los Temporizadores de Systemd

Los temporizadores de systemd son archivos de unidad de systemd que controlan cuándo se activan otras unidades de systemd, típicamente unidades de service. A diferencia de cron, que es un demonio independiente, los temporizadores de systemd son una parte integral del sistema de inicio systemd. Esta integración profunda trae varios beneficios significativos, especialmente en cuanto a confiabilidad, registro y gestión de recursos.

Un temporizador de systemd siempre funciona en conjunto con otra unidad, más comúnmente una unidad de service. El archivo .timer define cuándo debe ocurrir un evento, y el archivo .service correspondiente define qué acción debe realizarse cuando se activa ese evento. Esta clara separación de responsabilidades hace que los temporizadores de systemd sean altamente modulares y flexibles.

Ventajas Clave de los Temporizadores de Systemd sobre Cron

Aunque cron es funcional, los temporizadores de systemd abordan muchas de sus limitaciones, ofreciendo una solución de programación más robusta y rica en características:

  • Confiabilidad y Persistencia: Si un temporizador de calendario usa Persistent=true y el sistema se apaga durante una ejecución programada, systemd registra que la ejecución se perdió e inicia el servicio asociado después del próximo arranque. El cron normal generalmente no se pone al día sin una herramienta separada como anacron.
  • Integración con systemd: Los temporizadores se benefician del potente registro de systemd (a través de journalctl), la gestión de dependencias y el control de recursos (cgroups). Esto significa mejor monitoreo, informes de errores más claros y la capacidad de definir secuencias de inicio complejas o límites de recursos para tareas programadas.
  • Reproducibilidad y Control de Versiones: Los archivos de unidad de systemd son archivos de texto plano que se pueden almacenar fácilmente en sistemas de control de versiones. Esto permite implementaciones reproducibles y un seguimiento más fácil de los cambios en las tareas programadas en múltiples sistemas.
  • Programación Basada en Eventos: Más allá de la programación simple basada en tiempo, los temporizadores de systemd se pueden activar en relación con el arranque del sistema (OnBootSec) o después de la última activación de una unidad (OnUnitActiveSec), proporcionando opciones de programación más dinámicas.
  • Expresiones de Tiempo Flexibles: systemd ofrece un rico conjunto de expresiones de eventos de calendario, a menudo más legibles y versátiles que la sintaxis de cron, incluyendo horarias, diarias, semanales y fechas/horas específicas.
  • Gestión de Recursos y Dependencias: Los servicios de systemd lanzados por temporizadores heredan el entorno de systemd, incluyendo la configuración de cgroups, y pueden declarar dependencias de otras unidades de systemd (por ejemplo, esperar a que la red o una base de datos estén disponibles antes de ejecutarse).
  • Manejo de Salida Estándar/Error: systemd captura automáticamente stdout y stderr de los servicios lanzados por temporizadores y los dirige al diario del sistema, haciendo que la depuración y auditoría sean mucho más simples que con la salida basada en correo electrónico de cron o la redirección manual.

Configurando Temporizadores de Systemd

Configurar un temporizador de systemd implica crear dos archivos de unidad: una unidad de servicio (.service) y una unidad de temporizador (.timer). Estos archivos se colocan típicamente en /etc/systemd/system/ para temporizadores de todo el sistema o en ~/.config/systemd/user/ para temporizadores específicos de usuario.

1. La Unidad de Servicio (archivo .service)

La unidad de servicio define el comando o script real a ejecutar. Es un archivo de servicio estándar de systemd, pero a menudo diseñado para ejecutarse de forma no interactiva y para realizar una tarea específica.

Ejemplo: /etc/systemd/system/mytask.service

[Unit]
Description=Mi Servicio de Tarea Programada

[Service]
Type=oneshot
ExecStart=/usr/local/bin/mytask.sh
User=myuser
Group=mygroup
# Opcional: Limitar recursos en versiones recientes de systemd
# CPUWeight=50
# MemoryMax=1G

[Install]
WantedBy=multi-user.target

Explicación:

  • [Unit]: Contiene información genérica sobre la unidad.
    • Description: Una descripción legible por humanos.
  • [Service]: Define la configuración específica del servicio.
    • Type=oneshot: Indica que el servicio ejecuta un solo comando y luego termina. Esto es común para tareas programadas.
    • ExecStart: El comando o script a ejecutar. Proporciona la ruta completa.
    • User, Group: Define el usuario y grupo bajo los cuales se ejecutará el comando. Siempre ejecuta las tareas con los privilegios mínimos necesarios.
    • CPUWeight, MemoryMax: Controles opcionales de cgroup. Son útiles cuando un trabajo programado no debe privar al resto del host.
  • [Install]: Define cómo se debe habilitar la unidad.
    • WantedBy=multi-user.target: Aunque presente, esta sección es a menudo menos crítica para servicios activados por temporizadores, ya que la unidad del temporizador generalmente determina la activación. Sin embargo, puede ser útil si también deseas que el servicio sea activable manualmente o para integrarse en otros objetivos de systemd.

2. La Unidad de Temporizador (archivo .timer)

La unidad de temporizador define cuándo se debe activar la unidad de servicio correspondiente. Debe tener el mismo nombre que su contraparte de servicio (por ejemplo, mytask.timer para mytask.service).

Ejemplo: /etc/systemd/system/mytask.timer

[Unit]
Description=Ejecuta mytask.service diariamente

[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=600
AccuracySec=1min

[Install]
WantedBy=timers.target

Explicación:

  • [Unit]: Información genérica.
    • Description: Una descripción para el temporizador.
  • [Timer]: Define la configuración específica del temporizador.
    • OnCalendar: La configuración más común, define un evento de calendario. Utiliza expresiones como:
      • daily: Todos los días a medianoche.
      • weekly: Todos los lunes a medianoche.
      • monthly: El primer día de cada mes a medianoche.
      • hourly: Cada hora en el minuto.
      • *-*-* 03:00:00: Todos los días a las 3:00 AM.
      • Mon..Fri 08:00..17:00: Días laborables entre las 8 AM y las 5 PM.
      • Mon *-*-* 03:00:00: Todos los lunes a las 3 AM.
    • OnBootSec: Activa el servicio después de un tiempo específico desde el arranque del sistema. Por ejemplo, OnBootSec=10min.
    • OnUnitActiveSec: Activa el servicio después de un tiempo específico desde la última activación del servicio. Por ejemplo, OnUnitActiveSec=1h para ejecutar cada hora después de que se complete la ejecución anterior.
    • Persistent=true: Crucial para la confiabilidad. Si el sistema está apagado durante una ejecución programada, el servicio se activará poco después del próximo arranque.
    • RandomizedDelaySec=600: Añade un retraso aleatorio de hasta 600 segundos. Esto es útil cuando muchas máquinas comparten el mismo temporizador y no deseas que todos los hosts golpeen una base de datos, API o servidor de respaldo exactamente al mismo segundo.

Una Migración Real de Cron a Temporizador

Supongamos que actualmente tienes esta entrada de cron de root:

15 2 * * * /usr/local/sbin/backup-app.sh >> /var/log/backup-app.log 2>&1

Funciona en una máquina tranquila, pero tiene los puntos débiles habituales. Si el disco de respaldo no está montado, el script puede fallar a medio camino. Si el servidor está apagado a las 2:15 AM, la ejecución se omite. Si el script escribe un error útil, alguien tiene que recordar qué archivo de registro personalizado revisar. Si el script comienza a usar demasiada memoria, cron no te ayudará a contenerlo.

La versión de systemd separa el comando del horario:

# /etc/systemd/system/backup-app.service
[Unit]
Description=Respaldar datos de la aplicación
RequiresMountsFor=/mnt/backups
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
User=backup
Group=backup
WorkingDirectory=/srv/app
ExecStart=/usr/local/sbin/backup-app.sh
MemoryMax=1G
CPUWeight=40
Nice=10
# /etc/systemd/system/backup-app.timer
[Unit]
Description=Ejecutar respaldo de la aplicación cada noche

[Timer]
OnCalendar=*-*-* 02:15:00
Persistent=true
RandomizedDelaySec=15min
AccuracySec=1min
Unit=backup-app.service

[Install]
WantedBy=timers.target

Hay algunos detalles que vale la pena notar. RequiresMountsFor=/mnt/backups le dice a systemd que la ruta debe estar montada antes de que el servicio se inicie. After=network-online.target y Wants=network-online.target son útiles solo si tu administrador de red realmente proporciona un servicio de espera en línea; en muchas distribuciones ese servicio está deshabilitado por defecto. Si el respaldo solo escribe en un disco local, omite la dependencia de red.

Type=oneshot es adecuado para scripts que hacen su trabajo y terminan. No lo uses para un demonio que permanece en ejecución. WorkingDirectory= te salva de scripts que accidentalmente dependen de ser lanzados desde un shell en un directorio particular. User=backup suele ser mejor que ejecutar el trabajo como root y esperar que cada comando dentro del script sea cuidadoso.

Después de guardar los archivos:

sudo systemctl daemon-reload
sudo systemctl enable --now backup-app.timer
systemctl list-timers backup-app.timer

Para probar el trabajo inmediatamente, inicia el servicio, no el temporizador:

sudo systemctl start backup-app.service
journalctl -u backup-app.service -n 100 --no-pager

Esa distinción previene mucha confusión. Iniciar backup-app.timer arma el horario. Iniciar backup-app.service ejecuta el respaldo real.

Eligiendo la Expresión de Temporizador Correcta

OnCalendar= es el reemplazo más cercano a la sintaxis de cron, pero se lee de manera diferente. Puedes verificar lo que systemd cree que significa una expresión antes de implementarla:

systemd-analyze calendar 'Mon..Fri 03:30'
systemd-analyze calendar '*-*-01 04:00:00'
systemd-analyze calendar 'Sun *-*-* 23:00:00'

Usa temporizadores de calendario para trabajos de tiempo real: respaldos nocturnos, informes semanales, limpieza mensual, verificaciones de certificados y otras tareas donde el calendario humano importa. Usa temporizadores monótonos para el comportamiento de "ejecutar después de que algo suceda":

[Timer]
OnBootSec=10min
OnUnitActiveSec=1h

Ese patrón inicia el servicio diez minutos después del arranque, luego nuevamente una hora después de la última activación. Es una buena opción para sondeos, limpieza local y bucles de mantenimiento ligeros. No es lo mismo que "en el minuto cero de cada hora". Si el trabajo toma doce minutos, la siguiente ejecución se cuenta desde el momento de la activación, no desde tu expectativa de tiempo real.

También piensa en la superposición. Para una unidad de servicio normal, systemd no iniciará una segunda copia de la misma unidad activa solo porque llegó el próximo evento del temporizador. Si tu trabajo puede ejecutarse más tiempo que su intervalo, decide si eso es aceptable. A veces la respuesta correcta es un bloqueo en el script, como flock, porque puede producir un mensaje claro de "ejecución anterior aún activa". A veces la respuesta correcta es alargar el intervalo.

Hábitos Operativos que Ahorran Tiempo

La vista de temporizadores es tu primer panel de control:

systemctl list-timers --all

Muestra la última ejecución, la próxima ejecución y la unidad que cada temporizador activa. Si el temporizador está listado pero el servicio nunca se ejecuta, verifica la expresión del calendario y si el temporizador está habilitado. Si el servicio se ejecuta y falla, ignora el temporizador por un momento e inspecciona el servicio:

systemctl status backup-app.service
journalctl -u backup-app.service --since today

Cuando edites cualquiera de los archivos de unidad, ejecuta:

sudo systemctl daemon-reload
sudo systemctl restart backup-app.timer

Reiniciar el temporizador después de cambios en el horario es un buen hábito porque hace que el próximo tiempo de activación se actualice inmediatamente. Si solo cambiaste el script en sí, generalmente no necesitas daemon-reload.

Para temporizadores de usuario, usa systemctl --user y coloca las unidades en ~/.config/systemd/user/. Son útiles para estaciones de trabajo de desarrolladores y automatización por usuario, pero tienen una advertencia importante: por defecto, los servicios de usuario están vinculados a la sesión de inicio de sesión del usuario. Si necesitas que un temporizador de usuario siga ejecutándose después del cierre de sesión, habilita la persistencia con loginctl enable-linger username. Esa es una elección administrativa deliberada, no algo para ocultar dentro del artículo como una solución mágica.

Cuando Cron Sigue Siendo la Mejor Herramienta

No muevas todo ciegamente. Cron es más fácil de leer para tareas pequeñas locales de usuario, especialmente en servidores antiguos o contenedores mínimos donde systemd no es PID 1. Si tu único requisito es "ejecutar este comando inofensivo cada cinco minutos", cron puede ser la respuesta más clara.

Los temporizadores de systemd valen la pena cuando el trabajo tiene necesidades similares a las de un servicio: identidad controlada, registros en el diario, límites de recursos, dependencias, comportamiento de recuperación o implementación estándar a través de archivos de unidad. En la práctica, recurro a temporizadores cuando la tarea programada despertaría a alguien si fallara. El archivo de unidad adicional vale la pena cuando le da al próximo operador un camino directo desde "¿qué se ejecutó?" hasta "¿qué falló?" hasta "¿qué cambió?".

Un último hábito que vale la pena adoptar durante las migraciones: mantén la entrada de cron antigua comentada cerca solo hasta que el temporizador se haya ejecutado con éxito varias veces, luego elimínala. Los horarios duplicados son una fuente silenciosa de daños. Dos trabajos de respaldo pueden competir por el mismo bloqueo, dos trabajos de limpieza pueden eliminar archivos antes de lo esperado y dos trabajos de informes pueden enviar correos electrónicos duplicados. Después de habilitar el temporizador, verifica systemctl list-timers --all, confirma el diario del servicio y asegúrate de que la ruta antigua de cron ya no esté activa.