В этой статье мы рассмотрим плейбуки 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 <password_source> '<string_to_encrypt>' –'<variable_name>'

Например, мы хотим зашифровать строку db_password ‘12345679’ с помощью Ansible Vault:

Поскольку мы опустили <password_source>, мы вручную ввели пароль от 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 (перезапустить 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

Комментарии (4)


  1. FlyingDutchman
    28.09.2022 18:28
    +3

    Хотел было написать "А как же roles?!", но потом увидел, что статья - это мормышка-приманка для курсов по ansible. Пойду дальше изобретать велосипед - писать роли и объединять их в плэйбуки, автоматизирующие различные инфраструктурные задачи для Оракловских баз. По этой теме крайне мало информации - приходится самому копаться


  1. dyadyaSerezha
    28.09.2022 22:17

    Нет определения инвентаря и не сказана важная вещь, что все действия выполняются на всех хостах параллельно (точнее на неком N хостов, но это можно менять), что резко убыстряет обычный цикл по хостам. Это очень важная фича, если надо что-то сделать на большом количестве хостов.

    Также не сказано про тэги, используемые очень часто.

    Ну и если упомянули Spacelift, хорошо бы очень кратко сравнить с Ansible.


  1. stalker_by
    29.09.2022 15:28

    Блин, ну как можно начинать вводную статью про CM не с Molecule/Kitchen, TestInfra/InSpec, Ansible Lint?!

    Написать г@#но можно и без вводной статьи, начните вы курс с того как писать ПРАВИЛЬНО!

    P.S. Коммент скорее всего заминусуют, но говрю так потому что 6 лет учу людей в Configuration Management.


    1. stalker_by
      29.09.2022 15:32

      Помсотрел курс, Molecule и Ansible-Lint в конце, про модули есть, про Unit тесты ни слова...
      Long live вашим курсам.