Всем привет!


Я работаю DevOps-инженером в сервисе бронирования отелей Ostrovok.ru. В этой статье я хочу рассказать о нашем опыте тестирования ansible-ролей.


В Ostrovok.ru в качестве менеджера конфигураций мы используем ansible. Недавно мы пришли к необходимости тестирования ролей, но, как оказалось, инструментов для этого существует не так много — самым популярным, пожалуй, является фреймворк Molecule, поэтому мы решили использовать его. Но оказалось, что его документация умалчивает о многих подводных камнях. Достаточно подробного руководства на русском нам не удалось найти, поэтому мы решили написать эту статью.



Molecule


Молекула — фреймворк для помощи в тестировании ansible-ролей.


Упрощенное описание: Молекула создаёт инстанс на указанной вами платформе (облако, виртуалка, контейнер; подробнее см. раздел Driver), прогоняет на нём вашу роль, затем запускает тесты и удаляет инстанс. В случае возникновения неудачи на одном из шагов, Молекула сообщит вам об этом.


Теперь подробнее.


Немного теории


Рассмотрим две ключевые сущности Молекулы: Scenario и Driver.


Scenario


Сценарий содержит в себе описание того, что, где, как и в какой последовательности будет выполнено. У одной роли может быть несколько сценариев, и каждый — это директория по пути <role>/molecule/<scenario>, содержащая в себе описания необходимых для теста действий. Обязательно должен присутствовать сценарий default, который будет автоматически создан, если вы инициализируете роль с помощью Молекулы. Имена следующих сценариев выбираются на ваше усмотрение.


Последовательность действий тестирования в сценарии называется matrix, и по умолчанию она такова:


(Шаги, помеченные ?, по умолчанию пропускаются, если не описаны пользователем)


  • lint — прогон линтеров. По умолчанию используются yamllint и flake8,
  • destroy — удаление инстансов с прошлого запуска Молекулы (если остались),
  • dependency? — установка ansible-зависимости тестируемой роли,
  • syntax — проверка синтаксиса роли с помощью ansible-playbook --syntax-check,
  • create — создание инстанса,
  • prepare? — подготовка инстанса; например, проверка / установка python2
  • converge — запуск тестируемого плейбука,
  • idempotence — повторный запуск плейбука для теста на идемпотентность,
  • side_effect? — действия, не относящиеся непосредственно к роли, но нужные для тестов,
  • verify — запуск тестов получившейся конфигурации с помощью testinfra(по умолчанию) /goss/inspec,
  • cleanup? — (в новых версиях) — грубо говоря, "очистка" внешней инфраструктуры, задетой Молекулой,
  • destroy — удаление инстанса.

Эта последовательность покрывает большинство случаев, но, при необходимости, её можно изменить.


Каждый из вышеперечисленных шагов можно запускать отдельно с помощью molecule <command>. Но стоит понимать, что для каждой такой cli-команды может существовать своя последовательность действий, узнать которую можно, выполнив molecule matrix <command>. Например, при запуске команды converge (прогон тестируемого плейбука) будут выполнены следующие действия:


$ molecule matrix converge
...
L-- default         # название сценария
    +-- dependency  # установка зависимостей
    +-- create      # создание инстанса
    +-- prepare     # преднастройка инстанса
    L-- converge    # прогон плейбука

Последовательность этих действий можно редактировать. Если что-то из списка уже выполнено, то оно будет пропущено. Текущее состояние, а также конфиг инстансов, Молекула хранит в директории $TMPDIR/molecule/<role>/<scenario>.


Добавить шаги с ? можно, описав желаемые действия в формате ansible-плейбука, а имя файла сделать соответственно шагу: prepare.yml/side_effect.yml. Ожидать эти файлы Молекула будет в папке сценария.


Driver


Драйвер – это сущность, где создаются инстансы для тестов.
Список стандартных драйверов, для которых у Молекулы готовы шаблоны, таков: Azure, Docker, EC2, GCE, LXC, LXD, OpenStack, Vagrant, Delegated.


В большинстве случаев шаблоны – это файлы create.yml и destroy.yml в папке сценария, которые описывают создание и удаление инстанса соответственно.
Исключения составляют Docker и Vagrant, так как взаимодействия с их модулями может происходить без вышеупомянутых файлов.


Стоит выделить драйвер Delegated, так как в случае его использования в файлах создания и удаления инстанса описана только работа с конфигурацией инстансов, остальное должен описать инженер.


Драйвером по умолчанию является Docker.


Теперь перейдём к практике и дальнейшие особенности рассмотрим там.


Начало работы


В качестве "hello world" протестируем простую роль установки nginx. В качестве драйвера выберем докер – думаю, он установлен у большинства из вас (и помним, что докер — драйвер по умолчанию).


Подготовим virtualenv и установим в него molecule:


> pip install virtualenv
> virtualenv -p `which python2` venv
> source venv/bin/activate
> pip install molecule docker  # molecule установит ansible как зависимость; docker для драйвера

Следующим шагом инициализируем новую роль.
Инициализация новой роли, как и нового сценария, производятся с помощью команды molecule init <params>:


> molecule init role -r nginx
--> Initializing new role nginx...
Initialized role in <path>/nginx successfully.
> cd nginx
> tree -L 1
.
+-- README.md
+-- defaults
+-- handlers
+-- meta
+-- molecule
+-- tasks
L-- vars

6 directories, 1 file

Получилась типичная ansible-роль. Далее все взаимодействия с CLI Молекулы производятся из корня роли.


Посмотрим, что находится в директории роли:


> tree molecule/default/
molecule/default/
+-- Dockerfile.j2  # Jinja-шаблон для Dockerfile
+-- INSTALL.rst.   # Немного информации об установке зависимостей сценария
+-- molecule.yml   # Файл конфигурации
+-- playbook.yml   # Плейбук запуска роли
L-- tests          # Директория с тестами стадии verify
    L-- test_default.py

1 directory, 6 files

Разберём конфиг molecule/default/molecule.yml (заменим только docker image):


---
dependency:
  name: galaxy
driver:
  name: docker
lint:
  name: yamllint
platforms:
  - name: instance
    image: centos:7
provisioner:
  name: ansible
  lint:
    name: ansible-lint
scenario:
  name: default
verifier:
  name: testinfra
  lint:
    name: flake8

dependency


Эта секция описывает источник зависимостей.


Возможные варианты: galaxy, gilt, shell.


Shell – это просто командная оболочка, которая используется в случае, если galaxy и gilt не покрывают ваших потребностей.


Не буду здесь долго останавливаться, достаточно описано в документации.


driver


Название драйвера. У нас это docker.


lint


В качестве линтера используется yamllint.


Полезные опции в данной части конфига — это возможность указать файл конфигурации для yamllint, пробросить переменные окружения либо отключить линтер:


lint:
  name: yamllint
  options:
    config-file: foo/bar
  env:
    FOO: bar
  enabled: False

platforms


Описывает конфигурацию инстансов.
В случае с докером в роли драйвера, Молекула итерируется по этой секции, и каждый элемент списка доступен в Dockerfile.j2 как переменная item.


В случае с драйвером, в котором обязательны create.yml и destroy.yml, секция доступна в них как molecule_yml.platforms, а итерации по ней описаны уже в этих файлах.


Поскольку Молекула предоставляет управление инстансами ansible-модулям, то и список возможных настроек надо искать там. Для докера, например, используется модуль docker_container_module. Какие модули используются в остальных драйверах, можно найти в документации.


А также примеры использования различных драйверов можно найти в тестах самой Молекулы.


Заменим здесь centos:7 на ubuntu.


provisioner


"Поставщик" — сущность, управляющая инстансами. В случае Молекулы это ansible, поддержка других не планируется, поэтому эту секцию можно с оговоркой назвать расширенной конфигурацией ansible.
Здесь можно указать много всего, выделю основные, на мой взгляд, моменты:


  • playbooks: можно указывать, какие плейбуки должны использоваться на определённых стадиях.

provisioner:
  name: ansible
  playbooks:
    create: create.yml
    destroy: ../default/destroy.yml
    converge: playbook.yml
    side_effect: side_effect.yml
    cleanup: cleanup.yml


provisioner:
  name: ansible
  config_options:
    defaults:
      fact_caching: jsonfile
    ssh_connection:
      scp_if_ssh: True


provisioner:
  name: ansible  
  connection_options:
    ansible_ssh_common_args: "-o 'UserKnownHostsFile=/dev/null' -o 'ForwardAgent=yes'"

  • options: параметры Ansible и переменные окружения

provisioner:
  name: ansible  
  options:
    vvv: true
    diff: true
  env:
    FOO: BAR

scenario


Название и описание последовательностей сценария.
Изменить матрицу действий по умолчанию какой-либо команды можно, добавив ключ <command>_sequence и как значение для него определив нужный нам список шагов.
Допустим, мы хотим изменить последовательность действий при запуске команды прогона плейбука: molecule converge


# изначально:
# - dependency
# - create
# - prepare
# - converge
scenario:
  name: default
  converge_sequence:
    - create
    - converge

verifier


Настройка фреймворка для тестов и линтера к нему. По умолчанию в качестве линтера используется testinfra и flake8. Возможные опции схожи с вышеизложенными:


