Дождались: поддержка YAML и Ansible (без коров) в dapp


    В начале этого года мы посчитали, что наша 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). Этапы обработки конфигурации — следующие:

    1. dapp читает dappfile.y[a]ml;
    2. запускается Go-шаблонизатор, рендерится итоговый YAML;
    3. отрендереный конфиг разбивается на YAML-документы (--- с переводом строки);
    4. проверяется, что каждый YAML-документ содержит на верхнем уровне атрибут dimg или artifact;
    5. проверяется состав остальных атрибутов;
    6. если всё в порядке — составляется окончательный конфиг из указанных 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/*
    - 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-сборщик состоит из трёх частей:

    1. Образ dappdeps/ansible, в котором лежит Python 2.7, собранный со своей glibc и остальными библиотеками, чтобы работать в любом дистрибутиве (особенно актуально для Alpine). Тут же установлен Ansible.
    2. Поддержка синтаксиса описания сборки стадий с помощью Ansible в dappfile.yaml.
    3. 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.


    Читайте также в нашем блоге:

    • +21
    • 5,1k
    • 4

    Флант

    286,70

    Специалисты по DevOps и высоким нагрузкам в вебе

    Поделиться публикацией
    Комментарии 4
      +1

      А какие аналогичные dapp тулзы есть ещё и в чем отличия/плюсы dapp?

        0
        Спасибо за отличный вопрос — он достоин отдельной статьи, постараемся в ближайшее время.
        P.S. А сегодня как раз опубликовали про совсем свежий Jenkins X, реакция на который от одного из наших разработчиков звучит так: «Эта штука не ускорит инкрементальные билды приложения для последовательных git-коммитов — одна из главных фишек dapp».
          +1

          Да уж, вопрос объёмный. Много интересного можно найти вот здесь: https://github.com/veggiemonk/awesome-docker. К этому можно добавить CI: собирать и выкатывать можно используя возможности Travis, shippable, gitlab, упомянутый jenkins и т.д.


          До перехода на ansible мы знали про rocker — это довольно известная утилита, но в этом году у проекта какой-то кризис (https://github.com/grammarly/rocker/issues/199)


          После релиза 0.27 узнали в группе pro_ansible про ansible container —
          этот проект больше всего похож на dapp. Тут и сборка, и выкат в Kubernetes, и даже монтируемый образ с python и ansible, как наш образ dappdeps/ansible (у нас правда один образ работает со всеми дистрибутивами). Пока смотрим, что можно бы у них почерпнуть и чем посодействовать.
          Кэш отличается: у нас кэш можно привязать к изменениям в git, а в ansible-container можно привязать к ansible ролям.

          0

          .

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое