Automated Django Deployment with Fabric

I used Capistrano before to automate my deployments, but something about using a Ruby application to deploy Django projects feels awfully wrong. So I was happy to see Fabric emerge as an alternative, and I finally put it to good use while deploying this blog.

Fabric is still a very young project, and there isn’t much documentation available. The trunk version is quite different from the latest release (0.0.9 right now), but I decided to stick with the release anyway since I didn’t feel like digging into the source code to gather the latest docs. Installation on Ubuntu was almost painless thanks to easy_install. One of the dependencies, pycrypto failed during the installation, spitting out some gcc errors because I didn’t have the Python development headers installed. So I had to get those as well:

sudo apt-get install python2.5-dev
sudo easy_install Fabric

When you run it, Fabric looks for a configuration file called a “fabfile” in the same directory. You define your environment variables and deployment procedures in a Pythonic way. It’s very similar to Capistrano, but simpler, and I like its approach a lot better than Capistrano. There are only a handful of methods that you can call:

I don’t have a staging environment for this blog, only my production box. So I started by creating a simple production method:

def production():
"Set the variables for the production environment"
set(fab_hosts=["10.0.0.1"])
set(fab_user="deploy")
set(remote_dir="/path/to/dir")

fab_hosts and fab_user are two variables used by Fabric to figure out how to connect to the remote server(s). remote_dir is a custom variable I put in to store the path to my working directory on the server.

The next step is to build a deploy method, this can be as simple as a git pull on the remote box, but I wanted to go a bit further than that. My requirements were like this:

Pulling a specific commit from the git repo and exporting it as an archive was my biggest concern. After a little bit of research, I stumbled upon git archive. This command basically lets you export a git tree from a local or remote repository and tarball it. You can specify which commit you want, you can even tell it to only export certain directories in the repo. I ended up using something like this:

git archive -v --format=tar --prefix=deploy/ v0.0.2 conf build | gzip > tmp/archive.tar.gz

This command exports the specified directories from the local repo at the v0.0.2 tag, tarballs them, then compresses the file with gzip. I also looked into using the —remote option, but GitHub doesn’t seem to support this functionality - I stopped researching after seeing this post from their support team.

The next step was to build the tedious deployment procedure in the fabfile. It took a bit of trial and error, and a couple of failed deployments, but here it is:

def deploy(hash="HEAD"):
"Deploy the application by packaging a specific hash or tag from the git repo"
# Make sure that the required variables are here
require("fab_hosts", provided_by=[production])
require("fab_user", provided_by=[production])
require("remote_dir", provided_by=[production])

# Set the commit hash (HEAD if not given)
set(hash=hash)

# Create a temporary local directory, export the given commit using git archive
local("mkdir ../tmp")
local("cd ..; git archive -v --format=tar --prefix=deploy/ $(hash) \
conf build/libs build/taylanpince | gzip > tmp/archive.tar.gz")

# Upload the archive to the server
put("../tmp/archive.tar.gz", "$(remote_dir)/archive.tar.gz")

# Extract the files from the archive, remove the file
run("cd $(remote_dir); tar -xzvf archive.tar.gz; rm -f archive.tar.gz")

# Move directories out of the build folder and get rid of it
run("mv $(remote_dir)/deploy/build/* $(remote_dir)/deploy/")
run("rm -rf $(remote_dir)/deploy/build")

# Create a symlink for the Django settings file
run("cd $(remote_dir)/deploy/taylanpince; ln -s ../conf/settings.py settings_local.py")

# Move the uploaded files directory from the active version to the new version, create a symlink
run("mv $(remote_dir)/app/files $(remote_dir)/deploy/files")
run("cd $(remote_dir)/deploy/taylanpince/media; ln -s ../../files")

# Remove the active version of the app and move the new one in its place
run("rm -rf $(remote_dir)/app")
run("mv $(remote_dir)/deploy $(remote_dir)/app")

# Restart Apache
sudo("/usr/sbin/apachectl graceful")

# Remove the temporary local directory
local("rm -rf ../tmp")

It’s pretty self explanatory, basically a lot moving stuff around, symlinking and such. Finally, I can deploy by just typing this:

fab production deploy

And I can do tagged deployments like this (I can also give a specific commit hash instead of a tag):

fab production deploy:hash=v0.0.2

Simply beautiful. I am definitely going to make the Fabric deployment a default part of my process from now on.

Update: Forgot to mention this, Will Larson’s Deploying Django with Fabric post is also an excellent guide, and he uses the latest version from trunk.