Предыстория


На создание этой статьи меня толкнула недавняя публикация о деплое. В приведённой статье описан способ разворачивания проекта на основе rbenv, я же опишу ситуацию с rvm и настройкой upstart.

Задачи и требования


Итак, дано: простейшее RubyOnRails-приложение. В случае разворачивания своего проекта я ставил перед собой следующие задачи:
  • ОС Ubuntu LTS 14.04;
  • Веб-сервер Nginx;
  • Сервер приложения Puma;
  • Использование RVM для установки ruby требуемой версии;
  • Автоматический запуск приложения при запуске VPS-сервера, возможность управлять приложением как службой;
  • Автоматизация процесса деплоя с помощью mina;

Почему Ubuntu?
Просто я к ней привык (точнее к её производной — Linux Mint).

Почему puma, а не unicorn или passenger?
О пуме я слышал неплохие отзывы, а у unicron страшненький сайт. Passenger, на мой взгляд, нарушает принцип единственной обязанности — я хочу иметь веб-сервер и приложение-сервер.

Почему RVM?
Я к нему привык — у меня он установлен на локальной машине, хочу его видеть и на production.

Почему mina?
Она действительно проще чем capistrano и при этом быстрее. Скорость достигается за счёт того, что для каждой задачи capistrano создаёт отдельное ssh-соединение. Mina же формирует shell-скрипт и выполняет его в рамках одного соединения.

В данном случае задача легко разбивается на 3 этапа:
  1. Убедиться, что приложение корректно запускается (пока без автоматизации);
  2. Настроить сервер так, чтобы наше rails-приложение работало как полноценная служба;
  3. На основе этого настроить автоматизированное разворачивание с помощью mina.


Первый запуск


На сервере создан пользователь webapp, от имени которого будет работать наше приложение. Установлен rvm (в данном примере только для пользователя webapp), ruby нужной нам версии, nginx и прочее. В Gemfile присутствует следующая строчка:
# ...
gem 'puma', group: :production
# ...

Предварительная настройка mina

Добавим в Gemfile следующую строчку:
gem 'mina', group: :development

Затем сгенерируем конфигурационный файл командой:
mina init

После чего приведём созданный файл config/deploy.rb к следующему виду:
config/deploy.rb
require 'mina/bundler'
require 'mina/rails'
require 'mina/git'

set :domain, 'awesome_address'
set :user, 'webapp'
set :deploy_to, '/home/webapp/awesome'
set :repository, 'https://github.com/awesome_user/awesome.git'
set :branch, 'master'

set :shared_paths, ['config/database.yml', 'config/secrets.yml', 'config/puma.rb', 'log']

task :setup => :environment do
  queue! %[mkdir -p "#{deploy_to}/#{shared_path}/log"]
  queue! %[chmod g+rx,u+rwx "#{deploy_to}/#{shared_path}/log"]

  queue! %[mkdir -p "#{deploy_to}/#{shared_path}/config"]
  queue! %[chmod g+rx,u+rwx "#{deploy_to}/#{shared_path}/config"]
  
  queue! %[mkdir -p "#{deploy_to}/#{shared_path}/puma"]
  queue! %[chmod g+rx,u+rwx "#{deploy_to}/#{shared_path}/puma"]

  queue! %[touch "#{deploy_to}/#{shared_path}/config/database.yml"]
  queue! %[touch "#{deploy_to}/#{shared_path}/config/secrets.yml"]
  queue! %[touch "#{deploy_to}/#{shared_path}/config/puma.rb"]
  queue  %[echo "-----> Be sure to edit '#{deploy_to}/#{shared_path}/config/database.yml', 'secrets.yml' and puma.rb."]
end

desc "Deploys the current version to the server."
task :deploy => :environment do
  deploy do
    # Put things that will set up an empty directory into a fully set-up
    # instance of your project.
    invoke :'git:clone'
    invoke :'deploy:link_shared_paths'
    invoke :'bundle:install'
    #invoke :'rails:db_migrate'
    #invoke :'rails:assets_precompile'
    #invoke :'deploy:cleanup'
  end
end


В данном файле закомментированы строки кода, отвечающие за выполнение рутинных операций по разворачиванию проекта. Пока эти действия мы будем выполнять вручную.

Также хочу отметить, что приведённый файл настройки mina я не добавляю в контроль версий.

После чего запустим команду:
mina setup

И получим необходимую структуру папок на сервере, а также конфигурационные файлы, которые необходимо заполнить корректными данными. Предполагается, что данные файлы вы не будете добавлять в контроль версий, а хранить исключительно на сервере. Ниже приведён пример минимальной конфигурации для puma.
puma.rb
environment "production"

bind  "unix:///home/webapp/awesome/shared/puma/puma.sock"
pidfile "/home/webapp/awesome/shared/puma/puma.pid"
state_path "/home/webapp/awesome/shared/puma/puma.state"

