В посте рассматриваются следующие Ansible модули loop: with_items, with_nested, with_subelements, with_dict.
Все эти with* уже deprecated, и рекомендуется использовать loop.
Одна из моих ролей в Chromatic — член команды DevOps. Помимо прочего, это включает в себя работу с нашими серверами и серверами наших клиентов. Это, в свою очередь, означает, что я трачу много времени на работу с Ansible, популярным инструментом для инициализации, настройки и развертывания серверов и приложений.
Проще говоря, машина, на которой запущен Ansible, запускает команды на другом компьютере через SSH. Эти команды указываются декларативно (не обязательно) с использованием небольших участков YAML, называемых задачами. Эти TASKS вызывают модули Ansible, которые специализируются на выполнении опций с определенными компонентами, такими как файлы, базы данных и т. д.
Например, следующая задача использует модуль File (документация, код) для создания определенного каталога, если он еще не существует, и изменяет его атрибуты, если они еще не установлены правильно:
- file:
path: /home/jenkins/.ssh
state: directory
owner: jenkins
group: jenkins
mode: 700Несколько задач, относящихся к одной задаче, сгруппированы в роли, а несколько ролей могут быть сгруппированы в playbooks. Затем можно использовать playbook для выполнения точно таких же шагов конфигурации на любом количестве серверов одновременно.
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, выполняемые в следующих примерах, являются более или менее произвольными примерами, связанными с созданием пользователей и их каталогов, но они тесно связаны с реальными задачами, которые могут потребоваться на производственных серверах (но обратите внимание: данные, с которыми мы должны работать, и количество задач, которые мы используем для достижения правильной конфигурации, явно нереально!)
Примеры основываются друг на друге для выполнения следу��щих простых задач на гипотетическом сервере:
Убедитесь, что присутствуют четыре пользователя:
alice,bob,carolиdan.
Убедитесь, что домашний каталог каждого пользователя содержит два каталога:
.ssh/иloops.
Убедитесь, что каждый из четырех домашних каталогов пользователей содержит по одному каталогу для каждого другого пользователя. Например, домашний каталог пользователя 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"TASKS
- name: "Loop 1: create users using 'with_items'."
user:
name: "{{ item.name }}"
with_items: "{{ users_with_items }}"Здесь мы используем модуль 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.
РЕЗУЛЬТАТЫ
/home/
├── alice/
└── bob/Цикл 2: Создавайте каталоги общих пользователей, используя WITH_NESTED
Примечание: Для цикла 2 нужны созданные юзеры, например с помощью цикла 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"
- "loopsTASKS
# 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 }}"Как показано в приведенной выше задаче, к двум спискам в with_nested можно получить доступ, используя item.0 (для users_with_items) и item.1 (для common_directories) соответственно. Это позволяет нам, например, создайте каталог /home/alice/.ssh на самой первой итерации.
Результаты
/home/
├── alice/
│ ├── .ssh/
│ └── loops/
└── bob/
├── .ssh/
└── loops/Цикл 3: Создавайте личные каталоги пользователей, используя WITH_SUBELEMENTS
Примечание: Для цикла 3 нужны созданные юзеры, например с помощью цикла 1. Иначе будет ошибка 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"TASKS
- 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 }}"TASKS
- 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"TASKS
- 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 во втором примере, но следует отметить два отличия:
- Вторая строка в разделе
with_nestedвыглядит несколько необычно:
- "{{ home_directories.stdout_lines | union(common_directories) }}"Есть еще одна строка, начинающаяся с
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 и другим подобным функциям, когда вы впервые сталкиваетесь с ними). Прошло некоторое время, прежде чем я действительно начал понимать, что необходимо присоединить цикл к задаче — даже если это иногда означает повторение одних и тех же данных более одного раза — вместо выполнения одной или нескольких задач внутри цикла.
Надеюсь, этот пост поможет вам избавиться от моего первоначального затруднения. С этой целью я собрал виртуальную машину Vagrant (Vagrant изначально поддерживает использование Ansible для подготовки) и Ansible playbook, который я использовал для создания и тестирования этих примеров). Просто следуйте инструкциям в README, чтобы запустить примеры из этого сообщения или попробовать свои собственные. Если у вас есть какие-либо вопросы или комментарии, напишите нам в Twitter по адресу @chromaticHQ!
