В этой части мы перестаём говорить о простом и приятном и начинаем говорить о трудном. Переменные в Ansible: scope, precedence, рекурсивная интерполяция. Для тех, кто дочитает до конца, маленький бонус: упрощённая таблица приоритетов, с которой можно жить. Предыдущие части: 1, 2.
Обычно рассказ про переменные в Ансибл начинают с чего-то очень простенького, что создаёт у читателя иллюзию, что переменные в Ансибл — это как в любом другом языке программирования. Мутабельные или немутабельные, локальные и глобальные. Это не так.
Это не так.
У Ансибла возникла уникальная модель переменных (модель памяти?), которую надо учить с нуля. И рассматривать мы её начнём с того места, где значения используются (обычно переменные Ансибла рассматривают с того места, откуда они появляются). Почему? Потому что при рассказе в этом направлении у нас образуется направленный граф, который куда легче уложить в голову.
Обратите внимание — я сказал "значения", потому что "переменные" — это всего лишь имена к значениям. У перменных свой глубокий внутренний мир, и про него во второй части.
В рамках этого рассказа я буду использовать понятия "появляется" и /"используется". Появляется — это то место, где значение было задано. А "попадает" — это то место, где значение начинает влиять на работу Ансибла, точнее, на его сайд-эффекты — на реальный процесс исполнения модулей в целевой системе. Пока переменные перекладываются из места на место в секции vars
, значения никуда не "попадают" и на окружающий мир никак не влияют.
Это первая важная мысль, которую надо помнить в голове: пока значение не было использовано чем-то, влияющим на окружающий мир, оно может содержать в себе любые синтаксические ошибки и ссылки на несуществующие переменные и т.д., и это никого не смутит. Почему так — читайте дальше.
Итак, где значения используются?
- В параметрах к модулю.
- copy: src=foo.conf dest=/etc/foo.conf
. В этом примереfoo.conf
и/etc/foo.conf
— значения. Они записываются в параметры модуля в целевой системе. В процессе записи, всё, что было в значении, приводится в буквальную форму. Например, на этом этапе вычисляется jinja, числа/строки приводятся к тому формату, который ожидает от них модуль (например, если модуль ожидает строку, а вы передали число, именно на этом этапе Ансибл выдаст предупреждение, что число было сконвертировано в строку). - При выполнении модуля (на самом деле это action plugin) 'template' или соответствующего lookup plugin'а по имени template. Они используют значения из переменных в шаблоне в момент своего исполнения (для lookup plugin когда этот момент наступит совсем не очевидно, потому произойдёт это только когда интерполируется значение, упоминающее этот плагин).
- В параметрах play. Любые значения в параметрах play (не путать с переменными уровня play) будут использованы буквально в момент начала play. Например, если у вас jinja2 в
gather_facts
, то она будет вычислена именно в момент начала выполнения play. Аналогично —hosts
,remote_user
и другие. - Магические переменные Ансибла, которые влияют на процесс исполнения модуля. В основном, это
ansible_host
,ansbile_user
,ansible_extra_ssh_args
,ansible_transport
и т.д. Значения из этих переменных вычисляются буквально для каждой таски каждый раз перед выполнением таски.
Вооружённые этим знанием мы уже можем попытаться догадываться, что за фигня происходит в этом коде:
- name: Do not do this
file: path=hello.txt state=touch
loop: '{{ groups.all }}'
vars:
ansible_host: '{{ hostvars[item].ansible_host }}'
Этот ужас обычно невозможно объяснить начинающим. С правильным подходом к понятию "используется" он становится чуть меньшим ужасом (но всё ещё остаётся примером того, как не надо писать).
Что тут происходит?
- Переменная
ansible_host
всегда (в пределах этой таски) равна строке{{ hostvars[item].ansible_host }}
. Ноль анализа содержимого. Если это валидный yaml, то годится. - Выполняется цикл
loop
. Цикл использует значение{{ groups.all }}
. Это значение — jinja. Производится интерполяция строки, он и превращается в список имён хостов. loop читает этот список, выполняет таску нужное количество раз, выставляя для каждого запуска таски переменную item в значение "следующий элемент из списка". - Модуль использует значения
hello.txt
иtouch
. Ввнутри нет jinja, так что значения остаются просто сами собой. - Перед каждым вызовом модуля
file
значение переменнойansible_host
используется для вычисления буквального параметра для транспорта ssh — куда подключаться. И вот тут вот обнаруживается, что внутриansible_host
находится Jinja, и происходит интерполяция. Каждый раз, когда надо выполнить модуль (т.е. много раз в течение одного loop). Оказывается, что jinja ссылается на переменную item, а её значение разное на каждом проходе цикла (т.е. каждый раз item содержит в себе что-то другое). В результате вычисляется значение, которое используется для подключения для каждого нового прохода цикла. Итог? Извращённая делегация. Не делайте так, но понимайте как оно устроено.
Обратите внимание — этот принцип применим ко всем случаям. Например, переменная в инвентори может ссылаться на факт, который будет выставлен где-то далеко в недрах роли. Если значение из этой переменной не будет использовано до выполнения set_fact
, то всё ок (кроме здравого смысла), и такое Ансибл съедает без особых проблем.
Это же открывает возможности неявной параметризации. Так, например (и это уже почти best practice) можно задать в групповых переменных IP-адрес для фильтрации:
allow_access: '{{ ansible_default_ipv4.address }}'
При том, что у каждого хоста факты появляются уже в процессе выполнения setup (или gathering_facts), переменная allow_access мирно хранит в себе выражение Jinja до момента, пока не потребуется использовать это значение — и только тогда Ансибл пошевелится выяснить, что же это за переменная такая и чему она равна.
Jinja
Второй важный аспект работы Ансибла — это использование Jinja (языка шаблонизации). В подавляющем большинстве случаев все строковые значения, которые используются, проходят через механизм интерполяции (шаблонизации), за вычетом нескольких мест (см ниже). Интерполяция выполняется следующим образом: в каком-то месте ожидается использование строки. Перед использованием строка обрабатывается шаблонизатором. Если шаблонизатор что-то поменял, что получившаяся строка передаётся в шаблонизатор ещё раз. И до тех пор, пока шаблонизатор не перестанет менять строку. После этого строка передаётся в то место, которое его использует (параметр модуля, магическая переменная для запуска модуля и т.д.). Другими словами, шаблонизатор применяет Jinja рекурсивно, пока есть что менять.
Рассмотрим простой пример:
- debug:
msg: '{{ message }}'
vars:
foo: 'foo'
foobar: '{{ foo + "bar" }}'
message: 'This is {{ foobar }}'
В момент, когда строковое значение передаётся как параметр 'msg' в модуль 'debug' (это не модуль, а action plugin, но для наших целей это не важно), строка {{ message }}
прогоняется через шаблонизатор. Тот видит усы ({{
и }}
) и заменяет их содержимым переменной message. Получается значение This is {{foobar }}
. Дальше это значение прогоняется через шаблонизатор ещё раз получается This is {{ foo + "bar" }}
. После ещё одного раунда тест становится This is foobar
. После ещё одного раудна интерполяции текст не поменялся, т.е. интерполяция закончена. Получившаяся строка уходит в параметр msg модулю debug.
Въедливый читатель может сказать, что на самом деле это рекурсивная интерполяция, а не циклическая, но с точки зрения рассказа это влияет только на проблемы бесконечной рекурсии.
Теперь поговорим про большое WTF, которое подстерегает любого "программиста на ямле".
- hosts: localhost
tasks:
- debug: msg={{foo}}
vars:
foo: ''{{ foo + 1 }}'
vars:
foo: 1
Мне пришлось писать полную play, потому что мне надо было "задать" foo в двух местах. Сначала на уровне play, потом на уровне таски. Что происходит в debug? Он ожидает в msg строку. Эта строка проходит через шаблонизатор, который делает строку {{ foo + 1 }}
, потом {{ foo + 1 }} + 1
и т.д. Почти мговенно ансибл достигает глубины рекурсии и говорит, что дальше нельзя.
А теперь смотрите на эту магию:
- hosts: localhost
tasks:
- set_fact:
foo: '{{ foo + 1 }}'
- debug: msg={{foo}}
vars:
foo: 1
Этот код выведет число "два"? Почему? Потому что set_fact
-это модуль и он использует переменную foo. А ещё он создаёт более приоритетную переменную foo, которая потом и используется. Этот код куда более трудный для понимания, чем кажется, потому что старое foo (со значением 1) никуда не исчезает, просто у новой foo из фактов приоритет выше. Но, об этом следующем разделе.
Пока что я хочу дать вам интуицию, что как только строка попадает в параметры модуля, она используется. И именно в этот момент происходит интерполяция.
Разберём ещё один пример на развитие интуции.
- hosts: localhost
vars:
foo: '{{ bar * 2 }}'
tasks:
- debug: var=foo
loop: [1,2,3]
vars:
bar: '{{ item + 10 }}'
Я специально написал vars для play наверху (иногда так делают для наглядности). Но ещё это докидывает WTF для человека, который не до конца уверен в происходящем.
Что тут случается? Мы делаем итерацию по списку из чисел 1, 2, 3. У нас в таске присутствуют переменные:
foo
со значением '{{ bar * 2 }}'
и bar
со значением '{{ item + 10 }}'
. Обратите внимание, что на этапе vars (как на уровне play, так и на уровне таски), всем (включая анисбл) пофигу, что в этих переменных. Строка и строка. Совершенно не важно, что они "рядом", что они задаются в каком-то специальном порядке.
Важными эти переменные становятся только в момент выполнения модуля debug. Значение foo рекурсивно разворачивается в {{ bar *2 }}
, которое разворачивается в {{ (item + 10) * 2 }}
. Итоговое значение которое в зависимости от item (меняется loop'ом) становится 22, потом 24, потом 26.
Переменные могут приходить из разных мест — переменные инвентори, переменные групп, дефолты ролей, факты, переменные таск и плейбук и т.д. За вычетом scope/precedence (см ниже), откуда бы ни пришла переменная, она будет подставляться в строку в момент использования строки.
Вот вам ещё один смешной пример:
- hosts: localhost
vars:
foo: '{{ bar }}'
tasks:
- debug: var=foo
vars:
bar: 'one value'
- debug: var=foo
vars:
bar: 'another value'
С учётом вышесказанного точно понятно что происходит. Значение строки используется в момент вызова модуля, происходит рекурсивная интерполяция, в ходе интерполяции используются разные значения bar для первой и второй таски. Mystery solved.
Блочный Jinja
Так как все значения параметров для модулей проходят интерполяцию, то шаблоны (точнее, jinja) можно использовать в любых параметрах любых модулей. Можно использовать любую jinja. Не только {{ такую }}
, но и {% if True %} "такую" {%endif %}
. Что открывает возможность иногда сильно упростить код. В сочетании с многострочными строками yaml это даже читаемо.
- foo_module:
username: <
{% for user in myusers %}
{% if user.lower() in good and user.upper() in other %}
{{ user }}
{% endif %}
{% endfor %}
Ровно тот же метод используется в 'content' для модуля file
. На этот раз пример просто копипаст из одного из моих продакшенов:
- name: Configure sfcapd systemd service
become: true
copy:
content: |
[Unit]
Description=sflow capture service
[Service]
Type=simple
ExecStart=/usr/bin/sfcapd sfcapd -w -T{{ sflow_extensions }} -p {{ sflow_port }} -B {{ sflow_buffer_size }} -l {{ sflow_data_path }} -b {{ sflow_host }}
Restart=always
[Install]
WantedBy=multi-user.target
dest: /etc/systemd/system/sfcapd.service
notify:
- restart sfcapd
Что мы тут получаем? Экономию человеческого времени на беготне между 100500 файлов в разных каталогах. Файл маленький, тривиальный. Можно template
, можно copy
с content
. Что читаемее, то используем.
Мы почти разобрались со значениями. Осталось три вопроса.
Если вам в какой-то момент времени нужно иметь строку с усами (например, это шаблон prometheus или любого другого приложения на Go), то вы можете запретить интерполяцию строки. Это делается с использованием смеси чёрной yaml-магии и чёрной магии Ансибла.
В Ansible есть два типа строк: safe и unsafe. safe-строки — это обычные строки, которые интерполируются. unsafe строки оставляются как есть и шаблонизатор никогда к ним не прикасается.
Например:
- debug:
msg: !unsafe'{{ inventory_hostname }}'
Выведет на экран строку "как есть", то есть {{ inventory_hostname }}
.
Когда я узнал про эту фичу, в моей жизни исчезла большая боль и неудобство.
Второе — когда значения строк интерполируются, а когда нет? Я думал писать список таких случаев, а потом понял, что действует одно простое правило: не интерполируются ключи словарей на любом уровне вложенности ямла. Нельзя использовать шаблоны в именах модулей, ключах словарей в переменных, именах переменных (которые по сути те же ключи).
Типы
Я старательно обходил этот вопрос так долго, как можно, потому что теперь из милых странностей Ансибла мы погружаемся в бездны оппортунистической типа-типизации, после которых PHP начинает казаться вершиной теории типов.
Давайте начнём с боевого примера.
---
- hosts: localhost
gather_facts: false
tasks:
- name: Case1
debug: var=item
loop: '{{ [1,2,3,4] }}'
- name: Case2
debug: var=item
loop: '{{ foo + bar }}'
- name: Case3
debug: var=item
loop: ' {{ [9,10] }}'
vars:
foo: '[5,6'
bar: '7,8]'
Case1 выглядит как слегка извращённая Jinja без особенностей. Ну да, внутри jinja2 список, мы его выводим. Могли бы так же написать и без усов, а с использованием списков yaml'а.
Case2 должен вызвать первый вопрос от типонаци (типонаци — это как граммарнаци, но с любовью к строгой типизации) — мы взяли две строки, объединили их, но почему-то считаем, что это список. Запахло PHP.
Case3 должен вызвать духовное удовлетворение у типонаци — Ансибл ругается на ошибку, потому что на входе строка вместо списка.
Но комбинация Case2 и Case3 — это то место, где Ансибл Ужасен с большой буквы. Суровая страшная правда жизни состоит в том, что всё, что приходит из переменных Ансибла является строкой в начале. А в конце (перед началом использования) Ансибл пробует это прочитать как json. И если ему удаётся, то он интерпретирует его как получившийся тип (из json'а).
А вот эти два кейса можно считать точкой завершения разумного и предсказуемого в Ansible:
- name: Case6, space at the end
debug: var=item
loop: '{{ [15, 16] }} '
- name: Case7, space at the start
debug: var=item
loop: ' {{ [15, 16] }}'
Case6 работает, потому что в loop список (не смотря на пробел в конце), а Case7 не работает, потому что у него пробел в начале. Почему? Потому. Жри что дали.
… Список таких неочевидных вещей очень большой. Наверное, я потом напишу отдельный пост, состоящий только из WTF'ных автотипизаций ансибла.
Байка: пока я писал этот кусочек, я экспериментировал с разными микросниппетами Ансибла. 5 человек, пищущие на Анисбле для каждого сниппета выдавали разные прогнозы, и ни разу никто не угадал что произойдёт. Я сохранил эти сниппеты для отдельной статьи "хаха, это ансибл". Но поверьте мне, типо-типизация — это жуть.
Основное, что нужно запомнить из этого раздела: настоящими типами обладают только буквальные значения yaml, всё остальное проходит через оппортунистическую типизацию.
Scope переменных и "слои" переменных
У переменных Ансибла есть срок жизни. В отличие от типа-"типизации", scope вполне понятен и чаще всего не вызывает вопросов. Перменные Ансибла живут:
- Вечно, если они заданы в inventory и group_vars.
- До окончания play, если заданы в ролях или переменных play. (Вот зачем важно знать, что такое "play" — без этого вы не знаете, как долго ваши переменные существуют).
- До окончания task, если это переменные таски.
- До окончания плейбуки, если это факт. (именно этим отличаются переменные и факты — факты живут дольше).
Я игнорирую модуль include_role
в своих рассказах, потому что взаимоотношение сроков жизни и приоритетов переменных для include_role и всего остального настолько запутанно (и меняется от версии к версии), что можно сказать только одно — не используйте include_role. Да и вообще, include любого типа.
Scope на самом деле независимы — переменные в одном scope не заменяют переменные другого scope:
---
- hosts: localhost
gather_facts: false
vars:
foo: 2
tasks:
- name: Case1
debug: var=foo
vars:
foo: 1
- name: Case2
debug: var=foo
Пока действуют переменные уровня play, вы видите foo=1, как только scope закончился, вы начинаете видеть foo из scope play, где foo равно 2. (Про precedence мы говорим чуть ниже). В нормальном режиме переменные не "появляются" и не "исчезают", они декларативно заданы (и известны в своём неинтерпретированном состоянии до начала выполнения плейбуки). Есть несколько источников "динамических" переменных — include_vars
, set_fact
, register
и т.д.
Variable precedence
Это источник невероятных приключений и веселья. Больше веселья доставляет только include_role
и типа-"типизация". У Ансибла, если в каком-то месте доступны переменные из нескольких scope, существует набор правил, какие переменные будут использоваться. Эти правила называются variable precedence и это одна из самых плохо сделанных частей Ансибла (что признают даже его разработчики — но исправлять они не могут, потому что любое прикосновение к этой области сломает самым непонятным образом уже написанные плейбуки).
Изначально тезис звучал просто: переменные, приходящие из разных источников имеют разный приоритет. Например, если в инвентори сказано, что http_port: 8088
, то это приоритетнее, чем http_port: 80
в дефолтах роли. Переменные из комадной строки ансибла -e
самые приоритетные и т.д. И всё было бы просто, если бы не путаница с тем, чьи групповые перменные находятся в group_vars/
.
Предположим, у вас есть проект, и в каждом файле есть переменная foo
inventory.yaml # [all:vars] foo=inventory
group_vars/all.yaml # foo: g_v_a
playbook.yaml
У вас есть внутри playbook.yaml таска
- debug: var=foo
Что она выведет?
… Совершенно не важно, сколько вы будете зубрить variable precedence из документации Анисбла, потому что вы не знаете, что такое group_vars тут.
Чтобы разобраться со всем этим, давайте обсудим одну малоизвестную, но критически важную деталь: плагин host_group_vars
дока.
host_group_vars
В определённые моменты времени (из которых нас интересует момент загрузки инвентори и момент чтения плейбуки) Ансибл запускает плагин host_group_vars. Плагин смотрит в файловую систему в поисках файлов, в которых могут быть group vars или host vars. Он это делает в контексте сущности, для которой делается загрузка. Если group_vars/all.yaml читается в контексте инвентори, то получается переменная инвентори. Если в контексте playbook, получается переменная playbook.
Таким образом, если инвентори и плейбука лежат в одном каталоге, то group_vars читаются ими обоими. Но побеждает тот, кому полагается по variable precedence (playbook), так что содержимое внешних group_vars победит.
Но это ещё не всё. У вас одни и те же group_vars могут быть как снаружи, так и внутри файла инвентори. В этом случае побеждает тот, кто прочитан последний. Сначала читается инвентори, потом данные плагина для этой инвентори, потом следующая инвентори, потом данные плагина второй инвентори и т.д. Потом читаются переменные плейбуки.
На всё это накладывается черезполосица разных приоритетов group_vars/other, group_vars/all, host_vars и т.д. Попытка это всё удержать в голове обычно заканчивается либо ложной картиной мира, либо отчаянием.
Ниже я попробую выписать элементы здравомыслия, которые сложились в моей практике по этому вопросу, но за их пределами — тут нет красивого решения и всюду неожиданности.
При этом приходится писать много переменных в инвентори. Чтобы не провоцировать precedence WTF, есть такое правило: никогда не создавать перекрытие по именам между групповыми переменными (инвентори и плейбук), никогда не создавать перекрытия между hostvars между инвентори и плейбукой.
Разумная таблица приоритетов
Примерно такая же, как тут, но без WTF'ов.
- role/defaults — всегда проигрывает. Идеальное место для размещения переменных, которые надо переопределять. Дефолты ролей видны в соответствующих pre/post tasks.
- group_vars/all — переменные группы all наименее приоритетные
- group_vars/other_groups
- host_vars
- результат выполнения gather_facts: true (host_facts)
- Переменные уровня play
- Переменные уровня block
- Переменные уровня task
- set_fact/register.
set_fact
ещё и живёт дольше, чем плей (т.е. может передавать данные между разными play). - -e побеждает всех и всегда.
Обратите внимание, что некоторые элементы пропущены (не используйте их). Для каждого уровня запрещены конфликты и "нюансы". Результат куда проще для запоминания и осознания и предсказания.
Пример конфликта: одна и та же переменная в group_vars/all
внутри файла инвентори и в файле group_vars/other_group.yaml
.
Динамические чудеса, которые нам в гит попушил матёрый чудак
Существует множество механизмов для надругательства над переменными, которые не вошли в эти статьи. Несколько видов include'ов (переменных, ролей, тасок), динамические инвентори, возможность делать import из include'а, add_host
, позволяющий создавать переменные разного типа на ходу и т.д. Вы можете делать set_fact из делегации. Вы можете создать кастомный модуль, который будет добавлять переменные под видом фактов. Вы можете выставлять переменные уровня jinja, живущие до окончания интерполяции куска строки. Вы можете подгрузить ещё один комплект group_host_vars (плагином) по ходу исполнения play. Вы можете всё это написать, но вы не хотите это ни сопровождать, ни читать.
Keep it clean, keep it simple.
acmnu
Ох, сколько боли за этой! Я могу сказать, что "Variable precedence" делает практически невозможным разработку больших объемов Ansible кода. Вместо этого механизма, я бы предпочел жесткую инкапсуляцию переменных, т.е. полную невозможность для ролей или модулей использовать переменные, кроме тех, что переданы непосредственно.
Можно делать квазиинкапсуляцию, банально запуская ansible-playbook бинарник внутри других плейбуков, но это на столько грязно и беспомощно, что аж материться не хочется, а только лечь, обнять колени и плакать.
amarao Автор
К сожалению, модель с инкапсуляцией не возможна в ansible. В нём предполагается, что оператор может сделать -e и поменять любую переменную. Решение спорное, мягко говоря, но это центральная идея всей модели переменных ансибла.
snp
Нет там никакой боли. Без этого механизма вы не сможете, например, задавать умолчания в роли (defaults/main.yml) и, при этом, сохранить возможность их переопеределять в отдельных случаях.
На самом деле, самая большая боль ансибла — это Jinja2. Если бы они взяли mako, было бы сильно лучше.
Это не бинарник, а код на питоне.
Зачем?
anonymous
Иногда не хватает возможности запустить роль A из ролей B и C с разными параметрами. Это позволило бы уменьшить дублирование, да и вообще сделало бы ансибл более интуитивным для людей привыкших программировать.
amarao Автор
Это интуитивно привычно для людей, которые привыкли программировать. Эти люди приходят и начинают программировать на Ансибле, Ансибл им устраивает итальянскую забастовку, они пишут невообразимое, а потом жалуются, что невообразимое ведёт себя невообразимо.
Ансибл — не язык программирования. Когда вы пишите программу на Ансибл, вы используете Ансибл неправильно. Никакие подсистемы Ансибла не предназначены для использования в качестве языков программирования, и в Ансибле "не хватает для программирования" не каких-то отдельных элементов, а всего.
anonymous
Да, ансибл не язык программирования, тут я с вами полностью согласен.
Вот есть задача — сделать несколько однотипных вещей отличающихся отдельными элементами, например десяток конфигов в conf.d или скажем несколько папочек со стандартной структурой. Есть несколько вариантов:
1) написать десять почти одинаковых блоков — прямое дублирование, большой объем глупой ручной работы и ошибок в случае последующих изменений
2) include_role — получим все проблемы с приоритетами
3) модуль — сложно, долго и не очень понятно зачем тогда ансибл нужен, если все равно программируем
как правильнее всего решать задачу?
snp
Я обычно делаю по «2». Никаких проблем с приоритетами нет, на самом деле. Автор статьи преувеличивает.
Модули писать — это правильное решение. И тесты к ним обязательно.
Вопрос — в уровнях абстракции. Например, вот есть модуль
mongodb_replicaset
, хотя можно было тот же самый функционал и на yaml написать, с определёнными костылями. Но тут мы получаем законченный модуль, который делает всё, что нужно и не требуется в него внутрь лезть.amarao Автор
Переделать входные данные. Каждое действие идёт по списку и делает его по item. Три таски, каждая из которых работает со своим loop.
Главная ошибка, которую делают при разработке в Ансибл, фиксируют формат данных на входе (инвентори/груп-варз) и пытаются трансформировать их (или подстроить код с loop/include) под эти данные. Этот подход работает в программировании и не работает в анисбл.
Вместо этого вы пишите простой код и смотрите, какой формат данных под этот код хорошо подойдёт. Дальше либо у вас всё совсем просто, либо вам нужна программа (на языке программирования), чтобы трансформировать данные из неудобного для Ансибл формата в удобный.
snp
include_role
же есть.Программную часть надо выносить в кастомные модули/фильтры/lookup. У ансибла — декларативный (в целом, но не полностью) подход. С опытом понимание приходит.
amarao Автор
Вот
include_role
вам обещает приключения невероятного масштаба, потому что у include_role другие наборы приоритетов, которые не совпадают при приоритетами для обычной роли. Я могу допустить существование include_tasks, потому что очень нужно цикл для блока, но include_role — это реальное программирование ненадлежащими средствами.snp
Например, есть набор рутинных тасков. Ну конкретнее — добавляет набор из 1 файла и 1 шаблона. Этих наборов может быть несколько, вызывается из разных ролей, передаются разные значения. Очень отдалённое подобие LWRP из шефа (если знакомо).
Можно написать модуль для этого, но относительно долго и получаем кучу питонокода против 3 ансиблотасков (создать диру, положить файл, положить шаблон).
Есть другие идеи как это сделать лучше?
amarao Автор
Добавить файл: 1 таска. Добавить шаблон — одна таска. Надо — написали. Не надо — не написали. Всё же просто. И так в каждом месте, где вам надо файл и таску.
… Зачем в этом месте программировать микрофреймворк на ненадлежащем инструменте, когда всех дел — две таски?
snp
Ну это тупой копипаст одного и того же. А как же DRY?
Если авторы ансибла, добавили
include_role
, то они считают, что это полезно.amarao Автор
DRY касается не каждой строчки, которую вы написали (сто пятьсот вызовов роли — это вам не нарушение DRY?), а бизнес-логики, данных и алгоритмов. Две таски — ни то и не другое, так что лучше их повторить. Вам будет проще писать код, всем окружащим — проще его читать.
Если вы хотите формализм — include_role находится в статусе preview и к использованию в продакшене не должен допускаться. Феерические взрывы в нём на границе 2.4-2.7 должны были хорошо проиллюстрировать, что такое 'preview'.
snp
И что там взрывалось? У меня всё отлично работало, с момента его появление. И это не «ошибка выжившего». Это означает, что его можно использовать, если понимать, как.
amarao Автор
Ну, поскольку я с вами разговариваю, я тоже "выжил". Но, например, вот этот код:
В 2.4 role foo выполнялась на хосте other_host, а теперь начала выполняться на текущем хосте. Догадайтесь, насколько всё взорвалось при наличии такой конструкции в коде и смены того, где роль исполняется.
Если вы прошли по минному полю и ни разу не наступили на регрессию, ура.
А есть те, кто наступили. https://github.com/ansible/ansible/issues?q=is%3Aissue+is%3Aopen+include_role+label%3Abug
snp
Ну вот я не вижу смысла инклюдить всю роль целиком без указания конкретного таска. Я бы так ни за что не сделал. Потому что такое выносится в play, там ему и место.
Если
include_role
с заданным таском (как в моих комментах выше), то этот таск пишется с расчётом, что его будут инклюдить из других ролей. Поэтому с переменными аккуратно обращаюсь.Ну и опять же, я не топлю за
include_role
повсюду — по опыту, такая необходимость возникает редко. Например, для добавления кастомных apt репозиториев.apt_repo
не годится, т.к. не умеет в deb822 добавлять. Но когда будет время, я всё же модуль напишу, может даже PR в апстрим сделаю.И когда модуль будет готов, то все
include_role
удобно заменятся на модуль.amarao Автор
В апстрим уже не сделаете, а сделать коллекцию с модулем — это всегда пожалуйста.
Это лучше, чем include_role.
snp
Почему?
amarao Автор
В 2.10 все модули (кроме нужных для работы самого ансибла, типа debug) вынесли во внешние коллекции (community.general и т.д.), так что заслать модуль в апстрим анисбла больше не опция.
anonymous
А что делать, если надо сделать 100 почти одинаковых наборов действий, например создать десяток каталогов и заполнить их файлами из темплейтов с разными параметрами?
amarao Автор
Если же вы хотите сделать так, чтобы у вас было 100500 ролей, но каждая роль дёргала бы общую роль, которая бы предоставляла универсальный API для выполнения почти похожих действий, контролируемых при помощи передаваемых аргументов в функцию; плюс поддерживала бы наследование от базового класса и полиморфизм с декораторами, то вы хотите не Ансибл, а язык программирования.
И до тех пор, пока вы не поверите в утверждение, что Анисбл — не язык программирования, вы будете пытаться на нём программировать, обжигаться, ругаться и чувствовать большое страдание.
anonymous
Я верю, что ansible не язык программирования. Теперь мне нужно найти разумное решение для задачи, когда нужно создать несколько директорий %main_dir% с контентом:
%main_dir%/
------ b/foo.conf
------ c/bar.conf
------- /foobar.conf
где каждый файл конфига генерируется со своими параметрами для каждой из %main_dir%.
amarao Автор
Я не до конца понимаю "свои параметры", но выглядит это так:
name и прочие стили можете дописать сами.
Это лучше, чем include. А ещё лучше не сношать мозг и написать столько тасок, сколько надо выполнить, потому что вы получите халявные name для того, чтобы подсказать читающему, нафига вы это делаете.
anonymous
Поясню, я хотел бы иметь указанную выше структуру в каждой из папок:
* /srv/test, при этом чтобы в темплейтах поле user имедо значение test, а password 'testpass';
* /srv/staging c user=staging и password='stagingPassword';
* /srv/prod с user=admin, password='prodP@sswo0rd'.
При этом мне не требуется каждый раз писать отдельный комментарий для файла foo.conf, вместо этого мне бы хотелось иметь возможность добавить файл newfoo.com один раз, но чтобы он появился правильно заполненным в каждой из папок (test, staging, prod), и возможность добавить новое окружение (например preprod) со своими параметрами.
amarao Автор
Я уже объяснил принцип, детали реализации, как и глубокое понимание зачем вам так надо хотеть — это уже в ваших руках.
Код с include — это code smell. Ещё не совсем фатально, но требует пристального внимания. Как
unsafe{}
в Rust.anonymous
Да, возможно это code smell, но кажется это единственное, что решает проблему. То, что вы предложили на мой взгляд не решает и очень трудозатратно в поддержке, но в любом случае спасибо за интересную дискуссию.