Using systemd timers as a cron replacement
by Andy Zeigert
5 min read
I recently decided to upgrade an old utility instance on AWS from Amazon Linux 2 to Amazon Linux 2023. This was mostly easy, as the linux flavors are very similar, however…
Cron is not installed by default on Amazon Linux 2023, and AWS recommends using systemd
to run scheduled tasks instead of using cron
. So I decided to learn how to do that.
The basic need: Make daily backups of several Postgres databases hosted on RDS and push those backup files to S3, then perform some rolling backup management on S3.1
The existing server used cron to do this. Here’s an example crontab:
CRON_TZ=PST
@midnight $HOME/postgresql_auto_backup_s3/pg_backup_rotated.sh -c $HOME/postgresql_auto_backup_s3/app_backup.config >> $HOME/psql2s3_logs/backup.log 2>&1
And it has worked well for years! So why change? Well, using cron
on AL2023 is possible, but it requires some unofficial packages that could break or be difficult to maintain. Since there was going to be learning involved either way, why not use the recommended way?
Although my setup was specific to my postgres backup jobs, I'm going to demonstrate a generic implementation below.
System services overview
This post assumes the reader is already somewhat familiar with systemd
and systemctl
. I'm an amateur myself, so I'll explain as simple as I know how. systemd
does a bunch of things, but I think of it as being responsible for setting up the system when it boots by mounting disks, starting services, etc.
systemctl
is a command for creating and managing services, which can be just about anything. In our example case, it will be a single bash script.
Another use case for a service is to run Node on a production server. Two advantages of running Node as a service is that systemd
will restart the service if it crashes and maintain logs.
The example services below will be "oneshot" services, i.e. they will be called and run when needed, not left running all the time.
Services are defined using unit files, which are generally stored in /etc/systemd/system
.
Setup
For this tutorial, I'll be using a clean Ubuntu 22.04.3 running via multipass.2 My original experience was with Amazon Linux 2023, but this demo is intended to be generic.
My original bash script is a bit niche, so for the purposes of this tutorial I wrote something simpler:
#!/bin/bash
# example.sh
base=15
product=$(($base * $1))
now=$(date '+%Y-%m-%d %H:%M:%S')
echo The input number is $1.
echo The product of $base times input is ${product}.
echo This script was executed on $now.
The above script checks for a single integer argument3, multiplies it by 15 and then echos the result as well as the date and time it was executed.
Run this script:
bash example.sh 5
The result should be something similar to the output below:
ubuntu@verified-whistler:~$ bash example.sh 5
The input number is 5.
The product of 15 times input is 75.
This script was executed on 2024-02-04 22:16:11.
Create a service unit
Service units are saved in /etc/systemd/system
. To create one, create a new file there (I did not log in as root, so I am using sudo):
sudo vim /etc/systemd/system/example.service
[Unit]
Description="Run a script and pass an integer as the only argument"
[Service]
Type=oneshot
ExecStart=/bin/bash /home/ubuntu/example.sh 5
User=ubuntu
The service can now be run using the following command:
sudo systemctl start example.service
Not seeing the expected output? That's because output of services is sent directly to logs by default. You can quickly check the most recent logs of a service using:
sudo systemctl status example.service
The service output should be shown with information about the invocation.
Add a system timer
In order to run our service on a schedule, we need to create a system timer. A system timer is a service unit that calls a service at a specific interval OR at a specific point in time or repeating point in time.
Create a new timer:
sudo vim /etc/systemd/system/example.timer
Add the following to the timer file. It will invoke the example service unit every fifteen seconds.
[Unit]
Description="15 second timer for example service"
[Timer]
OnBootSec=15sec
OnUnitActiveSec=15sec
AccuracySec=1us
Persistent=true
Unit=example.service
Now activate the timer using:
sudo systemctl enable example.timer
And start it using:
sudo systemctl start example.timer
View the status of all timers — including next time, last time and time left until next — using:
sudo systemctl list-timers
You can also view the status of this specific timer:
sudo systemctl status example.timer
And the status of example.service
now shows example.timer
as the trigger. This script is now running every 15 seconds.
Stop the timer using:
sudo systemctl disable example.timer
Note: Any changes made to service unit files requires restarting the daemon using:
sudo systemctl daemon-reload
Create a service unit template
For my original implementation, I needed to run multiple instances of the same script nightly, passing different arguments for each job. This could be accomplished by creating individual unit files and timers for each job. However, systemd
also supports creating unit and timer templates, which allow the user to invoke many service units based on those shared templates.
To create a service template, create a service unit file with an @ character before the .timer bit, e.g.:
vim /etc/systemd/system/example@.service
The contents should be nearly identical to our one-off unit above, with one exception:
[Unit]
Description="Run a script and pass an integer as the only argument"
[Service]
Type=oneshot
ExecStart=/bin/bash /home/ubuntu/example.sh %i
User=ubuntu
The %i
acts as a variable, and is replaced by a string that appears after the @
character when we create a new service based on our template.
Be sure to run sudo systemctl daemon-reload
. Once loaded, you can run a new unit based on the template using:
sudo systemctl start example@1
The 1 is passed as the argument to the script. The output can be viewed, as usual, using sudo systemctl status example@1
.
Any number of services can be started by passing another argument to the template.
Create a system timer template
We can now create a timer template file that, when enabled, will accept a naming parameter and pass that parameter to our service template:
Create the timer using sudo vim /etc/systemd/system/15sec@.timer
and add the following:
[Unit]
Description="15 second timer for example service"
[Timer]
OnBootSec=15sec
OnUnitActiveSec=15sec
AccuracySec=1us
Persistent=true
Unit=example@%i.service
Run sudo systemctl daemon-reload
. We can now create any number of timed services using this template:
sudo systemctl start 15sec@33.timer
sudo systemctl start 15sec@666.timer
sudo systemctl start 15sec@999.timer
Each command will start a new timer that runs in perpetuity, which will in turn call example@.service
and pass the parameter on to the bash script.
Show all started timers using systemctl list-timers
.
Obviously, running a script every 15 seconds has limited utility. Systemd timers also support realtime timers, so scripts can be run on a schedule.