Sustituye tus tareas cron por temporizadores de systemd
Sustituye tareas cron por temporizadores systemd: crea unidades service y timer, valida horarios OnCalendar, activa ejecuciones pendientes y revisa registros y errores.
Para sustituir una tarea cron por un temporizador (timer) de systemd, se escriben dos archivos de unidad: un .service que ejecuta el script y un .timer que indica cuándo hacerlo. Después, se habilita el temporizador con systemctl enable --now.
Si alguna vez has abierto /var/log/backup.log a las 9 de la mañana sin poder saber si la ejecución de la noche anterior funcionó, ya sabes por qué tanta gente abandona cron. Esta guía migra una línea real del crontab a un temporizador:
30 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
Cada sección sustituye una parte de esa línea. La programación pasa a ser OnCalendar=, la redirección pasa a ser el journal y la pregunta “¿se ejecutó?” se responde con systemctl status. Para más contexto sobre cron, consulta cómo automatizar tareas cron. Los comandos funcionan en cualquier distribución actual basada en systemd.
Puntos clave
- Un temporizador de systemd es una unidad
.timercuya sección[Timer]decide cuándo se ejecuta el.servicecorrespondiente. El servicio define qué se ejecuta, y el temporizador es la unidad que se habilita. - El servicio que activa un temporizador no necesita sección
[Install]. El temporizador se vincula al arranque medianteWantedBy=timers.target. systemd-analyze calendarcomprueba que una expresiónOnCalendar=se analiza correctamente y muestra la próxima vez que se disparará.Persistent=trueejecuta una tarea que no se ejecutó porque la máquina estaba apagada en cuanto el temporizador vuelve a activarse.- Los temporizadores de systemd no tienen un equivalente integrado al
MAILTOde cron. Para recibir alertas de fallos, usaOnFailure=.
Lo que cron no puede hacer
Cron inicia comandos según una programación y poco más. Las carencias siguientes son los motivos habituales para migrar una tarea:
- Sin registro más allá de la salida enviada por correo. O rediriges la salida a un archivo manualmente o dependes de
MAILTO. Las tareas con temporizador escriben en el journal automáticamente (consulta Lectura de los logs). - Sin dependencias entre tareas. Las tareas con temporizador son unidades normales, así que pueden depender de otras unidades de systemd.
- No se ejecuta nada si la máquina estaba apagada a la hora programada.
Persistent=truerecupera la ejecución perdida (consulta la sección sobre Persistent). - No hay una forma sencilla de comprobar el resultado de la noche anterior.
systemctl statusmuestra la última ejecución y su código de salida.
Crea un temporizador de systemd de principio a fin
Un temporizador funcional consiste en dos archivos en /etc/systemd/system/ y un comando: sudo systemctl enable --now backup.timer. Empieza por el script.
#!/usr/bin/env bash
# /usr/local/bin/backup.sh
set -euo pipefail
SRC=/srv/app/data
DEST=/var/backups/app
tar -czf "$DEST/app-$(date +%F).tar.gz" -C "$SRC" .
find "$DEST" -name 'app-*.tar.gz' -mtime +14 -delete
set -euo pipefail hace que el script termine con un código distinto de cero en cuanto falla cualquier comando, y ese código de salida es el que registra systemd. Haz que el script sea ejecutable y crea el directorio de destino. Si los bits de permisos no te resultan familiares, la guía de permisos de archivos en Linux los explica.
sudo chmod +x /usr/local/bin/backup.sh
sudo install -d -o deploy -m 750 /var/backups/app
A continuación, el servicio, /etc/systemd/system/backup.service:
[Unit]
Description=Nightly tarball of /srv/app/data
[Service]
Type=oneshot
User=deploy
ExecStart=/usr/local/bin/backup.sh
Type=oneshot en systemd.service(5) es adecuado para scripts que se ejecutan hasta completarse. systemd solo considera la unidad como iniciada cuando el script ha terminado. Salvo que añadas RemainAfterExit=, la unidad vuelve inmediatamente al estado inactivo y nunca aparece como “active”. Sustituye deploy por una cuenta existente sin privilegios de root que pueda leer SRC y escribir en DEST. Sin User=, la tarea se ejecuta como root. El servicio no tiene sección [Install] ni ninguna referencia al temporizador. La página sobre temporizadores de la Arch Wiki explica el motivo: el temporizador es la unidad que se habilita, y es él quien activa el servicio con el nombre correspondiente.
Ahora, el temporizador, /etc/systemd/system/backup.timer:
[Unit]
Description=Run backup.service daily at 02:30
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
[Install]
WantedBy=timers.target
timers.target es un target especial que systemd utiliza durante el arranque para iniciar todos los temporizadores que deben estar en ejecución. Recarga systemd, ejecuta la tarea una vez manualmente y después prográmala:
sudo systemctl daemon-reload
sudo systemctl start backup.service # test run now; returns when the script exits
systemctl status backup.service # check the result
sudo systemctl enable --now backup.timer
systemctl list-timers backup.timer
Como cada tarea programada es un servicio normal, puedes ejecutarla bajo demanda con systemctl start backup.service y depurarla sin esperar a las 02:30. Por sí solo, enable únicamente crea los enlaces simbólicos. La opción --now de systemctl(1) además inicia el temporizador. list-timers admite un patrón de unidad, y --all incluye los temporizadores inactivos. Entre ejecuciones, el servicio aparece como inactivo. Es lo normal.
OnCalendar frente a la línea de cinco campos de cron
Ejecuta systemd-analyze calendar sobre cualquier expresión OnCalendar= antes de incluirla en un archivo de temporizador. Confirma que la expresión se analiza correctamente y muestra la próxima vez que se disparará. La gramática de systemd.time(7) es DayOfWeek Year-Month-Day Hour:Minute:Second. Cualquier campo de fecha u hora puede ser * para coincidir con todos los valores, una lista de valores separados por comas o un rango escrito con ... Puedes omitir por completo el día de la semana. Si lo incluyes, usa los nombres de los días en inglés, como Mon o Monday, no *.
Las programaciones de cron más habituales se corresponden con expresiones OnCalendar= tal como se muestra a continuación. La chuleta de cron en Linux cubre la parte de cron de esta tabla:
| Programación | cron | OnCalendar |
|---|---|---|
| Todos los días a las 02:30 | 30 2 * * * | *-*-* 02:30:00 |
| Días laborables a las 22:30 | 30 22 * * 1-5 | Mon..Fri 22:30 |
| Domingos a las 03:00 | 0 3 * * 0 | Sun *-*-* 03:00:00 |
| Día 1 de cada mes, a medianoche | 0 0 1 * * | *-*-01 00:00:00 |
| Cada 15 minutos | */15 * * * * | *:00/15 |
La línea de cron 30 22 * * 1-5 se convierte en OnCalendar=Mon..Fri 22:30, porque los números de día de la semana 1 a 5 de cron corresponden de lunes a viernes. El sufijo /15 significa “repetir cada 15 unidades”. Valida una expresión con systemd-analyze calendar:
systemd-analyze calendar "Mon..Fri 22:30" --iterations=3
Salida de ejemplo (las fechas son marcadores de posición):
Original form: Mon..Fri 22:30
Normalized form: Mon..Fri *-*-* 22:30:00
Next elapse: Mon YYYY-MM-DD 22:30:00 CEST
(in UTC): Mon YYYY-MM-DD 20:30:00 UTC
From now: 5h 12min left
Iteration #2: Tue YYYY-MM-DD 22:30:00 CEST
Iteration #3: Wed YYYY-MM-DD 22:30:00 CEST
La línea normalizada muestra la forma abreviada expandida a la gramática completa. La línea (in UTC) solo aparece cuando la hora local no es UTC. Las versiones anteriores a systemd 253 etiquetan las ejecuciones adicionales como Iter. #2 en lugar de Iteration #2. --iterations= requiere systemd 242 o posterior.
Una advertencia sobre la precisión: el valor predeterminado de AccuracySec de un temporizador es de un minuto, por lo que no se garantiza que una tarea programada a las 02:30:00 se inicie exactamente en ese segundo. Si necesitas la máxima precisión que ofrece systemd, añade AccuracySec=1us a la sección [Timer].
Persistent=true: la función pensada para portátiles
Con Persistent=true, un temporizador que no llegó a ejecutarse a la hora programada porque la máquina estaba apagada inicia su servicio en cuanto el temporizador vuelve a activarse. Normalmente, eso significa en el siguiente arranque. Cron por sí solo no tiene equivalente. Si el portátil está apagado a las 02:30, cron se salta la tarea. Existe una herramienta aparte, anacron, para recuperar las tareas perdidas (en los sistemas de la familia RHEL se distribuye con cronie), pero solo programa en días completos, no a una hora concreta del día. systemd.timer(5) registra cuándo se disparó por última vez cada temporizador persistente. Persistent= no tiene efecto en temporizadores sin OnCalendar=, y la ejecución de recuperación sigue respetando cualquier retardo de RandomizedDelaySec=.
Los temporizadores monotónicos, como OnBootSec= y OnUnitActiveSec=, se comportan de otra manera. Dejan de contar mientras el sistema está suspendido, salvo que se configure WakeSystem=. Usa OnCalendar= junto con Persistent=true para las tareas ligadas a una hora del día.
Lectura de los logs y del estado de la última ejecución
Para comprobar si la tarea de anoche se completó correctamente, ejecuta systemctl status backup.service. Muestra cuándo se ejecutó el servicio por última vez, si tuvo éxito o falló, y el código de salida del proceso. journalctl(1) muestra todo lo que imprimió el script:
systemctl status backup.service
journalctl -u backup.service --since yesterday
journalctl -u backup.service --since "-2h"
--since acepta yesterday, today, marcas de tiempo absolutas y desplazamientos relativos precedidos de -. Ten en cuenta que yesterday equivale a la medianoche del inicio del día de ayer, por lo que la salida también incluye las entradas de hoy.
Merece la pena provocar a propósito un fallo en una tarea con temporizador de systemd al menos una vez. Edita el script para que contenga SRC=/srv/app/missing y ejecútalo:
sudo systemctl start backup.service
systemctl status backup.service
journalctl -u backup.service -n 20
tar falla, set -e hace que el script termine con un código distinto de cero y la unidad queda en estado failed, con ese código de salida en su estado. El journal contiene el propio mensaje de error de tar. Restaura SRC cuando termines.
¿Cuándo sigue siendo válido cron?
Cron sigue siendo válido, y más sencillo, para una única tarea sin dependencias. Una tarea es una sola línea en un crontab, cron está disponible prácticamente en todas partes y una tarea nocturna sin dependencias funciona perfectamente con él. Los temporizadores te cuestan dos archivos y unos cuantos comandos systemctl. Además, no tienen un equivalente integrado a MAILTO. Para recibir una notificación cuando una tarea falle, apunta la directiva OnFailure= del servicio a una unidad que envíe la alerta.
Conclusión
Pasar de cron a los temporizadores de systemd te ofrece tres cosas que cron por sí solo no puede proporcionar: una tarea que puedes ejecutar e inspeccionar bajo demanda, una salida que llega al journal sin redirecciones y ejecuciones de recuperación tras un periodo de inactividad. Elige la tarea del crontab que más te preocupa. Escribe su servicio y su temporizador, valida la programación con systemd-analyze calendar, pruébala con systemctl start y elimina la línea del crontab cuando systemctl status indique que una ejecución programada se ha completado correctamente.
Preguntas frecuentes
¿Puedo ejecutar un temporizador de systemd como usuario normal, sin root?
Sí. Coloca los archivos .service y .timer en ~/.config/systemd/user/ y gestiónalos con systemctl --user, por ejemplo systemctl --user enable --now backup.timer. De forma predeterminada, la instancia de systemd del usuario se detiene cuando se cierra la última sesión de ese usuario, por lo que los temporizadores también dejan de dispararse. Ejecuta sudo loginctl enable-linger seguido del nombre de usuario para que la instancia de usuario se inicie durante el arranque y siga en ejecución sin necesidad de una sesión abierta.
¿Qué diferencia hay entre AccuracySec y RandomizedDelaySec?
AccuracySec da margen a systemd para agrupar eventos de temporizador y despertar la CPU con menos frecuencia. El temporizador puede dispararse en cualquier momento entre la hora programada y AccuracySec después, y esa ventana es de 1 minuto de forma predeterminada. RandomizedDelaySec actúa en sentido contrario a la agrupación: retrasa cada ejecución una cantidad aleatoria de tiempo hasta el valor que establezcas (0 de forma predeterminada), de modo que los temporizadores con la misma programación se reparten en lugar de dispararse a la vez. Si configuras ambos, el retardo aleatorio se aplica antes de que systemd alinee el evento con los demás.
¿Cómo programo una tarea puntual con systemd sin escribir archivos de unidad?
Usa systemd-run con una opción de temporizador, por ejemplo sudo systemd-run --on-calendar='Mon 14:00' /usr/bin/cleanup.sh. Esto crea un par transitorio de temporizador y servicio. Solo el temporizador se inicia de inmediato, y el servicio se ejecuta cuando vence el temporizador. Opciones como --on-active= y --on-boot= crean temporizadores transitorios monotónicos. Las unidades transitorias no sobreviven a un reinicio, así que las tareas recurrentes deben seguir definiéndose en archivos de unidad.
¿El archivo del temporizador tiene que llamarse igual que el servicio que activa?
No, pero es el comportamiento predeterminado. Un temporizador activa el servicio con el mismo nombre sin el sufijo, por lo que backup.timer inicia backup.service. Para activar una unidad con otro nombre, establece Unit= en la sección [Timer], por ejemplo Unit=nightly-backup.service. El valor no puede ser otra unidad .timer. Aun así, systemd.timer(5) recomienda que el temporizador y la unidad que inicia compartan el mismo nombre base.