Building an Environment From Scratch With Capistrano 2

I recently attended the Pragmatic Programmers Advanced Rails Studio. Overall, I thought it was great. Even though I've had personal experience in most of the topics covered, it filled in a lot of gaps in my personal knowledge. I came out of the three days feeling more well-rounded as a Rails developer. I also met some cool people.

On the topic of deployment, Chad Fowler offered that Capistrano is really just an automated remote shell. That got me thinking: if you could do anything through Capistrano that you could do on the command line, you could not only automate the deployment of the application, but the construction and configuration of the environment in which that application runs.

I rebuilt my 256 slice at Slicehost with a brand new Ubuntu Hardy 8.04 install to test this out. I wanted to see if I could automate the installing and configuring of everything it would take to run a Rails application through Apache and Passenger. It worked! What follows are the steps I took to make it happen.

Setup SSH and sudo

I started with a blank application, and "capified" it. If you don't have Capistrano installed, install it via RubyGems.

 sudo gem install capistrano --no-ri --no-rdoc rails slicehost cd slicehost/ capify . 

Open up config/deploy.rb and start configuring it as you would for any other Rails application. Just make sure you have the same "default_run_options" line as I do. Here's what mine looks like:

 default_run_options[:pty] = true set :application, "slicehost" set :repository, "git://github.com/mig/cthulhu.git" set :deploy_to, "/home/deploy/#{application}" set :user, "deploy" set :scm, :git role :app, "myslice.com" role :web, "myslice.com" role :db, "myslice.com", :primary => true 

I've listed "deploy" as my user. This is important – the only bit of manual configuration we will do is to create this user account.

Now I will ssh into my brand new slice, change my password, and create the "deploy" user:

 ssh myslice.com -l root passwd adduser deploy 

The "deploy" user should get sudo privileges. To make things easier for this example, don't require a password. While this is fine for my DMZ-like slice, you should not leave the NOPASSWD flag on your user after finishing – you have been warned.

 visudo 

Add this line to the bottom:

 deploy ALL=(ALL) NOPASSWD: ALL 

One last thing, add your public key to the deploy user's authorized_keys file:

On your local machine:

 scp ~/.ssh/id_rsa.pub deploy@myslice.com: ssh myslice.com -l deploy 

On your slice:

 mkdir .ssh cat id_rsa.pub >> .ssh/authorized_keys rm id_rsa.pub chmod 600 .ssh/authorized_keys chmod 700 .ssh 

Now on to the fun stuff...

Installing the Basics

Open up config/deploy.rb and create a custom namespace for our tasks:

 namespace :slicehost do # tasks go here end 

I'm going to use Ubuntu's apt-get to install some of the basic necessities. Put these tasks in our :slicehost namespace. I've included tasks for git and sqlite3 here, you might want something different like subversion and postgres. Look at the attached file at the end for more examples.

 desc "Update apt-get sources" task :update_apt_get do sudo "apt-get update" end desc "Install Development Tools" task :install_dev_tools do sudo "apt-get install build-essential -y" end desc "Install Git" task :install_git do sudo "apt-get install git-core git-svn -y" end desc "Install SQLite3" task :install_sqlite3 do sudo "apt-get install sqlite3 libsqlite3-ruby -y" end 

To run any of these, drop to your shell and issue a cap command:

 cap slicehost:update_apt_get 

To see a list of available cap commands, including our custom ones:

 cap -T 

Install the Rails Stack

The next example is only a little more complex, the thing you should note is the use of sudo within the command string itself. Include the && between commands if you need a command to run in a directory other than the default.

Let's install Ruby and Rails:

 desc "Install Ruby, Gems, and Rails" task :install_rails_stack do [ "sudo apt-get install ruby ruby1.8-dev irb ri rdoc libopenssl-ruby1.8 -y", "mkdir -p src", "cd src", "wget http://rubyforge.org/frs/download.php/29548/rubygems-1.0.1.tgz", "tar xvzf rubygems-1.0.1.tgz", "cd rubygems-1.0.1/ && sudo ruby setup.rb", "sudo ln -s /usr/bin/gem1.8 /usr/bin/gem", "sudo gem install rails --no-ri --no-rdoc" ].each {|cmd| run cmd} end 

Just run the new installer task and you should be ready to go:

 cap slicehost:install_rails_stack 

That was easy! We've got a full Rails stack running on our slice. From here we could go a few different routes. I've been eager to try the new mod_rails Passenger out, so let's set that up!

Apache and Passenger (aka mod_rails)

First, we need to install Apache:

 desc "Install Apache" task :install_apache do sudo "apt-get install apache2 apache2.2-common apache2-mpm-prefork apache2-utils libexpat1 apache2-prefork-dev libapr1-dev -y" end 

And now the Passenger install. This part is trickiest, because it requires our input on the remote server. This is where the "default_run_options" setting comes in handy.

 desc "Install Passenger" task :install_passenger do run "sudo gem install passenger --no-ri --no-rdoc" input = '' run "sudo passenger-install-apache2-module" do |ch,stream,out| next if out.chomp == input.chomp || out.chomp == '' print out ch.send_data(input = $stdin.gets) if out =~ /enter/i end end 

Here's what's happening: the run command is passed a block, the block is telling the run command to step through all output while printing everything to the screen. Any time the words "Enter" or "ENTER" are encountered in the output, the execution waits for our input. Any input is then redirected back into the Passenger installer running on the remote server.

So, we've finished installing all the software we need (for the moment). All that's needed is to configure Apache to use Passenger, and to set up a virtual host for our application.

Apache Configuration

Here is the Apache configuration, taken directly from the output of the Passenger install:

 desc "Configure Passenger" task :config_passenger do passenger_config =<<-EOF LoadModule passenger_module /usr/lib/ruby/gems/1.8/gems/passenger-1.0.1/ext/apache2/mod_passenger.so RailsSpawnServer /usr/lib/ruby/gems/1.8/gems/passenger-1.0.1/bin/passenger-spawn-server RailsRuby /usr/bin/ruby1.8 EOF put passenger_config, "src/passenger" sudo "mv src/passenger /etc/apache2/conf.d/passenger" end desc "Configure VHost" task :config_vhost do vhost_config =<<-EOF <VirtualHost *:80> ServerName blog.pggbee.com DocumentRoot #{deploy_to}/public </VirtualHost> EOF put vhost_config, "src/vhost_config" sudo "mv src/vhost_config /etc/apache2/sites-available/#{application}" sudo "a2ensite #{application}" end 

That looks more complicated that it really is. The trick to these tasks is the "put" method – it takes a string and a remote filename and uploads the contents of the string to the file on the remote server.

This allows us to generate the configurations locally, create them on the remote server, and then use sudo to move them into their proper place.

That's it. Our slice is ready for us to deploy as normal. I am not going to cover that here, as it's been covered elsewhere and in much more depth than I can go into here.

Wrapping Up

Once you've created all your custom tasks and verify that they work, it's a good idea to put them all together in one setup method (I use :setup_env in my :slicehost namespace). All I have to do is run the task, sit back, and watch:

 cap slicehost:setup_env 

Try it out for yourself. Download the entire deploy.rb.txt file, remove the .txt extension and drop it into your project.

Matt Swasey

,
Posted in Article Category: #Code
on