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
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.