- PVSM.RU - https://www.pvsm.ru -
В начале этого года мы посчитали, что наша Open Source-утилита для сопровождения процессов CI/CD — dapp версии 0.25 — обладает достаточным набором функций и была начата работа над нововведениями. В версии 0.26 появился синтаксис YAML, а Ruby DSL был объявлен классическим (далее перестанет поддерживаться вовсе). В следующей версии, 0.27, основным нововведением можно считать появление сборщика с Ansible. Пришло время рассказать об этих новинках подробнее.
Мы разрабатываем dapp [1] более 2 лет и активно применяем в повседневном обслуживании множества проектов различных масштабов. Первые версии утилиты задумывались с целью использовать Chef для сборки образов. Когда мы добавили к этому то обстоятельство, что Ruby был знаком практически всем нашим инженерам и разработчикам, приняли логичное решение реализовать dapp как Ruby gem. Посчитали уместным и сделать конфиг Dappfile в виде Ruby DSL — тем более, что известен успешный пример из близкой области — Vagrant.
По мере развития утилиты пришло понимание, что в dapp нужна вторая специализация — доставка приложений в Kubernetes. Так появился режим работы с Helm charts [2], а инженеры освоили синтаксис YAML и шаблоны на Go в то время, как разработчики начали отправлять патчи в Helm. С одной стороны, доставка в Kubernetes стала неотъемлемой частью dapp, а с другой — стандартом де-факто в экосистеме Docker и Kubernetes является Go. Наш dapp, будучи написанным на Ruby, теперь выбивается из общей картины: если нам сложно повторно использовать код Docker, то пользователям зачастую просто не хочется ставить Ruby на сборочные машины — ведь куда проще и привычнее скачать бинарник… Как результат, основными целями развития dapp стали: а) перевод кодовой базы на Go, б) реализация синтаксиса YAML.
Кроме того, за прошедшее время Chef перестал нас устраивать по ряду причин как для управления машинами, так и для сборки. Как выяснилось, переход на Ansible решает часть проблем не только наших DevOps-инженеров: самым частым вопросом на конференциях стала поддержка Ansible в dapp. Таким образом, третьей целью стала реализация Ansible-сборщика.
Ранее знакомство с синтаксисом YAML я уже представлял в этой статье [3], однако теперь рассмотрю его подробнее.
Конфигурация сборки может быть описана в файле dappfile.yaml
(или dappfile.yml
). Этапы обработки конфигурации — следующие:
dappfile.y[a]ml
;---
с переводом строки);
Классический 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
Основное описание всех доступных атрибутов доступно в документации [4].
В 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-шаблонов можно почитать в документации [5] на модуль text/template и в документации [6] на модуль sprig, функции из которого дополняют стандартные возможности.
Ansible-сборщик состоит из трёх частей:
dappfile.yaml
.dappfile.yml
. Builder создаёт playbook и генерирует команду для его запуска.Ansible разрабатывается как система управления большим количеством удалённых хостов и поэтому вещи, которые актуальны для локального запуска, могут игнорироваться разработчиками. Например, нет вывода в реальном времени от запускаемых команд, как это было в Chef: сборка может включать длительную команду, вывод которой было бы хорошо видеть в реальном времени, но Ansible покажет вывод только после завершения. При запуске через GitLab CI это может быть расценено как подвисание билда.
Второй неприятностью стали stdout callbacks [7], которые входят в состав Ansible. Среди них не оказалось «умеренно информативного». Тут либо слишком многословный вывод с полным результатом в виде JSON, либо минимализм с названием хоста, именем модуля и статусом. Конечно, я утрирую, но подходящего модуля для сборки образов действительно нет.
Третье, с чем мы столкнулись, — зависимость некоторых модулей Ansible от внешних утилит (не страшно), модулей Python (ещё менее страшно) и от бинарных модулей Python (кошмар!). Опять же, авторы Ansible не учитывали, что их творение будут запускать отдельно от системных бинарников и что, например, userdel
будет находиться не в /sbin
, а где-то в другой директории…
Проблема с бинарными модулями — это особенность модуля apt. В нём используется модуль python-apt в виде SO-библиотеки. Другой особенностью модуля apt оказалось, что при выполнении таска, в случае неудачной загрузки python-apt, происходит попытка установить пакет с этим модулем в систему.
Чтобы решить вышеперечисленные проблемы, был реализован [8] «живой» вывод для тасков raw и script, т.к. они могут запускаться без механизма Ansiballz. Также пришлось реализовать свой stdout callback, добавить в dappdeps/ansible сборку useradd
, userdel
, usermod
, getent
и подобных утилит и скопировать модули python-apt.
В итоге, сборщик Ansible в dapp работает с Linux-дистрибутивами Ubuntu, Debian, CentOS, Alpine, но не все модули [9] ещё протестированы и потому в dapp есть список модулей, которые точно поддерживаются. Если в конфигурации использовать модуль не из списка, то сборка не запустится — это временная мера. Список поддерживаемых модулей можно увидеть здесь [10].
Конфигурация сборки с помощью 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
Пример взят из документации [11].
Теперь возникает вопрос: если в dappfile.yml
есть только список тасков, то где всё остальное (верхний уровень playbook, inventory), как включить become
и где говорящие коровы (или как их отключить)? Пора описать способ запуска Ansible.
За запуск отвечает билдер — это не очень сложный кусок кода, который определяет параметры запуска Docker-контейнера со стадией: переменные среды, команду запуска ansible-playbook, нужные монтирования. Также билдер создаёт во временной директории приложения [12] каталог, где генерируется несколько файлов:
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
Настройки 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
, чтобы запустить скрипт или копирование от пользователя.
В 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
:
Это наша третья цель в развитии dapp, однако с точки зрения пользователя мало что меняет, кроме упрощения установки. Для релиза 0.26 на Go был реализован парсер dappfile.yaml
. Сейчас продолжается работа по переводу на Go основной функциональности dapp: запуск сборочных контейнеров, билдеры, работа с Git. Поэтому будет не лишней ваша помощь в тестировании — в том числе, модулей Ansible. Ждём issue на GitHub [1] или заходите в нашу группу в Telegram: dapp_ru [13].
Так что там с коровами-то? Программы cowsay нет в dappdeps/ansible, а используемый callback stdout не вызывает те методы, где включается cowsay. К сожалению, Ansible в dapp без коров (но вас никто не остановит от создания issue).
Читайте также в нашем блоге:
Автор: diafour
Источник [18]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/sistemnoe-administrirovanie/276016
Ссылки в тексте:
[1] dapp: https://github.com/flant/dapp
[2] режим работы с Helm charts: https://habrahabr.ru/company/flant/blog/336170/
[3] этой статье: https://habrahabr.ru/company/flant/blog/348436/
[4] документации: https://github.com/flant/dapp/blob/master/docs/yaml.md
[5] документации: https://golang.org/pkg/text/template/
[6] документации: http://masterminds.github.io/sprig/
[7] stdout callbacks: https://docs.ansible.com/ansible/devel/plugins/callback.html
[8] был реализован: https://github.com/ansible/ansible/compare/stable-2.4...flant:stable-2.4+dapp
[9] модули: http://docs.ansible.com/ansible/latest/modules_by_category.html
[10] здесь: https://github.com/flant/dapp/blob/master/pkg/config/raw_ansible_task.go#L60-L123
[11] документации: https://github.com/flant/dapp/blob/master/docs/yaml.md#ansible
[12] временной директории приложения: http://flant.github.io/dapp/definitions.html#%D0%B2%D1%80%D0%B5%D0%BC%D0%B5%D0%BD%D0%BD%D0%B0%D1%8F-%D0%B4%D0%B8%D1%80%D0%B5%D0%BA%D1%82%D0%BE%D1%80%D0%B8%D1%8F-%D0%BF%D1%80%D0%B8%D0%BB%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D1%8F
[13] dapp_ru: https://t.me/dapp_ru
[14] Официально представляем dapp — DevOps-утилиту для сопровождения CI/CD: https://habrahabr.ru/company/flant/blog/333682/
[15] Сборка и дeплой приложений в Kubernetes с помощью dapp и GitLab CI: https://habrahabr.ru/company/flant/blog/345580/
[16] Практика с dapp. Часть 1: Сборка простых приложений: https://habrahabr.ru/company/flant/blog/336212/
[17] Собираем Docker-образы для CI/CD быстро и удобно вместе с dapp (обзор и видео доклада): https://habrahabr.ru/company/flant/blog/324274/
[18] Источник: https://habrahabr.ru/post/351838/?utm_campaign=351838
Нажмите здесь для печати.