MyTetra Share
Делитесь знаниями!
Циклы в Ansible
Время создания: 06.12.2022 13:35
Автор: Christopher Torgalson
Текстовые метки: ansible, цикл, loop, with_items
Раздел: Компьютер - Linux - Оркестрация - Ansible
Запись: xintrea/mytetra_syncro/master/base/1670322940wapdfcsipm/text.html на raw.github.com

Здесь рассматриваются следующие инструменты Ansible для организации циклов:


  • with_items,
  • with_nested,
  • with_subelements,
  • with_dict.


На самом деле все эти with* уже устарели (deprecated), и вместо них рекомендуется использовать цикл loop.


Ansible декларативный?

Задачи (TASKS) в Ansible записываются декларативно, то есть мы не указываем, какая базовая реализация должна использоваться для выполнения TASKS. Это полезно, поскольку обеспечивает высокий уровень абстракции, очень читаемый и относительно простой для написания кода, а в некоторых случаях позволяет нам использовать одну и ту же задачу на разных платформах. Например, есть модуль Ansible Copy , который используется для копирования файлов на конечный компьютер. В следующей задаче Ansible копирует файл конфигурации в существующий каталог на удаленном компьютере и устанавливает владельца, группу и права доступа к файлу:


- name: Copy SSH config file into Alice’s .ssh directory.

copy:

src: files/config

dest: /home/alice/.ssh/config

owner: alice

group: alice

mode: 0600


Для достижения того же результата мы могли бы, например, написать серию команд или функцию в bash, используя scp, chown и chmod. С Ansible мы можем сосредоточиться на желаемой конфигурации, не слишком заботясь о деталях.

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

Одно место, где я заметил это в Ansible, — это многократное выполнение одной и той же TASKS с набором разных элементов. В частности, я нашел инструменты циклов Ansible немного странными, не в последнюю очередь потому, что их шестнадцать — по сравнению с PHP, который имеет четыре вида циклов .

На самом деле для этого есть причина, если вас интересует внутреннее устройство Ansible. На странице Loops в документации указано, что «loops на самом деле представляют собой комбинацию вещей с _ + lookup(), поэтому любой плагин поиска можно использовать в качестве источника для цикла». Поиск (Lookups) — это тип плагина Ansible , который используется для «доступа к данным в Ansible из внешних источников», и если вы сравните документацию Loops и каталог плагинов Ansible на Github , вы увидите многие из них с одинаковыми именами.

Однако в документации Ansible поисковые запросы рассматриваются как «расширенная тема», и нет необходимости углубляться в исходный код, чтобы научиться использовать сами циклы. В оставшейся части этого поста описаны несколько наиболее часто используемых циклов Ansible, а также некоторые вещи, которые я узнал о том, как их использовать.


Циклы Ansible

TASKS, выполняемые в следующих примерах, являются более или менее произвольными примерами, связанными с созданием пользователей и их каталогов, но они тесно связаны с реальными задачами, которые могут потребоваться на производственных серверах (но обратите внимание: данные, с которыми мы должны работать, и количество задач, которые мы используем для достижения правильной конфигурации, явно нереально!)

Примеры основываются друг на друге для выполнения следующих простых задач на гипотетическом сервере:

  1. Убедитесь, что присутствуют четыре пользователя: alice, bob, carol и dan.
  2. Убедитесь, что домашний каталог каждого пользователя содержит два каталога: .ssh/ и loops.
  3. Убедитесь, что каждый из четырех домашних каталогов пользователей содержит по одному каталогу для каждого другого пользователя. Например, домашний каталог пользователя alice по завершении должен выглядеть так:


/home/alice/

├── .ssh/

├── bob/

├── carol/

├── dan/

└── loops/


Пример 1. Создание пользователей через цикл WITH_ITEMS

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


- name: Remove user ‘Chuck’ from the system.

user:

name: chuck

state: absent

remove: yes


Чтобы повторить эту задачу для нескольких пользователей — скажем, нам нужно удалить пользователей Chuck и Craig — мы просто добавляем в задачу параметр with_items. with_items принимает либо список (показанный здесь), либо переменную (как в остальных следующих примерах):

- name: Remove users ‘Chuck’ and ‘Craig’ from the system.

user:

name: "{{ item }}"

