[Перевод] Плейбуки Ansible — советы и примеры
В этой статье мы рассмотрим плейбуки Ansible — схемы для действий по автоматизации. Плейбуки — это простой, целостный и воспроизводимый способ определить все действия, которые мы хотели бы автоматизировать.
Содержание
Что такое плейбук Ansible?
Плейбуки — это базовые компоненты Ansible, которые записывают и исполняют конфигурацию Ansible. Обычно это основной способ автоматизировать набор задач, которые мы хотели бы выполнять на удалённой машине.
Они собирают все ресурсы, которые нужны, чтобы оркестрировать упорядоченные процессы и не выполнять одни и те же действия вручную. Плейбуки можно использовать повторно и распространять. Их можно легко написать в YAML и так же легко прочитать.
Структура плейбука
Плейбук состоит из сценариев (play), которые выполняются в заданном порядке. Сценарий представляет собой список задач для определённой группы хостов.
Каждая задача связана с модулем, отвечающим за действие, и параметрами конфигурации. Поскольку большинство задач идемпотентны, плейбук можно спокойно запускать несколько раз.
Плейбуки пишутся в 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.
Для настройки поведения Ansible мы можем определить и другие ключевые слова на разных уровнях — задача, сценарий, плейбук. Более того, большинство ключевых слов можно задать в среде выполнения в виде флагов командной строки в файле конфигурации Ansible, ansible.cfg, или инвентаре. В правилах приоритета прописано, как Ansible ведёт себя в таких случаях.
Затем с помощью параметра tasks мы определяем список задач для каждого сценария. У каждой задачи должно быть понятное имя. Задача выполняет операцию с помощью модуля.
Например, первая задача в первом сценарии использует модуль ansible.builtin.copy. Для модуля мы обычно определяем аргументы. Во второй задаче первого сценария используется модуль ansible.builtin.user для управления учётными записями пользователей. В нашем примере мы настраиваем имя пользователя, состояние пользовательского аккаунта и UID.
Запуск плейбука
При запуске плейбука Ansible выполняет задачи по порядку, по одной за раз, для всех указанных хостов. Это поведение по умолчанию можно скорректировать по необходимости с помощью стратегий.
Если при выполнении задачи происходит сбой, Ansible останавливает выполнение плейбука для этого хоста, но продолжает на других, где задача выполнена успешно. Во время выполнения Ansible отображает информацию о статусе подключения, именах задач, статусе выполнения и наличии изменений.
В конце Ansible предоставляет сводку по выполнению плейбука с указанием выполненных и невыполненных задач. Давайте на примере посмотрим, как это работает. Выполним наш пример плейбука командой ansible-playbook.
В выходных данных мы видим имена сценариев, задачу Gathering Facts (сбор фактов), другие задачи сценария, а в конце Play Recap — сводку по выполнению сценария. Мы не определили группу хостов databases, поэтому второй сценарий плейбука был пропущен.
С помощью флага –limit мы можем ограничить выполнение плейбука несколькими хостами. Например:
ansible-playbook example-simple-playbook.yml --limit host1
Использование переменных в плейбуках
Переменные замещают значения, чтобы мы могли повторно использовать плейбук и другие объекты Ansible. Имя переменной может содержать только буквы, цифры и символы подчёркивания и должно начинаться с буквы.
В Ansible переменные можно определить на нескольких уровнях — см. приоритет переменных. Например, можно задать глобальные переменные для всех хостов, переменные для отдельного хоста или переменные для конкретного сценария.
Для переменных на уровне группы и хоста нужны каталоги group_vars и host_vars. Например, чтобы определить переменные для группы databases, создаём файл 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 Vault. Хранить такие данные обычным текстом небезопасно, поэтому мы можем зашифровывать и расшифровывать их с помощью команды ansible-vault.
Зашифровав секреты и защитив их паролем, мы можем спокойно размещать их в репозитории кода. Ansible Vault защищает данные только при хранении. После расшифровки секретов мы должны обращаться с ними бережно, чтобы случайно не раскрыть.
Шифровать можно переменные или файлы. Зашифрованные переменные расшифровываются по требованию только при необходимости, а зашифрованные файлы расшифровываются всегда, потому что Ansible не знает заранее, понадобится ли их содержимое.
В любом случае, нужно продумать стратегию управления паролями для Vault. Зашифрованное содержимое мы помечаем тегом ! 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 '' –''
Например, мы хотим зашифровать строку db_password »12345679» с помощью Ansible Vault:
Поскольку мы опустили
Чтобы просмотреть содержимое зашифрованной переменной, которую мы сохранили в файле 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 (перезапустить Apache) будет выполняться, только если в конфигурации что-то изменилось. Обработчики можно считать неактивными задачами, которые ждут уведомления.
Нужно понимать, что по умолчанию обработчики выполняются после завершения всех остальных задач. При таком подходе они выполняются только один раз, даже если триггеров несколько.
Это поведение можно изменить с помощью задачи 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
Выходные данные этой задачи, которая использует loop и 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 по циклам.
Советы по плейбукам Ansible
Придерживайтесь этих рекомендаций при создании плейбуков, чтобы работать продуктивнее.
1. Чем проще, тем лучше.
Не усложняйте задачу. В Ansible есть много вариантов и вложенных структур, и если мы будем бездумно сочетать функции друг с другом, мы получим слишком сложную структуру. Не пожалейте времени на то, чтобы упростить артефакты Ansible. В долгосрочной перспективе это обязательно окупится.
2. Размещайте артефакты Ansible в системе управления версиями.
Плейбук рекомендуется хранить в git или другой системе управления версиями.
3. Создавайте понятные имена для задач, сценариев и плейбуков.
Придумывайте имена, по которым будет сразу понятно, что делает артефакт.
4. Стремитесь к удобочитаемости.
Следите за отступами и добавляйте пустые строки между задачами, чтобы код было проще читать.
5. Всегда явно упоминайте состояние задачи.
У многих модулей есть дефолтное состояние, что позволяет нам пропускать параметр состояния, но лучше явно указывать это значение, чтобы избежать недоразумений.
6. Написание комментариев при необходимости.
Иногда определения задания будет недостаточно, чтобы объяснить всю ситуацию, так что используйте комментарии для сложных частей в плейбуке.
Заключение
В этой статье мы рассмотрели основной компонент автоматизации в Ansible — плейбуки. Мы узнали, как создавать, структурировать и запускать плейбуки.
Мы также рассмотрели использование переменных, защиту чувствительных данных, контроль выполнения задач с помощью обработчиков и условий, а также итерацию по задачам с помощью циклов.
Кстати, если вы хотите использовать модель «инфраструктура как код», попробуйте Spacelift. Он поддерживает рабочие процессы Git, политику как код, программируемую конфигурацию, совместное использование контекста и многие другие удобные функции. Сейчас эта платформа работает с Terraform, Pulumi и CloudFormation с поддержкой Ansible. Здесь можно создать аккаунт, чтобы получить бесплатную пробную версию.
А если хотите глубже изучить Ansible, приходите на курс Ansible: Infrastructure as Code
Вы научитесь конфигурировать рутинные задачи и никакие правки конфигураций вас не остановят. Будете не просто конфигурировать, но и делать это с помощью удобного и простого инструмента. Сможете выполнять сложные задачи, настраивать под свои задачи и смело залезать под капот Ansible. Пойметё, когда и как писать свои модули.
Курс состоит не только из теории, но и из опыта спикера, его набитых шишек, кейсов, а также 78 тестовых и 46 практических заданий на стендах в личном кабинете.
Коротко о программе:
— Узнаете как работать с переменными, как писать плейбуки и роли;
— Развернете LEMP стек, PostgreSQL и Mongo кластеры, задеплоите Flask приложение;
— Напишите свой модуль для Ansible;
— Настроите IaC в Gitlab;
— Разберетесь с работой с облаками и enterprise решениями.
Посмотреть подробную программу и записаться: https://slurm.io/ansible