Deploy com Git, Capistrano, Nginx e Unicorn na Velocidade da Luz

rocket+git+capistrano+rails+unicorn+nginx

 

 

 

 

 

 

 

 

Todos nós que já desenvolvemos aplicações WEB e tivemos que implantalas, lembramos dos dias que simplesmente carregamos os arquivos via FTP para o servidor e pronto, tudo estava feito, ou quase isso. Hoje em dia temos que clonar repositórios GIT, reiniciar servidores, definir permissões, criar links simbólicos para arquivos de configurações e outros, limpar cache... dentre outros procedimentos cansativos..

Doutor, o que está errado?

Na minha opinião existem dois problemas críticos com deploys de hoje:

  •      Eles são lentos
  •      Eles causam downtime.

Ambos os temas foram discutidos por grandes empresas como Twitter e Github. Eles têm otimizado o seu processo de deploy para permitir implementações rápidas e contínuas (sem o downtime),  utilizando o Capistrano. Que possíbilita que com pouco trabalho, implantar soluções de deploy similares a do  Github e Twitter.

Então vamos lá

Este guia vai ajudar você a configurar o servidor com Rails 3 com rápido deoloy e zero-downtime. Eu vou estar usando Nginx + Unicorn como servidor para a aplicação, git + capistrano para o deploy.

 Nossa Lista de Compras

Você vai precisar dos seguintes ingredientes:

  • Um Ubuntu Server recente (Eu usei 12.04 Netty);
  • Sua aplicação Rails 3;
  • Um respositório remoto com Git, contendo sua aplicação.

 Pressupostos

Eu estou fazendo algumas suposições sobre sua aplicação:

  •      ruby 1.9.2
  •      Aplicações Rails 3.1 usando Postgres chamado my_site
  •      Você quer usar o RVM e Bundle

Configurar o servidor

Existem algumas coisas que você precisa configurar antes de começar. Os comandos são executados como Root.

Aqui está a lista completa de comando apt-get que eu usei.

 

apt-get update
apt-get upgrade -y
apt-get install build-essential ruby-full libmagickcore-dev imagemagick libxml2-dev \
  libxslt1-dev git-core postgresql postgresql-client postgresql-server-dev-9.1 nginx curl
apt-get build-dep ruby1.9.1

obs: em postgresql-server-dev-9.1 substitua pela versão corrente...

Você irá precisar designar um usuário para rodar sua aplicação. Acredite em mim, você não irá querer fazer isso com o Root. Eu chamo o meu de deployer

useradd -m -g staff -s /bin/bash deployer
passwd deployer

Para permitir que o deployer execute comando como super-user, adicionar isso em /etc/sudoers.

 # /etc/sudoers

%staff ALL=(ALL) ALL

 Ruby and RVM

Feito isso, você está pronto para instalar rvm. certifique-se de executar isso como root.

