[Из песочницы] Работа с Ansible — задачи с несколькими неизвестными
Гуглил информацию по Ansible, наткнулся на статью на Хабре. Прочитал и сильно удивился: ведь можно сделать красивее! Если вы заинтересованы — добро пожаловать под кат!
Сразу замечу — это всё работает на реальном проекте (количество узлов — больше 80, но меньше 100).
Вот хитрые задачи с неизвестными, с которыми сталкивается как минимум каждый третий, а может, и второй DevOps.
- В качестве параметра конфигурационного файла использовать заранее неизвестные адреса узлов, причём эти узлы относятся к другой роли (решение)
- Сформировать inventory из неизвестных адресов узлов динамически — через обращение к службам AWS (решение)
- Cформировать конфигурационный файл Nginx, в котором должны быть прописаны заранее неизвестные адреса backend-узлов (решение)
Итак, сначала — самое простое:
Подстановка адресов узлов
Возьмём пример: у нас есть группа узлов «app_nodes», на которые ставится некое приложение с помощью файла заданий вот такого вида:
--- - 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) пустой список заданий:
--- - hosts: zk_nodes gather_facts: yes tasks: [] - hosts: app_nodes gather_facts: yes roles: - role: common - role: application
Для работы этого файла заданий необходимые группы узлов должны быть заданы в 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-каталога:
. ├── ec2.ini ├── ec2.py ├── groups └── group_vars ├── all ├── zk_nodes ├── proxies └── app_nodes
Здесь: ec2.py — сам скрипт, его можно взять здесь, ссылка прямая; ec2.ini — файл с настройками скрипта, его можно взять здесь, ссылка прямая; groups — файл, описывающий группы узлов, с которыми вы собираетесь работать в этом inventory, group_vars — каталог, содержащий значения переменных как для каждой конкретной группы, так и общие для всех.
Далее под спойлером — те настройки, которые у меня отличаются от 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:
[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/имя_группы. Один из примеров использования — отдельный ключ для конкретной группы узлов:
ansible_ssh_private_key_file:»/tmp/key.pem»
Рассмотрев динамический inventory, мы можем изящно решить третью задачу:
Формирование конфигурационного файла Nginx
Понятно, что шаблоном конфигурации Nginx никого на Хабре не удивить, поэтому ограничусь только блоком 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-адреса заменены на вымышленные. Любые совпадения являются случайными. Там, где имена файлов или каталогов важны для функционала, приведены объяснения, почему они именно так называются.
Помните, вы и только вы сами несёте ответственность за состояние ваших проектов перед менеджером, заказчиком и собственной совестью. Я не претендую на полноту изложения. Надеюсь, эта статья сэкономит кому-то время, а кого-то подтолкнёт в верном направлении. В меру возможности и своего понимания отвечу на вопросы в комментариях.