Тестирование и непрерывная интеграция для Ansible-ролей при помощи Molecule и Jenkins

phw-bjychth3ak3cgzeqz6r4izy.jpeg

После того, как Ansible вошёл в нашу практику, количество кода на нём и, в частности, ролей стало расти очень быстро. Роли для бэка, фронта, прокси, баз данных, мониторинга, сбора логов и т. д. и т. п.—их количество насчитывает десятки. Часть из ролей специфична для определённого проекта, но многие решают общие задачи, ими хочется делиться между проектными командами, чтобы не создавать одно и то же решение дважды.

Вместе с большим количеством кода появляется и старая, знакомая проблема: страх изменений. Люди не желают вносить изменения в «чужую» роль, опасаясь испортить её, вместо этого создают собственную копию. Рефакторинг кода не производится, если код прямо сейчас не находится в фокусе разработки, из-за опасения, что внесённые проблемы могут быть обнаружены спустя слишком большое время. Итог: плохой код растёт как снежный ком.

У знакомой проблемы есть и знакомое решение: автоматическое тестирование и CI. Но Ansible — слишком специфичная технология. Можно ли для проверки Ansible-кода делать что-нибудь, кроме ручной накатки роли на виртуальную машину и проверки, что ничего не «отвалилось»? Я полагал, что нельзя, пока не узнал о существовании проекта Molecule.

При использовании Molecule все проблемы с «хрупкостью» Ansible-кода полностью устраняются. Если вы — приверженец test-driven development, то при помощи Molecule можно вести разработку Ansible в цикле «написал тест — test failed — написал код — test passed». Если нет, то даже просто «прикрутив Molecule» к существующему коду и не написав ни единого теста, вы уже получаете систему проверки вашей роли на соответствие стандартам кодирования, на срабатывание и идемпотентность.

Первый вопрос, который возник у меня, когда я узнал про Molecule — куда он разворачивает роль в процессе тестирования? Ответ — возможны варианты. Самый простой — если на машине с Molecule должен доступен Docker, роли разворачиваются в Docker-контейнеры, которые по прогону тестов выбрасываются. Но вы можете использовать в Molecule и другие драйверы, такие как Vagrant, Azure, EC2, GCE и т. п.

Теперь — обо всём по порядку.

Установка

Как и Ansible, Molecule написан на питоне и ставится командой

pip install molecule


Есть нюансы: по состоянию на март 2018 мне требовалось вручную с помощью pip обновлять пакеты cryptography и pyopenssl, иначе возникали проблемы с запуском и работоспособностью Molecule. Если вы хотите использовать Docker-драйвер (что рекомендую), то также потребуется установить Docker и pip-пакет docker-py.

В качестве машины для разработки я использую Windows и Cygwin, на котором у меня отлично работает Ansible. К сожалению, подружить Cygwin, Windows Docker и Molecule мне не удалось, поэтому работаю с Molecule только в Linux.

Инициализация проекта


Если ваша роль называется oldrole, то для добавления в неё Molecule нужно выполнить команду

molecule init scenario -r oldrole


Для создания совершенно новой роли надо выполнить

molecule init role -r newrole


Эти команды создают папки и файлы, всё самое интересное — в папке molecule. На самом деле, я, кажется, воспользовался этой командой всего лишь один раз. Затем я копировал структуру файлов на другие роли вручную, чтобы переносить свои индивидуальные настройки и тесты.

Поехали?


После инициализации выполните в корне проекта команду

molecule test


С первого раза не «взлетит», конечно, но вы сразу увидите то, что Molecule пытается сделать с вашей ролью, и где возникают проблемы. Вывод в консоль будет содержать планируемый cценарий выполнения:

--> Test matrix
└── default
    ├── lint
    ├── destroy
    ├── dependency
    ├── syntax
    ├── create
    ├── prepare
    ├── converge
    ├── idempotence
    ├── side_effect
    ├── verify
    └── destroy


Как видим, Molecule предлагает готовый шаблонный сценарий для всесторонней проверки вашей роли, начиная от статического анализа кода и заканчивая выполнением инфраструктурных тестов по результатам её выполнения. Разумеется, при помощи настроек можно изменять этот сценарий или выполнять только отдельные шаги.

