Сразу замечу — это всё работает на реальном проекте (количество узлов — больше 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-файле:
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 L-- group_vars +-- all +-- zk_nodes +-- proxies L-- 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/имя_группы. Один из примеров использования — отдельный ключ для конкретной группы узлов:
Рассмотрев динамический inventory, мы можем изящно решить третью задачу:
Формирование конфигурационного файла Nginx
Понятно, что шаблоном конфигурации Nginx никого на Хабре не удивить, поэтому ограничусь только блоком upstream с пояснениями.
{% 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-адреса заменены на вымышленные. Любые совпадения являются случайными. Там, где имена файлов или каталогов важны для функционала, приведены объяснения, почему они именно так называются.
Помните, вы и только вы сами несёте ответственность за состояние ваших проектов перед менеджером, заказчиком и собственной совестью. Я не претендую на полноту изложения. Надеюсь, эта статья сэкономит кому-то время, а кого-то подтолкнёт в верном направлении. В меру возможности и своего понимания отвечу на вопросы в комментариях.
Комментарии (2)
tnt4brain
10.09.2015 08:57+1Замечание совершенно верное, в конце действительно будет запятая. На момент написания статьи средства для избавления от этой запятой у меня не было, но и необходимости от неё избавляться — тоже: приложение на Java эту запятую благополучно игнорирует.
Однако беглый поиск подсказал, что достаточно заключить разделяющую запятую вот в такую конструкцию:
{% if not loop.last %} , {% endif %}
Применительно к строчке из статьи получим вот что:
zk.connect={% for host in groups['zk_nodes'] %}{{ hostvars[host]['ansible_eth0']['ipv4']['address'] }}:{{ zk_port }}{% if not loop.last %},{% endif %} {% endfor %}
Или можно использовать второй вариант, приведённый в комментариях на SO по ссылке выше, но лично я в Jinja2 пока не настолько силён, чтобы уверенно написать, как правильно две подстановочные переменные со статическим символом (",") сцепить в список через фильтр «join» :-)
Mazdader
Мне кажется, в результате выполнения
мы получим строку с запятой в конце, что неприемлемо для многих приложений. У Вас есть рецепт, как этого избежать?