Skip to content

Mark Embling

Deployment with Fabric

Those of you who have been following me on Twitter will know that I've recently decided to focus on learning Python, and looking into python-based web frameworks like django and pylons. As I've been reading through the various materials about python, it strikes me that its a very clean language, and more importantly, multi-purpose. Generally my development is done in either PHP or C#, and whilst the same can be said C#, PHP has always been all about the web. Of course, it can do a lot more than that, but first and foremost it solves the problem of building web applications. To this end, I have never used it for general shell/admin duties such as deploying sites - this job has fallen me using manual deployment in the past and Capistrano more recently.

Whilst I like the solution provided by Capistrano, its always seemed a little bit strange to use a Ruby-based tool to deploy sites which are never anything to do with ruby. Yes - I'm picky. However, since I will be heading further into the world of python, I thought now is as good a time as any to see if there is a python-based alternative. The answer is yes - Fabric

At the time of writing, the current version of Fabric is 0.9a3. This means it is still somewhat of a moving target as things are changed and improved heading towards the 1.0 release. However it is more than capable in its current state and is well worth the move. Its worth noting that (again, at the current time of writing) the documentation is somewhat out of date and often the examples do not work. Definitely refer to the changes area of the documentation.

Since the "fabfile" is just a simple python file, its very easy to pick up for those with experience with the language. Each task is a simple function.

In addition, these blog posts were very useful to me (but please remember the examples are old and no longer work as-is):

However, my deployment scenario is a little bit more complicated than these examples. It is worth noting here that I am talking in particular about my PHP-based sites, although I see no reason why I will change this when dealing with django/pylons/whatever sites in the future. The flow of events is like this:

  1. Delete current site files from server
  2. Export the latest version from the subversion repository
  3. Rename production config files from production.whatever to whatever (e.g. production.config.xml to config.xml)
  4. Calculate the version number for the release. This is calculated by adding the subversion revision number to a predefined (manually updated) version string (e.g. 1.2 + 543 = 1.2.543
  5. Insert the version number into the site/config so it is able to display in the footer etc
  6. The current trunk is copied to the tags folder with the given version number (...svn/tags/1.2.543)

As can be seen from the flow of events above, there is a brief period in which the site is not available or fully functional during the deployment process. Therefore this solution is not appropriate for sites which must be high-availability. However it suits sites like mine (and most other blogs) fine.

Rather than explain every single detail, I think the links provided within this post provide all the background reading needed. Therefore without further ado, here is my final code.

from fabric.api import *
import re

app_details = {'name': 'AppName', \
               'version': '1.2'}

def production():
    env.hosts = ['my.production.server']
    env.user = 'my-username'
    env.app_details = app_details
    env.deploy_to = '/var/www/my-site'
    env.repository = {'url': 'http://my.repository/svn/trunk', \
                      'username': 'svn-username', \
                      'password': 'svn-password', \
                      'command': 'svn export --force', \
                      'tag_url': 'http://my.repository/svn/tags'}

def deploy():
    """Run the appropriate tasks for deployment"""
    clean()
    export()
    deploy_config()
    deploy_htaccess()
    record_version()
    tag_release()

def export():
    """Export the project from the Subversion repository"""
    run('%s --username %s --password %s --no-auth-cache %s %s' % \
        (env.repository['command'], env.repository['username'], env.repository['password'], env.repository['url'], env.deploy_to))

def deploy_config():
    """Rename the configuration file"""
    run('mv -f %(d)s/application/config/production.config.xml %(d)s/application/config/config.xml' % {'d': env.deploy_to})

def deploy_htaccess():
    """Rename the .htaccess/access.conf file"""
    run('mv -f %(d)s/www/production.access.conf %(d)s/www/access.conf' % {'d': env.deploy_to})

def record_version():
    """Writes out the version number for the deployment to the app"""
    rev = get_revision_number()
    version = '%s.%s' % (env.app_details['version'], rev)
    run('sed \'s/const VERSION = 0;/const VERSION = "%(v)s";/g\' %(d)s/application/Bootstrap.php > %(d)s/application/Bootstrap.php.tmp; mv -f %(d)s/application/Bootstrap.php.tmp %(d)s/application/Bootstrap.php' % \
        {'v': version, 'd': env.deploy_to})

def tag_release():
    """Tag the release in the SVN repository with the version number"""
    rev = get_revision_number()
    version = '%s.%s' % (env.app_details['version'], rev)
    run('svn copy --username %(u)s --password %(p)s --no-auth-cache --message \'Tagging release %(v)s.\' %(url)s %(tag_url)s/%(v)s' % \
        {'u': env.repository['username'], 'p': env.repository['password'], 'v': version, 'url': env.repository['url'], 'tag_url': env.repository['tag_url']})

def clean():
    """Delete the original application in preparation for new deployment"""
    run("rm -rf %s" % (env.deploy_to,))

def restart():
    """Restart apache on the server"""
    sudo('/etc/init.d/apache2 restart')

def get_revision_number():
    """Helper method for getting the current revision number from the SVN repository"""
    rev = local('svn info --username %s --password %s --no-auth-cache %s | grep ^Revision' % \
              (env.repository['username'], env.repository['password'], env.repository['url']))
    match = re.search(r'(\d*)$', rev)
    rev = match.group(0)
    return rev

It is worth noting that the env used above used to be known as config. In addition, most examples on blogs and so on do not import the fabric api - these are both breaking changes from older versions and worth bearing in mind when reading older posts about fabric.

I'm sure this script can be improved upon - bear in mind it is the first version and may have some unforeseen bugs. However, it has deployed the latest version of this site just fine :). Feel free to post comments and questions. And last but not least, a big thank you goes out to the authors and contributors of fabric.