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:
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.
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.
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
root:~# fab deploy