Год назад я довел свое первое рельсовое приложение до приемлемого вида. Вопрос использования готового кода в продакшене ранее меня не заинтересовал. С чего вдруг? Несложный язык, лаконичный фреймворк — уж деплой-то явно не сложнее, чем преодоление ментального тормоза после PHP.
Команда разработчиков Rails рекомендует использовать Phusion Passenger, он что-то вроде mod_php — установил, разместил файлы и полетел. На момент изучения вопроса на форумах хватало баталий о производительности решений; Passenger в них фаворитом не значился.
Совета относительно альтернативы я спросил у техдиректора сайта с миллионом уников в сутки — тот отправил меня гуглить на тему Nginx и Unicorn. Инструкция по настройке продакшена, найденная на Хабре, датировалась 2009 годом. Помимо прочего, ее просто переполняли изъяны уроков «Как нарисовать сову».
Отдельные составляющие процесса кое-где разжеваны по-английский, но монолитный tutorial на глаза так и не попался. В традициях рельсового сообщества лежит принцип, предписывающий делиться результатами и опытом решения проблем.
Опытным рельсоводам текст вряд ли покажется интересным, но если таковые найдут время на замечания — буду признателен и внесу необходимые правки.
— О чем пойдет речь?
— Эта инструкция поможет новичкам подобрать и настроить
— А подробнее?
— Мы будем использовать Ubuntu 14.04, RVM, Nginx, Unicorn и Capistrano. Текст состоит из двух глав: подготовки проекта и настройки сервера. Все локальные манипуляции описываются в Mac OS X. Для выполнения процедур не будет лишней современная IDE вроде RubyMine, если нет — сойдет и TextMate. Все описываемые действия зафиксированы в специальном репозитории.
Глава первая Хостинг
Выбор хостинга
Сладкая парочка Nginx и Unicorn имеет весьма внушительный аппетит на оперативную память, а сами рельсы требуют установки ряда дополнительного ПО. Эти ограничения явно говорят о необходимости
В рамках этого текста я буду использовать свежесозданный дроплет Digital Ocean на базе Ubuntu 14.04. (Промо-кодов на месяц—два бесплатного пользования в интернете хватает, кому нужна рефералка с десятью долларами на счете — дам ссылку.)
Обновление системных пакетов
Заходим в систему под рутом и обновляем пакеты:
sudo apt-get update
sudo apt-get upgrade
Установка Git и NodeJS
sudo apt-get install git-core nodejs
Создание пользователя
Предполагаю, что у вас (как и у меня) есть коммерческий интерес к деплою. Создадим отдельного пользователя (другими словами — клиента), в домашней директории которого мы и будем разворачивать приложение. Помимо очевидного, такой подход дает преимущества в виде отдельного RVM (ruby version manager), что позволит использовать разные версии интерпретатора и гемов для разных клиентов и приложений. Мы создадим пользователя demo
и добавим его в группу sudo
.
sudo adduser demo
sudo adduser demo sudo
Закрываем сессию, заходим в систему как пользователь demo
.
Отмена запроса пароля для sudo
Для некоторых процедур деплоя вам потребуются привилегии суперпользователя. Чтобы команды исполнялись с помощью Capistrano и не вызывали ошибок, необходимо отключить запрос пароля. Отредактируйте файл sudoers
: sudo nano /etc/sudoers.
# Найдите следующую строку:
%sudo ALL=(ALL:ALL) ALL
# И приведите ее к виду:
%sudo ALL=(ALL) NOPASSWD:ALL
Генерация SSH-ключей
Потребуются две пары ключей. С помощью одной из них вы (и Capistrano) будете проходить авторизацию на сервере с локального компьютера. Второй парой вы предоставите серверу доступ в репозиторий (так называемый ключ развертывания). Кодовые фразы в обоих случаях оставьте пустыми.
Первая пара (необходимо генерировать локально):
ssh-keygen -t rsa -b 2048
Если вы знакомы с процедурой генерации и использования ключей, то путь к файлу и стойкость выбирайте по своему вкусу; иначе — оставьте по умолчанию.
Теперь необходимо скопировать содержимое публичного ключа (по умолчанию ~/.ssh/id_rsa.pub
на локальном компьютере) и добавить его в файл ~/.ssh/authorized_keys
на сервере. После этой нехитрой манипуляции с помощью SSH вы сможете подключиться к серверу без пароля. Если нет — проверьте права на сервере: 700 для ~/.ssh
и 600 для ~/.ssh/*
.
Вторая пара (на сервере):
ssh-keygen -t rsa -b 2048
Аналогичным образом содержимое из серверного ~/.ssh/id_rsa.pub
нужно добавить в список ключей развертывания (в Гитхабе их можно найти в настройках каждого репозитория).
Установка свежего Nginx
Версия Nginx, доступная в Ubuntu, зачастую более старая, нежели в официальном репозитории разработчика. Я стараюсь использовать свежую версию, но если вы других взглядов — установите Nginx самостоятельно и пропустите эту часть.
Мы добавим официальные репозитории Ubuntu в системный список sudo nano /etc/apt/sources.list. В конец файла добавим строки:
# Nginx official repository
deb http://nginx.org/packages/ubuntu/ trusty nginx
deb-src http://nginx.org/packages/ubuntu/ trusty nginx
Помните, что использованные параметры актуальны только для Ubuntu 14.04. Информацию по установке на другие версии ОС ищите на сайте разработчика. Для установки Nginx из указанных репозиториев потребуется также загрузить и добавить в систему ключ:
wget http://nginx.org/keys/nginx_signing.key
sudo apt-key add nginx_signing.key
Теперь можно обновить список доступных пакетов и установить Nginx:
sudo apt-get update
sudo apt-get install nginx
Удалим дефолтные конфиги из /etc/nginx/conf.d
. (Разумеется, не вздумайте делать этого, если работаете не на «чистом» сервере.) Виртуальный хост для приложения мы создадим в следующей главе.
sudo rm /etc/nginx/conf.d/*
sudo service nginx restart
Установка RVM
curl -sSL https://get.rvm.io | bash -s stable
source ~/.rvm/scripts/rvm
В случае с чистым сервером понадобится установить также некоторые зависимости:
rvm requirements
Осталось установить непосредственно Ruby необходимой вашему приложению версии. К примеру, новую стабильную версию 2.1.3:
rvm install 2.1.3
rvm use 2.1.3
Проверить корректность установки можно с помощью команд ruby -v и rvm info
Установка компонентов приложения
Наверняка, ваше приложение будет использовать еще ряд компонентов (не включая гемы), вроде базы данных или графического процессора Imagemagick, их установку придется закончить самостоятельно. Тут стоит добавить небольшую ремарочку (если забыть про которую — автоматический деплой будет завершаться ошибкой): рельсам для работы с некоторыми компонентами иногда требуются дополнительные пакеты. Например, для использования MySQL вам понадобится установить, помимо прочего, пакет libmysqlclient-dev
.
Глава вторая Подготовка приложения
Использование Git
Я предполагаю, что у вас уже есть готовое приложение. Первое правило — проект должен находиться под управлением Git. Это де-факто стандарт в рельсовом мире (даже файл .gitignore в корне создаваемых рельсовых приложений недвусмысленно намекает на это). Более того, последние версии гема Capistrano, который будет отвечать непосредственно за деплой, нативно поддерживают только эту систему.
Что такое Capistrano
Официальное определение Capistrano звучит так: «A remote server automation and deployment tool». С точки зрения пользователя, Capistrano — штука, которая позволит выполнить произвольный набор команд на удаленном сервере через SSH. Существует и другие инструменты для деплоя (например, Mina), но пока Capistrano также некий стандарт, тем более, что позволяет выполнять параллельный деплой приложения сразу на ряд серверов, в том числе разделенных по ролям.
Принцип работы Capistrano
На сервере структура приложения под контролем Capistrano в целом состоит из трех директорий: repo
, releases
и shared
. В первой хранится копия репозитория, во второй — релизы, в третьей — общие файлы, необходимые приложению и не зависящие от релиза. Также в корне присутствует симлинк current
, ссылающийся на версию текущего релиза и лог-файл деплоев.
Когда вы (со своего локального компьютера) отдаете команду Capistrano выполнить деплой, устанавливается SSH-соединение с сервером и начинается выполнение нехитрого алгоритма. Для начала Capistrano сверяется с удаленным репозиторием и получает недостающие коммиты. После создается новый релиз (в директории releases
). Туда перекладывается актуальная версия кода и там же выполняется ряд тестов.
Проще говоря, для каждого нового релиза Capistrano выполняет привычные команды вроде bundle install
, rake db:migrate
, rake assets:precompile
, постоянно проверяя наличие конфликтов и ошибок. Пропустили точку с запятой в default.scss
, не закомитили актуальный Gemfile.lock
, подключили Paperclip, но забыли установить на сервере Imagemagic? Во всех этих случаях Capistrano покажет ошибку и прекратит установку, никак не затронув текущий работающий релиз. Если деплой прошел удачно, но результат вас не устроил, с помощью Capistrano можно сделать rollback
к предыдущему релизу.
Организация файлов конфигураций
Каждый релиз для Capistrano — самостоятельная сущность. При очередном деплое происходит ее полная замена, поэтому ряд файлов необходимо хранить за пределами структуры приложения (как правило, загружаемый пользователями и генерируемый приложением контент, а также ряд конфигов).
Нам понадобится общая директория, необходимые файлы из которой мы будем линковать к каждому новому релизу при деплое; назовем ее shared и создадим локально в корне проекта mkdir ./shared, предварительно добавив исключение в .gitignore. (Я, разумеется, в учебный репозиторий исключение добавлять не буду.)
Теперь в общей директории создадим заранее будущую структуру. Для начала нам понадобятся папки config
и run
. В config
положите актуальные для боевого сервера database.yml
и secrets.yml
. (Лично я предпочитаю уже на локальном компьютере переместить два этих файла из config, где потом создать на них ссылки.)
mv ./config/database.yml ./shared/config
ln -fs ./shared/config/database.yml ./config/database.yml
mv ./config/secrets.yml ./shared/config
ln -fs ./shared/config/secrets.yml ./config/secrets.yml
Конфигурация Nginx
Здесь же — в shared/config
, — мы создадим конфиг для Nginx shared/config/nginx.conf
. В базовом виде он состоит из двух небольших частей: апстрима и типичного виртуального хоста. Будьте особенно внимательны с путями (в этом и всех дальнейших) в конфигурационных фалах. 90% ошибок, возникавших при деплое, были связаны именно с ними.
upstream unicorn {
server unix:/home/demo/application/shared/run/unicorn.sock fail_timeout=0;
}
server {
listen 80 default;
root /home/demo/application/current/public;
try_files $uri/index.html $uri.html $uri @app;
location ^~ /assets/ {
expires max;
add_header Cache-Control public;
}
location @app {
proxy_pass http://unicorn;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
}
error_page 500 502 503 504 /500.html;
location = /500.html {
root /home/demo/application/current/public;
}
}
Мы используем наш
Конфигурация Unicorn
Раз уж мы создали апстрим, в котором указали путь к сокету Unicorn, давайте перейдем и к его конфигу. Необходимо добавить гем Unicorn в Gemfile
; я также укажу конкретную версию (такая педантичность когда-нибудь убережет вас от ошибок из-за обратной несовместимости новых версий некоторых гемов). Не забывайте, что добавление новых гемов должно сопровождаться выполнением bundle install и комитом в репозиторий. Если забыть про git push, то на сервер будет загружена старая версия кода, которая, в силу отсутствия указанных гемов, будет вызывать ошибки при деплое.
group :production do
gem 'unicorn', '~> 4.8.3'
end
В директории shared/config
создайте файл unicorn.rb
. По большей части в нем мы должны указать пути к составным частям нашего приложения. Для удобства это можно делать с использованием переменных.
# Рабочие директории приложения на сервере
root = '/home/demo/application'
rails_root = "#{root}/current"
# Файлы, хранящие идентификаторы запущенных Unicorn-процессов
pidfile = "#{root}/shared/run/unicorn.pid"
pidfile_old = pidfile + '.oldbin'
pid pidfile
# Главные параметры
worker_processes 1
preload_app true
timeout 30
# Путь к сокету
listen "#{root}/shared/run/unicorn.sock", :backlog => 1024
# Путь к лог-файлам
stderr_path "#{rails_root}/log/unicorn_error.log"
stdout_path "#{rails_root}/log/unicorn.log"
# Установки сборщика мусора
GC.copy_on_write_friendly = true if GC.respond_to?(:copy_on_write_friendly=)
# Блок инструкций, выполняемых до запуска сервера
before_exec do |server|
ENV["BUNDLE_GEMFILE"] = "#{rails_root}/Gemfile"
end
# Инструкции для управления воркерами и состоянием соединения с БД
before_fork do |server, worker|
defined?(ActiveRecord::Base) and
ActiveRecord::Base.connection.disconnect!
if File.exists?(pidfile_old) && server.pid != pidfile_old
begin
Process.kill("QUIT", File.read(pidfile_old).to_i)
rescue Errno::ENOENT, Errno::ESRCH
end
end
end
after_fork do |server, worker|
defined?(ActiveRecord::Base) and
ActiveRecord::Base.establish_connection
end
Также указываются некоторые системные значения (полный список и документация). Обратить внимание следует на количество воркеров — worker_processes
, — каждый из которых употребит некоторое количество памяти (сколько именно — зависит от приложения) и на timeout
(обычно от 15 до 30 секунд). Многие упоминают и про preload_app
(значение false
которого может сократить время старта воркера); давайте пока остановимся на true
, а потом вы решите сами.
Подключение и настройка Capistrano
Необходимые гемы
В Gemfile
нужно внести Capistrano и серию гемов, реализующих его связь с RVM, Bundler и Rails.
group :development do
gem 'capistrano', '~> 3.2.1'
gem 'capistrano-rvm', '~> 0.1.1'
gem 'capistrano-bundler', '~> 1.1.3'
gem 'capistrano-rails', '~> 1.1.2'
end
Осталось выполнить bundle install, после чего инициализировать Capistrano с помощью cap install. Будет создан набор файлов, список которых вы увидите в консоли. Мы будем работать с тремя из них: Capfile
, config/deploy.rb
и config/deploy/production.rb
. (В config/deploy
Capistrano создает дефолтные файлы staging.rb
и production.rb
. Мы настроим только боевой сервер с помощью production.rb
.)
Обновление Capfile
Все гемы, связанные с Capistrano, функциональность которых мы собираемся использовать, необходимо подключить в создавшемся в корне Capfile
. Взгляните на дефолтный файл и раскомментируйте нужные строки или используйте следующее содержимое:
require 'capistrano/setup'
require 'capistrano/deploy'
require 'capistrano/rvm'
require 'capistrano/bundler'
require 'capistrano/rails'
Dir.glob('lib/capistrano/tasks/*.rb').each { |r| import r }
Настройка продакшен-сервера
Откройте файл config/deploy/production.rb
и внимательно посмотрите на его содержимое. Оно поможет вам разобраться в формате и возможностях настройки. В целом, если вы используете авторизацию с помощью SSH-ключа, никакой экзотики писать не придется; всего одна строчка:
server '178.62.252.46', user: 'demo', roles: %w{web app}
Сценарий деплоя
Мы добрались до самой интересной части, файла config/deploy.rb
. Именно он описывает параметры, процедуры и сценарий предстоящего деплоя. Я опишу каждый блок файла, но если хотите взглянуть на него в завершенном виде — воспользуйтесь репозиторием.
Обязательные параметры
Прежде всего, требуется указать версию Capistrano, для которой предназначен данный сценарий:
lock '3.2.1'
Capistrano требует ряд обязательных параметров:
# Репозиторий
set :repo_url, 'git@github.com:eboyko/deneb.git'
# Используемое окружение
set :rails_env, 'production'
А также параметр deploy_to
, определяющий путь для деплоя приложения на сервере. Выше мы уже говорили, что подразумеваем коммерческий интерес, поэтому сделаем файл сценария более универсальным с помощью переменных и зададим недостающий параметр:
# Имя пользователя
set :username, 'demo'
# Имя приложения
set :application, 'application'
# Путь для деплоя
set :deploy_to, "/home/#{fetch(:username)}/#{fetch(:application)}"
Установим также параметр log_level
(дефолтное значение :debug
делает Capistrano излишне разговорчивым):
set :log_level, :info
Заметьте, глобальные переменные Capistrano (вроде shared_path
) можно использовать напрямую; заданные с помощью set
— через метод fetch
.
Релизонезависимые данные
Мы уже рассмотрели необходимость доступа работающего релиза к данным, созданным предыдущими версиями. К примеру, вы храните файлы, загружаемые пользователями, в public/upload
. Чтобы подключать их к каждому новому релизу, в config/deploy.rb
можно задать параметр :linked_dirs
:
set :linked_dirs, %w{public/upload}
При каждом деплое public/upload
будет заменяться симлинком, ведущим на директорию shared/public/upload
, где и скапливались данные за время работы предыдущих релизов. Аналогичным образом, с помощью :linked_files
, линкуются отдельные файлы:
set :linked_files, %w{config/secrets.yml config/database.yml}
Учтите, что добавление в :linked_files
указанных файлов (config/secrets.yml
и config/database.yml
) обязательно. В противном случае деплой завершится ошибкой по причине отсутствия подключения к базе данных.
Процедуры
Capistrano позволяет создавать наборы процедур, которые для удобства можно объединять в пространства имен. Неймспейс :deploy
уже существует, его можно лишь дополнить; все включенные в него процедуры для сервера production
можно вызвать командой cap production deploy
Сет-ап
В результате предыдущих шагов у нас сформировалась директория shared
, в которой, помимо прочего, лежат файлы конфигураций (shared/config
). Логичным первым шагом будет загрузить их на сервер. Для этого все в том же файле config/deploy.rb
, в неймспейсе :setup
, мы напишем процедуру:
namespace :setup do
desc 'Загрузка конфигурационных файлов на удаленный сервер'
task :upload_config do
on roles :all do
execute :mkdir, "-p #{shared_path}"
['shared/config', 'shared/run'].each do |f|
upload!(f, shared_path, recursive: true)
end
end
end
end
Как вы понимаете, процедура подразумевает создание shared_path
(потому как на сервере еще нет никакой структуры) и загрузку туда локальной директории shared/config
. Вы можете выполнить ее с помощью команды cap production setup:upload_config
Управление Nginx
Я отмечал выше, что Capistrano по сути — способ выполнить произвольные команды на удаленном сервере. Мы загрузили конфигурационные файлы, в том числе для Nginx. Теперь напишем несколько процедур для управления: создание симлинка на конфиг и релоад/рестарт сервиса (для них потребуются права sudo
).
namespace :nginx do
desc 'Создание симлинка в /etc/nginx/conf.d на nginx.conf приложения'
task :append_config do
on roles :all do
sudo :ln, "-fs #{shared_path}/config/nginx.conf /etc/nginx/conf.d/#{fetch(:application)}.conf"
end
end
desc 'Релоад nginx'
task :reload do
on roles :all do
sudo :service, :nginx, :reload
end
end
desc 'Рестарт nginx'
task :restart do
on roles :all do
sudo :service, :nginx, :restart
end
end
after :append_config, :restart
end
Заметили приятную мелочь? — Можно задать последовательность исполнения различных процедур как в рамках одного неймспейса, так и между ними.
Управление Unicorn
Наверняка уже есть гем, расширяющий Capistrano в плоскости управления Unicorn, но мне было весьма интересно узнать, как он на самом деле устроен и работает. Поэтому сейчас, аналогично предыдущим примерам, напишем две процедуры (старт и завершение) для Unicorn.
set :unicorn_config, "#{shared_path}/config/unicorn.rb"
set :unicorn_pid, "#{shared_path}/run/unicorn.pid"
namespace :application do
desc 'Запуск Unicorn'
task :start do
on roles(:app) do
execute "cd #{release_path} && ~/.rvm/bin/rvm default do bundle exec unicorn_rails -c #{fetch(:unicorn_config)} -E #{fetch(:rails_env)} -D"
end
end
desc 'Завершение Unicorn'
task :stop do
on roles(:app) do
execute "if [ -f #{fetch(:unicorn_pid)} ] && [ -e /proc/$(cat #{fetch(:unicorn_pid)}) ]; then kill -9 `cat #{fetch(:unicorn_pid)}`; fi"
end
end
end
Не забудьте задать значения параметров :unicorn_config
и :unicorn_pid
.
Процедуры до и после деплоя
После деплоя нам нужно удалить самые старые релизы (по умолчанию Capistrano хранит пять последних), очистить кеши и перезапустить Unicorn. Главный рабочий блок :deploy
будет выглядеть примерно так:
namespace :deploy do
after :finishing, 'application:stop'
after :finishing, 'application:start'
after :finishing, :cleanup
end
Вынесение процедур в отдельные файлы
Чтобы не загромождать файл deploy.rb
, написанные процедуры можно (а может быть и нужно) выносить за его пределы. В Capfile
последняя строка отвечает за иморт таких задач из директории lib/capistrano/tasks
,— именно туда и стоит перенести эту часть логики.
Выполнение деплоя
Начиная с этого момента, все, что вам потребуется сделать для выкатки новой версии своего приложения, — закомитить изменения, сделать git push и использовать команду cap production deploy.
По аналогии вы сможете настроить сервер staging
, деплой на который осуществить cap staging deploy.
Дополнения
Что-то не получается? — Оставляйте комментарии, давайте дополним статью.
Автор: eboyko