Deploying an RoR app using Jenkins and Knife



The simplest way to deploy a Ruby on Rails app would be to package the deploy-able code into a tar and un-tar it at the server location. This can be achieved using rake but rake doesn't provide ssh ability. Another way is to use rake with Capistrano, but this would be useful when implementing a master-less puppet or chef-solo system for configuration management.

Working on an RoR project, recently we discovered another approach to deploy an RoR app - using a custom knife plugin. There is a chef-deploy resource provided by chef which requires git repository access. Standards suggest that there should not be any development tools installed on the application server such as git etc. A series of discussions led us to decide on a chef based approach as we already had chef server managing the configuration on the nodes.

Place the following knife plugin in the code repository's .chef/plugins/knife directory. Knife will automatically load the plugin. Configure jenkins as a knife client and.... ready to deploy! This plugin deploys an aggregate of the APPS. A new method can be written to restart the application server once the code is deployed.

knife command would look like APP_HOME=<location/to/deploy/app> JENKINS_HOST=<jenkins_url> knife deploy --build-no BUILD_NO --env ROLE

module KnifePlugins

class DeployAggregate < Chef::Knife
banner 'knife hcah deploy aggregate'

APPS = []
APIS = []
ALL_APPS = APPS | APIS

option :build_no,
:long => '--build-no BUILD_NO',
:description => 'Aggregate app build number'

option :deploy_environment,
:long => '--env  ENVIRONMENT',
:description => 'Environment to deploy build on'

deps do
require 'chef/search/query'
require 'chef/knife/ssh'
Chef::Knife::Ssh.load_deps
end

def run
if !valid_options or !valid_env
ui.fatal 'Please provide build number and env name to deploy' +
'e.g. APP_HOME=/path/to/app/home [JENKINS_HOST=http://jenkin_url/job/job_name] [S3_BUCKET=s3://s3_bucket/] knife deploy aggregate --build-no 59 --env qa'
exit 1
end

@build_number = config[:build_no]
@env = config[:deploy_environment]
@app_name = "aggregate"
@jenkins_host = ENV['JENKINS_HOST']
@s3_bucket = ENV['S3_BUCKET']
@app_home = ENV['APP_HOME']

app_nodes = deploy_on_env(@env)
if app_nodes.nil?
ui.msg "No app server nodes found for environment #{@env}"
exit 1
end

app_nodes.each do |app_node|
if app_node.include?("ec2")
node_id = app_node.ec2.public_hostname
else
node_id = app_node.ipaddress
end
ui.msg '-'*80
ui.msg "Starting deploy of build #{@build_number} on #{node_id}"
ui.msg '-'*80
ui.msg cmd=deploy_command(app_node)
knife_ssh(node_id, cmd).run
end
end

private

def deploy_on_env env
query = "chef_environment:#{env} AND roles:app_server"
query_nodes = Chef::Search::Query.new
app_servers = query_nodes.search('node', query)
return app_servers[0] if app_servers[0].size > 0
end

def valid_options
true if option_present(:build_no) && option_present(:deploy_environment)
end

def option_present option
!config[option].nil? && !config[option].strip.empty?
end

def valid_env
true if (env_present('JENKINS_HOST') && env_present('APP_HOME') || env_present('S3_BUCKET') && env_present('APP_HOME'))
end

def env_present variable
!ENV[variable].nil? && !ENV[variable].strip.empty?
end
def deploy_command app_node
command_string = "rm -f /tmp/deploy.sh;"
cmd = ["set -e",cd_to_app_home,load_env, manifest_backup, pre_deploy_cleanup, copy_artifact(@env), extract_artifact,
post_deploy_cleanup, post_deploy_env_config, post_deploy_mongoid_config, post_deploy_newrelic(app_node), post_deploy_db_seed,
cd_to_app_home, restart_passenger].compact.join(';')
command_string += "echo '#{cmd}' >> /tmp/deploy.sh;"
command_string += "sh /tmp/deploy.sh"
end

def cd_to_app_home
"cd #{@app_home}"
end

def load_env
"export RAILS_ENV=production; export RACK_ENV=production"
end

def manifest_backup
'if [ -f manifest.txt ]; then mv -f manifest.txt manifest.old; fi'
end

def pre_deploy_cleanup
ALL_APPS.collect do |app|
"rm -rf #{app}"
end.join(';')
end

def copy_artifact env
if env.include?("aws")
"s3cmd get #{@s3_bucket}/#{@build_number}/aggregate.tar.gz"
else
"wget #{@jenkins_host}/#{@build_number}/artifact/#{@app_name}.tar.gz -O #{@app_name}.tar.gz"
end
end

def extract_artifact
"echo 'extracting aggregate archive to #{@app_home}'; tar -zxf #{@app_name}.tar.gz"
end

def post_deploy_env_config
(APPS+APIS).collect do |app|
"ln -s #{@app_home}/config/#{app}.env_config.yml #{@app_home}/#{app}/config/env_config.yml"
end.join(';')
end

def post_deploy_newrelic app_node
return unless app_node.newrelic
(APPS+APIS).collect do |app|
"ln -s #{@app_home}/config/#{app}.newrelic.yml #{@app_home}/#{app}/config/newrelic.yml"
end.join(';')
end

def post_deploy_mongoid_config
APIS.collect do |api|
"ln -s #{@app_home}/config/#{api}.mongoid.yml #{@app_home}/#{api}/config/mongoid.yml"
end.join(';')
end

def post_deploy_db_seed
APIS.collect do |api|
"cd  #{@app_home}/#{api}; RACK_ENV=production bundle exec rake db:seed"
end.join(';')
end

def post_deploy_cleanup
"rm -f #{@app_name}.tar.gz"
end

def restart_passenger
cmd = 'echo restarting passenger worker processes for apps;'
cmd += ALL_APPS.collect do |app|
"touch #{app}/tmp/restart.txt"
end.join(';')
end
def knife_ssh server_name, ssh_command
ssh = Chef::Knife::Ssh.new
ssh.ui = ui
ssh.name_args = [ server_name, ssh_command]
ssh.config[:manual] =  true
ssh.config[:ssh_user] =  'user'
ssh.config[:identity_file] = Chef::Config[:identity_file]
ssh.config[:on_error] = :raise
ssh
end
end
end

Comments

Popular posts from this blog

To DR or Not To DR

High Availability NAT for AWS VPC with Multiple Private Subnets.

Load Balancer with SSL offloading - nginx + HAProxy