В начале этого года мы посчитали, что наша Open Source-утилита для сопровождения процессов CI/CD — dapp версии 0.25 — обладает достаточным набором функций и была начата работа над нововведениями. В версии 0.26 появился синтаксис YAML, а Ruby DSL был объявлен классическим (далее перестанет поддерживаться вовсе). В следующей версии, 0.27, основным нововведением можно считать появление сборщика с Ansible. Пришло время рассказать об этих новинках подробнее.
Предыстория
Мы разрабатываем dapp более 2 лет и активно применяем в повседневном обслуживании множества проектов различных масштабов. Первые версии утилиты задумывались с целью использовать Chef для сборки образов. Когда мы добавили к этому то обстоятельство, что Ruby был знаком практически всем нашим инженерам и разработчикам, приняли логичное решение реализовать dapp как Ruby gem. Посчитали уместным и сделать конфиг Dappfile в виде Ruby DSL — тем более, что известен успешный пример из близкой области — Vagrant.
По мере развития утилиты пришло понимание, что в dapp нужна вторая специализация — доставка приложений в Kubernetes. Так появился режим работы с Helm charts, а инженеры освоили синтаксис YAML и шаблоны на Go в то время, как разработчики начали отправлять патчи в Helm. С одной стороны, доставка в Kubernetes стала неотъемлемой частью dapp, а с другой — стандартом де-факто в экосистеме Docker и Kubernetes является Go. Наш dapp, будучи написанным на Ruby, теперь выбивается из общей картины: если нам сложно повторно использовать код Docker, то пользователям зачастую просто не хочется ставить Ruby на сборочные машины — ведь куда проще и привычнее скачать бинарник… Как результат, основными целями развития dapp стали: а) перевод кодовой базы на Go, б) реализация синтаксиса YAML.
Кроме того, за прошедшее время Chef перестал нас устраивать по ряду причин как для управления машинами, так и для сборки. Как выяснилось, переход на Ansible решает часть проблем не только наших DevOps-инженеров: самым частым вопросом на конференциях стала поддержка Ansible в dapp. Таким образом, третьей целью стала реализация Ansible-сборщика.
Синтаксис YAML
Ранее знакомство с синтаксисом YAML я уже представлял в этой статье, однако теперь рассмотрю его подробнее.
Конфигурация сборки может быть описана в файле dappfile.yaml
(или dappfile.yml
). Этапы обработки конфигурации — следующие:
- dapp читает
dappfile.y[a]ml
; - запускается Go-шаблонизатор, рендерится итоговый YAML;
- отрендереный конфиг разбивается на YAML-документы (
---
с переводом строки); - проверяется, что каждый YAML-документ содержит на верхнем уровне атрибут dimg или artifact;
- проверяется состав остальных атрибутов;
- если всё в порядке — составляется окончательный конфиг из указанных dimg’ей и artifact’ов.
Классический Dappfile — это Ruby DSL, благодаря чему было возможно некоторое программирование: обращение к словарю ENV
за переменными окружения, определение dimg в циклах, определение общих инструкций сборки с помощью наследования контекста. Чтобы не отбирать такие возможности у разработчиков, было решено добавить в dappfile.yml
поддержку Go-шаблонов — аналогично chart’ам Helm.
Однако мы отказались от наследования контекста через вложенность и через dimg_group’ы, т.к. это вносило больше неразберихи, чем удобства. Поэтому dappfile.yml
— это линейный массив YAML-документов, каждый из которых представляет собой описание dimg или artifact.
Как и раньше, dimg может быть один и он может быть безымянным:
dimg: ~
from: alpine:latest
shell:
beforeInstall:
- apk update
Артефакты обязаны иметь имя, т.к. теперь описывается не экспорт файлов из образа-артефакта, а импорт (аналогично возможности multi-stage из Dockerfile). Потому нужно указывать, из какого артефакта требуется получить файлы:
artifact: application-assets
...
---
dimg: ~
...
import:
- artifact: application-assets
add: /app/public/assets
after: install
- artifact: application-assets
add: /vendor
to: /app/vendor
after: install
Директивы git
, git remote
, shell
перешли из DSL в YAML практически «как есть», но вместо подчеркиваний используется camelCase (как в Kubernetes):
git:
- add: /
to: /app
owner: app
group: app
excludePaths:
- public/assets
- vendor
- .helm
stageDependencies:
install:
- package.json
- Bowerfile
- Gemfile.lock
- app/assets/*
git:
- url: https://github.com/kr/beanstalkd.git
add: /
to: /build
shell:
beforeInstall:
- useradd -d /app -u 7000 -s /bin/bash app
- rm -rf /usr/share/doc/* /usr/share/man/*
- apt-get update
- apt-get -y install apt-transport-https git curl gettext-base locales tzdata
setup:
- locale-gen en_US.UTF-8
Основное описание всех доступных атрибутов доступно в документации.
docker ENV и LABEL
В dappfile.yml
переменные окружения и метки можно добавить так:
docker:
ENV:
<key>: <value>
...
LABELS:
<key>: <value>
...
В YAML не получится повторять ENV
или LABELS
, как это было в Dappfile и в Dockerfile.
Шаблонизатор
Шаблоны можно использовать для определения общей конфигурации сборки для разных dimg или artifact'ов. Это может быть, например, простое указание общего базового образа с помощью переменной:
{{ $base_image := "alpine:3.6" }}
dimg: app
from: {{ $base_image }}
...
---
dimg: worker
from: {{ $base_image }}
… или нечто более сложное с применением определяемых шаблонов:
{{ $base_image := "alpine:3.6" }}
{{- define "base beforeInstall" }}
- apt: name=php update_cache=yes
- get_url:
url: https://getcomposer.org/download/1.5.6/composer.phar
dest: /usr/local/bin/composer
mode: 0755
{{- end}}
dimg: app
from: {{ $base_image }}
ansible:
beforeInstall:
{{- include "base beforeInstall" .}}
- user:
name: app
uid: 48
...
---
dimg: worker
from: {{ $base_image }}
ansible:
beforeInstall:
{{- include "base beforeInstall" .}}
...
В этом примере часть инструкций для стадии beforeInstall
определены как общая часть и далее подключаются в каждом dimg.
Подробнее о возможностях Go-шаблонов можно почитать в документации на модуль text/template и в документации на модуль sprig, функции из которого дополняют стандартные возможности.
Поддержка Ansible
Ansible-сборщик состоит из трёх частей:
- Образ dappdeps/ansible, в котором лежит Python 2.7, собранный со своей glibc и остальными библиотеками, чтобы работать в любом дистрибутиве (особенно актуально для Alpine). Тут же установлен Ansible.
- Поддержка синтаксиса описания сборки стадий с помощью Ansible в
dappfile.yaml
. - Builder в dapp, запускающий контейнеры для стадий. В этих контейнерах выполняются таски, указанные в
dappfile.yml
. Builder создаёт playbook и генерирует команду для его запуска.
Ansible разрабатывается как система управления большим количеством удалённых хостов и поэтому вещи, которые актуальны для локального запуска, могут игнорироваться разработчиками. Например, нет вывода в реальном времени от запускаемых команд, как это было в Chef: сборка может включать длительную команду, вывод которой было бы хорошо видеть в реальном времени, но Ansible покажет вывод только после завершения. При запуске через GitLab CI это может быть расценено как подвисание билда.
Второй неприятностью стали stdout callbacks, которые входят в состав Ansible. Среди них не оказалось «умеренно информативного». Тут либо слишком многословный вывод с полным результатом в виде JSON, либо минимализм с названием хоста, именем модуля и статусом. Конечно, я утрирую, но подходящего модуля для сборки образов действительно нет.
Третье, с чем мы столкнулись, — зависимость некоторых модулей Ansible от внешних утилит (не страшно), модулей Python (ещё менее страшно) и от бинарных модулей Python (кошмар!). Опять же, авторы Ansible не учитывали, что их творение будут запускать отдельно от системных бинарников и что, например, userdel
будет находиться не в /sbin
, а где-то в другой директории…
Проблема с бинарными модулями — это особенность модуля apt. В нём используется модуль python-apt в виде SO-библиотеки. Другой особенностью модуля apt оказалось, что при выполнении таска, в случае неудачной загрузки python-apt, происходит попытка установить пакет с этим модулем в систему.
Чтобы решить вышеперечисленные проблемы, был реализован «живой» вывод для тасков raw и script, т.к. они могут запускаться без механизма Ansiballz. Также пришлось реализовать свой stdout callback, добавить в dappdeps/ansible сборку useradd
, userdel
, usermod
, getent
и подобных утилит и скопировать модули python-apt.
В итоге, сборщик Ansible в dapp работает с Linux-дистрибутивами Ubuntu, Debian, CentOS, Alpine, но не все модули ещё протестированы и потому в dapp есть список модулей, которые точно поддерживаются. Если в конфигурации использовать модуль не из списка, то сборка не запустится — это временная мера. Список поддерживаемых модулей можно увидеть здесь.
Конфигурация сборки с помощью Ansible в dappfile.yml
похожа на конфигурацию shell
. В ключе ansible
перечисляются нужные стадии и для каждой из них определяется массив тасков — практически как в обычном playbook, только вместо атрибута tasks
указывается имя стадии:
ansible:
beforeInstall:
- name: "Create non-root main application user"
user:
name: app
comment: "Non-root main application user"
uid: 7000
shell: /bin/bash
home: /app
- name: "Disable docs and man files installation in dpkg"
copy:
content: |
path-exclude=/usr/share/man/*
path-exclude=/usr/share/doc/*
dest: /etc/dpkg/dpkg.cfg.d/01_nodoc
install:
- name: "Precompile assets"
shell: |
set -e
export RAILS_ENV=production
source /etc/profile.d/rvm.sh
cd /app
bundle exec rake assets:precompile
args:
executable: /bin/bash
Пример взят из документации.
Теперь возникает вопрос: если в dappfile.yml
есть только список тасков, то где всё остальное (верхний уровень playbook, inventory), как включить become
и где говорящие коровы (или как их отключить)? Пора описать способ запуска Ansible.
За запуск отвечает билдер — это не очень сложный кусок кода, который определяет параметры запуска Docker-контейнера со стадией: переменные среды, команду запуска ansible-playbook, нужные монтирования. Также билдер создаёт во временной директории приложения каталог, где генерируется несколько файлов:
-
hosts
— inventory для Ansible. Здесь только один хост localhost с указанием пути к Python внутри монтируемого образа dappdeps/ansible; -
ansible.cfg
— конфигурация Ansible. В конфиге указан тип подключенияlocal
, путь к inventory, путь к callback stdout, пути к временным директориям и настройкиbecome
: все таски запускаются от пользователя root; если использоватьbecome_user
, то процессу пользователя будут доступны все переменные среды и будет правильно установлена$HOME
(sudo -E -H
); -
playbook.yml
— этот файл генерируется из списка тасков для выполняемой стадии. В файле указывается фильтрhosts: all
и отключается неявный сбор фактов настройкойgather_facts: no
. Модули setup и set_fact — в списке поддерживаемых, поэтому можно использовать их для явного сбора фактов.
Список тасков для стадии beforeInstall
из примера ранее превращается в такой playbook.yml
:
---
hosts: all
gather_facts: no
tasks:
- name: "Create non-root main application user"
user:
name: app
...
- name: "Disable docs and man files installation in dpkg"
copy:
content: |
path-exclude=/usr/share/man/*
path-exclude=/usr/share/doc/*
dest: /etc/dpkg/dpkg.cfg.d/01_nodoc
Особенности применения Ansible для сборки
Become
Настройки become
в ansible.cfg
такие:
[become]
become = yes
become_method = sudo
become_flags = -E -H
become_exe = path_to_sudo_insdie_dappdeps/ansible_image
Поэтому в тасках достаточно указать только become_user: username
, чтобы запустить скрипт или копирование от пользователя.
Модули command
В Ansible есть 4 модуля для запуска команд и скриптов: raw
, script
, shell
и command
. raw
и script
выполняются без механизма Ansiballz, что немного быстрее, и для них есть live-вывод. С помощью raw
можно выполнять многострочные скрипты ad-hoc:
- raw: |
mvn -B -f pom.xml -s /usr/share/maven/ref/settings-docker.xml dependency:resolve
mvn -B -s /usr/share/maven/ref/settings-docker.xml package -DskipTests
Правда, не поддерживается атрибут environment
, но это можно обойти так:
- raw: |
mvn -B -f pom.xml -s $SETTINGS dependency:resolve
mvn -B -s $SETTINGS package -DskipTests
args:
executable: SETTINGS=/usr/share/maven/ref/settings-docker.xml /bin/ash -e
Файлы
На данном этапе нет механизма проброса файлов из репозитория в контейнеры, кроме директивы git
. Для добавления в образ различного рода конфигов, скриптов и других небольших файлов можно воспользоваться модулем copy:
- name: "Disable docs and man files installation in dpkg"
copy:
content: |
path-exclude=/usr/share/man/*
path-exclude=/usr/share/doc/*
dest: /etc/dpkg/dpkg.cfg.d/01_nodoc
Если файл большой, то, чтобы не хранить его внутри dappfile.yml
, можно воспользоваться Go-шаблоном и функцией .Files.Get
:
- name: "Disable docs and man files installation in dpkg"
copy:
content: |
{{.Files.Get ".dappfiles/01_nodoc" | indent 6}}
dest: /etc/dpkg/dpkg.cfg.d/01_nodoc
В дальнейшем будет реализован механизм подключения файлов в сборочный контейнер, чтобы было проще копировать большие и бинарные файлы, а также использовать include*
или import*
.
Шаблонизация
Про Go-шаблоны в dappfile.yaml
уже было сказано. Ansible со своей стороны поддерживает шаблоны jinja2, а разделители этих двух систем совпадают, поэтому вызов jinja нужно экранировать от Go-шаблонизатора:
- name: "create temp file for archive"
tempfile:
state: directory
register: tmpdir
- name: Download archive
get_url:
url: https://cdn.example.com/files/archive.tgz
dest: '{{`{{ tmpdir.path }}`}}/archive.tgz'
Отладка проблем со сборкой
При выполнении таска может случиться какая-то ошибка, но сообщений на экране иногда не хватает для понимания. В этом случае можно начать с указания переменной окружения ANSIBLE_ARGS="-vvv"
— тогда в выводе будут все аргументы для тасков и все аргументы результатов (похоже на использование json stdout callback).
Если ситуация не проясняется, можно запустить сборку в режиме introspect: dapp dimg bulid --introspect-error
. Тогда сборка остановится после ошибки и в контейнере будет запущен shell. Будет видна команда, вызвавшая ошибку, а в соседнем терминале можно зайти во временную директорию и править playbook.yml
:
Переход на Go
Это наша третья цель в развитии dapp, однако с точки зрения пользователя мало что меняет, кроме упрощения установки. Для релиза 0.26 на Go был реализован парсер dappfile.yaml
. Сейчас продолжается работа по переводу на Go основной функциональности dapp: запуск сборочных контейнеров, билдеры, работа с Git. Поэтому будет не лишней ваша помощь в тестировании — в том числе, модулей Ansible. Ждём issue на GitHub или заходите в нашу группу в Telegram: dapp_ru.
P.S.
Так что там с коровами-то? Программы cowsay нет в dappdeps/ansible, а используемый callback stdout не вызывает те методы, где включается cowsay. К сожалению, Ansible в dapp без коров (но вас никто не остановит от создания issue).
P.P.S.
Читайте также в нашем блоге:
- «Официально представляем dapp — DevOps-утилиту для сопровождения CI/CD»;
- «Сборка проектов с dapp. Часть 1: Java»;
- «Сборка и дeплой приложений в Kubernetes с помощью dapp и GitLab CI»;
- «Практика с dapp. Часть 1: Сборка простых приложений»;
- «Практика с dapp. Часть 2. Деплой Docker-образов в Kubernetes с помощью Helm»;
- «Собираем Docker-образы для CI/CD быстро и удобно вместе с dapp (обзор и видео доклада)».
Автор: diafour