Всем привет!
Я работаю 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: 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
- 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: 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)
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
спасибо за статью.