Рубин на рельсах: продакшен и деплой для чайников

в 14:33, , рубрики: capistrano, ruby on rails, ruby on rails 4, rvm

Год назад я довел свое первое рельсовое приложение до приемлемого вида. Вопрос использования готового кода в продакшене ранее меня не заинтересовал. С чего вдруг? Несложный язык, лаконичный фреймворк — уж деплой-то явно не сложнее, чем преодоление ментального тормоза после PHP.

Команда разработчиков Rails рекомендует использовать Phusion Passenger, он что-то вроде mod_php — установил, разместил файлы и полетел. На момент изучения вопроса на форумах хватало баталий о производительности решений; Passenger в них фаворитом не значился.

Совета относительно альтернативы я спросил у техдиректора сайта с миллионом уников в сутки — тот отправил меня гуглить на тему Nginx и Unicorn. Инструкция по настройке продакшена, найденная на Хабре, датировалась 2009 годом. Помимо прочего, ее просто переполняли изъяны уроков «Как нарисовать сову».

Отдельные составляющие процесса кое-где разжеваны по-английский, но монолитный tutorial на глаза так и не попался. В традициях рельсового сообщества лежит принцип, предписывающий делиться результатами и опытом решения проблем.

Опытным рельсоводам текст вряд ли покажется интересным, но если таковые найдут время на замечания — буду признателен и внесу необходимые правки.

— О чем пойдет речь?
— Эта инструкция поможет новичкам подобрать и настроить хостинг, а также подготовить имеющийся проект для первоначального деплоя и систематической выкатки обновлений в ключе zero downtime.

— А подробнее?
— Мы будем использовать Ubuntu 14.04, RVM, Nginx, Unicorn и Capistrano. Текст состоит из двух глав: подготовки проекта и настройки сервера. Все локальные манипуляции описываются в Mac OS X. Для выполнения процедур не будет лишней современная IDE вроде RubyMine, если нет — сойдет и TextMate. Все описываемые действия зафиксированы в специальном репозитории.

Глава первая Хостинг

Выбор хостинга

Сладкая парочка Nginx и Unicorn имеет весьма внушительный аппетит на оперативную память, а сами рельсы требуют установки ряда дополнительного ПО. Эти ограничения явно говорят о необходимости VPS. Есть вариант использовать специализированный хостинг вроде Heroku, но для новичков будет полезным процесс настройки проделать руками.

В рамках этого текста я буду использовать свежесозданный дроплет 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;
    }
}

Мы используем наш VPS для единственного приложения, поэтому хост будет использоваться по умолчанию. (Не нужно же объяснять, что в иной ситуации придется явно указать server_name и позаботиться об уникальности имен апстримов?)

Конфигурация 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

Источник

Поделиться

* - обязательные к заполнению поля