python

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

  • вторник, 19 мая 2015 г. в 02:11:22
http://habrahabr.ru/post/257671/

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

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


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

Наши цели

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

Устанавливаем 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.


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