Destpêka Puppet

Puppet — это система управления конфигурацией. Он используется для приведения хостов к нужному состоянию и поддержания этого состояния.

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

Destpêka Puppet

Bazovaya agahî

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

Используется pull-модель работы: по умолчанию раз в полчаса клиенты обращаются к серверу за конфигурацией и применяют её. Если вы работали с Ansible, то там используется другая, push-модель: администратор инициирует процесс применения конфигурации, сами по себе клиенты ничего применять не будут.

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

Знакомство с манифестами

В терминологии Puppet к паппет-серверу подключаются nodes (nodes). Конфигурация для нод пишется в манифестах на специальном языке программирования — Puppet DSL.

Puppet DSL — декларативный язык. На нём описывается желаемое состояние ноды в виде объявления отдельных ресурсов, например:

  • Файл существует, и у него определённое содержимое.
  • Пакет установлен.
  • Сервис запущен.

Ресурсы могут быть взаимосвязаны:

  • Есть зависимости, они влияют на порядок применения ресурсов.
    Например, «сначала установи пакет, затем поправь конфигурационный файл, после этого запусти сервис».
  • Есть уведомления — если ресурс изменился, он отправляет уведомления подписанным на него ресурсам.
    Например, если изменяется конфигурационный файл, можно автоматически перезапускать сервис.

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

Puppet написан на Ruby, поэтому многие конструкции и термины взяты оттуда. Ruby позволяет расширять Puppet — дописывать сложную логику, новые типы ресурсов, функции.

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

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

Вот разделы официальной документации, которые помогут разобраться с синтаксисом, если приведённых примеров будет недостаточно:

Вот пример того, как выглядит манифест:

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