verifier:
  name: testinfra
  additional_files_or_dirs:
    - ../path/to/test_1.py
    - ../path/to/test_2.py
    - ../path/to/directory/*
  options:
    n: 1
  enabled: False
  env:
    FOO: bar
  lint:
    name: flake8
    options:
      benchmark: True
    enabled: False
    env:
      FOO: bar

Вернёмся к нашей роли. Отредактируем файл tasks/main.yml до такого вида:


---
- name: Install nginx
  apt:
    name: nginx
    state: present

- name: Start nginx
  service:
    name: nginx
    state: started

И добавим тесты в molecule/default/tests/test_default.py


def test_nginx_is_installed(host):
    nginx = host.package("nginx")
    assert nginx.is_installed

def test_nginx_running_and_enabled(host):
    nginx = host.service("nginx")
    assert nginx.is_running
    assert nginx.is_enabled

def test_nginx_config(host):
    host.run("nginx -t")

Готово, осталось только запустить (из корня роли, напомню):


> molecule test

Длинный выхлоп под спойлером:
--> Validating schema <path>/nginx/molecule/default/molecule.yml.
Validation completed successfully.
--> Test matrix

L-- default
    +-- lint
    +-- destroy
    +-- dependency
    +-- syntax
    +-- create
    +-- prepare
    +-- converge
    +-- idempotence
    +-- side_effect
    +-- verify
    L-- destroy

--> Scenario: 'default'
--> Action: 'lint'
--> Executing Yamllint on files found in <path>/nginx/...
Lint completed successfully.
--> Executing Flake8 on files found in <path>/nginx/molecule/default/tests/...
Lint completed successfully.
--> Executing Ansible Lint on <path>/nginx/molecule/default/playbook.yml...
Lint completed successfully.
--> Scenario: 'default'
--> Action: 'destroy'

    PLAY [Destroy] *****************************************************************

    TASK [Destroy molecule instance(s)] ********************************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    TASK [Wait for instance(s) deletion to complete] *******************************
    ok: [localhost] => (item=None)
    ok: [localhost]

    TASK [Delete docker network(s)] ************************************************

    PLAY RECAP *********************************************************************
    localhost                  : ok=2    changed=1    unreachable=0    failed=0

--> Scenario: 'default'
--> Action: 'dependency'
Skipping, missing the requirements file.
--> Scenario: 'default'
--> Action: 'syntax'

    playbook: <path>/nginx/molecule/default/playbook.yml

--> Scenario: 'default'
--> Action: 'create'

    PLAY [Create] ******************************************************************

    TASK [Log into a Docker registry] **********************************************
    skipping: [localhost] => (item=None)

    TASK [Create Dockerfiles from image names] *************************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    TASK [Discover local Docker images] ********************************************
    ok: [localhost] => (item=None)
    ok: [localhost]

    TASK [Build an Ansible compatible image] ***************************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    TASK [Create docker network(s)] ************************************************

    TASK [Create molecule instance(s)] *********************************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    TASK [Wait for instance(s) creation to complete] *******************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    PLAY RECAP *********************************************************************
    localhost                  : ok=5    changed=4    unreachable=0    failed=0

--> Scenario: 'default'
--> Action: 'prepare'
Skipping, prepare playbook not configured.
--> Scenario: 'default'
--> Action: 'converge'

    PLAY [Converge] ****************************************************************

    TASK [Gathering Facts] *********************************************************
    ok: [instance]

    TASK [nginx : Install nginx] ***************************************************
    changed: [instance]

    TASK [nginx : Start nginx] *****************************************************
    changed: [instance]

    PLAY RECAP *********************************************************************
    instance                   : ok=3    changed=2    unreachable=0    failed=0

--> Scenario: 'default'
--> Action: 'idempotence'
Idempotence completed successfully.
--> Scenario: 'default'
--> Action: 'side_effect'
Skipping, side effect playbook not configured.
--> Scenario: 'default'
--> Action: 'verify'
--> Executing Testinfra tests found in <path>/nginx/molecule/default/tests/...
    ============================= test session starts ==============================
    platform darwin -- Python 2.7.15, pytest-4.3.0, py-1.8.0, pluggy-0.9.0
    rootdir: <path>/nginx/molecule/default, inifile:
    plugins: testinfra-1.16.0
collected 4 items

    tests/test_default.py ....                                               [100%]

    ========================== 4 passed in 27.23 seconds ===========================
Verifier completed successfully.
--> Scenario: 'default'
--> Action: 'destroy'

    PLAY [Destroy] *****************************************************************

    TASK [Destroy molecule instance(s)] ********************************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    TASK [Wait for instance(s) deletion to complete] *******************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    TASK [Delete docker network(s)] ************************************************

    PLAY RECAP *********************************************************************
    localhost                  : ok=2    changed=2    unreachable=0    failed=0

Наша простая роль протестировалась без проблем.
Стоит помнить, что если возникли проблемы при работе molecule test, то, если вы не изменяли стандартную последовательность, Молекула удалит инстанс.


Для дебага полезны следующие команды:


> molecule --debug <command> # debug info. При обычном запуске Молекула скрывает логи.
> molecule converge          # Оставляет инстанс после прогона тестируемой роли.
> molecule login             # Зайти в созданный инстанс.
> molecule --help            # Полный список команд.

Существующая роль


Добавление нового сценария к существующей роли происходит из директории роли следующими командами:


# полный список доступных параметров
> molecule init scenarion --help
# создание нового сценария
> molecule init scenario -r <role_name> -s <scenario_name>

В случае, если это первый сценарий в роли, то параметр -s можно опустить, так как будет создан сценарий default.


Заключение


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


Спасибо за ваше внимание. Если у вас есть опыт тестирования ansible-ролей, и он не связан с Молекулой — расскажите о нем в комментариях!

Комментарии (14)


  1. koropovskiy
    15.04.2019 14:15

    спасибо за статью.


  1. yvm
    15.04.2019 15:11
    +1

    Если сравнивать со спецназом, то ansible тренируется в здании похожем на захваченное террористами. Docker приезжает на спецоперецию со своим зданием.


  1. mkovalevskyi
    15.04.2019 22:45

    его документация умалчивает о многих подводных камнях

    так о каких камнях она умалчивает?..


    1. Flyik Автор
      16.04.2019 13:21

      Например, о директории, в которой хранится state, и знание о которой поможет в дебаге с большим количеством инстансов. К сожалению, не могу вспомнить всего списка, т.к. с момента написания статьи прошло некоторое время, а с версии 2.20 документация заметно улучшилась по сравнению с тем, что было в 2.19. Но, возможно, выражение о "многих подводных камнях" в данном случае действительно слишком претензиозное.


      1. mkovalevskyi
        16.04.2019 17:46

        Претензиозность дело такое, на сам текст не осбо влияет, но если открываешь статью именно что б найти «умолчанное» — то это достаточно сложно сделать.


  1. crazylh
    16.04.2019 00:22

    Вот было бы что-то такое же, но для плейбуков.


    1. rakhinskiy
      16.04.2019 13:48

      Так вроде можно

      molecule.yml / playbook.yml
      molecule.yml
      ...
      ansible:
        requirements_file: requirements.yml
      ...
      
      playbook.yml
      ...
      - name: Converge
        hosts: all
        any_errors_fatal: true
        roles:
          - role: common
      ...
      


  1. rakhinskiy
    16.04.2019 13:23

    А вы пробовали использовать вместо testinfra что то еще с молекулой?
    Например serverspec/inspec/goss.
    Можно ли расшарить один enviroment молекулы между разными ролями?
    Например я тестирую свои роли для CentOS/Debian/OpenSUSE/Oracle/Scientific/Ubuntu и на выходе у меня получается 11 виртуальных машин (Которые собраны с помощью packer и максимально приближены к чистой инсталяции firewall/selinx включены и т.д.).
    И вот поднимать по 11 инстансов для каждой роли не очень удобно.


    1. Flyik Автор
      16.04.2019 13:27

      Нет, нам пока хватает testinfra.
      Не совсем понял второй вопрос, что Вы имеете в виду под environment?


      1. rakhinskiy
        16.04.2019 13:35

        Например есть у меня роль common (настройки timezone/ssh/cron/...) и например elk (elasticsearc/kibana/logstash/filebeat/...) для каждой роли поднимается свой набор виртуалок. У меня получается их много, если бы можно было как то расшарить их между ролями.
        Да вопрос наверно не совсем корректный, а точнее не имеет смысла.
        Роли должны все таки тестироваться изолированно.


    1. andrei_neustroev
      16.04.2019 14:01

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


    1. AlexGluck
      16.04.2019 21:13

      Мы окружение описываем в матрице CI. Вот так выглядит наш файл для молекулы

      molecule.yml
      ---
      dependency:
        name: galaxy
      driver:
        name: docker
      lint:
        name: yamllint
        options:
          config-file: molecule/default/.yamllint
      platforms:
        - name: ${MOLECULE_PROJECT_NAMESPACE:-infra}-${MOLECULE_JOB_STAGE:-test}-${MOLECULE_PROJECT_NAME:-activemq}-01-${MOLECULE_DISTRO:-centos7}
          image: "geerlingguy/docker-${MOLECULE_DISTRO:-centos7}-ansible"
          image_version: latest
          command: ${MOLECULE_DOCKER_COMMAND:-"/usr/sbin/init"}
          volumes:
            - /sys/fs/cgroup:/sys/fs/cgroup:ro
          privileged: true
          pre_build_image: true
      provisioner:
        name: ansible
        lint:
          name: ansible-lint
        playbooks:
          converge: ${MOLECULE_PLAYBOOK:-playbook.yml}
      scenario:
        name: default
      verifier:
        name: testinfra
        lint:
          name: flake8



  1. danilychen
    16.04.2019 13:37

    В статье тема раскрыта более шире


    1. Flyik Автор
      16.04.2019 13:39

      В предложенной Вами статье действительно шире раскрыта глобальная тема тестирования ролей. Наша же статья только о Молекуле