Capistrano est un outil écrit en ruby qui permet de faciliter les déploiements d’application. Il n’est pas obligatoire que l’application soit une application rails, cela fonctionne aussi avec des applications sinatra et même des applications non ruby (node.js…)
L’objectif du multistaging est de pouvoir déployer différement selon les environnements (staging, production…)
Pour commencer nous allons ajouter les gems nécessaires à notre projet:
gem 'capistrano'
gem 'capistrano-ext'
Une fois les gems enregistrés nous allons les installer et mettre en place capistrano:
$ bundle install --binstubs
$ bin/capify .
$ mkdir config/deploy
$ cp config/environments/production.rb config/environments/staging.rb
$ cap staging deploy:setup
Le binstubs permet de freezer les bins, c’est une bonne pratique. Cela permet de lancer des commandes comme bin/rake plutôt que bundle exec rake.
Le capify va créer le fichier config/deploy.rb que nous allons éditer par la suite.
La création du dossier config/deploy va permettre de mettre en place une configuration de déploiement propre à chaque environnement (ex: config/deploy/production.rb).
Toutefois l’essentiel de la configuration se fera dans config/deploy.rb car elle varie peu d’un environnement à l’autre.
require "bundler/capistrano"
# allowing shell interactions
default_run_options[:pty] = true
# multistaging
set :stages, %w(staging production)
set :default_stage, "staging"
require 'capistrano/ext/multistage'
set :application, "fake"
set :user, "synbioz"
set :repository, "ssh://user@server/git/#{application}.git"
set :ssh_options, { :forward_agent => true }
set :deploy_to, "/var/www/#{application}"
set :scm, :git
set :deploy_via, :remote_cache
# number of releases we want to keep
set :keep_releases, 3
set :use_sudo, false
# default rails_env, should be overrided in config/deploy/#{environnement}.rb
set :rails_env, "staging"
# unicorn informations
set :unicorn_config, "#{current_path}/config/unicorn.rb"
set :unicorn_pid, "#{shared_path}/pids/unicorn.pid"
set :unicorn_bin, "#{current_path}/bin/unicorn"
# useful for rbenv
set :default_environment, {
'PATH' => "/home/synbioz/.rbenv/shims:/home/synbioz/.rbenv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
}
namespace :maintenance do
task :start do
run "ln -nfs #{shared_path}/system/_maintenance.html #{shared_path}/system/maintenance.html"
end
task :stop do
run "rm -f #{shared_path}/system/maintenance.html"
end
end
namespace :deploy do
task :start, :roles => :app, :except => { :no_release => true } do
run <<-CMD
cd #{current_path} && #{unicorn_bin} -c #{unicorn_config} -E #{rails_env} -D
CMD
end
task :force_stop, :roles => :app, :except => { :no_release => true } do
run <<-CMD
if [ -e #{unicorn_pid} ]; then
kill -9 $(cat #{unicorn_pid});
fi
CMD
end
task :stop, :roles => :app, :except => { :no_release => true } do
run <<-CMD
if [ -e #{unicorn_pid} ]; then
kill $(cat #{unicorn_pid});
fi
CMD
end
task :graceful_stop, :roles => :app, :except => { :no_release => true } do
run <<-CMD
if [ -e #{unicorn_pid} ]; then
kill -s QUIT $(cat #{unicorn_pid});
fi
CMD
end
task :reload, :roles => :app, :except => { :no_release => true } do
run <<-CMD
if [ -e #{unicorn_pid} ]; then
kill -s USR2 $(cat #{unicorn_pid});
fi
CMD
end
task :restart, :roles => :app, :except => { :no_release => true } do
run <<-CMD
if [ -e #{unicorn_pid} ]; then
kill -9 $(cat #{unicorn_pid});
sleep 5;
cd #{current_path} && #{unicorn_bin} -c #{unicorn_config} -E #{rails_env} -D;
fi
CMD
end
end
desc "Create shared folders."
after 'deploy:setup', :roles => :app do
# for unicorn
run "mkdir -p #{shared_path}/sockets"
run "mkdir -p #{shared_path}/config"
# only for sqlite
run "mkdir -p #{shared_path}/db"
end
desc "Link db.yml."
after 'deploy:update_code', :roles => :app do
run "ln -nfs #{shared_path}/config/database.yml #{release_path}/config/database.yml"
end
desc "Seed datas when cold deploying."
after 'deploy:migrate', :roles => :app, :only => { :no_release => true } do
run "cd #{release_path} && bin/rake db:seed RAILS_ENV=#{rails_env}"
end
desc "Link maintenance, rbenv and precompile assets."
after 'deploy:symlink', :roles => [:web, :app] do
run "ln -nfs #{shared_path}/config/.rbenv-version #{release_path}/config/.rbenv-version"
run "cp #{release_path}/public/_maintenance.html #{shared_path}/system/"
run "cd #{release_path}; RAILS_ENV=#{rails_env} bin/rake assets:precompile"
end
desc "remove unused releases."
after "deploy", "deploy:cleanup"
Le lancement de deploy:setup va créer les dossiers nécessaires sur le serveur.
Une fois réalisé, il faudra mettre en place une fois pour toute les fichiers de configuration nécessaires sur le serveur, à savoir shared/config/database.yml et shared/config/.rbenv-version
Ceux ci seront liés dans chaque release.
Nous pouvons maintenant lancer le premier déploiement (cold):
$ cap staging deploy:cold
Certaines tâches ne s’effectuent qu’au deploy:cold, dans notre exemple le db:seed. Pour cela nous avons utilisé le paramètre :only => { :no_release => true }.
A l’issu du deploy:cold si tout s’est bien passé la tâche start sera lancée et unicorn démarré.
Pour les prochains déploiements il suffira de déployer via:
$ cap staging deploy
La force d’unicorn est d’avoir un processus master qui s’occupe de charger l’application et de démarrer des workers qui pourront traiter les requêtes.
Chaque worker n’a donc pas à charger l’application rails, ce qui entraine un important gain de temps. Si un worker plante, le master s’occupera de le relancer.
De plus unicorn offre la promesse d’une application 0 downtime. En effet l’envoi d’un signal USR2 au master lui indique de recharger l’application rails (voir tâche reload dans deploy.rb).
A noter toutefois que cette technique n’est pas fiable à 100%. Il m’est arrivé que l’application ne soit pas rechargée. J’ai donc ajouté une tâche restart qui s’assurer d’arrêter puis démarrer l’application.
La configuration d’unicorn se fait dans config/unicorn.rb Si la configuration d’unicorn est différente entre le staging et la production on pourrait utiliser un sous dossier comme pour capistrano.
# unicorn_rails -c config/unicorn.rb -E production -D
worker_processes 4
user "synbioz", "users"
working_directory "/var/www/fake/current"
listen "/var/www/fake/shared/sockets/unicorn.sock", :backlog => 1024
pid "/var/www/fake/shared/pids/unicorn.pid"
# nuke workers after 30 seconds instead of 60 seconds (the default)
timeout 30
stderr_path "/var/www/fake/shared/log/unicorn.stderr.log"
stdout_path "/var/www/fake/shared/log/unicorn.stdout.log"
# combine REE with "preload_app true" for memory savings
# http://rubyenterpriseedition.com/faq.html#adapt_apps_for_cow
preload_app true
GC.respond_to?(:copy_on_write_friendly=) and
GC.copy_on_write_friendly = true
before_fork do |server, worker|
defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!
old_pid = "#{server.config[:pid]}.oldbin"
if File.exists?(old_pid) && server.pid != old_pid
begin
sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
Process.kill(sig, File.read(old_pid).to_i)
rescue Errno::ENOENT, Errno::ESRCH
end
end
end
after_fork do |server, worker|
defined?(ActiveRecord::Base) and
ActiveRecord::Base.establish_connection
end
La configuration de notre vhost sous nginx est très simple:
server { listen 80; server_name fake.server; access_log /var/log/nginx/fake.access.log; error_log /var/log/nginx/fake.error.log; # direct to maintenance if this file exists if (-f $document_root/system/maintenance.html) { rewrite ^(.*)$ /system/maintenance.html last; break; } location / { root /www/fake/current/public; proxy_redirect http://fake/ /; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # If the file exists as a static file serve it directly if (-f $request_filename) { break; } if (!-f $request_filename) { proxy_pass http://fake; break; } } error_page 500 502 503 504 /500.html; location = /500.html { root /www/fake/current/public; } }
Dans le fichier nginx.conf il suffit de référencer la socket unicorn:
upstream fake { server unix:/www/fake/shared/sockets/unicorn.sock; }
La seule subtilité vient du rewrite en cas de présence du fichier de maintenance. Cela permet de mettre en place une page qui interceptera toutes les requêtes.
Cette page peut être mise en place très simplement avec notre commande capistrano:
$ cap staging maintenance:start
En espérant que cet article facilite vos déploiements à venir.
L’équipe Synbioz.
Libres d’être ensemble.