bash -s stable < <(curl -s https://raw.github.com/wayneeseguin/rvm/master/binscripts/rvm-installer)
source /home/deployer/.rvm/scripts/rvm

Agora instale o Ruby, neste caso o ruby-1.9.2-p290, e o rubygem.

rvm install ruby-1.9.3
wget http://production.cf.rubygems.org/rubygems/rubygems-1.8.10.tgz
tar zxvf rubygems-1.8.10.tgz
cd rubygems-1.8.10
ruby setup.rb

Criae um arquivo ~ /gemrc, este estabelece alguns padrões necessários para o seu servidor de produção:

# ~/.gemrc
---
:verbose: true
:bulk_threshold: 1000
install: --no-ri --no-rdoc --env-shebang
:sources:
- http://gemcutter.org
- http://gems.rubyforge.org/
- http://gems.github.com
:benchmark: false
:backtrace: false
update: --no-ri --no-rdoc --env-shebang
:update_sources: true

Agora crie o ~/.rvmrc

 # ~/.rvmrc

rvm_trust_rvmrcs_flag=1

Nota: fazer isso tanto para o usuário root quanto para o deployer para evitar confusão mais tarde.

Porque você vai estar rodando sua aplicação em modo de produção otempo, adicione a seguinte linha ao arquivo /etc/environment, de modo que você não tem que repeti-lo com todos os comandos Rails que vocẽ vai usar:

RAILS_ENV=production

Eu não sei que todo mundo usa Postgres, mas eu uso. Primeiro, crie o banco e o login como o usuário postgres:

sudo -u postgres createdb my_site
sudo -u postgres psql

Depois execute essas SQL

CREATE USER my_site WITH PASSWORD 'password';
GRANT ALL PRIVILEGES ON DATABASE my_site TO my_site;

Nginx

Nginx é um grande parte da engenharia Russa. Você precisará de algumas configurações:

# /etc/nginx/sites-available/default
upstream my_site {
  # fail_timeout=0 means we always retry an upstream even if it failed
  # to return a good HTTP response (in case the Unicorn master nukes a
  # single worker for timing out).

  # for UNIX domain socket setups:
  server unix:/tmp/my_site.socket fail_timeout=0;
}

server {
    # if you're running multiple servers, instead of "default" you should
    # put your main domain name here
    listen 80 default;

    # you could put a list of other domain names this application answers
    server_name my_site.example.com;

    root /home/deployer/apps/my_site/current/public;
    access_log /var/log/nginx/my_site_access.log;
    rewrite_log on;

    location / {
        #all requests are sent to the UNIX socket
        proxy_pass  http://my_site;
        proxy_redirect     off;

        proxy_set_header   Host             $host;
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;

        client_max_body_size       10m;
        client_body_buffer_size    128k;

        proxy_connect_timeout      90;
        proxy_send_timeout         90;
        proxy_read_timeout         90;

        proxy_buffer_size          4k;
        proxy_buffers              4 32k;
        proxy_busy_buffers_size    64k;
        proxy_temp_file_write_size 64k;
    }

    # if the request is for a static resource, nginx should serve it directly
    # and add a far future expires header to it, making the browser
    # cache the resource and navigate faster over the website
    # this probably needs some work with Rails 3.1's asset pipe_line
    location ~ ^/(images|javascripts|stylesheets|system)/  {
      root /home/deployer/apps/my_site/current/public;
      expires max;
      break;
    }
}

E mais isso

# /etc/nginx/nginx.conf
user deployer staff;

# Change this depending on your hardware
worker_processes 4;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
    multi_accept on;
}

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay off;
    # server_tokens off;

    # server_names_hash_bucket_size 64;
    # server_name_in_redirect off;

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    gzip on;
    gzip_disable "msie6";

    # gzip_vary on;
    gzip_proxied any;
    gzip_min_length 500;
    # gzip_comp_level 6;
    # gzip_buffers 16 8k;
    # gzip_http_version 1.1;
    gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

    ##
    # Virtual Host Configs
    ##

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Ok, agora o Nginx está funcionando. Você preicsa inicia-lo, ele deve dar um erro 500 ou um erro de proxy:

/etc/init.d/nginx start

Unicorn

A parte seguinte envolve a criação de Capistrano e unicórnio para o seu projeto. É aqui que a verdadeira magia acontece.

Você estará fazendo cap deploy 99% do tempo. Este comando precisa ser rápido. Para realizar isso, quero utilizar o poder do git. Em vez de ter O Capistrano fazendo  malabarismos em torno de um monte de diretórios, o que é dolorosamente lento, eu quero usar o git para mudar para a versão correta do meu aplicativo. Isso significa que vou ter apenas um diretório que é atualizado pelo git somente quando ele precisar.

Vamos começar adicionando algumas gem para sua aplicação. Quando tive-lo feito executae bundle install

# Gemfile
gem "unicorn"

group :development do
  gem "capistrano"
end

Pŕoximo passo é adicionar um arquivo de configuração para o unicorn: config/uniconr.rb

[geshi lang='ruby']

# config/unicorn.rb
# Set environment to development unless something else is specified
env = ENV["RAILS_ENV"] || "development"

# See http://unicorn.bogomips.org/Unicorn/Configurator.html for complete
# documentation.
worker_processes 4

# listen on both a Unix domain socket and a TCP port,
# we use a shorter backlog for quicker failover when busy
listen "/tmp/my_site.socket", :backlog => 64

# Preload our app for more speed
preload_app true

# nuke workers after 30 seconds instead of 60 seconds (the default)
timeout 30

pid "/tmp/unicorn.my_site.pid"

# Production specific settings
if env == "production"
# Help ensure your application will always spawn in the symlinked
# "current" directory that Capistrano sets up.
working_directory "/home/deployer/apps/my_site/current"

