|
|||||||
Циклы в 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* уже устарели (deprecated), и вместо них рекомендуется использовать цикл loop. Задачи (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, а также некоторые вещи, которые я узнал о том, как их использовать. Циклы AnsibleTASKS, выполняемые в следующих примерах, являются более или менее произвольными примерами, связанными с созданием пользователей и их каталогов, но они тесно связаны с реальными задачами, которые могут потребоваться на производственных серверах (но обратите внимание: данные, с которыми мы должны работать, и количество задач, которые мы используем для достижения правильной конфигурации, явно нереально!) Примеры основываются друг на друге для выполнения следующих простых задач на гипотетическом сервере:
/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... } Мы писали задачу как обычно, за исключением того, что:
Также стоит отметить, что внутри цикла 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:
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 и переменной 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 и другим подобным функциям, когда вы впервые сталкиваетесь с ними). Прошло некоторое время, прежде чем я действительно начал понимать, что необходимо присоединить цикл к задаче — даже если это иногда означает повторение одних и тех же данных более одного раза — вместо выполнения одной или нескольких задач внутри цикла. |
|||||||
Так же в этом разделе:
|
|||||||
|
|||||||
|