ktyl ~ blog

Drone CI

When it comes to automation, GitLab CI has been my go-to for running builds, tests and deployments of projects from static websites to 3D open-world games. This has generally been on a self-hosted installation, and often makes use of physical runners. However, I have some gripes: I mostly only use it for the CI, but it comes with an issue tracker and Git hosting solution too - great for some cases, but overkill in so many others. Because it's such a complete solution, GitLab is a bit of a resource hog, and can often run frustratingly slowly.

Recently I've been playing with a friend's self-hosted instance of Drone CI as a lightweight alternative, and I much prefer it. I didn't set up the instance, so that part is out of scope for this post, but in case it's relevant, we're using a self-hosted Gitea instance to host the source. You can find out about configuring Drone with Gitea here.

Yet Another Yaml Config

Like GitLab, Drone is configured via a YAML file at the project root, called .drone.yml. Drone is configured by creating 'steps' to the pipeline, where GitLab uses 'jobs'.

My first project's automation requirements were small - all I needed for a deployment was to copy all the files in a directory on every push to the main branch. This means I needed secure access to the host, and the ability to copy files to it. I didn't want to dedicate any permanent resources to such a small project, so opted for the docker pipeline option.

My pipeline would contain a single deploy step which would configure SSH access to the host, and then use it to copy the relevant files from the checked out version of the project. I decided to use ubuntu as the Docker image for familiarity and accessibility - there are probably better options. Drone widely supports Docker image registries; I have not used Docker much, but would like to get more experience with it.

kind: pipeline
type: docker
name: deploy

steps:
- name: deploy
  image: ubuntu
  when:
    branch:
    - main

  commands:
    - echo hello world

Secrets

A hugely important aspect of automation is ensuring the security of one's pipelines. Automated access between pipelines is a big risk, and should be locked down as much as possible. For passing around secrets such as passwords and SSH keys, Drone has a concept of secrets. I created a private key on my local machine for the runner's access to the remote host, and added a per-repository secret to contain the value. This is a named string value which can be accessed from within the context of a single pipeline step.

I also created secrets to contain values for the remote host address and the user to login as. These are less of a security concern than the private SSH key, but we should obfuscate them anyway. It's also a useful step towards generalising the pipeline for other projects: I can use the same set of commands in multiple CI configurations, and just update the secrets from the project page.

This block was placed in the same step definition as above, below the image: entry:

environment:
  HOST:
    from_secret: host
  USER:
    from_secret: user
  SSH_KEY:
    from_secret: ssh_key

Connecting

To use the SSH key, we need to spin up ssh-agent and load our key into it. Since it's passed into the job as an environment variable, this involves first writing it to a file. We also need to disable host key checking (the bit that asks if you're sure you want to connect to a new host) as we're making an automated SSH connection, and therefore won't be there to type 'yes'.

# configure ssh
- eval $(ssh-agent -s)
- mkdir -p ~/.ssh
- echo "$SSH_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-add
- echo "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config

Finally, it's time to run some SSH commands. I had a bit of trouble getting the hang of variable templating here - it took some trial and error to figure out what variables would get expanded and when. Since my HOST and USER values are defined in secrets, I had to get them from my evironment variables and into a correctly formatted string for the SSH target. As I would be running multiple commands, I also wanted to store this in a variable to keep the SSH commands short in the Drone config.

What ended up working for me was this:

# environment variables get expanded (twice?)
- host="$${USER}@$${HOST}"
# running 'hostname' on the deploy target
- ssh $host "hostname"

Images

It's pretty cool to be able to pass a repository through several Docker images through the pipeline. I have my website's Makefile set up to build off my local machine, which is on Arch. It therefore depends on Arch-specific package names. I didn't want to have to hack around my existing build configuration just to build it automatically, but I also found that the deploy steps I'd already written worked best on Ubuntu.

For Drone, this is no problem - I can simply specify image: archlinux in the build stage, and image: ubuntu for the deploy step. My Makefile and local workflow requires no changes at all, but I can still use the more robust deploy steps from Ubuntu.

Final thoughts

I like Drone's minimalist approach to CI. There isn't much in terms of configuration, and the interface is much snappier than Gitlab's. It will take a bit more work to get a full workflow - Gitlab basically has one out the box - but working with more separate components should provide flexibility and resilience in the long run.

I'd like to explore some more features, like templates for steps shared between repositories, and spend more time tuning exactly when pipelines run. I also want to try building some more complex projects, such as those using game engines like Godot, and those targeting multiple target platforms. Those are adventures for another day, though.

That's all for now, thanks for reading and see you next time!

References