Ampelofilosofies

homeaboutrss
The mind is like a parachute. If it doesn't open, you're meat.

Capistrano: Programmatically adjusting task execution based on roles

15 Feb 2008

Capistrano started as the Rails deployment tool. Really impressive stuff, like automatic rollback of all software to the previous version if anything went wrong (with a good use of svn) and several limitations (too Rails-centric, too SVN-centric). Capistrano 2 (currently at 2.1.0) went back to the basics, split the Rails stuff and defined a very nice subset for doing remote operations on many servers. As always, my use case deviates from the normal.

The idea is to be able to say

 cap deploy:some_package DEPLOY_TO=some_node

and have Capistrano assemble and configure the package, copy it and install it. Lets build some vocabulary first:

  • A package is a set of properly configured files that can be used to install software on a server. Each package defines a set of parameters that are used in it’s configuration (things like connection strings, usernames etc.)

  • A node is a file containing all the parameters with the correct values for a particular server.

A simplified scenario would be an application package A and a database package D. They can be deployed on separate servers (two nodes each with the parameters for the corresponding package) or on the same server (a single node with the parameters for both packages).

Now supposed we have sold our super duper application to 2 clients, each of them has setup 2 servers and they wait for us to deploy.

The simple way would be to define the server to deploy to on the command line (i.e. using capistrano variables ‘cap -s’), or edit the capfile everytime and assign the correct ip address to each role. The more releases - and clients - you get, the more “mühsam” this is (mühsam means laborious in german). In other words, the process won’t scale. Given the definition above, the address of the server should be part of the node information.

Luckily, Capistrano uses lazy evaluation for most of it’s functionality, meaning values are calculated when they are needed and not when the script loads. This means that we can assign a task to a role and define/modify the role just before the first remote operation is executed

 task :example, roles=>:important do 
   role :important, "important@server"
   run("start important example")
 end

Caveat: Calling role does not replace the values for the role, it just adds new values, so the following

 role :important, "important@server"
 task :example, roles=>:important do
   role :important, "important@other.server"
   run("start important example")
 end

means that the ‘important example’ runs on important@other.server and important@server.