Введення в Puppet

Puppet – це система керування конфігурацією. Він використовується для приведення хостів до потрібного стану та підтримки цього стану.

Я працюю з Puppet вже понад п'ять років. Цей текст — по суті, переведена та переупорядкована компіляція ключових моментів з офіційної документації, яка дозволить новачкам швидко вникнути в суть Puppet.

Введення в Puppet

Базова інформація

Схема роботи Puppet - клієнт-серверна, хоча підтримується варіант роботи без сервера з обмеженою функціональністю.

Використовується pull-модель роботи: за замовчуванням раз на півгодини клієнти звертаються до сервера по конфігурацію та застосовують її. Якщо ви працювали з Ansible, там використовується інша, push-модель: адміністратор ініціює процес застосування конфігурації, самі по собі клієнти нічого застосовувати не будуть.

При мережній взаємодії використовується двостороннє TLS-шифрування: сервер і клієнт мають свої закриті ключі та відповідні їм сертифікати. Зазвичай сервер випускає сертифікати для клієнтів, але в принципі можливе використання зовнішнього CA.

Знайомство з маніфестами

У термінології Puppet до паппет-сервера підключаються ноди (Nodes). Конфігурація для нід пишеться у маніфестах спеціальною мовою програмування - Puppet DSL.

Puppet DSL – декларативна мова. На ньому описується бажаний стан ноди у вигляді оголошення окремих ресурсів, наприклад:

  • Файл існує, і він має певний вміст.
  • Пакет встановлений.
  • Сервіс запущено.

Ресурси можуть бути взаємопов'язані:

  • Є залежність, вони впливають на порядок застосування ресурсів.
    Наприклад, «спочатку встанови пакет, потім виправи конфігураційний файл, після цього запусти сервіс».
  • Є повідомлення – якщо ресурс змінився, він надсилає повідомлення підписаним на нього ресурсам.
    Наприклад, якщо змінюється конфігураційний файл, можна автоматично перезапустити сервіс.

Крім того, у Puppet DSL є функції та змінні, а також умовні оператори та селектори. Також підтримуються різні механізми шаблонизації – EPP та ERB.

Puppet написаний на Ruby, тому багато конструкцій та термінів взято звідти. Ruby дозволяє розширювати Puppet дописувати складну логіку, нові типи ресурсів, функції.

Під час роботи Puppet маніфести кожної конкретної ноди на сервері компілюються в каталог. Каталог - Це список ресурсів та їх взаємозв'язків після обчислення значення функцій, змінних та розкриття умовних операторів.

Синтаксис та кодстайл

Ось розділи офіційної документації, які допоможуть розібратися із синтаксисом, якщо наведених прикладів буде недостатньо:

Ось приклад того, як виглядає маніфест:

# Комментарии пишутся, как и много где, после решётки.
#
# Описание конфигурации ноды начинается с ключевого слова node,
# за которым следует селектор ноды — хостнейм (с доменом или без)
# или регулярное выражение для хостнеймов, или ключевое слово default.
#
# После этого в фигурных скобках описывается собственно конфигурация ноды.
#
# Одна и та же нода может попасть под несколько селекторов. Про приоритет
# селекторов написано в статье про синтаксис описания нод.
node 'hostname', 'f.q.d.n', /regexp/ {
  # Конфигурация по сути является перечислением ресурсов и их параметров.
  #
  # У каждого ресурса есть тип и название.
  #
  # Внимание: не может быть двух ресурсов одного типа с одинаковыми названиями!
  #
  # Описание ресурса начинается с его типа. Тип пишется в нижнем регистре.
  # Про разные типы ресурсов написано ниже.
  #
  # После типа в фигурных скобках пишется название ресурса, потом двоеточие,
  # дальше идёт опциональное перечисление параметров ресурса и их значений.
  # Значения параметров указываются через т.н. hash rocket (=>).
  resource { 'title':
    param1 => value1,
    param2 => value2,
    param3 => value3,
  }
}

Відступи та переклади рядків не є обов'язковою частиною маніфесту, однак є рекомендований гід по стилю. Короткий виклад:

  • Двопробельні відступи, таби не використовуються.
  • Фігурні дужки відокремлюються пробілом, двокрапка пробілом не відокремлюється.
  • Коми після кожного параметра, у тому числі останнього. Кожен параметр – на окремому рядку. Виняток робиться для випадку без параметрів та одного параметра: можна писати на одному рядку без коми (тобто. resource { 'title': } и resource { 'title': param => value }).
  • Стрілки параметрів повинні бути на одному рівні.
  • Стрілки взаємозв'язку ресурсів пишуться їх.

Розташування файлів на паппетсервері

Для подальших пояснень я запроваджу поняття «коренева директорія». Коренева директорія - це директорія, в якій знаходиться конфігурація Puppet для конкретної ноди.

Коренева директорія залежить від версії Puppet і використання оточень. Оточення - це незалежні набори конфігурації, які зберігаються в окремих директоріях. Зазвичай використовуються у поєднанні з гітом, у такому разі оточення створюються з гілок гіта. Відповідно, кожна нода перебуває у тому чи іншому оточенні. Це налаштовується на самій ноді, або ENC, про що я розповім в наступній статті.

  • У третій версії («старий Паппет») базовою директорією була /etc/puppet. Використання оточень опціональне – ми, наприклад, їх не використовуємо зі старим Паппетом. Якщо оточення використовуються, то вони зазвичай зберігаються в /etc/puppet/environments, кореневою директорією буде директорія оточення. Якщо оточення не використовуються, кореневою директорією буде базова.
  • Починаючи з четвертої версії («новий Паппет») використання оточень стало обов'язковим, а базову директорію перенесли до /etc/puppetlabs/code. Відповідно, оточення зберігаються в /etc/puppetlabs/code/environments, Коренева директорія - директорія оточення.

