Предыстория
На создание этой статьи меня толкнула недавняя публикация о деплое. В приведённой статье описан способ разворачивания проекта на основе 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 этапа:
- Убедиться, что приложение корректно запускается (пока без автоматизации);
- Настроить сервер так, чтобы наше rails-приложение работало как полноценная служба;
- На основе этого настроить автоматизированное разворачивание с помощью mina.
Первый запуск
На сервере создан пользователь webapp, от имени которого будет работать наше приложение. Установлен rvm (в данном примере только для пользователя webapp), ruby нужной нам версии, nginx и прочее. В Gemfile присутствует следующая строчка:
# ...
gem 'puma', group: :production
# ...
Предварительная настройка mina
Добавим в Gemfile следующую строчку:
gem 'mina', group: :development
Затем сгенерируем конфигурационный файл командой:
mina init
После чего приведём созданный файл 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.
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 со следующим содержимым:
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:
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, получим следующее:
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 и создать задачу для установки корректной версии руби и гемсета.
Всё вместе может выглядеть следующим образом:
# ...
# 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)
Vizakenjack
28.08.2015 19:59+2Почему Ubuntu?
Просто я к ней привык (точнее к её производной — Linux Mint).
Почему RVM?
Я к нему привык — у меня он установлен на локальной машине, хочу его видеть и на production.
Как-то непрофессионально, «я привык и мне норм, пользуйтесь тем же». Ну про ubuntu еще понятно, а вот насчет RVM я бы послушал, чем он лучше rbenv'aMetus
28.08.2015 20:48Полагаю, мой аргумент в данном случае действительно не сильно профессионален. Однако, каждый из нас имеет право на выбор инструментов, пусть они и являются всего-лишь личными предпочтениями.
Тем более это не является призывом использовать rvm вместо rbenv, а просто описание данного способа.
or10n
28.08.2015 23:33чем таким особенным обладает пума-конфиг, что его нельзя хранить в репозитории, но при этом можно хранить нгинкс-сайт-конфиг?
Metus
29.08.2015 00:12Я не храню nginx-конфиг в репозитории также.
В принципе, ничего особенного в нём нет, но там могут быть указаны: количество воркеров, тредов и т.д.
Если же проект opensource, то не уверен, что является хорошей идеей навязывать конкретные настройки сервера, ведь подразумевается «внутри git — не меняем».matiouchkine
31.08.2015 11:24В миру обычно сам-то файл складывают в репозиторий, а внутри пишут что-то типа
`workers: <%= ENV['WORKERS_COUNT'] %>`
Anstak
Читал похожую статью с rbenv, подумал «плохо что не написано на rvm вместо rbenv», теперь есть, спасибо! Надо будет как-нибудь попробовать, сейчас пользуюсь стандартной связкой rvm+nginx+unicorn+capistrano.