Система Ansible является одним из самых распространенных средств автоматизации и настройки ресурсов в сети. С помощью этого решения можно осуществлять подготовку контейнеров и виртуальных машин, и многое другое. Ранее мы уже рассматривали автоматизацию управления с помощью Ansible. Напомним, что система работает в так называемом проталкивающем режиме. Вся работа с инфраструктурой осуществляется с сервера управления. И с этой машины ведется применение настроек к управляемым узлам. Ansible является безагентским решением, то есть все управление узлами ведется с помощью выполнения команд и сценариев по протоколу SSH.
В этой статье мы продолжим рассматривать Ansible, и поговорим о шаблонах. Управление конфигурациями нескольких серверов и сред — одно из ключевых направлений использования Ansible. Но эти конфигурационные файлы могут отличаться для каждого удаленного управляемого сервера или каждого кластера. Например, нескольких параметров могут отличаться, хотя все остальные настройки будут такими же. Создание статических файлов для каждой из этих конфигураций не является эффективным решением, и это займет намного больше времени, и каждый раз, когда добавляется новый узел, вам придется добавлять больше файлов. И здесь нам на помощь приходят модули шаблонов Ansible.
Шаблоны представляют собой простые текстовые файлы, содержащие параметры конфигурации. Во время выполнения плейбуков, в зависимости от условий, например, от того, какой узел вы используете, переменные будут заменены соответствующими значениями.
Однако, возможность заменять параметры в конфигурациях это далеко не единственное преимущество шаблонов. Мы можем добавлять условные операторы, циклы, фильтры для преобразования данных, выполнения арифметических вычислений и т. д. Файлы шаблонов обычно имеют расширение.j2, которое обозначает используемый механизм создания шаблонов Jinja2. Jinja2, представляет собой предназначенный для проектирования язык шаблонов для Python. В нем можно работать с управляющими структурами, проводить различные манипуляции данными, сравнения и т. д. Рассмотрим работу с Jinja2 подробнее.
Управляющие структуры
Для обозначения управляющих структур используются блоки {% …%}. Такие открывающие и закрывающие блоки уведомляют синтаксический анализатор Jinja2 что предоставляется некое управляющее выражение вместо обычного текста или имени переменной.
Внутри эти блоков могут находиться некоторые условия являющиеся по сути кодом, обеспечивающим реализацию определенной логики. Например:
{% if feature.enabled %}
feature = True
{% else %}
feature = False
{% endif %}
Здесь мы проверяем значение переменной feature.enabled
на предмет того, существует ли она и не установлена ли она в значение False. В зависимости от результата мы присваиваем соответствующие значения.
Переменные
Но посмотрим как работают шаблоны на конкретных примерах. Начнем с простого, заменим переменные по умолчанию значениями, указанными в playbook.
Файл плейбука будет иметь следующий вид:
---
- hosts: all
vars:
variable1: 'Test line 1'
variable2: 'Test line 2'
tasks:
- name: Template Example
template:
src: input1.j2
dest: /home/otus/Documents/Ansible/output.txt
В этом плейбуке мы объявили две переменные variable1 и variable2 и указали в качестве файла источника example1.j2 и в качестве файла вывода output.txt.
Файл input1.j2 у нас имеет следующий вид:
{{ variable1 }}
Simple Text
{{ variable2 }}
И в результате выполнения данного плебука у нас получится следующее:
Test line 1
Simple Text
Test line 2
Как вы можете видеть, обе переменные в example1.j2 заменены их значениями. Важно понимать, что при использовании модуля шаблона Ansible вам необходимо иметь два параметра:
src:
исходный файл шаблона.
dest:
путь назначения на удаленном сервере
Дополнительные атрибуты шаблона
Также, мы можем использовать дополнительные параметры для изменения поведения модуля шаблона по умолчанию:
force
— если целевой файл уже существует, то этот параметр решает, следует ли его заменять или нет. По умолчанию значение равно «да».
mode
— если вы хотите явно установить разрешения для целевого файла, то вы можете использовать этот параметр.
backup
— если вы хотите, чтобы файл резервной копии был создан в каталоге назначения, вам следует установить значение параметра backup равным «yes». По умолчанию это значение равно «нет». Файл резервной копии будет создаваться каждый раз, когда происходит изменение в каталоге назначения.
group
— имя группы, которой должен принадлежать файл/каталог. Это похоже на выполнение chown для файла в системах Linux.
Списки в шаблонах Ansible
Но вернемся к «программированию» шаблонов в Ansible. Допустим, нам необходимо поправить порты в файле конфигурации. В следующем примере воспользуемся модулем template для печати всех элементов, присутствующих в списке, с помощью цикла for.
- hosts: all
vars:
list1: ['80','8080','443', '8443']
tasks:
- name: Template Loop example.
- template:
src: input2.j2
dest: /home/otus/Documents/Ansible/output.txt
Файл input2.j2 имеет следующий вид:
List of ports.
{% for item in list1 %}
localhost.com:{{ item }}
{% endfor %}
И что мы получаем в результате:
List of ports.
localhost.com:80
localhost.com:8080
localhost.com:443
localhost.com:8443
Когда файлов много
Важным преимуществом шаблонов является возможность вносить разные правки в разные файлы. То есть, если у нас имеется список входных и выходных файлов, то мы можем править их в одном файле шаблона. Например, если мы хотим обработать три группы файлов, каждая с разным источником и назначением, то можно использовать параметр with_items.
- hosts: all
tasks:
- name: Template with_items example.
template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
with_items:
- {src: 'example.j2',dest: '/home/otus/Documents/Ansible/output.txt'}
- {src: 'example1.j2',dest: '/home/otus/Documents/Ansible/output1.txt'}
- {src: 'example2.j2',dest: '/home/otus/Documents/Ansible/output2.txt'}
…
Фильтрация
Фильтры это способ преобразования шаблонных выражений из одного вида данных в другой. Они принимают некоторые аргументы, обрабатывают их и возвращают результат. Ansible содержит множество встроенных фильтров, которые можно использовать в плейбуках. При этом, важно понимать, что создание шаблонов происходит на сервере Ansible, а не на целевом хосте задачи, поэтому фильтры также выполняются на сервере, поскольку они манипулируют локальными данными.
Посмотрим несколько примеров работы со списками. Так, для получения уникального набора из списка необходимо использовать фильтр:
{{ list1 | unique }}
Если требуется объединение двух списков, то воспользуемся фильтром union:
{{ list1 | union(list2) }}
Чтобы получить пересечение 2 списков (уникальный список всех элементов в обоих):
{{ list1 | intersect(list2) }}
Чтобы получить разницу в 2 списках (элементы в 1, которые не существуют во 2):
{{ list1 | difference(list2) }}
Чтобы получить симметричную разницу в 2 списках (элементы, исключительные для каждого списка):
{{ list1 | symmetric_difference(list2) }}
Также, с помощью Ansible можно генерировать случайные числа. Это бывает полезно например при создании временных файлов.
“/tmp/file{{ 100 |random(step=5) }}”
В этом примере мы создаем файл со случайным индексом на конце. Этот индекс будет выбран случайным образом из диапазона от 0 до 100 с шагом 5.
Полезным свойством фильтров является возможность предоставлять значения переменным по умолчанию. То есть можно указать значения по умолчанию для переменных непосредственно в своих шаблонах, с помощью фильтра ‘default’. Часто это лучший подход, чем сбой, если переменная не определена. Например:
{{ variable1 | default(1) }}
Фильтры для сетевых значений
Еще несколько полезных фильтров позволяют извлекать информацию об IP адресах и списка текстовых строк.
# Example list of values
test_list = ['192.24.2.1', 'host.fqdn', '::1', '192.168.32.0/24', 'fe80::100/10', True, '', '42540766412265424405338506004571095040/64']
Получим все IP адреса с помощью фильтра ipaddr:
# {{ test_list | ipaddr }}
['192.24.2.1', '::1', '192.168.32.0/24', 'fe80::100/10', '2001:db8:32c:faad::/64']
Получим только IPv4 адреса
# {{ test_list | ipv4 }}
['192.24.2.1', '192.168.32.0/24']
И IPv6 адреса:
# {{ test_list | ipv6 }}
['::1', 'fe80::100/10', '2001:db8:32c:faad::/64']
А если нам необходимо извлечь информацию о том, являются ли адреса в списке публичными или приватными, то сделать это можно следующим образом:
# {{ test_list | ipaddr('public') }}
['192.24.2.1', '2001:db8:32c:faad::/64']
# {{ test_list | ipaddr('private') }}
['192.168.32.0/24', 'fe80::100/10']
И в завершение примеров полезного использования шаблонов подготовим шаблон, который извлекает из переданного значения IP_address/mask непосредственно IP адрес, подсеть, маску, бродкастовый адрес.
{% set ipv4_host = host_prefix | unique | ipv4('host/prefix') | first %}
iface eth0 inet static
address {{ ipv4_host | ipaddr('address') }}
network {{ ipv4_host | ipaddr('network') }}
netmask {{ ipv4_host | ipaddr('netmask') }}
broadcast {{ ipv4_host | ipaddr('broadcast') }}
Например, для '192.0.2.48/24 получим следующее:
# Generated configuration file
iface eth0 inet static
address 192.0.2.48
network 192.0.2.0
netmask 255.255.255.0
broadcast 192.0.2.255
Заключение
Использование хорошо поддерживаемой и мощной библиотеки шаблонов в дополнение к функциям автоматизации и удаленного управления Ansible предоставляет широкий спектр возможностей для вывода довольно сложных файлов с разумными затратами усилий. Как и в случае с любым инструментом, иногда слишком умное обращение с ним может затруднить понимание происходящего при последующем просмотре кода. Поэтому вместо того, чтобы пытаться придумать кратчайший способ записать что‑либо в шаблоне, вы можете рассмотреть более подробное решение проблемы — например, простой цикл for вместо хитроумной комбинации фильтров и функций, — если это окажется более прагматичным и читабельным подходом. Именно это функционал мы сегодня рассмотрели.
Традиционно хочу порекомендовать вам бесплатный урок, где мы разберем Kubernetes под капотом, из каких компонентов состоит кластер и как они взаимодействуют друг с другом. Поговорим также про основные команды для взаимодействия с кластером и создания ресурсов.