Since Voxel hosts some of the web’s most widely read, most highly trafficked websites, we run across (and thrive on) complex, multi-server, multi-stage deployments. This means that we’re continually trying to find the best way to automate releases into these environments. Recently, we were tasked with figuring out the best way to push code updates onto a large web cluster running a popular Ruby on Rails application. Here’s what we did:

Capistrano Deployment

Aside from being a being a pretty and colorful picture, there’s a lot happening here. In this example, we have five web/app servers that are running the Rails app (let’s call it “myapp”), two database servers setup in master/slave replication, one staging server, one testing server, and one SVN server. The load balancer distributes traffic evenly between the web/app servers.

On the web/app servers we are using mongrels to serve up HTTP to Apache, and using god to monitor mongrels and make sure our application is happy and responding. Think it’s going to be hard to push code updates to this cluster? Think it’s going to be hard to add more web/app servers and keep everything up to date? Think again. We can setup new web/app servers and update code on existing servers with one command:

cap production deploy

I’m getting ahead of myself. First, how do we setup the Capistrano server? Here’s a post-install script for you:

# make sure we can build gems
yum install -y ruby ruby-libs ruby-devel ruby-rdoc libxml2 libxml2-devel gcc

# install ruby gems
wget http://rubyforge.org/frs/download.php/60718/rubygems-1.3.5.tgz
tar -zxvf rubygems-1.3.5.tgz
cd rubygems-1.3.5
ruby setup.rb install

# using gems, install everything we need
gem install rails capistrano xml-simple libxml-ruby capistrano-ext

# install san_juan which extends capistrano functions to god
gem install jnewland-san_juan -s http://gems.github.com

# install the hapi client for ruby
wget http://api.voxel.net/clients/ruby/ruby-hAPI-1.0.5.tar.gz
tar -zxvf ruby-hAPI-1.0.5.tar.gz
cd ruby-hAPI
sed -i "s/ 'ParaOpts' => '>= 1.0.0',//g" hapi.gemspec
gem build hapi.gemspec
gem install hapi

# setup our capistrano application and capify it
mkdir -p /var/local/www/
cd /var/local/www/
rails capistrano
cd capistrano
capify .

This will setup a new server with a new capistrano app in /var/local/www/capistrano/ (let’s call it {cap_dir} for short). This relies on the capistrano-ext gem which allows for multistage deployment. Now, setup your environments by changing your {cap_dir}/config/deploy.rb file to this:

set :stages, %w(testing staging production)
require 'capistrano/ext/multistage'

By setting up multiple stages, this will allow you to execute commands in certain environments, like `cap staging deploy` or `cap production migrate`. The environment-specific capfiles are located in {cap_dir}/config/deploy/{environment}.rb. Let’s take a look at production.rb:

require 'san_juan'
require 'hapi'

# the environment, globally
$environment = "production"

# the application name
set :application, "myapp"

# where we're deploying to on the web/app servers, and set it globally
set :deploy_to, "/var/local/www/myapp"
$deploy_to = deploy_to

# set hapi credentials so we can get an up-to-date list of servers we're deploying to
# the preferred method of authentication is to use hAPI authkeys. more information
# can be found here
set :hapi_username, "hapiusername"
set :hapi_password, "hapipassword"

# where the repository is currently at
set :repository,  "svn://svn.mydomain.com/svn/myapp/branches/trunk"

# how we'll be getting the code from the repository
set :scm, :subversion

# specify the credentials for the repository
set :scm_username, "svnusername"
set :scm_password, "svnpassword"

# this ssh user needs to be able to login on every web/app/db server and have
# permissions in the $deploy_to directory
set :runner, "myapp_deployment"

# don't use sudo (make sure the :runner user has $deploy_to directory permissions)
set :use_sudo, false

# set this server manually so the rake db:migrate gets run properly, and only once
role :db, "app1.mydomain.com", :primary => true

###################################################
# end configuration, start running
###################################################

# use the remote copy and update instead of checking out the entire app every time
set :deploy_via, :remote_cache

# make sure we don't copy files we don't want or need
set :copy_exclude, [".svn", ".DS_Store"]

# authenticate with hapi
hapi = HAPI.new( { :hapi_username => hapi_username, :hapi_password => hapi_password } )

# get all devices from hapi
labels = hapi.voxel_devices_list.map { |d| d['label'] }

# IMPORTANT:
# this will assign the roles of app and web to any device whose label matches "app", like
# "app1.mydomain.com" and "app24.mydomain.com". It's important that there aren't other
# servers that match this regular expression, and it's also VERY important that the labels
# are resolvable FQDNs because capistrano will connect via ssh (e.g. `ssh {$label}`).
# in this case, the app servers act as the web servers as well. If they didn't, you could edit
# this to regex on "web" (or whatever you'd standardized on)
role(:app) { labels.select { |l| l =~ /app/ } }
role(:web) { labels.select { |l| l =~ /app/ } }

# specify which servers need the mongrels cluster stopped and started
# IMPORTANT: this name (e.g. "mongrels") needs to match the w.group = 'mongrels'
# in the config.god on the web/app servers. Click here for more
# information. Search for "grouping watches" to see where and how to
# specify this information.
san_juan.role :app, %w(mongrels)

###################################################
# end setup, start code
###################################################

# before trying to update our app, make sure the app is actually setup on this server
before "deploy:update" do
	# check to see if the releases directory exists
	run "if [ -d '#{$deploy_to}/releases' ]; then echo yes; else echo no; fi;" do |channel, stream, data|
		# if it's not setup
		if data.strip != "yes" then
			# deploy the app
			`cap #{$environment} deploy:setup`
		end
	end
end

# overload some methods to use god to stop and start mongrels on the web/app servers
namespace :deploy do
	desc <<-DESC
		Start the mongrels cluster on the web/app server using god
	DESC
	task :start do
		god:app.mongrels.start
	end
	desc <<-DESC
		Stop the mongrels cluster on the web/app server using god
	DESC
	task :stop do
		god:app.mongrels.stop
	end
	desc <<-DESC
		Restart the mongrels cluster on the web/app server using god
	DESC
	task :restart do
		deploy:stop
		deploy:start
	end
end

Issuing a `cap production deploy` from the Capistrano server will login to each of the web/app servers and update the code to the latest version and restart mongrels using god. For a detailed description of what happens when you call `deploy` see here. Make sure to setup ssh_keys for the “myapp_deployment” user on all servers so capistrano won’t prompt for a password. (The SSH keys tutorial I linked to leaves one thing out: The authorized_keys file needs to be chmod 0600 and the ~/.ssh/ directory needs to be chmod 0700.)

It might take an hour or so to set this up and test it properly, but it will save you countless hours in the long run. Instead of logging into each server and issuing a set of commands, you can sit back and issue one deploy command. This is especially helpful in multi-stage, multi-server environments.

Posted in General Posts | No Comments »

Leave a Reply

Make a Comment

Your Name

Email

Homepage

Comment