- PVSM.RU - https://www.pvsm.ru -

Puppet. Часть 1: введение в Hiera

Puppet. Часть 1: введение в Hiera - 1
Данная статья является первой из трех статей, в которых я хочу дать свое видение проблемы управления большими инфраструктурами с помощью Puppet. Первая часть является введением в мощный инструмент организации иерархии Puppet Hiera. Данная статья ориентирована на людей, уже знакомых с Паппетом, но еще не знакомых с Хиерой. В ней я постараюсь дать базовые знания об этом мощном инструменте и о том, как он облегчает управление большим количеством серверов.

Вы наверняка знаете или представляете, что управление большой инфраструктурой с помощью Puppet [1] — непростая задача. Если для десяти серверов Паппет не нужен, для пятидесяти в самый раз и код можно писать как угодно, то когда речь идет о 500+ серверов, то в этом случае приходится уже серьезно думать об оптимизации своих усилий. Плохо, что Паппет изначально, видимо, не задумывался, как решение для больших инфраструктур, по крайней мере иерархия в него изначально заложена из рук вон плохо. Стандартные node definitions [2] совершенно неприменимы в больших компаниях. Node inheritance [3] (также как и class inheritance [4]) Puppetlabs не рекомендуют больше использовать вообще, вместо этого лучше загружать данные о иерархии из внешних источников, таких как Hiera [5] и External Node Classifier [6] (ENC).
Несмотря на то, что изначально концепция ENC мало чем отличается от Хиеры, тем не менее конкретные реализации ENC, такие как Puppet Dashboard [7] и Foreman [8] мне в силу некоторых причин не очень нравятся. Объясню почему:

1) Данные о моей инфраструктуре находятся где-то в базе данных приложения. Как их оттуда доставать в случае падения приложения? Я не знаю. Могу строить догадки, но точно не знаю.
2) Мощные ENC из-за своей мощи плохо и сложно масштабируются. В отличие от них Хиера хранит все свои данные в текстовом виде. Текстовые данные очень легко синхронизировать через git и r10k [9] между несколькими Паппет мастерами, если возникнет такая потребность.

Опять же, я не отвергаю потенциал Puppet Dashboard и Foreman как средства мониторинга и репортинга. Красивый веб-интерфейс с графиками и картинками необходим, но необходим лишь как средство просмотра, не как средство изменения конфигурации свой инфраструктуры. И я также знаю, что Foreman много чего умеет помимо Паппета (Red Hat Satellite Server 6 [10] и Katello project [11], основанные на Foreman — яркие тому примеры). Но все же именно как место хранения конфигурации всей своей инфраструктуры Hiera мне нравится больше.

Что же такое Hiera? Это библиотека Ruby, которая по умолчанию включена в Паппет и помогает лучше организовать ваши данные в Паппете. Можно ли обойтись без нее? Можно. Можно все цифры и параметры писать в манифестах, но тогда они с определенной ступени развития приобретут совершенно устрашающий вид, и вам станет все сложнее вспомнить, где что хранится и что за что отвечает.

