Rails 5.1 Consulting und Schulung vom Autor:
www.wintermeyer-consulting.de/rails/

15.4. Webserver mit Capistrano-Deployment

In Abschnitt 15.3, „Webserver ohne Deployment“ haben wir ein neues Rails-Projekt auf dem Produktivsystem erstellt. Das ist logischweise meistens nicht der normale Weg. Normalerweise wird das Rails-Projekt von einem oder mehreren Entwicklern auf den eigenen Rechnern entwickelt und dann später auf den Produktivserver upgeloadet.
Gerade bei der Arbeit mit mehreren Entwicklern ist der Einsatz einer Versionsverwaltung angebracht. Mit Capistrano können Sie dann aus dieser Versionsverwaltung Updates des Rails-Projektes auf einen oder mehrere Webserver verteilen. In diesem Kapitel benutzen wir Git als verteilte Versionsverwaltung und Github (http://github.com) als Server zum Hosten des Gits.
Für dieses Tutorial benötigen Sie einen bereits fertig installierten Produktivserver mit nginx und einen mit RVM mit dem User deployer installierten Ruby 1.9.3 und Rails 3.2 (eine genaue Anleitung dazu finden Sie unter Abschnitt 15.2, „Grundinstallation Webserver“).

Entwicklungssystem

Wir fangen wieder mit einer neuen Rails-Applikation an. Bitte erstellen Sie diese Applikation auf Ihrem Entwicklungsrechner.

rails new blog

Wir erstellen die Mini-Blog Rail-Applikation:
Stefan-Wintermeyers-MacBook-Air:~ xyz$ rails new blog
[...]
Stefan-Wintermeyers-MacBook-Air:~ xyz$ cd blog
Stefan-Wintermeyers-MacBook-Air:blog xyz$ rails generate scaffold post subject content:text
[...]
Stefan-Wintermeyers-MacBook-Air:blog xyz$

Anmerkung

Falls Sie mit diesem System in der Entwicklungsumgebung arbeiten wollen, fehlt jetzt noch ein
Stefan-Wintermeyers-MacBook-Air:blog xyz$ rake db:migrate
==  CreatePosts: migrating ====================================================
-- create_table(:posts)
   -> 0.0015s
==  CreatePosts: migrated (0.0016s) ===========================================

Stefan-Wintermeyers-MacBook-Air:blog xyz$
Benötigte Gems für das Deployment
Für das Deployment und den Webserver benötigen wir einige Gems. Bitte fügen Sie diese Konfiguration in die Datei Gemfile:
source 'https://rubygems.org'

gem 'rails', '3.2.6'

gem 'sqlite3'

# Gems used only for assets and not required
# in production environments by default.
group :assets do
  gem 'sass-rails',   '~> 3.2.3'
  gem 'coffee-rails', '~> 3.2.1'

  # See https://github.com/sstephenson/execjs#readme for more supported runtimes
  # gem 'therubyracer', :platforms => :ruby

  gem 'uglifier', '>= 1.0.3'
end

gem 'jquery-rails'

# To use ActiveModel has_secure_password
# gem 'bcrypt-ruby', '~> 3.0.0'

group :production do
  # Use MySQL as the production database
  gem 'mysql'

  # Use unicorn as the app server
  gem 'unicorn'
end


group :development do
  # Use Capistrano for the deployment
  gem 'capistrano'
  gem 'rvm-capistrano'
end
Danach ein bundle install ausführen:
Stefan-Wintermeyers-MacBook-Air:blog xyz$ bundle install
[...]
Stefan-Wintermeyers-MacBook-Air:blog xyz$

Unicorn-Konfiguration

Für die Unicorn-Konfiguration nehmen wir als Basis die Datei https://raw.github.com/defunkt/unicorn/master/examples/unicorn.conf.rb und speichern sie an unseren Server angepasst wie folgt in der Datei config/unicorn.rb ab:
# Use at least one worker per core if you're on a dedicated server,
# more will usually help for _short_ waits on databases/caches.
worker_processes 4

# Since Unicorn is never exposed to outside clients, it does not need to
# run on the standard HTTP port (80), there is no reason to start Unicorn
# as root unless it's from system init scripts.
# If running the master process as root and the workers as an unprivileged
# user, do this to switch euid/egid in the workers (also chowns logs):
user "deployer", "www-data"

# Help ensure your application will always spawn in the symlinked
# "current" directory that Capistrano sets up.
APP_PATH = "/var/www/blog/current"
working_directory APP_PATH

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

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

# feel free to point this anywhere accessible on the filesystem
pid APP_PATH + "/tmp/pids/unicorn.pid"

# By default, the Unicorn logger will write to stderr.
# Additionally, ome applications/frameworks log to stderr or stdout,
# so prevent them from going to /dev/null when daemonized here:
stderr_path APP_PATH + "/log/unicorn.blog.stderr.log"
stdout_path APP_PATH + "/log/unicorn.blog.stdout.log"

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

before_exec do |server|
  ENV["BUNDLE_GEMFILE"] = "/var/www/blog/current/Gemfile"
end

Capistrano Konfiguration

Wir richten eine Capistrano Standardkonfiguration ein:
Stefan-Wintermeyers-MacBook-Air:blog xyz$ capify .  
[add] writing './Capfile'
[add] writing './config/deploy.rb'
[done] capified!
Stefan-Wintermeyers-MacBook-Air:blog xyz$
Danach richten wir die config/deploy.rb mit folgendem Inhalt ein. Bitte denken Sie daran, den Text ip.addresse.ihres.servers mit der IP-Addresse Ihres Webservers auszutauschen!
require "bundler/capistrano"
require "rvm/capistrano"
set :rvm_ruby_string, '1.9.3'

server "ip.addresse.ihres.servers", :web, :app, :db, primary: true

set :application, "blog"
set :user, "deployer"
set :deploy_to, "/var/www/#{application}"
set :deploy_via, :remote_cache
set :use_sudo, false

set :scm, "git"
set :repository, "git@github.com:ihr_github_account/#{application}.git"
set :branch, "master"

default_run_options[:pty] = true
ssh_options[:forward_agent] = true

after 'deploy', 'deploy:cleanup'
after 'deploy', 'deploy:migrate'

namespace :deploy do
  %w[start stop restart reload].each do |command|
    desc "#{command} unicorn server"
    task command, roles: :app, except: {no_release: true} do
      run "sudo /etc/init.d/unicorn_#{application} #{command}"
    end
  end

  # Use this if you know what you are doing.
  #
  # desc "Zero-Downtime restart of Unicorn"
  # task :restart, :except => { :no_release => true } do
  #   run "sudo /etc/init.d/unicorn_#{application} reload"
  # end
end
Und die Datei Capfile müssen wir noch wie folgt abändern:
load 'deploy'
# Uncomment if you are using Rails' asset pipeline
load 'deploy/assets'
Dir['vendor/gems/*/recipes/*.rb','vendor/plugins/*/recipes/*.rb'].each { |plugin| load(plugin) }
load 'config/deploy' # remove this line to skip loading any of the default tasks

Github als Repository einrichten

Bitte legen Sie sich unter https://github.com einen neuen Account an oder benutzen Sie einen bereits existierenden Github-Account. Legen Sie mit diesem Account ein neues Repository namens "blog" an.

Tipp

Für ein einfacheres Arbeiten empfehle ich Ihnen Ihren SSH Key in Ihrem Github-Account unter https://github.com/settings/ssh einzutragen.
Jetzt können Sie Ihr Projekt commiten und pushen. Bitte tauschen Sie dabei ihr_github_account mit Ihrem Github-Account aus:
Stefan-Wintermeyers-MacBook-Air:blog xyz$ git init
Initialized empty Git repository in /Users/xyz/blog/.git/
Stefan-Wintermeyers-MacBook-Air:blog xyz$ git add .
Stefan-Wintermeyers-MacBook-Air:blog xyz$ git commit -m 'first commit'
[...]
Stefan-Wintermeyers-MacBook-Air:blog xyz$ git remote add origin git@github.com:ihr_github_account/blog.git
Stefan-Wintermeyers-MacBook-Air:blog xyz$ git push -u origin master
[...]
Stefan-Wintermeyers-MacBook-Air:blog xyz$
Jetzt ist Ihr Rails-Projekt in einem Github-Repository gehostet und kann von Ihnen unter https://github.com/ihr_github_account/blog eingesehen werden.

Webserver

Folgende Schritte müssen Sie auf dem Webserver-System durchführen.

SSH Key generieren

Für den User deployer erstellen wir einen public SSH-Key. Bitte loggen Sie sich auf dem Webserver als User deployer ein. Am einfachsten ist das Deployment später bei der Verwendung einer leeren Passphrase.
deployer@debian:~$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/deployer/.ssh/id_rsa): 
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/deployer/.ssh/id_rsa.
Your public key has been saved in /home/deployer/.ssh/id_rsa.pub.
The key fingerprint is:
ba:11:90:2a:e3:8f:5b:2e:70:99:50:86:a1:9a:2c:b7 deployer@debian
The key's randomart image is:
+--[ RSA 2048]----+
|.o               |
|o o  .           |
|.o  o            |
|+. . .           |
|*ooo  . S        |
|+++.   o         |
|.oE.  o          |
| .=    o         |
| ooo  .          |
+-----------------+
deployer@debian:~$ cat .ssh/id_rsa.pub 
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDJEGixOcPRdBMry7PPG/Rgla50EM+JPKGYGD/yJ8v7bdrfT68t2/eVbj6+YebWh1tRebE3qqouqmZjIlocr1j67SmfXZ/sswBT/pXOhP89JtHPMolx7rUQ8wQF3aDrnVDJG0gdvRm212vN2bou3N5dzhekmWmbS3R0ZGNM9ZgTw8rhTOd1M2QVTzyV1i1PehoFxOu1WIc1gN5C42zihbJ6fGgVb45WeKzXSi6bQ6PMKD1gAMJpXHPvKLhi0wLN0wNOJwa6BKR3pmgICSBuoziAhhCS/7gBDJnqRmx1zax/1CShJD3QEGHvofA9okYuYVqyrJi1hdF8ZgMnQCb31I21 deployer@debian
deployer@debian:~$
Der erzeugte Key liegt in der Datei /home/deployer/.ssh/id_rsa.pub.

