Всем привет!
Я работаю 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? — подготовка инстанса; например, проверка / установка python2converge— запуск тестируемого плейбука,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: flake8dependency
Эта секция описывает источник зависимостей.
Возможные варианты: galaxy, gilt, shell.
Shell – это просто командная оболочка, которая используется в случае, если galaxy и gilt не покрывают ваших потребностей.
Не буду здесь долго останавливаться, достаточно описано в документации.
driver
Название драйвера. У нас это docker.
lint
В качестве линтера используется yamllint.
Полезные опции в данной части конфига — это возможность указать файл конфигурации для yamllint, пробросить переменные окружения либо отключить линтер:
lint:
name: yamllint
options:
config-file: foo/bar
env:
FOO: bar
enabled: Falseplatforms
Описывает конфигурацию инстансов.
В случае с докером в роли драйвера, Молекула итерируется по этой секции, и каждый элемент списка доступен в 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- config_options: конфиг ansible
provisioner:
name: ansible
config_options:
defaults:
fact_caching: jsonfile
ssh_connection:
scp_if_ssh: True- connection_options: параметры подключения
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: BARscenario
Название и описание последовательностей сценария.
Изменить матрицу действий по умолчанию какой-либо команды можно, добавив ключ <command>_sequence и как значение для него определив нужный нам список шагов.
Допустим, мы хотим изменить последовательность действий при запуске команды прогона плейбука: molecule converge
# изначально:
# - dependency
# - create
# - prepare
# - converge
scenario:
name: default
converge_sequence:
- create
- convergeverifier
Настройка фреймворка для тестов и линтера к нему. По умолчанию в качестве линтера используется 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)

yvm
15.04.2019 15:11+1Если сравнивать со спецназом, то ansible тренируется в здании похожем на захваченное террористами. Docker приезжает на спецоперецию со своим зданием.

mkovalevskyi
15.04.2019 22:45его документация умалчивает о многих подводных камнях
так о каких камнях она умалчивает?..
Flyik Автор
16.04.2019 13:21Например, о директории, в которой хранится state, и знание о которой поможет в дебаге с большим количеством инстансов. К сожалению, не могу вспомнить всего списка, т.к. с момента написания статьи прошло некоторое время, а с версии 2.20 документация заметно улучшилась по сравнению с тем, что было в 2.19. Но, возможно, выражение о "многих подводных камнях" в данном случае действительно слишком претензиозное.

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

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

rakhinskiy
16.04.2019 13:48Так вроде можно
molecule.yml / playbook.ymlmolecule.yml ... ansible: requirements_file: requirements.yml ... playbook.yml ... - name: Converge hosts: all any_errors_fatal: true roles: - role: common ...
rakhinskiy
16.04.2019 13:23А вы пробовали использовать вместо testinfra что то еще с молекулой?
Например serverspec/inspec/goss.
Можно ли расшарить один enviroment молекулы между разными ролями?
Например я тестирую свои роли для CentOS/Debian/OpenSUSE/Oracle/Scientific/Ubuntu и на выходе у меня получается 11 виртуальных машин (Которые собраны с помощью packer и максимально приближены к чистой инсталяции firewall/selinx включены и т.д.).
И вот поднимать по 11 инстансов для каждой роли не очень удобно.
Flyik Автор
16.04.2019 13:27Нет, нам пока хватает testinfra.
Не совсем понял второй вопрос, что Вы имеете в виду под environment?
rakhinskiy
16.04.2019 13:35Например есть у меня роль common (настройки timezone/ssh/cron/...) и например elk (elasticsearc/kibana/logstash/filebeat/...) для каждой роли поднимается свой набор виртуалок. У меня получается их много, если бы можно было как то расшарить их между ролями.
Да вопрос наверно не совсем корректный, а точнее не имеет смысла.
Роли должны все таки тестироваться изолированно.

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

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

danilychen
16.04.2019 13:37В статье тема раскрыта более шире

Flyik Автор
16.04.2019 13:39В предложенной Вами статье действительно шире раскрыта глобальная тема тестирования ролей. Наша же статья только о Молекуле
koropovskiy
спасибо за статью.