Если результат выполнения «свалился», а имеющейся в консоли информации недостаточно, попробуйте выполнить

molecule --debug test


 — это сделает вывод гораздо более подробным.

Статический анализ


«Ай-яй-яй, вы забыли поставить три минуса в начале YAML-файла! А вот тут у вас пробелы в пустой строке!» Таких сообщений на первом шаге будет больше всего. К счастью, многие из них являются warning-ами и не приводят к остановке всей проверки. Но есть и более существенные подсказки. Например, на данном этапе система может указать на то, что вы неправильно используете модуль shell, когда можно было бы обойтись модулем command, или используете команду wget, когда можно было бы воспользоваться специальным модулем get_url. Кроме того, если у вас есть тесты (а их надо писать на питоне), система прогоняет статический анализатор flake8 над python-кодом.

Запуск роли


Если на этапе статического анализа не нашлось критических замечаний, система создаёт ноду и переходит к запуску роли при помощи самого Ansible. То, как она это будет делать, определяется в файле molecule/default/molecule.yml. В моём случае он выглядит так:

---
dependency:
  name: galaxy
  options:
    role-file: requirements.yml
driver:
  name: docker
lint:
  name: yamllint
platforms:
  - name: instance
    image: solita/ubuntu-systemd:latest
    command: /sbin/init
    privileged: True
    volumes:
      - "/sys/fs/cgroup:/sys/fs/cgroup:rw"
provisioner:
  name: ansible
  lint:
    name: ansible-lint
scenario:
  name: default
verifier:
  name: testinfra
  lint:
    name: flake8


Если тестируемая роль не может быть установлена без того, чтобы перед этим не были установлены другие роли, то систему molecule надо попросить скачать требуемые зависимости. Это делается через настройку dependency и файл requirements.yml (подробности про этот файл есть в справке по Galaxy).

В разделе driver вы выбираете драйвер, с которым будете работать (возможные варианты описаны здесь).

В разделе platforms вы выбираете платформы, на которые будет накатываться ваша роль. В случае с докером вы указываете базовые докер-контейнеры, и тут я прошу обратить особое внимание на пример выше. Если ваша роль создаёт сервисы на базе systemd, то чтобы иметь возможность запускать их в докер-контейнере, вам потребуется использовать специальный образ solita/ubuntu-systemd и специально настраивать его, как указано в примере. Если вы хотите протестировать вашу роль на разных платформах (например, на разных дистрибутивах Linux), то вы можете указать здесь несколько значений.

Про назначение других разделов конфигурационного файла сказано в документации (например, здесь вы можете переопределить матрицу выполнения и сконфигурировать статические анализаторы).

Следующий файл, который вам наверняка потребуется править — playbook.yml. Он представляет из себя обычный Ansible playbook, который выполняется для накатки роли на ноду, и для одной из наших ролей выглядит так:

---
- name: Converge
  hosts: all
  roles:
    - role: ansiblebit.oracle-java
    - role: fluteansible
  tasks:
    - name: mkdir for score
      file:
        dest: /var/opt/flute/score
        state: directory
        mode: "0775"
        group: flute

    - name: copy flute config file
      copy:
        src: flute.xml
        dest: opt/flute/flute.xml
        mode: "0644"

    - name: start flute service
      service:
        name: flute
        state: started

Здесь вы прописываете

  1. Всё, что должно предшествовать установке вашей роли (в нашем случае — роль ansiblebit.oracle-java, которая также указана в requirements.yml и поэтому будет автоматически установлена).
  2. Установку собственно тестируемой роли (в нашем случае — fluteansible).
  3. Любые дополнительные шаги, нужные для дальнейших проверок (в нашем случае мы копируем конфигурационные файлы и запускаем сервис, чтобы инфраструктурные тесты смогли проверить, что сервис запущен и корректно выполняется).

Шаги «converge» и «idempotence»


На шаге converge molecule просто выполняет playbook.yml на чистой тестовой ноде.

