Deploying Code with Python's Fabric

After using Capistrano for deployment for the past four years, I recently came across the Python Fabric library. Fabric is flexible, lightweight, and easy to get up and running. I've put together an example Fabric script that shows how easy it is to get full featured deployments up and running. This script runs in Python, but can deploy code of any language. It was tested on a minty fresh Debian Wheezy VM.

The deployment pattern I'm using is:

  1. On a deployment box, update local copy of the code from version control
  2. Upload a zip archive of code to remote host servers and unzip code
  3. Run final pre-deployment tasks
  4. Make code live
  5. Run post deployment tasks
  6. Cleanup

Instead of extracting new files over old files, or deleting old files and then extracting new files, I've added some functionality that allows for near atomic switch between old and new code. This is accomplished by pointing the docroot of your web server at a symlink to the "current" latest code. /var/www/projectx/current, for example in the script below. During deployment, the script creates a new folder named with the datetime of the deployment in a releases archive folder that you specify: /var/www/projectx/releases/20131013051253/. After all the code is extracted, the symlink pointing to the old code at is replaced with a new symlink pointing to the new datetime named folder, effectively making the new code live all at once.

Dependencies

On the host managing the deployment you will need to install Python, Fabric, Zip.

apt-get install python-pip zip unzip
pip install fabric

If you run into a problem with the above "pip install fabric" command, install the following package:

apt-get install python-dev

On the hosts you are deploying to, you will need to install Unzip.

apt-get install unzip

Also on the hosts you are deploying to, you will need to create a folder that holds your project, /var/www/projectx/ in the script below and a child folder to hold all past releases. This child folder is /var/www/projectx/releases/ in the script below. You may want to create a new user specifically to access these hosts during the deploy. In the example below, this user is deploy.

Another item you'll want to change in the script below is the env.hosts list, which holds all the hosts you are deploying code to.

Script

This script must be named fabfile.py, as per Fabric convention, and is best stored in the root of your project in version control.
from fabric.api import local, run, env, put
import os, time

# remote ssh credentials
env.hosts = ['10.1.1.25']
env.user = 'deploy'
env.password = 'XXXXXXXX' #ssh password for user
# or, specify path to server public key here:
# env.key_filename = ''

# specify path to files being deployed
env.archive_source = '.'

# archive name, arbitrary, and only for transport
env.archive_name = 'release'

# specify path to deploy root dir - you need to create this
env.deploy_project_root = '/var/www/projectx/'

# specify name of dir that will hold all deployed code
env.deploy_release_dir = 'releases'

# symlink name. Full path to deployed code is env.deploy_project_root + this
env.deploy_current_dir = 'current'

def update_local_copy():
	# get latest / desired tag from your version control system
	print('updating local copy...')

def upload_archive():
	# create archive from env.archive_source
	print('creating archive...')
	local('cd %s && zip -qr %s.zip -x=fabfile.py -x=fabfile.pyc *' \
		% (env.archive_source, env.archive_name))

	# create time named dir in deploy dir
	print('uploading archive...')
	deploy_timestring = time.strftime("%Y%m%d%H%M%S")
	run('cd %s && mkdir %s' % (env.deploy_project_root + \
		env.deploy_release_dir, deploy_timestring))

	# extract code into dir
	print('extracting code...')
	env.deploy_full_path = env.deploy_project_root + \
		env.deploy_release_dir + '/' + deploy_timestring
	put(env.archive_name+'.zip', env.deploy_full_path)
	run('cd %s && unzip -q %s.zip -d . && rm %s.zip' \
		% (env.deploy_full_path, env.archive_name, env.archive_name))

def before_symlink():
	# code is uploaded, but not live. Perform final pre-deploy tasks here
	print('before symlink tasks...')

def make_symlink():
	# delete existing symlink & replace with symlink to deploy_timestring dir
	print('creating symlink to uploaded code...')
	run('rm -f %s' % env.deploy_project_root + env.deploy_current_dir)
	run('ln -s %s %s' % (env.deploy_full_path, env.deploy_project_root + \
		env.deploy_current_dir))

def after_symlink():
	# code is live, perform any post-deploy tasks here
	print('after symlink tasks...')

def cleanup():
	# remove any artifacts of the deploy process
	print('cleanup...')
	local('rm -rf %s.zip' % env.archive_name)

def deploy():
	update_local_copy()
	upload_archive()
	before_symlink()
	make_symlink()
	after_symlink()
	cleanup()
	print('deploy complete!')

You can also clone this on GitHub Gists: https://gist.github.com/elliottb/7744008

Usage

root:~# fab deploy