Разворачиваем web-приложение при помощи Fabric

от автора

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

  • Одни и те же повторяющиеся действия занимают существенное количество времени
  • Про какие-то действия забывают, другие путают местами
  • Человеческий фактор, легкомысленность и недальновидность

В этой статье я расскажу о том, как превратить увлекательное и, местами, непредсказуемое приключение в простую, рутинную и скучную операцию с предсказуемым результатом.

Наши цели
  • Выполняем операции быстрее, в т.ч. параллельно на нескольких серверах
  • Имеем хорошо протестированную процедуру
  • Имеем простой в работе инструмент, с помощью которого можно выполнить любое действие с приложением
Устанавливаем Fabric

Мы будем использоваться fabric. Это набор отличный универсальный инструмент для автоматизации задач системного администрирования. Он хорошо представлен в этой статье.

Итак, предположим, что у нас уже есть рабочая станция с Linux и python(2.5-2.7). Установим необходимые средства:

# pip install fabric # pip install fabtools 
Немного рассуждений

Взаимодействовать с сервером мы будем через ssh. Поэтому я предполагаю, что вы уже сгенерировали ключ, определили его использование в ~/.ssh/config и положили публичную часть на сервер. Для удобства управления пользователями на сервере, мы создаем каждому из деплой-иженеров персональный аккаунт, который в процессе работы может выполнять команды от пользователя проекта или оборачиваться в него.

Из чего же будет состоять наш простейший деплой проект:

myapp_deploy/         fabfile.py    #собственно сам скрипт, его наличие обязательно.         sudoers/                 devel_sudo    #инклуд в /etc/sudoers для дополнения прав деплой-инженера         config/                 config-production.php    #конфиг приложения 

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

Пишем скрипт
from fabric.api import env, run, sudo, local, put, settings import fabtools  def production():         """Defines production environment"""         env.hosts = ['192.168.1.2']    #адрес хоста         env.shell = 'bash -c'    #интерпритатор для выполнения команд на удаленном хосте         env.use_ssh_config = True    #импортируем конфигурацию нашего ssh-клиента          env.project_user = "myappuser"    #пользователь, от которого работает проект на удаленном сервере         env.sudo_user = env.project_user         env.base_dir = "/apps"    #базовая директория для проекта         env.domain_name = "myapp.example.com"    #FQDN для нашего продакшн окружения         env.domain_path = "%(base_dir)s/%(domain_name)s" % { 'base_dir':env.base_dir, 'domain_name':env.domain_name }    #здесь мы сформировали абсолютное имя каталога для myapp         env.current_path = "%(domain_path)s/current" % { 'domain_path':env.domain_path }    #путь до текущей версии myapp         env.releases_path = "%(domain_path)s/releases" % { 'domain_path':env.domain_path }    #путь до каталога с релизами         env.git_clone = "git@git.example.com:myapp.git"    #репозиторий, откуда мы будем клонировать проект.          env.config_file = "config/config-production.php"    #конфигурационные параметры для нашего проекта  

Теперь займемся непосредственно функционалом. Для начала нам надо произвести первичную настройку сервера. Эту операцию мы будем проводить только в случае, если надо поменять какие-то настройки в системе. Вызов функции лучше поручить системному администратору, имеющему полные привилегии в системе.

def permissions():         """Set proper permissions for release"""         sudo("chown -R %(project_user)s:%(project_user)s %(domain_path)s" % { 'domain_path':env.domain_path, 'project_user':env.sudo_user })  def setup():         """Prepares one or more servers for deployment"""         with settings(sudo_user='root'):                 sudo("mkdir -p %(domain_path)s/releases" % { 'domain_path':env.domain_path })                     sudo("mkdir -p %(base_dir)s/logs/" % { 'base_dir':env.base_dir })                     permissions()                      put("sudoers/devel_sudo", "/tmp/devel_sudo")                 sudo("chown root:root /tmp/devel_sudo")                 sudo("chmod 0440 /tmp/devel_sudo")                 sudo("mv /tmp/devel_sudo /etc/sudoers.d/devel_sudo")  

Далее определимся с хранением релизов на сервере. Вот как будет выглядеть структура каталогов:

/apps         myapp.example.com/                 current    #симлинк на текущий релиз                 releases/    #каталог релизов                          release0                         release1                         ...                         releaseN    #наш последний релиз 

Индекс релиза мы будем определять с помощью таймстемпа. Соответственно нам надо находить последний релиз и предпоследний релизы (на случай, если мы захотим откатиться) и пути до них.

def releases():         """List a releases made"""         env.releases = sorted(sudo('ls -x %(releases_path)s' % { 'releases_path':env.releases_path }).split())         if len(env.releases) >= 1:                 env.current_revision = env.releases[-1]                 env.current_release = "%(releases_path)s/%(current_revision)s" % { 'releases_path':env.releases_path, 'current_revision':env.current_revision }         if len(env.releases) > 1:                 env.previous_revision = env.releases[-2]                 env.previous_release = "%(releases_path)s/%(previous_revision)s" % { 'releases_path':env.releases_path, 'previous_revision':env.previous_revision } 

