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
Post a Comment