state: absent

remove: yes

with_items:

- chuck

- craig


Возвращаясь к нашему первому примеру цикла, мы можем использовать with_items для создания первых пользователей в нашем списке, alice и bob:

Переменные для задачи:


users_with_items:

- name: "alice"

personal_directories:

- "bob"

- "carol"

- "dan"

- name: "bob"

personal_directories:

- "alice"

- "carol"

- "dan"


Код задачи:


- name: "Loop 1: create users using 'with_items'."

user:

name: "{{ item.name }}"

with_items: "{{ users_with_items }}"


Результат:


/home/

├── alice/

└── bob/


Здесь мы используем модуль Ansible User для перебора переменной с именем users_with_items. Эта переменная содержит имена и информацию о двух пользователях, но задача только гарантирует, что пользователи существуют в системе, она не создает каталоги, содержащиеся в списке personal_directories каждого пользователя (обратите внимание, что personal_directories — это просто произвольный ключ в массиве данных для нашего примера).

Это примечательная особенность циклов Ansible (и Ansible в целом): поскольку TASKS вызывают определенные модули с определенной проблемной областью, обычно в задаче невозможно выполнять более одного вида вещей. В данном конкретном случае это означает, что мы не можем убедиться, что personal_directories пользователя существуют из этой TASKS (т. е. Потому что мы используем модуль User, а не модуль File).

Цикл with_items работает примерно так же, как этот цикл PHP:


<?php


foreach ($users_with_items as $user) {

// Do something with $user...

}


Мы писали задачу как обычно, за исключением того, что:


  • Мы заменили имя переменной item.name на имя пользователя.
  • Мы добавили строку with_items, определяющую переменную для перебора.


Также стоит отметить, что внутри цикла Ansible текущая итерация всегда находится в переменной item, и доступ к любому заданному свойству осуществляется с помощью синтаксиса item.property.


Пример 2: Создание служебных каталогов внутри каталогов пользователей, используя WITH_NESTED


Примечание: Для данного примера нужны существующие Unix-пользователи, созданные, например, с помощью цикла из примера 1. Иначе будет ошибка chown failed: failed to look up user


В этом примере мы используем две переменные: users_with_items из цикла 1, и новую переменную common_directories, которая представляет собой список всех каталогов, которые должны присутствовать в каталоге каждого пользователя. Это означает, что (снова возвращаясь к PHP), нам нужно что-то, что работает примерно так:


<?php


foreach ($users_with_items as $user) {

foreach ($common_directories as $directory) {

// Create $directory for $user...

}

}


В Ansible мы можем использовать цикл with_nested. Циклы with_nested принимают два списка, второй из которых повторяется на каждой итерации первого:

Переменные


users_with_items:

- name: "alice"

personal_directories:

- "bob"

- "carol"

- "dan"

- name: "bob"

personal_directories:

- "alice"

- "carol"

- "dan"


common_directories:

- ".ssh"

- "loops


Код задачи:


# Note that this does not set correct permissions on /home/{{ item.x.name }}/.ssh!

- name: "Loop 2: create common users' directories using 'with_nested'."

file:

dest: "/home/{{ item.0.name }}/{{ item.1 }}"

owner: "{{ item.0.name }}"

group: "{{ item.0.name }}"

state: directory

with_nested:

- "{{ users_with_items }}"

- "{{ common_directories }}"


Результат:


/home/

├── alice/

│ ├── .ssh/

│ └── loops/

└── bob/

├── .ssh/

└── loops/


Как показано в приведенной выше задаче, к двум спискам в with_nested можно получить доступ, используя item.0 (для users_with_items) и item.1 (для common_directories) соответственно. Это позволяет нам, например, создавать каталог /home/alice/.ssh на самой первой итерации.


Пример 3: Создание каталогов внутри каталогов пользователей используя WITH_SUBELEMENTS


Примечание: Для данного примера нужны существующие Unix-пользователи, созданные, например, с помощью цикла из первого примера. Иначе будет ошибка chown failed: failed to look up user


В этом примере мы используем другой вид вложенного цикла with_subelements для создания каталогов, перечисленных в переменной users_with_items из цикла 1. В PHP цикл может выглядеть примерно так:


<?php


