Контейнеризация приложений сегодня является не просто модным трендом. Объективно такой подход позволяет во многом оптимизировать процесс серверной разработки путем унификации поддерживаемых инфраструктур (dev, test, staging, production). Что в итоге приводит к значительному сокращению издержек на протяжении всего цикла жизни серверного приложения.
Хотя большая часть из перечисляемых достоинств Docker является правдой, тех, кто на практике столкнется с контейнерами, может постигнуть легкое разочарование. И так как Docker не является панацеей, а всего лишь входит в список из «лекарственных средств» от рецепта автоматического деплоя, разработчикам приходится осваивать дополнительные технологии, писать дополнительный код и т.д.
Разработкой своего собственного рецепта автоматизации конфигурирования и разворачивания контейнеров на различные инфраструктуры мы занимались в последние несколько месяцев, параллельно с коммерческими проектами. А полученный результат практически полностью удовлетворил наши текущие потребности в автодеплое.
Выбор инструмента
Когда впервые возникает потребность в автоматическом деплое Docker-приложений, то первое, что подсказывает опыт (либо поисковик) — это постараться приспособить для этой задачи Docker Compose. Изначально задумывавшийся как инструмент для быстрого запуска контейнеров на тестовой инфраструктуре, Docker Compose, однако, можно использовать и на бою. Еще один вариант, который рассматривался нами в качестве подходящего инструмента — Ansible, имеющий в своем составе модули для работы с контейнерами и образами Docker.
Но ни то, ни другое решение нас как разработчиков не устроило. И самая главная причина этого кроется в способе описания конфигураций — при помощи файлов YAML. Для того, чтобы понять эту причину, я задам простой вопрос: кто-нибудь из вас умеет программировать на YAML? Удивлюсь, если кто-то ответит утвердительно. Отсюда главный недостаток всех инструментов, использующих для конфигурирования всевозможные варианты разметок (от INI/XML/JSON/YAML до более экзотических, вроде HCL) — невозможность расширения логики стандартными способами. Среди недостатков можно также упомянуть отсутствие autocomplete и возможности прочитать исходный код используемой функции, отсутствие подсказок о типе и количестве аргументов и прочих радостей использования IDE.
Далее, мы посмотрели в сторону Fabric и Capistrano. Для конфигурирования они используют обычный язык программирования (Python и Ruby, соответственно), то есть позволяют писать кастомную логику внутри конфигурационых файлов с возможностью использования внешних модулей, чего мы, собственно, и добивались. Мы не стали долго выбирать между Fabric и Capistrano и почти сразу остановились на первом. Наш выбор, в первую очередь, был обусловлен наличием экспертизы в Python и почти полным ее отсутствием в Ruby. Плюс, смутила довольно сложная структура проекта Capistrano.
В общем, выбор пал на Fabric. Благодаря его простоте, удобству, компактности и модульности он поселился в каждом нашем проекте, позволяя хранить само приложение и логику его развертывания в одном репозитории.
Наш первый опыт написания конфига для автоматического деплоя при помощи Fabric дал возможность выполнять основные действия по обновлению приложения на боевой и тестовой инфраструктурах и позволил экономить значительный объем времени разработчика (отдельного релиз-менеджера у нас нет). Но при этом файл с настройками вышел довольно громоздким и трудным для переноса на другие проекты. Мы задумались над тем, как задачу адаптации конфигов под другой проект решать легче и быстрее. В идеале хотелось получить универсальный и компактный шаблон конфигурации развертывания на стандартный набор имеющихся инфраструктур (test, staging, production). К примеру, сейчас наши конфиги для автодеплоя выглядят примерно так:
from fabric import colors, api as fab
from fabricio import tasks, docker
##############################################################################
# infrastructures
##############################################################################
@tasks.infrastructure
def STAGING():
fab.env.update(
roledefs={
'nginx': ['devops@staging.example.com'],
},
)
@tasks.infrastructure(color=colors.red)
def PRODUCTION():
fab.env.update(
roledefs={
'nginx': ['devops@example.com'],
},
)
##############################################################################
# containers
##############################################################################
class NginxContainer(docker.Container):
image = docker.Image('nginx')
ports = '80:80'
##############################################################################
# tasks
##############################################################################
nginx = tasks.DockerTasks(
container=NginxContainer('nginx'),
roles=['nginx'],
)
Приведенный пример кода содержит в себе описание нескольких стандартных действий для управления контейнером, в котором запускается небезызвестный веб сервер. Вот что мы увидим, попросив Fabric вывести список команд из директории с этим файлом:
Available commands: PRODUCTION STAGING nginx deploy[:force=no,tag=None,migrate=yes,backup=yes] - backup -> pull -> migrate -> update nginx.deploy deploy[:force=no,tag=None,migrate=yes,backup=yes] - backup -> pull -> migrate -> update nginx.pull pull[:tag=None] - pull Docker image from registry nginx.revert revert - revert Docker container to previous version nginx.rollback rollback[:migrate_back=yes] - migrate_back -> revert nginx.update update[:force=no,tag=None] - recreate Docker container
Здесь стоит немного пояснить, что кроме типовых deploy, pull, update и пр. в списке присутствуют также таски PRODUCTION и STAGING, которые при запуске никаких действий не совершают, но подготавливают окружение для работы с выбранной инфраструктурой. Без них большинство других тасков выполниться не сможет. Это «стандартный» workaround (обход) того факта, что Fabric не поддерживает явного выбора инфраструктуры для работы. Следовательно, для того, чтобы запустить процесс развертывания/обновления контейнера с Nginx, к примеру на STAGING, нужно выполнить следующую команду:
fab STAGING nginx
Как нетрудно уже было догадаться, практически вся «магия» скрыта за этими строками:
nginx = tasks.DockerTasks(
container=NginxContainer('nginx'),
roles=['nginx'],
)
Ciao, Fabricio!
В общем, позвольте представить Fabricio — модуль, который расширяет стандартные возможности Fabric, добавляя в него функционал для работы с контейнерами Docker. Разработка Fabricio позволила нам перестать думать о сложности реализации автоматического деплоя и целиком сосредоточиться на решении поставленных бизнес-задач.
Очень часто мы сталкиваемся с ситуацией, когда на боевой инфраструктуре заказчика имеются ограничения на доступ в интернет. В этом случае мы решаем задачу деплоя при помощи приватного Docker Registry, запущенного в локальной сети администратора (либо просто на его рабочем компьютере). Для этого в примере выше нужно всего лишь заменить тип тасков — DockerTasks на PullDockerTasks. Список доступных команд в этом случае приобретет вид:
Available commands: PRODUCTION STAGING nginx deploy[:force=no,tag=None,migrate=yes,backup=yes] - prepare -> push -> backup -> pull -> migrate -> update nginx.deploy deploy[:force=no,tag=None,migrate=yes,backup=yes] - prepare -> push -> backup -> pull -> migrate -> update nginx.prepare prepare[:tag=None] - prepare Docker image nginx.pull pull[:tag=None] - pull Docker image from registry nginx.push push[:tag=None] - push Docker image to registry nginx.revert revert - revert Docker container to previous version nginx.rollback rollback[:migrate_back=yes] - migrate_back -> revert nginx.update update[:force=no,tag=None] - recreate Docker container
Новые команды prepare и push
docker run --name registry --publish 5000:5000 --detach --restart always registry:2
Пример со сборкой образа отличается от первых двух, аналогично, только типом тасков — в данном случае это BuildDockerTasks. Список команд для задач со сборкой такой же, как и в предыдущем примере, за исключением того, что команда prepare вместо скачивания образа из основного Registry строит его из локальных исходников.
Для использования PullDockerTasks и BuildDockerTasks необходим установленный на компьютере администратора клиент Docker. После анонсирования публичных бета-версий Docker для платформ MacOS и Windows это уже не такая головная боль для пользователей.
Fabricio является полностью открытым проектом, любые доработки приветствуются. При этом мы сами активно продолжаем дополнять его новыми возможностями, исправлять баги и устранять неточности, постоянно совершенствуя необходимый нам инструмент. На текущий момент основными возможностями Fabricio являются:
- сборка образов Docker
- запуск контейнеров из образов с произвольными тегами
- совместимость с режимом параллельного выполнения тасков на разных хостах
- возможность отката к предыдущему состоянию (rollback)
- работа с публичными и приватными Docker Registry
- группировка типовых задач в отдельные классы
- автоматическое применение и откат миграций Django-приложений
Установить и попробовать Fabricio можно через стандартный пакетный менеджер Python:
pip install --upgrade fabricio
Поддержка пока ограничена Python 2.5-2.7. Данное ограничение является прямым следствием поддержки соответствующих версий модулем Fabric. Надеемся, что в ближайшем будущем Fabric обзаведется возможностью запуска на Python 3. Хотя необходимости в этом особой нет — в большинстве дистрибутивов Linux, а также MacOS, версия 2 является дефолтной версией Python.
Буду рад ответить в комментариях на любые вопросы, а также выслушать конструктивную критику.
Комментарии (28)
kop
27.06.2016 18:56Аналогично, первая мысль после прочтения: «Зачем писать велосипеды, если есть K8S?».
renskiy
27.06.2016 21:09+4Насколько я понял, вы имеет в виду Kubernetes? Или по крайней мере практики его использования в качестве средства для оркестрации Docker.
Fabricio не является заменой Kubernetes/Swarm. Главная цель Fabricio — кастомизация логики деплоя через создание типовых классов с описанием логики запуска контейнера (которая в случае Docker Compose и Kubernetes прописывается в файлах YAML) на полноценном языке программирования с всеми вытекающими преимуществами ООП.
При этом на целевой инфраструктуре, в теории, может быть развернута любая система запуска Docker контейнеров (мы ориентируемся в этом случае на Swarm, который начиная с версии Docker 1.12 является частью самого Docker).
Andrey_Zentavr
28.06.2016 02:34не знаю чем вам Ансибл не сгодился? мы вполне себе неплохо дженкинс+ансибл используем.
Нужно вам найти ДевОПСа хорошего.
Кстати, до этого мы лет пять строили велосипеды через Фабрику. Теперь вот всем этим занимается Ансибл.renskiy
28.06.2016 08:58+3Ansible нам напомнил о некогда довольно популярном Apache Ant, только последний использует XML, а Ansible более модный YAML. Оба ведут себя неплохо до тех пор, пока идешь по дорожке, протоптанной авторами (или пока им хватает энтузиазма развивать продукт).
Да и накладно содержать специалиста по Ansible только ради DevOps обязанностей.SamVimes
28.06.2016 10:07А в чем проблема запускать шелл-, питон-скрипты из Ansible?
renskiy
28.06.2016 10:24+2Так и проблем наверно нет, но их можно и без Ansible запускать. А структура проекта Ansible и без внешних скриптов выглядит довольно запутанно.
С Fabric/Fabricio я могу выбрать такую структуру, которую захочу. И прописать всю логику в одном файле (fabfile.py), либо создать целый подпроект (внутри модуля fabfile) с разделением на роли, инфраструктуры и пр.foxmuldercp
28.06.2016 10:30На первый взгляд запутано, но в принципе достаточно неплохо структурировано
foxmuldercp
28.06.2016 10:30Соглашусь, к сожалению, при попытке уйти вправо/влево от того, что предоставляет модуль Ansible, приходится городить raw команды, например, к тем же iptables у меня треть плейбука именно raw, потому как штатно нужного мне функционала, пока не придвидится, и учитывая ньюансы, не проще ли отказаться от обертки и дергать ssh -c somecommmand напрямую…
VolCh
28.06.2016 10:56Вопрос надо ставить «можем ли мы себе позволить отдельного человека по DevOps» и если можем, то при выборе продуктов — его слово решающим должно быть, а не разработчиков или админов.
VolCh
28.06.2016 10:52Для того, чтобы понять эту причину, я задам простой вопрос: кто-нибудь из вас умеет программировать на YAML?
Как по мне, то если процесс деплоя нельзя выразить в декларативной форме, то что-то то ли с архитектурой, то ли с процессом не так. А так тот же Ansible имеет и переменные, и условия, и циклы.
Среди недостатков можно также упомянуть отсутствие autocomplete и возможности прочитать исходный код используемой функции, отсутствие подсказок о типе и количестве аргументов и прочих радостей использования IDE
Вот это более весомый аргумент, но в принципе решаемый плагинами к IDE. Например, в PhpStorm YAML конфиги инфраструктуры Symfony довольно хорошо интегрированы. И для Ansible плагин есть https://plugins.jetbrains.com/plugin/7792
welcomerooot
28.06.2016 11:13Спасибо за статью!
А можете немого поподробнее про rollback и откат миграций? Как это делает fabric? Очень интересен этот момент.renskiy
28.06.2016 12:04+2Команда rollback последовательно запускает два стандартных метода у базовой сущности Fabricio (Container): migrate_back и revert. По-умолчанию, у первого метода отсутствует реализация, но потомки класса могут это поведение переопределить. DjangoContainer при откате миграций на предыдущую версию парсит список примененных миграций у текущей версии контейнера и у той, на которую нужно откатиться, составляет разницу и откатывает только те миграции, которых нет в старом контейнере.
varnav
28.06.2016 17:24+1Кто-нибудь из вас умеет программировать на YAML?
Я умею! А на Ruby и Python — наоборот, не умею.
gudron
28.06.2016 18:39А как дела с сетью? Имеется ввиду, сеть на уровне связки докер-контейнеров. А не на уровне конкртеного контейнера.
docker-compose позволяет настраивать некоторые вещи через:
networks:
app_net:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.16.100.0/28
ip_range: 172.16.100.0/28
gateway: 172.16.100.7
renskiy
28.06.2016 19:33Управление сетью не предусмотрено, но у класса Container есть свойство network, в котором можно задать имя сети.
ushliy
28.06.2016 19:24+1ИМХО, Ansible обеспечивает огромный простор для деплоя разнообразных приложений. На данный момент у меня им управляется зоопарк из Docker, LXC и физических серверов. Прекрасно дружит с Jenkins. Присоединюсь ко мнению, что с архитектурой что-то не так, если не хватает декларативного YAML для деплоя. Да и с теми же кастомными скриптами, проще один плейбук запустить, чем ломиться по ssh и выполнять поштучно. А если серверов больше одного, так и тем более.
З.Ы.: Зачем отдельный специалист по Ansible, если там настолько хорошая документация, что рядовой *nix админ со средним английским осилит за неделю, максимумgudron
29.06.2016 16:55Не могли бы вы уточнить как именно ansible у вас работает? Посмотрел на http://docs.ansible.com/ansible-container/. Не увидел большой разницы с тем же docker-compose. Увидел, что умеет билдить, пушить и пулить контейнеры, но так и не понял каким образом работает, то что называется деплоем.
Не увидел, опять же, как делать rollback.
или вы просто через ansible, вызывается на host-машине pull && run контейнеров?ushliy
29.06.2016 17:04Ansible разворачивает проект на новых серверах, после сборки в Jenkins пулит на существующие. Согласен, Compose это все умеет. Но как-то не прижился, потом написали свои костыли и не вспоминали про него)
renskiy
29.06.2016 17:19Если проект один, то костыли да, решают проблему. Но копировать костыли из проекта в проект очень накладно. А нам приходится часто начинать новые проекты. Поэтому все костыли мы унифицировали и вынесли в отдельный проект.
ushliy
29.06.2016 17:25Костыли не настолько страшные и весьма унифицируемые. В рамках работы Ansible. Но по сравнению с развивающимся весьма динамично Swarm, это костыли. Но опять же, учитывая, что Docker — лишь часть структуры, Swarm не решит всех проблем, а Ansible — вполне
VolCh
30.06.2016 06:371. Ansible готовит новый хост (грубо, чистая ось только с sshd) к исполнению докер-контейнеров.
2. Ansible не просто запускает контейнеры, а обеспечивает им всю среду, в частности обеспечивает наличие на хосте файлов, которые монтируются в контейнер (или файлов, которые копируются в контейнер при билде).
rudenkovk
30.06.2016 11:06Ринат, во многом ты прав. Но в сути, это обычная проблема, когда программисты пишут деплой, вы делаете так, как привыкли и вам удобнее. Это будет продолжаться ровно до момента, когда вы передадите весь этот набор деплоя в руки эксплуатации/девопсов. После этого будет резко удобно все писать на ansible (или прочих salt/puppet). Ровно потому, что при всей простоте того же fabric, поддержке все же надо будет освоить прилично писать на питоне, и поддерживать ваше понимание об организации кода. Я уже имел опыт перевода с хорошо написанного fabric на ansible. И на ansible, в результате, код получался короче, проще, красивее и быстрее. Особенно если нужно разливать на очень заковыристые inventory.
PS по мне, inventory, это вообще беда fabric.
renskiy
01.07.2016 10:09Это будет продолжаться ровно до момента, когда вы передадите весь этот набор деплоя в руки эксплуатации/девопсов.
На этом месте, как правило, и работа наша заканчивается — это специфика разработки аутсорс. А так-как эксплуатация полностью продолжается на стороне заказчика, то нам нет смысла развивать у себя экспертизу в этой области.
Есть правда у нас в разработке пара типовых решений PaaS, где скорее всего процесс эксплуатации будет на нашей стороне. Поживем увидим.
Fafnir
Если гоняете микро-сервисы, попробуйте Kubernetes. Используем его уже где-то полгода, не без проблем, но полет нормальный.
rumkin
Кубернет требует работы через google cloud, что выглядит очень-очень странным решением. Ну, и ко всему требует коммиты на каждый чих, что лично мне совсем не нравится и является ненужным порогом при освоении технологии.
kop
> Кубернет требует работы через google cloud
Его можно запросто поднять хоть на AWS, хоть на своем железе.
> Ну, и ко всему требует коммиты на каждый чих
Вовсе не обязательно хранить ваши конфигурации в VCS. С ними можно работать как душе угодно (хоть через UI, хоть редактировать напрямую с сервера). А в идеале вообще использовать Helm.