Отступы и переводы строк не являются обязательной частью манифеста, однако есть рекомендованный rêberê şêwazê. Краткое изложение:

  • Двухпробельные отступы, табы не используются.
  • Фигурные скобки отделяются пробелом, двоеточие пробелом не отделяется.
  • Запятые после каждого параметра, в том числе последнего. Каждый параметр — на отдельной строке. Исключение делается для случая без параметров и одного параметра: можно писать на одной строке и без запятой (т.е. 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

Типы ресурсов

Полный список поддерживаемых типов ресурсов находится в документации, здесь же я опишу пять базовых типов, которых в моей практике хватает для решения большинства задач.

dosî

Управляет файлами, директориями, симлинками, их содержимым, правами доступа.

Parametre:

  • название ресурса — путь к файлу (опционально)
  • şop — путь к файлу (если он не задан в названии)
  • pêredîtin — тип файла:
    • absent — удалить файл
    • present — должен быть файл любого типа (если файла нет — будет создан обычный файл)
    • file — обычный файл
    • directory — директория
    • link — симлинк
  • naveroka — содержимое файла (подходит только для обычных файлов, нельзя использовать вместе с kanî an armanc)
  • kanî — ссылка на путь, из которого нужно копировать содержимое файла (нельзя использовать вместе с naveroka an armanc). Может быть задана как в виде URI со схемой puppet: (тогда будут использованы файлы с паппет-сервера), так и со схемой http: (надеюсь, понятно, что будет в этом случае), и даже со схемой file: или в виде абсолютного пути без схемы (тогда будет использован файл с локальной ФС на ноде)
  • armanc — куда должен указывать симлинк (нельзя использовать вместе с naveroka an kanî)
  • xwedî — пользователь, которому должен принадлежать файл
  • kom — группа, которой должен принадлежать файл
  • awa — права на файл (в виде строки)
  • recurse — включает рекурсивную обработку директорий
  • paqij — включает удаление файлов, которые не описаны в Puppet
  • cebir — включает удаление директорий, которые не описаны в Puppet

pakêt

Устанавливает и удаляет пакеты. Умеет обрабатывать уведомления — переустанавливает пакет, если задан параметр reinstall_on_refresh.

Parametre:

  • название ресурса — название пакета (опционально)
  • nav — название пакета (если не задано в названии)
  • Pêşkêşker — пакетный менеджер, который нужно использовать
  • pêredîtin — желаемое состояние пакета:
    • present, installed — установлена любая версия
    • latest — установлена последняя версия
    • absent — удалён (apt-get remove)
    • purged — удалён вместе с конфигурационными файлами (apt-get purge)
    • held — версия пакета заблокирована (apt-mark hold)
    • любая другая строка — установлена указанная версия
  • reinstall_on_refresh - eslî true, то при получении уведомления пакет будет переустановлен. Полезно для source-based дистрибутивов, где пересборка пакетов может быть необходима при изменении параметров сборки. По умолчанию false.

xizmetkar

Управляет сервисами. Умеет обрабатывать уведомления — перезапускает сервис.

Parametre:

  • название ресурса — сервис, которым нужно управлять (опционально)
  • nav — сервис, которым нужно управлять (если не задано в названии)
  • pêredîtin — желаемое состояние сервиса:
    • running — запущен
    • stopped — остановлен
  • bikêrkirin — управляет возможностью запуска сервиса:
    • true — включен автозапуск (systemctl enable)
    • mask — замаскирован (systemctl mask)
    • false — выключен автозапуск (systemctl disable)
  • veguhestin — команда для перезапуска сервиса
  • — команда для проверки статуса сервиса
  • hasrestart — указать, поддерживает ли инитскрипт сервиса перезапуск. Если false и указан параметр veguhestin — используется значение этого параметра. Если false и параметр veguhestin не указан — сервис останавливается и запускается для перезапуска (но в systemd используется команда systemctl restart).
  • hasstatus — указать, поддерживает ли инитскрипт сервиса команду status. Ger false, то используется значение параметра . По умолчанию true.

exec

Запускает внешние команды. Если не указывать параметры dike, onlyif, Heger an refreshonly, команда будет запускаться при каждом прогоне Паппета. Умеет обрабатывать уведомления — запускает команду.

Parametre:

  • название ресурса — команда, которую нужно выполнить (опционально)
  • ferman — команда, которую нужно выполнить (если она не задана в названии)
  • şop — пути, в которых искать исполняемый файл
  • onlyif — если указанная в этом параметре команда завершилась с нулевым кодом возврата, основная команда будет выполнена
  • Heger — если указанная в этом параметре команда завершилась с ненулевым кодом возврата, основная команда будет выполнена
  • dike — если указанный в этом параметре файл не существует, основная команда будет выполнена
  • refreshonly - eslî true, то команда будет запущена только в том случае, когда этот exec получает уведомление от других ресурсов
  • cwd — директория, из которой запускать команду
  • bikaranîvan — пользователь, от которого запускать команду
  • Pêşkêşker — с помощью чего запускать команду:
    • posix — просто создаётся дочерний процесс, обязательно указывать şop
    • legan — команда запускается в шелле /bin/sh, можно не указывать şop, можно использовать глоббинг, пайпы и прочие фичи шелла. Обычно определяется автоматически, если есть всякие спецсимволы (|, ;, &&, || и так далее).

cron

Управляет кронджобами.

Parametre:

  • название ресурса — просто какой-то идентификатор
  • pêredîtin — состояние кронджоба:
    • present — создать, если не существует
    • absent — удалить, если существует
  • ferman — какую команду запускать
  • dor — в каком окружении запускать команду (список переменных окружения и их значений через =)
  • bikaranîvan — от какого пользователя запускать команду
  • deqqe, seet, roja hefteyê, meh, monthday — когда запускать крон. Если какой-то из этих аттрибутов не указан, его значением в кронтабе будет *.

В Puppet 6.0 cron wek ku удалили из коробки в puppetserver, поэтому нет документации на общем сайте. Но он есть в коробке в puppet-agent, поэтому ставить его отдельно не надо. Документацию по нему можно посмотреть в документации к пятой версии Паппетаan jî на Гитхабе.

Про ресурсы в общем

Требования к уникальности ресурсов

Самая частая ошибка, с которой мы встречаемся — Duplicate declaration. Эта ошибка возникает, когда в каталог попадают два и более ресурса одинакового типа с одинаковым названием.

Поэтому ещё раз напишу: в манифестах для одной ноды не должно быть ресурсов одинакового типа с одинаковым названием (title)!

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

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

В других типах ресурсов есть аналогичные параметры, помогающие избежать дубликации, — name у xizmetkar, command у exec, и так далее.

Метапараметры

Некоторые специальные параметры есть у каждого типа ресурса, независимо от его сущности.

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

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

  • xwestin — в этом параметре указывается, от каких ресурсов зависит данный ресурс.
  • berî — в этом параметре указывается, какие ресурсы зависят от данного ресурса.
  • qeydkirin — в этом параметре указывается, от каких ресурсов получает уведомления данный ресурс.
  • agahdan — в этом параметре указывается, какие ресурсы получают уведомления от данного ресурса.

Все перечисленные метапараметры принимают либо одну ссылку на ресурс, либо массив ссылок в квадратных скобках.

Ссылки на ресурсы

Ссылка на ресурс — это просто упоминание ресурса. Используются они в основном для указания зависимостей. Ссылка на несуществующий ресурс вызовет ошибку компиляции.

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

Nimûne:

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

Зависимости и уведомления

Документация здесь.

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

В отличие от зависимостей, уведомления не транзитивны. Для уведомлений действуют следующие правила:

  • Если ресурс получает уведомление, он обновляется. Действия при обновлении зависят от типа ресурса — exec запускает команду, xizmetkar перезапускает сервис, pakêt переустанавливает пакет. Если для ресурса не определено действие при обновлении, то ничего не происходит.
  • За один прогон Паппета ресурс обновляется не больше одного раза. Это возможно, так как уведомления включают в себя зависимости, а граф зависимостей не содержит циклов.
  • Если Паппет меняет состояние ресурса, то ресурс отправляет уведомления всем подписанным на него ресурсам.
  • Если ресурс обновляется, то он отправляет уведомления всем подписанным на него ресурсам.

Обработка неуказанных параметров

Как правило, если у какого-то параметра ресурса нет значения по умолчанию и этот параметр не указан в манифесте, то Паппет не будет менять это свойство у соответствующего ресурса на ноде. Например, если у ресурса типа dosî не указан параметр owner, то Паппет не будет менять владельца у соответствующего файла.

Знакомство с классами, переменными и дефайнами

Предположим, у нас несколько нод, на которых есть одинаковая часть конфигурации, но есть и различия — иначе мы могли бы описать это всё в одном блоке node {}. Конечно, можно просто скопировать одинаковые части конфигурации, но в общем случае это плохое решение — конфигурация разрастается, при изменении общей части конфигурации придётся править одно и то же во множестве мест. При этом легко ошибиться, ну и вообще принцип DRY (don’t repeat yourself) не просто так придумали.

Для решения такой проблемы есть такая конструкция, как класс.

Dersa

Çar — это именованный блок паппет-кода. Классы нужны для переиспользования кода.

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

# Описание класса начинается с ключевого слова 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
}