foreach ($users_with_items as $user) {

foreach ($user['personal_directories'] as $directory) {

// Create $directory for $user...

}

}


Обратите внимание, что мы перебираем массив $users_with_items и $user['personal_directories'] для каждого пользователя.

Переменные:


users_with_items:

- name: "alice"

personal_directories:

- "bob"

- "carol"

- "dan"

- name: "bob"

personal_directories:

- "alice"

- "carol"

- "dan"


Код задачи:


- name: "Loop 3: create personal users' directories using 'with_subelements'."

file:

dest: "/home/{{ item.0.name }}/{{ item.1 }}"

owner: "{{ item.0.name }}"

group: "{{ item.0.name }}"

state: directory

with_subelements:

- "{{ users_with_items }}"

- personal_directories


Цикл with_subelements работает почти так же, как with_nested, за исключением того, что вместо второй переменной он принимает переменную и ключ другого списка, содержащегося в этой переменной — в данном случае personal_directories. Как и в цикле 2, первая итерация этого цикла создает (или проверяет существование) /home/alice/bob.

Результат:


/home/

├── alice/

│ ├── .ssh/

│ ├── bob/

│ ├── carol/

│ ├── dan/

│ └── loops/

└── bob/

├── .ssh/

├── alice/

├── carol/

├── dan/

└── loops/


Цикл 4: Создание пользователей с использованием WITH_DICT

Цикл 3 завершил настройку домашних каталогов, принадлежащих пользователям alice и bob, но есть еще два выдающихся пользователя, которые нужно создать: carol и dan. В этом примере этих пользователей создаются с помощью новой переменной users_with_dict и цикла Ansible with_dict.

Обратите внимание, что структура данных здесь содержит значимые ключи (dict или dictionary — это имя Python для ассоциативного массива); with_dict может быть лучшим вариантом, если вы вынуждены использовать данные с таким типом структуры. Цикл, который мы создаем здесь в Ansible, в PHP примерно такой:


<?php


foreach ($users_with_dict as $user => $properties) {

// Create a user named $user...

}


Переменные:


users_with_dict:

carol:

common_directories: "{{ common_directories }}"

dan:

common_directories: "{{ common_directories }}"


Код задачи:


- name: "Loop 4: create users using 'with_dict'."

user:

name: "{{ item.key }}"

with_dict: "{{ users_with_dict }}"


Тип цикла with_dict довольно краток и позволяет получить доступ к ключам переменной и соответствующим значениям. К сожалению, у него есть один практический недостаток, а именно то, что невозможно перебрать подэлементы dict с помощью with_dict (так, например, мы не можем использовать with_dict для создания общих каталогов каждого пользователя).

Результаты:


/home/

├── alice/

│ ├── .ssh/

│ ├── bob/

│ ├── carol/

│ ├── dan/

│ └── loops/

├── bob/

│ ├── .ssh/

│ ├── alice/

│ ├── carol/

│ ├── dan/

│ └── loops/

├── carol/

└── dan/


Цикл 5: Создание личных каталогов, если они не существуют

Поскольку мы не можем легко использовать users_with_dict, нам нужно использовать доступные инструменты Ansible, чтобы сделать это по-другому. Поскольку теперь мы создали необходимых пользователей alice, bob, carol и dan, мы можем повторно использовать цикл with_nested вместе с содержимым каталога /home. В этом примере используется несколько новых функций, не связанных с циклами, чтобы показать, как циклы могут быть интегрированы в относительно сложные TASKS:


  • Регистрируемые переменные Ansible
  • Ansible условные выражения
  • Jinja2 (переменные)
  • Jinja2 (фильтры)

Переменные:


common_directories:

- ".ssh"

- "loops"


Код задачи:


- name: "Get list of extant users."

shell: "find * -type d -prune | sort"

args:

chdir: "/home"

register: "home_directories"

changed_when: false


- name: "Loop 5: create personal user directories if they don't exist."

file:

dest: "/home/{{ item.0 }}/{{ item.1 }}"

owner: "{{ item.0 }}"

group: "{{ item.0 }}"

state: directory

with_nested:

- "{{ home_directories.stdout_lines }}"

- "{{ home_directories.stdout_lines | union(common_directories) }}"

when: "'{{ item.0 }}' != '{{ item.1 }}'"


