Тестирование ansible роли для RabbitMQ кластера с помощью molecule

от автора

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 (Disclaimer: используйте на свой страх и риск, или отключайте skip_list полностью, если не уверены)

--- 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-relative-paths   - risky-shell-pipe   - role-name   - unnamed-task

.yamllint

--- extends: default ignore: |   templates/   sites/   files/   old/   README.md   LICENSE  rules:   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:latest  ENV DEBIAN_FRONTEND noninteractive ENV 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 user RUN groupadd --system ansible \   && useradd --system --comment "Ansible remote management" --home-dir /home/ansible --create-home --gid ansible --shell /bin/bash --password "*" an sible && echo "%ansible ALL = (ALL) NOPASSWD:ALL" > /etc/sudoers.d/ansible  # Add company repo RUN 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.target  VOLUME ["/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, node02, 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: yes certs_dir: /etc/rabbitmq/ssl rabbitmq_ssl: yes rabbitmq_ssl_certs:   - "_.company.net" rabbitmq_cookie: NJWHJPAOPYKSGTRGDLTN # обратите внимание, здесь мы указываем короткие имена нод, заданные в нашем molecule.yml # все ноды из списка должны узнавать друг друга по этим коротким именам rabbitmq_nodes:   - node01   - node02   - node03  rabbitmq_master: rabbit@node01 rabbitmq_master_node: node01  rabbitmq_vhosts:   - name: /test  rabbitmq_users:   - user: test     password: test     vhost: /test  rabbitmq_exchanges:   - name: test     type: direct     durable: yes     vhost: /test  rabbitmq_queues:   - name: test     durable: yes     vhost: /test  rabbitmq_bindings:   - name: test     destination: test     destination_type: queue     vhost: /test  rabbitmq_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: True driver:   name: docker platforms:   - 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: default provisioner:   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: 1 verifier:   name: ansible lint: |   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/python3  import pika, sys  url = 'amqp://test:test@localhost/%2ftest' params = pika.URLParameters(url) params.socket_timeout = 1  connection = 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 cluster molecule side-effect -s cluster

Проверяем verify

molecule verify -s cluster

Если все хорошо, запускаем полный тест и проверяем лог.

molecule test -s cluster >/tmp/log 2>&1 tail -f /tmp/log

ссылка на оригинал статьи https://habr.com/ru/post/557854/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *