Последнее время довольно много работаю с Ансиблом. Интересный и зачастую удобный инструмент автоматизации, но довольно специфический в обращении и требующий привыкания.
Одним из наиболее полезных источников по Ансиблу для меня является книга «Mastering Ansible», где в достаточно краткой форме рассматриваются приёмы его использования, более сложные, чем в большинстве интернет-роликов и статей. Но и в роликах порой встречаются жемчужины, например, «Ansible for Network Configuration Templates» Дэвида Малера. Ну, а начинал я знакомство с забавной, но от этого не делающейся менее полезной, серии видео «Ansible на русском языке» на канале ADV-IT.
Возникла задача: автоматизировать формирование конфигурационного файла Apache ActiveMQ, где содержится несколько сотен очередей, перезапустить сервис с новыми настройками, а в случае сбоя запуска или ошибок в логе вернуть всё как было.
Очереди в конфигурационном файле activemq.xml
выглядели так:
<!-- ESFL.CLNS -->
<route>
<from uri="activemq:queue:IN_RQ_CLNTSEARCH?concurrentConsumers=1"/>
<to uri="F1SIPQM01-BEAN:queue:IN.RQ.CLNTSEARCH_01.02.03?exchangePattern=InOnly"/>
</route>
<route>
<from uri="F1SIPQM01-BEAN:queue:OUT.RS.CLNTSEARCH_01.02.03.RKF?disableReplyTo=true&concurrentConsumers=5"/>
<to uri="activemq:queue:OUT_RS_CLNTSEARCH?exchangePattern=InOnly"/>
</route>
Особенности конфигурации:
- Первая строка-комментарий — это название группы очередей. Нужно для ориентирования.
- Если с
activemq
начинается строка from, то это исходящая очередь, если to — входящая. В другой строке на этом месте прописывается имя внешнего подключения. - Параметр
concurrentConsumers
присутствует во всех строках from. В большинстве случаев имеет значение «1», но могут быть варианты. - Параметр
disableReplyTo=true
должен быть в строке from, если очередь входящая. - Параметр
exchangePattern=InOnly
должен быть во всех строках to.
В формировании конфигурационных файлов неоценимую помощь оказывают шаблоны Ансибла, использующие язык jinja2 (который я называю то жижей-два, то ниндзей-два), где могут применяться блоки с условиями, циклы, макросы и т. п. Идея решения моей задачи заключается в том, чтобы цикл в шаблоне добавлял очереди в конфигурацию, а значения брались из групповых переменных (group_vars). Это удобно, когда нужно один и тот же шаблон конфигурации распространять на разные окружения. К примеру, в файле activemq_test.yml
будет один набор переменных, а в файле activemq_prod.yml
— другой, и тогда всё будет зависеть от простого членства сервера в той или иной группе в инвентори-файле.
По непонятной мне причине, разработчики Ансибла сделали так, что строки с описанием логики, которые не должны никак отражаться в формируемом конечном файле, оставляют там лишние пустые строки и пробелы. Это делает формирование файлов, чувствительных к отступам, типа YAML, очень сложным. У меня был XML, которому отступы безразличны, но, тем не менее, хотелось сделать разметку красиво и ровно. Я долго мучился с синтаксисом
{%- -%}
, расставляя и убирая дефисы, но сделать как следует всё равно не выходило. Провозившись с этим довольно долгое время, я уже было плюнул и хотел оставить как есть, но в обстоятельной статье Jinja2 Tutorial - Part 3 - Whitespace control, где вся эта чехарда с пустыми строками и пробелами досконально разбирается, нашёл прекрасный совет:
Always render with trim_blocks
and lstrip_blocks
options enabled.
Оказалось, что по умолчанию в Ансибле включена только опция trim_blocks
. Когда я добавил в шаблон первой строкой опцию, включающую lstrip_blocks
, логические строки перестали оказывать какое-либо влияние на форматирование, и я, наконец, смог без помех отрегулировать все отступы.
#jinja2: lstrip_blocks:True
Впрочем, в официальной документации по jinja2 всё это тоже написано, но постфактум-то всегда всё кажется очевидным, тем более, в примерах из книжки всё дефисами и регулировалось, что мешало быстрее напасть на след какого-то другого решения.
В конце концов получился такой шаблон с вложенным циклом (один перебирает группы, а второй — очереди в них) и условиями:
#jinja2: lstrip_blocks:True
{% for g in activemq %}
<!-- {{ g.group |upper }} -->
{% for q in g.queues %}
<route>
<from uri="{{ (q.bean |upper + '-BEAN') if q.direction == "in" else 'activemq' }}:queue:{{ q.name_from |upper }}?{{ 'disableReplyTo=true&' if q.direction == "in" }}concurrentConsumers={{ q.con_cons | default('1') }}"/>
{% if q.name_to != "stop" %}
<to uri="{{ (q.bean |upper + '-BEAN') if q.direction == "out" else 'activemq' }}:queue:{{ q.name_to |upper }}?exchangePattern=InOnly"/>
{% else %}
<stop/>
{% endif %}
</route>
{% endfor %}
{% endfor %}
Там, где это необходимо, значения переменных автоматически приводятся к верхнему регистру и выполняют все предварительные условия формирования правильных ссылок очередей. Имена внешних подключений всегда оканчиваются на -BEAN
, поэтому, чтобы не писать это окончание в каждой переменной, оно прибавляется к её значению уже в шаблоне.
Переменные к шаблону выглядят так:
activemq:
- group: "esFl.ClnS"
queues:
- direction: "out"
bean: "f1sipqm01"
name_from: "in_rq_clntsearch"
name_to: "in.rq.clntsearch_01.02.03"
- direction: "in"
bean: "F1SiPqm01"
name_from: "OuT.rS.CLntSeARch_01.02.03.rKf"
name_to: "oUt_Rs_ClntsEaRcH"
con_cons: 5
Чтобы параметр concurrentConsumers
для той или иной очереди принимал наиболее частое значение «1», то переменную con_cons
вообще не надо для неё указывать. А в какой строке будет слово activemq
или имя внешнего подключения, определяется переменной direction
.
Теперь строки в очередях формируются сами по небольшому числу вводных и там меньше шансов наделать ошибок, бесконечно копируя примерно одни и те же параметры.
Перейдём к шагам выполнения задачи:
- name: Stop ActiveMQ
command:
cmd: "{{ activemq_bin }} stop"
removes: "{{ activemq_pidfile }}"
- name: Remove logs and config backups
file:
path: "{{ item }}"
state: absent
with_fileglob:
- "{{ activemq_log }}*"
- "{{ activemq_conf }}.*"
- name: Deploy new config
template:
src: "activemq.xml.j2"
dest: "{{ activemq_conf }}"
owner: "{{ deploy_user }}"
mode: 0660
backup: true
- name: Start ActiveMQ
command: "{{ activemq_bin }} start"
- name: Wait
pause:
seconds: 30
#- name: Add fail to log
# lineinfile:
# dest: "{{ activemq_log }}"
# line: 'Failed: ERROR'
- name: Check ActiveMQ status
command: "{{ activemq_bin }} status"
ignore_errors: true
register: amq_status # It must contain "ActiveMQ is running"
- name: Check ActiveMQ log
command: cat "{{ activemq_log }}"
register: log_status
- name: Restore old config file if any fail
when: '("failed" in log_status.stdout_lines |lower) or ("is running" not in amq_status.stdout_lines |lower)'
block:
- name: Stop ActiveMQ if it's running
command:
cmd: "{{ activemq_bin }} stop"
removes: "{{ activemq_pidfile }}"
- name: Copy log and restore config
shell: |
cp "{{ activemq_log }}" /tmp/activemq_error.log
cp $(find {{ activemq_dir }}/conf -name 'activemq.xml.*') "{{ activemq_conf }}"
- name: Start ActiveMQ
command: "{{ activemq_bin }} start"
При каждом запуске удаляются файлы резервных копий конфигурации, поэтому копия только одна. Файл резервной копии имеет имя такого вида: activemq.xml.26721.2025-09-15@08:33:32~
. В середине закомментирован отладочный шаг добавления в лог записи, инициирующей сбой и откат на предыдущую версию. Восстановление объединено в блок, чтобы было удобно запускать его целиком по условию. В блоке восстановления копируется лог по пути, где его подберёт CI-система (в моём случае Teamcity) в виде артефакта сборки, чтобы не лезть за ним на сервер.
Можно, наверное, сделать ещё какие-нибудь проверки — например, перезапускать ActiveMQ только при изменении конфигурации, а не каждый раз, но не всё сразу. Уже выглядит неплохо, а там доработаю при необходимости.