# feel free to point this anywhere accessible on the filesystem
user 'deployer', 'staff'
shared_path = "/home/deployer/apps/my_site/shared"

stderr_path "#{shared_path}/log/unicorn.stderr.log"
stdout_path "#{shared_path}/log/unicorn.stdout.log"
end

before_fork do |server, worker|
# the following is highly recomended for Rails + "preload_app true"
# as there's no need for the master process to hold a connection
if defined?(ActiveRecord::Base)
ActiveRecord::Base.connection.disconnect!
end

# Before forking, kill the master process that belongs to the .oldbin PID.
# This enables 0 downtime deploys.
old_pid = "/tmp/unicorn.my_site.pid.oldbin"
if File.exists?(old_pid) && server.pid != old_pid
begin
Process.kill("QUIT", File.read(old_pid).to_i)
rescue Errno::ENOENT, Errno::ESRCH
# someone else did our job for us
end
end
end

after_fork do |server, worker|
# the following is *required* for Rails + "preload_app true",
if defined?(ActiveRecord::Base)
ActiveRecord::Base.establish_connection
end

# if preload_app is true, then you may also want to check and
# restart any other shared sockets/descriptors such as Memcached,
# and Redis.  TokyoCabinet file handles are safe to reuse
# between any number of forked children (assuming your kernel
# correctly implements pread()/pwrite() system calls)
end

[/geshi]

Ok, como você pode ver temos umas coisas bem legais aqui como reiniciar o servidor sem intervalos ( zero-downtime). Deixe-me contar mais algumas coisas sobre isso.

Unicorn iniciar o processo principal e gera diversos workers ( nós configuramos 4). Quando voce envia o sinal USR2 ao Unicorn, então ele renomeia o servidor principal (antigo) e cria um novo processo, que assume o serviço do master. O antigo serviço ainda continua rodando.

Agora, quando o novo serviço princial iniciar e segmentar esse trabalho, este checka o PID do novo e do antigo processo principal. Se eles forem diferentes, então o novo é iniciado e o para o antigo é enviado um sinal QUIT e shutdown gracefully.

Capistrano

Agora para o Capistrano, adicionando as seguintes linhas no Gemfile.

# Gemfile
group :development do
gem "capistrano"
end

E geramos os arquivos necessários para o capoistrano

capify .

Abra seu config/deploy.rb e substitua pelos seguintes comandos

Este script de deploy não é totalmente abrangente, mas possui um pulo do gato, quando deletamos a pasta atual, mantendo apenas a última versão do projeto, assim não acumulando espaço desnecessário.

# config/deploy.rb
require "bundler/capistrano"

set :scm,             :git
set :repository,      "git@codeplane.com:you/my_site.git"
set :branch,          "origin/master"
set :migrate_target,  :current
set :ssh_options,     { :forward_agent => true }
set :rails_env,       "production"
set :deploy_to,       "/home/deployer/apps/my_site"
set :normalize_asset_timestamps, false

set :user,            "deployer"
set :group,           "staff"
set :use_sudo,        false

role :web,    "123.456.789.012"
role :app,    "123.456.789.012"
role :db,     "123.456.789.012", :primary => true

set(:latest_release)  { fetch(:current_path) }
set(:release_path)    { fetch(:current_path) }
set(:current_release) { fetch(:current_path) }

set(:current_revision)  { capture("cd #{current_path}; git rev-parse --short HEAD").strip }
set(:latest_revision)   { capture("cd #{current_path}; git rev-parse --short HEAD").strip }
set(:previous_revision) { capture("cd #{current_path}; git rev-parse --short HEAD@{1}").strip }

default_environment["RAILS_ENV"] = 'production'

# Use our ruby-1.9.2-p290@my_site gemset
default_environment["PATH"]         = "--"
default_environment["GEM_HOME"]     = "--"
default_environment["GEM_PATH"]     = "--"
default_environment["RUBY_VERSION"] = "ruby-1.9.2-p290"

default_run_options[:shell] = 'bash'

