Remote Rake Tasks with Capistrano

Here’s the punch line:


namespace :rake do
  task :show_tasks do
    run("cd #{deploy_to}/current; /usr/bin/rake -T")
  end
end

This is a Capistrano task which invokes Rake on the remote server. It’s the obvious answer to a problem which has been vexing me for weeks. I’ve been trying, without success, to convert a nice little Rake task


require 'lib/hodel_3000_compliant_logger'

namespace :log do
  desc "Annotate the logfile with a message. Options LOGFILE=xxx, MSG=xxx"
  task :annotate do
    logfile = ENV['LOGFILE'] || “log/production.log”
    puts “annotating #{logfile} with #{ENV['MSG']}”
    logger = Hodel3000CompliantLogger.new(logfile)
    logger.debug “ANN_#{ENV['MSG'].to_s} (at #{Time.now.strftime(”%Y-%m-%d %H:%M:%S”)})”
  end
end

WTF Why?

into a Capistrano task so that I could run it on the remote server. Yes, I can hear some of you laughing. You already know what I finally realized last night. Capistrano is not Rake. Capistrano is not even Rake-at-a-distance. Capistrano might bear a superficial resemblance to Rake with its block-style syntax and namespaces, they might both be tools for automation written in Ruby, but Capistrano is not Rake.

Capistrano knows where all your servers are, it knows how to log into them and talk to them. But, Capistrano is kind of shy around your beautiful shiny servers. It just does boring low-level things like create directories and symlink files. That’s it. It’s kind of obvious in retrospect, since Capistrano is a general tool for automating deployment, that it shouldn’t be able to do things natively in Ruby on the remote server because it shouldn’t even care if Ruby is installed on the remote server.

The nice thing is, once you get this distinction straight (and apologies if I am the only one so confused and this is boringly obvious to everyone else), it becomes easy to work out which sort of task should be done in Capistrano and which in Rake. I currently have a Rake task which logs into my servers and downloads backup files. This should really be in Capistrano, not Rake, since it doesn’t really require any Ruby fanciness and Capistrano already knows where my servers are. The Rake task I just showed you for annotating log files should stay as a Rake task since it needs Ruby.

Furthermore, once you put Capistrano and Rake together you can develop gorgeous, sexy, intricate Rake tasks on your local development machine, and then you can invoke those very same tasks (which you have tested and invoked many times in development) remotely.

So, to get started and make sure things are working as they should, let’s list the rake tasks available on the remote server:


Anas-Macbook:~/work/myapp/trunk ana$ cap rake:show_tasks
  * executing `rake:show_tasks'
  * executing "cd /var/www/apps/myapp/current; /usr/bin/rake -T"
    servers: ["myapp.com"]
    [myapp.com] executing command
 ** [out :: myapp.com] (in /var/www/apps/myapp/releases/20071230002007)
 ** [out :: myapp.com] rake db:migrate                # Migrate the database through scripts in db/migrate. Target specific version with VERSION=x
 ** [out :: myapp.com] rake db:schema:dump            # Create a db/schema.rb file that can be portably used against any DB supported by AR
 ** [out :: myapp.com] rake db:schema:load            # Load a schema.rb file into the database
…
 ** [out :: myapp.com] rake tmp:sessions:clear        # Clears all files in tmp/sessions
 ** [out :: myapp.com] rake tmp:sockets:clear         # Clears all files in tmp/sockets
    command finished

This should be more or less the same list as on your development machine. If the task you want to run is missing, then you might be missing a gem or file which the rake task depends on. (And be sure you have actually deployed the latest version of your code.) Rake won’t raise an error if you require something which isn’t there, the tasks just won’t be available to you. This is good, I have rake tasks which require the “rsruby” gem but I’m only going to invoke those on my local machine. I don’t want to install rsruby remotely and I don’t want to see an error message each time I run an unrelated task.

I’m going to suggest that we don’t write a generic remote rake invoker such as:


task :invoke do
  run("cd #{deploy_to}/current; /usr/bin/rake #{ENV['task']}”)
end

Since there are lots of things in the -T list which I really, really, really don’t want happening on my production server, or yours. :-)

Here’s my rake:log:annotate:


namespace :log do
  desc "Annotate the remote production log file."
  task :annotate do
    msg = ENV["MSG"] || “NB”
    msg = msg.gsub(”\”", “”) # Get rid of unsafe characters.
    run(”cd #{deploy_to}/current; /usr/bin/rake log:annotate MSG=\”#{msg}\”")
  end
end

For example:


cap rake:log:annotate MSG="End of Google AdWords campaign."

(I’m going to have Capistrano automatically annotate my log files every time I deploy, but that’s another day’s discussion.)

So, you get the idea. I guess I should stick in a caveat about making sure you have a good reason to muck about with your production server. You can obviously do a lot of damage by running a naughty rake task (like db:schema:load). That’s not really the biggest problem with this, though. The biggest problem is that this will encourage you to write little rake tasks to manually do things which should be built into your application or your automated deployment process. “cap rake” isn’t automation, unless you automatically invoke “cap rake” from another capistrano recipe. So, I’ll just say please PLEASE please don’t use this technique to stray into a manual deployment process.

Vlad the Deployer fans: please leave me a comment about how much cooler/easier/exactly the same this would be with Vlad. I’m curious and I love the name but I use RailsMachine for hosting so will be sticking with Capistrano until Bradley tells me otherwise.

Here’s the code on Pastie.


Comments

Louise Crow 02 Jan 2008

Thanks for posting this Ana, also really like the idea of noting deployments in the production log.

David Masover 08 Jan 2008

Trying to choose between Vlad and Capistrano initially, but the Vlad homepage does mention something about including Rails Machine recipes.

Seth 04 Feb 2008

Good stuff, thanks.

Ryan Davis 23 May 2008

“Furthermore, once you put Capistrano and Rake together you can develop gorgeous, sexy, intricate Rake tasks [...]”

yup… you sure can. which is exactly why vlad is built on top of rake. The simplicity and cleanliness of it is wonderful, and it didn’t make sense for cap to be building its own dependency mechanism when such a clean one already exists.

billy 02 Jun 2008

Good post but it seems kind of hacky. I thought for sure there would be a way of running rake tasks without hard coding the path to the rake executable. Since we know that cap handles db:migrate just fine, I dug around in the capistrano code and came up with this similar version:

Capistrano::Configuration.instance(:must_exist).load do
task :update_staging_domains, :roles => :app do
rake = fetch(:rake, “rake”)
rails_env = fetch(:rails_env, “staging”)
run “cd #{current_release}; #{rake} RAILS_ENV=#{rails_env} db:update_staging_domains”
end
end