Инструкция: как тестировать ansible-роли и узнавать о проблемах до продакшена

Всем привет!

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

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


a-ianpukaixuflfp5svh_vpj8tu.png

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

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

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


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

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


Scenario

Сценарий содержит в себе описание того, что, где, как и в какой последовательности будет выполнено. У одной роли может быть несколько сценариев, и каждый — это директория по пути /molecule/, содержащая в себе описания необходимых для теста действий. Обязательно должен присутствовать сценарий 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 . Но стоит понимать, что для каждой такой cli-команды может существовать своя последовательность действий, узнать которую можно, выполнив molecule matrix . Например, при запуске команды converge (прогон тестируемого плейбука) будут выполнены следующие действия:

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

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

Добавить шаги с ? можно, описав желаемые действия в формате 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 :

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

6 directories, 1 file

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

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

> tree molecule/default/
molecule/default/
├── Dockerfile.j2  # Jinja-шаблон для Dockerfile
├── INSTALL.rst.   # Немного информации об установке зависимостей сценария
├── molecule.yml   # Файл конфигурации
├── playbook.yml   # Плейбук запуска роли
└── tests          # Директория с тестами стадии verify
    └── 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

Название и описание последовательностей сценария.
Изменить матрицу действий по умолчанию какой-либо команды можно, добавив ключ _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 /nginx/molecule/default/molecule.yml.
Validation completed successfully.
--> Test matrix

└── default
    ├── lint
    ├── destroy
    ├── dependency
    ├── syntax
    ├── create
    ├── prepare
    ├── converge
    ├── idempotence
    ├── side_effect
    ├── verify
    └── destroy

--> Scenario: 'default'
--> Action: 'lint'
--> Executing Yamllint on files found in /nginx/...
Lint completed successfully.
--> Executing Flake8 on files found in /nginx/molecule/default/tests/...
Lint completed successfully.
--> Executing Ansible Lint on /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: /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 /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: /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  # debug info. При обычном запуске Молекула скрывает логи.
> molecule converge          # Оставляет инстанс после прогона тестируемой роли.
> molecule login             # Зайти в созданный инстанс.
> molecule --help            # Полный список команд.


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

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

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

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


Заключение

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

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

© Habrahabr.ru