Pull to refresh

Deploy с помощью Salt

Reading time 8 min
Views 33K

До сих пор во многих компаниях deploy создает большие проблемы и может занимать дни, недели и в особо запущенных случаях месяцы. Но ситуация не безнадежна. Существует много инструментов и практик, способных помочь в этом нелегком деле. Вот только эти инструменты чаще всего за один-два дня не освоишь, а сроки горят.

Чего обычно хочется:
  • Возможность поднять проект локально на машине разработчика. Весь или хотя бы частями. Причем очень хочется, чтобы Dev конфигурация отличалась от Prod в минимуме параметров. Это позволит избежать “work on my machine” багов. Да и вообще, когда один разработчик работает на OS X, другой на Windows, а продакшен на Debian, то жди беды, это не считая того, что каждый делает работу по настройке окружения.
  • Dev конфигурацию хочется разворачивать на любой машине и ОС в пару команд в консоли. Это опять же позволит уменьшить фактор “work on my machine” багов. А еще позволит привлекать других разработчиков в проект за минимальное время (vagrant up и поехали).
  • Конфигурация должна быть понятна и программисту, и админу.

Всего этого мы добьемся на связке Salt + Vagrant на примере Django проекта. Но большинство техник будут полезны разработчикам не только на Python, но на других языках.


Сразу дам ссылку на исходники: bitbucket.org/awnion/salt-django-deploy

Что же такое Salt?


Если вы знакомы с Salt, то можете пропустить этот раздел.

Salt — это достаточно мощный инструмент для управления кластером (cluster orchestration), но на мой личный взгляд даже использование на одной машине вполне оправдано и не будет оверкилом (грубо говоря если в вашей команде ровно 1 разработчик, то это не значит, что не надо использовать систему контроля версий).

Salt состояния — это YAML файлы с расширением sls, которые описывают в каком состоянии должна быть машина. Например, вот этот вот файл должен лежать тут, вот этот сервис должен быть запущен, вот этот юзер должен иметь такие права и так далее. В Salt можно поддерживать состояние не только системных утилит (apt, rpm, cron, init скрипты и разные конфиги), но и, например, можно проверить существует ли такой-то пользователь в RabbitMQ, последней ли версии репозиторий git, все ли пакеты стоят в вашем virtualenv и так далее. Полный список состояний можно найти тут docs.saltstack.com/ref/states/all, и на мой взгляд он весьма внушительный.

Несколько фактов про Salt

  • В качестве темплейтного языка для конфигов и фалов состояний Salt использует Jinja. Это невероятно удобно и позволяет следовать DRY даже в конфигах.
  • Salt уже используют такие компании, как Apple, NASA, LinkedIn и многие другие.
  • Salt написан на Python, хотя для его использования знать Python вообще не обязательно.
  • Pdf документация для Salt около 1000 страниц, и написана она вполне добротно. Там вы найдете не только описание API, но и практики использования и примеры.


Dev конфигурация


На мой взгляд сложно переоценить важность хорошей и удобной среды разработки. Но на настройку “все под себя” может уйти пара дней, а то и неделя. Давайте сэкономим эти дни нашим коллегам в будущем и создадим конфигурацию, которая позволит поднять текущую версию проекта в одну команду:
git clone <repo_url> && cd <repo_name> && vagrant up

Ладно, это на самом деле это 3 команды, но если вы можете проще — жму вам руку.

Итак, мы будем строить нашу dev конфигурацию на Vagrant (тем, кто не знаком с Vagrant, настоятельно рекомендую познакомиться):
mkdir my_app && cd my_app
git init
vagrant init

В нашей папке my_app появился git репозиторий и конфиг для Vagrant.

Далее заменяем содержание Vagrantfile на следующее:
Vagrant.configure("2") do |config|
  config.vm.box = "precise64"

  config.vm.hostname = "dev-my-app"
  config.vm.network :private_network, ip: '3.3.3.3'

  config.vm.synced_folder "salt/roots/salt", "/srv/salt/"
  config.vm.synced_folder "salt/roots/pillar", "/srv/pillar/"

  config.vm.provision :salt do |salt|
    salt.minion_config = "salt/minion"
    salt.run_highstate = true
  end
end


Этот конфиг позволит создать гостевую машину на Ubuntu, в конфиге мы задали имя хоста и IP, а так же определили, какие папки синхронизировать и указали, что для приведения нашей машинки к нужному состоянию мы будем использовать salt (кстати, корень нашего проекта будет по умолчанию синхронизован с папкой /vagrant гостевой машины).

Подробней о том, что тут происходит, можно узнать тут

