Настраиваем сервера и деплоим приложения при помощи Chef

Illustration of a young person sitting on the floor against a wall, reading a book with one leg stretched out and the other bent, wearing casual clothing and orange sneakers. Illustration of a young person sitting on the floor against a wall, reading a book with one leg stretched out and the other bent, wearing casual clothing and orange sneakers.

В этой статье я рассмотрю немного необычный способ деплоить веб-приложения на Ruby on Rails: при помощи системы управления конфигурациями Chef. Статья вряд ли подойдёт новичкам, особенно если вы ещё ни разу не деплоили приложения даже на Heroku. Более того, следует рассматривать этот текст не как полноценное подробное руководство по развёртыванию приложений с Chef. Это, в первую очередь, мой личный эксперимент, проведённый с целью ознакомления с этими технологиями. Получившийся в итоге код действительно деплоит Rails приложение на Digital Ocean, предварительно настраивая Nginx, Unicorn и Postgresql. Тем не менее я намеренно избегал слишком глубокого погружения в детали, и точно так же я опущу многие детали в статье. Приступим!

Немного о Chef

Chef используют чтобы настраивать сервера. Делают это при помощи кукбуков, которые описывают желаемое состояние сервера. В кукбуках есть рецепты, в которых и находится определение состояния сервера. В Chef есть разделение на chef-server и chef-client. В chef-server хранятся кукбуки и информация о всех ваших серверах (нодах), chef-client это отдельные ноды. У каждой ноды есть run list — список кукбуков и\или рецептов, которые должны быть выполнены на этой ноде. Управление chef-server и нодами происходит с workstation. Workstation это не что иное, как компьютер разработчика.

Для многих задач опен сорс сообщество уже написало кукбуки: для настройки postgresql, ruby, nginx и т.п. Но для деплоя конкретного приложения всё равно придётся писать свой кукбук, в котором в качестве зависимостей мы укажем все необходимые кукбуки.

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

Таким образом нам нужно сделать следующее:

  • настроить chef-server
  • настроить workstation
  • подключить к chef-server какой нибудь сервер
  • собрать необходимые для Rails приложения кукбуки
  • написать свой кукбук, который будет использовать скачанные ранее кукбуки и деплоить приложение

Hosted Chef, Digital Ocean и наша рабочая станция

Есть два варианта для chef-server: установить его на свой сервер или использовать Enterprise Chef. Enterprise Chef это такой хостинг chef-server с красивым интерфейсом. А самое главное — до 5 нод можно настраивать бесплатно. Прямо то что нам нужно.

Самый дешёвый VPS хостинг который я встречал это Digital Ocean, всего 5$ в месяц. Отлично годится как минимум для этого эксперимента. Но не спешите создавать там свою первую виртуалку, мы сделаем это немного иным способом.

Итак, регистрируемся на Opscode (компания, создавшая chef) и быстренько настраиваем workstation следуя официальной документации: https://learnchef.opscode.com/get-started/. Внимание: вам нужно сделать всё до Step 3: Set up a node to manage.

Итого: chef-server настроен, мы к нему можем быстро подключать новые сервера, находящиеся на Digital Ocean.

Собираем кукбуки

Для Chef существует несколько инструментов, облегчающих работу с кукбуками: librarian-chef и berkshelf. Я почему-то решил использовать librarian-chef, хотя вроде как berkshelf более популярен. Заходим в наш chef репозиторий, ставим librarian, инициализируем его:

gem install librarian-chef
librarian-chef init

Дальше самое интересное: нужно найти правильные кукбуки и указать их в Cheffile'е. Для Rails приложения нам понадобятся как минимум Postgresql, Nginx и Ruby. А сам кукбук, отвечащий за деплой приложения, нужно положить в отдельную папку, например cookbooks-dev. Создать новый кукбук можно командой knife cookbook create rails-app. Он его создаст в папке cookbooks, но мы его передвинем в cookbooks-dev.

Knife — это программа для управления chef-server через workstation. Позволяет загружать кукбуки, получать список нод и многое-многое другое.

Объясню почему: в папке cookbooks мы храним уже готовые к использованию кукбуки, поэтому вести разработку кукбуков прямо там слегка некорректно. Более того, если ваш кукбук не указан в Cheffile и вы снова запустите установку кукбуков, то он будет удалён из cookbooks. Таким образом гораздо больше смысла имеет хранить свои кукбуки в cookbooks-dev и указывать путь к ним в Cheffile. Когда новая версия кукбука готова, нужно будет прогнать установку кукбуков, которая скопирует новую версию вашего кукбука в cookbooks.

