Puppet es un sistema de gestión de configuraciones. Se utiliza para llevar los hosts al estado deseado y mantener ese estado.
He estado trabajando con Puppet durante más de cinco años. Este texto es, en esencia, una compilación traducida y reordenada de puntos clave de la documentación oficial, que permitirá a los principiantes entender rápidamente los conceptos básicos de Puppet.

Información básica
El esquema de funcionamiento de Puppet es cliente-servidor, aunque también se admite una opción de funcionamiento sin servidor con funcionalidad limitada.
Se utiliza un modelo de trabajo de tipo pull: por defecto, cada media hora los clientes se conectan al servidor para obtener la configuración y aplicarla. Si has trabajado con Ansible, allí se utiliza un modelo diferente, push: el administrador inicia el proceso de aplicación de la configuración, y los clientes no aplicarán nada por su cuenta.
En la interacción de red se utiliza cifrado TLS bidireccional: tanto el servidor como el cliente tienen sus propias claves privadas y los certificados correspondientes. Normalmente, el servidor emite certificados para los clientes, pero en principio, también es posible utilizar una CA externa.
Introducción a los manifiestos
En la terminología de Puppet se conecta al servidor Puppet y se nodos (nodes). La configuración para los nodos se escribe en manifiestos en un lenguaje de programación especial: Puppet DSL.
Puppet DSL es un lenguaje declarativo. En él se describe el estado deseado del nodo en forma de declaraciones de recursos individuales, por ejemplo:
- El archivo existe y tiene un contenido específico.
- El paquete está instalado.
- El servicio está en funcionamiento.
Los recursos pueden estar interrelacionados:
- Existen dependencias que afectan el orden de aplicación de los recursos.
Por ejemplo, "primero instala el paquete, luego modifica el archivo de configuración, después inicia el servicio". - Hay notificaciones: si un recurso cambia, envía notificaciones a los recursos suscritos a él.
Por ejemplo, si se modifica un archivo de configuración, se puede reiniciar automáticamente el servicio.
Además, en Puppet DSL hay funciones y variables, así como operadores condicionales y selectores. También se admiten varios mecanismos de plantillado: EPP y ERB.
Puppet está escrito en Ruby, por lo que muchas construcciones y términos provienen de allí. Ruby permite extender Puppet: escribir lógica compleja, nuevos tipos de recursos, y funciones.
Durante la ejecución de Puppet, los manifiestos para cada nodo específico en el servidor se compilan en un directorio. Directorio — es una lista de recursos y sus interrelaciones tras calcular el valor de funciones, variables y desgloses de operadores condicionales.
Sintaxis y estilo de código
Aquí tienes secciones de la documentación oficial que te ayudarán a comprender la sintaxis, si los ejemplos proporcionados no son suficientes:
Aquí hay un ejemplo de cómo se ve un manifiesto:
# Комментарии пишутся, как и много где, после решётки.
#
# Описание конфигурации ноды начинается с ключевого слова node,
# за которым следует селектор ноды — хостнейм (с доменом или без)
# или регулярное выражение для хостнеймов, или ключевое слово default.
#
# После этого в фигурных скобках описывается собственно конфигурация ноды.
#
# Одна и та же нода может попасть под несколько селекторов. Про приоритет
# селекторов написано в статье про синтаксис описания нод.
node 'hostname', 'f.q.d.n', /regexp/ {
# Конфигурация по сути является перечислением ресурсов и их параметров.
#
# У каждого ресурса есть тип и название.
#
# Внимание: не может быть двух ресурсов одного типа с одинаковыми названиями!
#
# Описание ресурса начинается с его типа. Тип пишется в нижнем регистре.
# Про разные типы ресурсов написано ниже.
#
# После типа в фигурных скобках пишется название ресурса, потом двоеточие,
# дальше идёт опциональное перечисление параметров ресурса и их значений.
# Значения параметров указываются через т.н. hash rocket (=>).
resource { 'title':
param1 => value1,
param2 => value2,
param3 => value3,
}
}Los espacios y saltos de línea no son una parte obligatoria del manifiesto, aunque hay un . Resumen:
- No se utilizan espacios dobles ni tabuladores.
- Las llaves se separan por un espacio, los dos puntos no se separan por un espacio.
- Se deben incluir comas después de cada parámetro, incluido el último. Cada parámetro debe estar en una línea separada. Se hace una excepción para el caso sin parámetros y un solo parámetro: se puede escribir en una línea y sin coma (es decir,
resource { 'title': }yresource { 'title': param => value }). - Las flechas de los parámetros deben estar en el mismo nivel.
- Las flechas de las interrelaciones de recursos se escriben delante de ellos.
Ubicación de archivos en el servidor Puppet
Para más explicaciones, introduciré el concepto de "directorio raíz". El directorio raíz es el directorio donde se encuentra la configuración de Puppet para un nodo específico.
El directorio raíz varía dependiendo de la versión de Puppet y el uso de entornos. Los entornos son conjuntos independientes de configuraciones que se almacenan en directorios separados. Normalmente se utilizan en combinación con git, en ese caso, los entornos se crean a partir de ramas de git. En consecuencia, cada nodo se encuentra en uno u otro entorno. Esto se configura en el propio nodo, o en ENC, de lo que hablaré en el siguiente artículo.
- En la tercera versión ("Puppet antiguo") el directorio base era
/etc/puppet. El uso de entornos es opcional; por ejemplo, no los utilizamos con Puppet antiguo. Si se utilizan entornos, normalmente se almacenan en/etc/puppet/environments, el directorio raíz será el directorio del entorno. Si no se utilizan entornos, el directorio raíz será el base. - A partir de la cuarta versión ("Puppet nuevo") el uso de entornos se volvió obligatorio, y el directorio base se trasladó a
/etc/puppetlabs/code. Por lo tanto, los entornos se almacenan en/etc/puppetlabs/code/environments, el directorio raíz es el directorio del entorno.
En el directorio raíz debe haber un subdirectorio manifests, en el cual se encuentra uno o varios manifiestos con la descripción de los nodos. Además, debe haber un subdirectorio modules, en el que se encuentran los módulos. Qué son los módulos, lo explicaré más adelante. Además, en el antiguo Puppet también puede haber un subdirectorio files, en el cual se encuentran varios archivos que copiamos a los nodos. En el nuevo Puppet, todos los archivos se han trasladado a módulos.
Los archivos de los manifiestos tienen la extensión .pp.
Un par de ejemplos prácticos
Descripción del nodo y el recurso en él
En el nodo server1.testdomain se debe crear un archivo /etc/issue con el contenido Debian GNU/Linux n l. El archivo debe pertenecer al usuario y al grupo root, los permisos de acceso deben ser 644.
Escribiendo un manifiesto:
node 'server1.testdomain' { # bloque de configuración relacionado con el nodo server1.testdomain
file { 'etc/issue': # describimos el archivo /etc/issue
ensure => present, # este archivo debe existir
content => 'Debian GNU/Linux n l', # debe tener este contenido
owner => root, # usuario propietario
group => root, # grupo propietario
mode => '0644', # permisos del archivo. Se indican en forma de cadena (entre comillas), porque de lo contrario, un número con 0 al inicio se interpretará como escrito en sistema octal, y todo saldrá mal
}
}Interrelaciones de recursos en el nodo
En el nodo server2.testdomain debe tener nginx en funcionamiento con una configuración preparada con antelación.
Descomponemos la tarea:
- Es necesario que se instale el paquete
nginx. - Es necesario que se copien los archivos de configuración desde el servidor.
- Es necesario que se inicie el servicio
nginx. - En caso de actualización de la configuración, es necesario reiniciar el servicio.
Escribiendo un manifiesto:
nodo 'server2.testdomain' { # bloque de configuración relacionado con el nodo server2.testdomain
paquete { 'nginx': # describimos el paquete nginx
ensure => instalado, # debe estar instalado
}
# La flecha directa (->) indica que el recurso a continuación debe
# crearse después del recurso descrito anteriormente.
# Tales dependencias son transitivas.
-> archivo { '/etc/nginx': # describimos el archivo /etc/nginx
ensure => directorio, # debe ser un directorio
source => 'puppet:///modules/example/nginx-conf', # su contenido debe tomarse del servidor Puppet en la dirección indicada
recurse => verdadero, # copiar archivos recursivamente
purge => verdadero, # se deben eliminar archivos sobrantes (los que no están en la fuente)
force => verdadero, # eliminar directorios sobrantes
}
# La flecha ondulada (~>) indica que el recurso a continuación debe
# suscribirse a los cambios del recurso descrito anteriormente.
# La flecha ondulada incluye la directa (->).
~> servicio { 'nginx': # describimos el servicio nginx
ensure => en ejecución, # debe estar ejecutándose
enable => verdadero, # debe iniciarse automáticamente al arrancar el sistema
}
# Cuando un recurso de tipo servicio recibe una notificación,
# el servicio correspondiente se reinicia.
}Para que esto funcione, debe haber una disposición de archivos aproximadamente así en el servidor Puppet:
/etc/puppetlabs/code/environments/production/ # (это для нового Паппета, для старого корневой директорией будет /etc/puppet)
├── manifests/
│ └── site.pp
└── modules/
└── example/
└── files/
└── nginx-conf/
├── nginx.conf
├── mime.types
└── conf.d/
└── some.confTipos de recursos
La lista completa de tipos de recursos admitidos se encuentra , aquí describiré cinco tipos básicos que en mi práctica son suficientes para resolver la mayoría de las tareas.
file
Gestiona archivos, directorios, enlaces simbólicos, su contenido y permisos.
Parámetros:
- nombre del recurso — ruta al archivo (opcional)
- path — ruta al archivo (si no se especifica en el nombre)
- ensure — tipo de archivo:
ausente— eliminar archivopresent— debe ser un archivo de cualquier tipo (si no hay archivo se creará un archivo vacío)file— archivo normaldirectorio— directorioenlace— enlace simbólico
- texto alternativo — contenido del archivo (aplicable solo a archivos normales, no se puede usar junto con source o target)
- source — enlace a la ruta desde la que se debe copiar el contenido del archivo (no se puede usar junto con texto alternativo o target). Se puede especificar como tipo URI con el esquema
puppet:(entonces se usarán archivos del servidor Puppet), así como con el esquemahttp:(se sabe lo que habrá en este caso), e incluso con el esquemafile:o como una ruta absoluta sin esquema (entonces se utilizará el archivo del sistema local en el nodo) - target — a dónde debe apuntar el enlace simbólico (no se puede usar junto con texto alternativo o source)
- propietario — usuario al que debe pertenecer el archivo
- group — grupo al que debe pertenecer el archivo
- modo — permisos del archivo (en forma de cadena)
- recurse — incluye el procesamiento recursivo de directorios
- purge — incluye la eliminación de archivos que no están descritos en Puppet
- force — incluye la eliminación de directorios que no están descritos en Puppet
package
Instala y elimina paquetes. Puede manejar notificaciones: reinstala el paquete si se establece el parámetro reinstall_on_refresh.
Parámetros:
- nombre del recurso — nombre del paquete (opcional)
- name — nombre del paquete (si no se proporciona en el nombre)
- provider — gestor de paquetes que se debe utilizar
- ensure — estado deseado del paquete:
present,installed— se ha instalado cualquier versiónlatest— se ha instalado la versión más recienteausente— eliminado (apt-get remove)purged— eliminado junto con los archivos de configuración (apt-get purge)held— la versión del paquete está bloqueada (apt-mark hold)cualquier otra cadena— se ha instalado la versión especificada
- reinstall_on_refresh — si
true, entonces al recibir la notificación el paquete se reinstalará. Útil para distribuciones basadas en source, donde la recompilación de paquetes puede ser necesaria al cambiar los parámetros de compilación. De forma predeterminadafalse.
el servicio
Gestiona servicios. Puede manejar notificaciones: reinicia el servicio.
Parámetros:
- nombre del recurso — servicio que se debe gestionar (opcional)
- name — servicio que se debe gestionar (si no se proporciona en el nombre)
- ensure — estado deseado del servicio:
running— está en ejecuciónstopped— detenido
- enable — gestiona la capacidad de inicio automático del servicio:
true— se ha habilitado el inicio automático (systemctl enable)mask— está enmascarado (systemctl mask)false— se ha desactivado el inicio automático (systemctl disable)
- restart — comando para reiniciar el servicio
- status — comando para verificar el estado del servicio
- hasrestart — especifica si el script de inicio del servicio admite reinicio. Si
falsey se especifica el parámetro restart — se utiliza el valor de este parámetro. Sifalsey el parámetro restart no se especifica — el servicio se detiene y se reinicia (pero en systemd se utiliza el comandosystemctl restart). - hasstatus — especifica si el script de inicio del servicio admite el comando
status. Sifalse, entonces se utiliza el valor del parámetro status. De forma predeterminadatrue.
exec
Ejecuta comandos externos. Si no se especifican parámetros creates, onlyif, unless o refreshonly, el comando se ejecutará en cada ejecución de Puppet. Puede manejar notificaciones: ejecuta el comando.
Parámetros:
- nombre del recurso — comando que debe ejecutarse (opcional)
- command — comando que debe ejecutarse (si no se especifica en el nombre)
- path — rutas en las que buscar el archivo ejecutable
- onlyif — si el comando especificado en este parámetro finalizó con un código de retorno cero, se ejecutará el comando principal
- unless — si el comando especificado en este parámetro finalizó con un código de retorno distinto de cero, se ejecutará el comando principal
- creates — si el archivo especificado en este parámetro no existe, se ejecutará el comando principal
- refreshonly — si
true, se ejecutará el comando solo si este exec recibe una notificación de otros recursos - cwd — directorio desde el cual ejecutar el comando
- usuario — usuario que ejecuta el comando
- provider — mediante qué se ejecuta el comando:
- posix — simplemente se crea un proceso hijo, es obligatorio especificar path
- shell — el comando se ejecuta en un shell
/bin/sh, no es necesario especificar path, se pueden usar globbing, pipes y otras características del shell. Normalmente se determina automáticamente si hay varios caracteres especiales (|,;,&&,||y así sucesivamente).
cron
Administra trabajos de cron.
Parámetros:
- nombre del recurso — simplemente algún identificador
- ensure — estado del trabajo de cron:
present— crear si no existeausente— eliminar si existe
- command — qué comando ejecutar
- entorno — en qué entorno ejecutar el comando (lista de variables de entorno y sus valores a través de
=) - usuario — desde qué usuario ejecutar el comando
- minuto, hora, día de la semana, mes, día del mes — cuándo ejecutar cron. Si alguno de estos atributos no está especificado, su valor en el crontab será
*.
En Puppet 6.0 cron como si en puppetserver, por lo tanto no hay documentación en el sitio general. Pero está en puppet-agent, por lo tanto, no es necesario instalarlo por separado. La documentación sobre él se puede consultar , o .
Sobre los recursos en general
Requisitos de unicidad de los recursos
El error más común que encontramos es — Declaración duplicada. Este error ocurre cuando dos o más recursos del mismo tipo con el mismo nombre se cuelan en el directorio.
Por lo tanto, lo reitero: en los manifiestos para un nodo no debe haber recursos del mismo tipo con el mismo nombre (title)!
A veces hay necesidad de instalar paquetes con el mismo nombre, pero con diferentes gestores de paquetes. En tal caso, se debe usar el parámetro name, para evitar el error:
package { 'ruby-mysql':
ensure => installed,
name => 'mysql',
provider => 'gem',
}
package { 'python-mysql':
ensure => installed,
name => 'mysql',
provider => 'pip',
}En otros tipos de recursos hay parámetros similares que ayudan a evitar la duplicación, name sa-logic-subsets-canary-vs.yaml el servicio, command sa-logic-subsets-canary-vs.yaml exec, etc.
Metaparámetros
Algunos parámetros especiales están presentes en cada tipo de recurso, independientemente de su naturaleza.
Lista completa de metaparámetros .
Lista breve:
- require — en este parámetro se indica de qué recursos depende este recurso.
- before — en este parámetro se indica qué recursos dependen de este recurso.
- subscribe — en este parámetro se indica de qué recursos recibe notificaciones este recurso.
- notify — en este parámetro se indica qué recursos reciben notificaciones de este recurso.
Todos los metaparámetros mencionados aceptan ya sea un enlace a un recurso o un arreglo de enlaces en corchetes.
Enlaces a recursos
Un enlace a un recurso es simplemente una mención del recurso. Se utilizan principalmente para indicar dependencias. Un enlace a un recurso que no existe causará un error de compilación.
La sintaxis para un enlace es la siguiente: tipo de recurso en mayúscula (si el nombre del tipo contiene dos puntos dobles, cada parte del nombre entre los puntos se escribe en mayúscula), seguido en corchetes del nombre del recurso (¡el caso del nombre no cambia!). No debe haber espacios, los corchetes se escriben inmediatamente después del nombre del tipo.
Ejemplo:
file { '/file1': ensure => present }
file { '/file2':
ensure => directory,
before => File['/file1'],
}
file { '/file3': ensure => absent }
File['/file1'] -> File['/file3']Dependencias y notificaciones
Como se mencionó anteriormente, las dependencias simples entre recursos son transitivas. Por cierto, tenga cuidado al establecer dependencias, ya que se pueden crear dependencias cíclicas, lo que causará un error de compilación.
A diferencia de las dependencias, las notificaciones no son transitivas. Para las notificaciones se aplican las siguientes reglas:
- Si un recurso recibe una notificación, se actualiza. Las acciones al actualizar dependen del tipo de recurso — exec ejecuta un comando, el servicio reinicia un servicio, package reinstala un paquete. Si no se ha definido una acción de actualización para el recurso, entonces no ocurre nada.
- En una sola ejecución de Puppet, un recurso se actualiza no más de una vez. Esto es posible porque las notificaciones incluyen las dependencias, y el gráfico de dependencias no contiene ciclos.
- Si Puppet cambia el estado de un recurso, el recurso envía notificaciones a todos los recursos suscritos a él.
- Si un recurso se actualiza, envía notificaciones a todos los recursos suscritos a él.
Tratamiento de parámetros no especificados
Por lo general, si algún parámetro de un recurso no tiene un valor por defecto y este parámetro no está especificado en el manifiesto, Puppet no cambiará esta propiedad del recurso correspondiente en el nodo. Por ejemplo, si el recurso de tipo file no tiene especificado el parámetro propietario, Puppet no cambiará el propietario del archivo correspondiente.
Introducción a clases, variables y defines
Supongamos que tenemos varios nodos que tienen una parte de configuración idéntica, pero también hay diferencias; de lo contrario, podríamos describir todo en un solo bloque. node {}. Por supuesto, se pueden copiar las partes idénticas de la configuración, pero en general, esta es una mala solución: la configuración se expande y, al cambiar la parte común de la configuración, será necesario modificar lo mismo en muchos lugares. Es fácil cometer un error, y el principio DRY (no te repitas) no se inventó por nada.
Para resolver este problema, existe una construcción como clase.
Clases
son bloques de código de Puppet nombrados. Las clases son necesarias para reutilizar el código.
Primero, se debe describir la clase. La descripción en sí no agrega recursos en ninguna parte. La clase se describe en los manifiestos:
# Описание класса начинается с ключевого слова class и его названия.
# Дальше идёт тело класса в фигурных скобках.
class example_class {
...
}Después de esto, se puede usar la clase:
# первый вариант использования — в стиле ресурса с типом class
class { 'example_class': }
# второй вариант использования — с помощью функции include
include example_class
# про отличие этих двух вариантов будет рассказано дальшеEjemplo de la tarea anterior: extraeremos la instalación y configuración de nginx en una clase:
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
}Variables
La clase del ejemplo anterior no es flexible, porque siempre trae la misma configuración de nginx. Hagamos que la ruta de la configuración sea variable, así podremos usar esta clase para instalar nginx con cualquier configuración.
Esto puede hacerse .
Atención: ¡las variables en Puppet son inmutables!
Además, solo se puede acceder a una variable después de que se haya declarado; de lo contrario, el valor de la variable será undef.
Ejemplo de trabajo con variables:
# создание переменных
$variable = 'value'
$var2 = 1
$var3 = true
$var4 = undef
# использование переменных
$var5 = $var6
file { '/tmp/text': content => $variable }
# интерполяция переменных — раскрытие значения переменных в строках. Работает только в двойных кавычках!
$var6 = "Variable with name variable has value ${variable}"En Puppet hay espacios de nombres, y las variables, respectivamente, tienen ámbito: una variable con el mismo nombre puede estar definida en diferentes espacios de nombres. Al resolver el valor de la variable, se busca primero en el espacio de nombres actual, luego en el contenedor, y así sucesivamente.
Ejemplos de espacios de nombres:
- global — donde van las variables fuera de la descripción de la clase o nodo;
- espacio de nombres del nodo en la descripción del nodo;
- espacio de nombres de la clase en la descripción de la clase.
Para evitar ambigüedades al acceder a una variable, se puede especificar el espacio de nombres en el nombre de la variable:
# переменная без пространства имён
$var
# переменная в глобальном пространстве имён
$::var
# переменная в пространстве имён класса
$classname::var
$::classname::varAcordemos que la ruta a la configuración de nginx se encuentra en la variable $nginx_conf_source. Entonces, la clase se verá de la siguiente manera:
class nginx_example {
package { 'nginx':
ensure => installed,
}
-> file { '/etc/nginx':
ensure => directory,
source => $nginx_conf_source, # aquí usamos la variable en lugar de una cadena fija
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
}Sin embargo, el ejemplo dado es malo porque hay un cierto "conocimiento oculto" de que en algún lugar dentro de la clase se utiliza una variable con ese nombre. Es mucho más correcto hacer que este conocimiento sea común: las clases pueden tener parámetros.
Parámetros de clase — son variables en el espacio de nombres de la clase, se definen en la cabecera de la clase y pueden ser utilizados como variables normales en el cuerpo de la clase. Los valores de los parámetros se especifican al usar la clase en el manifiesto.
Se puede asignar un valor predeterminado a un parámetro. Si un parámetro no tiene un valor predeterminado y no se asigna un valor al usarlo, esto provocará un error de compilación.
Vamos a parametrizar la clase del ejemplo anterior y agregaremos dos parámetros: el primero, obligatorio, es la ruta a la configuración, y el segundo, opcional, es el nombre del paquete con nginx (en Debian, por ejemplo, hay paquetes 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', # задаём параметры класса точно так же, как параметры для других ресурсов
}
}En Puppet, las variables son tipadas. Hay . Los tipos de datos generalmente se utilizan para validar los valores de los parámetros que se pasan a las clases y definiciones. Si el parámetro pasado no corresponde al tipo especificado, ocurrirá un error de compilación.
El tipo se escribe directamente antes del nombre del parámetro:
class example (
String $param1,
Integer $param2,
Array $param3,
Hash $param4,
Hash[String, String] $param5,
) {
...
}Clases: incluir classname vs class{‘classname’:}
Cada clase es un recurso del tipo class. Al igual que con cualquier otro tipo de recurso, no puede haber dos instancias de la misma clase en un nodo.
Si intentas agregar una clase al mismo nodo dos veces usando class { 'classname':} (sin importar si con parámetros diferentes o iguales), habrá un error de compilación. Sin embargo, en el caso de usar la clase al estilo recurso se pueden definir explícitamente todos sus parámetros en el manifiesto.
Sin embargo, si se utiliza include, se puede agregar la clase tantas veces como se desee. La cuestión es que include es una función idempotente que verifica si la clase ha sido añadida al catálogo. Si la clase no está en el catálogo, la añade, y si ya está, no hace nada. Pero en el caso de usar include no se pueden establecer los parámetros de la clase al declarar la clase: todos los parámetros obligatorios deben ser proporcionados desde una fuente de datos externa, Hiera o ENC. Hablaremos de ellos en el siguiente artículo.
Defines
Como se mencionó en el bloque anterior, la misma clase no puede estar presente en el nodo más de una vez. Sin embargo, en algunos casos es necesario poder aplicar un mismo bloque de código con diferentes parámetros en un nodo. En otras palabras, hay una necesidad de un tipo propio de recurso.
Por ejemplo, para instalar un módulo PHP, hacemos lo siguiente en Avito:
- Instalamos el paquete con este módulo.
- Creamos un archivo de configuración para este módulo.
- Creamos un enlace simbólico a la configuración para php-fpm.
- Creamos un enlace simbólico a la configuración para php cli.
En tales casos, se utiliza una construcción como (define, defined type, defined resource type). Define es similar a una clase, pero hay diferencias: en primer lugar, cada define es un tipo de recurso y no un recurso; en segundo lugar, cada define tiene un parámetro implícito $title, donde se almacena el nombre del recurso al declararlo. Al igual que con las clases, primero se debe describir el define, y luego se puede utilizar.
Un ejemplo simplificado con un módulo para 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'], # los desencadenadores de los paquetes php de Debian crean enlaces simbólicos y reinician el servicio php-fpm, lo cual no necesitamos, ya que manejamos tanto los enlaces simbólicos como el servicio mediante 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' }
}En define, es más fácil detectar el error de declaración duplicada. Esto ocurre si hay recursos con nombres constantes en el define, y en algún nodo hay dos o más instancias de ese define.
Para protegerse de esto, es sencillo: todos los recursos dentro del define deben tener nombres que dependan de $title. Como alternativa, se pueden añadir recursos de manera idempotente; en el caso más simple, es suficiente extraer los recursos comunes de todas las instancias de define a una clase separada e incluir esa clase en el define: la función include es idempotente.
Existen otras formas de lograr la idempotencia al añadir recursos, específicamente el uso de funciones defined y ensure_resources, pero hablaré de esto en la próxima serie.
Dependencias y notificaciones para clases y defines
Las clases y defines añaden las siguientes reglas al procesamiento de dependencias y notificaciones:
- la dependencia de una clase/define añade dependencias de todos los recursos de la clase/define;
- la dependencia de una clase/define añade dependencias a todos los recursos de la clase/define;
- la notificación de una clase/define notifica a todos los recursos de la clase/define;
- la suscripción a una clase/define se suscribe a todos los recursos de la clase/define.
Operadores condicionales y selectores
if
Aquí es simple:
if EXPRESIÓN1 {
...
} elsif EXPRESIÓN2 {
...
} else {
...
}unless
unless es el opuesto de if: el bloque de código se ejecutará si la expresión es falsa.
unless EXPRESIÓN {
...
}case
Aquí tampoco hay nada complicado. Como valores, se pueden usar valores normales (cadenas, números, etc.), expresiones regulares o tipos de datos.
case EXPRESIÓN {
VALOR1: { ... }
VALOR2, VALOR3: { ... }
por defecto: { ... }
}Selectores
Un selector es una construcción del lenguaje, similar a case, solo que en lugar de ejecutar un bloque de código, devuelve un valor.
$var = $othervar ? { 'val1' => 1, 'val2' => 2, por defecto => 3 }Módulos
Cuando la configuración es pequeña, se puede mantener fácilmente en un solo manifiesto. Pero cuanto más grande es la configuración que describimos, más clases y nodos aparecen en el manifiesto, y se vuelve difícil de manejar.
Además, hay un problema de reutilización del código: cuando todo el código está en un solo manifiesto, es complicado compartir ese código con otros. Para solucionar estos dos problemas, en Puppet existe una entidad llamada módulos.
Módulos son conjuntos de clases, definiciones y otras entidades de Puppet, organizadas en un directorio separado. En otras palabras, un módulo es un fragmento independiente de la lógica de Puppet. Por ejemplo, puede haber un módulo para trabajar con nginx, que contendrá únicamente lo necesario para trabajar con nginx, o puede haber un módulo para trabajar con PHP, y así sucesivamente.
Los módulos tienen versiones y también tienen soporte para dependencias entre ellos. Hay un repositorio público de módulos llamado .
En el servidor Puppet, los módulos se encuentran en el subdirectorio modules del directorio raíz. Dentro de cada módulo, la estructura estándar de directorios es: manifests, files, templates, lib, etc.
Estructura de archivos en el módulo
En la raíz del módulo, pueden existir los siguientes directorios con nombres descriptivos:
manifests— contiene los manifiestosfiles— contiene los archivostemplates— contiene las plantillaslib— contiene código Ruby
Esta no es una lista completa de directorios y archivos, pero es suficiente para este artículo por ahora.
Nombres de recursos y nombres de archivos en el módulo
Los recursos (clases, definiciones) en el módulo no pueden llamarse de cualquier manera. Además, hay una correspondencia directa entre el nombre del recurso y el nombre del archivo donde Puppet buscará la descripción de ese recurso. Si se violan las reglas de nomenclatura, Puppet simplemente no encontrará la descripción de los recursos, y se producirá un error de compilación.
Las reglas son simples:
- Todos los recursos en el módulo deben estar en el espacio de nombres del módulo. Si el módulo se llama
foo, entonces todos los recursos en él deben llamarsefoo::, o simplementefoo. - El recurso con el nombre del módulo debe estar en el archivo
init.pp. - Para los demás recursos, la estructura de nombres de los archivos es la siguiente:
- el prefijo con el nombre del módulo se elimina
- todos los dos puntos dobles, si los hay, se reemplazan por barras diagonales
- se añade la extensión
.pp
Voy a mostrarlo con un ejemplo. Supongamos que estoy escribiendo un módulo nginx. Contiene los siguientes recursos:
- clase
nginxque se describe en el manifiestoinit.pp; - clase
nginx::serviceque se describe en el manifiestoservice.pp; - define
nginx::serverque se describe en el manifiestoserver.pp; - define
nginx::server::locationque se describe en el manifiestoserver/location.pp.
Plantillas
Seguro que ya sabes lo que son las plantillas, no es necesario que lo detalle aquí. Pero por si acaso dejaré .
Cómo utilizar plantillas: el valor de la plantilla se puede revelar mediante la función template, a la que se le pasa la ruta de la plantilla. Para recursos de tipo file se usa junto con el parámetro texto alternativo. Por ejemplo, así:
file { '/tmp/example': content => template('modulename/templatename.erb')Una ruta del tipo / supone un archivo /modules//templates/.
Además, hay una función inline_template — se le pasa el texto de la plantilla, no el nombre del archivo.
Dentro de las plantillas se pueden utilizar todas las variables de Puppet en el ámbito actual.
Puppet admite plantillas en formato ERB y EPP:
Breve sobre ERB
Estructuras de control:
<%= ВЫРАЖЕНИЕ %>— insertar el valor de la expresión<% ВЫРАЖЕНИЕ %>— calcular el valor de la expresión (sin insertarlo). Aquí entran los operadores condicionales normales (if), bucles (each).<%# КОММЕНТАРИЙ %>
Las expresiones en ERB se escriben en Ruby (de hecho, ERB significa Embedded Ruby).
Para acceder a las variables del manifiesto hay que añadir @ al nombre de la variable. Para eliminar el salto de línea que aparece después de la estructura de control, hay que utilizar la etiqueta de cierre -%>.
Ejemplo de uso de una plantilla
Supongamos que estoy escribiendo un módulo para gestionar ZooKeeper. La clase responsable de crear la configuración se vería así:
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'),
}
}Y la plantilla correspondiente zoo.cfg.erb — se vería así:
0 -%>
server.=:::
dataDir=
=Hechos y variables integradas
A menudo, una parte específica de la configuración depende de lo que está ocurriendo en ese momento en el nodo. Por ejemplo, dependiendo de qué versión de Debian se esté utilizando, es necesario instalar una u otra versión del paquete. Se puede realizar un seguimiento de todo esto manualmente, reescribiendo los manifiestos si hay cambios en los nodos. Pero este enfoque no es serio; la automatización es mucho mejor.
Para obtener información sobre los nodos en Puppet, existe un mecanismo llamado hechos. Hechos son información sobre el nodo disponible en los manifiestos como variables normales en el espacio de nombres global. Por ejemplo, el nombre del host, la versión del sistema operativo, la arquitectura del procesador, la lista de usuarios, la lista de interfaces de red y sus direcciones, y mucho más. Los hechos están disponibles en los manifiestos y plantillas como variables normales.
Ejemplo de trabajo con hechos:
notify { "Ejecutando OS ${facts['os']['name']} versión ${facts['os']['release']['full']}": }
# el recurso tipo notify simplemente muestra un mensaje en el logHablando formalmente, un hecho tiene un nombre (string) y un valor (se disponen de varios tipos: strings, arrays, diccionarios). Hay . También se pueden escribir propios. Los recolectores de hechos se describen , o como . También los hechos pueden presentarse en forma de en los nodos.
Durante su ejecución, el agente Puppet primero copia desde el servidor Puppet todos los recolectores de hechos disponibles al nodo, luego los ejecuta y envía los hechos recolectados al servidor; solo después de eso, el servidor comienza la compilación del catálogo.
Hechos en forma de archivos ejecutables
Estos hechos se colocan en módulos en el directorio facts.d. Por supuesto, los archivos deben ser ejecutables. Al ejecutarse, deben enviar a la salida estándar información ya sea en formato YAML o en formato "clave=valor".
Recuerde que los hechos se distribuyen a todos los nodos que están bajo la administración del servidor Puppet sobre el que se implementa su módulo. Por lo tanto, en el script, asegúrese de verificar que en el sistema existen todos los programas y archivos necesarios para que su hecho funcione.
#!/bin/sh
echo "testfact=success"#!/bin/sh
echo '{"testyamlfact":"success"}'Hechos en Ruby
Estos hechos se colocan en módulos en el directorio 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
endHechos de texto
Estos hechos se colocan en los nodos en el directorio /etc/facter/facts.d en el antiguo Puppet o /etc/puppetlabs/facts.d en el nuevo Puppet.
examplefact=examplevalue---
examplefact2: examplevalue2
anotherfact: anothervalueAcceso a los hechos
Se puede acceder a los hechos de dos maneras:
- a través del diccionario
$facts:$facts['fqdn']; - usando el nombre del hecho como nombre de variable:
$fqdn.
Es mejor usar el diccionario $facts, y aún mejor especificar el espacio de nombres global ($::facts).
Variables integradas
Además de los hechos, hay algunas , disponibles en el espacio de nombres global.
- trusted facts — variables que provienen del certificado del cliente (ya que el certificado generalmente se emite en el servidor Puppet, el agente no puede simplemente cambiar su certificado, por lo que las variables son 'confiables'): nombre del certificado, nombre del host y del dominio, extensiones del certificado.
- server facts — variables relacionadas con la información del servidor: versión, nombre, dirección IP del servidor, entorno.
- agent facts — variables añadidas directamente por el puppet-agent, no por facter: nombre del certificado, versión del agente, versión de Puppet.
- master variables — variables del puppetmaster (sic!). Ahí está más o menos lo mismo que en server facts, además de los valores de los parámetros de configuración.
- compiler variables — variables del compilador que varían en cada ámbito: nombre del módulo actual y nombre del módulo desde el cual se accedió al objeto actual. Se pueden usar, por ejemplo, para verificar que sus clases privadas no se utilizan directamente desde otros módulos.
Apéndice 1: ¿cómo ejecutar y depurar todo esto?
En el artículo hubo muchos ejemplos de código Puppet, pero no se mencionó cómo ejecutar este código. Bueno, rectifico.
Para trabajar con Puppet, solo se necesita un agente, pero en la mayoría de los casos también se requerirá un servidor.
Agente
Como mínimo, desde la versión cinco, los paquetes puppet-agent de contienen todas las dependencias (ruby y los gemas correspondientes), por lo que no hay dificultades en la instalación (hablo de distribuciones basadas en Debian; no usamos distribuciones basadas en RPM).
En el caso más simple, para aplicar una configuración de Puppet, solo es necesario ejecutar el agente en modo sin servidor: siempre que el código Puppet esté copiado en el nodo, ejecuta puppet apply:
atikhonov@atikhonov ~/puppet-test $ cat helloworld.pp
node default {
notify { '¡Hola, mundo!': }
}
atikhonov@atikhonov ~/puppet-test $ puppet apply helloworld.pp
Notice: Catálogo compilado para atikhonov.localdomain en el entorno de producción en 0.01 segundos
Notice: ¡Hola, mundo!
Notice: /Stage[main]/Main/Node[default]/Notify[¡Hola, mundo!]/message: definido 'mensaje' como '¡Hola, mundo!'
Notice: Catálogo aplicado en 0.01 segundosLo mejor, por supuesto, es levantar el servidor y ejecutar los agentes en los nodos en modo demonio; así, cada media hora aplicarán la configuración descargada del servidor.
Se puede simular un modelo de trabajo push; ingresa al nodo que te interese y ejecuta sudo puppet agent -t. La opción -t (--test) en realidad incluye varias opciones que se pueden habilitar también de forma individual. Entre estas opciones están las siguientes:
- no trabajar en modo demonio (por defecto, el agente se ejecuta en modo demonio);
- finalizar el trabajo tras aplicar el catálogo (por defecto, el agente continuará trabajando y aplicará la configuración cada media hora);
- escribir un registro detallado de su trabajo;
- mostrar cambios en los archivos.
El agente tiene un modo de operación sin cambios; se puede utilizar en caso de que no estés seguro de que has escrito una configuración correcta y quieras verificar qué cambiará el agente durante su ejecución. Este modo se habilita con el parámetro --noop en la línea de comandos: sudo puppet agent -t --noop.
Además, se puede habilitar un registro de depuración; en él, puppet escribe sobre todas las acciones que lleva a cabo: sobre el recurso que está procesando en ese momento, sobre los parámetros de ese recurso, sobre qué programas ejecuta. Por supuesto, este es el parámetro --debug.
Servidor
No voy a tratar en este artículo la configuración completa de puppetserver ni el despliegue de código en él, solo diré que se instala una versión perfectamente funcional del servidor que no requiere configuración adicional para trabajar en condiciones de un número reducido de nodos (digamos, hasta cien). Un mayor número de nodos ya requerirá ajuste; por defecto, puppetserver ejecuta no más de cuatro trabajadores, para mayor rendimiento es necesario aumentar su número y no olvidar aumentar los límites de memoria, de lo contrario, la mayor parte del tiempo el servidor estará realizando recolección de basura.
El despliegue de código — si necesitas algo rápido y simple, mira (en r10k)[], para instalaciones pequeñas debería ser suficiente.
Adición 2: recomendaciones para escribir código
- Extrae toda la lógica en clases y defines.
- Mantenga las clases y definiciones en módulos, no en manifiestos con descripciones de nodos.
- Utilice hechos.
- No haga condicionales por nombres de host.
- No dude en agregar parámetros para las clases y definiciones; es mejor que la lógica implícita oculta en el cuerpo de la clase/definición.
Y por qué lo recomiendo, lo explicaré en el siguiente artículo.
Conclusión
Terminamos aquí con la introducción. En el siguiente artículo hablaré sobre Hiera, ENC y PuppetDB.
Solo los usuarios registrados pueden participar en la encuesta. , por favor.
De hecho, hay mucho más material; puedo escribir artículos sobre los siguientes temas, vote sobre qué le gustaría leer:
- 59,1%Constructos avanzados de Puppet: ciclos, mapeo y otras expresiones lambda, coleccionistas de recursos, recursos exportados e interacción entre hosts a través de Puppet, etiquetas, proveedores, tipos de datos abstractos.
- 31,8%"Soy el admin de mamá" o cómo en Avito unimos varios servidores Puppet de diferentes versiones, y en principio, parte sobre la administración del servidor Puppet.
- 81,8%Cómo escribimos código Puppet: entorno de herramientas, documentación, pruebas, CI/CD.
Votaron 22 usuarios. 9 usuarios se abstuvieron.
Fuente: habr.com