Итак, мы готовы начать описывать состояния. Создадим файл salt/minion со следующим содержанием:
file_client: local


Фактически мы говорим, что эта машина хранит свои состояния сама. По умолчанию salt настроен так, что он берет файлы состояний с master-сервера, а локально их только кэширует где-то в /var/cache/salt. Поэтому если вы не хотите чего-то кастомного, то этот файл на prod машине скорее всего вообще не понадобится.

Теперь создадим две папки:
mkdir -p salt/roots/pillar
mkdir -p salt/roots/salt

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

Создадим файл salt/roots/salt/top.sls
base:
  'dev-my-app':
    - nginx
    - python
    - supervisor


Как можно было догадаться sls очень похож на yaml. Но главным отличаем тут будет то, что sls это еще и jinja темплейт со всеми вытекающими (дальше вы увидите, что это реально приносит пользу).

base — это название конфигурации состояний нашего воображаемого кластера. dev-my-app — это hostname нашей гостевой машины. Тут используется pattern matching, то есть мы могли бы указать ‘dev-*’, и все состояни ниже применились бы ко всем машинам типа dev-alpha, dev-foobar и т.п. Далее следует список состояний, которые нам надо будет описать.

Создадим заявленные состояния python, nginx и supervisor:

salt/roots/salt/python.sls
# это состояние просто проконтролирует, что пакеты python и python-virtualenv
# установлены, а если нет -- то поставит их с зависимостями
python:
  pkg:
    - installed
    - names:
      - python
      - python-virtualenv


salt/roots/salt/nginx.sls
# это состояние поставит nginx и запустит его как сервис, при этом require 
# показывает, что состояние service нужно запустить после состояния pkg
nginx:
  pkg:
    - installed
  service:
    - running
    - reload: True  # сервис поддерживает reload
    - require:
      - pkg: nginx


salt/roots/salt/supervisor.sls
supervisor:
  pkg:
    - installed
  service:
    - running
    - require:
      - pkg: supervisor


Итак, можно уже запустить “vagrant up”. Эта процедура скачает образ Ubuntu (если у вас его еще нет в кэше образов), установит туда salt и запустит синхронизацию состояния.
Теперь у нас на нашей гостевой машине есть python, supervisor и nginx.
Можете проверить это зайдя на машину через vagrant ssh или зайдя на 3.3.3.3

Пока вроде бы все просто. Продолжим:

Создадим переменные pillar:

salt/roots/pillar/top.sls
base:
  'dev-my-app':
    - my-app


salt/roots/pillar/my-app.sls
my_app:
  gunicorn_bind: 127.0.0.1:8000
  dns_name: dev.my-app.com
  venv_dir: /home/vagrant/my_app_env
  work_dir: /vagrant

Первый файл говорит, что хосту dev-my-app назначены переменные из конфига my-app. Второй файл — собственно сами переменные.

Теперь создадим папку для состояний конфигов нашего Django приложения:
mkdir -p salt/roots/salt/my_app


salt/roots/salt/my_app/init.sls
{% set my_app = pillar['my_app'] %}

my_app.venv:
  virtualenv.managed:
    - name: {{ my_app['venv_dir'] }}
    - system_site_packages: False
    - require:
      - pkg: python

my_app.pip:
  pip.installed:
    - bin_env: {{ my_app['venv_dir'] }}
    - names:
      - Django==1.6
      - gunicorn==18.0
    - require:
      - virtualenv: my_app.venv

my_app.nginx.conf:
  file.managed:
    - name: /etc/nginx/sites-enabled/my_app.conf
    - source: salt://my_app/nginx.my_app.conf
    - context: # помимо переменных вроде pillar, мы можем передать дополнительный контекст для тепмлейта
        bind: {{ my_app['gunicorn_bind'] }}
        dns_name: {{ my_app['dns_name'] }}
    - template: jinja
    - makedirs: True
    - watch_in:
      - service: nginx

my_app.supervisor.conf:
  file.managed:
    - name: /etc/supervisor/conf.d/my_app.conf
    - source: salt://my_app/supervisor.my_app.conf
    - context:
        app_name: my_app
        bind: {{ my_app['gunicorn_bind'] }}
        gunicorn: {{ my_app['venv_dir'] }}/bin/gunicorn
        directory: {{ my_app['work_dir'] }}
        workers: {{ grains['num_cpus'] * 2 + 1 }}  # в академических целях выпендрился
    - template: jinja
    - makedirs: True

my_app.supervisor:
  supervisord.running:
    - name: my_app
    - watch:
      - file: my_app.supervisor.conf
    - require:
      - pip: my_app.pip
      - pkg: supervisor