В чем профит использования Хиеры? Вы начинаете отделять конкретные параметры вашей инфраструктуры (uid'ы пользователей, ключи ssh, настройки dns, всевозможные централизованные файлики и т.д.) от кода Паппета, который их собственно применяет на вашей инфраструктуре. Это приводит к тому, что если вам однажды потребуется узнать, какой UID у такого-то пользователя на таком-то сервере или даже группе серверов — вы сразу будете четко знать, где хранится эта информация, а не будете судорожно листать все свои манифесты в поисках нужного пользователя и пытаться предугадать, к чему приведет изменение UID'а «вот в этом месте». Само собой, не надо ждать от Хиеры чуда. В конце концов, это всего лишь способ хранения и организации ваших данных.

Но довольно лирики, приступим к делу. Hiera (от hierarchy) оперирует иерархией. И я написал следующую иерархию в /etc/puppet/hiera.yaml:

:hierarchy:
    - "%{::environment}/nodes/%{::fqdn}"
    - "%{::environment}/roles"
    - "%{::environment}/%{::environment}"
    - common
:backends:
    - yaml
:yaml:
    :datadir: '/etc/puppet/hiera'

Запомните эту иерархию, в дальнейшем я буду ее активно использовать.
Для тех, кто не очень знаком с Хиерой, поясню. Мы задаем папку "/etc/puppet/hiera" как хранилище данных Хиеры. Файлы в этой папке обязательно должны иметь расширение .yaml и формат данных YAML [12]. Далее, мы задаем имена файлов, которые Хиера будет ожидать увидеть в своей папке. Поскольку Хиера вызывается из кода Паппета, то ей доступны те же самые переменные, что и Паппету, включая факты [13]. Встроенным фактом каждой ноды является ее environment, что можно использовать в Хиере в виде переменной %{::environment}. FQDN [14]ноды в Хиере предсказуемо выглядит как %{::fqdn}. Таким образом данная иерархия соответствует подобной файловой структуре:

/etc/puppet/hiera/
|-- common.yaml
|-- production/
|----- production.yaml
|----- roles.yaml
|----- nodes/
|-------- prod-node1.yaml
|-------- prod-node2.yaml
|-- development/
|----- development.yaml
|----- roles.yaml
|----- nodes/
|-------- dev-node1.yaml
|-------- dev-node2.yaml

Порядок следования уровней в hiera.yaml (не в файловой структуре) важен. Хиера начинает просмотр сверху вниз, а дальше все зависит от метода вызова Хиеры, которым вы воспользуетесь в Паппет манифесте. Есть три метода [15], продемонстрирую их на примере. Пусть наша иерархия описана вышеописанным файлом hiera.yaml, создадим три файла следующего содержания:

/etc/puppet/hiera/common.yaml

classes:
  - common_class1
  - common_class2
roles:
  common_role1:
    key1: value1
    key2: value2
common: common_value

/etc/puppet/hiera/production/production.yaml

classes:
  - production_class1
  - production_class2
roles:
  production_role1:
    key1: value1
    key2: value2
production: production_value

/etc/puppet/hiera/production/nodes/testnode.yaml

classes:
  - node_class1
  - node_class2
roles:
  node_role1:
    key1: value1
    key2: value2
node: node_value

Hiera поддерживает запросы из командной строки. На самом деле легче всего понять принцип ее работы именно из консоли. Hiera по умолчанию держит свой конфиг в /etc/hiera.yaml. Нужно сделать этот файл символической ссылкой на /etc/puppet/hiera.yaml. После этого делаем простой вызов:

[root@testnode]# hiera classes
["common_class1", "common_class2"]

Поскольку в это запросе мы не предоставили информации об environment и fqdn Хиера берет данные из самого нижнего уровня иерархии — файла common.yaml. В квадратных скобках отображаются элементы массива. Попробуем предоставить данные об environment:

[root@testnode]# hiera classes ::environment=production
["production_class1", "production_class2"]
[root@testnode]# hiera classes ::environment=production ::fqdn=testnode
["node_class1", "node_class2"]

Данные из production.yaml находятся выше в иерархии, поэтому они более приоритетны и перезаписывают данные полученные из common.yaml. Аналогичным образом данные из testnode.yaml перезаписывают данные из production.yaml. Однако, если данных нет в вышестоящей иерархии, то логичным образом данные берутся из нижестоящих:

[root@testnode]# hiera common ::environment=production
common_value
[root@testnode]# hiera production ::environment=production ::fqdn=testnode
production_value

В данном случае возвращаются строки, а не массивы, согласно вышеприведенным файлам.
Данный вид запроса называется priority lookup [16]. Он, как видите, всегда возвращает первое найденное значение в иерархии (с самым высоким приоритетом), а затем завершается без исследования нижележащих иерархий. В Паппете, ему соответствует стандартная функция hiera(). В нашем примере это был бы вызов hiera('classes'). Поскольку Паппет всегда вызывает Хиеру из соответствующего случаю контекста, нам нет необходимости дополнительно что-то указывать в строке запроса.

Следующий вид запроса — Array merge [17]. Смотрим:

[root@testnode]# hiera --array classes
["common_class1", "common_class2"]
[root@testnode]# hiera --array classes ::environment=production
["production_class1", "production_class2", "common_class1", "common_class2"]
[root@testnode]# hiera --array classes ::environment=production ::fqdn=testnode
["node_class1", "node_class2", "production_class1", "production_class2", "common_class1", "common_class2"]

Данный вид запроса проходит по всем уровням иерархии и собирает все найденные значения (строки и массивы) в один большой единый массив. В терминологии Паппета данный запрос называется hiera_array(). Однако, данный вид запроса не способен собирать хеши. Если при своем проходе он встретит хеш, то выдаст ошибку:

[root@testnode]# hiera --array roles
/usr/share/ruby/vendor_ruby/hiera/backend/yaml_backend.rb:38:in `block in lookup': Hiera type mismatch: expected Array and got Hash (Exception)

В аналогичной ситуации priority lookup пройдет нормально и вернет хеш (в фигурных скобках):

[root@testnode]# hiera roles
{"common_role1"=>{"key1"=>"value1", "key2"=>"value2"}}

Что же делать, если нам нужно собрать хеши? Используем третий тип запроса: Hash merge [18]:

[root@testnode]# hiera --hash roles
{"common_role1"=>{"key1"=>"value1", "key2"=>"value2"}}
[root@testnode]# hiera --hash roles ::environment=production
{"common_role1"=>{"key1"=>"value1", "key2"=>"value2"},
 "production_role1"=>{"key1"=>"value1", "key2"=>"value2"}}
[root@testnode]# hiera --hash roles ::environment=production  ::fqdn=testnode
{"common_role1"=>{"key1"=>"value1", "key2"=>"value2"},
 "production_role1"=>{"key1"=>"value1", "key2"=>"value2"},
 "node_role1"=>{"key1"=>"value1", "key2"=>"value2"}}

Этот запрос аналогично предыдущему проходит по всем уровням иерархии и собирает все хеши в один большой общий хеш. Несложно догадаться, что при попытке собрать им массивы или строки он вернет ошибку:

[root@testnode]# hiera --hash classes
/usr/share/ruby/vendor_ruby/hiera/backend/yaml_backend.rb:42:in `block in lookup': Hiera type mismatch: expected Hash and got Array (Exception)

В Паппете данный запрос называется hiera_hash(). Что происходит если на разным уровнях иерархии один и тот же хеш имеет разные наборы «ключ => значение»? Например, пользователь test на уровне common имеет UID=100, а на уровне ноды testnode имеет UID=200? В этом случае по каждому конкретному ключу hash lookup будет вести себя как priority lookup, то есть возвращать более приоритетное значение. Подробнее почитать об этом можно здесь [19].

Ладно, круто (ну или нет [20]), но зачем это все нам?
Паппет автоматически [19] (в версиях 3.х для этого даже ничего не надо настраивать) просматривает Хиеру на предмет параметров, которые могут быть им использованы.
Для начала простой чуть-чуть модифицированный пример с сайта Паппета [21] (кстати в примере сейчас указаны устаревшие параметры ntp::autoupdate и ntp::enable, у меня ниже приведены их актуальные названия ). Будем мучить многострадальный модуль puppetlabs-ntp [22]. Допустим мы хотим выразить в Паппете следующую конфигурацию ntp:

/etc/ntp.conf

tinker panic 0
restrict restrict default kod nomodify notrap nopeer noquery
restrict restrict -6 default kod nomodify notrap nopeer noquery
restrict restrict 127.0.0.1
restrict restrict -6 ::1
server 0.pool.ntp.org iburst burst
server 1.pool.ntp.org iburst burst
server 2.pool.ntp.org iburst burst
server 3.pool.ntp.org iburst burst
driftfile /var/lib/ntp/drift

Для этого добавим в common.yaml в Хиере следующие строки:

classes:
  - ntp
ntp::restrict:
  - restrict default kod nomodify notrap nopeer noquery
  - restrict -6 default kod nomodify notrap nopeer noquery
  - restrict 127.0.0.1
  - restrict -6 ::1
ntp::service_ensure: running
ntp::service_enable: true
ntp::servers:
  - 0.pool.ntp.org iburst burst
  - 1.pool.ntp.org iburst burst
  - 2.pool.ntp.org iburst burst
  - 3.pool.ntp.org iburst burst

Легко заметить, что здесь просто перечислены конкретные значения переменных класса ntp, которые будут переданы классу при его вызове. Эти переменные объявлены в шапке класса ntp (файл modules/ntp/manifests/init.pp). При таком способе передачи параметров классу из Хиеры обязательно нужно использовать fully qualified [23] имена переменных, чтобы Паппет корректно загрузил их в нужный scope [24] (область видимости).
Единственное, что остается сделать — добавить в основной Паппет-манифест вашего environment (site.pp) одну строчку:

hiera_include('classes')

Данная строчка, несмотря на свою простоту и краткость, производит много работы за кулисами. Во первых, Паппет проходит по всем(!) иерархиям Хиеры и подгружает все классы, объявленные во всех разделах "classes:" Хиеры. Затем Паппет проходит по всем fully qualified переменным в Хиере и подгружает их в область видимости соответствующего класса. Легко догадаться, что если вы уберете класс ntp из списка classes, но забудете убрать переменные этого класса в YAML файле, то Паппет выдаст ошибку наподобие «cannot find declared class ntp». Без подгруженного класса его переменные теряют всякий смысл.
Здесь я должен сказать, что слово classes (как и все остальные) в YAML файлах Хиеры не несет какого-то специального или зарезервированного смысла. Вместо classes можно писать любое другое слово, например production_classes, my_classes, my-%{::environment}. Да, последнее тоже верно, в именах разделов Хиеры и ключей хешей также можно использовать переменные Паппета [25]. В значениях хешей, а также в строковых переменных и массивах использовать переменные нельзя, а порой жаль!

Таким образом, мы эффективно вынесли параметры сервиса ntp из манифеста Паппета в иерархию Хиеры. Теперь в соответствии с иерархией, описанной в начале статьи, данные параметры ntp будут применены абсолютно ко всем нодам в вашей инфраструктуре. Но если вы захотите переопределить данные параметры на более высоком уровне environment или на уровне конкретного сервера — вы легко можете это сделать, указав нужные вам значения переменных на нужном вам уровне иерархии.

На самом деле данный способ автоматически импортировать данные из Хиеры в Паппет — не единственный.

Скрытый текст

image

У предыдущего метода есть один существенный недостаток: он слишком автоматический. Если на простых конфигурациях мы легко можем предсказать его поведение, то в случае большого количества хостов не всегда можно с уверенностью сказать, к чему приведет добавление еще одного класса в список импортируемых. Например, вы можете использовать модуль puppetlabs-apache [26], чтобы добавить на некоторые ноды определенную конфигурацию апача. Если вы включите безобидную фразу

classes:
  - apache

в файл production.yaml, то это приведет к установке, настройке и запуску апача на всех production хостах. Более того, модуль apache сотрет всю предыдущую конфигурацию апача, которая уже была настроена до него.

Скрытый текст

image

Вот такое у него веселое дефолтное поведение [27]! Так что простой 'include apache' порой может дорого обойтись, если не прочитать документацию.

Но что же делать?! Вписывать апач в YAML только нужных нам нод? Как-то это не совсем централизованно получается…
Чтобы иметь выбор, что мы хотим инклудить, а что не хотим, была создана Паппет функция create_resources() [28]. Ее применение прекрасно описано здесь [29].
Функция create_resources(resource, hash1, hash2): создает Паппет ресурс resource, передавая ему на вход hash1 и hash2. Hash2 опционален, но если он указан, то его ключи и значения будут добавлены к hash1. Если один и тот же параметр указан и в hash1, и в hash2, то hash1 является более приоритетным. Паппет ресурс может быть либо из списка стандартных (см. Puppet type reference [30]), либо предварительно объявленным (defined type [31]) нами или в классе. Примером стандартного ресурса является ресурс user, примером объявленного — apache::vhost из модуля apache. Рассмотрим пример с апачем (здесь позволю себе скопипастить хороший пример из вышеприведенной ссылки [29]).

Допустим, мы хотим перенести в Хиеру следующую конфигурацию двух виртуальных хостов апача:

apache::vhost { 'foo.example.com':
      port          => '80',
      docroot       => '/var/www/foo.example.com',
      docroot_owner => 'foo',
      docroot_group => 'foo',
      options       => ['Indexes','FollowSymLinks','MultiViews'],
      proxy_pass    => [ { 'path' => '/a', 'url' => 'http://backend-a/' } ],
}
apache::vhost { 'bar.example.com':
    port     => '80,
    docroot: => '/var/www/bar.example.com',
}

В Хиере это будет выглядеть так:

apache::vhosts:
  foo.example.com:
    port: 80
    docroot: /var/www/foo.example.com
    docroot_owner: foo
    docroot_group: foo
    options:
      - Indexes
      - FollowSymLinks
      - MultiViews
    proxy_pass:
      -
        path: '/a'
        url: 'http://localhost:8080/a'
  bar.example.com:
    port: 80
    docroot: /var/www/bar.example.com

Все, что остается написать в Паппет манифесте, это:

$myvhosts = hiera('apache::vhosts', {})
create_resources('apache::vhost', $myvhosts)

Здесь в первой строчке мы попросили Хиеру загрузить всю конфигурацию из раздела apache::vhosts. Информация была загружена в виде двух хешей: 'foo.example.com' и 'bar.example.com' (если совсем точно, то в переменную $myvhosts попал безымянный хеш состоящий из двух именованных хешей). После чего данные хеши по очереди были переданы на вход ресурсу apache::vhosts, что приведет к их созданию Паппетом.

Еще один хороший пример, как можно перенести данные из манифестов в Хиеру. Управление пользователями. Если написать в Хиере следующий код:

Скрытый текст

users:
  user1:
     ensure: present
     home: /home/user1
     shell: /bin/sh
     uid: 10001
     managehome: true
  user2:
     ensure: present
     home: /home/user2
     shell: /bin/sh
     uid: 10002
     groups:
       - secondary_group1
       - secondary_group2
  user3:
     ensure: present
     home: /home/user3
     shell: /bin/sh
     uid: 10003
     groups:
       - secondary_group3
       - secondary_group4

А затем в site.pp написать:

$node_users = hiera_hash('users')
create_resources(user, $users, {})

то это приведет к созданию всех вышеперечисленных пользователей. Заметьте, что вызов hiera_hash эффективно соберет всех пользователей, объявленных в разделе users:, со всей вашей иерархии. Если где-то возникнут конфликты (разный UID пользователя в разных файлах), Хиера будет брать значение, описанное в более высоком уровне иерархии. Логично.

Также, create_resiurces() наряду с defined types является одним из способов организовать итерацию по циклу в Паппете, который изначально лишен данной функции (по крайней мере без future parser, вы же не настолько безумны, чтобы его пока использовать?). Оба способа итерации неплохо описаны здесь [32].

Вот для начала и все. Я дал основы использования Хиеры. Используя стандартные функции Паппета, hiera(), hiera_array(), hiera_hash(), hiera_include() и create_resources(), как вы уже наверняка догадались, можно много чего напридумывать.
В следующей статье я постараюсь описать управление серверными ролями с помощью Паппета и Хиеры.

Автор: celebrate

Источник [33]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/it-infrastruktura/74586

Ссылки в тексте:

[1] Puppet: https://puppetlabs.com/

[2] node definitions: https://docs.puppetlabs.com/puppet/latest/reference/lang_node_definitions.html

[3] Node inheritance: https://docs.puppetlabs.com/puppet/latest/reference/lang_node_definitions.html#inheritance

[4] class inheritance: https://docs.puppetlabs.com/puppet/latest/reference/lang_classes.html#aside-when-to-inherit

[5] Hiera: https://docs.puppetlabs.com/hiera/latest

[6] External Node Classifier: https://docs.puppetlabs.com/guides/external_nodes.html

[7] Puppet Dashboard: https://docs.puppetlabs.com/dashboard/manual/1.2/bootstrapping.html

[8] Foreman: http://theforeman.org/

[9] r10k: https://github.com/adrienthebo/r10k

[10] Red Hat Satellite Server 6: https://access.redhat.com/products/red-hat-satellite

[11] Katello project: http://www.katello.org/

[12] YAML: http://www.yaml.org/YAML_for_ruby.html

[13] факты: https://docs.puppetlabs.com/puppet/latest/reference/lang_facts_and_builtin_vars.html

[14] FQDN : https://ru.wikipedia.org/wiki/FQDN

[15] три метода: https://docs.puppetlabs.com/hiera/1/lookup_types.html

[16] priority lookup: https://docs.puppetlabs.com/hiera/1/lookup_types.html#priority-default

[17] Array merge: https://docs.puppetlabs.com/hiera/1/lookup_types.html#array-merge

[18] Hash merge: https://docs.puppetlabs.com/hiera/1/lookup_types.html#hash-merge

[19] здесь: https://docs.puppetlabs.com/hiera/1/lookup_types.html#deep-merging-in-hiera--120

[20] ну или нет: http://memesmix.net/media/created/mtl2g5.jpg

[21] сайта Паппета: https://docs.puppetlabs.com/hiera/1/complete_example.html#making-decisions-and-expressing-them-in-hiera

[22] puppetlabs-ntp: https://forge.puppetlabs.com/puppetlabs/ntp

[23] fully qualified: https://docs.puppetlabs.com/puppet/latest/reference/lang_variables.html#naming

[24] scope: https://docs.puppetlabs.com/puppet/latest/reference/lang_scope.html

[25] переменные Паппета: https://docs.puppetlabs.com/hiera/1/puppet.html#puppet-variables-passed-to-hiera

[26] puppetlabs-apache: https://forge.puppetlabs.com/puppetlabs/apache

[27] веселое дефолтное поведение: https://forge.puppetlabs.com/puppetlabs/apache#setup

[28] create_resources(): https://docs.puppetlabs.com/references/latest/function.html#createresources

[29] здесь: http://puppetlunch.com/puppet/hiera.html

[30] Puppet type reference: https://docs.puppetlabs.com/references/latest/type.html

[31] defined type: https://docs.puppetlabs.com/learning/definedtypes.html

[32] здесь: https://tobrunet.ch/2013/01/iterate-over-datastructures-in-puppet-manifests/

[33] Источник: http://habrahabr.ru/post/242657/