Основы Ansible, без которых ваши плейбуки — комок слипшихся макарон, часть 3

  • Tutorial

В этой части мы перестаём говорить о простом и приятном и начинаем говорить о трудном. Переменные в Ansible: scope, precedence, рекурсивная интерполяция. Для тех, кто дочитает до конца, маленький бонус: упрощённая таблица приоритетов, с которой можно жить. Предыдущие части: 1, 2.


Обычно рассказ про переменные в Ансибл начинают с чего-то очень простенького, что создаёт у читателя иллюзию, что переменные в Ансибл — это как в любом другом языке программирования. Мутабельные или немутабельные, локальные и глобальные. Это не так.


Это не так.


У Ансибла возникла уникальная модель переменных (модель памяти?), которую надо учить с нуля. И рассматривать мы её начнём с того места, где значения используются (обычно переменные Ансибла рассматривают с того места, откуда они появляются). Почему? Потому что при рассказе в этом направлении у нас образуется направленный граф, который куда легче уложить в голову.


Обратите внимание — я сказал "значения", потому что "переменные" — это всего лишь имена к значениям. У перменных свой глубокий внутренний мир, и про него во второй части.


В рамках этого рассказа я буду использовать понятия "появляется" и /"используется". Появляется — это то место, где значение было задано. А "попадает" — это то место, где значение начинает влиять на работу Ансибла, точнее, на его сайд-эффекты — на реальный процесс исполнения модулей в целевой системе. Пока переменные перекладываются из места на место в секции vars, значения никуда не "попадают" и на окружающий мир никак не влияют.


Это первая важная мысль, которую надо помнить в голове: пока значение не было использовано чем-то, влияющим на окружающий мир, оно может содержать в себе любые синтаксические ошибки и ссылки на несуществующие переменные и т.д., и это никого не смутит. Почему так — читайте дальше.


Итак, где значения используются?


  1. В параметрах к модулю. - copy: src=foo.conf dest=/etc/foo.conf. В этом примере foo.conf и /etc/foo.conf — значения. Они записываются в параметры модуля в целевой системе. В процессе записи, всё, что было в значении, приводится в буквальную форму. Например, на этом этапе вычисляется jinja, числа/строки приводятся к тому формату, который ожидает от них модуль (например, если модуль ожидает строку, а вы передали число, именно на этом этапе Ансибл выдаст предупреждение, что число было сконвертировано в строку).
  2. При выполнении модуля (на самом деле это action plugin) 'template' или соответствующего lookup plugin'а по имени template. Они используют значения из переменных в шаблоне в момент своего исполнения (для lookup plugin когда этот момент наступит совсем не очевидно, потому произойдёт это только когда интерполируется значение, упоминающее этот плагин).
  3. В параметрах play. Любые значения в параметрах play (не путать с переменными уровня play) будут использованы буквально в момент начала play. Например, если у вас jinja2 в gather_facts, то она будет вычислена именно в момент начала выполнения play. Аналогично — hosts, remote_user и другие.
  4. Магические переменные Ансибла, которые влияют на процесс исполнения модуля. В основном, это 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 }}'

Этот ужас обычно невозможно объяснить начинающим. С правильным подходом к понятию "используется" он становится чуть меньшим ужасом (но всё ещё остаётся примером того, как не надо писать).


Что тут происходит?


  1. Переменная ansible_host всегда (в пределах этой таски) равна строке {{ hostvars[item].ansible_host }}. Ноль анализа содержимого. Если это валидный yaml, то годится.
  2. Выполняется цикл loop. Цикл использует значение {{ groups.all }}. Это значение — jinja. Производится интерполяция строки, он и превращается в список имён хостов. loop читает этот список, выполняет таску нужное количество раз, выставляя для каждого запуска таски переменную item в значение "следующий элемент из списка".
  3. Модуль использует значения hello.txt и touch. Ввнутри нет jinja, так что значения остаются просто сами собой.
  4. Перед каждым вызовом модуля 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 вполне понятен и чаще всего не вызывает вопросов. Перменные Ансибла живут:


  1. Вечно, если они заданы в inventory и group_vars.
  2. До окончания play, если заданы в ролях или переменных play. (Вот зачем важно знать, что такое "play" — без этого вы не знаете, как долго ваши переменные существуют).
  3. До окончания task, если это переменные таски.
  4. До окончания плейбуки, если это факт. (именно этим отличаются переменные и факты — факты живут дольше).