Hint: при составлении зависимостей require, watch и пр. имейте в виду, что состояния будут проверяться в произвольном порядке. При составлении статьи я допустил такого рода ошибку, и пакеты django и gunicorn пытались устанавливаться в еще не созданный virtualenv.

salt/roots/salt/my_app/nginx.my_app.conf
server {
    listen 80;
    server_name {{ dns_name }} _;
    location / {
        proxy_pass http://{{ bind }};
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}


salt/roots/salt/my_app/supervisor.my_app.conf
[program:{{ app_name }}]
command={{ gunicorn }} {{ app_name }}.wsgi:application -b {{ bind }} -w {{ workers }}
directory={{ directory }}
user=nobody
autostart=true
autorestart=true
redirect_stderr=true


Добавляем только что созданное состояние в salt/roots/salt/top.sls
base:
  ‘dev-my-app’:
    - nginx
    - python
    - supervisor
    - my_app  # <----


Мы почти закончили. Но не хватает самого главного — кода нашего воображаемого Django приложения. Давайте создадим пустой тестовый Django проект следующим образом:
vagrant provision


Этот процесс займет несколько минут (большую часть времени будут ставиться django и gunicorn в virtualenv).

Когда provision отработает заходим на гостевую машину:
vagrant ssh


И внутри делает следующее:
/home/vagrant/my_app_env/bin/django-admin.py startproject my_app /vagrant
exit


И на хост машине опять делаем vagrant provision, и чтобы проверить, что все работает в файле hosts пропишите временно:
dev.my-app.com 3.3.3.3


Переходим в браузере на dev.my-app.com, и если все хорошо, то мы увидим It worked!

Dev конфигурация построена. Можно коммитить.

Теперь если вы захотите отдать поиграться со своим проектом другу, то ему нужно будет сделать только git clone и vagrant up. Причем этот воображаемый друг получит не только исходники самого проекта, но и будет иметь представление о том, как его будут деплоить.

Кроме всего прочего по умолчанию наш проект крутится самостоятельно под управлением gunicorn+supervisor. Но что если мы хотим удаленно подебажить или мы хотим вернуть наш любимый autoreload изменений кода? Не вопрос:
vagrant ssh
supervisorctl stop my_app
/home/vagrant/my_app_env/bin/python /vagrant/manage.py runserver


Теперь мы можем спокойно редактировать код, и все изменения будут подхватываться django сервером автоматически.
И если у нас все еще временно подправлен hosts, то на запросы dev.my-app.com будет отвечать все тот же django сервер.

Prod конфигурация


Вот мы и добрались до самого главного. Будем считать, что деплоиться мы будем на prod-my-app.

Далее мы рассмотрим вариант деплоя в ситуации, когда у нас есть отдельный сервер для salt-master (далее просто мастер).

Копируем на мастер конфиги, добавляем в /srv/salt/top.sls
  ‘prod-my-app’:
    - nginx
    - python
    - supervisor
    - my_app


Или в нашем случае можно сделать вот так:
base:
  ‘*-my-app’:
    - nginx
    - python
    - supervisor
    - my_app


Далее делаем то же самое с файлом /srv/pillar/top.sls
base:
  '*-my-app':
    - my-app


В /srv/pillar/my_app.sls меняем переменные согласно нашей карте раскладки.

На prod-my-app ставим salt-minion. Подключаем salt-minion к salt-master (как это сделать читай тут).
Теперь на мастере можно запустить применение конфигов:
sudo salt prod-my-app state.highstate


Что осталось:

Доставка конфигов на мастер

Тут куча способов от rsync до git. Все зависит от вашей внутренней политики.

Доставка исходников проекта на prod-my-app

Тут опять же куча вариантов. Лично я делаю так: при помощи salt поддерживаю на prod-my-app репозиторий git на определенном коммите, хэш которого хранится в pillar, и если он меняется, то salt в конце работы запускает скрипты деплоя.

Честно говоря это не самый лучший способ, но он самый простой. В идеале создавать, например, нативные пакеты, или поднимать приватный pypi.

Ссылки


www.saltstack.com
www.vagrantup.com
hynek.me/talks/python-deployments — весьма полезная статья, которая содержит набор тезисов на тему деплоя python проектов.

PS
К сожалению очень многие вещи пришлось упустить, иначе бы статья раздулась до неприлично большого размера. Я старался упускать либо очевидные вещи, либо то, что легко найти в документации. Тем не менее задавайте вопросы в комментариях.
Tags:
Hubs:
+44
Comments 34
Comments Comments 34

Articles