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.
В итоге мы получили отличное средство, которое с небольшими доработками можно использовать в большом количестве проектов с разной спецификой.