Я игнорирую модуль 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.

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 71

    0

    Ох, сколько боли за этой! Я могу сказать, что "Variable precedence" делает практически невозможным разработку больших объемов Ansible кода. Вместо этого механизма, я бы предпочел жесткую инкапсуляцию переменных, т.е. полную невозможность для ролей или модулей использовать переменные, кроме тех, что переданы непосредственно.


    Можно делать квазиинкапсуляцию, банально запуская ansible-playbook бинарник внутри других плейбуков, но это на столько грязно и беспомощно, что аж материться не хочется, а только лечь, обнять колени и плакать.

      0

      К сожалению, модель с инкапсуляцией не возможна в ansible. В нём предполагается, что оператор может сделать -e и поменять любую переменную. Решение спорное, мягко говоря, но это центральная идея всей модели переменных ансибла.

        0

        Нет там никакой боли. Без этого механизма вы не сможете, например, задавать умолчания в роли (defaults/main.yml) и, при этом, сохранить возможность их переопеределять в отдельных случаях.


        На самом деле, самая большая боль ансибла — это Jinja2. Если бы они взяли mako, было бы сильно лучше.


        ansible-playbook бинарник

        Это не бинарник, а код на питоне.


        внутри других плейбуков

        Зачем?

          0
          Иногда не хватает возможности запустить роль A из ролей B и C с разными параметрами. Это позволило бы уменьшить дублирование, да и вообще сделало бы ансибл более интуитивным для людей привыкших программировать.
            +2

            Это интуитивно привычно для людей, которые привыкли программировать. Эти люди приходят и начинают программировать на Ансибле, Ансибл им устраивает итальянскую забастовку, они пишут невообразимое, а потом жалуются, что невообразимое ведёт себя невообразимо.


            Ансибл — не язык программирования. Когда вы пишите программу на Ансибл, вы используете Ансибл неправильно. Никакие подсистемы Ансибла не предназначены для использования в качестве языков программирования, и в Ансибле "не хватает для программирования" не каких-то отдельных элементов, а всего.

              0
              Да, ансибл не язык программирования, тут я с вами полностью согласен.

              Вот есть задача — сделать несколько однотипных вещей отличающихся отдельными элементами, например десяток конфигов в conf.d или скажем несколько папочек со стандартной структурой. Есть несколько вариантов:
              1) написать десять почти одинаковых блоков — прямое дублирование, большой объем глупой ручной работы и ошибок в случае последующих изменений
              2) include_role — получим все проблемы с приоритетами
              3) модуль — сложно, долго и не очень понятно зачем тогда ансибл нужен, если все равно программируем

              как правильнее всего решать задачу?
                0

                Я обычно делаю по «2». Никаких проблем с приоритетами нет, на самом деле. Автор статьи преувеличивает.


                модуль — сложно, долго и не очень понятно зачем тогда ансибл нужен

                Модули писать — это правильное решение. И тесты к ним обязательно.


                Вопрос — в уровнях абстракции. Например, вот есть модуль mongodb_replicaset, хотя можно было тот же самый функционал и на yaml написать, с определёнными костылями. Но тут мы получаем законченный модуль, который делает всё, что нужно и не требуется в него внутрь лезть.

                  +1

                  Переделать входные данные. Каждое действие идёт по списку и делает его по item. Три таски, каждая из которых работает со своим loop.


                  Главная ошибка, которую делают при разработке в Ансибл, фиксируют формат данных на входе (инвентори/груп-варз) и пытаются трансформировать их (или подстроить код с loop/include) под эти данные. Этот подход работает в программировании и не работает в анисбл.


                  Вместо этого вы пишите простой код и смотрите, какой формат данных под этот код хорошо подойдёт. Дальше либо у вас всё совсем просто, либо вам нужна программа (на языке программирования), чтобы трансформировать данные из неудобного для Ансибл формата в удобный.

                0
                запустить роль A из ролей B и C с разными параметрами

                include_role же есть.


                более интуитивным для людей привыкших программировать.

                Программную часть надо выносить в кастомные модули/фильтры/lookup. У ансибла — декларативный (в целом, но не полностью) подход. С опытом понимание приходит.

                  0

                  Вот include_role вам обещает приключения невероятного масштаба, потому что у include_role другие наборы приоритетов, которые не совпадают при приоритетами для обычной роли. Я могу допустить существование include_tasks, потому что очень нужно цикл для блока, но include_role — это реальное программирование ненадлежащими средствами.

                    0
                    include_role — это реальное программирование ненадлежащими средствами.

                    Например, есть набор рутинных тасков. Ну конкретнее — добавляет набор из 1 файла и 1 шаблона. Этих наборов может быть несколько, вызывается из разных ролей, передаются разные значения. Очень отдалённое подобие LWRP из шефа (если знакомо).


                    - include_role:
                        name: foo
                        tasks_from: fubar
                      vars:
                        foo__v_smth: "{{ baz__whatever }}"

                    Можно написать модуль для этого, но относительно долго и получаем кучу питонокода против 3 ансиблотасков (создать диру, положить файл, положить шаблон).


                    Есть другие идеи как это сделать лучше?

                      0

                      Добавить файл: 1 таска. Добавить шаблон — одна таска. Надо — написали. Не надо — не написали. Всё же просто. И так в каждом месте, где вам надо файл и таску.


                      … Зачем в этом месте программировать микрофреймворк на ненадлежащем инструменте, когда всех дел — две таски?

                        0

                        Ну это тупой копипаст одного и того же. А как же DRY?


                        Если авторы ансибла, добавили include_role, то они считают, что это полезно.

                          0

                          DRY касается не каждой строчки, которую вы написали (сто пятьсот вызовов роли — это вам не нарушение DRY?), а бизнес-логики, данных и алгоритмов. Две таски — ни то и не другое, так что лучше их повторить. Вам будет проще писать код, всем окружащим — проще его читать.


                          Если вы хотите формализм — include_role находится в статусе preview и к использованию в продакшене не должен допускаться. Феерические взрывы в нём на границе 2.4-2.7 должны были хорошо проиллюстрировать, что такое 'preview'.

                            0
                            Феерические взрывы в нём на границе 2.4-2.7 должны были хорошо проиллюстрировать, что такое 'preview'.

                            И что там взрывалось? У меня всё отлично работало, с момента его появление. И это не «ошибка выжившего». Это означает, что его можно использовать, если понимать, как.

                              0

                              Ну, поскольку я с вами разговариваю, я тоже "выжил". Но, например, вот этот код:


                              - include_role: role=foo
                                delegate_to: other_host

                              В 2.4 role foo выполнялась на хосте other_host, а теперь начала выполняться на текущем хосте. Догадайтесь, насколько всё взорвалось при наличии такой конструкции в коде и смены того, где роль исполняется.


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


                              А есть те, кто наступили. https://github.com/ansible/ansible/issues?q=is%3Aissue+is%3Aopen+include_role+label%3Abug

                                0

                                Ну вот я не вижу смысла инклюдить всю роль целиком без указания конкретного таска. Я бы так ни за что не сделал. Потому что такое выносится в play, там ему и место.


                                Если include_role с заданным таском (как в моих комментах выше), то этот таск пишется с расчётом, что его будут инклюдить из других ролей. Поэтому с переменными аккуратно обращаюсь.


                                Ну и опять же, я не топлю за include_role повсюду — по опыту, такая необходимость возникает редко. Например, для добавления кастомных apt репозиториев.


                                apt_repo не годится, т.к. не умеет в deb822 добавлять. Но когда будет время, я всё же модуль напишу, может даже PR в апстрим сделаю.


                                И когда модуль будет готов, то все include_role удобно заменятся на модуль.

                                  0

                                  В апстрим уже не сделаете, а сделать коллекцию с модулем — это всегда пожалуйста.


                                  Это лучше, чем include_role.

                                    0
                                    В апстрим уже не сделаете

                                    Почему?

                                      0

                                      В 2.10 все модули (кроме нужных для работы самого ансибла, типа debug) вынесли во внешние коллекции (community.general и т.д.), так что заслать модуль в апстрим анисбла больше не опция.

                          0
                          А что делать, если надо сделать 100 почти одинаковых наборов действий, например создать десяток каталогов и заполнить их файлами из темплейтов с разными параметрами?
                            0
                            - file:
                                path: '{{ item }}'
                              loop: '{{ directories_to_create }}'
                            - template:
                                src: '{{ item }}.j2'
                                dest: '/etc/{{ item }}'
                              loop:
                                - foo.conf
                                - bar.conf
                                - foobar.conf

                            Если же вы хотите сделать так, чтобы у вас было 100500 ролей, но каждая роль дёргала бы общую роль, которая бы предоставляла универсальный API для выполнения почти похожих действий, контролируемых при помощи передаваемых аргументов в функцию; плюс поддерживала бы наследование от базового класса и полиморфизм с декораторами, то вы хотите не Ансибл, а язык программирования.


                            И до тех пор, пока вы не поверите в утверждение, что Анисбл — не язык программирования, вы будете пытаться на нём программировать, обжигаться, ругаться и чувствовать большое страдание.

                              0
                              Я верю, что ansible не язык программирования. Теперь мне нужно найти разумное решение для задачи, когда нужно создать несколько директорий %main_dir% с контентом:
                              %main_dir%/
                              ------ b/foo.conf
                              ------ c/bar.conf
                              ------- /foobar.conf

                              где каждый файл конфига генерируется со своими параметрами для каждой из %main_dir%.
                                0

                                Я не до конца понимаю "свои параметры", но выглядит это так:


                                - hosts: somegroup
                                  vars:
                                     configs:
                                        - dir: b
                                          file: foo.conf
                                          template: foo.conf.j2
                                          config: foo.conf
                                        - dir: c
                                          file: bar.conf
                                          template: bar.conf.j2
                                          config: bar.conf
                                  tasks:
                                    - file:
                                         path: '{{"/".join([main,item.dir] }}'
                                         state: directory
                                      loop: '{{ configs }}'
                                    - template:
                                         src: '{{ item.template }}'
                                         dest: '{{ "/".join([main,item.dir,item.config] }}'
                                      loop: '{{ configs }}'

                                name и прочие стили можете дописать сами.


                                Это лучше, чем include. А ещё лучше не сношать мозг и написать столько тасок, сколько надо выполнить, потому что вы получите халявные name для того, чтобы подсказать читающему, нафига вы это делаете.

                                  0
                                  Поясню, я хотел бы иметь указанную выше структуру в каждой из папок:
                                  * /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) со своими параметрами.
                                    0

                                    Я уже объяснил принцип, детали реализации, как и глубокое понимание зачем вам так надо хотеть — это уже в ваших руках.


                                    Код с include — это code smell. Ещё не совсем фатально, но требует пристального внимания. Как unsafe{} в Rust.

                                      0
                                      Да, возможно это code smell, но кажется это единственное, что решает проблему. То, что вы предложили на мой взгляд не решает и очень трудозатратно в поддержке, но в любом случае спасибо за интересную дискуссию.
              0
              Статья прям про Боль. Скажите, а будет ли статья про best practices, использование которых позволило бы снизить боль до разумных пределов? Например соглашения об именовании, договоренностей о том где объявлять переменные или скажем стандартных приемов, позволяющих выяснить объявлена переменная или нет.
                +1

                Так это и есть статья про best practice. Пишите просто, пишите ясно, не пишите то, что нельзя написать просто и ясно. Конфликты по именам переменных — это настолько несущественная ерунда на фоне выкрутас, которые люди устраивают с include'ами и миллионом переопределений переменных из неожиданных мест, что ей можно пренебречь.


                Обычно люди начинают страдать, когда у них роль без инклюда сталкивается с самой собой с инклюдом (а приоритеты разные! Изоляции нет!). А проблема в том, что был include_role, а не в разных приоритетах.

                  +1
                  Мы в команде стараемся себе не позволять такого экстрима с инклудами как вы описываете, просто в силу того, что сложно. Но еще мы например пришли к ряду простых соглашений, которые немного упрощают жизнь, например, что все переменные ролей имеют префикс с названием роли или что мы используем `defaults` и не используем `vars` (в силу приоритетов). Хочется узнать есть ли у вас что-то похожее.
                    +1

                    У нас не монорепа на всея компанию, так что сложность у нас частично управляется партиционированием. Мы используем git vendor для общих плейбук (мы называем их facilities), таки как базовые настройки системы, мониторинг и инфраструктура для интеграционных тестов. Сейчас как раз в одной из facilities оказалось, что переупотребили group_vars на уровне playbook, из-за чего страдают пользователи инвентори. Рефакторим.


                    В целом, главное правило анисбла — keep it simple. Если не получается simple, никакие соглашения не помогут. До определённого уровня изоляция хорошо работает на уровне запусков ansible (опять партиционирование).


                    Как-то так. Рядом соседний отдел страдает с chef'ом, и я хочу сказать, что у них проблем Ансибла нет, зато есть массовые проблемы из-за того, что инфраструктуру пытаются сопровождать как программу, с проблемами сопровождения сложного софта со сложными зависимостями, и это жрёт время нереально.

                      0
                      У нас вместо монорепы много ролей, но пока не договорились бывало, что запуск вида `-e target_ip=...` неожиданно затрагивал более одной роли. -С и --diff конечно спасают, но люди склонны совершать ошибки.
                        0
                        запуск вида -e target_ip=... неожиданно затрагивал более одной роли

                        Жёстко договориться об именовании переменных. Например, я делаю так (с dunder между):


                        role__some_variable

                        А то, что общее на несколько ролей/хостов свести к минимуму и задокументировать.

                          0

                          Есть такое. Чаще всего проблему можно уменьшить за счёт использования нескольких плейбук.

                          0
                          Можете описать ваш механизм git vendor?
                  0

                  разрешите вопрос как эксперту?
                  Хотя возможно он относится не к переменным, а к инвентори (предыдущей статье), впрочем до этой статьи я их не особо различал


                  Есть у меня две таски два play:


                  1. запускает питон скрипт, который лезет в NMS, ищет неиспользуемые порты на всех коммутаторах и под каждый коммутатор создает host_vars со списком таких портов
                  2. заходит на каждый коммутатор и гасит все порты из списка

                  Так вот если объединить эти два плея в один плейбук, то он не работает:
                  1-й play наполняет инвентори как пложено, но 2-й play этот свежий инвентори не видит и ничего не делает.
                  Если запускать в разных плейбуках, то все работает как полагается
                  Похоже inventory вычитывается в самом начале плейбука и больше не перечитывается


                  Так вот вопросы:
                  Есть возможность как то сказать ансиблу чтоб он перечитал Inventory?
                  Допустимо ли создавать такие плейбуки или это bad-design без вариантов?


                  p.s. в итоге я на ansible забил и ушел на питон целиком

                    0

                    А зачем inventory меняется?


                    Нужно проходить по каждому коммутатору из инвентори — будет 1 play по коммутаторам из (условно) 2 тасков:


                    1) проверили порты
                    2) сделали с нужными портами что-то

                      0

                      он не меняется. его изначально нет.
                      есть только хостнэймы свичей в ./hosts и creds-ы в group_vars
                      host_vars заполняется первым таском, который для кажого свича ходит за инфой к стороннему сервису

                        +1

                        Первая таска, которая ходит за инфой к свитчу обычно называется 'setup' и она делает gather_facts. Вполне себе паттерн, хорошо работающий. Если это медленно, есть такая штука, как кеширование фактов.


                        А вот когда из нормального паттерна вы делаете самомодификацию инвентори, вот тут вот вам открывается цирк, и кони, и мотоциклисты на стенке.

                          0

                          ясно, спасибо
                          паттерн значит

                          +1

                          Ага, понял, ну это, в целом, не для ансибла задача. Т.к. нельзя на свиче запускать код. Т.е. каждый хост в inventory — это машинка, куда можно по SSH зайти и запустить там модуль.


                          Но если надо, то можно просто на localhost запускать. Т.е. будет что-то такое:


                          - hosts: localhost
                            gather_facts: false
                            vars:
                              switches:
                                - sw1
                                - sw2
                                - sw3
                            tasks:
                              - command: "check_ports {{ item }}"
                                loop: "{{ switches }}"
                                register: switch_ports_status
                          
                              - command: "manage_ports {{ item.whatever }}"
                                loop: "{{ switch_ports_status }}"
                            0

                            да для свичей код на локалхосте всегда
                            хотя некоторое оборудование уже начинает нативно поддерживать питонов и ансиблы на свичах. но пока экзотика


                            пример кода понятен разумеется
                            просто я не отличал до этого переменные через инвентори, факты и регистеред
                            потому и наворотил то, что наворотил
                            так то я register там тоже использую


                              tasks:
                                - name: cleanup old configurations
                                  local_action: file path="./tmp/{{ inventory_hostname }}.cfg" state=absent
                                  delegate_to: localhost
                            
                                - name: generate new configuruation
                                  template:
                                    src: ./template/hw-shut.j2
                                    dest: "./tmp/{{ inventory_hostname }}.cfg"
                                  when: 
                                    - interfaces is defined 
                                    - interfaces is not none
                                  register: task_1
                            
                                - name: merge the new configuration with existing one and save it
                                  napalm_install_config:
                                    provider: "{{ hwpv }}"
                                    config_file: "./tmp/{{ inventory_hostname }}.cfg"
                                    commit_changes: True
                                  when: task_1.changed

                            Хорошо что вопрос задал. Выступил лохом, конечно, но оно того стоило — разобрались.
                            Еще хорошо, что статью про мой тот велосипед забыл опубликовать на хабре, хотя она была готова еще год назад :)

                              0

                              Ох, ну совсем тяжко. Во-первых не идемпотентно. Во-вторых when.… changed называется handler. В третьих это надо было делать через setup и факты.


                              на самом деле надо было иметь просто модуль и/или юзать network_cli.

                                0
                                when.… changed называется handler

                                Вот не факт. В chef есть нотифаи :immediate, а в ансибле такого не завезли.


                                meta: flush_handlers тоже не всегда годится, т.к. нельзя отдельные хендлеры дёрнуть, только все разом в очереди.

                              0

                              А свитче не обязательно запускать код. Есть куча сетевых модулей.


                              А вот 'command' в таком месте — закопайте, пожалуйста. Не идемпотентно, транспорт на баше, etc.

                                0

                                Command это для примера, разумеется.


                                Не идемпотентно, транспорт на баше

                                Command не запускает шелл (shell запускает). А при желании, можно и запуск внешних скриптов сделать абсолютно идемпотентно.

                                  0

                                  Запуск скриптов можно сделать идемпотентно, но двухходовка query/command — грязный код. Иногда приходится, но грязно. А скрипты идемпотентными сами по себе вы не сделаете, потому что надо как-то репортить changed в Ansible, а у shell нет критериев для этого. Нормальный модуль просто отдаёт changed в выводе в конце, но если вы написали программу, которая так делает, то поздравляю, 80% модуля готово. Осталось написать приём параметров и положить в library/.

                                    0
                                    А скрипты идемпотентными сами по себе вы не сделаете, потому что надо как-то репортить changed в Ansible, а у shell нет критериев для этого.

                                    Да ладно. Есть changed_when. Типа такого:


                                    - command: foo
                                      register: foo_result
                                      changed_when: not foo_result.stdout|regex_search("\\n\s+No migrations to apply")
                                      notify: systemctl restart foo.target

                                    Выглядит не очень — согласен. Но всё там идемпотентно и оно работает. Причёсывать и писать модуль django_migrate — ну вот хз. По каждому чиху модуль пилить? Разве что если есть время и перфекционизм.

                          0
                          Можно делать add_host, но лучше использовать как вам выше описали.
                            0

                            Специально для таких приключенцев есть group_host_vars плагин, про который целый раздел написан. А в целом — запись в host_vars из плейбуки это как самомодифицирующийся код, хорошо для малавари и спортивного программирования. В хорошем коде быть такого не должно.


                            Используйте факты, они для этого и придуманы.

                              0

                              про group_host_vars написано, что мы его не захотим. Зачем же Вы его мне советуете?
                              Ну и странно (для меня) inventory относить к коду.
                              Я как то привык относиться к inventory как к данным.
                              И нет ничего необычного и "приключенческого" когда одна функция пишет какие то данные, скажем в БД, а другая функция в том же коде эти данные потом читает и использует.
                              про гетфактс спасибо, но это будет сложней реализовать чем запускать плеи раздельно.
                              благо необходимость запускать этот велосипед требуется не чаще раза в год.

                                +1

                                Записывая что-то в инвентори из плейбуки, которая с этой инвентори работает, вы делаете странное. Очень. Я посоветовал вам как этим странным жить дальше.


                                Ещё одна вещь, которую вы можете делать (не уверен, что это можно для коммутаторов) — можно иметь кастомные факты на целевом хосте (/etc/ansible/facts.d/).

                                  +1

                                  Исходя из моего опыта лучшее что можно сделать с ансиблом для коммутаторов и прочих сетевых железяк — это перестать использовать ансибл для работы с ними :)
                                  Тут возможно проблема не в самом ансибле (хотя вымораживает, что половина скриптов для версии 2.2 не работает в 2.9), а в качестве модулей под эти самые железяки написанные. Благо к 2020-му появилось множество самых разных интерфейсов и фреймворков для автоматизации моей деятельности, с более ожидаемыми "паттернами", нормальным дебагом и быстрее ансибла на порядки.


                                  Тем не менее я очень благодарен Вам за ваши статьи и ответы на мои глупые вопросы.

                                  +1
                                  Формально это данные, но они настолько сильно сильно влияют на ход выполнения вашей программы, что думать о последствиях изменений в них так же трудно как про код. Плюс ко всему у вас появляется еще весь комплекс проблем связанный с актуальностью и валидностью данных в вашем инвентори.
                                    0

                                    не ну комплекс проблем с актуальностью и валидностью я предусмотрел :)
                                    прям в доке к скрипту написал


                                    перед запуском скрипта выполнить команду: rm -rf host_vars/

                                    Ладно ладно я про паттерн понял уже. Это не ансибл странный, это у него паттерны

                                0
                                Генерация inventory в самом ansbile выглядит как экзотика, так что вполне возможно, что для вас проще будет пользоваться связкой python+fabric.
                                0
                                Нельзя использовать шаблоны в именах модулей, ключах словарей в переменных, именах переменных (которые по сути те же ключи).

                                Позвольте с этим не согласиться. Вот так работает. Но не уверен что стоит использовать.


                                - set_fact:
                                    var_{{ item.index }}: "{{ item.value }}"
                                  loop:
                                    - index: x1
                                      value: 'aaa'
                                    - index: x2
                                      value: 'bbb'
                                - debug: var=var_x1
                                - debug: var=var_x2

                                Может быть потому что факты — это переменные другого порядка :)


                                role/vars/ же используем для OS-dependent и для перевода человекочитаемых переменных в ansible-читаемые. типа предварительной фильтрации единого списка на два в зависимости от атрибута enabled/disabled


                                А вот зачем использовать vars: для отдельных тасков — это для меня совсем непонятно.

                                  0

                                  С праздником, однако!
                                  Спасибо, годный материал, обязательно поковыряю в этом направлении.
                                  Ну а в честь праздника предложения:


                                  • play ансибловый — это русская плюха. Нарисовал такой плюху, и, хрясь ей по серваку/группе/всем узлам! Без тестирования. :) :D
                                  • playbook, соответственно — набор плюх. А для граммарнаци и адептов английских терминов — плюхбук. :)
                                    0
                                    Что же использовать вместо include_role? meta?
                                      0
                                      1. Переносить код в playbook'и. Если вам надо в пяти местах роль foo, вы в пяти местах указываете роль foo в списке ролей. Самый трушный путь.
                                      2. Если вы делаете include_role в цикле, то а) вы делаете очень вонючий код, б) чаще всего это можно переписать на отдельные циклы для одной таски в) аварийно есть include_tasks (который почти как include_role, но не тащит с собой прелюбопытнейшие дефолты include'женой роли).
                                      3. И как всегда, рефакторинг надо начинать со структуры входных данных. 90% случаев когда люди хотят странного, проистекает от того, что они принимают существующие структуры (инвентри и group_vars) как константу и хотят "дописать" не меняя существующее.
                                        0
                                        Я подключаю через include_role небольшие роли типа заббикс например. Если делать это в команде через плейбуки, то нужно явно знать какую роль подключать (если есть роль nginx, то нужно также подключить роль zabbix.nginx) или просто не знают об этом или забывают указывать.
                                          0

                                          Ну, изоляцию вы не напишите, а для проблемы, про которую вы говорите, мы просто делаем переиспользуемые плейбуки. Т.е. наш "common_monitoring" ставит всё, что нужно. Без дополнительных ролей, и на хостах, на которых нужно.


                                          Мы сами себе тут практику изобрели, но она не общепринятая. С интересом жду момента, когда galaxy станет более вменяемым для вендоринга, тогда можно будет плейбуки в коллекциях раздавать.


                                          Когда вы из одной роли вызываете другую, то это уже code smell. Список ролей задаёт play, а не роль.

                                      0
                                      В момент, когда строковое значение передаётся как параметр 'msg' в модуль 'debug' (это не модуль, а action plugin, но для наших целей это не важно), строка {{ message }} прогоняется через шаблонизатор. Тот видит усы ({{ и }}) и заменяет их содержимым переменной message. Получается значение This is {{foobar }}. Дальше это значение прогоняется через шаблонизатор ещё раз получается This is {{ foo + "bar" }}. После ещё одного раунда тест становится This is foobar. После ещё одного раудна интерполяции текст не поменялся, т.е. интерполяция закончена. Получившаяся строка уходит в параметр msg модулю debug.

                                      Что-то здесь не так. Я попробовал написать такое:


                                      - debug:
                                           msg: '{{ message }}'
                                        vars:
                                          three_plus: 3 +
                                          message: '{% raw %}{{ {% endraw %}{{ three_plus }} 2 }}'

                                      Согласно описанной в посте логике, значение message должно прогнаться через шаблонизатор ещё раз и в итоге выдать 5, но на практике получаем "msg": "{{ 3 + 2 }}" и никто ни на какие усы внимания не обращает.


                                      Слишком доигрались с упрощёнными объяснениями?)


                                      Или даже можно упростить пример:


                                      - debug:
                                           msg: '{{ message }}'
                                        vars:
                                          message: '{% raw %}{{ 3 + 2 }}{% endraw %}'

                                      Второго прогона шаблонизатора не случается и пятёрки тоже не случается.

                                        0

                                        raw возвращает unsafe-строки, unsafe-строки не прогоняются через шаблонизатор ещё раз.


                                        Рекурсивная jinja — это не упрощение, оно так внутри устроено.

                                          0

                                          Ну тогда вот так:


                                          - debug:
                                               msg: '{{ message }}'
                                            vars:
                                              ус1: "{"
                                              ус2: "{"
                                              message: '{{ ус1 }}{{ ус2 }} 3 + 2 }}'

                                          Здесь тоже кто-то возвращает unsafe-строку?


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

                                            0

                                            Нет, и вы показали любопытный пример, который я не мог разгадать и который вполне достоин ansible-bingo (утащил).


                                            Для сравнения:


                                                - debug:
                                                    var: '{{ message }}'
                                                  vars:
                                                    ус1: "{"
                                                    ус2: "{"
                                                    message: '{{ ус1 }}{{ ус2 }} 3 + 2 }}'
                                              0

                                              О, здесь бритва Оккама слегка подзатупилась) Но на первый взгляд похоже это особенности конкретно модуля debug:


                                              Be aware that this option already runs in Jinja2 context and has an implicit {{ }} wrapping, so you should not be using Jinja2 delimiters unless you are looking for double interpolation.
                                                0

                                                implicit {{ }} это очень крутой wtf в ансибле.


                                                    - name: Bingo 10
                                                      debug:
                                                      when: foo
                                                      vars:
                                                        foo: '{{ "n" + "o" }}'
                                                      tags: [bingo10]
                                                      # Enjoy your ADIS... Ansible.
                                              0

                                              судя по тому что, если сделать


                                                      ус1: "{{"
                                                      ус2: ""

                                              и выдаёт "unexpected 'end of template"
                                              то проверка на наличие "усов" и необходимость повторного прогона делается для каждого параметра отдельно, до подстановки, а не после на всю строку

                                                0

                                                Спасибо. Бесконечный источник веселья на Ансибл-пати во время Ансибл-бинго.


                                                А, нет, это не источник wtf, это ожидаемое поведение. Если у вас незакрытая jinja, то это ошибка.

                                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                        Самое читаемое