Skip to content

Instantly share code, notes, and snippets.

@Yoshyn
Last active February 20, 2017 11:43
Show Gist options
  • Save Yoshyn/353f6518dec413ae9bfcc3b99e1fe22b to your computer and use it in GitHub Desktop.
Save Yoshyn/353f6518dec413ae9bfcc3b99e1fe22b to your computer and use it in GitHub Desktop.
Capistrano : Set of task/script that can be add to deploy.rb
# Contains :
# * deploy_git_sync.rb : stop deployment if git is not synchronized remote branch.
# * deploy_lock_per_user.rb : stop deployment if somebody is already deploying the application.
# * deploy_smart_asset.rb : Avoid compiling asset if not needed.
# * passenger_restart.rb : Restart passenger after deploy.
# * sidekiq_restart.rb : Restart sidekiq after deploy.
# Question? Why this and not use gem ?
# -> Answer : Why use a gem for this? :)
# Sync with the deploy branch
namespace :deploy do
desc "Makes sure local git is in sync with remote."
task :check_revision do
on roles :all, exclude: :no_release do |server|
unless `git rev-parse HEAD` == `git rev-parse origin/#{fetch(:branch)}`
info "WARNING: HEAD is not the same as origin/#{fetch(:branch)}"
info "Run `git push` to sync changes."
exit
end
end
end
before :deploy, "deploy:check_revision"
end
# Avoid several user launch deployment in same time
# Set defaults.
set :lock_expire_duration, fetch(:lock_expire_duration, (30 * 60)) # 30 minutes
set :deploy_lock_filename, fetch(:deploy_lock_filename, 'capistrano.lock.yml')
set :deploy_lock_filepath, -> { shared_path.join(fetch(:deploy_lock_filename)) }
namespace :deploy do
namespace :lock do
def fetch_current_lock(server)
current_lock = if test("[ -f #{fetch(:deploy_lock_filepath)} ]")
c_lock_data = capture "cat #{fetch(:deploy_lock_filepath)}"
YAML.load(c_lock_data) if c_lock_data && c_lock_data.size > 0
end
set :current_lock, current_lock
end
task :check do
on roles :all, exclude: :no_release do |server|
fetch_current_lock(server)
if !(current_lock = fetch(:current_lock))
info "No lock file #{fetch(:deploy_lock_filename)} found on #{server}."
else
current_time = Time.now.getutc.to_i
warn "Found #{fetch(:deploy_lock_filename)} on #{server}."
warn "This lock was created by #{current_lock[:user]} at #{Time.at(current_lock[:created_at]).strftime('%Y-%m-%d-%H:%M')}."
if (remain_seconds = (current_lock[:expire_at].to_i - current_time)) > 0
info "The lock expire in ~#{remain_seconds/60} minute(s)."
info "You can manualy remove this lock : cap #{fetch(:rails_env)} deploy:lock:remove"
error "Aborting deployment !"
exit 1
else
info " -> This lock file is expired, removing..."
invoke "deploy:lock:remove"
end
end
end
end
task :create do
on roles :all, exclude: :no_release do |server|
user = `hostname`.strip
created_at = Time.now.getutc.to_i
info "Create a new lock at #{Time.at(created_at).strftime('%Y-%m-%d-%H:%M')} on #{server} for #{user}"
current_lock = {
user: user,
created_at: created_at,
expire_at: created_at + fetch(:lock_expire_duration)
}
upload! StringIO.new(current_lock.to_yaml), fetch(:deploy_lock_filepath)
end
end
task :remove do
on roles :all, exclude: :no_release do |server|
info "Remove lock on #{server}"
execute :rm, "-f #{fetch(:deploy_lock_filepath)}"
end
end
end
before "deploy:starting", "deploy:lock:check"
before "deploy:starting", "deploy:lock:create"
after "deploy:finished", "deploy:lock:remove"
after 'deploy:failed', :failed do
invoke "deploy:lock:remove"
end
end
# Into production.rb file !
# frozen_string_literal: true
set :branch, "production"
set :migration_role, :app
set :migration_servers, :app
set :assets_roles, :web
set :deploy_to, "/path/to/the/project/#{fetch(:application)}/#{fetch(:branch)}"
set :conditionally_migrate, true
server 'XX.XX.XX.XXX', user: 'my_user', roles: %w{app}
server 'XX.XX.XX.XXX', user: 'my_user', roles: %w{web}
# set the locations that we will look for changed assets to determine whether to precompile
set :assets_dependencies, %w(app/assets lib/assets vendor/assets Gemfile.lock)
# clear the previous precompile task
Rake::Task["deploy:assets:precompile"].clear_actions
class PrecompileRequired < StandardError; end
namespace :deploy do
namespace :assets do
desc "Precompile assets"
task :precompile do
on release_roles(fetch(:assets_roles)) do
within release_path do
with rails_env: fetch(:rails_env) do
begin
# find the most recent release
latest_release = capture(:ls, '-xr', releases_path).split[1]
# precompile if this is the first deploy
raise PrecompileRequired unless latest_release
latest_release_path = releases_path.join(latest_release)
# precompile if the previous deploy failed to finish precompiling
execute(:ls, latest_release_path.join('assets_manifest_backup')) rescue raise(PrecompileRequired)
fetch(:assets_dependencies).each do |dep|
# execute raises if there is a diff
execute(:diff, '-Naur', release_path.join(dep), latest_release_path.join(dep)) rescue raise(PrecompileRequired)
end
info("Skipping asset precompile, no asset diff found")
# copy over all of the assets from the last release
execute(:cp, '-r', latest_release_path.join('public', fetch(:assets_prefix)), release_path.join('public', fetch(:assets_prefix)))
rescue PrecompileRequired
execute(:rake, "assets:precompile")
end
end
end
end
end
end
end
namespace :deploy do
desc "Restart Web server. Avoid call this : (cap env deploy no_restart_web=true)"
task :restart_web do
on roles(:web) do |host|
display_passenger_informations()
restart_web = ENV['restart_web'] || ask_in("Restart web", %w{yes no})
if restart_web.start_with?('y')
info "Restart passenger on host : #{host} (role=web)"
execute 'passenger-config', "restart-app --rolling-restart #{current_path}"
end
end
end
after :finished, "deploy:restart_web"
end
def ask_in(title, choises)
input_tmp = nil
while !choises.include? input_tmp
ask(:input, "#{title} (#{choises.join('|')}) ? ")
input_tmp = fetch(:input)
end
input_tmp
end
def display_passenger_informations()
passenger_pids = capture("ps aux | grep Passenger | grep RubyApp | grep -v grep | awk '{print $2}'", raise_on_non_zero_exit: false).split("\n")
if passenger_pids && passenger_pids.size > 0
warn "Found running passenger process !"
passenger_folders_timestamps = capture("pwdx #{passenger_pids.join(' ')}").split("\n")
passenger_folders_with_pids = passenger_folders_timestamps.inject(Hash.new { |hash, key| hash[key] = [] }) do |acc, sft|
match_data = sft.match(/^(\d+):\s+\/.*\/(\d+)$/)
acc[match_data[2].to_i] << match_data[1]
acc
end
releases = capture("ls #{deploy_path}/releases", raise_on_non_zero_exit: false).split("\n").map(&:to_i).sort.reverse
releases.each do |release|
release_info = capture("cat #{deploy_to}/revisions.log | grep #{release}", raise_on_non_zero_exit: false)
if release_info.to_s.size == 0
info "=> \e[31m#{release}\e[0m : Release folder exist but does not appear into revisions.log !"
elsif !(pids = passenger_folders_with_pids[release]).empty?
info "=> \e[35m#{release_info}\e[0m : Passenger process are running with PID : #{pids.join(', ')}"
else
info "=> #{release_info}"
end
end
else
info "No Passenger Process seems to be running."
end
end
namespace :deploy do
desc "Clean sidekiq log on app server. Avoid call this : (cap env deploy no_remove_sidekiq_log=true)"
task :clean_sidekiq_log do
on roles(:app) do |host|
info "Remove the shared/log/sidekiq.log on host : #{host} (role=app)"
sidekiq_path = "#{current_path}/shared/log/sidekiq.log"
if test("[ -f #{sidekiq_path} ]")
execute :rm, "#{sidekiq_path}"
end
end
end
desc "Kill and reset sidekiq, relaunch queue and restart process"
task :sidekiq_manage do
on roles(:app) do |host|
within release_path do
with rails_env: fetch(:rails_env) do
display_sidekiq_informations()
manage_sidekiq = ENV['manage_sidekiq'] || ask_in("Manage sidekiq (Will kill sidekiq process)", %w{yes no})
if manage_sidekiq.start_with?('y')
info "Sidekiq : Kill sidekiq process on host : #{host} (role=app)"
execute :rake, "sidekiq:kill_process"
execute :rake, "sidekiq:clean_tmp_files"
info "Sidekiq : start_process on host : #{host} (role=app)"
execute :rake, "sidekiq:start_process"
end
end
end
end
end
after :finishing, "deploy:clean_sidekiq_log"
after :finished, "deploy:sidekiq_manage"
end
def display_sidekiq_informations()
sidekiq_pids = capture("ps aux | grep sidekiq | grep -v grep | awk '{print $2}'", raise_on_non_zero_exit: false).split("\n")
if sidekiq_pids && sidekiq_pids.size > 0
warn "Found running sidekiq process !"
sidekiq_folders_timestamps = capture("pwdx #{sidekiq_pids.join(' ')}").split("\n")
sidekiq_folders_with_pids = sidekiq_folders_timestamps.inject(Hash.new { |hash, key| hash[key] = [] }) do |acc, sft|
match_data = sft.match(/^(\d+):\s+\/.*\/(\d+)$/)
acc[match_data[2].to_i] << match_data[1]
acc
end
releases = capture("ls #{deploy_path}/releases", raise_on_non_zero_exit: false).split("\n").map(&:to_i).sort.reverse
releases.each do |release|
release_info = capture("cat #{deploy_to}/revisions.log | grep #{release}", raise_on_non_zero_exit: false)
if release_info.to_s.size == 0
info "=> \e[31m#{release}\e[0m : Release folder exist but does not appear into revisions.log !"
elsif !(pids = sidekiq_folders_with_pids[release]).empty?
info "=> \e[35m#{release_info}\e[0m : Sidekiq process are running with PID : #{pids.join(', ')}"
else
info "=> #{release_info}"
end
end
execute :rake, "sidekiq:stats"
else
info "No sidekiq process seems to be running."
end
end
############# RAKE TASKS ##############
execute :rake, "sidekiq:kill_process"
namespace :sidekiq do # FIX_ME : 10 busy is a dummy grep search
desc "Kill all sidekiq_process"
task kill_process: :environment do
ps = Sidekiq::ProcessSet.new
ps.each { |p| p.quiet! }; sleep(2)
ps.each { |p| p.stop! }; sleep(1)
system %x(
pid=$(ps -ef | grep sidekiq | grep '10 busy' | grep -v tmux | grep -v sidekiq:kill_process | grep -v grep | awk '{ print $2 }');
if ! [ -z "$pid" ]; then
kill -9 $pid
fi
)
end
end
execute :rake, "sidekiq:clean_tmp_files"
namespace :sidekiq do
desc "Clean all sidekiq temporary File in the tmp directory"
task clean_tmp_files: :environment do
system %x(
cd #{release_path}/current/tmp/;
rm -rf uploads/* &&
rm -f /tmp/open-uri*
rm -f /tmp/mini_magick*;
rm -f /tmp/magick-*;
)
end
end
execute :rake, "sidekiq:start_process"
namespace :sidekiq do
desc "Start all the process"
task start_process: :environment do
system "bundle exec sidekiq -e #{Rails.env} -C 'config/sidekiq.yml' -d"
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment