Creating templated Systemd services

2 minute read

Last time I wrote an article about NAT traversal using FRP, which has been my personal solution for exposing SSH access of machines behind NAT to the internet for a long time.

As time goes by, I get more devices behind NAT and more VPS hosts providing FRP access, and the need for connecting one device with multiple FRP hosts arises. Surely, one solution would be writing multiple config files and Systemd service files for each instance of frpc, which would just run perfectly.

Writing multiple Systemd service files

Let’s start this with one frpc.service file that I wrote and am using:

[Unit]
Description=FRP Client Service
After=network.target
StartLimitIntervalSec=0

[Service]
Type=simple
Restart=always
RestartSec=1
User=root
ExecStart=/usr/local/bin/frpc -c /etc/frpc.ini

[Install]
WantedBy=multi-user.target

Now I want to add another frpc instance with an alternate configuration, I could just copy the above file, modify the ExecStart line, and save it as another file.

However, that’s undoubtably a suboptimal solution, especially given that Systemd service files can use “template variables”1. Having too many otherwise identical service configuration files is particularly prone to making a mess. With “template variables”, you can simplify all this job into one single file

Using Systemd service instance variables

Among all “instance variables”, the most commonly used one is “instance name” %i. You’ll just replace the variable part with %i, and in my case, it’s the config file name for frpc.

Instead of putting frpc config files directly under /etc, the first thing I did is making a directory /etc/frpc for all of them. Then I put the “default” one into the directory as /etc/frpc/default.ini, and re-written the service file, utilizing instance variables, as this:

[Unit]
Description=FRP Client Service (%i)
After=network.target
StartLimitIntervalSec=0

[Service]
Type=simple
Restart=always
RestartSec=1
User=root
ExecStart=/usr/local/bin/frpc -c /etc/%i.ini

[Install]
WantedBy=multi-user.target

Notice the two appearances of %i here: The first one in unit description, and the second one on the line ExecStart.

There’s also another thing to note: It’s no longer applicable to name the file as frpc.service, but instead, [email protected]. The AT sign in the file name indicates it’s a “template service”.

Now, to instantiate the [email protected] service into instance “default” (which is also the config file name in /etc/frpc), the following commands were used to manage it:

systemctl start [email protected]
systemctl stop [email protected]
systemctl enable [email protected]

And an extra note on the enable command: If you notice the output from systemctl, it should read like this:

[email protected]:~$ sudo systemctl enable [email protected]
Created symlink /etc/systemd/system/multi-user.target.wants/[email protected] → /etc/systemd/system/[email protected]

Yep, the file isn’t modified in any way, only a symlink is created.

As you can guess, the instance name %i is substituted at the time the file is parsed. This means you can modify the service file on the go and any changes will take effect the next time you run a systemctl command that reads the file.

And here’s the topic: For each additional frpc instance, the only thing to do is to place its config file under /etc/frpc/something.ini, and the new instance can be launched at [email protected].

For a complete list of instance specifiers, here’s a good reference. Time to get yourself some work to cleanup your messy Systemd services :)

  1. It’s official name is “instance specifier”, which IMO is less intuitive. 

Tags:

Updated:

Comments