Подход «инфраструктура как код» упрощает создание и управление инфраструктурой, но это всё ещё код, и относится к нему надо как к любому коду. А значит, нам нужно внедрять практики SDLC. О реализации одной из них и будет эта статья. А точнее, про тестирование инфраструктурного кода.
Мы пользуемся Docker-провайдером, но это накладывает некоторые ограничения на запуск. Например, там нельзя редактировать файлы hostname, нельзя поставить нормальный файрвол и т. д. Эти ограничения иногда довольно болезненны, когда то же самое отправляешь в эксплуатационную среду, которая хоть и чуть-чуть, но отличается. Мы в проекте используем Ansible, и перед настройкой production-окружение хотим протестировать наши playbook. И для этого используем Molecule совместно с TestInfra. Как? Сейчас расскажем.
Что такое Molecule?
Molecule — это инструмент тестирования Ansible-ролей. Что-то вроде модульных тестов для инфраструктуры. В официальной документации сказано:
Molecule призван помочь в разработке и тестировании ролей Ansible, и способствует внедрению подхода, результатом которого являются комплексно проработанные роли, которые хорошо написаны, просты в понимании и поддержке.
Преимущества использования Ansible Molecule:
Изоляция тестовой среды. Molecule создаёт изолированную среду для тестирования ролей Ansible, что позволяет избежать конфликтов с другими приложениями и сервисами.
Автоматизация тестирования. Molecule позволяет автоматизировать создание, настройку и тестирование ролей Ansible, что упрощает и ускоряет разработку.
Поддержка различных платформ, таких как Docker, Vagrant, OpenStack и других, что позволяет тестировать роли Ansible в различных окружениях.
Интеграция с CI/CD. Molecule может быть встроен в системы непрерывной интеграции и доставки (CI/CD), такие как Jenkins, GitLab CI, Travis CI и другие, что позволяет автоматизировать тестирование и развёртывание ролей Ansible.
Поддержка различных тестовых фреймворков. Molecule поддерживает Testinfra, Serverspec и другие фреймворки, что позволяет тестировать роли Ansible на различных уровнях (интеграционные, функциональные, модульные тесты и т. д.).
Давайте подробнее рассмотрим тестовую матрицу Molecule. По умолчанию она выглядит так: dependency, lint, cleanup, destroy, syntax, create, prepare, converge, idempotence, side_effect, verify, cleanup, destroy.
Рассмотрим каждый шаг подробно:
dependency: в рамках своих ролей вы можете использовать какие-то внешние зависимости из экосистемы Ansible. Molecule получает их на этом шаге для дальнейшего работы.
lint: проверка ролей не соответствие правилам различных линтеров, например yamllint, ansible-lint, flake8.
destroy: удаление тестового окружения.
syntax: проверка синтаксиса ваших playbook.
create: создание экземпляров для дальнейшей работы. Это могут быть Docker-контейнеры или виртуальные машины, в зависимости от используемого вами драйвера.
prepare: подготовка ваших экземпляров, например, добавление дополнительных пользователей, или установка пакетов.
converge: выполнение playbook, которые вы хотите протестировать.
idempotence: проверка на идемпотентность, заключается в выполнении фазы converge ещё раз. И проверка, что все задачи вернули статус “OK”, а не “changed”.
side_effect: выполнение дополнительных playbook для ваших ролей. Как вариант, перед выполнением каких-либо тестов добавьте данные в тестируемые системы.
verify: выполнение тестов, проверка, что ваши playbook не просто выполнились, а дают ожидаемые результаты. Можно использовать модуль Ansible-assert или Framework-testinfra.
Подробнее о возможностях Molecule можно узнать из этого видео.
Использование Molecule
Одно из главных преимуществ Molecule — поддержка различных платформ. Подробнее об этом мы поговорим ниже.
Molecule создаёт и управляет экземпляры, на которые «накатывает» playbook и прогоняет тесты. Проще всего разворачивать экземпляры с помощью Docker-драйвера, он позволяет создавать в для тестов Docker-контейнеры. В большинстве случаев этого вполне достаточно, но есть одно «но»: в production-окружении ваши роли устанавливаются на физические серверы или виртуальные машины, и образы ОС могут отличаться от ОС в образе Docker.
Решение есть! На помощь приходит delegated driver и, конечно же, VK Cloud :) Весь исходный код, используемый в статье, можно найти тут.
Для начала установим необходимые для работы Molecule пакеты, в том числе OpenSDK, с которым мы будем работать:
# Установка ansible
python3 -m pip install --user ansible
# Установка последней версии molecule
python3 -m pip install -U git+https://github.com/ansible-community/molecule
# Установка testinfra
python3 -m pip install --user pytest-testinfra
# Установка openstacksdk.
pip3 install openstacksdk
И снова обратимся к документации:
Delegated не является драйвером по умолчанию, используемым в Molecule. В соответствии с этим драйвером разработчики несут ответственность за реализацию сreate и destroy playbook. Однако разработчик должен придерживаться API-интерфейса конфигурации экземпляра.
Тут всплывает новое понятие в Molecule — «конфигурация экземпляра» (instance-config). Давайте разберёмся что это такое. На самом деле это обычный YAML-файл, который находится в директории $HOME/.cache/molecule/<role-name>/<scenario-name>/instance_config.yml и имеет такую структуру:
address: IP-адрес;
identity_file: файл с приватным ключом;
instance: имя экземпляра;
port: порт для подключения по SSH;
user: пользователь для подключения по SSH;
password: gароль для подключения по SSH.
Чтобы заполнить эту структуру, нужно сначала сгенерировать сам файл. Для этого воспользуемся командой для инициализации сценария с delegated driver:
molecule init scenario -driver-name=delegated
Молекула создаст для подготовленные шаблоны основных файлов:
converge.yml
create.yml
destroy.yml
molecule.yml
prepare.yml
Начнём с файла create.yml, который отвечает за создание экземпляров для Molecule. Как мы видим, за нас уже сделана часть работы по генерированию файла instance_config, достаточно немного его отредактировать и добавить специфичные для VK Cloud опции:
- name: Populate instance config dict
ansible.builtin.set_fact:
instance_conf_dict: {
'address': "{{ item.server.addresses['ext-net'][0].addr }}",
'instance': "{{ item.server.metadata.hostname }}",
'name': "{{ item.server.name }}",
'user': "{{ molecule_yml.driver.options.ansible_connection_options.ansible_ssh_user }}",
'port': "22",
'identity_file': "{{ ssh_key_file }}",
}
with_items: "{{ instance_created.instances }}"
register: instance_config_dict
when: instance_created.changed
Внимательный читатель заметит использование переменной molecule_yml — тут находятся переменные, определённые в файле molecule.yml. Теперь дело за малым: написать задачу для создания виртуальной машины. Для этого воспользуемся OpenStack Ansible-модулем:
- name: Create a new instance
openstack.cloud.server:
state: present
auth:
auth_url: "{{ auth_url }}"
username: "{{ username }}"
password: "{{ password }}"
project_id: "{{ project_id }}"
user_domain_name: "{{ user_domain_name }}"
availability_zone: "{{ item.availability_zone }}"
name: "{{ item.name }}-{{ 50 | random | to_uuid }}"
image: "{{ item.image }}"
key_name: "{{ key_name }}"
timeout: 200
config_drive: true
region_name: RegionOne
flavor: "{{ item.flavor }}"
security_groups: "{{ item.security_group }}"
nics:
- net-id: "{{ net_id }}"
boot_from_volume: true
volume_size: "{{ item.volume_size }}"
terminate_volume: true
meta:
hostname: "{{ item.hostname }}"
register: instance
В файле снова появляются неизвестные нам переменные, например, переменная security_group отвечает за группу, которая будет использоваться в самом Ansible. Но если посмотреть в задачу, которая вызывает создание виртуальной машины, то всё становится на свои места:
- name: Create instances
ansible.builtin.include_tasks: tasks/create_instance.yml
loop: "{{ molecule_yml.platforms }}"
Опишем наши виртуальные машины в molecule.yml:
platforms:
- name: web01
hostname: web01
image: "CentOS-7.9-202107"
flavor: Basic-1-1-10
volume_size: 10
security_group: default,ssh
availability_zone: GZ1
groups:
- webservers
Так как создание виртуальных машин — это не мгновенный процесс, какое-то время необходимо подождать, поэтому мы добавляем ожидание её доступности по SSH, и только после подтверждения её создания продолжает дальнейшие операции:
- name: Wait for sshd to come up on {{ item.name }}
ansible.legacy.wait_for:
host: "{{ instance_created.instances[0].server.addresses['ext-net'][0].addr }}"
port: 22
timeout: 90
Не стоит забывать, что после того, как мы использовали виртуальную машину, её нужно удалить — убрать за собой. Для этого давайте заглянем в playbook destroy, он отвечает за удаление виртуальных машин. Нам нужно получить список созданных виртуалок, он есть в instance_config:
- name: Set VM for destroy
ansible.builtin.set_fact:
instance_content: "{{ lookup('file', '{{ molecule_instance_config }}') }}"
А далее создаём задачу по циклу, которая удалит наши виртуальные машины:
- name: Destroy an instance
openstack.cloud.server:
name: "{{ item.name }}"
auth:
auth_url: "{{ auth_url }}"
username: "{{ username }}"
password: "{{ password }}"
project_id: "{{ project_id }}"
user_domain_name: "{{ user_domain_name }}"
state: absent
Осталось совсем немного. Регистрируемся на платформе VK Cloud, если у вас ещё нет учетной записи. При регистрации вы получите на баланс 3000 рублей для тестов. Для активации API необходимо добавить двухфакторную аутентификацию. Далее заполняем специфичные для проекта переменные:
auth_url - адрес API openstack
username — ваш email;
password — ваш пароль;
project_id;
user_domain_name;
net_id — идентификатор вашей сети;
key_name — ключ SSH, предварительно добавленный в «ключевые пары»;
ssh_key_file — ~/.ssh/id_rsa.
В примере из репозитория есть роль, которая установит веб-сервер и MySQL-сервер, можете потом попробовать.
Наш основной playbook:
---
- name: Apply for common configuration to all the nodes
hosts: all
become: yes
remote_user: centos
roles:
- common
- name: Deploy MySQL and configure databases
hosts: dbservers
become: yes
remote_user: centos
roles:
- db
- name: Deploy Apache, PHP and configure website code
hosts: webservers
become: yes
remote_user: centos
roles:
- web
Добавляем в converge playbook запуск нашего основного playbook:
- name: Run Full playbook
ansible.builtin.import_playbook: ../../site.yml
И не забудем про тесты, ради чего мы всё это делаем. Запускаем Molecule и ждём результат:
def test_service_httpd(host):
s = host.service("mysqld")
assert s.is_enabled
assert s.is_running
def test_service_firewalld(host):
s = host.service("firewalld")
assert s.is_enabled
assert s.is_running
def test_httpd_port(host):
host.socket("tcp://:::3306").is_listening
Итоги
Благодаря интеграции VK Cloud и Molecule мы в своём проекте уменьшили длительность тестирования ролей. Тестирование с Docker Driver заняло у нас 1 ч 50 минут. При этом было много тестов, которые просто падали: где-то Docker отвалился, где-то ещё какая-то ерунда случилась. То есть были инфраструктурные причины, на которые очень трудно повлиять. А вот с delegated driver мы получили чистые 40 минут времени, но стабильно, и всегда всё проходило хорошо.
А самое главное теперь наши роли тестируются на тех образах виртуальных машин, на которых и работает production.