Ansible і Rails — гнучка заміна Capistrano із збереженням знайомого комфорту

Capistrano — улюблений багатьма rails-розробниками інструмент, за допомогою якого можна швидко і без заморочок автоматизувати розгортання вашого додатка. Capistrano — стандарт де-факто для системи розгортання RoR, must know технологія для будь-якого поважаючого себе рубиста, той інструмент, яким у свій час заздрили розробники на python і PHP.
Незважаючи на комфорт, від якого не хочеться відмовлятися, чим більш складні задачі мені доводилося вирішувати, тим частіше Capistrano показував себе до них не пристосованим.

Я відзначив наступні недоліки:
  • Відомі проблеми зі швидкістю. Внаслідок своєї універсальності, Capistrano деплоит повільно, виконуючи зайві перевірки та виклики, які ви не можете контролювати.
  • Послідовний деплой. Нешвидка час розгортання потрібно помножити на кількість цільових серверів (проте, можна налаштувати розпаралелювання комманд явним чином).
  • Сильна пов'язаність з рейками. Конфіги і залежно Capistrano переплітаються з додатком, стаючи його частиною. Не можна створити нове оточення-розгортання (наприклад сервера для раннього викочування функціоналу) без створення нового rails-оточення. У складних ситуаціях Capistrano змушує йти від хорошої практики тримати тільки development, test і production оточення.
  • Плагіни — палиця о двох кінцях. Даючи можливість швидко «прикрутити» розгортання тієї чи іншої залежності програми, плагіни позбавляють вас контролю ситуації, що змушують діяти так, як діє розробник плагіна. Про вплив зайвих «рухів» плагінів на швидкість деплоя я написав вище.
  • Складний деплой гетерогенних додатків. Трендом останніх років в рейках стало виділення найбільш важких (бекграундных або мережевих) завдань в окремі сервіси, не обов'язково написані на ruby. У такій ситуації capistrano змушує вас плодити зоопарк з різних систем розгортання різних мов і технологій.
Багато ruby-розробники перейшли на Mina або вирішують свої проблеми за допомогою ще більш складних систем управління конфігураціями зразок Chef і Puppet. Всі вони мають свої особливості і недоліки і різною мірою вирішують описані вище проблеми. Мені ж вдалося їх вирішити їх за допомогою Ansible, не розгубивши переваг Capistrano, до яких я звик.

Ansible це інструмент для керування конфігураціями і в його завдання входить не тільки описане в цій статті, виконання віддалених команд на серверах для розгортання і управління окремим додатком, але і автоматизація адміністрування серверного допомогою збережених серверних конфігурацій (ролей мовою Ansible). А значить Ansible (як втім і Chef і Puppet) дозволяє набагато більше, ніж Capistrano і в остаточному підсумку вони не йдуть з ним ні в яке порівняння. Однак, завдання цієї статті дати rails-розробникам відправну точку для міграції і роз'яснити на цьому прикладі основи Ansible. В кінці цієї статті, чарівна команда cap production deploy перетвориться на ansible-playbook deploy.yml -i inventory/production
Кому цікаво — прошу під кат.

Установка
Ansible написана на пітоні. Не кожному рубисту це сподобається, але я розвію страхи відразу — ні одного рядка на «ворожому» вам писати не доведеться. Притягальна сила Ansible в тому, що всі скрипти деплоя це конфігураційні файли, у відомому форматі yml з простим і потужним описовим синтаксисом.

Установка проста ansible теж проста і швидка. Встановлювати ansible потрібно тільки на локальній машині:
sudo easy_isntall pip
sudo pip install -U ansible

На цьому взаємодія з утилітами python закінчується і тепер нам доступна команда ansible-playbook, за допомогою якої і здійснюється деплой. Команда має лише один обов'язковий аргумент — відносний шлях до playbook-файлу.

Ansible-playbook
Playbook-файл це список завантажуваних завдань або інших плейбуков. Завдяки вкладеності, ми можемо ефективно ізолювати завдання по верствам і добитися можливості запускати тільки те, що нам зараз потрібно.
В якості прикладу для розгортання візьмемо myawesomestartup — це якесь rails-додаток зі зв'язкою passenger 5 standalone і nginx в якості веб-сервера і sidekiq для фонових завдань. Фізична інфраструктура в прикладі — два продакшн сервера:
prima.myawesomestartup.com
secunda.myawesomestartup.com

