Auto-deployments with systemd

English
,

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 scping 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.