thraxil.org:

Continuously Deploying Django with GitHub Actions

by anders pearson Sun 12 May 2019 17:49:31

A couple years ago, I wrote a post covering how I set up a continuous deployment pipeline for my personal Django apps using Docker and Jenkins.

A lot has changed since then so I thought it was about time I updated. Especially since this weekend I switched from Jenkins to GitHub Actions.

So, a quick disclaimer before I go on: GitHub Actions is currently in public beta. They have been clear that it is not yet considered stable for production and comes with no warranty. It could change and break my setup at any time. They also haven't released any information about what it will cost when it is fully out. These are my personal apps so I'm fine with all of that. I'll try to remember to update this post when the beta is over, but in the meantime, use it at your own risk.

My old post covered the basic setup and, honestly, the general approach hasn't changed much; I've just been swapping out the pieces. It's worth reading the old post for more details, but I'll recap the basic approach here so the rest of this post can just cover the new GitHub Actions stuff.

The old approach to deployment was that I ran Jenkins server alongside my app servers. When there was a push to the master branch on GitHub, it would trigger a build there, which would do the following:

Jenkins used to run those steps with a fairly small shell script described in the old post. At some point, I converted that to a "proper" Jenkins pipeline specified in a Jenkinsfile. I never wrote about that, and I'm replacing it now, but it's up here if you are curious. It did some things better than the the shell script version and made for a much nicer overall experience in the Jenkins web interface, but mostly it made me glad that I don't often have to code in Groovy.

Jenkins worked OK for me but it's always been a pretty awkward part of the stack and not a lot of fun to run and keep updated.

So when Github Actions came out and I got access to the public beta, I decided to see if I could replace my Jenkins setup.

What I came up with is seems to work pretty well.

Actions for a project are stored as code in a .github directory in your project. There's a nice web UI for editing actions, but it's worth looking at the code. My old blog post covered the deployment of my antisocial feed reader app, so for consistency, let's look at how the GitHub Actions setup looks for it.

I've just put everything in a main.workflow file. There are actually two workflows in there. The first two stanzas just set up and run Jessie Frazelle's branch-cleanup-action GitHub action which keeps things tidy by deleting merged branches. She has a great blog post on how it works that helped me start to get my head around Actions.

The main deploy workflow starts with:

workflow "run tests" {
  on = "push"
  resolves = ["sentry release"]
}

The name "run tests" is because I'm using the same workflow to run the unit tests on PRs and, if they pass and the branch is merged into master (which only I can do), it runs the rest of the steps to deploy the app. That, along with the way that Actions chains things together "backwards" by specifying dependencies makes it a little confusing at first, but it starts to make sense once you dig in.

The resolves part of the workflow basically points to the end of the workflow. Each action in the workflow then uses needs to specify which other actions must run successfully before it will start. So you specify the last action(s) in your workflow and chain things together backwards from there. It builds a dependency graph and executes things in the correct order, running actions in parallel when possible.

My workflow here is pretty linear so I just put the actions in the order that they run.

The first one action that runs is:

action "Build docker image" {
  uses = "actions/docker/cli@master"
  args = "build -t thraxil/antisocial:$GITHUB_SHA ."
}

That builds the docker image, tagged with the git SHA, which GitHub Actions conveniently exposes in the $GITHUB_SHA variable. As I mentioned before, with my Dockerfiles, the unit tests run during the build, so this step also serves as a good check on PRs.

The next action:

action "Deploy branch filter" {
  needs = "Build docker image"
  uses = "actions/bin/filter@master"
  args = "branch master"
}

This uses one of the built in utility actions to check what branch the action is running on. If it is master, the workflow will continue. Otherwise (it's just running a check on a PR branch), it halts the workflow, skipping all of the actions after it.

The next two are fairly self-explanatory:

action "docker login" {
  needs = "Deploy branch filter"
  uses = "actions/docker/login@master"
  secrets = ["DOCKER_USERNAME", "DOCKER_PASSWORD"]
}

action "docker push" {
  needs = ["docker login"]
  uses = "actions/docker/cli@master"
  args = ["push", "thraxil/antisocial:$GITHUB_SHA"]
}

They log us into the Docker Hub and push the docker image there. The only new bit is that the login action uses the secrets field to grant itself access to some secret settings that are stored in the GitHub project settings.

The bulk of the deploy work (that I had to do, at least) is in the next stanza:

action "deploy" {
  needs = "docker push"
  uses = "thraxil/django-deploy-action@master"
  secrets = [
     "PRIVATE_KEY",
     "KNOWN_HOSTS",
     "WEB_HOSTS",
  ]
  env = {
    SSH_USER = "anders"
    APP = "antisocial"
  }
}

That uses a custom action that I placed in its own repo so I could easily re-use it across my projects.

The great thing about GitHub Actions is that they are just Docker containers that get some specific environment variables and shared directories set up and run how you need them. GitHub Actions will find the Dockerfile in there, build it (if it hasn't already cached), and run it in the appropriate environment.

That means that it's easy to package up pretty much any common deployment tool you can think of as a GitHub Action (if someone else hasn't already done it) or just put your own together if you know how to make a Docker image and write a little shell script.

If I were doing this from scratch or if it were a more complicated deployment process, I'd probably grab or build an Ansible action and do the rest of the deployment that way. In my case though, it's only a couple steps and I pretty much already had a shell script written (see the old post). So I just made an action that pretty much runs that script in a container, with some tweaks to make it work in the new environment.

The Dockerfile is pretty minimal. Just builds off debian:stable-slim (which GitHub highly recommends to keep images small and using a common base to maximize caching), installs openssh-client (the only package we need that isn't already there) and drops in a small script.

That script should look familiar from the old blog post. It just does a few additional things to deal with the GitHub environment variables and setting up a valid SSH config. All of the variables that it needs are either set via env in the workflow config above, or they are stored in the project settings as secrets (SSH private key, etc.)

Finally, since I like to use Sentry to track exceptions and Sentry does a better job if you tell it when you deploy new code, I use a community published action to publish a new sentry release for the project.

That's basically it. I'm mostly pleased with the setup. It took me a few hours to figure out all the pieces and work through some stupid bugs (my own) to get it working how I wanted, but now it's pretty solid.

Like I said earlier, I really like that it works by just stringing together Docker containers. That means there's never any question about when GitHub Actions will support some tool. If you can stick it in a Docker image, you can use it. Configuration is straightforward once you've spent some time with it (and the Web UI is surprisingly usable and powerful).

I feel like I'm only scratching the surface of what it's capable of (I'm not even running anything in parallel). The more interesting uses will be less of this "traditional" kind of deployment pipeline and will take better advantage of the Actions' direct access to the rest of GitHub's APIs. Right now it feels like the community is still figuring out what those possibilities are and I'm excited to see what patterns emerge.

TAGS: django docker devops continuous deployment github actions

formatting is with Markdown syntax. Comments are not displayed until they are approved by a moderator. Moderators will not approve unless the comment contributes value to the discussion.

namerequired
emailrequired
url
remember info?