У кореневій директорії має бути піддиректорія manifests, в якій лежить один або кілька маніфестів з описом нід. Крім того, там має бути піддиректорія modules, В якій лежать модулі. Що таке модулі, я розповім трохи згодом. Крім того, у старому Паппеті також може бути піддиректорія files, в якій лежать різні файли, які ми копіюємо на ноди. У новому Паппеті всі файли винесені в модулі.

Файли маніфестів мають розширення .pp.

Пара бойових прикладів

Опис ноди та ресурсу на ній

На ноді server1.testdomain має бути створений файл /etc/issue із вмістом Debian GNU/Linux n l. Файл повинен належати користувачеві та групі root, права доступу мають бути 644.

Пишемо маніфест:

node 'server1.testdomain' {   # блок конфигурации, относящийся к ноде server1.testdomain
    file { '/etc/issue':   # описываем файл /etc/issue
        ensure  => present,   # этот файл должен существовать
        content => 'Debian GNU/Linux n l',   # у него должно быть такое содержимое
        owner   => root,   # пользователь-владелец
        group   => root,   # группа-владелец
        mode    => '0644',   # права на файл. Они заданы в виде строки (в кавычках), потому что иначе число с 0 в начале будет воспринято как записанное в восьмеричной системе, и всё пойдёт не так, как задумано
    }
}

Взаємозв'язки ресурсів на ноді

На ноді server2.testdomain повинен бути запущений nginx, що працює з заздалегідь підготовленою конфігурацією.

Декомпозуємо завдання:

  • Потрібно, щоб було встановлено пакет nginx.
  • Потрібно, щоб було скопійовано конфігураційні файли із сервера.
  • Потрібно, щоб було запущено сервіс nginx.
  • У разі оновлення конфігурації необхідно перезапустити сервіс.

Пишемо маніфест:

node 'server2.testdomain' {   # блок конфигурации, относящийся к ноде server2.testdomain
    package { 'nginx':   # описываем пакет nginx
        ensure => installed,   # он должен быть установлен
    }
  # Прямая стрелка (->) говорит о том, что ресурс ниже должен
  # создаваться после ресурса, описанного выше.
  # Такие зависимости транзитивны.
    -> file { '/etc/nginx':   # описываем файл /etc/nginx
        ensure  => directory,   # это должна быть директория
        source  => 'puppet:///modules/example/nginx-conf',   # её содержимое нужно брать с паппет-сервера по указанному адресу
        recurse => true,   # копировать файлы рекурсивно
        purge   => true,   # нужно удалять лишние файлы (те, которых нет в источнике)
        force   => true,   # удалять лишние директории
    }
  # Волнистая стрелка (~>) говорит о том, что ресурс ниже должен
  # подписаться на изменения ресурса, описанного выше.
  # Волнистая стрелка включает в себя прямую (->).
    ~> service { 'nginx':   # описываем сервис nginx
        ensure => running,   # он должен быть запущен
        enable => true,   # его нужно запускать автоматически при старте системы
    }
  # Когда ресурс типа service получает уведомление,
  # соответствующий сервис перезапускается.
}

Щоб це працювало, потрібно приблизно таке розташування файлів на паппет-сервері:

/etc/puppetlabs/code/environments/production/ # (это для нового Паппета, для старого корневой директорией будет /etc/puppet)
├── manifests/
│   └── site.pp
└── modules/
    └── example/
        └── files/
            └── nginx-conf/
                ├── nginx.conf
                ├── mime.types
                └── conf.d/
                    └── some.conf

Типи ресурсів

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

файл

Керує файлами, директоріями, симлінками, їх вмістом, правами доступу.

параметри:

  • назва ресурсу шлях до файлу (опціонально)
  • шлях шлях до файлу (якщо він не заданий у назві)
  • забезпечувати - Тип файлу:
    • absent - Видалити файл
    • present - повинен бути файл будь-якого типу (якщо файлу немає - буде створено звичайний файл)
    • file - Звичайний файл
    • directory - Директорія
    • link - Сімлінк
  • зміст — вміст файлу (підходить лише для звичайних файлів, не можна використовувати разом із джерело або мета)
  • джерело - Посилання на шлях, з якого потрібно копіювати вміст файлу (не можна використовувати разом з зміст або мета). Може бути задана як у вигляді URI зі схемою puppet: (тоді будуть використані файли з паппет-сервера), так і зі схемою http: (сподіваюся, зрозуміло, що буде в цьому випадку), і навіть зі схемою file: або у вигляді абсолютного шляху без схеми (тоді буде використаний файл із локальною ФС на ноді)
  • мета - куди повинен вказувати симлінк (не можна використовувати разом з зміст або джерело)
  • власник - Користувач, якому повинен належати файл
  • група - Група, якій повинен належати файл
  • режим - права на файл (у вигляді рядка)
  • рецидивувати - включає рекурсивну обробку директорій
  • чистка — включає видалення файлів, які не описані у Puppet
  • змусити — включає видалення директорій, які не описані у Puppet

пакет

Встановлює та видаляє пакети. Вміє обробляти повідомлення - встановлює пакет, якщо заданий параметр reinstall_on_refresh.