Wichtig

Bitte loggen Sie sich jetzt in Ihren Github-Account ein und fügen im Admin-Bereich Ihres Github-Projektes diesen Key bei Deploy Keys hinzu (siehe https://github.com/ihr_github_account/blog/admin/keys).
Danach verbinden Sie sich mit ssh auf der Console einmal mit dem Github SSH-Server und bestätigen Sie die "Are you sure you want to continue connection (yes/no)?" Frage mit einem yes.
deployer@debian:~$ ssh git@github.com
The authenticity of host 'github.com (207.97.227.239)' can't be established.
RSA key fingerprint is 16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'github.com,207.97.227.239' (RSA) to the list of known hosts.
PTY allocation request failed on channel 0
Hi ihr_github_account/blog! You've successfully authenticated, but GitHub does not provide shell access.
                 Connection to github.com closed.
deployer@debian:~$
Jetzt können Sie auf Ihrem Entwicklungssystem mit dem Deployen anfangen.

cap deploy:setup

Mit cap deploy:setup können wir die notwendige Verzeichnisstruktur auf dem Zielsystem einrichten. Wenn Sie Ihren SSH-Key nicht auf dem Zielsystem abgelegt haben, werden Sie vom Skript nach dem Passwort des Users deployer gefragt. Je nach Anbindung und CPU-Power kann dieser initiale Setup-Schritt länger dauern.
Stefan-Wintermeyers-MacBook-Air:blog xyz$ cap deploy:setup
[...]
Stefan-Wintermeyers-MacBook-Air:blog xyz$ 
Nach dem cap deploy:setup finden Sie auf Ihrem Webserver die folgende Verzeichnisstruktur:
/var/www/blog
├── releases
└── shared
    ├── log
    ├── pids
    └── system
Das erste Deploy können wir mit cap deploy starten:
Stefan-Wintermeyers-MacBook-Air:blog xyz$ cap deploy
[...]
Stefan-Wintermeyers-MacBook-Air:blog xyz$ 
Danach sehen wir das Ergebnis des Deploys wieder in der Verzeichnisstruktur des Webservers:
/var/www/blog
├── current -> /var/www/blog/releases/20120711131031
├── releases
│   └── 20120711131031
└── shared
    ├── assets
    ├── bundle
    ├── cached-copy
    ├── log
    ├── pids
    └── system
Capistrano hat für die neue Applikations-Version im Unterverzeichnis /var/www/blog/releases ein neues Verzeichnis angelegt und dieses mit dem /var/www/blog/current-Verzeichnis verlinkt.

Webserver-Konfiguration

Jetzt müssen wir noch ein Init-Skript und eine Konfigurationsdatei auf dem Webserver schreiben und aktivieren.

Unicorn Init-Skript

Bitte loggen Sie sich als User root auf dem Webserver ein und erstellen Sie das Init-Skript /etc/init.d/unicorn_blog mit folgendem Inhalt:
#!/bin/bash

### BEGIN INIT INFO
# Provides:          unicorn
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Unicorn webserver
# Description:       Unicorn webserver for the blog
### END INIT INFO

UNICORN=/home/deployer/.rvm/bin/bootup_unicorn
UNICORN_ARGS="-D -c /var/www/blog/current/config/unicorn.rb -E production"
KILL=/bin/kill
PID=/var/www/blog/shared/pids/unicorn.pid

sig () {
  test -s "$PID" && kill -$1 `cat $PID`
}

case "$1" in
        start)
                echo "Starting unicorn..."
                $UNICORN $UNICORN_ARGS
                ;;
        stop)
                sig QUIT && exit 0
                echo >&2 "Not running"
                ;;
        reload)
                sig USR2 && exit 0
                ;;
        restart)
                $0 stop
                $0 start
                ;;
        status)
                ;;
        *)
                echo "Usage: $0 {start|stop|reload|restart|status}"
                ;;
