python

SaltStack: Создание зависимых или ссылающихся конфигураций сервисов

  • пятница, 20 марта 2015 г. в 02:10:48
http://habrahabr.ru/post/253443/

О чем эта статья?


Знакомство с возможностями SaltStack по созданию конфигураций сервисов зависимых друг от друга; от сервисов расположенных на других или всех остальных подчиненных системах и т. д. Если проще — рассмотрим как каждая подчиненная система может получить данные с других таких же систем в момент создания и распространения своих конфигураций.

Это третья статься в серии о SaltStack, первую читайте здесь, вторую — тут.

Задача: развернуть кластер какого-то сервиса, так что-бы в конфигурациях нод были ссылки на все остальные ноды этого кластера


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

Рассмотрим самый простой вариант описанной задачи, — записать в файл /etc/hosts на каждой ноде адреса и имена всех остальных нод в кластере.

Как решить это с помощью SaltStack? Самое простое, что приходит в голову — записать имена и адреса всех нод в pillar файл и подключить его ко всем стейтам всех нод:

pillar/cluster-nodes.sls
cluster-nodes:
  node1: 
    name: node1.domain.com
    ip: 10.0.0.1
  node2: 
    name: node2.domain.com
    ip: 10.0.0.2
  node3: 
    name: node3.domain.com
    ip: 10.0.0.3

  .......

  nodeN: 
    name: nodeN.domain.com
    ip: 10.0.0.N


pillar/top.sls
base:
  '*':
    - cluster-nodes


Создадим стейт для внесения этих данных в /etc/hosts. Для этого будем использовать salt.states.host.

states/cluster-nodes.sls
{% for node in salt['pillar.get']('cluster-nodes', []) %}
cluster-node-{{node['name']}}:
  host.present:
    - ip: {{node['ip']}}
    - names:
      - {{node['name']}}
      - {{node['name'].split('.')[0]}}
{% endfor %}


После применения стейта получим на всех нодах примерно вот такой /etc/hosts:

127.0.0.1     localhost 
10.0.0.1      node1.domain.com   node1
10.0.0.2      node2.domain.com   node2
10.0.0.3      node3.domain.com   node3
.........
10.0.0.N      nodeN.domain.com   nodeN

Казалось бы — цель достигнута — со всех хостов видны другие. Такого рода решение хоть и не элегантное, т.к. придется делать правки в pillar файл при добавлении новых нод к кластеру, но имеет право на существование в тех случаях когда имена и адреса всех нод заведомо известны и фиксированы.

А что делать если каждая из нод получает свой адрес, к примеру, по DHCP? Или генерацией нод занимается какой-нибудь IaaS провайдер типа Amazon EC2, GoGrid или Google Grid? Там ни адресов ни имен нод заранее нет и за фиксированные адреса придется еще и доплачивать.

(Лирическое отступление — в скором будущем напишу статью о том, как создать с помощью SaltStack свою инфраструктуру в EC2).

В принципе, после установки миниона на ноду, информацию о его имени и адресе можно получить пользуя salt-grains.

К примеру, получить эти данные можно на минионе так:

#salt-call grains.item fqdn fqdn_ip4
local:
    ----------
    fqdn:
        ip-10-6-0-150.ec2.internal
    fqdn_ip4:
        - 10.6.0.150


Или в описании стейта:

{% set host_name = salt['grains.get']('fqdn') %}
{% set host_ip = salt['grains.get']('fqdn_ip4') %}

Все бы ничего — но есть одно существенное но: это все доступно на мастере и не доступно на отдельных минионах. То есть в момент генерации конфигураций на каждом из минионов видны данные самого миниона, кое-что из мастера и совсем ничего об остальных минионах.

Вот тут и встает вопрос — как же получить данные с других минионов при генерации конфигураций конкретного миниона?

Ответ прост: для этого можно использовать salt-mine. В документации написано не много, но достаточно, чтобы понять общее назначение этого сервиса. Как он работает? Мастер кеширует с некоторой периодичностью определенный набор данных с каждого из минионов и предоставляет доступ к этим закешированным данным всем минионам.

Как это реализовать?
1. Задаем mine_functions — описание функций для получения и кеширования отдельных данных с минионов. Могут быть определены с помощью непосредственного описания в /etc/salt/minion на каждом из минионов, или подключением pillar файла с описанием этих функций на мастере для каждого из минионов.
2. Либо ждем некоторое время (mine_interval сек. — можно указать в конфигурации миниона) либо форсим обновление руками с помощью salt '*' mine.update
3. Пользуем ф-цию mine.get для получения необходимых данных с мастера при конфигурировании текущего миниона.

Рассмотрим как решить задачу с хостами при помощи описанных шагов. Итак:

1. Создаем запись в pillar файл и подключаем его ко всем минионам:

pillar/minefuncs.sls
mine_functions:
  grains.item: [fqdn, fqdn_ip4]


pillar/top.sls
base:
  '*':
    - minefuncs


2. Форсим сбор данных с минионов.

3. Создаем стейт для /etc/hosts:

{% for node, fqdn_data in salt['mine.get']('*', 'grains.item', expr_form='glob').items() %}
cluster-node-{{fqdn_data['fqdn']}}:
  host.present:
    - names:
      - {{fqdn_data['fqdn'].split('.')[0]}}
      - {{fqdn_data['fqdn']}}
    - ip: {{fqdn_data['fqdn_ip4'][0]}}
{% endfor %}

В итоге мы получим автоматически сгенерированный хостс файл содержащий имена и адреса всех минионов.

Если есть необходимость вычленять конкретный набор минионов для которых будет применяться стейт (к примеру у одних нод кластера одна роль, у других — другая и надо вычленить только ноды с определенными ролями) можно воспользоваться следующими рекомендациями:

1. Для всех минионов определяем custom grain (это несложно и описано в стандартной документации), к примеру: grains:roles:name_node и grains:roles:data_node.
2. Сделать выборку в mine.get по указанным ролям. К примеру вот так:

{% for node, fqdn_data in salt['mine.get']('roles:data_node', 'grains.item', expr_form='grain').items() %}
cluster-node-{{fqdn_data['fqdn']}}:
  host.present:
    - names:
      - {{fqdn_data['fqdn'].split('.')[0]}}
      - {{fqdn_data['fqdn']}}
    - ip: {{fqdn_data['fqdn_ip4'][0]}}
{% endfor %}


{% for node, fqdn_data in salt['mine.get']('* and not G@roles:data_node', 'grains.item', expr_form='compound').items() %}
cluster-node-{{fqdn_data['fqdn']}}:
  host.present:
    - names:
      - {{fqdn_data['fqdn'].split('.')[0]}}
      - {{fqdn_data['fqdn']}}
    - ip: {{fqdn_data['fqdn_ip4'][0]}}
{% endfor %}

В этих случаях стоит обратить внимание на expr_form и описание выражения для выборки.

Вот, собственно, и есть решение задачи описанной выше — с тем, чтобы была возможность получать конфигурационные данные с минионов.

С помощью описанных техник можно также передавать различные данные между минионами в процессе генерации их конфигураций.

Надеюсь статейка будет полезна всем, кто использует SaltStack в достаточно нетривиальных конфигурациях.

Спасибо за чтение.