Molecule это фреймворк, предназначенный для тестирования ролей в Ansible. На хабре довольно много статей про тестирование с помощью molecule и почти во всех статьях говорится о неких "сложных сценариях тестирования для ansible", и далее в примерах обычно идут какие-то простенькие роли и тесты. Мне стало интересно протестировать более сложную роль, например роль для создания RabbitMQ кластера.
Используемые версии программ на момент написания статьи. Не гарантируется корректная работа для molecule версии ниже 3.3
debian 10 Buster
ansible-3.4.0
molecule-3.3.0
docker-ce-20.10.6
yamllint-1.26.1
ansible-lint-5.0.8
Устанавливаем ansible и molecule.
pip3 install --user ansible (как именно устанавливать не столь важно, в приведенном примере установка идет в хоумдир пользователя).
pip3 install --user molecule[docker] (мы будем использовать драйвер докера)
Устанавливаем линтеры
pip3 install --user ansible-lint yamllint
Установка докера выходит за рамки этой статьи, стоит отметить только что докер вы можете установить на эту же машину, где будете запускать molecule или же установить докер на любую другую машину в сети (например если мощности локальной машины не хватает) или же использовать уже существующий докер сервер.
Во втором случае на локальную машину нужно установить только докер клиент и выставить переменную DOCKER_HOST="ssh://ansible@адрес_вашего_докер_сервера", где ansible - аккаунт, который имеет ssh доступ на сервер и под которым будут создаваться докер контейнеры. Аккаунт также должен состоять в группе docker на докер сервере.
Описание ансибл роли тоже выходит за рамки статьи, можно взять любую похожую роль, где есть логика создания кластера.
Переходим в нашу условную роль
cd roles/role_rabbitmq
Создаем конфиги для линтеров в текущей директории (дефолтные конфиги будут генерить много лишних алертов) или же создаем линки на общие конфиги.
.ansible-lint
---exclude_paths: - .cache/ - .git/ - molecule/skip_list: - command-instead-of-module - git-latest - no-handler - package-latest - empty-string-compare - command-instead-of-shell - meta-no-info - no-changed-when - no-relative-paths - risky-shell-pipe - role-name - unnamed-task
.yamllint
---extends: defaultignore: | templates/ sites/ files/ old/ README.md LICENSErules: braces: min-spaces-inside: 0 max-spaces-inside: 1 brackets: min-spaces-inside: 0 max-spaces-inside: 1 comments: require-starting-space: false level: error indentation: spaces: 2 indent-sequences: consistent line-length: disable truthy: disable
Создаем директорию molecule/cluster для нашего сценария.
mkdir molecule/cluster
Открываем в редакторе файл molecule/cluster/Dockerfile.j2. Данный конфиг будет использоваться при создании докер контейнеров. Опять же ничто не ограничивает вашу фантазию - можно использовать уже готовый имидж с ансибл на борту или создать свой.
FROM registry.company.net/debian/buster:latestENV DEBIAN_FRONTEND noninteractiveENV pip_packages "ansible"ENV http_proxy "http://10.10.0.1:8888"ENV https_proxy "http://10.0.0.1:8888"ENV no_proxy "127.0.0.1,localhost,*.company.net,10.0.0.0/8,192.168.0.0/16,172.0.0.0/8"# Install dependencies.RUN apt update \ && apt-get install -y --no-install-recommends \ sudo systemd systemd-sysv \ build-essential wget libffi-dev libssl-dev \ python3-apt python3-cryptography python3-pip python3-dev python3-setuptools python3-wheel \ procps passwd curl lsof netcat gnupg ca-certificates openssh-client less vim iputils-ping iproute2 \ debian-archive-keyring dnsutils \ && rm -rf /var/lib/apt/lists/* \ && rm -Rf /usr/share/doc && rm -Rf /usr/share/man \ && apt-get clean# Create ansible userRUN groupadd --system ansible \ && useradd --system --comment "Ansible remote management" --home-dir /home/ansible --create-home --gid ansible --shell /bin/bash --password "*" ansible && echo "%ansible ALL = (ALL) NOPASSWD:ALL" > /etc/sudoers.d/ansible# Add company repoRUN curl -k "https://certs.company.net/ca.pem" > /usr/local/share/ca-certificates/ca.crt && update-ca-certificates \ && curl -k "https://company.net/repos/keys/company_repo_key.gpg" | apt-key add \ && echo "deb https://company.net/repos/buster buster-local main > /etc/apt/sources.list.d/company.list && apt-get update && pip3 install $pip_packages# Install Ansible inventory file.RUN mkdir -p /etc/ansible && echo "[local]\nlocalhost ansible_connection=local" > /etc/ansible/hosts# Exclude /usr/share/doc# Если программа использует файлы из /usr/share/doc, то следует добавить диру в игнор для dpkg, иначе файлы будут удаленыRUN sed -i 's/path-exclude \/usr\/share\/doc/#path-exclude \/usr\/share\/doc/' /etc/dpkg/dpkg.cfg.d/docker# Make sure systemd doesn't start agettys on tty[1-6].RUN rm -f /lib/systemd/system/multi-user.target.wants/getty.targetVOLUME ["/sys/fs/cgroup"]CMD ["/lib/systemd/systemd"]
Создаем файл molecule/cluster/prepare.yml. Данный файл используется молекулой также как и в ансибле - для различных pre-tasks. В данном случаем мы обновляем дебиан пакеты и устанавливаем питон модуль pika для RabbitMQ.
---- name: prepare hosts: all gather_facts: no # не используем сбор фактов для ускорения выполнения tasks: - name: update apt cache block: - name: update apt cache apt: update_cache: yes - name: perform upgrade of all packages to the latest version apt: upgrade: dist force_apt_get: yes - name: install python pika pip: name: - pika executable: pip3
Создаем файл molecule/cluster/converge.yml. В данном файле мы непосредственно указываем ансибл роль для тестирования. Обратите внимание на hosts, имя должно совпадать с именем группы в molecule.yml
---- name: Converge hosts: rabbitmq_cluster roles: - role: role_rabbitmq
Создаем файл molecule/cluster/molecule.yml. По сути это главный файл, где мы описываем все необходимые параметры для запуска наших тестов. В данном случае мы создаем докер сеть cluster 192.168.0.0/24 и создаем три докер контейнера в этой сети - node01, node01, node03 с заранее заданными айпи адресами 192.168.0.1/2/3. Это нужно для создания RabbitMQ кластера из трех нод, где ноды должны видеть друг друга.
Инвентори мы определяем как
inventory: links: group_vars: ../../../../files/molecule/group_vars/
и
groups: - rabbitmq_cluster
Поэтому создаем в структуре ансибл файл files/molecule/group_vars/rabbitmq_cluster.yml где описываем все необходимые параметры нашей ансибл роли role_rabbitmq
---rabbitmq_cluster: yescerts_dir: /etc/rabbitmq/sslrabbitmq_ssl: yesrabbitmq_ssl_certs: - "_.company.net"rabbitmq_cookie: NJWHJPAOPYKSGTRGDLTN# обратите внимание, здесь мы указываем короткие имена нод, заданные в нашем molecule.yml# все ноды из списка должны узнавать друг друга по этим коротким именамrabbitmq_nodes: - node01 - node02 - node03rabbitmq_master: rabbit@node01rabbitmq_master_node: node01rabbitmq_vhosts: - name: /testrabbitmq_users: - user: test password: test vhost: /testrabbitmq_exchanges: - name: test type: direct durable: yes vhost: /testrabbitmq_queues: - name: test durable: yes vhost: /testrabbitmq_bindings: - name: test destination: test destination_type: queue vhost: /testrabbitmq_policies: - name: ha-replica vhost: /test tags: ha-mode: exactly ha-params: 2 ha-sync-mode: automatic
molecule.yml
---dependency: name: galaxy options: ignore-certs: Truedriver: name: dockerplatforms: - name: node01 image: registry.company.net/debian/buster:latest # pre_build_image: true privileged: True tmpfs: - /run - /tmp volumes: - /sys/fs/cgroup:/sys/fs/cgroup:ro - /run/dbus/system_bus_socket:/run/dbus/system_bus_socket:ro capabilities: - SYS_ADMIN command: "/lib/systemd/systemd" dns_servers: - 10.0.0.1 groups: - rabbitmq_cluster docker_networks: - name: cluster ipam_config: - subnet: "192.168.0.0/24" gateway: "192.168.0.254" networks: - name: cluster ipv4_address: "192.168.0.1" network_mode: default - name: node02 image: registry.company.net/debian/buster:latest privileged: True tmpfs: - /run - /tmp volumes: - /sys/fs/cgroup:/sys/fs/cgroup:ro - /run/dbus/system_bus_socket:/run/dbus/system_bus_socket:ro capabilities: - SYS_ADMIN command: "/lib/systemd/systemd" dns_servers: - 10.0.0.1 groups: - rabbitmq_cluster networks: - name: cluster ipv4_address: "192.168.0.2" network_mode: default - name: node03 image: registry.company.net/debian/buster:latest privileged: True tmpfs: - /run - /tmp volumes: - /sys/fs/cgroup:/sys/fs/cgroup:ro - /run/dbus/system_bus_socket:/run/dbus/system_bus_socket:ro capabilities: - SYS_ADMIN command: "/lib/systemd/systemd" dns_servers: - 10.0.0.1 groups: - rabbitmq_cluster networks: - name: cluster ipv4_address: "192.168.0.3" network_mode: defaultprovisioner: name: ansible config_options: defaults: interpreter_python: auto_silent host_key_checking: False gathering: smart callback_whitelist: profile_tasks, timer, yaml ssh_connection: pipelining: True inventory: links: group_vars: ../../../../files/molecule/group_vars/ ansible_args: - -e molecule_run=True - -e use_proxy=False env: MOLECULE_NO_LOG: 0 ANSIBLE_VERBOSITY: 1verifier: name: ansiblelint: | set -e ansible-lint .scenario: name: cluster test_sequence: - dependency - lint - cleanup - destroy - syntax - create - prepare - converge - idempotence - side_effect - verify - cleanup - destroy
Через ansible_args можно добавлять различные переменные для ансибл роли
ansible_args: - -e molecule_run=True - -e use_proxy=False
Через env можно устанавливать различные переменные environment. В данном случае мы увеличиваем дебаг для ансибла (аналогично опции -v) через ANSIBLE_VERBOSITY.
MOLECULE_NO_LOG незадокументированная опция молекулы, позволяет ставить no_log=no для удобства отладки (по дефолту no_log в молекуле всегда yes). При этом в роли можно использовать такую конструкцию no_log: "{{ molecule_no_log|d(False)|ternary(False, True) }}". Если molecule_no_log=0, то выставить no_log: no, иначе no_log: yes. Так как используются тестовые аккаунты и пароли, то запись этих данных в лог некритична.
env: MOLECULE_NO_LOG: 0 ANSIBLE_VERBOSITY: 1
В последних версиях ansible-lint сам вызывает yamllint, поэтому можно указать линтер только ansible-lint
lint: | set -e ansible-lint .
В scenario мы определяем наш сценарий cluster и все шаги, необходимые для тестирования. Просмотреть все шаги можно через команду molecule matrix test.
Обратите внимание на сценарии side_effect и verify, если их непосредственно не указать, то у меня они почему-то не вызывались, хотя показаны в выводе molecule matrix.
scenario: name: cluster test_sequence: - dependency - lint - cleanup - destroy - syntax - create - prepare - converge - idempotence - side_effect - verify - cleanup - destroy
Попробуем запустить сценарий cluster, если не указать -s то молекула запустит сценарий default
molecule test -s cluster > /tmp/log 2>&1
Молекула начинает прогонять указанные в test_sequence шаги, причем делает это дважды для соблюдения idempotence. Если во втором тесте будут отличия от первого теста, то молекула завершит работу с ошибкой. Не всегда это работает как нужно (например если конфиг меняется динамически самим сервисом, как в случает с редис), хотя это всегда можно обойти директивой ансибла changed_when: no
В конце в логе /tmp/log должен появиться отчет с финальным сообщением "Idempotence completed successfully", то есть ошибок не найдено и роль можно смело использовать в продакшн ;). При возникновении ошибки на любом шаге, молекула прекращает работу и останавливает свои докер-контейнеры.
Если нужно посмотреть что же вызывает ошибку или проверить состояние конфига, то можно вызывать молекулу с опцией converge, molecule converge -s cluster. В этом случае молекула прогоняет все таски, указанные в converge.yml и не запускает destroy. Можно зайти в контейнер через "docker exec -it container_id /bin/bash" и просмотреть логи или проверить конфиги.
Самое интересное у молекулы на мой взгляд это side-effect и verify. Через side-effect таски можно задавать различные деструктивные действия (что-то вроде chaos monkey). А через verify таски можно проверять финальное состояние системы.
Например скажем молекуле сделать рестарт сервису rabbitmq на каждой ноде после создания кластера (но ничто не ограничивает вас в фантазии).
Создаем файл molecule/cluster/side_effect.yml
---- name: Side Effect serial: 1 hosts: all gather_facts: no # факты нам не нужны tasks: - name: restart rabbitmq service block: - name: stop rabbitmq service systemd: name: rabbitmq-server state: stopped failed_when: no - name: pause pause: seconds: 15 - name: start rabbitmq service systemd: name: rabbitmq-server state: started failed_when: no
Создаем файл molecule/cluster/verify.yml и добавим различные базовые проверки для нашего кластера (опять же ничто не ограничивает вашу фантазию).
---- name: Verify hosts: all gather_facts: no tasks: - name: cluster status block: - name: get cluster status command: "rabbitmqctl cluster_status --formatter json" register: output - name: set facts set_fact: cluster_output: "{{ output.stdout|from_json }}" - name: print nodes debug: var: cluster_output.disk_nodes - name: verify fail fail: msg: "FAIL: number of nodes is less than 3" when: - cluster_output.disk_nodes | length < 3 run_once: yes - name: check vhosts block: - name: get vhosts command: "rabbitmqctl list_vhosts --formatter json" register: output - name: set facts set_fact: vhost_output: "{{ output.stdout|from_json }}.name" - name: print vhosts debug: var: vhost_output - name: verify fail fail: msg: "FAIL: vhost is missing" when: - "'/test' not in vhost_output" run_once: yes - name: check users block: - name: get users command: "rabbitmqctl list_users --formatter json" register: output - name: set facts set_fact: user_output: "{{ output.stdout|from_json }}.user" - name: print users debug: var: user_output - name: verify fail fail: msg: "FAIL: user is missing" when: - "'test' not in user_output" run_once: yes - name: check queues block: - name: get queues command: "rabbitmqctl -p /test list_queues --formatter json" register: output - name: set facts set_fact: queue_output: "{{ output.stdout|from_json }}.name" - name: print queues debug: var: queue_output - name: verify fail fail: msg: "FAIL: queue is missing" when: - "'test' not in queue_output" run_once: yes - name: check exchanges block: - name: get exchanges command: "rabbitmqctl -p /test list_exchanges --formatter json" register: output - name: set facts set_fact: exchange_output: "{{ output.stdout|from_json }}.name" - name: print exchanges debug: var: exchange_output - name: verify fail fail: msg: "FAIL: exchange is missing" when: - "'test' not in exchange_output" run_once: yes - name: check bindings block: - name: get bindings command: "rabbitmqctl -p /test list_bindings --formatter json" register: output - name: set facts set_fact: binding_output: "{{ output.stdout|from_json }}.source_name" - name: print bindings debug: var: binding_output - name: verify fail fail: msg: "FAIL: binding is missing" when: - "'test' not in binding_output" run_once: yes - name: check policies block: - name: get policies command: "rabbitmqctl -p /test list_policies --formatter json" register: output - name: set facts set_fact: policy_output: "{{ output.stdout|from_json }}.name" - name: print policies debug: var: policy_output - name: verify fail fail: msg: "FAIL: policy is missing" when: - "'ha-replica' not in policy_output" run_once: yes - name: check publish block: - name: install consumer script copy: src: ../../../../files/molecule/scripts/consumer.py dest: /usr/local/bin/consumer.py owner: root mode: 0755 - name: publish a message to a queue rabbitmq_publish: url: "amqp://test:test@localhost:5672/%2Ftest" queue: test body: "Test message" content_type: "text/plain" durable: yes - name: receive a message from the queue command: /usr/local/bin/consumer.py run_once: yes
Так как ansible lookup не очень хорошо работает в докер-контейнере, создаем files/molecule/scripts/consumer.py, небольшой скрипт на питоне, который печатает сообщения из очереди test.
#!/usr/bin/python3import pika, sysurl = 'amqp://test:test@localhost/%2ftest'params = pika.URLParameters(url)params.socket_timeout = 1connection = pika.BlockingConnection(params)channel = connection.channel()channel.queue_declare(queue='test', durable=True)method_frame, header_frame, body = channel.basic_get(queue = 'test')if method_frame is None: connection.close() sys.exit('Queue is empty!')else: channel.basic_ack(delivery_tag=method_frame.delivery_tag) connection.close() print(body)
Проверяем side-effect
molecule converge -s clustermolecule side-effect -s cluster
Проверяем verify
molecule verify -s cluster
Если все хорошо, запускаем полный тест и проверяем лог.
molecule test -s cluster >/tmp/log 2>&1tail -f /tmp/log