Как собрать Docker-контейнеры с помощью Ansible

d8c78bf7fa11144d6b1a135ba11928fe.png

Docker — это система контейнеризации, собирающая независимые части ОС без установки библиотек в основную систему. В отличие от виртуалок, которые собираются долго, такие контейнеры собираются и запускаются достаточно быстро. Это позволило Docker и Kubernetes стать одним из главных средств автоматизации и деплоя.

Как собирать и загружать контейнеры

Например, у нас есть файл hosts с build_host и runner_host. Нам нужно собрать контейнеры из готовых Docker-файлов и перенаправить их на runner_host, который будет их запускать.

c626db8c79bffc414cc7623d3b667190.png

Производим установку при помощи yum-repository, а затем передаем списки точно так же, как и apt, который можно сделать путем передачи списка переменных. Рекомендую этот способ в отличие от with_items и циклов, поскольку так будет быстрее. Если у модуля есть внутренняя оптимизация, тогда он будет принимать списки и прогонять их быстрее. Рекомендую пользоваться только таким методом.

Далее устанавливается SDK Docker и поднимается сам сервис. Следующая задача — сборка контейнера на build_host. При сборке контейнеров нужно взять их из directory files на локальной машине вместе с Ansible и из всех Docker-файлов внутри папок.

Мы видим gather_facts: no, поскольку нас не интересует Build container как виртуальная машина, т. к. мы туда ничего не устанавливаем, а используем с уже готовым набором софта, чтобы выполнять необходимые операции. 

7f94e83a6730443662d7ccc2c3a05ddd.png

Теги

Первое, что мы видим, — это теги. Они могут выставляться у тасок, плеев в плейбуке и у ролей. Они также могут быть у тасок, включаемых из роли, в случае, если вы объявляете ее отдельно. Теги нужны для запуска определенных участков кода. 

Ansible по умолчанию при запуске плейбука проверяет все сверху донизу, но вам такая опция может быть нужна не всегда. Иногда у вас могут быть контрольные плейбуки, которые выполняют несколько вещей: устанавливают Docker, собирают Docker-контейнеры, загружают и запускают их. Это три таски и три разных плея. Необязательно запускать все с самого начала, чтобы что-то запустить в Docker-контейнере. Вместо этого мы можем поставить отдельные теги и с помощью команды — и -tags и имени тега запустить соответствующий плей. Если у вас есть какой-то плей и какая-то таска после этого плея в другом плее, вы можете запустить и ее, поставив ей такой же тег, как у вашего плея, тогда запустится плей и одна таска. Комбинировать это можно как угодно. Один плей, как и одна таска, могут иметь несколько тегов. 

Следите за именами, не давайте тегам разбегаться. Если у вас теги называются ansible_build, ansible_load, то тег community_docker_install будет несколько неуместен. В отличие от переменных: там теги внутри ролей именуются как угодно, а теги внутри плейбуков требуют определенного механизма именования.

И еще важный момент: существует два тега — always и never. Первый будет запущен всегда при команде -tags, второй, если вы его никак не отметите, вообще не будет запущен. Это может быть полезно тогда, когда у вас есть таска, без которой ничего не будет работать. В этом случае ей нужно присвоить тег always. Или, например, у вас есть таска, которую включать не нужно, но она вам нужна на какой-то крайний случай. Тогда используем тег never. В целом не советую строить комплексные структуры тегов: чем проще вы пишете плейбуки, тем лучше.

Когда все подготовлено, мы шерстим с помощью команды ansible.builtin.find путь ~/ansible/Docker/files, а в моем случае это путь, где все лежит. Нам нужно найти все, что имеет type: «directory». Команда delegate_to отправит все на локальный хост, где запущен Ansible. Вывод отправится в build_host в переменную files.

4d689f3d39c759266ceec063f414012c.png

Далее на build_host нужно удалить directory_dockerfiles и создать directory заново. У нас есть таска, которая повторяется несколько раз и которую нам нет смысла разворачивать, поскольку это цикл. Этот цикл можно увидеть при помощи directory_loop.

Циклы Ansible

В первых версиях Ansible единственное, что было для организации циклов, — это конструкция with_. Разработчикам Ansible не очень нравилась «магия», которая происходит с развязыванием этой конструкции. Они придумали цикл loop. Если вы передадите loop какую-то списковую переменную, то она будет работать так же, как и with_, но без модуля. Loop примет в себя любые данные в списочном формате. Что немаловажно, этот цикл прозрачен.

377c5a765bea77fea4b5f1e3f9f4ee4d.png

