Blog by Railsware

Advanced server definitions in Capistrano

Advanced server definitions in Capistrano

What if you have several servers with different users, ssh-keys, and even port numbers? How to manage all this stuff flexibly ? This tutorial covers poorly documented Capistrano features for advanced servers and roles configuration. Many of them obtained via digging into Capistrano sources.

This article covers following Capistrano topics:

Before we start lets create minimal recipe.

Bootstrap

$ gem install capistrano
$ mkdir test
$ cd test
$ capify .

Capistrano loads only two standard recipes by default:

$ cap -T
cap invoke    # Invoke a single command on the remote servers.
cap shell     # Begin an interactive Capistrano session.

Let’s create our own simple configuration from scratch. Before we choose good server names! :)

Assume we have several servers with following DNS names:

Lets give our servers some roles at config/deploy.rb:

  role :dog, 'mr-white.reservoir.dogs', 'mr-orange.reservoir.dogs', 'mr-blonde.reservoir.dogs'
  role :boss, 'mr-white.reservoir.dogs'
  role :nerd, 'mr-orange.reservoir.dogs', 'mr-blonde.reservoir.dogs'

Alternatively you may declare it with server option:

  server 'mr-white.reservoir.dogs',  :dog, :boss
  server 'mr-orange.reservoir.dogs', :dog, :nerd
  server 'mr-blonde.reservoir.dogs', :dog, :nerd

Command invocation

Capistrano has a simple recipe:

cap invoke # Invoke a single command on the remote servers.

Let’s ask servers about current date:

$ cap invoke COMMAND=date
  * executing `invoke'
  * executing "date"
    servers: ["mr-white.reservoir.dogs", "mr-orange.reservoir.dogs", "mr-blonde.reservoir.dogs"]
    [mr-white.reservoir.dogs] executing command
    [mr-orange.reservoir.dogs] executing command
    [mr-blonde.reservoir.dogs] executing command
 ** [out :: mr-white.reservoir.dogs] Fri Oct 28 10:23:50 EDT 2011
 ** [out :: mr-orange.reservoir.dogs] Fri Oct 28 10:23:50 EDT 2011
 ** [out :: mr-blonde.reservoir.dogs] Fri Oct 28 10:23:50 EDT 2011
    command finished in 614ms

Running a task

Let’s create dead simple task that will show the date. Add following to your config/deploy.rb:

  desc "Show server' date"
  task :show_date do
    run('date')
  end

Look for our newly created task in the tasks list:

$ cap -T
cap invoke    # Invoke a single command on the remote servers.
cap shell     # Begin an interactive Capistrano session.
cap show_date # Show server' date

Let’s play:

$ cap show_date
  * executing `show_date'
  * executing "date"
    servers: ["mr-white.reservoir.dogs", "mr-orange.reservoir.dogs", "mr-blonde.reservoir.dogs"]
 ** [out :: mr-white.reservoir.dogs] Fri Oct 28 10:23:50 EDT 2011
 ** [out :: mr-orange.reservoir.dogs] Fri Oct 28 10:23:50 EDT 2011
 ** [out :: mr-blonde.reservoir.dogs] Fri Oct 28 10:23:50 EDT 2011

So with such configuration task will be executed on all defined servers.

But what if we want to run the task always only for specific role? Configure it as:

  desc "Show server' date"
  task :show_date, :roles => [:nerd]  do
    run('date')
  end

And it will be executed as expected:

