<= Home

Enterprise Capistrano: Deploying to Servers With Unique Credentials

Posted about 5 months ago

Capistrano makes an assumption that all servers in your application stack share the same credentials. As much logical sense as this makes, in large enterprise landscapes it is often not a reality. The nature of this world (Enterprise) is that trying to shift the mindset of all shareholders (infrastructure, support teams, security, etc) and then changing long running practices is not always feasible. This is especially true when doing so on numerous severs servicing numerous other systems that depend on the already in place mismatched credentials. For us...we have our normal application servers and database servers that DO share matching credentials. But along with them we use a separate server env to store and host our assets (images, css, js) with its own credentials.

Still desiring to leverage capistrano for its core automation benefits we had to monkey-patch capistrano so we could supply server specific username and passwords. Drop this in your lib folder or somewhere else more relevant. WARNING: This is basically cut-paste from the capistrano internals with some added sugar - I take no responsibility for the class design and multiple responsibilities in this rather big method.

	#This is a core class in the capistrano library. it is overridden so that we can support servers that have different user names and passwords
	module Capistrano
	  class SSH
	    class << self
	      alias :original_connect :connect 
	      def connect(server, options={}, &block)
	        special_server_settings = options[:server_authentication] ? options[:server_authentication][server.to_s] : nil
	        if special_server_settings.nil?
	          return original_connect(server, options, &block)
	        else
	          methods = [ %w(publickey hostbased), %w(password keyboard-interactive) ]
	          password_value = nil

	          ssh_options = (options[:ssh_options] || {}).dup
	          ssh_options[:username] =  special_server_settings[:user] || server.user || options[:user] || ssh_options[:username]
	          ssh_options[:port]     = server.port || options[:port] || ssh_options[:port] || DEFAULT_PORT

	          begin
	            connection_options = ssh_options.merge(
	              :password => password_value,
	              :auth_methods => ssh_options[:auth_methods] || methods.shift
	            )
            
	            connection = Net::SSH.start(server.host, connection_options, &block)
	            Server.apply_to(connection, server)

	          rescue Net::SSH::AuthenticationFailed
	            raise if methods.empty? || ssh_options[:auth_methods] 
	            password_value = special_server_settings[:password]
	            password_value = password_value.call if password_value.is_a? Proc
	            retry
	          end
	        end
	      end
	    end
	  end  
	end

Your capfile will look something similar to this:

role :app, 		'thisapp.rocks.com'
role :web, 		'thisapp.rocks.com'
role :db, 		'thisdb.rocks.com'
role :assets, 'my.asset.server01.com'

set :user, 'the-primary-user'
set :server_authentication, { 'my.asset.server01.com' => { 
                                :user => 'assetusr' ,
                                :password => 'password'}}
                            }
....