параметри:

  • назва ресурсу - Назва пакету (опціонально)
  • ім'я - Назва пакета (якщо не задано в назві)
  • Постачальник - пакетний менеджер, який потрібно використовувати
  • забезпечувати - Бажаний стан пакету:
    • present, installed — встановлена ​​будь-яка версія
    • latest — встановлена ​​остання версія
    • absent - Видалений (apt-get remove)
    • purged — видалено разом із конфігураційними файлами (apt-get purge)
    • held версія пакета заблокована (apt-mark hold)
    • любая другая строка — встановлено вказану версію
  • reinstall_on_refresh - якщо true, то при отриманні сповіщення пакет буде перевстановлений. Корисно для source-based дистрибутивів, де перескладання пакетів може бути необхідна при зміні параметрів збирання. За замовчуванням false.

обслуговування

Керує послугами. Вміє обробляти повідомлення – перезапускає сервіс.

параметри:

  • назва ресурсу - сервіс, яким потрібно керувати (опціонально)
  • ім'я - сервіс, яким потрібно керувати (якщо не задано у назві)
  • забезпечувати - Бажаний стан сервісу:
    • running - Запущений
    • stopped - Зупинено
  • включіть - Керує можливістю запуску сервісу:
    • true - Увімкнено автозапуск (systemctl enable)
    • mask - Замаскований (systemctl mask)
    • false - Вимкнений автозапуск (systemctl disable)
  • перезапуск - команда для перезапуску сервісу
  • статус команда для перевірки статусу сервісу
  • hasrestart - Вказати, чи підтримує інітскрипт сервісу перезапуск. Якщо false та вказано параметр перезапуск - Використовується значення цього параметра. Якщо false та параметр перезапуск не вказано - сервіс зупиняється і запускається для перезапуску (але в systemd використовується команда systemctl restart).
  • hasstatus — вказати, чи інітскрипт сервісу підтримує команду status. Якщо false, то використовується значення параметра статус. За замовчуванням true.

Exec

Запускає зовнішні команди. Якщо не вказувати параметри створює, onlyif, якщо не або refreshonly, команда запускатиметься при кожному прогоні Паппета. Вміє обробляти повідомлення – запускає команду.

параметри:

  • назва ресурсу - команда, яку потрібно виконати (опціонально)
  • команда - команда, яку потрібно виконати (якщо вона не задана у назві)
  • шлях - шляхи, в яких шукати виконуваний файл
  • onlyif - якщо вказана в цьому параметрі команда завершилася з нульовим кодом повернення, основна команда буде виконана
  • якщо не - якщо вказана в цьому параметрі команда завершилася з ненульовим кодом повернення, основна команда буде виконана
  • створює — якщо вказаний у цьому параметрі файл не існує, основна команда буде виконана
  • refreshonly - якщо trueкоманда буде запущена тільки в тому випадку, коли цей exec отримує повідомлення від інших ресурсів
  • обробляти - Директорія, з якої запускати команду
  • користувач - Користувач, від якого запускати команду
  • Постачальник - За допомогою чого запускати команду:
    • posix - просто створюється дочірній процес, обов'язково вказувати шлях
    • оболонка — команда запускається у шеллі /bin/sh, можна не вказувати шлях, можна використовувати глобінг, пайпи та інші фічі шелла. Зазвичай визначається автоматично, якщо є різні спецсимволи (|, ;, &&, || і так далі).

крон

Керує кронджобами.

параметри:

  • назва ресурсу — просто якийсь ідентифікатор
  • забезпечувати - Стан кронджоба:
    • present створити, якщо не існує
    • absent - Видалити, якщо існує
  • команда - яку команду запускати
  • навколишнє середовище — у якому оточенні запускати команду (список змінних оточення та їх значень через =)
  • користувач - від якого користувача запускати команду
  • хвилин, годину, будній день, місяць, monthday - коли запускати крон. Якщо якийсь із цих аттрибутів не вказаний, його значенням у кронтабі буде *.

У Puppet 6.0 крон як би видалили з коробки у puppetserver, тому немає документації на загальному сайті. Але він є в коробці у puppet-agent, тому ставити його окремо не треба. Документацію щодо нього можна переглянути у документації до п'ятої версії Паппета, або на Гітхабі.

Про ресурси загалом

Вимоги до унікальності ресурсів

Найчастіша помилка, з якою ми зустрічаємося. Duplicate declaration. Ця помилка виникає, коли до каталогу потрапляють два та більше ресурси однакового типу з однаковою назвою.

Тому ще раз напишу: у маніфестах для однієї ноди не повинно бути ресурсів однакового типу з однаковою назвою (title)!

Іноді необхідно поставити пакети з однаковою назвою, але різними пакетними менеджерами. У такому випадку потрібно скористатися параметром nameщоб уникнути помилки:

package { 'ruby-mysql':
  ensure   => installed,
  name     => 'mysql',
  provider => 'gem',
}
package { 'python-mysql':
  ensure   => installed,
  name     => 'mysql',
  provider => 'pip',
}

В інших типах ресурсів є аналогічні параметри, які допомагають уникнути дублікації. name у обслуговування, command у Exec, і так далі.

Метапараметри

Деякі спеціальні параметри мають кожен тип ресурсу, незалежно від його сутності.

Повний список метапараметрів у документації Puppet.

Короткий список:

  • вимагати - У цьому параметрі вказується, від яких ресурсів залежить цей ресурс.
  • перед тим — у цьому параметрі вказується, які ресурси залежить від цього ресурсу.
  • підписуватися — у цьому параметрі вказується, від яких ресурсів отримує цей ресурс.
  • повідомляти — у цьому параметрі вказується, які ресурси отримують повідомлення від цього ресурсу.

Усі перелічені метапараметри приймають або одне посилання ресурс, або масив посилань у квадратних дужках.

Посилання на ресурси

Посилання ресурс — це просто згадка ресурсу. Використовуються в основному для зазначення залежностей. Посилання на неіснуючий ресурс викликає помилку компіляції.

Синтаксис у посилання наступний: тип ресурсу з великої літери (якщо в назві типу містяться подвійні двокрапки, то з великої літери пишеться кожна частина назви між двокрапками), далі в квадратних дужках назва ресурсу (реєстр назви не змінюється!). Прогалин бути не повинно, квадратні дужки пишуться відразу після назви типу.

Приклад:

file { '/file1': ensure => present }
file { '/file2':
  ensure => directory,
  before => File['/file1'],
}
file { '/file3': ensure => absent }
File['/file1'] -> File['/file3']

Залежності та повідомлення

Документація тут.

Як було зазначено раніше, прості залежності між ресурсами транзитивні. До речі, будьте уважні під час проставлення залежностей — можна зробити циклічні залежності, що спричинить помилку компіляції.

На відміну від залежностей, повідомлення не є транзитивними. Для повідомлень діють такі правила:

  • Якщо ресурс отримує повідомлення, він оновлюється. Дії при оновленні залежать від типу ресурсу Exec запускає команду, обслуговування перезапускає сервіс, пакет встановлює пакет. Якщо ресурсу не визначено дію при оновленні, нічого не відбувається.
  • За один прогін Паппета ресурс оновлюється не більше одного разу. Це можливо, оскільки повідомлення включають залежність, а граф залежностей не містить циклів.
  • Якщо Паппет змінює стан ресурсу, ресурс надсилає повідомлення всім підписаним на нього ресурсам.
  • Якщо ресурс оновлюється, він надсилає повідомлення всім підписаним на нього ресурсам.

Обробка невказаних параметрів

Як правило, якщо якийсь параметр ресурсу не має значення за замовчуванням і цей параметр не вказаний у маніфесті, то Паппет не змінюватиме цю властивість у відповідного ресурсу на ноді. Наприклад, якщо ресурс типу файл не вказано параметр owner, то Паппет не змінюватиме власника у відповідного файлу.

Знайомство з класами, змінними та дефайнами

Припустимо, у нас кілька нід, на яких є однакова частина конфігурації, але є й відмінності, інакше ми могли б описати це все в одному блоці. node {}. Звичайно, можна просто скопіювати однакові частини конфігурації, але в загальному випадку це погане рішення - конфігурація розростається, при зміні загальної частини конфігурації доведеться правити те саме в багатьох місцях. При цьому легко помилитися, та й узагалі принцип DRY (не можу сказати) не просто так придумали.

Для вирішення такої проблеми є така конструкція, як клас.

Класи

клас - Це іменований блок паппет-коду. Класи потрібні для перевикористання коду.

Спочатку клас треба описати. Сам собою опис не додає нікуди жодних ресурсів. Клас описується в маніфестах:

# Описание класса начинается с ключевого слова class и его названия.
# Дальше идёт тело класса в фигурных скобках.
class example_class {
    ...
}

Після цього клас можна використати:

# первый вариант использования — в стиле ресурса с типом class
class { 'example_class': }
# второй вариант использования — с помощью функции include
include example_class
# про отличие этих двух вариантов будет рассказано дальше

Приклад з попереднього завдання - винесемо встановлення та налаштування nginx у клас:

class nginx_example {
    package { 'nginx':
        ensure => installed,
    }
    -> file { '/etc/nginx':
        ensure => directory,
        source => 'puppet:///modules/example/nginx-conf',
        recure => true,
        purge  => true,
        force  => true,
    }
    ~> service { 'nginx':
        ensure => running,
        enable => true,
    }
}

node 'server2.testdomain' {
    include nginx_example
}

Змінні

Клас з попереднього прикладу зовсім не гнучкий, тому що він завжди приносить ту саму конфігурацію nginx. Давайте зробимо так, щоб шлях конфігурації став змінним, тоді цей клас можна буде використовувати для встановлення nginx з будь-якою конфігурацією.

Це можна зробити за допомогою змінних.

Увага: змінні в Puppet незмінні!

Крім того, звертатися до змінної можна лише після того, як її оголосили, інакше значенням змінної виявиться undef.

Приклад роботи зі змінними:

# создание переменных
$variable = 'value'
$var2 = 1
$var3 = true
$var4 = undef
# использование переменных
$var5 = $var6
file { '/tmp/text': content => $variable }
# интерполяция переменных — раскрытие значения переменных в строках. Работает только в двойных кавычках!
$var6 = "Variable with name variable has value ${variable}"

У Puppet є простору імен, А у змінних, відповідно, є область видимості: змінна з тим самим ім'ям може бути визначена в різних просторах імен. При дозволі значення змінної змінна шукається в поточному неймспейсі, потім об'ємному, і так далі.

Приклади простору імен:

  • глобальне - туди потрапляють змінні поза описом класу чи ноди;
  • простір імен ноди у описі ноди;
  • простір імен класу у описі класу.