esac
Das Init-Skript muss jetzt noch scharf geschaltet und Unicorn gestartet werden:
root@debian:~# chmod +x /etc/init.d/unicorn_blog 
root@debian:~# update-rc.d -f unicorn_blog defaults
update-rc.d: using dependency based boot sequencing
root@debian:~# /etc/init.d/unicorn_blog start
root@debian:~# 
Ihr Rails-Projekt ist jetzt über die IP-Adresse des Webservers erreichbar.

nginx-Konfiguration

Für das Rails-Projekt fügen wir eine neue Konfigurationdatei /etc/nginx/conf.d/blog.conf mit folgendem Inhalt hinzu:
upstream unicorn {
  server unix:/tmp/unicorn.blog.sock fail_timeout=0;
}

server {
  listen 80 default deferred;
  # server_name example.com;
  root /var/www/blog/current/public;

  location / {
    gzip_static on;
  }

  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

  try_files $uri/index.html $uri @unicorn;
  location @unicorn {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://unicorn;
  }

  error_page 500 502 503 504 /500.html;
  client_max_body_size 4G;
  keepalive_timeout 10;
}
Die Default-Konfigurationsdatei benennen wir um, damit sie nicht mehr ausgeführt wird. Danach restarten wir nginx.
root@debian:~# mv /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.backup
root@debian:~# /etc/init.d/nginx restart
Restarting nginx: nginx.
root@debian:~#

