systemd.timer, an alternative to cron
Tags: linux, programming
Published: 2022-12-08 19:21:40

There will come a point in time during your time administering a Linux server where you will want to perform a job on a schedule. Perhaps you want to rotate some TLS certificates before they expire, or delete old files that are no longer needed. Typically for this, you would use [cron](https://en.wikipedia.org/wiki/Cron), perhaps the most widely used job scheduler for UNIX like systems. You would fire up the crontab for the user, punch in the schedule followed by the command, then write and quit. If you wanted to monitor the job, then you would add `MAILTO` at the top to receive the cron logs should the job fail.

[systemd][0] offers an alternative to cron via [systemd.timer][1], one that I prefer over cron for reasons I will get into later. With systemd.timer, you specify a `*.timer` file and a corresponding `*.service`, what with the latter being the job you want to perform. For example, using the example of certificate rotation, we might have a `certrotate.service` file that looks like this,

[0]: https://systemd.io/
[1]: https://www.freedesktop.org/software/systemd/man/systemd.timer.html

    [Unit]
    Description=Rotate TLS certificates

    [Service]
    Type=oneshot
    ExecStart=/usr/local/bin/uacme -d /etc/uacme.d -h /usr/local/share/uacme/uacme.sh issue example.com

and the corresponding `certrotate.timer` file,

    [Unit]
    Description=Weekly rotation of TLS certificates

    [Timer]
    OnCalendar=weekly
    # Set to true so we can store when the timer last triggered
    # on disk.
    Persistent=true

with both of these in place we can then start the timer.

    $ sudo systemctl start certrotate.timer

The above is certainly more verbose than cron, what with the main difference being that two files are required, one for the job itself, and another for the timer. The implementation of timers in systemd offers up the following benefits over that of cron:

* It allows for independent job execution via `systemctl start`
* Jobs can be configured to have dependencies
* Job output will automatically be written to systemd-journald
* Templated unit files

[2]: https://www.freedesktop.org/software/systemd/man/systemd.exec.html

On the first point, you can make the argument that this is possible via cron. Since cron does just execute the arbitrary commands, so you could run the same commands in the crontab via the terminal. This is true, however systemd offers up some convenience in this regard. With the above example for certificate rotation I can fire of the job like so,

    $ sudo systemctl start certrotate

without having to memorise the full sequence of flags and arguments to pass. This makes debugging jobs easier, and should one fail you can check the status via `systemctl status`, or the entire log with `journalctl -u`.

With the second point, since jobs are just regular `*.service` files, they can be configured to have dependencies via `Wants` and `Requires`, more details can be found under [systemd.unit][3]. This can allow for more sophisticated orchestration between jobs on a system, should any of them depend on another job being completed first.

[3]: https://www.freedesktop.org/software/systemd/man/systemd.unit.html

Regarding the final point, systemd offers the ability to have templated unit files via [specifiers][4]. This reduces the overhead of managing jobs that have repetitive logic. With the above example we currently only rotate the certificate for the domain `example.com`, what if we also want to rotate the certificates for any subdomains too? First we would rename both the `*.timer` and `*.service` files so a `@` precedes the suffix, so `certrotate@.timer`, and `certrotate@.server`. Then, in `certrotate@.service` we would modify `ExecStart` to use `%i` for the instance name of the service,

[4]: https://www.freedesktop.org/software/systemd/man/systemd.unit.html#Specifiers

    ExecStart=/usr/local/bin/uacme -d /etc/uacme.d -h /usr/local/share/uacme/uacme.sh issue %i

then we can perform the following to have the changes applied,

    $ sudo systemctl daemon-reload
    $ sudo systemctl start certrotate@example.com.timer

and this would be the same for each subsequent domain too.

    $ sudo systemctl start certrotate@api.example.com.timer
    $ sudo systemctl start certrotate@about.example.com.timer

And my favourite thing about systemd.timer is how it provides and easy overview of the jobs running on your server, and when they will next execute,

    $ systemctl list-timers
    NEXT                         LEFT          LAST                         PASSED       UNIT                                    ACTIVATES
    Fri 2022-12-09 00:00:00 UTC  4h 57min left Thu 2022-12-08 00:00:00 UTC  19h ago      logrotate.timer                         logrotate.service
    Fri 2022-12-09 00:00:00 UTC  4h 57min left Thu 2022-12-08 00:00:00 UTC  19h ago      man-db.timer                            man-db.service
    Fri 2022-12-09 06:06:34 UTC  11h left      Thu 2022-12-08 06:06:43 UTC  12h ago      apt-daily-upgrade.timer                 apt-daily-upgrade.service
    Fri 2022-12-09 08:19:01 UTC  13h left      Thu 2022-12-08 18:34:00 UTC  28min ago    apt-daily.timer                         apt-daily.service
    Fri 2022-12-09 09:55:30 UTC  14h left      Thu 2022-12-08 09:55:30 UTC  9h ago       systemd-tmpfiles-clean.timer            systemd-tmpfiles-clean.service

Now, one thing that systemd.timer lacks is the `MAILTO` functionality offered by cron. But this can be easily reimplemented by implementing another oneshot service that is invoked via `OnFailure`. For further information on this, see the [MAILTO][5] section from the Arch wiki on how to achieve this.

[5]: https://wiki.archlinux.org/title/Systemd/Timers#MAILTO

If you are someone who administers a Linux server, one that uses systemd specifically, I encourage you to invest some time in using systemd timers. Sure, they're more verbose than cron, but I've found the tradeoff in that regard to be one worth making. They're easy to manage, since they are just standalone services at the end of the day, make deduplication of jobs a cinch with unit specifiers, and offer easy monitoring via `systemctl list-timers` as shown above.