Мой Cheffile в итоге выглядел вот так:

#!/usr/bin/env ruby

site 'http://community.opscode.com/api/v1'

cookbook 'postgresql',
   git: 'http://github.com/hw-cookbooks/postgresql'

cookbook 'nginx',
   git: 'https://github.com/opscode-cookbooks/nginx'

cookbook 'ruby_build'

cookbook 'user'

cookbook 'rails-app',
   path: 'cookbooks-dev'

Кукбук для деплоя Rails

До этого момента мы только настраивали наше окружение и кукбуки. Пора написать что-нибудь самим. Мы уже создали кукбук rails-app, пора посмотреть из чего же он состоит. Самые важные на сегодня папки это attributes, recipes и templates.

В attributes хранятся различные атрибуты ноды. Атрибутом может быть что угодно, например, название rails приложения или ссылка на git репозиторий с кодом приложения.

В templates хранятся шаблоны различных файлов, которые будут использоваться при настройке ноды. Например, шаблон database.yml, в который нам нужно будет подставить значения для имени базы данных, имя пользователя БД и пароль. В качестве шаблонизатора используется ERB, никаких неожиданностей для программистов на Ruby.

В recipes хранятся рецепты, которые и выполняют всю работу. В основном рецепты состоят из ресурсов (chef resources). Ресурс описывает желаемое состояние системы. Ходят слухи что многие начинающие пользователи Chef пишут рецепты как простой код. На самом деле в рецепте должно быть именно описание того, как система должна быть настроена. Отсюда следует принцип идемпотентности ресурсов, согласно которому повторный запуск ресурса не должен изменять состояние системы если оно уже соответствует ожидаемому состоянию.

Помимо ресурсов в рецепте можно подключить другие рецепты, например из других кукбуков. К каждому встроенному в Chef ресурсу существует исчерпывающая документация, поэтому смело сверяйтесь с ней, если вам что-то непонятно. Когда я впервые пробовал Chef пару лет назад, с документацией были серьёзные проблемы. Сейчас, к счастью, этих проблем нет, на любой вопрос можно найти ответ.

Итак, в первую очередь нам нужно подключить рецепты для ruby, nginx, postgresl, а так же рецепт для установки основных библиотек, необходимых в том числе для компиляции ruby, nginx, postgresql.. ну вы поняли. Все последующие изменения вносятся в файл .cookbooks-dev/rails-app/recipes/default.rb

include_recipe 'apt'
include_recipe 'build-essential::default'
include_recipe 'ruby_build'
include_recipe 'postgresql::server'
include_recipe 'nginx'

Эти рецепты выполнятся прежде чем начнёт выполняться наш рецепт. Следующим шагом будет установка ruby и bundler:

ruby_build_ruby '2.0.0-p481' do
  prefix_path '/usr/local/'
  environment 'RUBY_CONFIGURE_OPTS' => '--with-openssl-dir=/etc/ssl/'
  action :install
end

gem_package 'bundler' do
  gem_binary '/usr/local/bin/gem'
  options '--no-ri --no-rdoc'
end

Ресурс ruby_build_ruby определён в кукбуке ruby_build. Как можно увидеть, он позволяет указать нужную версию ruby и различные опции для установки. gem_package идёт в комплекте с самим Chef и устанавливает гем (сюрприз-сюрприз). Не уверен что здесь нужны ещё какие либо объяснения, DSL Chef'а очень простой и человекочитаемый.

Теперь настроим Nginx!

template "#{node['nginx']['dir']}/sites-available/#{node['app']['name']}" do
  source "nginx_conf.erb"
  mode 0644
  owner node['nginx']['user']
  group node['nginx']['user']
end

# Этот встроенный в кукбук nginx ресурс просто-напросто делает в папке sites-enabled ссылку на конфиг из папки sites-available.
nginx_site node['app']['name'] do
  enable true
end

Здесь мы впервые используем ресурс template. В первую очередь мы передаём в него название файла (вместе с путём). Затем указываем какой шаблон из папки templates использовать и так же прописываем владельца файла и права доступа.

В шаблоны можно передавать переменные, примерно так же как мы это делаем в контроллерах Rails приложений. Чуть ниже будет пример этой передачи переменных.


# В кукбуке user есть ресурс user_account, воспользуемся им для создания пользователя, отвечающего за деплой приложения
user_account node['app']['deploy_user'] do
  create_group true
  ssh_keygen false
end

# Не забываем, что рецепт это всё ещё ruby код, объявим парочку переменных
app_name = node['app']['name']
deploy_to = "/var/www/#{app_name}"

