Автоматизация развертывания Docker-контейнеров на произвольной инфраструктуре
Контейнеризация приложений сегодня является не просто модным трендом. Объективно такой подход позволяет во многом оптимизировать процесс серверной разработки путем унификации поддерживаемых инфраструктур (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 готовят скачивают образ из основного Registry и закачивают его в локальный, откуда уже через туннель образ попадает на боевую инфраструктуру (команда pull). Запустить приватный Registry локально можно выполнив в терминале следующую строчку кода:
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.
Буду рад ответить в комментариях на любые вопросы, а также выслушать конструктивную критику.