Deployment Magic with Capistrano

Programming with Ruby and developing Web apps with Rails is fun; Obviously so, or you wouldn’t be reading about deployment issues. Deployment can be challenging and especially so if you come from more of a development background than a systems administration background. Fortunately, I’m tacking this from the latter camp, so I am going to present excerts of Capistrano deployment configure below.

Deployment Summary

My objective is to configure Capistrano to take a virgin box and install and configure all necessary services. Such a setup is no doubt specific to each environment.

The platform targeted is Debian GNU/Linux Sarge. The database server is PostgreSQL 8.1. The Web server is running Apache 2.2 using mod_proxy_balancer. The application server is Mongrel.

Two environments are targeted: QA and production.

Assumptions

For the process to proceed smoothly, a few assumptions are made about the configuration of each system, besides the target platform and decided upon software. First, it is assumed there is a deployment user account and that the deployment initiator has key trust with said account.

Second, it is assumed the deployment user has fairly extensive sudo access, which is to say it’s effectively root. Second, it is assumed that Subversion is your SCM and that it is available via the svn+ssh transport. (It would be trivial to change to WebDAV, though, assuming you have that configured.)

As an example of achieving the user configuration portion of this.

# adduser --system --home /home/deploy --shell /bin/bash --ingroup nogroup deploy
chmod u+w /etc/sudoers
echo "deploy  ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
chmod u-w /etc/sudoers

Configuration

The initial deployment environment must first be defined.

require 'lib/tasks/capistrano_svn_tags.rb'

deploy_to_env = ENV["RAILS_ENV"] ||= 'qa'

set :application, "vrl"
set :repository, "svn+ssh://nebula.gohideaway.com/home/svn/#{application}"
set :deploy_to, '/srv/rails'

set :svn_port, 5555
set :svn_user, 'jasonb'

set :db_name, "#{application}_#{deploy_to_env}"
set :db_user, 'thedbuser'
set :db_passwd, 'thedbpasswd'
set :db_allow_hosts, {'qa' => '10.0.1.0/24', 'production' => '127.0.0.1/32'}

set :mongrel_port, 8100
set :mongrel_servers, 3

ssh_options[:keys] = %w(/home/user/.ssh/id_deploy /home/user/.ssh/id_dsa)
ssh_options[:username] = 'local_user_with_svn_access'

Some options are from stock Capistrano, many are not.

I prefer to deploy by tag or branch rather than whatever features are creeping into trunk, so I have included capistrano_svn_tags.rb. Further, it’s common in many environments to have a staging or QA environment that’s essentially identical to the production environment. deploy_to_env is used later to determine which kind of deployment is desired.

The svn_port and svn_user options allow Subversion to tunnel its way back to my internal Subversion server using svn+ssh. Access is further restricted to a specific IP range via a firewall configuration. They’re used later in conjunction with a template installed in the deployment user’s .subversion/config configuration file.

The database options allow a database user and password to be created and fed into PostgreSQL. The /etc/postgresql/8.1/main/pg_hba.conf is further modified to ensure access via TCP is properly restricted. (listen_addresses may need to be tweaked in /etc/postgresql/8.1/main/postgresql.conf.)

Now, I must ensure the correct hosts are targeted and the appropriate rails_env is selected.

if deploy_to_env == 'qa'
        set :rails_env, 'qa'
        role :web, "deploy@qa.gohideaway.com"
        role :app, "deploy@qa.gohideaway.com"
        role :db,  "deploy@qa.gohideaway.com", :primary => true
else
        set :rails_env, 'production'
        set :checkout, 'export'
        role :web, "deploy@www.gohideaway.com"
        role :app, "deploy@www.gohideaway.com"
        role :db, "deploy@www.gohideaway.com", :primary => true
end

Building a Host

I chose to piggyback the host environment tasks to task setup.

task :before_setup do
        setup_core
end

task :after_setup do
        apache_conf
        mongrel_conf
end

The purpose of setup_core is simply to install all the necessary software for each node.

task :setup_core do
        setup_stack
        setup_web
        setup_app
        setup_db
end

For example, setup_web will install Apache 2.2 and enable several modules necessary for mod_proxy_balancer to function.