# Ещё один ресурс, directory, нужен для создания папок.
# Умеет создавать папки рекурсивно, то есть если родительской папки нет, то её он тоже создаст
directory deploy_to do
  recursive true
  owner node['app']['deploy_user']
  group node['app']['deploy_user']
  mode "0755"
  action :create
end

directory "#{deploy_to}/shared" do
  recursive false
  owner node['app']['deploy_user']
  group node['app']['deploy_user']
  mode "0755"
  action :create
end

%w{config system vendor_bundle log pids}.each do |dir|
  directory "#{deploy_to}/shared/#{dir}" do
    owner node['app']['deploy_user']
    group node['app']['deploy_user']
    mode '0755'
    recursive false
  end
end

# Уже знакомый нам ресурс template, но теперь с передачей переменных
template "#{deploy_to}/shared/config/database.yml" do
  source "database.yml.erb"
  owner owner
  group group
  mode 0644
  variables(
    :username => 'postgres',
    :pass => node['postgresql']['password']['postgres'],
    :database => "#{app_name}_#{node['app']['env']}",
  )
end

Мы приближаемся к самой сути деплоя приложения. Теперь нам нужно настроить ssh ключи для деплой пользователя. По уму, нужно использовать для этого data bags, о которых я не буду рассказывать в этой статье. Как я уже говорил раньше, на этот раз я намеренно упрощаю некоторые вещи, поэтому ключи для деплоя я кинул прямо в папку templates.

template "/home/#{node['app']['deploy_user']}/.ssh/id_rsa.pub" do
  source "id_rsa.pub"
  owner node['app']['deploy_user']
  mode 0600
end

template "/home/#{node['app']['deploy_user']}/.ssh/id_rsa" do
  source "id_rsa"
  owner node['app']['deploy_user']
  mode 0600
end

Ещё нам пригодится небольшой shell скрипт для использования ssh ключа во время деплоя, что бы без препятствий вытянуть код из private репозитория. Сам код скрипта взят из документации Chef.

file "#{deploy_to}/wrap-ssh4git.sh" do
  content "#!/bin/bash\n/usr/bin/env ssh -o \"StrictHostKeyChecking=no\" -i \"/home/deployer/.ssh/id_rsa\" \$1 \$2\n"
  owner node['app']['deploy_user']
  group node['app']['deploy_user']
  mode 0700
end

Пора переходить к деплою! В Chef есть специальный ресурс deploy, который делает примерно тоже самое что и Capistrano: вытягивает код в releases, делает симлинк на последний релиз, делает симлинки на файлы в shared. Ресурс большой, но, как это обычно и бывает в Chef, очень легко воспринимается даже не знакомому с этой технологией программисту. Тем не менее я постарался добавить как можно больше комментариев для пущей ясности.

