Віртуальні ресурси в Puppet

Як мені здається, основний зміст витруальных ресурсів стає більш зрозумілий вже на конкретних прикладах з експортованими ресурсами — коли віртуальні ресурси поміщаються в базу і використовується для обміну інформацією між агентами, але щоб зрозуміти рекурсію, потрібно зрозуміти рекурсію, тому почнемо з локального застосування. На прикладі.

Приклад буде трохи синтетичним. Мені було складно придумати досить короткий приклад, при цьому демонструє сенс віртуальних ресурсів. На практиці такі приклади з вшитими іменами користувачів зустрічаються рідко. Принаймні повинні.

Є сервер з встановленим Apache. Установка і настройка проводиться зручно і модно puppet-класом apache. Для простоти будемо зберігати в основному маніфесті site.pp. Всі з'являються проблеми в ході розвитку прикладу актуальні і в разі рознесення шматків логіки по модулям.

Припустимо, класу необхідний unix-користувач, в даному прикладі webUser, домашній каталог якого буде document root'ом для вебу. Тоді отримаємо наступний скелет site.pp:

class apache {
user { 'webUser' : ensure => present }
...
}
node default {
include apache
}

Все просто. Тепер ми вирішили додати у нашу інфраструктуру nginx неважливо для яких цілей. Головне, що йому теж потрібен користувач webUser для віддачі контенту. Додаємо клас:

class apache {
user { 'webUser' : ensure => present }
}

class nginx {
user { 'webUser' : ensure => present }
}
node default {
include apache
include nginx
}

Запускаємо:

root@puppet:/vagrant# puppet apply ./site.pp --noop
Error: Duplicate declaration: User[webUser] is already declared in file /vagrant/site.pp:17; cannot redeclare at /vagrant/site.pp:11 on node puppet.example.com
Error: Duplicate declaration: User[webUser] is already declared in file /vagrant/site.pp:17; cannot redeclare at /vagrant/site.pp:11 on node puppet.example.com

Зі зрозумілих причин воно не працює. Виходить, що в одній області видимості у нас два ресурсу з однаковим значенням namevar. Вирішити проблему можна, наприклад, винісши ресурс користувача в окремий клас:

class users {
user { 'webUser' : ensure => present }
}

class nginx { include users }
class apache { include users }

node default {
include apache
include nginx
}

Запускаємо — працює:

root@puppet:/vagrant# puppet apply ./site.pp --noop
Notice: Compiled catalog for puppet.example.com in environment production in seconds 0.07
Notice: /Stage[main]/users/User[webUser]/ensure: current_value absent, should be present (noop)
Notice: Class[users]: Would have triggered 'refresh' from 1 events
Notice: Stage[main]: Would have triggered 'refresh' from 1 events
Notice: Finished catalog run in seconds 0.02

Припустимо, що нам знадобилося додати нового користувача cacheUser, в папці якого ми будемо зберігати будь-якої кеш. Цим кешем користується як Apache, так і nginx, тому ми додаємо відповідного користувача в клас users:

class users {
user { 'webUser': ensure => present }
user { 'cacheUser': ensure => present }
}

Далі ми вирішили додати php5-fpm і uwsgi, яким потрібен webUser, але не потрібний cacheUser. У такій ситуації доведеться виділяти cacheUser в окремий клас, щоб підключати його окремо тільки в класах apache і nginx. Це незручно. До того ж немає гарантій, що трохи пізніше не доведеться виділити ще одного користувача в окремий клас. Тут-то на допомогу і приходять віртуальні ресурси.

Якщо до визначення ресурсу додати знак @:

@user { 'webUser': ensure => present }

Ресурс буде вважатися віртуальним. Такий ресурс не буде додаватися в каталог агента до тих пір, поки ми не визначимо. З документації:
A virtual resource declaration specifies a desired state for a resource without adding it to the catalog
Тому якщо виконати код нижче навіть при відсутності в системі користувачів webUser та cacheUser вони додані не будуть:

class users {
@user { 'webUser': ensure => present }
@user { 'cacheUser': ensure => present }
}
class nginx { include users }
class apache { include users }

node default {
include apache
include nginx
}

Перевіряємо:

root@puppet:/vagrant# puppet apply ./site.pp
Notice: Compiled catalog for puppet.example.com in environment production in seconds 0.07
Notice: Finished catalog run in seconds 0.02

Користувачі, як і очікувалося, не додалися.

Але слід бути уважним. Незважаючи на те, що віртуальний ресурс не додається в каталог, це не означає, що наступний код буде працювати:

class apache {
@user { 'webUser' : ensure => present }
}

class nginx { 
@user { 'webUser' : ensure => present }
}

node default {
include apache
include nginx
}