task :setup_web, :roles => [:web] do
        mods = %w{rewrite proxy_connect proxy_http proxy_balancer}
        current_task.servers.each do |host|
                sudo "apt-get -qq update"
                sudo "apt-get -qq install apache2"
                run <<-CMD
                        for mod in #{mods.join(' ')} ; do
                                if [ ! -h /etc/apache/mods-enabled/${mod}.load ] ; then
                                        sudo /usr/sbin/a2enmod $mod ;
                                fi
                        done
                CMD
                sudo "/etc/init.d/apache2 reload || true"
        end
end

More strangely, perhaps, is the configuration of PostgreSQL. Notice the nested quote action for the SQL. Illustrated is the only safe way of handling the quoting.

task :setup_db, :roles => [:db] do
        current_task.servers.each do |host|
                sudo "apt-get -qq update"
                hack = <<-HACK
                        if [ -f /var/lib/dpkg/info/ubuntu-minimal.list ] ; then
                                sudo apt-get -qq install postgresql-8.1 postgresql-client-8.1 ;
                        else
                                sudo apt-get -qq install postgresql-8.1 postgresql-client-8.1 ssl-cert/sarge-backports ;
                        fi
                HACK
                run hack

                # Check for role existence.
                #
                # On Debian it is necessary to assume the role of the
                # postgres user to have blanket access without a password
                # using a local UNIX domain socket.

                create_role = true
                cmd = <<-CMD
                        su - postgres -c "/usr/bin/psql -Upostgres -c
                        \\\"SELECT rolname FROM pg_roles WHERE rolname = '#{db_user}'\\\"
                         template1"
                CMD
                sudo cmd do |c,s,d|
                        if create_role
                                puts d
                                if d =~ /#{db_user}/ then create_role = false end
                        end
                end
                puts create_role

                cmd = <<-CMD
                        su - postgres -c "/usr/bin/psql -Upostgres -c
                        \\\"CREATE ROLE #{db_user} WITH PASSWORD '#{db_passwd}'LOGIN\\\"
                         template1"
                CMD
                if create_role
                        sudo cmd
                end

Later, a Debianized version of Mongrel is configured.

task :mongrel_conf, :roles => [:app] do
        # Obviously assumes you deploy from your Rails root.
        put(render(:template => File.read('config/mongrel.conf'),
                :servers => mongrel_servers,
                :port => mongrel_port,
                :railsroot => "#{deploy_to}/current",
                :env => rails_env),
                "#{deploy_to}/conf/mongrel.conf", :mode => 0640)

        sudo "ln -nsf #{deploy_to}/conf/mongrel.conf /etc/mongrel/sites-enabled"
        restart_mongrel
end

Clearly, Capistrano’s task system is quite flexible. My only disappointment is having to hop into my RAILS_ROOT to start my deployment, but then with my various configuration files checked into svn, it’s perhaps easier that way.

Other Magic

When deploying a tag, I must ensure that uploaded photos are not lost. Capistrano makes tasks like this fairly simple by providing shell access.

task :save_photos do
if rails_env == 'production' || rails_env == 'qa'
         cmd = <<-CMD
                 if [ ! -d #{deploy_to}/shared/photos ] ; then
                         sudo mkdir -p #{deploy_to}/shared/photos ;
                         sudo chown deploy:www-data #{deploy_to}/shared/photos ;
                         sudo chmod g+ws #{deploy_to}/shared/photos ;
                 fi ;
                 if [ ! -L #{deploy_to}/current/photos -a -d #{deploy_to}/current/photos ] ; then
                         echo "photos/ is a directory!" ;
                         exit 1 ;
                 fi ;
                 if [ ! -L #{deploy_to}/current/public/photos ] ; then
                         sudo ln -nsf #{deploy_to}/shared/photos \
                         #{deploy_to}/current/public/photos ;
                 fi ;
         CMD
         run cmd
end
end

Security Considerations

A number of steps have been taken in an attempt to secure the system.

The deploy system account with only SSH key trust permited for login is used strictly for deployment, as it requires sudo to perform some actions and is effectively a limted root shell as a result. Instead, Apache and Mongrel run as www-data. (In my configuration, su is granted to the deploy user via sudo, effectively providing ProgreSQL superuser access to deploy.)

The database connection is tunneled over TLS with a md5′d password for accessing the database over TCP. Only the postgres user has unchallenged local access, obtainable only by using su as root and assuming the euid of the postgres user.

One Comment

  1. Posted 6/11/2007 at 3:30 pm | Permalink

    This is cool. Now all you need is like 100 customers to buy rails websites (or 1000) and your automation will come in handy. Linux stuff is pretty fun.

Post a Comment

Your email is never shared. Required fields are marked *

*
*