В этой статье мы изучим Ansible Playbooks, которые, по сути, являются чертежами для автоматизации действий. Плейбуки позволяют нам определить рецепт со всеми шагами, которые мы хотели бы автоматизировать повторяющимся, простым и последовательным образом.
Если вы совсем новичок в Ansible, ознакомьтесь сначала с этим вводным учебником по Ansible.
- Что такое Ansible Playbook?
- Структура плейбука
- Запуск книги воспроизведения
- Использование переменных в плейбуках
- Работа с чувствительными данными
- Запуск задач при изменении с помощью обработчиков
- Условные задачи
- Циклы
- Советы и рекомендации по использованию Ansible Playbooks
- 1) Будьте как можно проще
- 2) Поместите артефакты Ansible под контроль версий
- 3) Всегда давайте описательные имена своим задачам, пьесам и плейбукам.
- 4) Стремитесь к удобочитаемости
- 5) Всегда указывайте состояние задач в явном виде
- 6) Используйте комментарии, когда это необходимо
- Ключевые точки
Что такое Ansible Playbook?
Плейбуки являются одним из основных компонентов Ansible, поскольку они записывают и выполняют конфигурацию Ansible. Как правило, плейбук — это основной способ автоматизации набора задач, которые мы хотим выполнить на удаленной машине.
Они помогают нашим усилиям по автоматизации, собирая все ресурсы, необходимые для организации упорядоченных процессов или избежания повторения ручных действий. Плейбуки можно повторно использовать и передавать друг другу, они разработаны так, чтобы быть удобными для человека и простыми в написании на YAML.
Структура плейбука
Плейбук состоит из одной или нескольких пьес, которые должны выполняться в определенном порядке. Плейбук — это упорядоченный список задач, которые должны быть запущены против нужной группы хостов.
Каждая задача связана с модулем, отвечающим за действие, и его конфигурационными параметрами. Поскольку большинство задач идемпотентны, мы можем спокойно повторно запускать учебник без каких-либо проблем.
Как уже говорилось, учебники воспроизведения пишутся на языке YAML с использованием стандартного расширения .yml с минимальным синтаксисом.
Мы должны использовать пробелы для выравнивания элементов данных, которые имеют одну и ту же иерархию, для отступа. Элементы, которые являются дочерними по отношению к другим элементам, должны иметь больший отступ, чем их родители. Не существует строгого правила для количества пробелов, используемых для отступов, но довольно часто используется два пробела, в то время как символы Tab не допускаются.
Ниже приведен пример простого плейбука, в котором всего две пьесы, каждая из которых имеет две задачи:
---
- name: Example Simple Playbook
hosts: all
become: yes
tasks:
- name: Copy file example_file to /tmp with permissions
ansible.builtin.copy:
src: ./example_file
dest: /tmp/example_file
mode: '0644'
- name: Add the user 'bob' with a specific uid
ansible.builtin.user:
name: bob
state: present
uid: 1040
- name: Update postgres servers
hosts: databases
become: yes
tasks:
- name: Ensure postgres DB is at the latest version
ansible.builtin.yum:
name: postgresql
state: latest
- name: Ensure that postgresql is started
ansible.builtin.service:
name: postgresql
state: started
Мы определяем описательное имя для каждой пьесы в соответствии с ее целью на верхнем уровне. Затем мы представляем группу хостов, на которых будет выполняться пьеса, взятую из инвентаря. Наконец, мы определяем, что эти пьесы должны выполняться от имени пользователя root с опцией become, установленной на yes.
Вы также можете определить множество других ключевых слов Playbook на разных уровнях, таких как play, tasks, playbook, чтобы настроить поведение Ansible. Более того, большинство из них можно задать во время выполнения как флаги командной строки в конфигурационном файле ansible, ansible.cfg, или в инвентаре. Ознакомьтесь с правилами старшинства, чтобы понять, как Ansible ведет себя в этих случаях.
Далее мы используем параметр tasks, чтобы определить список задач для каждой пьесы. Для каждой задачи мы определяем четкое и описательное имя. Каждая задача использует модуль для выполнения определенной операции.
Например, первая задача первой пьесы использует модуль ansible.builtin.copy. Вместе с модулем нам обычно приходится определять некоторые аргументы модуля. Для второй задачи первой пьесы мы используем модуль ansible.builtin.user, который помогает нам управлять учетными записями пользователей. В данном конкретном случае мы настраиваем имя пользователя, состояние учетной записи пользователя и его uid соответствующим образом.
Запуск книги воспроизведения
Когда мы запускаем плейбук, Ansible выполняет каждую задачу по порядку, по очереди, для всех хостов, которые мы выбрали. Это поведение по умолчанию может быть скорректировано в соответствии с различными случаями использования с помощью стратегий.
Если задача не выполняется, Ansible останавливает выполнение учебника для этого конкретного узла, но продолжает для других, которые выполнились успешно. Во время выполнения Ansible отображает некоторую информацию о состоянии соединения, именах задач, статусе выполнения, а также о том, были ли произведены какие-либо изменения.
В конце Ansible предоставляет сводку о выполнении плейбука вместе с информацией о неудачах и успехах. Давайте посмотрим на это в действии, запустив пример плейбука, который мы рассматривали ранее, с помощью команды ansible-playbook.
Из вывода мы видим имена игр, задачу Gathering Facts, задачи игр и в конце — Play Recap. Так как мы не определили группу хостов баз данных, вторая игра в учебнике была пропущена.
Мы можем использовать флаг —limit, чтобы ограничить выполнение Playbook определенными хостами. Например:
ansible-playbook example-simple-playbook.yml --limit host1
Использование переменных в плейбуках
Переменные — это держатели для значений, которые можно повторно использовать в Playbook или других объектах Ansible. Они могут содержать только буквы, цифры, знаки подчеркивания и начинаться с букв.
Переменные могут быть определены в Ansible на нескольких уровнях, поэтому ознакомьтесь с приоритетом переменных, чтобы понять, как они применяются. Например, мы можем задать переменные на глобальном уровне для всех хостов, на уровне хоста для конкретного хоста или на уровне игры для конкретной игры.
Чтобы задать переменные хоста и группы, создайте каталоги group_vars и host_vars. Например, чтобы определить групповые переменные для группы баз данных, создайте файл group_vars/databases. Задайте общие переменные по умолчанию в файле group_vars/all.
Более того, чтобы определить переменные хоста для конкретного хоста, создайте файл с тем же именем, что и хост, в каталоге hosts_vars.
Чтобы заменить какие-либо переменные во время выполнения, используйте флаг -e.
Наиболее простым методом определения переменных является использование блока vars в начале пьесы. Они определяются с помощью стандартного синтаксиса YAML.
- name: Example Variables Playbook
hosts: all
vars:
username: bob
version: 1.2.3
Другой способ — определить переменные во внешних YAML-файлах.
- name: Example Variables Playbook
hosts: all
vars_files:
- vars/example_variables.yml
Чтобы использовать их в задачах, мы должны сослаться на них, поместив их имя в двойные скобки, используя синтаксис Jinja2:
- name: Example Variables Playbook
hosts: all
vars:
username: bob
tasks:
- name: Add the user {{ username }}
ansible.builtin.user:
name: "{{ username }}"
state: present
Если значение переменной начинается с фигурных скобок, мы должны заключить все выражение в кавычки, чтобы YAML правильно интерпретировал синтаксис.
Мы также можем задавать переменные с несколькими значениями в виде списков.
package:
- foo1
- foo2
- foo3
Можно также ссылаться на отдельные значения из списка. Например, чтобы выбрать первое значение foo1:
package: "{{ package[0] }}"
Другим возможным вариантом является определение переменных с помощью словарей YAML. Например:
dictionary_example:
- foo1: one
- foo2: two
Аналогично, чтобы получить первое поле из словаря:
dictionary_example['foo1']
Для ссылки на вложенные переменные необходимо использовать скобки или точечную нотацию. Например, чтобы получить значение example_name_2 из этой структуры:
vars:
var1:
foo1:
field1: example_name_1
field2: example_name_2
tasks:
- name: Create user for field2 value
user:
name: "{{ var1['foo1']['field2'] }}"
Мы можем создавать переменные с помощью оператора register, который фиксирует вывод команды или задачи, а затем использовать их в других задачах.
- name: Example-2 Variables Playbook
hosts: all
tasks:
- name: Run a script and register the output as a variable
shell: "find example_file"
args:
chdir: "/tmp"
register: example_script_output
- name: Use the output variable of the previous task
debug:
var: example_script_output
Работа с чувствительными данными
Иногда нам необходимо получить доступ к конфиденциальным данным (ключи API, пароли и т.д.) в наших плейбуках. Ansible предоставляет Ansible Vault, чтобы помочь нам в таких случаях. Хранение этих данных в виде переменных в открытом виде считается риском для безопасности, поэтому мы можем использовать команду ansible-vault для шифрования и расшифровки этих секретов.
После того как секреты будут зашифрованы с помощью пароля по вашему выбору, вы можете безопасно поместить их под контроль исходных текстов в ваши репозитории кода. Ansible Vault защищает только данные в состоянии покоя. После расшифровки секретов мы обязаны обращаться с ними осторожно и не допустить их случайной утечки.
У нас есть возможность шифровать переменные или файлы. Зашифрованные переменные расшифровываются по требованию, только когда это необходимо, в то время как зашифрованные файлы расшифровываются всегда, поскольку Ansible не знает заранее, понадобится ли ему содержимое этих файлов.
В любом случае, нам нужно подумать о том, как мы будем управлять паролями хранилища. Чтобы определить зашифрованное содержимое, мы добавляем тег !vault, который сообщает Ansible, что содержимое должно быть расшифровано, и символ | перед нашей многострочной зашифрованной строкой.
Чтобы создать новый зашифрованный файл:
ansible-vault create new_file.yml
Затем будет запущен редактор для добавления нашего содержимого, которое нужно зашифровать. Также можно зашифровать существующие файлы с помощью команды encrypt:
ansible-vault encrypt existing_file.yml
Чтобы просмотреть зашифрованный файл:
ansible-vault view existing_file.yml
Чтобы отредактировать зашифрованный файл на месте, используйте команду edit для временной расшифровки файла:
ansible-vault edit existing_file.yml
Чтобы использовать другой пароль на зашифрованном файле, используйте команду rekey, используя исходный пароль:
ansible-vault rekey existing_file.yml
Если вам необходимо расшифровать файл, вы можете сделать это с помощью команды decrypt:
ansible-vault decrypt existing_file.yml
Аналогично, мы используем команду encrypt_string для шифрования отдельных строк, которые мы можем использовать позже в переменных и включать их в плейбуки или файлы переменных:
ansible-vault encrypt_string <password_source> '<string_to_encrypt>' –'<variable_name>'
Например, для шифрования строки db_password ‘12345679’ с помощью ansible vault:
Поскольку мы опустили , мы вручную ввели пароль хранилища. Этого также можно добиться, передав файл паролей с параметром —vault-password-file.
Чтобы просмотреть содержимое приведенной выше зашифрованной переменной, которую мы сохранили в файле vars.yml, используйте тот же пароль, что и раньше, с флагом —ask-vault-pass:
ansible localhost -m ansible.builtin.debug -a var="db_password" -e "@vars.yml" --ask-vault-pass
Vault password:
localhost | SUCCESS => {
"changed": false,
"db_password": "12345678"
}
Для управления несколькими паролями используйте опцию —vault-id для установки метки. Например, чтобы установить метку dev на файле и запросить пароль для использования:
ansible-vault encrypt existing_file.yml --vault-id dev@prompt
Чтобы подавить вывод данных из задачи, которая может записать в консоль чувствительное значение, мы используем атрибут no_log: true:
tasks:
- name: Hide sensitive value example
debug:
msg: "This is sensitive information"
no_log: true
Если мы запустим эту задачу, то заметим, что сообщение не выводится на консоль:
TASK [Hide sensitive value example] ***********************************
ok: [host1]
Наконец, давайте используем пример зашифрованной переменной, которую мы создали выше, в плейбуке и выполним его.
Отлично, мы убедились, что можем успешно расшифровать значение и использовать его в задачах.
Запуск задач при изменении с помощью обработчиков
В целом, модули Ansible являются идемпотентными и могут безопасно выполняться многократно, но бывают случаи, когда мы хотим запустить задачу только тогда, когда на хосте происходит изменение. Например, мы хотим перезапустить службу только при обновлении ее конфигурационных файлов.
Для решения этой задачи Ansible использует обработчики, запускаемые при уведомлении от других задач. Задачи уведомляют свои обработчики с помощью параметра notify: только тогда, когда задачи действительно что-то меняют.
Обработчики должны иметь глобально уникальные имена, и обычно их принято указывать в нижней части плейбука.
- name: Example with handler - Update apache config
hosts: webservers
tasks:
- name: Update the apache config file
ansible.builtin.template:
src: ./httpd.conf
dest: /etc/httpd.conf
notify:
- Restart apache
handlers:
- name: Restart apache
ansible.builtin.service:
name: httpd
state: restarted
В приведенном выше примере задача Restart apache будет запущена только тогда, когда мы изменим что-то в конфигурации. В действительности обработчики можно рассматривать как неактивные задачи, ожидающие запуска с помощью оператора notify.
Важно отметить, что обработчики запускаются по умолчанию после завершения всех других задач. Таким образом, обработчики выполняются только один раз, даже если они запускаются много раз.
Чтобы контролировать такое поведение, можно использовать задачу **meta: flush_handlers*, которая запускает все обработчики, которые уже были уведомлены на тот момент.
Задача также может уведомлять более одного обработчика в своем операторе notify.
Условные задачи
Для дальнейшего контроля потока выполнения в Ansible мы можем использовать условия. Условные элементы позволяют запускать или пропускать задачи в зависимости от выполнения определенных условий. Для создания таких условий можно использовать переменные, факты или результаты предыдущих задач, а также операторы.
Некоторые примеры использования могут быть следующими: обновить переменную на основе значения другой переменной, пропустить задачу, если переменная имеет определенное значение, выполнить задачу только в том случае, если факт из узла возвращает значение, превышающее пороговое.
Чтобы применить простой условный оператор, мы используем параметр when в задаче. Если условие выполнено, задача выполняется. В противном случае оно пропускается.
- name: Example Simple Conditional
hosts: all
vars:
trigger_task: true
tasks:
- name: Install nginx
apt:
name: "nginx"
state: present
when: trigger_task
В приведенном выше примере задача выполняется, поскольку условие выполнено.
Другая распространенная схема — управление выполнением задачи на основе атрибутов удаленного узла, которые мы можем получить из фактов. Просмотрите этот список с часто используемыми фактами, чтобы получить представление обо всех фактах, которые мы можем использовать в условиях.
- name: Example Facts Conditionals
hosts: all
vars:
supported_os:
- RedHat
- Fedora
tasks:
- name: Install nginx
yum:
name: "nginx"
state: present
when: ansible_facts['distribution'] in supported_os
Можно объединить несколько условий с помощью логических операторов и сгруппировать их с помощью круглых скобок:
when: (colour=="green" or colour=="red") and (size="small" or size="medium")
Оператор when поддерживает использование списка в случаях, когда у нас есть несколько условий, которые все должны быть истинными:
when:
- ansible_facts['distribution'] == "Ubuntu"
- ansible_facts['distribution_version'] == "20.04"
- ansible_facts['distribution_release'] == "bionic"
Другой вариант — использовать условия, основанные на зарегистрированных переменных, которые мы определили в предыдущих задачах:
- name: Example Registered Variables Conditionals
hosts: all
tasks:
- name: Register an example variable
ansible.builtin.shell: cat /etc/hosts
register: hosts_contents
- name: Check if hosts file contains "localhost"
ansible.builtin.shell: echo "/etc/hosts contains localhost"
when: hosts_contents.stdout.find(localhost) != -1
Циклы
Ansible позволяет нам повторять набор элементов в задаче, чтобы выполнить ее несколько раз с разными параметрами без ее переписывания. Например, для создания нескольких файлов мы можем использовать задачу, которая итерирует список имен каталогов, вместо того, чтобы писать пять задач с одним и тем же модулем.
Для итерации по простому списку элементов используйте ключевое слово loop. Мы можем ссылаться на текущее значение с помощью переменной item цикла.
- name: "Create some files"
ansible.builtin.file:
state: touch
path: /tmp/{{ item }}
loop:
- example_file1
- example_file2
- example_file3
Вывод приведенной выше задачи, использующей цикл и item:
TASK [Create some files] *********************************
changed: [host1] => (item=example_file1)
changed: [host1] => (item=example_file2)
changed: [host1] => (item=example_file3)
Также можно выполнять итерацию по словарям:
- name: "Create some files with dictionaries"
ansible.builtin.file:
state: touch
path: "/tmp/{{ item.filename }}"
mode: "{{ item.mode }}"
loop:
- { filename: 'example_file1', mode: '755'}
- { filename: 'example_file2', mode: '775'}
- { filename: 'example_file3', mode: '777'}
Другим полезным шаблоном является итерация по группе хостов инвентаря:
- name: Show all the hosts in the inventory
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ groups['databases'] }}"
Комбинируя условия и циклы, мы можем выбрать выполнение задания только для некоторых элементов в списке и пропустить его для других:
- name: Execute when values in list are lower than 10
ansible.builtin.command: echo {{ item }}
loop: [ 100, 200, 3, 600, 7, 11 ]
when: item < 10
Наконец, еще один вариант — использовать ключевое слово until для повторного выполнения задачи до тех пор, пока условие не станет истинным.
- name: Retry a task **until** we find the word "success" in the logs
shell: cat /var/log/example_log
register: logoutput
until: logoutput.stdout.find("success") != -1
retries: 10
delay: 15
В приведенном выше примере мы проверяем файл example_log 10 раз, с задержкой в 15 секунд между каждой проверкой, пока не найдем слово success. Если мы дадим задаче выполниться и через некоторое время добавим слово success в файл example_log, то заметим, что задача успешно завершилась.
TASK [Retry a task until we find the word “success” in the logs] *********
FAILED - RETRYING: Retry a task until we find the word "success" in the logs (10 retries left).
FAILED - RETRYING: Retry a task until we find the word "success" in the logs (9 retries left).
changed: [host1]
Ознакомьтесь с официальным руководством Ansible по Loops для более сложных случаев использования.
Советы и рекомендации по использованию Ansible Playbooks
Соблюдение этих советов и рекомендаций при создании своих плейбуков поможет вам быть более продуктивными и повысить эффективность работы.
1) Будьте как можно проще
Старайтесь, чтобы ваши задачи были простыми. В Ansible существует множество опций и вложенных структур, и, комбинируя множество функций, вы можете получить довольно сложные настройки. Потратив некоторое время на упрощение ваших артефактов Ansible, вы окупите их в долгосрочной перспективе.
2) Поместите артефакты Ansible под контроль версий
Считается лучшей практикой хранить плейбуки в git или любой другой системе контроля версий и пользоваться ее преимуществами.
3) Всегда давайте описательные имена своим задачам, пьесам и плейбукам.
Выбирайте имена, которые помогут вам и другим быстро понять функциональность и назначение артефакта.
4) Стремитесь к удобочитаемости
Используйте последовательные отступы и добавляйте пустые строки между задачами для повышения удобочитаемости.
5) Всегда указывайте состояние задач в явном виде
Многие модули имеют состояние по умолчанию, что позволяет нам пропустить параметр состояния. В таких случаях лучше всегда говорить об этом явно, чтобы избежать путаницы.
6) Используйте комментарии, когда это необходимо
Бывают случаи, когда определения задачи недостаточно, чтобы объяснить всю ситуацию, поэтому не стесняйтесь использовать комментарии для более сложных частей плейбуков.
Ключевые точки
В этой статье мы рассмотрели основной компонент автоматизации Ansible — плейбуки. Мы увидели, как создавать, структурировать и инициировать выполнение плейбуков.
Кроме того, мы изучили использование переменных, работу с конфиденциальными данными, управление выполнением задач с помощью обработчиков и условий, а также итерацию задач с помощью циклов.
Спасибо за чтение, и я надеюсь, что вам понравилась эта статья «Ansible: Работа с Playbooks» так же, как и мне.
Кстати, если вы хотите управлять инфраструктурой как кодом, Spacelift — это то, что вам нужно. Он поддерживает рабочие процессы Git, политику как код, программную конфигурацию, совместное использование контекста и множество других замечательных функций. В настоящее время он работает с Terraform, Pulumi и CloudFormation, а поддержка Ansible уже на подходе! Вы можете опробовать его бесплатно, перейдя сюда и создав пробную учетную запись.