sudo

Im Capistrano Deploy-Skript brauchen wir eine Möglichkeit, Unicorn per Init-Skript zu stoppen und starten. Dazu müssen wir sudo auf dem Webserver installieren:
root@debian:~# apt-get install sudo
[...]
root@debian:~# 
Und in der Datei /etc/sudoers die folgende Zeile hinzufügen:
deployer ALL= NOPASSWD: /etc/init.d/unicorn_blog

Deployment

Der große Vorteil der Arbeit mit Capistrano ist das einfache Einspielen neuer Versionen (das Deployment). Dies geht immer vom Entwicklungssystem mit dem Befehl cap deploy aus. Nach einem zweiten cap deploy sieht die Verzeichnisstruktur folgendermaßen aus:
/var/www/blog
├── current -> /var/www/blog/releases/20120711132357
├── releases
│   ├── 20120711131031
│   └── 20120711132357
└── shared
    ├── assets
    ├── bundle
    ├── cached-copy
    ├── log
    ├── pids
    └── system
So werden im Verzeichnis /var/www/blog/releases immer die jeweiligen Releases gespeichert.
Im Verzeichnis /var/www/blog/shared finden Sie Verzeichnisse, die vom jeweiligen Release geteilt (shared) werden. Diese werden immer automatisch innerhalb von /var/www/blog/current verlinkt.
Mit der von uns verwendeten Konfiguration werden automatisch die letzten 5 Releases gespeichert und alle älteren gelöscht.

Tipp

Capistrano ist ein sehr mächtiges Tool. Es lohnt sich für jeden Admin mal einen Blick auf das Capistrano Wiki (https://github.com/capistrano/capistrano/wiki) zu werfen.

Autor

Stefan Wintermeyer