Auto-deployments with systemd
Hi!
I like Kubernetes, and having written my own operators in the past, you quickly grow fond of the high level of automation that it allows you to achieve. However, now that I’m spending the days building my own projects, my requirements (and budget) can’t fit k8s anymore. A couple of production VPSs and my GitHub Actions free quota is all I’m allowing myself to afford on vanity projects, so let’s work with what we have.
GitHub Actions already builds the Canastra binary, so I’m just missing is a way to auto-deploy it to the production VPS. This is composed of two steps:
Copying the binary to the VPS
This can be as simple as creating a new Linux user, generating a SSH keypair and using scp/rsync. I’m going the extra mile to have an user that can’t do anything else, which means chroot. My /etc/ssh/sshd_config
now has:
1
2
3
4
Match Group sftpusers
AuthorizedKeysFile /etc/ssh/sftp-authorized-keys/%u
ChrootDirectory /data/sftp/%u
ForceCommand internal-sftp
It worked when scp
ing from my machine, but when inside the GitHub Action, I got a This service allows sftp connections only
error. Maybe something related to TTY? The sftp
command, however, worked, so I used this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- name: push to production
if: github.ref == 'refs/heads/main'
run: |
mkdir -p ~/.ssh
echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
eval "$(ssh-agent)"
ssh-add - <<< "$SSH_AUTH_KEY"
cat <<EOF | sftp user@server
cd bin/
put canastra-server
put canastra-server.sha256
bye
EOF
env:
SSH_AUTH_KEY: $
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
SSH_KNOWN_HOSTS: $
Restarting the service
Once the files were uploaded, all I needed was to run a single shell script in the server to restart the server. A common way to achieve this is using SSH’s ForceCommand feature, which unfortunately doesn’t work if you are already limiting the user to SFTP-only. You could have a second user that would be able to only run the script, but that’s way too much user management for my taste.
Thankfully, systemd offers a neat solution: systemd.path. My canastra-deploy.path
unit looks like this:
1
2
3
4
5
6
7
8
9
[Unit]
Description=Monitor /data/sftp/canastra/bin for new uploads
[Path]
PathChanged=/data/sftp/canastra/bin/canastra-server.sha256
Unit=canastra-deploy.service
[Install]
WantedBy=multi-user.target
And the service unit:
1
2
3
4
5
[Unit]
Description=Canastra Deployer
[Service]
ExecStart=/usr/local/bin/canastra-deploy.sh
With this approach, systemd monitors for changes in a particular file, and triggers the configured Unit=
once the target is modified. It’s important to use PathChanged=
instead of PathModified=
to avoid triggering the unit too early. From the manual:
PathChanged= may be used to watch a file or directory and activate the configured unit whenever it changes. It is not activated on every write to the watched file but it is activated if the file which was open for writing gets closed. PathModified= is similar, but additionally it is activated also on simple writes to the watched file
That’s all! Once I push a commit to the main
branch, GitHub Actions will build it, and copy the new binary to the server. systemd, once sees a change in the binary file, triggers the deploy script that eventually restarts the service, loading the new version.
Thank you.