Здесь у нас есть две TASKS: одна использует модуль shell для выполнения команды find на сервере, а другая использует file для создания каталогов.

При выполнении в каталоге /home команда find \ -type d -prune | sort (выполняется модулем shell) вернет только имена каталогов, найденных внутри /home, другими словами, имена всех пользователей, каталоги которых необходимо подготовить.

Вывод этой команды сохраняется в переменной home_directories строкой register: "home_directories" в задаче. Важная часть этой переменной, которую мы будем использовать в следующей задаче, выглядит так:


"stdout_lines": [

"alice",

"bob",

"carol",

"dan",

],


Вторая задача в этом примере (фактический цикл) почти полностью совпадает с циклом with_nested во втором примере, но следует отметить два отличия:


1. Вторая строка в разделе with_nested выглядит несколько необычно:

- "{{ home_directories.stdout_lines | union(common_directories) }}"


2. Есть еще одна строка, начинающаяся с when в конце TASKS:

when: "'{{ item.0 }}' != '{{ item.1 }}'"


Давайте пройдемся по ним по очереди. Нечетная строка под with_nested применяет фильтр Jinja2 к новому списку каталогов из первой TASKS выше (это часть home_directories.stdout_lines). Базовый синтаксис фильтров Jinja:


  • объект для фильтрации (home_directories.stdout_lines)
  • применить фильтр (|)
  • имя фильтра плюс аргументы, если есть (union (common_directories))


Другими словами, мы используем фильтр для объединения home_directories.stdout_lines и переменной common_directories из начала этого примера в единый массив:


item:

- .ssh

- alice

- bob

- carol

- dan

- loops


Это означает, что наш цикл with_nested будет перебирать каждый из home_directories.stdout_lines (первая строка with_nested) и гарантировать, что каждый из каталогов во второй строке существует в домашнем каталоге каждого пользователя.

К сожалению, это дало бы нам неверный результат — если бы мы полагались только на цикл, мы бы обнаружили, что домашний каталог каждого пользователя будет содержать каталог с тем же именем, что и домашний каталог! (например, /home/alice/alice, /home/bob/bob и т. д.) Вот где появляются условные выражения Ansible when:


when: "'{{ item.0 }}' != '{{ item.1 }}'"


Эта строка не позволяет задаче создать каталог, когда текущий элемент в home_directories.stdout_lines и текущий элемент в нашем объединении home_directories.stdout_lines идентичны (как указано в документации Ansible Loops , «… при объединении when с with_items (или любой другой оператор цикла), оператор when обрабатывается отдельно для каждого элемента »). В PHP то, что мы делаем во второй задаче, будет выглядеть примерно так:


<?php


$users = ['alice', 'bob', 'carol', 'dan'];

$common_directories = ['.ssh', 'loops'];

$directories = $user + $common_directories;


foreach ($users as $user) {

foreach ($directories as $directory) {

if ($directory != $user) {

// Create the directory…

}

}

}


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

Результат:


/home/

├── alice/

│ ├── .ssh/

│ ├── bob/

│ ├── carol/

│ ├── dan/

│ └── loops/

├── bob/

│ ├── .ssh/

│ ├── alice/

│ ├── carol/

│ ├── dan/

│ └── loops/

├── carol/

│ ├── .ssh/

│ ├── alice/

│ ├── bob/

│ ├── dan/

│ └── loops/

└── dan/

├── .ssh/

├── alice/

├── bob/

├── carol/

└── loops/


Выводы

Циклы Ansible довольно странные. Они не только декларативны (как и все остальное в Ansible), но и имеют много разных типов, некоторые из имен которых (with_nested? with_subitems?) Трудно распутать.

С другой стороны, они достаточно мощны, чтобы выполнять TASKS, хотя это может потребовать небольшого сдвига в мышлении (во многом подобно языковым функциям, таким как array_filter, array_reduce, array_map и другим подобным функциям, когда вы впервые сталкиваетесь с ними). Прошло некоторое время, прежде чем я действительно начал понимать, что необходимо присоединить цикл к задаче — даже если это иногда означает повторение одних и тех же данных более одного раза — вместо выполнения одной или нескольких задач внутри цикла.


Так же в этом разделе:
 
MyTetra Share v.0.59
Яндекс индекс цитирования