Щоб уникнути неоднозначності при зверненні до змінної, можна вказувати простір імен імені змінної:

# переменная без пространства имён
$var
# переменная в глобальном пространстве имён
$::var
# переменная в пространстве имён класса
$classname::var
$::classname::var

Домовимося, що шлях до конфігурації nginx лежить у змінній $nginx_conf_source. Тоді клас буде виглядати так:

class nginx_example {
    package { 'nginx':
        ensure => installed,
    }
    -> file { '/etc/nginx':
        ensure => directory,
        source => $nginx_conf_source,   # здесь используем переменную вместо фиксированной строки
        recure => true,
        purge  => true,
        force  => true,
    }
    ~> service { 'nginx':
        ensure => running,
        enable => true,
    }
}

node 'server2.testdomain' {
    $nginx_conf_source = 'puppet:///modules/example/nginx-conf'
    include nginx_example
}

Однак наведений приклад поганий тим, що є якесь «таємне знання» про те, що десь усередині класу використовує змінна з таким ім'ям. Набагато правильніше зробити це знання загальним — класи можуть мати параметри.

Параметри класу - це змінні в просторі імен класу, вони задаються в заголовку класу і можуть бути використані як перемінні в тілі класу. Значення параметрів вказується під час використання класу в маніфесті.

Можна задати значення за промовчанням. Якщо параметр не має значення за замовчуванням і значення не встановлено при використанні, це спричинить помилку компіляції.

Давайте параметризуємо клас з прикладу вище і додамо два параметри: перший, обов'язковий шлях до конфігурації, і другий, необов'язковий назва пакета з nginx (у Debian, наприклад, є пакети nginx, nginx-light, nginx-full).

# переменные описываются сразу после имени класса в круглых скобках
class nginx_example (
  $conf_source,
  $package_name = 'nginx-light', # параметр со значением по умолчанию
) {
  package { $package_name:
    ensure => installed,
  }
  -> file { '/etc/nginx':
    ensure  => directory,
    source  => $conf_source,
    recurse => true,
    purge   => true,
    force   => true,
  }
  ~> service { 'nginx':
    ensure => running,
    enable => true,
  }
}

node 'server2.testdomain' {
  # если мы хотим задать параметры класса, функция include не подойдёт* — нужно использовать resource-style declaration
  # *на самом деле подойдёт, но про это расскажу в следующей серии. Ключевое слово "Hiera".
  class { 'nginx_example':
    conf_source => 'puppet:///modules/example/nginx-conf',   # задаём параметры класса точно так же, как параметры для других ресурсов
  }
}

У Puppet змінні типизовані. Є багато типів даних. Типи даних зазвичай використовуються для валідації значень параметрів, що передаються класи і дефайни. Якщо переданий параметр не відповідає вказаному типу, може виникнути помилка компіляції.

Тип пишеться безпосередньо перед ім'ям параметра:

class example (
  String $param1,
  Integer $param2,
  Array $param3,
  Hash $param4,
  Hash[String, String] $param5,
) {
  ...
}

Класи: include classname vs class{'classname':}

Кожен клас є ресурсом типу клас. Як і у випадку з будь-якими іншими типами ресурсів, на одній ноді не може бути два екземпляри одного і того ж класу.

Якщо спробувати додати клас на ту саму ноду двічі за допомогою class { 'classname':} (Без різниці, з різними або з однаковими параметрами), буде помилка компіляції. Зате у разі використання класу в стилі ресурсу можна відразу в маніфесті явно задати всі його параметри.

Однак якщо використовувати include, то клас можна додавати скільки завгодно разів. Справа в тому що include — ідемпотентна функція, яка перевіряє, чи додано клас до каталогу. Якщо класу в каталозі немає - додає його, а якщо вже є, нічого не робить. Але у разі використання include не можна задати параметри класу під час оголошення класу - всі обов'язкові параметри повинні бути задані у зовнішньому джерелі даних Hiera або ENC. Про них ми поговоримо у наступній статті.

Дефайни

Як було сказано в попередньому блоці, той самий клас не може бути присутнім на ноді більше одного разу. Однак у деяких випадках потрібно мати можливість застосовувати той самий блок коду з різними параметрами на одній ноді. Іншими словами, є потреба у власному типі ресурсу.

Наприклад, для того, щоб встановити модуль PHP, ми в Авіто робимо таке:

  1. Встановлюємо пакет із цим модулем.
  2. Створюємо файл конфігурації для цього модуля.
  3. Створюємо симлінк на конфіг для php-fpm.
  4. Створюємо симлінк на конфіг для php cli.

У таких випадках використовується така конструкція, як дефайн (define, defined type, defined resource type). Дефайн схожий клас, але є відмінності: по-перше, кожен дефайн є типом ресурсу, а чи не ресурсом; по-друге, кожен дефайн має неявний параметр $title, куди потрапляє ім'я ресурсу за його оголошенні. Як і у випадку з класами, дефайн спочатку потрібно описати, після чого його можна використовувати.

Спрощений приклад із модулем для PHP:

