Гуглил информацию по 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
L-- group_vars
    +-- all
    +-- zk_nodes
    +-- proxies
    L-- 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-адреса заменены на вымышленные. Любые совпадения являются случайными. Там, где имена файлов или каталогов важны для функционала, приведены объяснения, почему они именно так называются.

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

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


  1. Mazdader
    10.09.2015 08:40

    Мне кажется, в результате выполнения

    zk.connect={% for host in groups['zk_nodes'] %}{{ hostvars[host]['ansible_eth0']['ipv4']['address'] }}:{{ zk_port }}, {% endfor %}

    мы получим строку с запятой в конце, что неприемлемо для многих приложений. У Вас есть рецепт, как этого избежать?


  1. 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» :-)