Він як і раніше буде видавати помилку компіляції. Мабуть те, що ресурс не буде доданий в каталог агенту не означає, що на етапі прекомпиляции puppet не перевіряє наявність дублікатів ресурсів.

Для визначення ресурсу використовується або spaceship оператор < | | > або з допомогою функція realize. Перепишемо наш маніфест з використанням як одного так і іншого синтаксису:

class users {
@user { 'webUser': ensure => present }
@user { 'cacheUser': ensure => present }

}
class nginx {
include users
realize User['webUser'], User['cacheUser']
}
class apache {
include users
User <| title == 'webUser' or title == 'cacheUser' |>
}

node default {
include apache
include nginx
}

У функцію realize можна передавати відразу кілька ресурсів, а в операторі <| |> можна вказувати кілька умов, за якими робиться пошук ресурсів для визначення.

Крім синтаксичної різниця у realize та <| |> є відмінності і в поведінку. Якщо ресурс з вказаною назвою не існує realize видасть помилку:

Error: Failed to realize virtual resources User[nonExistingUser] on node puppet.example.com

Оператор <| |> в такому разі помилку не видає, тому що він є свого роду надбудовою над функцією realize. До всіх найденым ресурсів по заданому в його тілі пошуковому запиту застосовується функція realize. Відповідно, якщо не знайшлося ресурсу за заданими критеріями помилки не виникає, так як не викликається функція realize.

До речі, у оператора <| |> є ще два досить хороших застосування. Його можна використовувати для зміни стану ресурсу в класі. Наприклад:

class configurations 
{
file { '/etc/nginx.conf' : ensure => present } 
file { '/etc/apache2.conf' : ensure => present }
}
node s1.example.com { 
include configurations 
}
node s2.example.com { 
include configurations 
File <| title == '/etc/apache2.conf' |> { ensure => absent }
}

Виключить файл /etc/apache2.conf для ноди s2.example.com.
Також його можна використовувати з операторами ~> та ->. Таким чином, ми можемо повідомити всі сервіси про які-небудь зміни, або зажадати перед установкою будь-якого пакета додати всі yum репозиторії:
Yumrepo <| |> -> Package <| |>


Як мені здається, основною перевагою віртуальних ресурсів є те, що їх можна експортувати і робити доступними для інших агентів. Щоб експортувати віртуальний ресурс необхідний додати ще один знак @ перед його описом.

Класичний приклад з документації Puppet:

class ssh {
# Declare:
@@sshkey { $hostname:
type => dsa,
key => $sshdsakey,
}
# Collect:
Sshkey <<| |>>
}

У даному прикладі ми визначили віртуальний ресурс sshkey. Оператор-колектор <<| |>> містить порожнє тіло, тому вивантажує всі експортовані об'єкти класу Sshkey. Таким чином, будь-який агент, в маніфесті якого підключається клас ssh, експортує свій публічний ключ (@@sshkey), а потім імпортує до себе всі ключі, додані іншими агентами (Sshkey <<| |>>).

Експортовані ресурси зберігаються в PuppetDB — БД від PuppetLabs. Після підключення PuppetDB кожен скопилированный pupet master'ом каталог кладеться в базу PuppetDB, яка в свою чергу надає пошуковий інтерфейс для пошуку по каталогах.

Вказуючи @@, ми помічаємо ресурс як експортований і інформуємо puppet, що ресурс необхідно додати у каталог і поставити мітку exported. Коли puppet master бачить оператор <<| |>>, він робить пошуковий запит до PuppetDB і додає всі знайдені експортовані ресурси, що підходять під критерій пошуку.

Важливо, що експортовані ресурси знаходяться в глобальній області видимості, тому їх назви повинні бути унікальними.

У цього функціоналу величезний потенціал і мені досить часто доводиться ним користуватися. Автоматизація додавання серверів моніторинг або nginx бэкендов.

Краще використовувати існуючі модулі, але для демонстрації принципу даний приклад підійде.
В дуже спрощеному вигляді додавати бэкенды можна приблизно таким чином, визначаючи бекенд:

define nginx::backend($connectStr) {
@@concat::fragment { $title:
content => "server $connectStr;",
target => '/etc/nginx/conf.d/backend.conf'
}
}

І фронтенд:

class nginx::frontend {
concat { '/etc/nginx/conf.d/backend.conf' :
ensure => present,
ensure_newline => true
}
Concat::Fragment <|| target == '/etc/nginx/conf.d/backend.conf' ||>
}

Тепер кожен бекенд буде додавати nginx::backend:

nginx::backend { ${::fqdn}_backend" :
connectStr => '127.0.0.1:8080'
}

Експортуючи таким чином дані про себе у PuppetDB, а фронтэнд підключаючи клас nginx::frontend збирає їх, складає і складає у файл backend.conf.

Більш детальну інформацію про синтаксис і паттернах використання можна знайти за наступними посиланнями:

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.