define php74::module (
  $php_module_name = $title,
  $php_package_name = "php7.4-${title}",
  $version = 'installed',
  $priority = '20',
  $data = "extension=${title}.son",
  $php_module_path = '/etc/php/7.4/mods-available',
) {
  package { $php_package_name:
    ensure          => $version,
    install_options => ['-o', 'DPkg::NoTriggers=true'],  # триггеры дебиановских php-пакетов сами создают симлинки и перезапускают сервис php-fpm - нам это не нужно, так как и симлинками, и сервисом мы управляем с помощью Puppet
  }
  -> file { "${php_module_path}/${php_module_name}.ini":
    ensure  => $ensure,
    content => $data,
  }
  file { "/etc/php/7.4/cli/conf.d/${priority}-${php_module_name}.ini":
    ensure  => link,
    target  => "${php_module_path}/${php_module_name}.ini",
  }
  file { "/etc/php/7.4/fpm/conf.d/${priority}-${php_module_name}.ini":
    ensure  => link,
    target  => "${php_module_path}/${php_module_name}.ini",
  }
}

node server3.testdomain {
  php74::module { 'sqlite3': }
  php74::module { 'amqp': php_package_name => 'php-amqp' }
  php74::module { 'msgpack': priority => '10' }
}

У дефайні найпростіше зловити помилку Duplicate declaration. Це відбувається, якщо в дефайні є ресурс з константним ім'ям, і на якійсь ноді два і більше екземплярів цього дефайну.

Захиститися від цього просто: всі ресурси всередині дефайну повинні мати назву, що залежить від $title. Як альтернатива — ідемпотентне додавання ресурсів, у найпростішому випадку достатньо винести загальні для всіх екземплярів дефайну ресурси в окремий клас та інклюдити цей клас у дефайні — функція include ідемпотентна.

Є й інші способи досягти ідемпотентності при додаванні ресурсів, а саме використання функцій defined и ensure_resources, але про це розповім у наступній серії.

Залежності та повідомлення для класів та дефайнів

Класи та дефайни додають такі правила до обробки залежностей та повідомлень:

  • залежність від класу/дефайну додає залежність від усіх ресурсів класу/дефайну;
  • залежність класу/дефайну додає залежності всіх ресурсів класу/дефайну;
  • повідомлення класу/дефайну повідомляє всі ресурси класу/дефайну;
  • підписка на клас/дефайн підписує на всі ресурси класу/дефайну.

Умовні оператори та селектори

Документація тут.

if

Тут все просто:

if ВЫРАЖЕНИЕ1 {
  ...
} elsif ВЫРАЖЕНИЕ2 {
  ...
} else {
  ...
}

якщо не

unless - це if навпаки: блок коду буде виконаний, якщо вираз хибний.

unless ВЫРАЖЕНИЕ {
  ...
}

випадок

Тут також нічого складного. Як значення можна використовувати звичайні значення (рядки, числа і так далі), регулярні вирази, а також типи даних.

case ВЫРАЖЕНИЕ {
  ЗНАЧЕНИЕ1: { ... }
  ЗНАЧЕНИЕ2, ЗНАЧЕНИЕ3: { ... }
  default: { ... }
}

Селектори

Селектор – це мовна конструкція, схожа на caseтільки замість виконання блоку коду вона повертає значення.

$var = $othervar ? { 'val1' => 1, 'val2' => 2, default => 3 }

Модулі

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

Крім того, є проблема перевикористання коду — коли весь код в одному маніфесті складно цим кодом ділитися з іншими. Для вирішення цих двох проблем у Puppet є така суть, як модулі.

Модулі - Це набори класів, дефайнів та інших Puppet-сутностей, винесених в окрему директорію. Іншими словами, модуль – це незалежний шматок Puppet-логіки. Наприклад, може бути модуль для роботи з nginx, і в ньому буде те і тільки те, що потрібно для роботи з nginx, а може бути модуль для роботи з PHP і так далі.

Модулі версіонуються, також підтримуються залежність модулів один від одного. Є відкритий репозиторій модулів. Лялькова кузня.

На паппет-сервері модулі лежать у піддиректорії modules кореневої директорії. Усередині кожного модуля стандартна схема директорій - manifests, files, templates, lib тощо.

Структура файлів у модулі

У корені модуля можуть бути наступні директорії з назвами:

  • manifests - У ній лежать маніфести
  • files - У ній лежать файли
  • templates - У ній лежать шаблони
  • lib - У ній лежить Ruby-код

Це не повний список директорій та файлів, але для цієї статті поки що достатньо.

Назви ресурсів та імена файлів у модулі

Документація тут.

Ресурси (класи, дефайни) у модулі не можна називати як завгодно. Крім того, є пряма відповідність між назвою ресурсу та ім'ям файлу, в якому Puppet шукатиме опис цього ресурсу. Якщо порушувати правила іменування, то Puppet не знайде опис ресурсів, і вийде помилка компіляції.

Правила прості:

  • Усі ресурси в модулі повинні бути в неймспейсі модуля. Якщо модуль називається foo, то всі ресурси в ньому мають називатися foo::<anything>, або просто foo.
  • Ресурс з назвою модуля має бути у файлі init.pp.
  • Для інших ресурсів схема іменування файлів така:
    • префікс із ім'ям модуля відкидається
    • всі подвійні двокрапки, якщо вони є, замінюються на сліші
    • дописується розширення .pp

Продемонструю на прикладі. Припустимо, я пишу модуль nginx. У ньому є такі ресурси:

  • клас nginx описаний у маніфесті init.pp;
  • клас nginx::service описаний у маніфесті service.pp;
  • дефайн nginx::server описаний у маніфесті server.pp;
  • дефайн nginx::server::location описаний у маніфесті server/location.pp.

Шаблони

Напевно ви і самі знаєте, що таке шаблони, не розписуватиму тут докладно. Але про всяк випадок залишу посилання на Вікіпедію.

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

file { '/tmp/example': content => template('modulename/templatename.erb')

Шлях виду <modulename>/<filename> має на увазі файл <rootdir>/modules/<modulename>/templates/<filename>.

Крім того, є функція inline_template — їй на вхід передається текст шаблону, а чи не ім'я файла.

Всередині шаблонів можна використовувати всі змінні Puppet у поточній області видимості.

Puppet підтримує шаблони у форматі ERB та EPP:

Коротко про ERB

Керуючі конструкції:

  • <%= ВЫРАЖЕНИЕ %> - Вставити значення виразу
  • <% ВЫРАЖЕНИЕ %> - Обчислити значення вираз (не вставляючи його). Сюди просто йдуть умовні оператори (if), цикли (each).
  • <%# КОММЕНТАРИЙ %>

Вирази в ERB пишуться на Ruby (власне, ERB – це Embedded Ruby).

Для доступу до змінних із маніфесту потрібно дописати @ до імені змінної. Щоб прибрати переклад рядка, що з'являється після керуючої конструкції, потрібно використовувати тег, що закриває -%>.

Приклад використання шаблону

Припустимо, я пишу модуль для керування ZooKeeper. Клас, який відповідає за створення конфіга, виглядає приблизно так:

class zookeeper::configure (
  Array[String] $nodes,
  Integer $port_client,
  Integer $port_quorum,
  Integer $port_leader,
  Hash[String, Any] $properties,
  String $datadir,
) {
  file { '/etc/zookeeper/conf/zoo.cfg':
    ensure  => present,
    content => template('zookeeper/zoo.cfg.erb'),
  }
}

А відповідний йому шаблон zoo.cfg.erb - Так:

<% if @nodes.length > 0 -%>
<% @nodes.each do |node, id| -%>
server.<%= id %>=<%= node %>:<%= @port_leader %>:<%= @port_quorum %>;<%= @port_client %>
<% end -%>
<% end -%>

dataDir=<%= @datadir %>

<% @properties.each do |k, v| -%>
<%= k %>=<%= v %>
<% end -%>

Факти та вбудовані змінні

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

Для отримання інформації про ноди Puppet є такий механізм, як факти. Факти - Це інформація про ноду, доступна в маніфестах у вигляді звичайних змінних у глобальному просторі імен. Наприклад, ім'я хоста, версія операційної системи, архітектура процесора, список користувачів, список мережевих інтерфейсів та їх адрес, і багато, багато іншого. Факти доступні в маніфестах та шаблонах як звичайні змінні.

Приклад роботи з фактами:

notify { "Running OS ${facts['os']['name']} version ${facts['os']['release']['full']}": }
# ресурс типа notify просто выводит сообщение в лог

Якщо говорити формально, то факт має ім'я (рядок) і значення (доступні різні типи: рядки, масиви, словники). Є набір вбудованих фактів. Також можна писати власні. Складачі фактів описуються як функції на Ruby, Або як виконувані файли. Також факти можуть бути подані у вигляді текстових файлів з даними на нодах.

Під час роботи паппет-агент спочатку копіює з паппетсервера на ноду всі доступні збирачі фактів, після чого запускає їх та відправляє на сервер зібрані факти; вже після цього сервер розпочинає компіляцію каталогу.

Факти у вигляді виконуваних файлів

Такі факти кладуться у модулі в директорію facts.d. Зрозуміло, що файли повинні бути виконуваними. При запуску вони повинні виводити на стандартний висновок інформацію у форматі YAML, або у форматі «ключ = значення».

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

#!/bin/sh
echo "testfact=success"
#!/bin/sh
echo '{"testyamlfact":"success"}'

Факти на Ruby

Такі факти кладуться у модулі в директорію lib/facter.

# всё начинается с вызова функции Facter.add с именем факта и блоком кода
Facter.add('ladvd') do
# в блоках confine описываются условия применимости факта — код внутри блока должен вернуть true, иначе значение факта не вычисляется и не возвращается
  confine do
    Facter::Core::Execution.which('ladvdc') # проверим, что в PATH есть такой исполняемый файл
  end
  confine do
    File.socket?('/var/run/ladvd.sock') # проверим, что есть такой UNIX-domain socket
  end
# в блоке setcode происходит собственно вычисление значения факта
  setcode do
    hash = {}
    if (out = Facter::Core::Execution.execute('ladvdc -b'))
      out.split.each do |l|
        line = l.split('=')
        next if line.length != 2
        name, value = line
        hash[name.strip.downcase.tr(' ', '_')] = value.strip.chomp(''').reverse.chomp(''').reverse
      end
    end
    hash  # значение последнего выражения в блоке setcode является значением факта
  end
end

Текстові факти

Такі факти кладуться на ноди у директорію /etc/facter/facts.d у старому Паппеті або /etc/puppetlabs/facts.d у новому Паппеті.

examplefact=examplevalue
---
examplefact2: examplevalue2
anotherfact: anothervalue

Звернення до фактів

Звернутися до фактів можна двома способами:

  • через словник $facts: $facts['fqdn'];
  • використовуючи ім'я факту як ім'я змінної: $fqdn.

Найкраще використовувати словник $facts, а ще краще вказувати глобальний неймспейс ($::facts).

Ось необхідний розділ документації.

Вбудовані змінні

Крім фактів, є ще деякі зміннідоступні у глобальному просторі імен.

  • trusted facts — змінні, які беруться із сертифікату клієнта (оскільки сертифікат зазвичай випускається на паппет-сервері, агент не може просто так взяти та поміняти свій сертифікат, тому змінні та «довірені»): назва сертифіката, ім'я хоста та домену, розширення із сертифікату.
  • server facts —змінні, що стосуються інформації про сервер — версія, ім'я, IP-адреса сервера, оточення.
  • agent facts - Змінні, що додаються безпосередньо puppet-agent'ом, а не facter'ом - назва сертифіката, версія агента, версія паппета.
  • master variables - Змінні паппетмастера (sic!). Там приблизно те саме, що в server factsплюс доступні значення конфігураційних параметрів.
  • compiler variables — змінні компілятора, які різняться у кожній області видимості: ім'я поточного модуля та ім'я модуля, в якому було звернення до поточного об'єкта. Їх можна використовувати, наприклад, щоб перевіряти, чи ваші приватні класи не використовують безпосередньо з інших модулів.

Додаток 1: як це все запускати та дебатувати?

У статті було багато прикладів puppet-коду, але зовсім не було розказано, як цей код запускати. Що ж, виправляюсь.

Для роботи Puppet достатньо агента, але для більшості випадків буде потрібний і сервер.

агент

Як мінімум з п'ятої версії пакети puppet-agent з офіційного репозиторію Puppetlabs містять у собі всі залежності (ruby і відповідні gem'и), тому складнощів з установкою ніяких немає (говорю про Debian-based дистрибутиви - RPM-based дистрибутивами ми не користуємося).

У найпростішому випадку для застосування puppet-конфігурації достатньо запустити агент у безсерверному режимі: за умови, що puppet-код скопійований на ноду, запускаєте puppet apply <путь к манифесту>:

atikhonov@atikhonov ~/puppet-test $ cat helloworld.pp 
node default {
    notify { 'Hello world!': }
}
atikhonov@atikhonov ~/puppet-test $ puppet apply helloworld.pp 
Notice: Compiled catalog for atikhonov.localdomain in environment production in 0.01 seconds
Notice: Hello world!
Notice: /Stage[main]/Main/Node[default]/Notify[Hello world!]/message: defined 'message' as 'Hello world!'
Notice: Applied catalog in 0.01 seconds

Краще, звичайно, підняти сервер і запустити агенти на нодах в режимі демона - тоді раз на півгодини вони будуть застосовувати конфігурацію, завантажену з сервера.

Можна імітувати push-модель роботи — зайти на ноду, що вас цікавить, і запустити sudo puppet agent -t. Ключ -t (--test) насправді включає кілька опцій, які можна включати і окремо. Серед цих опцій такі:

  • не працювати в режимі демона (за умовчанням агент запускається в режимі демона);
  • завершити роботу після застосування каталогу (за умовчанням агент продовжить роботу і застосовуватиме конфігурацію раз на півгодини);
  • писати докладний лог роботи;
  • показувати зміни у файлах.

У агента є режим роботи без змін - ним можна користуватися у випадку, коли ви не впевнені, що написали коректну конфігурацію, і хочете перевірити, що саме змінить агент під час роботи. Увімкнено цей режим параметром --noop у командному рядку: sudo puppet agent -t --noop.

Крім того, можна включити налагоджувальний лог роботи - в ньому puppet пише про всі дії, які він робить: про ресурс, який в даний момент обробляє, про параметри цього ресурсу, про те, які програми запускає. Зрозуміло, це параметр --debug.

Сервер

Повноцінне налаштування паппетсервера і деплою на нього коду в цій статті я не розглядатиму, скажу лише, що з коробки ставиться цілком працездатна версія сервера, що не вимагає додаткового налаштування для роботи в умовах невеликої кількості нід (скажімо, до ста). Більшість нод вже вимагатиме тюнінгу - за замовчуванням puppetserver запускає не більше чотирьох воркерів, для більшої продуктивності потрібно збільшити їх кількість і не забути збільшити ліміти пам'яті, інакше більшу частину часу сервер буде garbage collect'ити.

Деплой коду — якщо потрібно швидко і просто, дивіться (на r10k)[https://github.com/puppetlabs/r10k], для невеликих інсталяцій його цілком вистачить.

Додаток 2: рекомендації щодо написання коду

  1. Виносите всю логіку в класи та дефайни.
  2. Тримайте класи та дефайни в модулях, а не в маніфестах з описом нід.
  3. Використовуйте факти.
  4. Не робіть if'ів за хостнеймами.
  5. Не соромтеся додавати параметри для класів та дефайнів – це краще, ніж неявна логіка, захована у тілі класу/дефайну.

А чому я рекомендую так робити – поясню у наступній статті.

Висновок

На цьому закінчимо із запровадженням. У наступній статті розповім про Hiera, ENC та PuppetDB.

Тільки зареєстровані користувачі можуть брати участь в опитуванні. Увійдіть, будь ласка.

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

  • 59,1%Advanced puppet constructs — some next-level shit: цикли, меппінг та інші лямбда-вираження, колектори ресурсів, експортовані ресурси та міжхостова взаємодія через Puppet, теги, провайдери, абстрактні типи даних.
  • 31,8%«Я у мамки адмін» або як ми в Авіто подружили кілька паппет-серверів різних версій, ну і в принципі частина про адміністрування паппет-сервера.
  • 81,8%Як пишемо паппет-код: інструментальна обв'язка, документація, тестування, CI/CD.18

Проголосували 22 користувачів. Утрималися 9 користувачів.

Джерело: habr.com