$ cap show_date
  * executing `show_date'
  * executing "date"
    servers: ["mr-orange.reservoir.dogs", "mr-blonde.reservoir.dogs"]
 ** [out :: mr-orange.reservoir.dogs] Fri Oct 28 10:23:50 EDT 2011
 ** [out :: mr-blonde.reservoir.dogs] Fri Oct 28 10:23:50 EDT 2011

Environment Variables

Capistrano reacts on four special environment variables that allow you to change server definitions temporary. Let’s read about it from capistrano help:

$ cap -H

  HOSTS
        Execute the tasks against this comma-separated list of hosts.
        Effectively, this makes the host(s) part of every roles.

  HOSTFILTER
        Execute tasks against this comma-separated list of host, but only if the
        host has the proper role for the task.

  HOSTROLEFILTER
       Execute tasks against the hosts in this comma-separated list of roles,
       but only if the host has the proper role for the task.

  ROLES
      Execute tasks against this comma-separated list of roles.  Hosts which
      do not have the right roles will be skipped.

Let’s define task that should be executed on servers with :nerd role

  desc "Show server' date"
  task :show_date, :roles => [:nerd]  do
    run('date')
  end

HOSTS Variable

Let’s run task with following HOSTS:

$ cap show_date HOSTS=mr-white.reservoir.dogs,mr-orange.reservoir.dogs
  * executing `show_date'
  * executing "date"
    servers: ["mr-white.reservoir.dogs", "mr-orange.reservoir.dogs"]
...

So command is executed on explicitly specified servers.

But it’s not limited to hosts specified in configuration:

cap show_date HOSTS=mr-white.reservoir.dogs,google.com
  * executing `show_date'
  * executing "date"
    servers: ["mr-white.reservoir.dogs", "google.com"]
...

So HOSTS variable overrides server definitions.

ROLES Variable

Let’s run a task with another role:

$ cap show_date ROLES=boss
  * executing `show_date'
  * executing "date"
    servers: ["mr-white.reservoir.dogs"]
...

So ROLES variable also overrides server definitions.

HOSTFILTER Variable

Hosts from HOSTFILTER variable are excluded from servers list for certain task:

$ cap show_date HOSTFILTER=mr-orange.reservoir.dogs,google.com
  * executing `show_date'
  * executing "date"
    servers: ["mr-orange.reservoir.dogs"]
...

So it filters servers, but does not override servers configured for task.

HOSTROLEFILTER Variable

Let’s try to filter servers with another role:

$ cap show_date HOSTROLEFILTER=boss
  * executing `show_date'
  * executing "date"
`show_date' is only run for servers matching {:roles=>:nerd}, but no servers matched

So it also excludes hosts from the servers list for certain task.

For example if we have following task configuration:

  task :show_date, :roles => [:app, :db, :www, :backup]  do
    ...
  end

Then the command

$ cap show_date HOSTROLEFILTER=db,www,another_role

will be executed only on servers that have :db and :www role.

So let’s summarize:

Thus it’s safe to use HOSTFILTER and HOSTROLEFILTER variables.

Advanced Server Definitions

Running command with custom options

Probably you don’t know that run command accepts the same options as that task itself. So it’s possible to write complex things like this:

task :shoot do
  run "date"
  run "uname",  :roles => :boss
  run "whoami", :roles => :nerd
end
$ cap shoot
  * executing `shoot'
  * executing "date"
    servers: ["mr-white.reservoir.dogs", "mr-orange.reservoir.dogs", "mr-blonde.reservoir.dogs"]
    [mr-orange.reservoir.dogs] executing command
    [mr-white.reservoir.dogs] executing command
    [mr-blonde.reservoir.dogs] executing command
 ** [out :: mr-white.reservoir.dogs] Fri Oct 28 12:00:19 EDT 2011
 ** [out :: mr-orange.reservoir.dogs] Fri Oct 28 12:00:19 EDT 2011
 ** [out :: mr-blonde.reservoir.dogs] Fri Oct 28 12:00:19 EDT 2011
    command finished in 737ms
  * executing "uname"
    servers: ["mr-white.reservoir.dogs"]
    [mr-white.reservoir.dogs] executing command
 ** [out :: mr-white.reservoir.dogs] Linux
    command finished in 479ms
  * executing "whoami"
    servers: ["mr-orange.reservoir.dogs", "mr-blonde.reservoir.dogs"]
    [mr-orange.reservoir.dogs] executing command
    [mr-blonde.reservoir.dogs] executing command
 ** [out :: mr-orange.reservoir.dogs] mongrel
 ** [out :: mr-blonde.reservoir.dogs] mongrel
    command finished in 746ms

So date is executed on all servers. And uname and whoami only on corresponding servers.

Task :roles and :hosts options

If you’ve read Capistrano source code, you may find yet another nifty option called :hosts there.
How does it differ from :roles options? Firstly, :hosts option have higher priority over :roles. Secondary, you always create new server definition object that is not in global server list by specifying server via :hosts options. It means such server won’t be affected with tasks that does not have a role.
It’s really nice option because sometimes you want to execute some commands (notifications for example) on certain hosts that are actually not a target for the code delivery.

task :log_deploy, :hosts => ['history.reservoir.dogs'] do
  run "cat '#{Time.now},#{real_revision}' >> deploy.log"
end

Specifying alternative server options

Sometimes you need to specify special server options like username, port, ssh key for each of your servers. And it’s pretty easy when your servers are defined globally:

role :app, 'app1.reservoir.dogs', 'app2.reservoir.dogs', {
  :user => 'nerd',
  :ssh_options => {
    :keys => './keys/nerd.pem'
  }
}

role :db, 'db.reservoir.dogs', {
  :user => 'boss',
  :ssh_options => {
    :keys => './keys/boss.pem'
  }
}

alternatively you may declare it with server option:

server 'app1.reservoir.dogs', :app, {
  :user => 'nerd1',
  :ssh_options => {
    :keys => './keys/nerd.pem'
  }
}

server 'app2.reservoir.dogs', :app {
  :user => 'nerd2',
  :ssh_options => {
    :keys => './keys/nerd.pem'
  }
}

server 'db.reservoir.dogs', :db {
  :user => 'boss',
  :ssh_options => {
    :keys => './keys/boss.pem'
  }
}

Specifying some server options in task

There is no problem if you use :roles in your task or run method. But what about :hosts task/run options?
At the first look there is an issue because :hosts option is an array of servers name.
But there is a trick available! Look at Capistrano::ServerDefinition

module Capistrano
  class ServerDefinition
    def initialize(string, options={})
      @user, @host, @port = string.match(/^(?:([^;,:=]+)@|)(.*?)(?::(\d+)|)$/)[1,3]
      ...
    end
  end
end

So solution is simple:

run("date", :hosts => ['nerd1@app1.reservoir.dogs:2201', 'nerd2@app1.reservoir.dogs:2201'])

task(:show_date, :hosts => ['nerd1@app1.reservoir.dogs:2201', 'nerd2@app1.reservoir.dogs:2201']) do
  run("date")
end

So you know how to specify alternative :user and :port options. But what about another server definition options e.g. ssh_options ?

Specifying any server options in task

Do we need to hack find_servers to pass specific :ssh_options for server defined in task? No! recently we found simple solution without hacking: look at server_list_from

  def server_list_from(hosts)
    hosts = hosts.split(/,/) if String === hosts
    hosts = build_list(hosts)
    hosts.map { |s| String === s ? ServerDefinition.new(s.strip) : s }
  end

So we can pass in :hosts options array of ServerDefinition objects! So solution looks like this:

run("date", :hosts => [
  Capistrano::ServerDefinition.new('app1.reservoir.dogs', {
    :user => 'nerd1',
    :port => 2201,
    :ssh_options => {
      :keys => './keys/nerd.pem'
    }
  }),
  Capistrano::ServerDefinition.new('app2.reservoir.dogs', {
    :user => 'nerd2',
    :port => 2202,
    :ssh_options => {
      :keys => './keys/nerd.pem'
    }
  })
])

task(:show_date, :hosts => [
  Capistrano::ServerDefinition.new('app1.reservoir.dogs', {
    :user => 'nerd1',
    :port => 2201,
    :ssh_options => {
      :keys => './keys/nerd.pem'
    }
  }),
  Capistrano::ServerDefinition.new('app2.reservoir.dogs', {
    :user => 'nerd2',
    :port => 2202,
    :ssh_options => {
      :keys => './keys/nerd.pem'
    }
  })
]) do
  run("date")
end

It is better to define servers as task/run options:

set(:boss_host, {
  Capistrano::ServerDefinition.new('boss.reservoir.dogs', {
    :user => 'boss',
    :port => 2222,
    :ssh_options => {
      :keys => './keys/boss.pem'
    }
})

run("date", :hosts => [ :boss_host ]

Capistrate the world!

We hope this article will help you to use Capistrano more efficiently.

Exit mobile version