Второй список — until. Таска будет выполняться сколько угодно раз, пока не выполнится условие выхода из этой таски. 

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

Рассмотрим пару примеров. Цикл loop вызывается при помощи query и lookup. Разница между ними в том, что query возвращает массив, а lookup — строчку. Оба цикла аналогичны with_inventory_hostnames.

a90c847ccfcf5ca3a0e05f0ea9355b65.png

То, что регистрируется как переменная files после ansible.builtin.find, имеет ключ files. Именно поэтому к нему обращаемся через loop:»{{files.files}}». Здесь включается таска «Copy files and build them», которая копирует и билдит файлы как контейнеры. Также включаются таски из container_assembly. Это не только способ разбить большую таску на несколько, но и единственный способ включить таски и выполнить несколько тасок в цикле. Обратите внимание, что у Ansible существует две директивы: build_tasks и import_tasks. Разница в том, что import_tasks импортирует таски и «развернет» их. Проще говоря, она скопирует таски из одного места в другое, но применить loop к ним вы не сможете, поскольку Ansible сделает это до того, как будет выполнять плейбук. Также вы не сможете поставить переменные в имени, т. к. Ansible сделает это до. Соответственно, include_tasks включает их динамически, что дольше при выводе плейбука, но зато вы сможете бегать по ним циклами, динамически создавать имена и т. д.

476d2ee37356fbf721f5a7bcd1438c40.png

Теперь посмотрим, что находится внутри container_assembly. Здесь мы сталкиваемся со следующим: известная вам команда file должна создать директорию для изображений. То есть внутри Docker/files она должна создать папку db и web. Такой стиль записи для питонистов-программистов: /root/dockerfiles/{{item.path.split ('/')[-1]}}. При копировании dockerfiles в другую папку я применяю более характерную для Ansible команду — фильтр. Выглядеть это будет так: /root/dockerfiles/{{item.path. | basename}}/Dockerfile. Такой вариант для сисадминов будет более читаем. 

bfb1720e00f34ef30f8892e742fa5433.png

Как строить контейнеры

Команда docker_image является нативной для Ansible и требует установленный Docker SDK. Эта команда построит контейнер. Его можно назвать »{{item.path | basename}}_container: v1.0». Внутри docker_image есть массив build, в который передается переменная path. 

Поскольку билд и запуск контейнеров происходят на двух разных машинах, нам эти контейнеры нужно поменять. Я буду действовать дедовскими методами: буду паковать контейнеры в .tar-файлы, архивировать их и перемещать через родительскую машину. Для этого я воспользуюсь командой docker_image, но уже не build, а archive — archive_path:»/root/{{item.path | basename}}_container: v1.0.tar». Она заархивирует контейнеры в .tar-файлы. После этого при помощи команды fetch я забираю эти файлы к себе на машину Ansible в виде .tar-архива в папку «files/{{item.path | basename}}_container: v1.0.tar». Никаких дополнительных путей в этом случае прописывать не нужно. Я ставлю flat: true. 

fa2664b7894a41af1a88f1a99d263582.png

Последний плей в плейбуке — это загрузка контейнеров, которая будет производиться на runner_host. Для этого используем команду «find docker directories» на локальной машине. В регистре допустимо оставить files. Те файлы, что мы собрали, нужно агрегировать и передать в container_load, который точно так же соберет и скопирует эти файлы. Теперь для загрузки контейнера из архива нам нужно его как-то назвать. Питонисты в этом случае сделали бы сплит, взяли имя контейнера, разделили его по точке и использовали в качестве имени. Я иду путем Ansible и даю следующее название:»{{item.path | splitext | first}}». Run container запустит контейнер. 

bafb2372bd1cffbfd8103fe1fa81c15d.png

Healthcheck

По традиции я делаю Healthcheck. Для этого я использую цикл until. Чтобы проверять что-то в этом цикле, необходимо зарегистрировать нужную переменную на хосте, и она будет регистрироваться при каждом вызове таски. При помощи модуля docker_host_info я собираю информацию о контейнерах и проверяю количество запущенных контейнеров. Это я делаю потому, что Docker может не запустить какие-то контейнеры, или они могут повиснуть в каком-то стартапе и не сразу вызваться. Я не могу проверить один раз и уйти: проверку нужно сделать несколько раз. Если я вызову команду Ansible плейбук с тегом load, она включит в себя healthcheck, но если я вызову команду healthcheck отдельно, она включит в себя только healthcheck.

b6dac8a9662bf1554ec39cc1de78314c.png

© Habrahabr.ru