Déployer et héberger une application ruby on rails avec capistrano unicorn et nginx

Publié le 15 novembre 2011 par Martin Catty | système

Cet article est publié sous licence CC BY-NC-SA

Déployer et héberger une application ruby on rails avec capistrano, unicorn et nginx

Mettre en place capistrano en multistage

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

Configurer Unicorn

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

Configurer Nginx

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.