Затем, на шаге idempotence, molecule выполняет «dry run» той же роли с опцией --diff (подробнее об этом см. документацию по Ansible), чтобы убедиться, что при повторном запуске та же самая роль не будет пытаться выполнить каких-нибудь изменений— ведь система уже находится в желаемом состоянии.

Как правило, идемпотентность Ansible-кода обеспечивается автоматически, но в некоторых случаях (прежде всего, для shell-команд) нужно самостоятельно указать условия, при которых команду не следует выполнять повторно.

Шаг «verify»


Здесь происходит самое интересное. Molecule по умолчанию использует систему testinfra для того, чтобы после накатки роли выполнить проверки на актуальное состояние тестовой ноды. Тесты пишутся на Питоне и находятся в файле molecule/default/tests/test_default.py. Названия тестовых процедур должны начинаться с »test_».

Практически всё, что вы желаете проверить относительно состояния системы, вы можете легко сделать в пару строчек кода на testinfra. К примеру, если вы хотите убедиться, что некоторая команда может быть запущена, проверить результат её выполнения и вывод в консоль, вы можете сделать это так:

def test_jython_installed(host):
    cmd = host.run('jython --version')
    assert cmd.rc == 0
    assert cmd.stderr.find(u'Jython 2') > -1

Проверка того, что сервис запущен, выглядит следующим образом:

def test_service_is_running(host):
    assert host.service('flute').is_running

Наличие файлов и их содержимое может быть проверено так:

def test_log_files(host):
    err = host.file('/var/log/flute/std.err')
    assert err.exists
    assert err.contains('Flute started')

Возможности testinfra этим далеко не исчерпываются, здесь находится подробная документация по встроенным возможностям.

CI/CD pipeline


Коль скоро у нас появились статические проверки и автотесты, то естественным следующим шагом будет построить конвейер CI/CD.

Релиз для Ansible-роли — это попадание в Ansible Galaxy, который считает релизом самый последний коммит в мастер-ветку вашего Github-репозитория. Таким образом, если использовать GitHub для контроля версий с защищённой master-веткой, то каждое слияние в мастер-ветку и будет релизом. Если необходимыми условиями для слияния поставить code review и проверку на CI-сервере (в которой выполняется Molecule-тест) — мы выстраиваем устойчивый конвейер поставки.

Мы используем Jenkins Multibranch Pipeline, и наш Jenkinsfile состоит всего из двух шагов:

node {
   stage ("Get Latest Code") {
      checkout scm
   }
   
   try {
     stage ("Molecule test") {
        /* Jenkins check out the role into 
        a folder with arbitrary name, so we need to
        let Ansible know where to find 'fluteansible' role*/
        sh 'mkdir -p molecule/default/roles'
        sh 'ln -s `pwd` molecule/default/roles/fluteansible'
        sh 'molecule test'
     }
   } catch(all) {
      currentBuild.result = "FAILURE"
      throw err
   }
}

Собственно, здесь было бы не о чем говорить, если бы не одна особенность. Jenkins Multibranch скачивает проект в папку с названием, отличающимся от названия проекта (она может называться как-нибудь так: fluteansible_PR-3-7YQKOAVNLO7P2S6Z4O6BLBAFYNYCTBDGTLQFWMXA35XGZZZMLQRA), поэтому для того, чтобы Molecule мог найти вашу роль по её «нормальному» имени, ему надо сделать «подсказку» в виде симлинка на корень проекта в папке molecule/default/roles.

В этой статье вы можете найти другой пример Jenkinsfile для Molecule, в котором автор не поленился и разбил каждый из шагов сценария molecule на stage, а при успешном выполнении прописывает тэги в Git-репозитории.

Заключение

Ansible — это сама по себе превосходная система. Но с наличием Molecule с ней можно начинать творить чудеса, создавая большие, сложно конфигурируемые роли, не ощущая при этом страха испортить код. Простота использования Molecule вместе с его возможностями делает Molecule «must have» инструментом при разработке Ansible-скриптов.

Пример роли, разрабатываемой с помощью Molecule и Jenkins можно посмотреть тут.

© Habrahabr.ru