В Puppet 6 появилась возможность откладывать выполнение функций на время выполнения агента, и теперь мы выпустили улучшения, которые облегчают эту задачу. Читайте дальше, чтобы узнать больше и убедиться, что ваши модули готовы к отложенным функциям.
- Что такое функции Puppet?
- Обновления языка Puppet
- Проблема №1: Функции языка Puppet не могут быть отложенными
- Проблема №2: строки и названия ресурсов не могут быть отложенными
- Проблема №3: аргументы функции (обычно) не могут быть отложенными
- Проблема #4: отложенные значения не могут быть использованы для логики
- Резюме
- Подробнее
Что такое функции Puppet?
Сначала давайте сделаем очень краткий обзор. Функции Puppet — это кусочки кода, которые выполняются во время компиляции каталога. Они могут выполнять различные действия, например, включать другие классы в каталог или помечать ресурсы в заданной области видимости как no-op, но нас сегодня интересуют те функции, которые возвращают значение. Например, fqdnrand()
детерминированно генерирует случайное число, а shellquote()
преобразует входные данные и возвращает строку, безопасную для выполнения оболочками типа Bash.
Значения, возвращаемые этими функциями, компилируются непосредственно в каталог и становятся такими же статичными, как и остальная часть документа. Другими словами, если вы просмотрите каталог, созданный для конкретного узла, вы сможете увидеть, какое именно случайное число было использовано для задания расписания работы cron, или вы сможете увидеть, как именно была заключена в кавычки или экранирована команда оболочки перед ее вызовом. И поскольку они неизменяемы, этот каталог всегда будет приводить в исполнение одно и то же желаемое состояние.
Но в некоторых обстоятельствах было бы полезнее отложить эту работу на агента Puppet, чтобы он выполнил ее в процессе применения каталога. Например, многие инфраструктуры предписывают, чтобы секреты предоставлялись чем-то вроде высокозащищенного сервера секретов, а не передавались через сервер управления конфигурацией. В таких случаях нас меньше волнует конкретное значение, чем то, что оно представляет. Нам не нужно, чтобы сервер Puppet знал пароль учетной записи базы данных, на которую настроен узел. Нам просто нужно указать агенту поместить пароль, каким бы он ни был, в соответствующий конфигурационный файл.
В подобных случаях можно вместо этого скомпилировать ссылку на саму функцию в каталоге и дать агенту указание вызывать ее во время выполнения, как показано ниже:
class { 'profile::myappstack':
db_adapter => 'postgresql',
db_address => 'pgsql.example.com',
db_password => Deferred(
'vault_lookup::lookup',
['appstack/dbpass', 'https://vault.example.com']
),
}
Теперь, вместо того, чтобы каталог содержал пароль, а Puppet-сервер имел доступ к паролю, агент сам будет получать пароль непосредственно из Vault, используя свои собственные зарегистрированные учетные данные. Это позволяет администраторам инфраструктуры иметь гораздо более тонкий контроль над доступом к конфиденциальной информации и быстро менять или отзывать его по мере необходимости.
Любая функция, возвращающая значение, может быть отложена таким образом, и во многих случаях это так просто. Однако есть некоторые сложности, некоторые из которых были упрощены недавними обновлениями Puppet. О них мы поговорим в первую очередь.
Обновления языка Puppet
В первой реализации Puppet каталог был эффективно обработан для преобразования отложенных функций в значения. Это означает, что перед выполнением каталога агент сканировал его и вызывал каждую отложенную функцию. Возвращенное значение вставлялось в каталог вместо функции. Затем каталог приводится в действие обычным образом. Проблема такого подхода заключается в том, что если функция зависела от какого-либо инструментария, установленного в рамках запуска Puppet, то при первом запуске она не сработает, поскольку была вызвана до установки. Если функция не умеет изящно обрабатывать отсутствующие инструменты, это может даже помешать выполнению каталога вообще.
Начиная с Puppet 7.17, функции теперь можно лениво оценивать с помощью нового параметра preprocess_deferred setting
. Это указывает агенту разрешать отложенные функции во время выполнения, а не до. Другими словами, если вы используете стандартные отношения Puppet для обеспечения управления инструментарием до классов или ресурсов, которые используют отложенные функции, использующие этот инструментарий, то все будет работать так, как ожидается, и функции будут выполняться правильно.
В Puppet 7.17 также улучшен способ проверки типизированных параметров классов. Тип данных отложенной функции — Deferred
, и старые версии Puppet фактически использовали этот тип при проверке сигнатур классов. Например, если бы класс profile::myappstack
, на который мы ссылались в примере выше, указывал, что параметр db_password
должен быть String
, то пример был бы неудачным, потому что тип Deferred
не соответствовал бы ожидаемому типу String
.
Начиная с версии Puppet 7.17, это больше не является проблемой. Отложенные функции интроспективно проверяются, и объявленный ими тип возврата будет использоваться для сопоставления типов. Если функция явно не объявляет возвращаемый тип, Puppet выведет предупреждение, но компиляция пройдет успешно. Для использования этого улучшения не требуется никаких изменений в коде, но если вы пишете классы, которые могут быть использованы в старых версиях Puppet, вы можете рассмотреть возможность использования вариативного типа данных, например Variant[String, Deferred]
для параметров, которые, как ожидается, будут отложенными.
class profile::myappstack(
String db_adapter,
String db_address,
Variant[String, Deferred] db_password,
) { ...
Третья, и, вероятно, самая сложная проблема заключается в том, что в зависимости от того, как авторы пишут свои модули, вы можете передавать или не передавать отложенные функции в качестве параметров во многие популярные модули Forge. Давайте рассмотрим несколько примеров и узнаем, как их предусмотреть и защитить наши собственные модули от отложенных функций.
Существует четыре основные причины несовместимости, и, вероятно, другие варианты, которые следуют похожим шаблонам. Мы начнем с самых простых и будем двигаться к самым сложным.
Проблема №1: Функции языка Puppet не могут быть отложенными
Начиная с версии Puppet 4.2, многие функции могут быть написаны непосредственно на языке Puppet, а не на Ruby. Такие функции часто используются для преобразования данных, как, например, этот пример из документации, который превращает список ACL в хэш ресурсов для использования с функцией create_resources()
. Поскольку эти функции не подключаются к агенту, они не могут быть отложены. В целом, это не очень важно, поскольку такие операции, как подключение к серверу Vault, не могут быть легко выполнены на языке Puppet. Но если у вас есть такая необходимость, то эту функцию нужно переписать на языке Ruby.
Проблема №2: строки и названия ресурсов не могут быть отложенными
Значение, полученное из отложенной функции, не может быть использовано в заголовке ресурса или интерполировано в строку. Например, допустим, вы добавили отладочный код в профиль myappstack
, чтобы посмотреть, какой пароль возвращает сервер Vault.
class profile::myappstack(
String db_adapter,
String db_address,
String db_password,
) {
notify { "Password: ${db_password}": }
#...
}
Вместо пароля, который вы ожидали увидеть, это текстовая форма функции Deferred!
$ puppet agent -t
…
Notice: Password: Deferred({'name' =>'vault_lookup::lookup', 'arguments' => ['appstack/dbpass', 'https://vault.example.com']})
А если бы вы написали ее без интерполяции строк, например notify { $db_password: }
, то она полностью провалит компиляцию и выдаст на первый взгляд бессмысленную ошибку, которая может вызвать у опытных программистов на C++ шаблонные воспоминания.
Error: Evaluation Error: Illegal title type at index 0. Expected
String, got Object[{name => 'Deferred', attributes => {'name' =>
Pattern[/A[$]?[a-z][a-z0-9_]*(?:::[a-z][a-z0-9_]*)*z/], 'argum
ents' => {type => Optional[Array], value => undef}}}] (file: /Us
ers/ben.ford/tmp/deferred.pp, line: 6, column: 12) on node arach
ne.local
Решение этой проблемы заключается в том, что переменные, которые вы ожидаете отложить, не должны использоваться в качестве заголовков ресурсов или в интерполированных строках. Уведомление в этом примере должно быть рефакторинговано следующим образом:
notify { 'vault server debugging':
# We cannot interpolate a string with a deferred value
# because that interpolation happens during compilation.
message => $db_password,
}
Если вам нужно интерполировать отложенное значение в строку, вы можете сделать это, отложив функцию sprintf()
. Например, вы можете написать это уведомление так:
notify { 'vault server debugging':
# Defer interpolation to runtime after the value is resolved.
message => Deferred(
'sprintf',
['Password: %s', $db_password]
),
}
Не забудьте удалить это сообщение, как только проблема с хранилищем будет решена, чтобы оно не выдало ваши секреты!
Проблема №3: аргументы функции (обычно) не могут быть отложенными
Тесно связанная с первой проблемой, аргументы функции не могут быть отложены, если только функция не предназначена для этого. Функции оцениваются во время компиляции, поэтому если вы отложите аргумент, он будет работать с объектом Ruby, а не с разрешенным значением. Обычно это заметно при попытке рендеринга шаблонизированных файлов. Например, если бы профиль myappstack
управлял конфигурационным файлом с шаблоном, он бы включал ту же текстовую форму функции отложенного поиска хранилища, что и выше:
$ cat /etc/myappstack/db.conf
dbpassword = Deferred({'name' =>'vault_lookup::lookup', 'arguments' => ['appstack/dbpass', 'appstack/dbpass']})
Для того чтобы правильно обрабатывать отложенные функции, использующая их функция также должна быть отложенной. Например, вы можете отложить рендеринг файла конфигурации базы данных, используя новую функцию deferrable_epp()
, которая откладывает рендеринг шаблона, когда это необходимо. Использование этой функции для генерации шаблонных файлов позволяет прозрачно обрабатывать отложенные параметры. Эта функция доступна начиная с puppetlabs-stdlib
версии 8.4.0. Обратите внимание, что она требует явной передачи переменных, которые вы будете использовать в шаблоне.
Если вам нужно поддерживать более ранние версии stdlib, то вам придется самостоятельно написать логику шаблона, которая может выглядеть примерно так:
$variables = { 'password' => $db_password }
if $db_password.is_a(Deferred) {
$content = Deferred(
'inline_epp',
[find_template('profiles/myappstack.db.epp').file, $variables],
)
}
else {
$content = epp('profiles/myappstack.db.epp', $variables)
}
file { '/path/to/configfile':
ensure => file,
content => $content,
}
Проблема #4: отложенные значения не могут быть использованы для логики
Значение, которое неизвестно до времени выполнения, нельзя использовать для принятия условных решений, поскольку вся логика решается во время компиляции. Примером может служить модуль puppetlabs-postgresql
, который по-разному обрабатывал предоставленные хэши паролей в зависимости от алгоритма, использованного для их создания. Если пользователь ожидает, что хэш пароля будет предоставлен во время выполнения секретным сервером, то во время выполнения он неизвестен, и компилятор не может выбрать подходящий путь кода.
Решением для такого рода проблем является рефакторинг, чтобы эти решения не нужно было принимать во время компиляции, или чтобы для принятия решений использовались другие данные. В случае с нашим модулем PostgreSQL мы рефакторизовали код таким образом, что все кодовые пути, затронутые этим условием, были отложены. Будет выполняться тот же самый код, но весь он будет оцениваться во время выполнения на агенте.
В зависимости от типа принимаемого решения, вы также можете рефакторить код до использования фактов, которые оцениваются на агенте до компиляции каталога, а затем принимать условные решения на основе разрешенных значений фактов.
Резюме
Я уверен, что вы видите общую нить в каждом из этих проблемных сценариев. Значения, вычисляемые отложенными функциями, просто не известны во время компиляции. Это означает, что ничто, обрабатываемое во время компиляции, не может их использовать. Вы не можете использовать отложенное значение для интерполяции в строку, или использовать его в качестве ключа для селектора, или принять логическое решение. Вы не можете вывести его непосредственно в файл шаблона, вместо этого вам нужно включить источник шаблона в каталог и скомпилировать его во время выполнения.
По сути, отложенная функция полезна только при передаче генерируемого ею значения непосредственно в параметр объявленного ресурса, и модули должны быть написаны с учетом этого. Авторы модулей должны предвидеть, что люди могут захотеть отложить определенные параметры, такие как пароли, токены или другие секретные значения, и обрабатывать такие случаи путем рефакторинга любого использования этих значений вне времени компиляции и во время выполнения.
Обновления экосистемы, упрощающие использование отложенных параметров:
- Установите
preprocess_deferred
, если ваши функции зависят от инструментария, установленного при запуске Puppet. - Отложенные функции интерполируются таким образом, чтобы их возвращаемые типы соответствовали типам данных, требуемым сигнатурами классов.
- Новая функция
deferrable_epp()
будет автоматически откладывать рендеринг шаблона epp, когда это необходимо.
Удачи! Мы всегда рады посмотреть на то, что вы создаете.
Бен — руководитель отдела по работе с сообществом и DevRel в Puppet.
Подробнее
- Прочитайте документацию по отложенным функциям.
- Посмотрите презентацию, которую я сделал на CfgMgmtCamp 2019