Guhêrbar

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

Dikare bê kirin с помощью переменных.

Внимание: переменные в 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’:}

Каждый класс является ресурсом типа sinif. Как и в случае с любыми другими типами ресурсов, на одной ноде не может присутствовать два экземпляра одного и того же класса.

Если попробовать добавить класс на одну и ту же ноду два раза с помощью 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 {
  ...
}

Heger

unless — это if наоборот: блок кода будет выполнен, если выражение ложно.

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

doz

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

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

Селекторы

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

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

Modûl

Когда конфигурация маленькая, её легко можно держать в одном манифесте. Но чем больше конфигурации мы описываем, тем больше классов и нод становится в манифесте, он разрастается, с ним становится неудобно работать.

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

Modûl — это наборы классов, дефайнов и прочих Puppet-сущностей, вынесенных в отдельную директорию. Иными словами, модуль — это независимый кусок Puppet-логики. Например, может быть модуль для работы с nginx, и в нём будет то и только то, что нужно для работы с nginx, а может быть модуль для работы с PHP, и так далее.

Модули версионируются, также поддерживаются зависимости модулей друг от друга. Есть открытый репозиторий модулей — Puppet Forge.

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

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

В корне модуля могут быть следующие директории с говорящими названиями:

  • manifests — в ней лежат манифесты
  • files — в ней лежат файлы
  • templates — в ней лежат шаблоны
  • lib — в ней лежит Ruby-код

Это не полный список директорий и файлов, но для этой статьи пока достаточно.

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

Документация здесь.

Ресурсы (классы, дефайны) в модуле нельзя называть как угодно. Кроме того, есть прямое соответствие между названием ресурса и именем файла, в котором Puppet будет искать описание этого ресурса. Если нарушать правила именования, то Puppet просто не найдёт описание ресурсов, и получится ошибка компиляции.

Qanûn hêsan in:

  • Все ресурсы в модуле должны быть в неймспейсе модуля. Если модуль называется 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.

Sêwiran

Наверняка вы и сами знаете, что такое шаблоны, не буду расписывать здесь подробно. Но на всякий случай оставлю ссылку на Википедию.

Как использовать шаблоны: значение шаблона можно раскрыть с помощью функции template, которой передаётся путь к шаблону. Для ресурсов типа dosî используем вместе с параметром 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 есть такой механизм, как факты. Rastiyan — это информация о ноде, доступная в манифестах в виде обычных переменных в глобальном пространстве имён. Например, имя хоста, версия операционной системы, архитектура процессора, список пользователей, список сетевых интерфейсов и их адресов, и многое, многое другое. Факты доступны в манифестах и шаблонах как обычные переменные.

Пример работы с фактами:

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 достаточно агента, но для большинства случаев нужен будет и сервер.

Agent

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

В простейшем случае для применения puppet-конфигурации достаточно запустить агент в беcсерверном режиме: при условии, что 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. Qûfle -t (--test) на самом деле включает несколько опций, которые можно включать и по отдельности. Среди этих опций следующие:

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

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

Кроме того, можно включить отладочный лог работы — в нём puppet пишет обо всех действиях, которые он производит: о ресурсе, который в данный момент обрабатывает, о параметрах этого ресурса, о том, какие программы запускает. Разумеется, это параметр --debug.

Server

Полноценную настройку паппетсервера и деплой на него кода в этой статье я не буду рассматривать, скажу лишь, что из коробки ставится вполне работоспособная версия сервера, не требующая дополнительной настройки для работы в условиях небольшого количества нод (скажем, до ста). Большее количество нод уже потребует тюнинга — по умолчанию puppetserver запускает не больше четырёх воркеров, для большей производительности нужно увеличить их число и не забыть увеличить лимиты памяти, иначе большую часть времени сервер будет garbage collect’ить.

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

Дополнение 2: рекомендации по написанию кода

  1. Выносите всю логику в классы и дефайны.
  2. Держите классы и дефайны в модулях, а не в манифестах с описанием нод.
  3. Пользуйтесь фактами.
  4. Не делайте if’ов по хостнеймам.
  5. Не стесняйтесь добавлять параметры для классов и дефайнов — это лучше, чем неявная логика, спрятанная в теле класса/дефайна.

А почему я рекомендую так делать — объясню в следующей статье.

encamê

На этом закончим со введением. В следующей статье расскажу про Hiera, ENC и PuppetDB.

Tenê bikarhênerên qeydkirî dikarin beşdarî anketê bibin. Têketinji kerema xwe.

На самом деле, материала гораздо больше — я могу написать статьи на следующие темы, проголосуйте, о чём вам интересно было бы почитать:

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

22 bikarhêneran deng dan. 9 bikarhêner jî betal bûn.

Source: www.habr.com