🐹 Puppet 3. Часть 2: Манифесты, синтаксис, команды.

Содержание:

1. Синтаксис и коды.
2. Расположение файлов на Puppet master.
3. Ресурсы.

3.1. Ресурс — file.
3.2. Ресурс — package.
3.3. Ресурс — service.
3.4. Ресурс — exec.
3.5. Ресурс — cron.

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

4.1. Требования к уникальности ресурсов.
4.2. Метапараметры.
4.3. Ссылки на ресурсы.
4.4. Зависимости и уведомления.
4.5. Обработка неуказанных параметров.

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

5.1. Классы.
5.2. Переменные.
5.3. Классы: include classname vs class{‘classname’:}.
5.4. Дефайны.
5.5. Зависимости и уведомления для классов и дефайнов.

6. Условные операторы и селекторы.

6.1. Операторы.
6.2. Селекторы.

7. Модули.

7.1. Структура файлов в модуле.
7.2. Названия ресурсов и имена файлов в модуле.

8. Шаблоны.

8.1. Вкратце про ERB.
8.2. Пример использования шаблона.

9. Факты и встроенные переменные.

9.1. Факты в виде исполняемых файлов.
9.2. Факты на Ruby.
9.3. Текстовые факты.
9.4. Обращение к фактам.
9.5. Встроенные переменные.

10. Немного о кроссдистрибутивности.
11. Оригиналы источников информации.


Данная статья — это часть цикла статей по эксплуатации системы управления конфигурацией — Puppet 3:

  1. Часть I: статья «Установка, настройка, начало эксплуатации»
  2. Часть II: статья «Манифесты, синтаксис, команды». <— Вы здесь!

На чем было опробовано:

  1. CentOS Linux release 7.9.2009 (Core).
  2. Puppet 3.8.7 (master+agent).

1. Синтаксис и коды.

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

Комментарии пишутся, как и много где, после решётки.

Описание конфигурации ноды начинается с ключевого слова node, за которым следует селектор ноды — хостнейм (с доменом или без) или регулярное выражение для hostname, или ключевое слово default.

После этого в фигурных скобках описывается собственно конфигурация ноды.

Одна и та же нода может попасть под несколько селекторов. Про приоритет селекторов написано в статье про синтаксис описания нод в официальной документации Puppet.

Конфигурация по сути является перечислением ресурсов и их параметров. У каждого ресурса есть тип и название.

Внимание! Не может быть двух ресурсов одного типа с одинаковыми названиями!

Описание ресурса начинается с его типа. Тип пишется в нижнем регистре.

Про разные типы ресурсов написано ниже.

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

Значения параметров указываются через, так называемый, hash rocket (=>).

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

node 'hostname', 'f.q.d.n', /regexp/ {
resource { 'title':
  param1 => value1,
  param2 => value2,
  param3 => value3,
  }
}

Отступы и переводы строк не являются обязательной частью манифеста, однако есть рекомендованный style guide: https://puppet.com.

Краткое изложение:

  • Отступы с двумя пробелами, табуляция не не используются.
  • Фигурные скобки отделяются пробелом, двоеточие пробелом не отделяется.
  • Запятые после каждого параметра, в том числе последнего.
  • Каждый параметр — на отдельной строке. Исключение делается для случая без параметров и одного параметра: можно писать на одной строке и без запятой (то есть resource { 'title': } и resource { 'title': param => value }).
  • Стрелки у параметров должны быть на одном уровне.
  • Стрелки взаимосвязи ресурсов пишутся перед ними.

2. Расположение файлов на Puppet master.

Для дальнейших объяснений введем понятие «корневая директория».

Корневая директория — это директория, в которой находится Puppet-конфигурация для конкретной ноды.

Корневая директория различается в зависимости от версии Puppet и использования окружений.

Окружения — это независимые наборы конфигурации, которые хранятся в отдельных директориях. Обычно используются в сочетании с гитом, в таком случае окружения создаются из веток гита. Соответственно, каждая нода находится в том или ином окружении. Это настраивается на самой ноде, либо в PuppetExternal Node Classifiers (ENC).