Деплой зачастую связан требует перезапуска каких-нибудь сервисов. Несмотря на то, что у нас простейший проект, нам потребуется релоадить php-fpm, чтобы избежать известной проблемы.

def restart():         """Restarts your application services"""         with settings(sudo_user='root',use_shell=False):                 sudo("/etc/init.d/php5-fpm reload") 

Теперь нам надо каким-то бразом забирать код из хранилища. Будем клонировать последние версии файлов из git-репозитория.

def checkout():         """Checkout code to the remote servers"""         env.timestamp = run("/bin/date +%s")         env.current_release = "%(releases_path)s/%(timestamp)s" % { 'releases_path':env.releases_path, 'timestamp':env.timestamp }         sudo("cd %(releases_path)s; git clone -q -b master --depth 1 %(git_clone)s %(current_release)s" % { 'releases_path':env.releases_path, 'git_clone':env.git_clone, 'current_release':env.current_release }) 

Копируем конфигурационные файлы в каталог с релизом

def copy_config():         """Copy custom config to the remote servers"""         if not env.has_key('releases'):    #определяем последний релиз, в который нам надо положить конфиг                 releases()         put("%s" % env.config_file, "/tmp/config.php")         sudo("cp /tmp/config.php %(current_release)s/config/" % { 'current_release':env.current_release })         run("rm /tmp/config.php") 

С помощью симлинка делаем последний релиз актуальным

def symlink():         """Updates the symlink to the most recently deployed version"""         if not env.has_key('current_release'):                 releases()         sudo("ln -nfs %(current_release)s %(current_path)s" % { 'current_release':env.current_release, 'current_path':env.current_path }) 

Собираем всю нашу процедуру деплоя в единое целое

def deploy():         """Deploys your project. This calls  'checkout','copy_config','migration','symlink','restart','cleanup'"""         checkout()         copy_config()         symlink()         restart() 

Подумаем над необходимостью чистить релизы.

def cleanup():         """Clean up old releases"""         if not env.has_key('releases'):                 releases()         if len(env.releases) > 10:                 directories = env.releases                 directories.reverse()                 del directories[:10]                 env.directories = ' '.join([ "%(releases_path)s/%(release)s" % { 'releases_path':env.releases_path, 'release':release } for release in directories ])                 sudo("rm -rf %(directories)s" % { 'directories':env.directories }) 

Если после релиза что-то пошло не так, нам нужна возможность быстро откатиться до предыдущей версии.

def rollback_code():         """Rolls back to the previously deployed version"""         if not env.has_key('releases'):                 releases()         if env.has_key('previous_release'):                 sudo("ln -nfs %(previous_release)s %(current_path)s && rm -rf %(current_release)s" % { 'current_release':env.current_release, 'previous_release':env.previous_release, 'current_path':env.current_path })         else:                 print "no releases older then current"                 sys.exit(1)  def rollback():         """Rolls back to a previous version and restarts"""         rollback_code()         restart() 

Отлично, на сегодня хватит питона.

Использование

Получить список всего того, что умеет наш скрипт, можно следующим образом:

$ fab --list Available commands:      checkout       Checkout code to the remote servers     cleanup        Clean up old releases     copy_config    Copy custom config to the remote servers     deploy         Deploys your project. This calls  'checkout','copy_config','migration','symlink','restart','cleanup'     permissions    Set proper permissions for release     production     Defines production environment     releases       List a releases made     restart        Restarts your application services     rollback       Rolls back to a previous version and restarts     rollback_code  Rolls back to the previously deployed version     setup          Prepares one or more servers for deployment     symlink        Updates the symlink to the most recently deployed version 

Отлично, давайте деплоиться на сервер

$ fab production deploy [192.168.1.2] Executing task 'deploy' [192.168.1.2] run: /bin/date +%s [192.168.1.2] out: 1431941361 [192.168.1.2] out:  [192.168.1.2] sudo: cd /apps/myapp.example.com/releases; git clone -q -b master --depth 1 git@git.example.com:myapp.git /apps/myapp.example.com/releases/1431941361 [192.168.1.2] sudo: ls -x /apps/myapp.example.com/releases [192.168.1.2] out: 1431940514  1431940525       1431940537  1431940547  1431940558  1431940568  1431940578  1431940589  1431940599  1431941361 [192.168.1.2] out:  [192.168.1.2] put: config/config-production.php -> /tmp/config.php [192.168.1.2] sudo: cp /tmp/config.php /apps/myapp.example.com/releases/1431941361/config/ [192.168.1.2] run: rm /tmp/config.php [192.168.1.2] sudo: ln -nfs /apps/myapp.example.com/releases/1431941361 /apps/myapp.example.com/current [192.168.1.2] sudo: /etc/init.d/php-fpm reload [192.168.1.2] out: Reloading php-fpm: [18-May-2015 05:29:29] NOTICE: configuration file /etc/php-fpm.conf test is successful [192.168.1.2] out: [192.168.1.2] out:                                       [  OK  ] [192.168.1.2] out: [192.168.1.2] out:   Done. Disconnecting from 192.168.1.2... done. 

В итоге мы получили отличное средство, которое с небольшими доработками можно использовать в большом количестве проектов с разной спецификой.

ссылка на оригинал статьи http://habrahabr.ru/post/257671/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *