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:
- roles configuration
- server configuration
- The way HOSTS, ROLES, HOSTFILTER, HOSTROLEFILTER variables affect configuration
- :roles and :hosts settings in the task and run methods
- How to specify server settings with :hosts option?
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:
- mr-white.reservoir.dogs
- mr-orange.reservoir.dogs
- mr-blonde.reservoir.dogs
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:
- HOSTS and ROLES variables override server definitions.
- HOSTFILTER and HOSTROLEFILTER variables filter servers.
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.