В третьей версии («старый Puppet») базовой директорией была /etc/puppet. Использование окружений опциональное — мы, например, их не используем со старым Puppet. Если окружения используются, то они обычно хранятся в /etc/puppet/environments, корневой директорией будет директория окружения. Если окружения не используются, корневой директорией будет базовая.

Начиная с четвёртой версии («новый Puppet») использование окружений стало обязательным, а базовую директорию перенесли в /etc/puppetlabs/code. Соответственно, окружения хранятся в /etc/puppetlabs/code/environments, корневая директория — директория окружения.

В корневой директории должна быть поддиректория manifests, в которой лежит один или несколько манифестов с описанием нод. Кроме того, там должна быть поддиректория modules, в которой лежат модули.

Кроме того, в старом Puppet также может быть поддиректория files, в которой лежат различные файлы, которые мы копируем на ноды. В новом Puppet же все файлы вынесены в модули.

Файлы манифестов имеют расширение *.pp.

3. Ресурсы.

Ресурс — это самая мелкая единица абстракции в Puppet.

Ресурсами могут быть:

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

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

В Puppet есть возможность добавлять свои ресурсы.

Например, webserver.pp:

include webserver;
   webserver::vhost { 'example.com':
   ensure => present,
   size => '1G',
   php => false,
   https => true 
}

Puppet при этом будет создавать logical volume размером в 1 ГиБ на сервере, монтировать его куда положено (например в /var/www/example.com), добавлять нужные записи в fstab, создавать нужные виртуальные хосты в nginx и apache, рестартовать оба демона, добавлять в ftp и sftp пользователя example.com с паролем mySuperSecretPassWord с доступом на запись в этот виртуальный хост.

Причем, самое интересное — это не автоматизация рутины, а регенерация своих серверов с нуля.

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

На сервере волшебным образом появятся пакеты, разложатся ваши ssh-ключи, установится фаервол, придут индивидуальные настройки bash, сети, установится и настроится весь софт, который вы предусмотрительно ставили с помощью Puppet.

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

Подробности есть на оригинальном сайте программного обеспечения:

3.1. Ресурс — file.

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

Параметры:

  • название ресурса — путь к файлу (опционально);
  • path — путь к файлу (если он не задан в названии);
  • ensure — тип файла:
    • absent — удалить файл;
    • present — должен быть файл любого типа (если файла нет — будет создан обычный файл);
    • file — обычный файл;
    • directory — директория;
    • link — симлинк.
  • content — содержимое файла (подходит только для обычных файлов, нельзя использовать вместе с source или target);
  • source — ссылка на путь, из которого нужно копировать содержимое файла (нельзя использовать вместе с content или target). Может быть задана как в виде URI со схемой puppet: (тогда будут использованы файлы с паппет-сервера), так и со схемой http: (надеюсь, понятно, что будет в этом случае), и даже со схемой file: или в виде абсолютного пути без схемы (тогда будет использован файл с локальной файловой системы на ноде);

Примеры:

    • puppet:///modules/<modulename>/<filename> будет брать файл с Puppet master по пути <корневая директория puppet>/modules/<modulename>/files/<filename>;
    • file:///path/to/file будет брать файл с самой ноды, на которой запущен Puppet node (не с Puppet master), по пути /path/to/file.
  • target — куда должен указывать симлинк (нельзя использовать вместе с content или source);
  • owner — пользователь, которому должен принадлежать файл;
  • group — группа, которой должен принадлежать файл;
  • mode — права на файл (в виде строки);
  • recurse — включает рекурсивную обработку директорий;
  • purge — включает удаление файлов, которые не описаны в Puppet;
  • force — включает удаление директорий, которые не описаны в Puppet.

3.2. Ресурс — package.

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

Параметры:

  • название ресурса — название пакета (опционально);
  • name — название пакета (если не задано в названии);
  • provider — пакетный менеджер, который нужно использовать;
  • ensure — желаемое состояние пакета:
    • present, installed — установлена любая версия;
    • latest — установлена последняя версия;
    • absent — удалён (apt-get remove);
    • purged — удалён вместе с конфигурационными файлами (apt-get purge);
    • held — версия пакета заблокирована (apt-mark hold);
    • любая другая строка — установлена указанная версия.
  • reinstall_on_refresh — если true, то при получении уведомления пакет будет переустановлен. Полезно для source-based дистрибутивов, где пересобирание пакетов может быть необходимо при изменении параметров сборки. По умолчанию false.

3.3. Ресурс — service.

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

Параметры:

  • название ресурса — сервис, которым нужно управлять (опционально);
  • name — сервис, которым нужно управлять (если не задано в названии);
  • ensure — желаемое состояние сервиса:
    • running — запущен;
    • stopped — остановлен.
  • enable — управляет возможностью запуска сервиса:
    • true — включен автозапуск (systemctl enable);
    • mask — замаскирован (systemctl mask);
    • false — выключен автозапуск (systemctl disable).
  • restart — команда для перезапуска сервиса;
  • status — команда для проверки статуса сервиса;
  • hasrestart — указать, поддерживает ли инитскрипт сервиса перезапуск. Если false и указан параметр restart — используется значение этого параметра. Если false и параметр restart не указан — сервис останавливается и запускается для перезапуска (но в systemd используется команда systemctl restart);
  • hasstatus — указать, поддерживает ли инитскрипт сервиса команду status. Если false, то используется значение параметра status. По умолчанию true.

3.4. Ресурс — exec.

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

Параметры:

  • название ресурса — команда, которую нужно выполнить (опционально);
  • command — команда, которую нужно выполнить (если она не задана в названии);
  • path — пути, в которых искать исполняемый файл;
  • onlyif — если указанная в этом параметре команда завершилась с нулевым кодом возврата, основная команда будет выполнена;
  • unless — если указанная в этом параметре команда завершилась с ненулевым кодом возврата, основная команда будет выполнена;
  • creates — если указанный в этом параметре файл не существует, основная команда будет выполнена;
  • refreshonly — если true, то команда будет запущена только в том случае, когда этот exec получает уведомление от других ресурсов;
  • cwd — директория, из которой запускать команду;
  • user — пользователь, от которого запускать команду;
  • provider — с помощью чего запускать команду:
    • posix — просто создаётся дочерний процесс, обязательно указывать path;
    • shell — команда запускается в шелле /bin/sh, можно не указывать path, можно использовать глоббинг, пайпы и прочие фичи шелла. Обычно определяется автоматически, если есть всякие спецсимволы (|, ;, &&, || и так далее).

3.5. Ресурс — cron.

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

Параметры:

  • название ресурса — просто какой-то идентификатор;
    ensure — состояние кронджоба:

    • present — создать, если не существует;
    • absent — удалить, если существует.
  • command — какую команду запускать;
  • environment — в каком окружении запускать команду (список переменных окружения и их значений через = равно );
  • user — от какого пользователя запускать команду;
  • minute, hour, weekday, month, monthday — когда запускать крон. Если какой-то из этих атрибутов не указан, его значением в кронтабе будет *.

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

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

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

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

Иногда есть необходимость поставить пакеты с одинаковым названием, но разными пакетными менеджерами.

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

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

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

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

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

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

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

  • require — в этом параметре указывается, от каких ресурсов зависит данный ресурс.
  • before — в этом параметре указывается, какие ресурсы зависят от данного ресурса.
  • subscribe — в этом параметре указывается, от каких ресурсов получает уведомления данный ресурс.
  • notify — в этом параметре указывается, какие ресурсы получают уведомления от данного ресурса.

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

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

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

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

Пример:

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

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

Оригинальная документация по вопросу данного раздела для Puppet 3 находится по этой ссылке: https://puppet.com.

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

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

Для уведомлений действуют следующие правила:

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

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

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

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

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

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

5.1. Классы.

Оригинальная документация по вопросу данного раздела для Puppet 3 находится по этой ссылке: https://puppet.com.

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

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

Класс описывается в манифестах:

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

5.2. Переменные.

Оригинальная документация по вопросу данного раздела для Puppet 3 находится по этой ссылке: https://puppet.com.

Класс из предыдущего примера совсем не гибок, потому что он всегда приносит одну и ту же конфигурацию 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
}

Однако приведённый пример плох тем, что есть некое «тайное знание» о том, что где-то внутри класса использует переменная с таким-то именем. Гораздо более правильно сделать это знание общим — у классов могут быть параметры.

Параметры класса — это переменные в пространстве имён класса, они задаются в заголовке класса и могут быть использованы как обычные переменные в теле класса. Значения параметров указывается при использовании класса в манифесте.

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

Давайте параметризуем класс из примера выше и добавим два параметра: первый, обязательный — путь к конфигурации, и второй, необязательный — название пакета с nginxDebian, например, есть пакеты 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 переменные типизированы. Есть много типов данных. Типы данных обычно используются для валидации значений параметров, передаваемых в классы и дефайны. Если переданный параметр не соответствует указанному типу, произойдёт ошибка компиляции.

Ссылка на официальную документация прилагаю: http://puppet.com.

Тип пишется непосредственно перед именем параметра:

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

5.3. Классы: include classname vs class{‘classname’:}.

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

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

Однако если использовать include, то класс можно добавлять сколько угодно раз. Дело в том, что include — идемпотентная функция, которая проверяет, добавлен ли класс в каталог. Если класса в каталоге нет — добавляет его, а если уже есть, то ничего не делает. Но в случае использования include нельзя задать параметры класса во время объявления класса — все обязательные параметры должны быть заданы во внешнем источнике данных — Hiera или ENC.

5.4. Дефайны.

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

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

  1. Устанавливаем пакет с этим модулем.
  2. Создаём конфигурационный файл для этого модуля.
  3. Создаём симлинк на конфиг для php-fpm.
  4. Создаём симлинк на конфиг для php cli.

В таких случаях используется такая конструкция, как дефайн (define, defined type, defined resource type). Дефайн похож на класс, но есть отличия: во-первых, каждый дефайн является типом ресурса, а не ресурсом; во-вторых, у каждого дефайна есть неявный параметр $title, куда попадает имя ресурса при его объявлении. Так же как и в случае с классами, дефайн сначала нужно описать, после этого его можно использовать.

Ссылка на официальную документация прилагается: http://puppet.com.

Упрощённый пример с модулем для PHP:

