Puppet es un sistema de gestión de configuración. Se utiliza para llevar los hosts al estado deseado y mantener este estado.
Llevo más de cinco años trabajando con Puppet. Este texto es esencialmente una compilación traducida y reordenada de puntos clave de la documentación oficial, que permitirá a los principiantes comprender rápidamente la esencia de Puppet.
Información básica
El sistema operativo de Puppet es cliente-servidor, aunque también admite funcionamiento sin servidor con funcionalidad limitada.
Se utiliza un modelo de operación pull: de forma predeterminada, una vez cada media hora, los clientes contactan al servidor para realizar una configuración y aplicarla. Si ha trabajado con Ansible, entonces utilizan un modelo de inserción diferente: el administrador inicia el proceso de aplicación de la configuración, los propios clientes no aplicarán nada.
Durante la comunicación de red, se utiliza cifrado TLS bidireccional: el servidor y el cliente tienen sus propias claves privadas y los certificados correspondientes. Normalmente, el servidor emite certificados para los clientes, pero en principio es posible utilizar una CA externa.
Introducción a los manifiestos
En terminología de marionetas al servidor títere conectar nodos (nodos). La configuración de los nodos está escrita. en manifiestos en un lenguaje de programación especial: Puppet DSL.
Puppet DSL es un lenguaje declarativo. Describe el estado deseado del nodo en forma de declaraciones de recursos individuales, por ejemplo:
El archivo existe y tiene contenido específico.
El paquete está instalado.
El servicio ha comenzado.
Los recursos se pueden interconectar:
Hay dependencias que afectan el orden en que se utilizan los recursos.
Por ejemplo, "primero instale el paquete, luego edite el archivo de configuración y luego inicie el servicio".
Hay notificaciones: si un recurso ha cambiado, envía notificaciones a los recursos suscritos a él.
Por ejemplo, si el archivo de configuración cambia, puede reiniciar automáticamente el servicio.
Además, Puppet DSL tiene funciones y variables, así como sentencias condicionales y selectores. También se admiten varios mecanismos de plantillas: EPP y ERB.
Puppet está escrito en Ruby, por lo que muchas de las construcciones y términos se toman de allí. Ruby le permite expandir Puppet: agregar lógica compleja, nuevos tipos de recursos y funciones.
Mientras se ejecuta Puppet, los manifiestos de cada nodo específico del servidor se compilan en un directorio. directorio es una lista de recursos y sus relaciones después de calcular el valor de funciones, variables y expansión de declaraciones condicionales.
Sintaxis y estilo de código
Aquí hay secciones de la documentación oficial que lo ayudarán a comprender la sintaxis si los ejemplos proporcionados no son suficientes:
# Комментарии пишутся, как и много где, после решётки.
#
# Описание конфигурации ноды начинается с ключевого слова node,
# за которым следует селектор ноды — хостнейм (с доменом или без)
# или регулярное выражение для хостнеймов, или ключевое слово default.
#
# После этого в фигурных скобках описывается собственно конфигурация ноды.
#
# Одна и та же нода может попасть под несколько селекторов. Про приоритет
# селекторов написано в статье про синтаксис описания нод.
node 'hostname', 'f.q.d.n', /regexp/ {
# Конфигурация по сути является перечислением ресурсов и их параметров.
#
# У каждого ресурса есть тип и название.
#
# Внимание: не может быть двух ресурсов одного типа с одинаковыми названиями!
#
# Описание ресурса начинается с его типа. Тип пишется в нижнем регистре.
# Про разные типы ресурсов написано ниже.
#
# После типа в фигурных скобках пишется название ресурса, потом двоеточие,
# дальше идёт опциональное перечисление параметров ресурса и их значений.
# Значения параметров указываются через т.н. hash rocket (=>).
resource { 'title':
param1 => value1,
param2 => value2,
param3 => value3,
}
}
La sangría y los saltos de línea no son una parte obligatoria del manifiesto, pero se recomienda guía de estilo. Resumen:
No se utilizan sangrías de dos espacios ni tabulaciones.
Las llaves están separadas por un espacio; los dos puntos no están separados por un espacio.
Comas después de cada parámetro, incluido el último. Cada parámetro está en una línea separada. Se hace una excepción para el caso sin parámetros y con un parámetro: se puede escribir en una línea y sin coma (es decir, resource { 'title': } и resource { 'title': param => value }).
Las flechas de los parámetros deben estar al mismo nivel.
Delante de ellos están escritas flechas de relación de recursos.
Ubicación de archivos en pappetserver
Para una explicación más detallada, presentaré el concepto de "directorio raíz". El directorio raíz es el directorio que contiene la configuración de Puppet para un nodo específico.
El directorio raíz varía según la versión de Puppet y los entornos utilizados. Los entornos son conjuntos independientes de configuración que se almacenan en directorios separados. Generalmente se usa en combinación con git, en cuyo caso los entornos se crean a partir de ramas de git. En consecuencia, cada nodo está ubicado en un entorno u otro. Esto se puede configurar en el propio nodo o en ENC, de lo que hablaré en el próximo artículo.
En la tercera versión ("old Puppet") el directorio base era /etc/puppet. El uso de entornos es opcional; por ejemplo, no los usamos con el antiguo Puppet. Si se utilizan ambientes, generalmente 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 directorio base.
A partir de la cuarta versión (“nuevo Puppet”), el uso de entornos se volvió obligatorio y el directorio base se trasladó a /etc/puppetlabs/code. En consecuencia, los entornos se almacenan en /etc/puppetlabs/code/environments, el directorio raíz es el directorio del entorno.
Debe haber un subdirectorio en el directorio raíz. manifests, que contiene uno o más manifiestos que describen los nodos. Además, debería haber un subdirectorio. modules, que contiene los módulos. Te diré qué módulos hay un poco más adelante. Además, el antiguo Puppet también puede tener un subdirectorio. files, que contiene varios archivos que copiamos a los nodos. En el nuevo Puppet, todos los archivos se colocan en módulos.
Los archivos de manifiesto tienen la extensión .pp.
Un par de ejemplos de combate.
Descripción del nodo y recurso en él.
en el nodo server1.testdomain se debe crear un archivo /etc/issue con contenido Debian GNU/Linux n l. El archivo debe ser propiedad de un usuario y grupo. root, los derechos de acceso deben ser 644.
Escribimos un manifiesto:
node 'server1.testdomain' { # блок конфигурации, относящийся к ноде server1.testdomain
file { '/etc/issue': # описываем файл /etc/issue
ensure => present, # этот файл должен существовать
content => 'Debian GNU/Linux n l', # у него должно быть такое содержимое
owner => root, # пользователь-владелец
group => root, # группа-владелец
mode => '0644', # права на файл. Они заданы в виде строки (в кавычках), потому что иначе число с 0 в начале будет воспринято как записанное в восьмеричной системе, и всё пойдёт не так, как задумано
}
}
Relaciones entre recursos en un nodo
en el nodo server2.testdomain nginx debe estar ejecutándose, trabajando con una configuración previamente preparada.
Descompongamos el problema:
Es necesario instalar el paquete. nginx.
Es necesario que los archivos de configuración se copien del servidor.
El servicio debe estar funcionando. nginx.
Si se actualiza la configuración, se debe reiniciar el servicio.
Escribimos un manifiesto:
node 'server2.testdomain' { # блок конфигурации, относящийся к ноде server2.testdomain
package { 'nginx': # описываем пакет nginx
ensure => installed, # он должен быть установлен
}
# Прямая стрелка (->) говорит о том, что ресурс ниже должен
# создаваться после ресурса, описанного выше.
# Такие зависимости транзитивны.
-> file { '/etc/nginx': # описываем файл /etc/nginx
ensure => directory, # это должна быть директория
source => 'puppet:///modules/example/nginx-conf', # её содержимое нужно брать с паппет-сервера по указанному адресу
recurse => true, # копировать файлы рекурсивно
purge => true, # нужно удалять лишние файлы (те, которых нет в источнике)
force => true, # удалять лишние директории
}
# Волнистая стрелка (~>) говорит о том, что ресурс ниже должен
# подписаться на изменения ресурса, описанного выше.
# Волнистая стрелка включает в себя прямую (->).
~> service { 'nginx': # описываем сервис nginx
ensure => running, # он должен быть запущен
enable => true, # его нужно запускать автоматически при старте системы
}
# Когда ресурс типа service получает уведомление,
# соответствующий сервис перезапускается.
}
Para que esto funcione, necesita aproximadamente la siguiente ubicación de archivo 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.conf
Tipos de recursos
Puede encontrar una lista completa de los tipos de recursos admitidos aquí en la documentación, aquí describiré cinco tipos básicos, que en mi práctica son suficientes para resolver la mayoría de los problemas.
presentar
Gestiona archivos, directorios, enlaces simbólicos, sus contenidos y derechos de acceso.
opciones:
nombre del recurso — ruta al archivo (opcional)
camino — ruta al archivo (si no se especifica en el nombre)
garantizar - Tipo de archivo:
absent - eliminar un archivo
present — debe haber un archivo de cualquier tipo (si no hay ningún archivo, se creará un archivo normal)
file - archivo normal
directory - directorio
link - enlace simbólico
contenido — contenido del archivo (apto sólo para archivos normales, no se puede utilizar junto con fuente o dirigidos)
fuente — un enlace a la ruta desde la que desea copiar el contenido del archivo (no se puede utilizar junto con contenido o dirigidos). Se puede especificar como un URI con un esquema puppet: (entonces se utilizarán archivos del servidor Puppet), y con el esquema http: (Espero que quede claro qué pasará en este caso), e incluso con el diagrama file: o como una ruta absoluta sin un esquema (entonces se usará el archivo del FS local en el nodo)
dirigidos — hacia dónde debe apuntar el enlace simbólico (no se puede utilizar junto con contenido o fuente)
propietario — el usuario que debería ser propietario del archivo
grupo de XNUMX — el grupo al que debe pertenecer el archivo
modo — permisos de archivo (como una cadena)
recurrente - permite el procesamiento recursivo de directorios
purga - permite eliminar archivos que no se describen en Puppet
forzar - permite eliminar directorios que no están descritos en Puppet
paquete
Instala y elimina paquetes. Capaz de manejar notificaciones: reinstala el paquete si se especifica el parámetro reinstall_on_refresh.
opciones:
nombre del recurso — nombre del paquete (opcional)
nombre — nombre del paquete (si no se especifica en el nombre)
proveedor - administrador de paquetes para usar
garantizar — estado deseado del paquete:
present, installed - cualquier versión instalada
latest - última versión instalada
absent - 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)
любая другая строка — la versión especificada está instalada
reinstall_on_refresh - si true, luego de recibir la notificación, el paquete se reinstalará. Útil para distribuciones basadas en código fuente, donde puede ser necesario reconstruir paquetes al cambiar los parámetros de compilación. Por defecto false.
de coches
Gestiona servicios. Capaz de procesar notificaciones: reinicia el servicio.
opciones:
nombre del recurso — servicio a gestionar (opcional)
nombre — el servicio que debe gestionarse (si no se especifica en el nombre)
garantizar — estado deseado del servicio:
running - lanzado
stopped - interrumpido
habilitar — controla la capacidad de iniciar el servicio:
true — la ejecución automática está habilitada (systemctl enable)
mask - disfrazado (systemctl mask)
false — la ejecución automática está deshabilitada (systemctl disable)
reanudar - comando para reiniciar el servicio
estado — comando para verificar el estado del servicio
ha reiniciado — indique si el script de inicio del servicio admite el reinicio. Si false y se especifica el parámetro reanudar — se utiliza el valor de este parámetro. Si false y parámetro reanudar no especificado: el servicio se detiene y comienza a reiniciarse (pero systemd usa el comando systemctl restart).
estado hast — indicar si el script de inicio del servicio admite el comando status. Si false, entonces se utiliza el valor del parámetro estado. Por defecto true.
ejecutivo
Ejecuta comandos externos. Si no especifica parámetros crea, sólo si, a menos que o actualizar solo, el comando se ejecutará cada vez que se ejecute Puppet. Capaz de procesar notificaciones: ejecuta un comando.
opciones:
nombre del recurso — comando a ejecutar (opcional)
comando — el comando a ejecutar (si no está especificado en el nombre)
camino — rutas en las que buscar el archivo ejecutable
sólo si — si el comando especificado en este parámetro se completa con un código de retorno cero, se ejecutará el comando principal
a menos que — si el comando especificado en este parámetro se completa con un código de retorno distinto de cero, se ejecutará el comando principal
crea — si el archivo especificado en este parámetro no existe, se ejecutará el comando principal
actualizar solo - si true, entonces el comando solo se ejecutará cuando este ejecutivo reciba notificaciones de otros recursos
cwd — directorio desde el cual ejecutar el comando
usuario — el usuario desde quien ejecutar el comando
proveedor - cómo ejecutar el comando:
POSIX — simplemente se crea un proceso hijo, asegúrese de especificar camino
shell - el comando se ejecuta en el shell /bin/sh, puede no estar especificado camino, puede utilizar globos, tuberías y otras funciones de shell. Generalmente se detecta automáticamente si hay caracteres especiales (|, ;, &&, || y así sucesivamente).
cron
Controla los trabajos cronológicos.
opciones:
nombre del recurso - solo algún tipo de identificador
garantizar - estado de la corona:
present - crear si no existe
absent - eliminar si existe
comando - 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, horas., día laborable, mes, mes dia - cuándo ejecutar cron. Si alguno de estos atributos no se especifica, su valor en el crontab será *.
El error más común que encontramos es Declaración duplicada. Este error ocurre cuando aparecen en el directorio dos o más recursos del mismo tipo con el mismo nombre.
Por eso, volveré a escribir: ¡Los manifiestos para el mismo nodo no deben contener recursos del mismo tipo con el mismo título!
A veces es necesario instalar paquetes con el mismo nombre, pero con diferentes administradores de paquetes. En este caso, es necesario utilizar el parámetro namepara evitar el error:
exigir — este parámetro indica de qué recursos depende este recurso.
antes - Este parámetro especifica qué recursos dependen de este recurso.
Suscríbase — este parámetro especifica de qué recursos este recurso recibe notificaciones.
notificar — Este parámetro especifica qué recursos reciben notificaciones de este recurso.
Todos los metaparámetros enumerados aceptan un único enlace de recurso o una serie de enlaces entre corchetes.
Enlaces a recursos
Un enlace a un recurso es simplemente una mención del recurso. Se utilizan principalmente para indicar dependencias. Hacer referencia a un recurso inexistente provocará un error de compilación.
La sintaxis del enlace es la siguiente: tipo de recurso con letra mayúscula (si el nombre del tipo contiene dos puntos, entonces cada parte del nombre entre dos puntos está en mayúscula), luego el nombre del recurso entre corchetes (el caso del nombre ¡no cambia!). No debe haber espacios; los corchetes se escriben inmediatamente después del nombre del tipo.
Como se indicó anteriormente, las dependencias simples entre recursos son transitivas. Por cierto, tenga cuidado al agregar dependencias: puede crear dependencias cíclicas, lo que provocará un error de compilación.
A diferencia de las dependencias, las notificaciones no son transitivas. Se aplican las siguientes reglas para las notificaciones:
Si el recurso recibe una notificación, se actualiza. Las acciones de actualización dependen del tipo de recurso: ejecutivo ejecuta el comando, de coches reinicia el servicio, paquete reinstala el paquete. Si el recurso no tiene definida una acción de actualización, entonces no sucede nada.
Durante una ejecución de Puppet, el recurso no se actualiza más de una vez. Esto es posible porque las notificaciones incluyen dependencias y el gráfico de dependencia 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.
Manejo de parámetros no especificados
Como regla general, si algún parámetro de recurso no tiene un valor predeterminado y este parámetro no está especificado en el manifiesto, Puppet no cambiará esta propiedad para el recurso correspondiente en el nodo. Por ejemplo, si un recurso de tipo presentar parámetro no especificado owner, entonces Puppet no cambiará el propietario del archivo correspondiente.
Introducción a clases, variables y definiciones.
Supongamos que tenemos varios nodos que tienen la misma parte de la configuración, pero también hay diferencias; de lo contrario, podríamos describirlo todo en un solo bloque. node {}. Por supuesto, puede simplemente copiar partes idénticas de la configuración, pero en general esta es una mala solución: la configuración crece y, si cambia la parte general de la configuración, tendrá que editar lo mismo en muchos lugares. Al mismo tiempo, es fácil cometer un error y, en general, el principio DRY (no repetirse) no se inventó por una razón.
Para resolver este problema existe un diseño como clase.
Классы
clase es un bloque con nombre de código poppet. Se necesitan clases para reutilizar el código.
Primero es necesario describir la clase. La descripción en sí no agrega ningún recurso en ninguna parte. La clase se describe en manifiestos:
# Описание класса начинается с ключевого слова class и его названия.
# Дальше идёт тело класса в фигурных скобках.
class example_class {
...
}
Después de esto se puede utilizar la clase:
# первый вариант использования — в стиле ресурса с типом class
class { 'example_class': }
# второй вариант использования — с помощью функции include
include example_class
# про отличие этих двух вариантов будет рассказано дальше
Un ejemplo de la tarea anterior: traslademos la instalación y configuración de nginx a una clase:
La clase del ejemplo anterior no es nada flexible porque siempre trae la misma configuración de nginx. Hagamos la ruta a la variable de configuración, luego esta clase se puede usar para instalar nginx con cualquier configuración.
Atención: ¡las variables en Puppet son inmutables!
Además, solo se puede acceder a una variable después de haber sido declarada; 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}"
La marioneta tiene espacios de nombres, y las variables, en consecuencia, tienen área de visibilidad: Una variable con el mismo nombre se puede definir en diferentes espacios de nombres. Al resolver el valor de una variable, la variable se busca en el espacio de nombres actual, luego en el espacio de nombres adjunto, y así sucesivamente.
Ejemplos de espacios de nombres:
global: las variables fuera de la clase o descripción del nodo van allí;
espacio de nombres del nodo en la descripción del nodo;
espacio de nombres de clase en la descripción de la clase.
Para evitar ambigüedades al acceder a una variable, puede especificar el espacio de nombres en el nombre de la variable:
# переменная без пространства имён
$var
# переменная в глобальном пространстве имён
$::var
# переменная в пространстве имён класса
$classname::var
$::classname::var
Aceptemos que la ruta a la configuración de nginx se encuentra en la variable $nginx_conf_source. Entonces la clase se verá así:
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
}
Sin embargo, el ejemplo dado es malo porque existe un “conocimiento secreto” de que en algún lugar dentro de la clase se usa una variable con tal o cual nombre. Es mucho más correcto generalizar este conocimiento: las clases pueden tener parámetros.
Parámetros de clase son variables en el espacio de nombres de la clase, se especifican en el encabezado de la clase y pueden usarse como variables regulares en el cuerpo de la clase. Los valores de los parámetros se especifican cuando se usa la clase en el manifiesto.
El parámetro se puede establecer en un valor predeterminado. Si un parámetro no tiene un valor predeterminado y el valor no se establece cuando se usa, se producirá un error de compilación.
Parametricemos la clase del ejemplo anterior y agreguemos 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 se escriben. Comer muchos tipos de datos. Los tipos de datos se utilizan normalmente para validar los valores de los parámetros pasados a clases y definiciones. Si el parámetro pasado no coincide con el tipo especificado, se producirá un error de compilación.
El tipo se escribe inmediatamente antes del nombre del parámetro:
class example (
String $param1,
Integer $param2,
Array $param3,
Hash $param4,
Hash[String, String] $param5,
) {
...
}
Clases: incluir nombre de clase vs clase{'nombre de clase':}
Cada clase es un recurso de tipo. clase. Como ocurre con cualquier otro tipo de recurso, no puede haber dos instancias de la misma clase en el mismo nodo.
Si intenta agregar una clase al mismo nodo dos veces usando class { 'classname':} (sin diferencia, con parámetros diferentes o idénticos), habrá un error de compilación. Pero si usa una clase en el estilo de recurso, puede establecer inmediatamente explícitamente todos sus parámetros en el manifiesto.
Sin embargo, si usas include, entonces la clase se puede agregar tantas veces como se desee. El hecho es que include es una función idempotente que comprueba si se ha agregado una clase al directorio. Si la clase no está en el directorio la agrega, y si ya existe no hace nada. Pero en caso de utilizar include No puede establecer parámetros de clase durante la declaración de clase; todos los parámetros requeridos deben configurarse en una fuente de datos externa: Hiera o ENC. Hablaremos de ellos en el próximo artículo.
define
Como se dijo en el bloque anterior, la misma clase no puede estar presente en un nodo más de una vez. Sin embargo, en algunos casos es necesario poder utilizar el mismo bloque de código con diferentes parámetros en el mismo nodo. En otras palabras, se necesita un tipo de recurso propio.
Por ejemplo, para instalar el módulo PHP, hacemos lo siguiente en Avito:
Instale el paquete con este módulo.
Creemos un archivo de configuración para este módulo.
Creamos un enlace simbólico a la configuración de php-fpm.
Creamos un enlace simbólico a la configuración de php cli.
En tales casos, un diseño como definir (definir, tipo definido, tipo de recurso definido). Un Define es similar a una clase, pero hay diferencias: primero, cada Define es un tipo de recurso, no un recurso; en segundo lugar, cada definición tiene un parámetro implícito $title, donde va el nombre del recurso cuando se declara. Al igual que en el caso de las clases, primero se debe describir una definición, después de lo cual 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'], # триггеры дебиановских 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' }
}
La forma más sencilla de detectar el error de declaración duplicada es en Definir. Esto sucede si una definición tiene un recurso con un nombre constante y hay dos o más instancias de esta definición en algún nodo.
Es fácil protegerse de esto: todos los recursos dentro de la definición deben tener un nombre dependiendo de $title. Una alternativa es la adición idempotente de recursos; en el caso más simple, basta con mover los recursos comunes a todas las instancias de la definición a una clase separada e incluir esta clase en la definición - función include idempotente.
Hay otras formas de lograr la idempotencia al agregar recursos, es decir, usar funciones defined и ensure_resources, pero te lo contaré en el próximo episodio.
Dependencias y notificaciones para clases y definiciones.
Las clases y definiciones agregan las siguientes reglas para manejar dependencias y notificaciones:
la dependencia de una clase/definir agrega dependencias de todos los recursos de la clase/definir;
una dependencia de clase/definición agrega dependencias a todos los recursos de clase/definición;
la notificación de clase/definición notifica todos los recursos de la clase/definición;
La suscripción class/define se suscribe a todos los recursos de class/define.
Cuando la configuración es pequeña, se puede mantener fácilmente en un manifiesto. Pero cuantas más configuraciones describimos, más clases y nodos hay en el manifiesto, crece y resulta incómodo trabajar con él.
Además, existe el problema de la reutilización del código: cuando todo el código está en un manifiesto, es difícil compartirlo con otros. Para solucionar estos dos problemas, Puppet tiene una entidad llamada módulos.
Módulos - estos son conjuntos de clases, definiciones y otras entidades Puppet ubicadas en un directorio separado. En otras palabras, un módulo es una pieza independiente de la lógica Puppet. Por ejemplo, puede haber un módulo para trabajar con nginx y contendrá qué y solo lo que se necesita para trabajar con nginx, o puede haber un módulo para trabajar con PHP, etc.
Los módulos tienen versiones y también se admiten las dependencias de los módulos entre sí. Hay un repositorio abierto de módulos. Fragua de marionetas.
En el servidor Puppet, los módulos se encuentran en el subdirectorio de módulos del directorio raíz. Dentro de cada módulo hay un esquema de directorio estándar: manifiestos, archivos, plantillas, lib, etc.
Estructura de archivos en un módulo
La raíz del módulo puede contener los siguientes directorios con nombres descriptivos:
manifests - contiene manifiestos
files - contiene archivos
templates - contiene plantillas
lib - contiene código Ruby
Esta no es una lista completa de directorios y archivos, pero por ahora es suficiente para este artículo.
Nombres de recursos y nombres de archivos en el módulo.
Los recursos (clases, definiciones) de un módulo no pueden tener el nombre que desee. Además, existe una correspondencia directa entre el nombre de un recurso y el nombre del archivo en el que Puppet buscará una descripción de ese recurso. Si viola las reglas de nomenclatura, Puppet simplemente no encontrará la descripción del recurso y obtendrá un error de compilación.
Las reglas son simples:
Todos los recursos de un módulo deben estar en el espacio de nombres del módulo. Si el módulo se llama foo, entonces todos los recursos que contiene deben tener un nombre foo::<anything>o solo foo.
El recurso con el nombre del módulo debe estar en el archivo. init.pp.
Para otros recursos, el esquema de nomenclatura de archivos es el siguiente:
se descarta el prefijo con el nombre del módulo
todos los dos puntos, si los hay, se reemplazan con barras
se agrega la extensión .pp
Lo demostraré con un ejemplo. Digamos que estoy escribiendo un módulo. nginx. Contiene los siguientes recursos:
clase nginx descrito en el manifiesto init.pp;
clase nginx::service descrito en el manifiesto service.pp;
definir nginx::server descrito en el manifiesto server.pp;
definir nginx::server::location descrito en el manifiesto server/location.pp.
Plantillas
Seguramente tú mismo sabes qué son las plantillas, no las describiré en detalle aquí. Pero lo dejaré por si acaso. enlace a wikipedia.
Cómo usar plantillas: el significado de una plantilla se puede ampliar usando una función template, al que se le pasa la ruta a la plantilla. Para recursos de tipo presentar utilizado junto con el parámetro content. Por ejemplo, así:
<%= ВЫРАЖЕНИЕ %> — inserta el valor de la expresión
<% ВЫРАЖЕНИЕ %> — calcular el valor de una expresión (sin insertarla). Las sentencias condicionales (if) y los bucles (cada uno) suelen ir aquí.
<%# КОММЕНТАРИЙ %>
Las expresiones en ERB están escritas en Ruby (ERB es en realidad Embedded Ruby).
Para acceder a las variables desde el manifiesto, debe agregar @ al nombre de la variable. Para eliminar un salto de línea que aparece después de una construcción de control, necesita usar una etiqueta de cierre -%>.
Ejemplo de uso de la plantilla
Digamos que estoy escribiendo un módulo para controlar ZooKeeper. La clase responsable de crear la configuración se parece a esta:
Y la plantilla correspondiente zoo.cfg.erb - Entonces:
<% 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 -%>
Hechos y variables integradas
A menudo, la parte específica de la configuración depende de lo que esté sucediendo actualmente en el nodo. Por ejemplo, dependiendo de la versión de Debian, deberá instalar una u otra versión del paquete. Puede monitorear todo esto manualmente, reescribiendo manifiestos si los nodos cambian. Pero éste no es un enfoque serio; la automatización es mucho mejor.
Para obtener información sobre los nodos, Puppet dispone de un mecanismo llamado hechos. Hechos - esta es información sobre el nodo, disponible en manifiestos en forma de variables ordinarias en el espacio de nombres global. Por ejemplo, nombre de host, versión del sistema operativo, arquitectura del procesador, lista de usuarios, lista de interfaces de red y sus direcciones, y mucho, mucho más. Los hechos están disponibles en manifiestos y plantillas como variables regulares.
Un ejemplo de trabajo con hechos:
notify { "Running OS ${facts['os']['name']} version ${facts['os']['release']['full']}": }
# ресурс типа notify просто выводит сообщение в лог
Durante la operación, el agente títere primero copia todos los recopiladores de datos disponibles del servidor pappets al nodo, después de lo cual los inicia y envía los datos recopilados al servidor; Después de esto, el servidor comienza a compilar el catálogo.
Hechos en forma de archivos ejecutables.
Estos datos se colocan en módulos en el directorio. facts.d. Por supuesto, los archivos deben ser ejecutables. Cuando se ejecutan, deben enviar información a la salida estándar en formato YAML o clave=valor.
No olvide que los hechos se aplican a todos los nodos controlados por el servidor poppet en el que se implementa su módulo. Por lo tanto, en el script, asegúrese de verificar que el sistema tenga todos los programas y archivos necesarios para que su hecho funcione.
#!/bin/sh
echo "testfact=success"
#!/bin/sh
echo '{"testyamlfact":"success"}'
hechos de rubí
Estos datos 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
end
Datos del texto
Estos hechos se colocan en los nodos del directorio. /etc/facter/facts.d en marionetas antiguas o /etc/puppetlabs/facts.d en el nuevo títere.
Además de los hechos, también hay algunas variables, disponible en el espacio de nombres global.
hechos confiables — variables que se toman del certificado del cliente (dado que el certificado generalmente se emite en un servidor poppet, el agente no puede simplemente tomar y cambiar su certificado, por lo que las variables son “confiables”): el nombre del certificado, el nombre del host y dominio, extensiones del certificado.
hechos del servidor —variables relacionadas con información sobre el servidor—versión, nombre, dirección IP del servidor, entorno.
hechos del agente — variables agregadas directamente por el agente títere y no por factor: nombre del certificado, versión del agente, versión del títere.
variables maestras - Variables de Pappetmaster (¡sic!). Es casi lo mismo que en hechos del servidor, además los valores de los parámetros de configuración están disponibles.
variables del compilador — variables del compilador que difieren en cada ámbito: el nombre del módulo actual y el nombre del módulo en el que se accedió al objeto actual. Se pueden utilizar, por ejemplo, para comprobar que tus clases privadas no se están utilizando directamente desde otros módulos.
Adición 1: ¿cómo ejecutar y depurar todo esto?
El artículo contenía muchos ejemplos de código de marionetas, pero no nos decía en absoluto cómo ejecutar este código. Bueno, me estoy corrigiendo.
Un agente es suficiente para ejecutar Puppet, pero en la mayoría de los casos también necesitarás un servidor.
Agente
Al menos desde la versión XNUMX, los paquetes de agentes marionetas de repositorio oficial de Puppetlabs contienen todas las dependencias (ruby y las gemas correspondientes), por lo que no hay dificultades de instalación (estoy hablando de distribuciones basadas en Debian; no utilizamos distribuciones basadas en RPM).
En el caso más simple, para utilizar la configuración de Puppet, basta con iniciar el agente en modo sin servidor: siempre que el código de Puppet esté copiado en el nodo, inicie puppet apply <путь к манифесту>:
atikhonov@atikhonov ~/puppet-test $ cat helloworld.pp
node default {
notify { 'Hello world!': }
}
atikhonov@atikhonov ~/puppet-test $ puppet apply helloworld.pp
Notice: Compiled catalog for atikhonov.localdomain in environment production in 0.01 seconds
Notice: Hello world!
Notice: /Stage[main]/Main/Node[default]/Notify[Hello world!]/message: defined 'message' as 'Hello world!'
Notice: Applied catalog in 0.01 seconds
Por supuesto, es mejor configurar el servidor y ejecutar agentes en los nodos en modo demonio; luego, cada media hora, aplicarán la configuración descargada del servidor.
Puede imitar el modelo de trabajo push: vaya al nodo que le interesa y comience sudo puppet agent -t... Llave -t (--test) en realidad incluye varias opciones que se pueden habilitar individualmente. Estas opciones incluyen lo siguiente:
no ejecutar en modo demonio (de forma predeterminada, el agente se inicia en modo demonio);
cerrar después de aplicar el catálogo (de forma predeterminada, el agente continuará trabajando y aplicará la configuración una vez cada media hora);
escribir un registro de trabajo detallado;
mostrar cambios en los archivos.
El agente tiene un modo de funcionamiento sin cambios; puede usarlo cuando no esté seguro de haber escrito la configuración correcta y desee comprobar qué cambiará exactamente el agente durante el funcionamiento. Este modo está habilitado por el parámetro --noop en la línea de comando: sudo puppet agent -t --noop.
Además, puede habilitar el registro de depuración del trabajo: en él, Puppet escribe sobre todas las acciones que realiza: sobre el recurso que está procesando actualmente, sobre los parámetros de este recurso, sobre qué programas inicia. Por supuesto que este es un parámetro. --debug.
Servidor
No consideraré la configuración completa del servidor pappets ni la implementación del código en este artículo; solo diré que lista para usar hay una versión completamente funcional del servidor que no requiere configuración adicional para funcionar con una pequeña cantidad de nodos (digamos, hasta cien). Una mayor cantidad de nodos requerirá ajuste: de forma predeterminada, el servidor de marionetas no inicia más de cuatro trabajadores; para un mayor rendimiento, debe aumentar su número y no olvide aumentar los límites de memoria; de lo contrario, el servidor recolectará basura la mayor parte del tiempo.
Implementación de código: si lo necesita de forma rápida y sencilla, mire (en r10k)[https://github.com/puppetlabs/r10k], para instalaciones pequeñas debería ser suficiente.
Anexo 2: Directrices de codificación
Coloque toda la lógica en clases y definiciones.
Mantenga las clases y definiciones en módulos, no en manifiestos que describan nodos.
Utilice los hechos.
No hagas preguntas basadas en nombres de host.
Siéntase libre de agregar parámetros para clases y definiciones; esto es mejor que la lógica implícita oculta en el cuerpo de la clase/definir.
Explicaré por qué recomiendo hacer esto en el próximo artículo.
Conclusión
Terminemos con la introducción. En el próximo artículo les hablaré sobre Hiera, ENC y PuppetDB.
Solo los usuarios registrados pueden participar en la encuesta. Registrarsepor favor
De hecho, hay mucho más material: puedo escribir artículos sobre los siguientes temas, votar sobre lo que le interesaría leer:
59,1%Construcciones de títeres avanzadas: algo de mierda del siguiente nivel: bucles, mapeo y otras expresiones lambda, recolectores de recursos, recursos exportados y comunicación entre hosts a través de Puppet, etiquetas, proveedores, tipos de datos abstractos.13
31,8%“Soy el administrador de mi madre” o cómo en Avito nos hicimos amigos de varios servidores poppet de diferentes versiones y, en principio, la parte de administrar el servidor poppet.7
81,8%Cómo escribimos código de marionetas: instrumentación, documentación, pruebas, CI/CD.18