activate_control_app


Также я нарочно не храню файл puma.rb в контроле версий, т.к. считаю, что конфигурация запуска сервера может быть индивидуальна как и настройки базы данных.

Для заметки же достаточно создать в приложении файлик config/puma.sample.rb с примером.

Теперь же запустим «разворачивание» нашего приложения:
mina deploy

После выполнения данной команды, на сервере появиться символическая ссылка /home/webapp/awesome/current на папку с кодом нашего проекта.

Настройка nginx
Создадим файл /etc/nginx/sites-available/awesome со следующим содержимым:
awesome
upstream awesome {
  server unix:///home/webapp/awesome/shared/puma/puma.sock;
}

server {
  listen 80 default_server;
  listen [::]:80 default_server ipv6only=on;
  
  root /home/webapp/awesome/current/public;
  
  server_name localhost;

  location / {
    proxy_pass http://awesome;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }

  location ~* ^/assets/ {
    # Per RFC2616 - 1 year maximum expiry
    expires 1y;
    add_header Cache-Control public;

    # Some browsers still send conditional-GET requests if there's a
    # Last-Modified header or an ETag header even if they haven't
    # reached the expiry date sent in the Expires header.
    add_header Last-Modified "";
    add_header ETag "";
    break;
  }
}

После чего создадим символическую ссылку:
cd /etc/nginx/sites-enabled/
ln -s ../sites-available/awesome awesome

И перезапустим nginx:
service nginx restart

Ручной запуск приложения
Теперь подготовим и запустим наш проект.
Подготавливаем:
cd /home/webapp/awesome/current
bash --login
rvm use ruby-2.2.1
bundle install --without development:test --path ./vendor/bundle --deployment
RAILS_ENV=production bundle exec rake db:migrate
RAILS_ENV=production bundle exec rake assets:precompile

Запускаем:
bundle exec puma -C config/puma.rb

Открываем проект в браузере — всё должно работать.

Приложение как служба


Для того, чтобы приложение работало как служба и запускалось при старте/перезагрузке системы, нужно создать init.d-скрипт или upstart-конфигурацию.

Я выбрал upstart, потому как получаемый файл выходит намного короче и проще для восприятия.

Для того, чтобы корректно подружить upstart и rvm, создаём обётку для исполняемых файлов наших гемов:
rvm alias create awesome ruby-2.2.1@default

После чего создаём upstart-конфигурацию /etc/init/awesome.conf:
awesome.conf
description "Awesome puma service"

# This starts upon bootup and stops on shutdown
start on runlevel [2345]
stop on runlevel [06]

setuid webapp
setgid webapp

respawn
respawn limit 3 30

script
  cd /home/webapp/awesome/current
  /home/webapp/.rvm/wrappers/awesome/bundle exec puma -C config/puma.rb
end script

И теперь мы можем делать так:
start awesome
stop awesome
restart awesome
status awesome


Итоговая автоматизация


Так как деплой будет происходить от имени пользователя webapp, а для перезапуска службы нам нужны права суперпользователя, добавим соответствующую строчку в sudoers.

Запускаем команду:
visudo

Добавляем:
webapp ALL=(ALL) NOPASSWD: /sbin/start awesome, /sbin/stop awesome, /sbin/restart awesome, /sbin/status awesome

И сохраняем.

И снова mina
В подобных случаях mina предлагает использовать собственный модуль mina/rvm, однако rvm предоставляет нам возможность создавать псевдонимы для версий ruby и gemset-ов — wrapper-ы.

Для того, чтобы при деплое запускался именно бандлер wrappera, необходимо добавить следующую строку:
set :bundle_bin, '/home/webapp/.rvm/wrappers/awesome/bundle'

Также добавим задачу для перезапуска сервера:
desc "Restart the puma web server."
task :restart do
  queue 'sudo restart awesome'
end

Добавив всё это и раскомментировав необходимые строки в задаче deploy, получим следующее:
deploy.rb
require 'mina/bundler'
require 'mina/rails'
require 'mina/git'

set :domain, 'awesome_address'
set :user, 'webapp'
set :deploy_to, '/home/webapp/awesome'
set :repository, 'https://github.com/awesome_user/awesome.git'
set :branch, 'master'

set :bundle_bin, '/home/webapp/.rvm/wrappers/awesome/bundle'

set :shared_paths, ['config/database.yml', 'config/secrets.yml', 'config/puma.rb', 'log']

task :setup => :environment do
  queue! %[mkdir -p "#{deploy_to}/#{shared_path}/log"]
  queue! %[chmod g+rx,u+rwx "#{deploy_to}/#{shared_path}/log"]

  queue! %[mkdir -p "#{deploy_to}/#{shared_path}/config"]
  queue! %[chmod g+rx,u+rwx "#{deploy_to}/#{shared_path}/config"]

  queue! %[mkdir -p "#{deploy_to}/#{shared_path}/puma"]
  queue! %[chmod g+rx,u+rwx "#{deploy_to}/#{shared_path}/puma"]

  queue! %[touch "#{deploy_to}/#{shared_path}/config/database.yml"]
  queue! %[touch "#{deploy_to}/#{shared_path}/config/secrets.yml"]
  queue! %[touch "#{deploy_to}/#{shared_path}/config/puma.rb"]
  queue  %[echo "-----> Be sure to edit '#{deploy_to}/#{shared_path}/config/database.yml', 'secrets.yml' and puma.rb."]
end

desc "Deploys the current version to the server."
task :deploy => :environment do
  deploy do
    # Put things that will set up an empty directory into a fully set-up
    # instance of your project.
    invoke :'git:clone'
    invoke :'deploy:link_shared_paths'
    invoke :'bundle:install'
    invoke :'rails:db_migrate'
    invoke :'rails:assets_precompile'
    invoke :'deploy:cleanup'
    to :launch do
      invoke :'restart'
    end
  end
end


desc "Restart the puma web server."
task :restart do
  queue 'sudo restart awesome'
end


Всё готово!
Теперь можем ещё раз запустить разворачивание проекта и посмотреть на результат:
mina deploy


Альтернативы


В качестве альтернативы можно рассматривать puma-jungle.
Это шаблоны upstart и init.d скриптов, которые корректно отрабатывают с rbenv, rvm и прочими менеджерами версий.

В случае с rvm нужно сделать так, чтобы корректно определялась версия ruby и gemset, если он используется — для этого можно использовать .ruby-version и .ruby-gemset файлы.

Можно положить их в контроль версий, а можно создавать во время деплоя через mina. Также вам придётся использовать библиотеку mina/rvm и создать задачу для установки корректной версии руби и гемсета.

Всё вместе может выглядеть следующим образом:
deploy.rb
# ...

# Rvm ruby version and gemset
rvm = { ruby_version: 'ruby-2.2.1', ruby_gemset: 'default' }

task :environment do
  invoke :"rvm:use[#{rvm[:ruby_version]}@#{rvm[:ruby_gemset]}]"
end

# ...

desc "Creates appropriate .ruby-version and .ruby-gemset files."
task :'rvm:dot_files' do
  queue! %[echo "#{rvm[:ruby_version]}" > .ruby-version]
  queue! %[echo "#{rvm[:ruby_gemset]}" > .ruby-gemset]
end


task :setup => :environment do
  # ...
end

task :deploy => :environment do
  deploy do
    invoke :'git:clone'
    invoke :'rvm:dot_files'
    # ...
  end
end

# ...


Конец


На этом моя публикация заканчивается. Надеюсь, было интересно и полезно.

Комментарии (7)


  1. Anstak
    28.08.2015 10:41

    Читал похожую статью с rbenv, подумал «плохо что не написано на rvm вместо rbenv», теперь есть, спасибо! Надо будет как-нибудь попробовать, сейчас пользуюсь стандартной связкой rvm+nginx+unicorn+capistrano.


  1. andrewb
    28.08.2015 10:51
    +1

    Для автоматизации генерация upstart скриптов рекомендую посмотреть на Foreman. В Procfile (как на heroku) можно указывать не только веб-сервер, но и любые другие скрипты или приложения.


  1. Vizakenjack
    28.08.2015 19:59
    +2

    Почему Ubuntu?
    Просто я к ней привык (точнее к её производной — Linux Mint).

    Почему RVM?
    Я к нему привык — у меня он установлен на локальной машине, хочу его видеть и на production.

    Как-то непрофессионально, «я привык и мне норм, пользуйтесь тем же». Ну про ubuntu еще понятно, а вот насчет RVM я бы послушал, чем он лучше rbenv'a


    1. Metus
      28.08.2015 20:48

      Полагаю, мой аргумент в данном случае действительно не сильно профессионален. Однако, каждый из нас имеет право на выбор инструментов, пусть они и являются всего-лишь личными предпочтениями.
      Тем более это не является призывом использовать rvm вместо rbenv, а просто описание данного способа.


  1. or10n
    28.08.2015 23:33

    чем таким особенным обладает пума-конфиг, что его нельзя хранить в репозитории, но при этом можно хранить нгинкс-сайт-конфиг?


    1. Metus
      29.08.2015 00:12

      Я не храню nginx-конфиг в репозитории также.

      В принципе, ничего особенного в нём нет, но там могут быть указаны: количество воркеров, тредов и т.д.
      Если же проект opensource, то не уверен, что является хорошей идеей навязывать конкретные настройки сервера, ведь подразумевается «внутри git — не меняем».


      1. matiouchkine
        31.08.2015 11:24

        В миру обычно сам-то файл складывают в репозиторий, а внутри пишут что-то типа

        `workers: <%= ENV['WORKERS_COUNT'] %>`