namespace :deploy do
  desc "Deploy your application"
  task :default do
    update
    restart
  end

  desc "Setup your git-based deployment app"
  task :setup, :except => { :no_release => true } do
    dirs = [deploy_to, shared_path]
    dirs += shared_children.map { |d| File.join(shared_path, d) }
    run "#{try_sudo} mkdir -p #{dirs.join(' ')} && #{try_sudo} chmod g+w #{dirs.join(' ')}"
    run "git clone #{repository} #{current_path}"
  end

  task :cold do
    update
    migrate
  end

  task :update do
    transaction do
      update_code
    end
  end

  desc "Update the deployed code."
  task :update_code, :except => { :no_release => true } do
    run "cd #{current_path}; git fetch origin; git reset --hard #{branch}"
    finalize_update
  end

  desc "Update the database (overwritten to avoid symlink)"
  task :migrations do
    transaction do
      update_code
    end
    migrate
    restart
  end

  task :finalize_update, :except => { :no_release => true } do
    run "chmod -R g+w #{latest_release}" if fetch(:group_writable, true)

    # mkdir -p is making sure that the directories are there for some SCM's that don't
    # save empty folders
    run <<-CMD
      rm -rf #{latest_release}/log #{latest_release}/public/system #{latest_release}/tmp/pids &&
      mkdir -p #{latest_release}/public &&
      mkdir -p #{latest_release}/tmp &&
      ln -s #{shared_path}/log #{latest_release}/log &&
      ln -s #{shared_path}/system #{latest_release}/public/system &&
      ln -s #{shared_path}/pids #{latest_release}/tmp/pids &&
      ln -sf #{shared_path}/database.yml #{latest_release}/config/database.yml
    CMD

    if fetch(:normalize_asset_timestamps, true)
      stamp = Time.now.utc.strftime("%Y%m%d%H%M.%S")
      asset_paths = fetch(:public_children, %w(images stylesheets javascripts)).map { |p| "#{latest_release}/public/#{p}" }.join(" ")
      run "find #{asset_paths} -exec touch -t #{stamp} {} ';'; true", :env => { "TZ" => "UTC" }
    end
  end

  desc "Zero-downtime restart of Unicorn"
  task :restart, :except => { :no_release => true } do
    run "kill -s USR2 `cat /tmp/unicorn.my_site.pid`"
  end

  desc "Start unicorn"
  task :start, :except => { :no_release => true } do
    run "cd #{current_path} ; bundle exec unicorn_rails -c config/unicorn.rb -D"
  end

  desc "Stop unicorn"
  task :stop, :except => { :no_release => true } do
    run "kill -s QUIT `cat /tmp/unicorn.my_site.pid`"
  end

  namespace :rollback do
    desc "Moves the repo back to the previous version of HEAD"
    task :repo, :except => { :no_release => true } do
      set :branch, "HEAD@{1}"
      deploy.default
    end

    desc "Rewrite reflog so HEAD@{1} will continue to point to at the next previous release."
    task :cleanup, :except => { :no_release => true } do
      run "cd #{current_path}; git reflog delete --rewrite HEAD@{1}; git reflog delete --rewrite HEAD@{1}"
    end

    desc "Rolls back to the previously deployed version."
    task :default do
      rollback.repo
      rollback.cleanup
    end
  end
end

def run_rake(cmd)
  run "cd #{current_path}; #{rake} #{cmd}"
end

Agora, há uma coisa que você precisa fazer. Eu gosto de rodar minhas aplicações utilizando sua propria. Isso mantém tudo mais limpo e isolado. Entre como usuário deployer  e crie o seu gemset. preenchendo os PATH com GEM_HOME , ​​GEM_PATH.

Não se esqueça de instalar o Bundle em seu GEM SET.

Database configuration

Eu sempre gostei de colocar o arquiv de configuração fora do GIT, Eu o coloco no diretório SHARED.

# /home/deployer/apps/my_site/shared/database.yml
production:
adapter: postgresql
encoding: unicode
database: my_site_production
pool: 5
username: my_site
password: password

First setup

Agora realize o setup de seu deploy, assim:

cap deploy:setup

Isto irá colnar seu repositório e o conectar com seu database.yml .Opcionalmente, você pode rodar suas migrations ou fazer o upload de suas SQL para iniciar rapidamente sua aplicação.

Deployments

Quando você tiver uma nova feature em um branch, realize esse procedimento para realizar o deploy disso:

  1. Merge feature_branch into master
  2. Rode os testes para verificar se tudo está OK.
  3. Push master
  4. Execute o  cap deploy

 

 

asd