Системное администрирование
IT-инфраструктура
Серверное администрирование
Puppet
15 ноября 2014

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

Tutorial

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

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

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

Опять же, я не отвергаю потенциал Puppet Dashboard и Foreman как средства мониторинга и репортинга. Красивый веб-интерфейс с графиками и картинками необходим, но необходим лишь как средство просмотра, не как средство изменения конфигурации свой инфраструктуры. И я также знаю, что Foreman много чего умеет помимо Паппета (Red Hat Satellite Server 6 и Katello project, основанные на 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. Далее, мы задаем имена файлов, которые Хиера будет ожидать увидеть в своей папке. Поскольку Хиера вызывается из кода Паппета, то ей доступны те же самые переменные, что и Паппету, включая факты. Встроенным фактом каждой ноды является ее environment, что можно использовать в Хиере в виде переменной %{::environment}. FQDN ноды в Хиере предсказуемо выглядит как %{::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 (не в файловой структуре) важен. Хиера начинает просмотр сверху вниз, а дальше все зависит от метода вызова Хиеры, которым вы воспользуетесь в Паппет манифесте. Есть три метода, продемонстрирую их на примере. Пусть наша иерархия описана вышеописанным файлом 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. Он, как видите, всегда возвращает первое найденное значение в иерархии (с самым высоким приоритетом), а затем завершается без исследования нижележащих иерархий. В Паппете, ему соответствует стандартная функция hiera(). В нашем примере это был бы вызов hiera('classes'). Поскольку Паппет всегда вызывает Хиеру из соответствующего случаю контекста, нам нет необходимости дополнительно что-то указывать в строке запроса.

Следующий вид запроса — Array merge. Смотрим:
[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:
[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, то есть возвращать более приоритетное значение. Подробнее почитать об этом можно здесь.

Ладно, круто (ну или нет), но зачем это все нам?
Паппет автоматически (в версиях 3.х для этого даже ничего не надо настраивать) просматривает Хиеру на предмет параметров, которые могут быть им использованы.
Для начала простой чуть-чуть модифицированный пример с сайта Паппета (кстати в примере сейчас указаны устаревшие параметры ntp::autoupdate и ntp::enable, у меня ниже приведены их актуальные названия ). Будем мучить многострадальный модуль puppetlabs-ntp. Допустим мы хотим выразить в Паппете следующую конфигурацию 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 имена переменных, чтобы Паппет корректно загрузил их в нужный scope (область видимости).
Единственное, что остается сделать — добавить в основной Паппет-манифест вашего 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}. Да, последнее тоже верно, в именах разделов Хиеры и ключей хешей также можно использовать переменные Паппета. В значениях хешей, а также в строковых переменных и массивах использовать переменные нельзя, а порой жаль!

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

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

У предыдущего метода есть один существенный недостаток: он слишком автоматический. Если на простых конфигурациях мы легко можем предсказать его поведение, то в случае большого количества хостов не всегда можно с уверенностью сказать, к чему приведет добавление еще одного класса в список импортируемых. Например, вы можете использовать модуль puppetlabs-apache, чтобы добавить на некоторые ноды определенную конфигурацию апача. Если вы включите безобидную фразу
classes:
  - apache
в файл production.yaml, то это приведет к установке, настройке и запуску апача на всех production хостах. Более того, модуль apache сотрет всю предыдущую конфигурацию апача, которая уже была настроена до него.
Скрытый текст
image

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

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

Допустим, мы хотим перенести в Хиеру следующую конфигурацию двух виртуальных хостов апача:
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_resources() наряду с defined types является одним из способов организовать итерацию по циклу в Паппете, который изначально лишен данной функции (по крайней мере без future parser, вы же не настолько безумны, чтобы его пока использовать?). Оба способа итерации неплохо описаны здесь.

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

+16
35,8k 163
Комментарии 11