Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
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: 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, 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: 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