define php74::module (
  $php_module_name = $title,
  $php_package_name = "php7.4-${title}",
  $version = 'installed',
  $priority = '20',
  $data = "extension=${title}.so\n",
  $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, но про это расскажу в следующей серии.

5.5. Зависимости и уведомления для классов и дефайнов.

Классы и дефайны добавляют следующие правила к обработке зависимостей и уведомлений:

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

6. Условные операторы и селекторы.

6.1. Операторы.

Ссылка на официальную документация прилагаю: http://puppet.com.

if

Здесь всё просто:

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

unless

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

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

case

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

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

6.2. Селекторы.

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

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

7. Модули.

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

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

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

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

Ссылка на Puppet Forge: https://forge.puppet.com.

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

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

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

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

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

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

Ссылка на официальную документация прилагается: http://puppet.com.

Ресурсы (классы, дефайны) в модуле нельзя называть как угодно. Кроме того, есть прямое соответствие между названием ресурса и именем файла, в котором 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.

8. Шаблоны.

Что такое шаблоны можно узнать по ссылке: https://en.wikipedia.org.

Как использовать шаблоны: значение шаблона можно раскрыть с помощью функции template, которой передаётся путь к шаблону. Для ресурсов типа file используем вместе с параметром content.

Например, так:

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

Путь вида <modulename>/<filename> подразумевает файл <rootdir>/modules/<modulename>/templates/<filename>.

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

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

Puppet поддерживает шаблоны в формате ERB и EPP:

8.1. Вкратце про ERB.

Управляющие конструкции:

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

Выражения в ERB пишутся на Ruby (собственно, ERB — это Embedded Ruby).

Для доступа к переменным из манифеста нужно дописать @ к имени переменной. Чтобы убрать перевод строки, появляющийся после управляющей конструкции, нужно использовать закрывающий тег -%>.

8.2. Пример использования шаблона.

Предположим, я пишу модуль для управления 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 -%>

9. Факты и встроенные переменные.

Зачастую конкретная часть конфигурации зависит от того, что в данный момент происходит на ноде. Например, в зависимости от того, какой релиз Debian стоит, нужно установить ту или иную версию пакета. Можно следить за этим всем вручную, переписывая манифесты в случае изменения нод. Это несерьёзный подход, автоматизация гораздо лучше.

Для получения информации о нодах в Puppet есть такой механизм, как факты.

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

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

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

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

Оригинальная документация прилагается:

Во время работы Puppet agent сначала копирует с Puppet master на ноду все доступные сборщики фактов, после чего запускает их и отправляет на сервер собранные факты. После этого сервер начинает компиляцию каталога.

9.1. Факты в виде исполняемых файлов.

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

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

#!/bin/sh
echo "testfact=success"

и

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

9.2. Факты на 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

9.3. Текстовые факты.

Такие факты кладутся на ноды в директорию /etc/facter/facts.d в старом Puppet или /etc/puppetlabs/facts.d в новом Puppet.

examplefact=examplevalue

и

---
examplefact2: examplevalue2
anotherfact: anothervalue

9.4. Обращение к фактам.

Обратиться к фактам можно двумя способами:

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

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

Что такое обращение к фактам можно узнать по ссылке: https://puppet.com/.

9.5. Встроенные переменные.

Что такое встроенные переменные можно узнать по ссылке: https://puppet.com/.

Кроме фактов, есть ещё некоторые переменные, доступные в глобальном пространстве имён.

  • trusted facts — переменные, которые берутся из сертификата клиента (так как сертификат обычно выпускается на Puppet master, Puppet agent не может просто так взять и поменять свой сертификат, поэтому переменные и «доверенные»): название сертификата, имя хоста и домена, расширения из сертификата.
  • server facts —переменные, относящиеся к информации о сервере — версия, имя, IP-адрес сервера, окружение.
  • agent facts — переменные, добавляемые непосредственно Puppet agent‘ом, а не facter‘ом — название сертификата, версия Puppet agent, версия Puppet.
  • master variables — переменные Puppet master (sic!). Там примерно то же самое, что в server facts, плюс доступны значения конфигурационных параметров.
  • compiler variables — переменные компилятора, которые различаются в каждой области видимости: имя текущего модуля и имя модуля, в котором было обращение к текущему объекту. Их можно использовать, например, чтобы проверять, что ваши приватные классы не используют напрямую из других модулей.

10. Немного о кроссдистрибутивности.

В Puppet есть возможность использовать кроссдистрибутивные манифесты, это одна из целей, для которых он создавался. Не рекомендуется этим пользоваться. Парк серверов должен быть максимально однотипным в плане системного программного обеспечения, это позволяет не думать в критические моменты «ай, блин, тут rc.d, а не init.d» (реверанс в сторону ArchLinux) и вообще позволяет меньше думать на рутинных задачах.

Многие ресурсы зависят от других ресурсов.

Например, для ресурса «сервис sshd» необходим ресурс «пакет sshd» и опционально «конфигурация sshd».

Посмотрим, как это реализуется:

file { 'sshd_config':
   path => '/etc/ssh/sshd_config',
   ensure => file,
   content => "Port 22
               Protocol 2
               HostKey /etc/ssh/ssh_host_rsa_key
               HostKey /etc/ssh/ssh_host_dsa_key
               HostKey /etc/ssh/ssh_host_ecdsa_key
               UsePrivilegeSeparation yes
               KeyRegenerationInterval 3600
               ServerKeyBits 768
               SyslogFacility AUTH
               LogLevel INFO
               LoginGraceTime 120
               PermitRootLogin yes
               StrictModes yes
               RSAAuthentication yes
               PubkeyAuthentication yes
               IgnoreRhosts yes
               RhostsRSAAuthentication no
               HostbasedAuthentication no
               PermitEmptyPasswords no
               ChallengeResponseAuthentication no
               X11Forwarding yes
               X11DisplayOffset 10
               PrintMotd no
               PrintLastLog yes
               TCPKeepAlive yes
               AcceptEnv LANG LC_*
               Subsystem sftp /usr/lib/openssh/sftp-server
               UsePAM yes",
   mode => 0644,
   owner => root,
   group => root,
   require => Package['sshd']
}

   package { 'sshd':
   ensure => latest,
   name => 'openssh-server'
}

   service { 'sshd':
   ensure => running,
   enable => true,
   name => 'ssh'
   subscribe => File['sshd_config'],
   require => Package['sshd']
}

Здесь используется инлайн-конфигурация, которая делает манифест некрасивым. На самом деле так почти никогда не делается, существует механизм шаблонов, основанный на ERB, и возможность просто использовать внешние файлы. Но нас не это интересует.

Самые интересные строчки здесь — это строчки зависимостей — require и subscribe.

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

Require означает ровно то, что ожидается. Если ресурс А зависит (require) от ресурса Б, то Puppet сначала обработает ресурс Б, а потом вернётся к ресурсу А.

Subscribe даёт чуть более хитрое поведение. Если ресурс А подписан (subscribe) на ресурс Б, то Puppet сначала обработает ресурс Б, а потом вернётся к ресурсу А (поведение как у require), и далее при изменениях Б, будет заново обрабатываться А. Это очень удобно для создания сервисов, зависящих от их конфигов (как в примере выше).

Если конфигурация изменяется, сервер перезапускается, не нужно самому об этом беспокоиться.

Существуют также notify, before, но мы их здесь касаться не будем.

Подробности есть на оригинальном сайте программного обеспечения: https://puppet.com.


Данная статья — это часть цикла статей по эксплуатации системы управления конфигурацией — Puppet 3:

  1. Часть I: статья «Установка, настройка, начало эксплуатации»
  2. Часть II: статья «Манифесты, синтаксис, команды». <— Вы здесь!

11. Оригиналы источников информации.

  1. ru.wikipedia.org «Puppet».
  2. habr.com «Как стать кукловодом или Puppet для начинающих».
  3. habr.com «Установка и нaстройка Puppet версии 3.8 на примере Centos 6.5».
  4. habr.com «Введение в Puppet».
  5. dmosk.ru «Как установить и настроить Puppet на CentOS».
  6. qastack.ru «Как узнать, какую версию Puppet вы используете на Centos?»
  7. losst.ru «Настройка firewall Centos?»
  8. puppet.com «Puppet language syntax examples».
  9. puppet.com «Resources».
  10. puppet.com «Node definitions».
  11. puppet.com «The Puppet language style guide».
  12. puppet.com «Puppet 3.8 Reference Manual».
  13. puppet.com «Resource Type Reference Puppet 3.8».
  14. puppet.com «Language: Relationships and Ordering».
  15. puppet.com «Language: Classes».
  16. puppet.com «Language: Variables».
  17. puppet.com «Values, data types, and aliases».
  18. puppet.com «Language: Defined Resource Types».
  19. puppet.com «Language: Conditional Statements».
  20. puppet.com «Language: Namespaces and Autoloading».
  21. puppet.com «Creating templates using Embedded Puppet».
  22. puppet.com «Language: Embedded Ruby (ERB) Template Syntax».
  23. puppet.com «Facter: Core Facts».
  24. puppet.com «Writing custom facts».
  25. puppet.com «External facts».
  26. puppet.com «Accessing facts from Puppet code».
  27. puppet.com «Built-in variables».

Читайте также: