Introducción a la marioneta

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.

Introducción a la marioneta

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:

Aquí hay un ejemplo de cómo se ve el manifiesto:

# Комментарии пишутся, как и много где, после решётки.
#
# Описание конфигурации ноды начинается с ключевого слова 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á *.

En marionetas 6.0 cron como si retirado de la caja en Puppetserver, por lo que no hay documentación en el sitio general. Pero él esta en la caja en Puppet-Agent, por lo que no es necesario instalarlo por separado. Puedes ver la documentación correspondiente. en la documentación de la quinta versión de PuppetO en GitHub.

Sobre los recursos en general

Requisitos para la unicidad de los recursos

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:

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

Otros tipos de recursos tienen opciones similares para ayudar a evitar la duplicación: name у de coches, command у ejecutivo, etcétera.

Metaparámetros

Cada tipo de recurso tiene algunos parámetros especiales, independientemente de su naturaleza.

Lista completa de metaparámetros en la documentación de Puppet.

Lista corta:

  • 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.

Ejemplo:

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

Dependencias y notificaciones

Documentación aquí.

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:

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 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.

Se puede hacer usando variables.

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:

  1. Instale el paquete con este módulo.
  2. Creemos un archivo de configuración para este módulo.
  3. Creamos un enlace simbólico a la configuración de php-fpm.
  4. 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.

Declaraciones condicionales y selectores

Documentación aquí.

if

Aquí todo es simple:

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

a menos que

a menos que sea un if al revés: el bloque de código se ejecutará si la expresión es falsa.

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

case

Aquí tampoco hay nada complicado. Puede utilizar valores regulares (cadenas, números, etc.), expresiones regulares y tipos de datos como valores.

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

Selectores

Un selector es una construcción de lenguaje similar a case, pero en lugar de ejecutar un bloque de código, devuelve un valor.

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

Módulos

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.

Documentación aquí.

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í:

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

Ver ruta <modulename>/<filename> implica archivo <rootdir>/modules/<modulename>/templates/<filename>.

Además, hay una función inline_template — recibe el texto de la plantilla como entrada, no el nombre del archivo.

Dentro de las plantillas, puede utilizar todas las variables de Puppet en el ámbito actual.

Puppet admite plantillas en formato ERB y EPP:

Brevemente sobre ERB

Estructuras de Control:

  • <%= ВЫРАЖЕНИЕ %> — 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:

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 - 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 просто выводит сообщение в лог

Formalmente hablando, un hecho tiene un nombre (cadena) y un valor (hay varios tipos disponibles: cadenas, matrices, diccionarios). Comer conjunto de hechos incorporados. También puedes escribir el tuyo propio. Se describen los recopiladores de datos. funciones similares en Rubyya sea como archivos ejecutables. Los hechos también pueden presentarse en la forma archivos de texto con datos en los nodos.

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.

examplefact=examplevalue
---
examplefact2: examplevalue2
anotherfact: anothervalue

Llegando a los hechos

Hay dos maneras de abordar los hechos:

  • a través del diccionario $facts: $facts['fqdn'];
  • usando el nombre del hecho como nombre de la variable: $fqdn.

lo mejor es usar un diccionario $facts, o incluso mejor, indique el espacio de nombres global ($::facts).

Aquí está la sección relevante de la documentación.

Variables incorporadas

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

  1. Coloque toda la lógica en clases y definiciones.
  2. Mantenga las clases y definiciones en módulos, no en manifiestos que describan nodos.
  3. Utilice los hechos.
  4. No hagas preguntas basadas en nombres de host.
  5. 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

22 usuarios votaron. 9 usuarios se abstuvieron.

Fuente: habr.com