І один стейджинг:
plebius.myawesomestartup.com

У папці ansible визначимо майстер-плейбук deploy.yml, що містить всі інші плейбуки,
---
- hosts: hosts
- include: release.yml # створення нового релізу
- include: app.yml # запуск сервера веб-прриложения
- include: sidekiq.yml # запуск воркеров sidekiq

Командою ansible-playbook deploy.yml, запустимо деплой цілком. Однак, можна запустити плейбуки і окремо, якщо нам потрібно перезапустити програму без викочування нового релізу.
Зверніть увагу на змінну hosts у ній міститься інформація про сервери, на яких буде проводитися розгортання. Цю змінну можна визначити в глобальній конфігурації ansible, однак ми поступимо по-іншому, скориставшись інвентарними файлами.

Інвентарні файли і конфігурація
Для зберігання груп хостів, їх ієрархії і налаштувань в ansible передбачені інвентарні файли. Це ini-файли з дуже простим синтаксисом.

Ми можемо описати групу хостів:
[hosts:children]
prima
secunda

У групі оголосимо самі хости:
[prima]
prima.myawesomestartup.com

[secunda]
secunda.myawesomestartup.com

Оголосимо змінні, специфічні для кожного конкретного хоста:
[prima:vars]
ansible_env_name=production
rails_env_name=production
database_name={{ lookup('env', 'PRIMA_DB_NAME') }}
database_username={{ lookup('env', 'PRIMA_DB_LOGIN') }}
database_password={{ lookup('env', 'PRIMA_DB_PASSWORD') }}
database_host={{ lookup('env', 'PRIMA_DB_HOST') }}
database_port={{ lookup('env', 'PRIMA_DB_PORT') }}

Зверніть увагу на фігурні дужки — в ansible всі файли є шаблонами Jinja2. В даному прикладі через шаблонизатор і команду пошук інтерполюються змінні оточення, з машини, з якої виконується розгортання. Це корисно для того, щоб не зберігати в системі контролю версій якусь чутливу інформацію, начебто секретних ключів або рядків підключення до БД.

Щоб приклад запрацював, потрібно оголосити наступні змінні у вашому ~/.bashrc або ~/.zshrc або (що більш безпечно і менш зручно) експортувати їх кожен раз перед кожним деплоем:
export PRIMA_DB_NAME=myawesomestartup_production
export PRIMA_DB_LOGIN=myawesomestartup
export PRIMA_DB_PASSWORD=secret
export PRIMA_DB_HOST=db.myawesomestartup.com
export PRIMA_DB_PORT=3306

Нижче наведено файли inventory/production і inventory/staging цілком:
inventory/production
; production

[prima]
prima.myawesomestartup.com

[prima:vars]
ansible_env_name=production
rails_env_name=production
database_name={{ lookup('env', 'PRIMA_DB_NAME') }}
database_username={{ lookup('env', 'PRIMA_DB_LOGIN') }}
database_password={{ lookup('env', 'PRIMA_DB_PASSWORD') }}
database_host{{ lookup('env', 'PRIMA_DB_HOST') }}
database_port={{ lookup('env', 'PRIMA_DB_PORT') }}
git_branch=master
app_path=/srv/www/prima.myawesomestartup.com
custom_server_options=--no-friendly-error-pages
sidekiq_process_number=4

[secunda]
secunda.myawesomestartup.com

[secunda:vars]
ansible_env_name=production
rails_env_name=production
database_name={{ lookup('env', 'SECUNDA_DB_NAME') }}
database_username={{ lookup('env', 'SECUNDA_DB_LOGIN') }}
database_password={{ lookup('env', 'SECUNDA_DB_PASSWORD') }}
database_host={{ lookup('env', 'SECUNDA_DB_HOST') }}
database_port={{ lookup('env', 'SECUNDA_DB_PORT') }}
git_branch=master
app_path=/srv/www/secunda.myawesomestartup.com
custom_server_options=--no-friendly-error-pages
sidekiq_process_number=4

[hosts:children]
prima
secunda


inventory/staging

; staging

[plebius]
plebius.myawesomestartup.com

[plebius:vars]
ansible_env_name=staging
rails_env_name=production
database_name={{ lookup('env', 'PLEBIUS_DB_NAME') }}
database_username={{ lookup('env', 'PLEBIUS_DB_LOGIN') }}
database_password={{ lookup('env', 'PLEBIUS_DB_PASSWORD') }}
database_host={{ lookup('env', 'PLEBIUS_DB_HOST') }}
database_port={{ lookup('env', 'PLEBIUS_DB_PORT') }}
git_branch=develop
app_path=/srv/www/plebius.myawesomestartup.com
custom_server_options=--friendly-error-pages
sidekiq_process_number=4

[hosts:children]
plebius


Шаблони конфіги покладемо в папку ansible/configs:
configs/database.yml
# configs/database.yml
{{rails_env_name}}:
adapter: mysql2
database: {{database_name}}
username: {{database_username}}
password: {{database_password}}
host: {{database_host}}
port: {{database_port}}
secure_auth: false


Для тих параметрів, які можна безпечно зберігати у системі контролю версія я волію dotenv.
Створимо таку структуру файлів у папці ansible/environments:
production/
prima.env
secunda.env
staging/
plebius.env


Релізи як в Capistrano
Capistrano за замовчуванням пропонує досить продуману структуру файлів на сервері.
releases/
20150631130156/
20150631130233/
20150631172431/
20150704162516/
20150712165952/
current - -> /www/domain/releases/20150712165952/
shared/

Папка releases містить п'ять останніх останніх релізів в папках з назвами типу 20150812165952, що містять в собі таймстамп часу деплоя цього релізу. Всередині кожного релізу лежить файл REVISION містить в собі хеш коміта з якого був зроблений реліз.
Симлинк current посилається на останній реліз в папці releases.
Папка shared містить загальні для всіх релізів файли (наприклад .pid і .sock), і ті файли, які виключені із системи контролю версій (наприклад, database.yml). Все це дозволяє безпечно відкочувати додаток в разі збою деплоя або викочування коду з несподіваними багами.
Повторимо це з допомогою Ansible:
ansible/release.yml
# ansible/release.yml
---
- hosts: hosts # хости оголошені в inventory-файлі для кожного оточення
завдання:
# установка деяких змінних начебто app_path і shared_path винесена в окремий міксин. Про це нижче
- include: tasks/_set_vars.yml tags=always
# створимо таймстамп поточного релізу і встановимо папку
- set_fact: timestamp="{{ lookup('pipe', 'date +%Y - %m - %d%H%M%S') }}"
- set_fact: release_path="{{ app_path }}/releases/{{ timestamp }}"
# Перевіримо існування необхідних папок. Якщо їх немає ansible їх створить
- name: Ensure shared directory exists
file: path={{ shared_path }} state=directory
- name: Ensure shared/assets directory exists
file: path={{ shared_path }}/assets state=directory
- name: Ensure tmp directory exists
file: path={{ shared_path }}/tmp state=directory
- name: Ensure log directory exists
file: path={{ shared_path }}/log state=directory
- name: Ensure bundle directory exists
file: path={{ shared_path }}/bundle state=directory
# Залишимо останні п'ять релізів включаючи поточний
- name: Leave only last releases
shell: "cd {{ app_path }}/releases && find ./ -maxdepth 1 | grep -G .............. | sort -r | tail -n +{{ keep_releases }} | xargs rm -rf"
- name: Create release directory
file: path={{ release_path }} state=directory
# Завантажити додаток з системи контролю версій
- name: Checkout git repo into release directory
git:
repo={{ git_repo }}
dest={{ release_path }}
version={{ git_branch }}
accept_hostkey=yes
# отримаємо хеш останнього коміта для файлу REVISION і запишемо його
- name: Get git branch head hash
shell: "cd {{ release_path }} && git rev-parse --short HEAD"
register: git_head_hash
- name: Create REVISION file in the release path
copy: content="{{ git_head_hash.stdout }}" dest={{ release_path }}/REVISION
# створимо симлинки необхідні для rails додатки
- name: Set assets link
file: src={{ shared_path }}/assets path={{ release_path }}/public/assets state=link
- name: Set tmp link
file: src={{ shared_path }}/tmp path={{ release_path }}/tmp state=link
- name: Set log link
file: src={{ shared_path }}/log path={{ release_path }}/log state=link
# скопіюємо шаблони .env і database.yml в новий реліз. При цьому в шаблони вставляються потрібні змінні для кожного хоста.
- name: Copy .env file
template: src=environments/{{ansible_env_name}}/{{ansible_hostname}}.env dest={{ release_path }}/.env
- name: Copy database.yml
template: src=configs/database.yml dest={{ release_path }}/config
- set_fact: rvm_wrapper_command="cd {{ release_path }} && RAILS_ENV={{ rails_env_name }} rvm ruby-{{ ruby_version }}@{{ full_gemset_name }} --create do"
# Bundle, міграції, компіляція ассетов...
- name: Run bundle install
shell: "{{ rvm_wrapper_command }} bundle install --path {{ shared_path }}/bundle --deployment --without development test"
- name: Run db:migrate
shell: "{{ rvm_wrapper_command }} rake db:migrate"
- name: Precompile assets
shell: "{{ rvm_wrapper_command }} rake assets:precompile"
# Симлинкнем наш реліз в папку current
- name: app Update version
file: src={{ release_path }} path={{ app_path }}/current state=link


Установка деяких змінних була винесена в окрему задачу-міксин, так як ці змінні ідентичні для всіх плейбуков і серверів:
# ansible/tasks/_set_vars.yml
---
- set_fact: app_name="myawesomestartup"
- set_fact: ruby_version="2.2.2"
- set_fact: ruby_gemset="myawesomestartup"
- set_fact: git_repo="ilpagency/rails-sidekiq-ansible-sample"
- set_fact: keep_releases="5"
- set_fact: full_app_name="{{ app_name }}-{{ ansible_env_name }}"
- set_fact: full_gemset_name="{{ ruby_gemset }}-{{ ansible_env_name }}"
- set_fact: current_path="{{ app_path }}/current"
- set_fact: shared_path="{{ app_path }}/shared"


Запуск passenger і sidekiq — теги і цикли Ansible
Створимо ще один плейбук для управління станом додатки ansible/app.yml, за допомогою якого програму можна запустити, зупинити або перезавантажити. Як і інші плейбуки, його можна запускати окремо, або як частина майстер-плейбука.
Для більшої гнучкості додамо теги app_stop і app_start. Теги, що дозволяють виконувати тільки ті частини завдань, які явно вказані при деплое. Якщо не вказувати теги деплое — плейбук буде виконаний повністю.

Ось як це виглядає на практиці:
# Перезапустити програму:
ansible-playbook app.yml -i inventory/production
# Тільки зупинити:
ansible-playbook app.yml -i inventory/production -t "app_stop"
# Тільки запустити:
ansible-playbook app.yml -i inventory/production -t "app_start"
# Це теж перезапуск:
ansible-playbook app.yml -i inventory/production -t "app_stop,app_start"

А от реалізація:
ansible/app.yml
# ansible/app.yml
---
- hosts: hosts # хости оголошені в inventory-файлі для кожного оточення
завдання:
- include: tasks/_set_vars.yml tags=always # always це спеціальний тег, завдання зазначена їм буде виконана завжди, при будь-яких зазначених команді деплоя тегах
- set_fact: socks_path={{ shared_path }}/tmp/socks
tags: always
- name: Ensure sockets directory exists
file: path={{ socks_path }} state=directory
tags: always
- set_fact: app_sock={{ socks_path }}/app.sock
tags: always
- set_fact: pids_path={{ shared_path }}/tmp/pids
tags: always
- name: Ensure pid directory exists
file: path={{ pids_path }} state=directory
tags: always
- set_fact: app_pid={{ pids_path }}/passenger.pid
tags: always
- set_fact: rvm_wrapper_command="cd {{ current_path }} && RAILS_ENV={{ rails_env_name }} rvm ruby-{{ ruby_version }}@{{ full_gemset_name }} --create do"
tags:always
- include: tasks/app_stop.yml tags=app_stop #ця задача буде запщуена якщо не вказано ні один тег або зазначено тег app_start
- include: tasks/app_start.yml tags=app_start # поведінка аналогічно попередньому, тільки тег - app_stop


Завдання запуску і зупинки програми окремо виділені файли ansible/tasks/app_start.yml і ansible/tasks/app_stop.yml:
ansible/tasks/app_start.yml
# ansible/tasks/app_start.yml
---
- name: start passenger
shell: "{{ rvm_wrapper_command }} bundle exec passenger start -d -S {{ app_sock }} --environment {{ rails_env_name }} --pid file {{ app_pid }} {{ custom_server_options }}"


ansible/tasks/app_stop.yml
# ansible/tasks/app_stop.yml
---
- name: stop passenger
shell: "{{ rvm_wrapper_command }} bundle exec passenger stop --pid file {{ app_pid }}"
ignore_errors: yes # якщо раптом додаток не запущено... ігноруємо помилки. Краще - додати явну перевірку.


З sidekiq ситуація схожа. Для нього реалізуємо окремий плейбук ansible/sidekiq.yml підтримує відповідні теги sidekiq_stop і sidekiq_start:
ansible/app.yml
# ansible/sidekiq.yml
---
- hosts: hosts
завдання:
- include: tasks/_set_vars.yml tags=always
- set_fact: pids_path={{ shared_path }}/tmp/pids
tags: always
- name: Ensure pid directory exists
file: path={{ pids_path }} state=directory
tags: always
- set_fact: rvm_wrapper_command="cd {{ current_path }} && RAILS_ENV={{ rails_env_name }} rvm ruby-{{ ruby_version }}@{{ full_gemset_name }} --create do"
tags: always
- include: tasks/sidekiq_stop.yml tags=sidekiq_stop
- include: tasks/sidekiq_start.yml tags=sidekiq_start


Завдання запуску і зупинки так-же виділені окремо у файли ansible/tasks/sidekiq_start.yml і ansible/tasks/sidekiq_stop.yml. Крім власне запуску і зупинки sidekiq, в цих завданнях демонструється робота з циклами Ansible і вирішується проблема запуску/зупинки відразу декількох процесів:
ansible/tasks/sidekiq_start.yml
# ansible/tasks/sidekiq_start.yml
---
- name: start sidekiq
shell: "{{ rvm_wrapper_command }} bundle exec sidekiq --index {{ item }} --pidfile {{ pids_path }}/sidekiq-{{ item }}.pid --environment {{ rails_env_name }} --logfile {{ shared_path }}/log/sidekiq.log --daemon" # мінлива item - суть i в циклі. Якщо в with_sequence вказати 4, то item буде 1,2,3,4
with_sequence: count={{ sidekiq_process_number }} # кількість процесів sidekiq зазначено в інвентарному файлі для кожного сервера і кожного оточення


ansible/tasks/sidekiq_stop.yml
# ansible/tasks/sidekiq_stop.yml
---
- name: stop sidekiq
shell: "{{ rvm_wrapper_command }} bundle exec sidekiqctl stop {{ pids_path }}/sidekiq-{{ item }}.pid 20"
ignore_errors: yes # І знову, бажано реалізувати перевірку на те, що запущено процес, а не игонорировать помилки.
with_sequence: count={{ sidekiq_process_number }}



Висновок
Тепер ми можемо користуватися Ansible для розгортання rails додатків:
cd myawesomestartup/ansible

# Деплой:
ansible-playbook deploy.yml -i inventory/production
# Перезапустити програму:
ansible-playbook app.yml -i inventory/production
# Перезапустити sidekiq:
ansible-playbook sidekiq.yml -i inventory/production
# Деплой в стейджинг з кастомних гілки:
ansible-playbook deploy.yml -i inventory/staging -e git_branch="hotfix/14082015-777-production_bug"

Оскільки ця стаття дає лише приклад (нехай і робочий), зазначу шляхи, за якими можна піти далі:
  1. Реалізувати graceful restart для Passenger.
  2. механізм ролей Ansible замість вкладених плейбуков.
  3. І взагалі привести цей приклад в більшу відповідність з рекомендаціями розробників.
І найголовніше. Ansible може набагато більше, ніж викочувати релізи програми і перезапускати сервера. Адже, повторюся, ansible не просто утиліта для деплоя, а повноцінний інструмент управління конфігураціями. Приміром, з допомогою ролей ви можете налаштувати розгортання програми з нуля, прямо на голе серверне залізо. А простота yml-нотацій дозволяє з легкістю змінювати знайдені рішення під свої потреби.

Всі вихідні коди з статті доступні на GitHub. Дякую за увагу.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.