[Из песочницы] Работа с Ansible — задачи с несколькими неизвестными

Гуглил информацию по Ansible, наткнулся на статью на Хабре. Прочитал и сильно удивился: ведь можно сделать красивее! Если вы заинтересованы — добро пожаловать под кат!
Сразу замечу — это всё работает на реальном проекте (количество узлов — больше 80, но меньше 100).

Вот хитрые задачи с неизвестными, с которыми сталкивается как минимум каждый третий, а может, и второй DevOps.

  1. В качестве параметра конфигурационного файла использовать заранее неизвестные адреса узлов, причём эти узлы относятся к другой роли (решение)
  2. Сформировать inventory из неизвестных адресов узлов динамически — через обращение к службам AWS (решение)
  3. Cформировать конфигурационный файл Nginx, в котором должны быть прописаны заранее неизвестные адреса backend-узлов (решение)


Итак, сначала — самое простое:

Подстановка адресов узлов


Возьмём пример: у нас есть группа узлов «app_nodes», на которые ставится некое приложение с помощью файла заданий вот такого вида:

application.yml
---
- hosts: app_nodes
  gather_facts: yes
  roles:
    - role: common
    - role: application


Представьте, что у нас есть сервис ZooKeeper, а в конфигурационный файл приложения необходимо прописать настройку для работы с этим сервисом:
zk.connect=127.0.0.1:2181,127.0.0.2:2181,127.0.0.3:2181,127.0.0.4:2181, 127.0.0.5:2181
Понятно, что ручками прописывать адреса всех, к примеру, пяти узлов, на каждый из которых установлен ZooKeeper, на узлы с приложением — никакого удовольствия. Что же сделать, чтобы Ansible вставлял в шаблон конфигурационного файла все адреса этих узлов?
Да ничего особенного. Ansible использует шаблонизатор Jinja2 поверх YAML, поэтому используем цикл шаблонизатора:
zk.connect={% for host in groups['zk_nodes'] %}{{ hostvars[host]['ansible_eth0']['ipv4']['address'] }}:{{ zk_port }}, {% endfor %}
В результате после работы шаблонизатора должна получиться искомая строчка, но вот незадача: мы работаем с узлами app_nodes, а используем в этом шаблоне сведения («факты») об узлах zk_nodes. Как получить адреса узлов zk_nodes, если в данном файле заданий мы вообще не работаем с этими узлами? Назначим этой конкретной группе узлов (zk_nodes) пустой список заданий:

application.yml
---
- hosts: zk_nodes
  gather_facts: yes
  tasks: []
- hosts: app_nodes
  gather_facts: yes
  roles:
    - role: common
    - role: application


Для работы этого файла заданий необходимые группы узлов должны быть заданы в inventory-файле:

environments/test/inventory

[zk_nodes]
127.0.0.1
127.0.0.2
127.0.0.3
127.0.0.4
127.0.0.5
[app_nodes]
127.0.0.10
127.0.0.11
127.0.0.12


А что же делать, если адреса хостов заранее неизвестны? К примеру — используются виртуальные машины в EC2? Здесь мы плавно переходим к ответу на второй вопрос.

Работа с динамическим inventory


Информации по этой теме в Интернете не слишком много — насколько я понял, ввиду существования Ansible Tower.

Итак, у вас есть некоторое количество узлов в EC2, и вы хотите централизованно и легко управлять ими. Одно из возможных решений — использовать Ansible + скрипт динамического inventory.

Вот как выглядит структура соответствующего inventory-каталога:

tree ./environments/dynamic
.
├── ec2.ini
├── ec2.py
├── groups
└── group_vars
    ├── all
    ├── zk_nodes
    ├── proxies
    └── app_nodes


Здесь: ec2.py — сам скрипт, его можно взять здесь, ссылка прямая; ec2.ini — файл с настройками скрипта, его можно взять здесь, ссылка прямая; groups — файл, описывающий группы узлов, с которыми вы собираетесь работать в этом inventory, group_vars — каталог, содержащий значения переменных как для каждой конкретной группы, так и общие для всех.

Далее под спойлером — те настройки, которые у меня отличаются от ini-файла по ссылке:

Изменённые параметры ec2.ini

#регионы, где находятся наши узлы
regions=us-east-5, us-west-2
#работаем только с «живыми» (running) узлами
instance_states=running
#все параметры group_by_… закомментированы, кроме одного:
group_by_tag_keys=true
#Отбираем, какие именно узлы войдут в наш inventory. В данном случае — те, у которых прописан тег «Name» с указанными значениями.
instance_filters = tag: Name=zk_node, tag: Name=app_node, tag: Name=proxy


Для того, чтобы Ansible корректно распознавал эти группы и узлы, пишем файл groups:

файл groups
[all:children]
zk_nodes
proxies
app_nodes

[zk_nodes:children]
tag_Name_zk_node

[proxies:children]
tag_Name_proxy

[app_nodes:children]
tag_Name_app_node

[tag_Name_zk_node]
[tag_Name_proxy]
[tag_Name_app_node]


Тут мы сообщаем Ansible, что группа узлов all, с которой он работает по умолчанию, содержит вложенные группы zk_nodes, proxies, app_nodes. Далее сообщаем, что эти группы также имеют вложенные группы, которые формируются динамически, а поэтому в их описаниях узлы вообще не указываются. Такая вот чёрная магия — во время своей работы скрипт динамического inventory создаст группы вида tag__ и наполнит эти группы узлами, а дальше можно работать с этими группами обычными средствами Ansible.

Да, сразу о каталоге group_vars. Он автомагически читается Ansible при загрузке inventory, и каждая группа в этом inventory получает переменные из файла group_vars/имя_группы. Один из примеров использования — отдельный ключ для конкретной группы узлов:

group_vars/zk_nodes

ansible_ssh_private_key_file:»/tmp/key.pem»


Рассмотрев динамический inventory, мы можем изящно решить третью задачу:

Формирование конфигурационного файла Nginx


Понятно, что шаблоном конфигурации Nginx никого на Хабре не удивить, поэтому ограничусь только блоком upstream с пояснениями.

nginx.conf upstream

upstream app_nodes {
{% for item in groups['app_nodes'] %} server {{ hostvars[item]['ec2_private_ip_address'] }}:{{ app_port}};
{% endfor %}
keepalive 600;
}


Этот блок конфигурации определяет группу upstream-серверов с разными адресам и общим для всех номером порта.
Шаблонизатор пробежится по всем узлам группы app_nodes, сгенерировав по строчке для каждого узла. Получится вот такой

Пример результата
upstream app_nodes {
127.0.0.1:3000;
127.0.0.2:3000;
127.0.0.3:3000;
127.0.0.4:3000;
keepalive 600;
}


Здесь отличие от ситуации с первым решением в отсутствии необходимости дополнительно обращаться с пустым списком заданий к группе узлов «app_nodes» — эта группа автоматически создаётся в числе прочих согласно файлу groups, приведённому выше, благодаря скрипту динамического inventory. Ну и, конечно, используется обращение по внутренним адресам VPC.

Послесловие


Названия окружений, задач, inventory, узлов, IP-адреса заменены на вымышленные. Любые совпадения являются случайными. Там, где имена файлов или каталогов важны для функционала, приведены объяснения, почему они именно так называются.

Помните, вы и только вы сами несёте ответственность за состояние ваших проектов перед менеджером, заказчиком и собственной совестью. Я не претендую на полноту изложения. Надеюсь, эта статья сэкономит кому-то время, а кого-то подтолкнёт в верном направлении. В меру возможности и своего понимания отвечу на вопросы в комментариях.

© Habrahabr.ru