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. В данном примере через шаблонизатор и команду lookup интерполируются переменные окружения, с машины, с которой выполняется развертывание. Это полезно для того, чтобы не хранить в системе контроля версий какую либо чувствительную информацию, вроде секретных ключей или строк подключения к БД.
Чтобы пример заработал, нужно объявить следующие переменные в вашем ~/.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_PASSWORD=3306
Ниже приведены файлы inventory/production и inventory/staging целиком:
; 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
; 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 {{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 --- - hosts: hosts # хосты объявлены в inventory-файле для каждого окружения tasks: # установка некоторых переменных вроде 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: Update app 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 --- - hosts: hosts # хосты объявлены в inventory-файле для каждого окружения tasks: - 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 pids 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 --- - 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 --- - 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/sidekiq.yml --- - hosts: hosts tasks: - include: tasks/_set_vars.yml tags=always - set_fact: pids_path={{ shared_path }}/tmp/pids tags: always - name: Ensure pids 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 --- - 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 --- - 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"
Поскольку эта статья даёт лишь пример (пусть и рабочий), отмечу пути, по которым можно пойти дальше:
- Реализовать graceful restart для Passenger.
- Использовать механизм ролей Ansible вместо вложенных плейбуков.
- И вообще привести этот пример в большее соответствие с рекомендациями разработчиков.
И самое главное. Ansible может гораздо больше, чем выкатывать релизы приложения и перезапускать сервера. Ведь, повторюсь, ansible не просто утилита для деплоя, а полноценный инструмент управления конфигурациями. К примеру, с помощью ролей вы можете настроить развертывание приложения с нуля, прямо на голое серверное железо. А простота yml-нотаций позволяет с лёгкостью модифицировать найденные решения под свои нужды.
Все исходные коды из статьи доступны на GitHub. Спасибо за внимание.
ссылка на оригинал статьи http://habrahabr.ru/post/266353/
Добавить комментарий