deploy deploy_to do
  # Тянем код из этой репки
  repo node['app']['repo']
  branch master

  # После деплоя прогоняем миграции
  migrate true

  # Вот этой вот командой
  migration_command "bundle exec rake db:create && bundle exec rake db:migrate”

  # Используя вот это вот окружение
  environment 'RAILS_ENV' => node['app']['env’]

  create_dirs_before_symlink %w{tmp public config deploy}

  # Все команды выполняем от деплой-пользователя
  user node['app']['deploy_user']

  # Используем созданный ранее скрипт для ssh запроса к репозиторию
  ssh_wrapper "#{deploy_to}/wrap-ssh4git.sh"

  # Симлинк на конфиг для базы данных
  symlink_before_migrate "config/database.yml" => "config/database.yml"

  # Делаем симлинки на конфиги и прочее
  symlinks(                   
    "config" => "config",
    "log" => "log",
    "tmp" => "tmp",
    "system" => "public/system"
  )

  # Перезапускаем приложение, в моём случае используется unicorn
  restart_command "if [ -f #{unicorn_pid} ] && [ -e /proc/$(cat #{unicorn_pid}) ];
                                then kill -USR2 `cat #{unicorn_pid}`;
                                else cd #{deploy_to}/current && bundle exec unicorn_rails -c #{unicorn_conf} -E #{node['app']['env']} -D;
                                fi"

  # Ставим гемы перед миграцией, в папку shared
  before_migrate do
    execute "bundle install --deployment --path #{deploy_to}/shared/vendor_bundle" do
      cwd release_path
    end
  end

  # И конечно же компилируем ассеты
  before_restart do
    execute "cd #{release_path} && RAILS_ENV=#{node['app']['env']} bundle exec rake assets:precompile && rm -rf #{release_path}/tmp/cache"
    directory "#{release_path}/public/assets" do
      owner node['app']['deploy_user']
      group node['app']['deploy_user']
      mode '0755'
      recursive true
    end
  end

  # Оповещаем сервис nginx что нужно перезапуститься
  notifies :restart, "service[nginx]"
  action :deploy
end

Ну как, здорово? По-моему это потрясающий способ настраивать сервера. А самое замечательное: написал рецепт один раз и больше никогда не думаешь об этом. Ах да, забыл показать как выглядят ./cookbooks-dev/rails-app/attributes/default.rb и ./cookbooks-dev/rails-app/templates/database.yml.erb.

default['app']['name'] = 'myawesomeapp'

default['app']['env'] = 'production'
default['app']['repo'] = 'git@bitbucket.org:Fodoj/myawesomeapp.git'

default['app']['deploy_user'] = 'deployer'

default['postgresql']['password']['postgres'] = '4815162342'
production:
  adapter: postgresql
  host: localhost
  port: 5432
  username: <%= @username %> # Вот они, переменные из ресурса template
  password: <%= @pass %>
  database: <%= @database %>
  schema_search_path: public
  encoding: utf8
  template: template1

Загружаем кукбуки и создаём роль.

Так как все кукбуки у нас уже готовы, остаётся только загрузить их на chef server:

librarian-chef install
knife cookbook upload --all

В Chef есть кое-что под названием Роли. Всё просто: в приложении с кучей серверов есть такие ноды, которые отвечают только за базу данных, есть load balancer'ы, есть ноды с собственно приложением. Chef позволяет определить роли, в ролях определить рецепты, которые должны быть выполнены на нодах с этой ролью и указать атрибуты, специфичные для нод с этой ролью. Вот простейшая роль, которой нам хватит что бы завершить этап написания кода:

name "application"
description "Sets up server with postgresql, nginx, ruby and rails"
run_list "recipe[rails-app]"
override_attributes({
  "starter_name" => "Kirill Shirinkin",
})

default_attributes "nginx" => { "default_site_enabled" => false },
                              "postgresql" => { "enable_pgdg_apt" => true } # Это что бы ставился самый свежий postgresql

Загружаем роль на chef-server: knife role from file roles/application.rb.

В бой!

А теперь самое крутое: сейчас мы одной командой создадим новый сервер на Digital Ocean, подключим его к chef серверу, настроим на нём всё что нужно настроить и задеплоим приложение. Это как Heroku, только круче, потому что мы сами всё написали и знаем как это работает ;-)

Регистрируемся на Digital Ocean, идём в панель управления, затем в раздел API, генерируем новый API ключ. Дальше, что бы не ходить руками в панель управления для создания серверов и что бы автоматизировать процесс подключения новых нод к chef-server мы будем использовать плагин knife-digital_ocean:

gem install knife-digital_ocean

Есть аналогичные плагины для AWS, Rackspace и прочих облачных сервисов, поэтому обычно нет нужды бутстрапить ноды самому (что, впрочем, не сложно, но неудобно).

Созданные ранее API ключи добавьте в .chef/knife.rb:

knife[:digital_ocean_client_id] = 'XXXXXXXXXXXX'
knife[:digital_ocean_api_key] = 'YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY'

Ну всё, поехали. Обратите внимание на опцию boostrap, благодаря ей свежесозданный сервер сразу же будет настроен для работы с нашим chef-server.

knife digital_ocean droplet create --server-name awesome-app-0 \
                                      --image 3101918 \
                                      --location 5 \
                                      --size 66 \
                                      --run-list "role[application]" \
                                      --bootstrap

Что здесь происходит:

  • Используя API ключи от Digital Ocean мы создаём новый сервер
  • Идём туда по ssh и настраиваем этот сервер на работу с chef сервером
  • Выполняем там все рецепты, указанные в роли application

Что бы снова запустить все рецепты можно использовать следующую команду:

knife ssh "name:awesome-app-0" "chef-client" -x root -a ipaddress

Это тоже самое что зайти по ssh на сервер и выполнить chef-client.

Делаем выводы: использовать Chef для деплоя rails приложений слегка сложнее чем Capistrano, но приложив немного усилий мы получаем полностью автоматизированный процесс настройки сервера с нуля до рабочего приложения. Написанные кукбуки можно использовать для скольки угодно приложений. Потратив чуть больше времени мы получаем в наш набор инструментов крутого разработчика настоящее супер-оружие. И это замечательно.