Introdución a Títeres

Puppet é un sistema de xestión de configuración. Utilízase para levar os hosts ao estado desexado e manter este estado.

Levo máis de cinco anos traballando con Puppet. Este texto é esencialmente unha compilación traducida e reordenada de puntos clave da documentación oficial, que permitirá aos principiantes comprender rapidamente a esencia de Puppet.

Introdución a Títeres

Información básica

O sistema operativo de Puppet é cliente-servidor, aínda que tamén admite o funcionamento sen servidor cunha funcionalidade limitada.

Utilízase un modelo de funcionamento pull: por defecto, unha vez cada media hora, os clientes póñense en contacto co servidor para realizar unha configuración e aplícana. Se traballou con Ansible, entón usan un modelo push diferente: o administrador inicia o proceso de aplicación da configuración, os propios clientes non aplicarán nada.

Durante a comunicación de rede, utilízase o cifrado TLS bidireccional: o servidor e o cliente teñen as súas propias claves privadas e os correspondentes certificados. Normalmente o servidor emite certificados para os clientes, pero en principio é posible utilizar unha CA externa.

Introdución aos manifestos

En terminoloxía dos títeres ao servidor de títeres conectar nós (nodos). A configuración dos nodos está escrita en manifestos nunha linguaxe de programación especial - Puppet DSL.

Puppet DSL é unha linguaxe declarativa. Describe o estado desexado do nodo en forma de declaracións de recursos individuais, por exemplo:

  • O ficheiro existe e ten contido específico.
  • O paquete está instalado.
  • O servizo comezou.

Os recursos pódense interconectar:

  • Hai dependencias, afectan á orde na que se utilizan os recursos.
    Por exemplo, "primeiro instala o paquete, despois edita o ficheiro de configuración e inicia o servizo".
  • Hai notificacións: se un recurso cambiou, envía notificacións aos recursos subscritos a el.
    Por exemplo, se o ficheiro de configuración cambia, pode reiniciar automaticamente o servizo.

Ademais, o Puppet DSL ten funcións e variables, así como instrucións e selectores condicionais. Tamén se admiten varios mecanismos de creación de modelos: EPP e ERB.

Puppet está escrito en Ruby, polo que moitos dos constructos e termos son tomados de aí. Ruby permítelle expandir Puppet: engade lóxica complexa, novos tipos de recursos e funcións.

Mentres Puppet se está a executar, os manifestos de cada nodo específico do servidor compílanse nun directorio. Directorio é unha lista de recursos e as súas relacións despois de calcular o valor de funcións, variables e expansión de instrucións condicionais.

Sintaxe e estilo de código

Aquí tes seccións da documentación oficial que che axudarán a comprender a sintaxe se os exemplos proporcionados non son suficientes:

Aquí tes un exemplo de como é o manifesto:

# Комментарии пишутся, как и много где, после решётки.
#
# Описание конфигурации ноды начинается с ключевого слова node,
# за которым следует селектор ноды — хостнейм (с доменом или без)
# или регулярное выражение для хостнеймов, или ключевое слово default.
#
# После этого в фигурных скобках описывается собственно конфигурация ноды.
#
# Одна и та же нода может попасть под несколько селекторов. Про приоритет
# селекторов написано в статье про синтаксис описания нод.
node 'hostname', 'f.q.d.n', /regexp/ {
  # Конфигурация по сути является перечислением ресурсов и их параметров.
  #
  # У каждого ресурса есть тип и название.
  #
  # Внимание: не может быть двух ресурсов одного типа с одинаковыми названиями!
  #
  # Описание ресурса начинается с его типа. Тип пишется в нижнем регистре.
  # Про разные типы ресурсов написано ниже.
  #
  # После типа в фигурных скобках пишется название ресурса, потом двоеточие,
  # дальше идёт опциональное перечисление параметров ресурса и их значений.
  # Значения параметров указываются через т.н. hash rocket (=>).
  resource { 'title':
    param1 => value1,
    param2 => value2,
    param3 => value3,
  }
}

A sangría e os saltos de liña non son unha parte obrigatoria do manifesto, pero hai unha recomendación guía de estilo. Resumo:

  • Sangrías de dous espazos, non se usan tabulacións.
  • As chaves están separadas por un espazo; os dous puntos non están separados por un espazo.
  • Comas despois de cada parámetro, incluído o último. Cada parámetro está nunha liña separada. Faise unha excepción para o caso sen parámetros e un parámetro: pode escribir nunha liña e sen coma (i.e. resource { 'title': } и resource { 'title': param => value }).
  • As frechas dos parámetros deben estar ao mesmo nivel.
  • Frechas de relación de recursos están escritas diante delas.

Localización dos ficheiros en pappetserver

Para unha explicación máis detallada, introducirei o concepto de "directorio raíz". O directorio raíz é o directorio que contén a configuración de Puppet para un nodo específico.

O directorio raíz varía dependendo da versión de Puppet e dos ambientes empregados. Os ambientes son conxuntos independentes de configuración que se almacenan en directorios separados. Adoita usarse en combinación con git, nese caso os ambientes créanse a partir de ramas de git. En consecuencia, cada nodo está situado nun ambiente ou noutro. Isto pódese configurar no propio nodo, ou en ENC, do que falarei no seguinte artigo.

  • Na terceira versión ("antigo Puppet") o directorio base era /etc/puppet. O uso de ambientes é opcional; por exemplo, non os usamos co vello Puppet. Se se usan ambientes, normalmente almacénanse /etc/puppet/environments, o directorio raíz será o directorio do entorno. Se non se usan ambientes, o directorio raíz será o directorio base.
  • A partir da cuarta versión ("new Puppet"), o uso de ambientes fíxose obrigatorio e o directorio base foi movido a /etc/puppetlabs/code. En consecuencia, os ambientes almacénanse /etc/puppetlabs/code/environments, o directorio raíz é o directorio do entorno.

Debe haber un subdirectorio no directorio raíz manifests, que contén un ou máis manifestos que describen os nodos. Ademais, debería haber un subdirectorio modules, que contén os módulos. Vouche dicir cales son os módulos un pouco máis tarde. Ademais, o vello Puppet tamén pode ter un subdirectorio files, que contén varios ficheiros que copiamos nos nodos. No novo Puppet, todos os ficheiros colócanse en módulos.

Os ficheiros de manifesto teñen a extensión .pp.

Un par de exemplos de combate

Descrición do nodo e recurso sobre el

No nodo server1.testdomain hai que crear un ficheiro /etc/issue con contido Debian GNU/Linux n l. O ficheiro debe ser propiedade dun usuario e dun grupo root, os dereitos de acceso deben ser 644.

Escribimos un manifesto:

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 в начале будет воспринято как записанное в восьмеричной системе, и всё пойдёт не так, как задумано
    }
}

Relacións entre recursos nun nodo

No nodo server2.testdomain nginx debe estar en execución, traballando cunha configuración preparada previamente.

Descompoñamos o problema:

  • O paquete debe estar instalado nginx.
  • É necesario que os ficheiros de configuración sexan copiados do servidor.
  • O servizo cómpre comezar nginx.
  • Se a configuración se actualiza, o servizo debe reiniciarse.

Escribimos un manifesto:

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 isto funcione, necesita aproximadamente a seguinte localización do ficheiro no servidor de títeres:

/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

Aquí pódese atopar unha lista completa dos tipos de recursos admitidos na documentación, aquí vou describir cinco tipos básicos, que na miña práctica son suficientes para resolver a maioría dos problemas.

arquivo

Xestiona ficheiros, directorios, ligazóns simbólicas, o seu contido e dereitos de acceso.

Parámetros:

  • nome do recurso - ruta ao ficheiro (opcional)
  • camiño — ruta ao ficheiro (se non se especifica no nome)
  • garantir - Tipo de ficheiro:
    • absent - borrar un ficheiro
    • present — debe haber un ficheiro de calquera tipo (se non o hai, crearase un ficheiro normal)
    • file - arquivo normal
    • directory - directorio
    • link - ligazón simbólica
  • contido — contido do ficheiro (apto só para ficheiros normais, non se pode usar xunto con fonte ou obxectivo)
  • fonte — unha ligazón á ruta desde a que desexa copiar o contido do ficheiro (non se pode usar xunto con contido ou obxectivo). Pódese especificar como un URI cun esquema puppet: (logo empregaranse ficheiros do servidor de títeres), e co esquema http: (Espero que quede claro o que pasará neste caso), e mesmo co diagrama file: ou como un camiño absoluto sen esquema (entón utilizarase o ficheiro do FS local do nodo)
  • obxectivo — onde debería apuntar a ligazón simbólica (non se pode usar xunto con contido ou fonte)
  • propietario — o usuario que debería ser propietario do ficheiro
  • grupo — o grupo ao que debe pertencer o ficheiro
  • modo - permisos de ficheiro (como unha cadea)
  • recorrer - permite o procesamento recursivo de directorios
  • purga - permite eliminar ficheiros que non se describen en Puppet
  • facer - permite eliminar directorios que non se describen en Puppet

paquete

Instala e elimina paquetes. Capaz de xestionar notificacións: reinstala o paquete se se especifica o parámetro reinstall_on_refresh.

Parámetros:

  • nome do recurso - nome do paquete (opcional)
  • nome - nome do paquete (se non se especifica no nome)
  • fornecedor - xestor de paquetes para usar
  • garantir - estado desexado do paquete:
    • present, installed - Calquera versión instalada
    • latest - última versión instalada
    • absent - eliminado (apt-get remove)
    • purged - eliminado xunto cos ficheiros de configuración (apt-get purge)
    • held - a versión do paquete está bloqueada (apt-mark hold)
    • любая другая строка — a versión especificada está instalada
  • reinstall_on_refresh - se a true, despois de recibir a notificación, o paquete reinstalarase. Útil para distribucións baseadas en fontes, onde pode ser necesario reconstruír paquetes ao cambiar os parámetros de compilación. Por defecto false.

servizo

Xestiona servizos. Capaz de procesar notificacións: reinicia o servizo.

Parámetros:

  • nome do recurso - servizo a xestionar (opcional)
  • nome — o servizo que se debe xestionar (se non se especifica no nome)
  • garantir - Estado desexado do servizo:
    • running - lanzado
    • stopped - parou
  • permitir — controla a capacidade de iniciar o servizo:
    • true — a execución automática está activada (systemctl enable)
    • mask - disfrazado (systemctl mask)
    • false — a execución automática está desactivada (systemctl disable)
  • reiniciar - comando para reiniciar o servizo
  • Estado — comando para comprobar o estado do servizo
  • reiniciar — indique se o initscript do servizo admite o reinicio. Se false e especifícase o parámetro reiniciar — utilízase o valor deste parámetro. Se false e parámetro reiniciar non especificado: o servizo detívose e comeza a reiniciarse (pero systemd usa o comando systemctl restart).
  • hasstatus — indique se o initscript do servizo admite o comando status. Se false, entón úsase o valor do parámetro Estado. Por defecto true.

exec

Executa comandos externos. Se non especifica parámetros crea, Só si, a menos que ou refrescantemente, o comando executarase cada vez que se execute Puppet. Capaz de procesar notificacións: executa un comando.

Parámetros:

  • nome do recurso - comando a executar (opcional)
  • mando — o comando que se vai executar (se non se especifica no nome)
  • camiño — camiños nos que buscar o ficheiro executable
  • Só si — se o comando especificado neste parámetro completouse cun código de retorno cero, executarase o comando principal
  • a menos que — se o comando especificado neste parámetro completouse cun código de retorno distinto de cero, executarase o comando principal
  • crea — se o ficheiro especificado neste parámetro non existe, executarase o comando principal
  • refrescantemente - se a true, entón o comando só se executará cando este exec reciba notificación doutros recursos
  • cwd — directorio desde o que executar o comando
  • usuario — o usuario desde o que executar o comando
  • fornecedor - como executar o comando:
    • posix — Simplemente créase un proceso fillo, asegúrese de especificalo camiño
    • cuncha - o comando lánzase no shell /bin/sh, pode non especificarse camiño, pode utilizar globbing, tubos e outras funcións de shell. Adoita detectarse automaticamente se hai caracteres especiais (|, ;, &&, || etc).

cron

Controla cronjobs.

Parámetros:

  • nome do recurso - só algún tipo de identificador
  • garantir - Estado do traballo de coroa:
    • present - crear se non existe
    • absent - borrar se existe
  • mando - que comando executar
  • ambiente — en que ambiente executar o comando (lista de variables de ambiente e os seus valores mediante =)
  • usuario — desde que usuario executar o comando
  • minuto, momento, día da semana, mes, día do mes - cando executar cron. Se non se especifica algún destes atributos, o seu valor no crontab será *.

En Puppet 6.0 cron como se eliminado da caixa en puppetserver, polo que non hai documentación no sitio xeral. Pero el está na caixa en puppet-agent, polo que non é necesario instalalo por separado. Podes ver a documentación do mesmo na documentación da quinta versión de PuppetOu en GitHub.

Sobre os recursos en xeral

Requisitos para a singularidade dos recursos

O erro máis común que atopamos é Declaración duplicada. Este erro ocorre cando no directorio aparecen dous ou máis recursos do mesmo tipo co mesmo nome.

Por iso, escribirei de novo: os manifestos para o mesmo nodo non deben conter recursos do mesmo tipo co mesmo título!

Ás veces é necesario instalar paquetes co mesmo nome, pero con xestores de paquetes diferentes. Neste caso, cómpre usar o parámetro namepara evitar o erro:

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

Outros tipos de recursos teñen opcións similares para evitar a duplicación − name у servizo, command у exec, etcétera.

Metaparámetros

Cada tipo de recurso ten uns parámetros especiais, independentemente da súa natureza.

Lista completa de metaparámetros na documentación de Títeres.

Lista curta:

  • requirir — este parámetro indica de que recursos depende este recurso.
  • antes - Este parámetro especifica que recursos dependen deste recurso.
  • Apúntate — este parámetro especifica de que recursos este recurso recibe notificacións.
  • notify — Este parámetro especifica que recursos reciben notificacións deste recurso.

Todos os metaparámetros listados aceptan unha única ligazón de recurso ou unha matriz de ligazóns entre corchetes.

Ligazóns a recursos

Unha ligazón de recurso é simplemente unha mención do recurso. Utilízanse principalmente para indicar dependencias. Facer referencia a un recurso inexistente provocará un erro de compilación.

A sintaxe da ligazón é a seguinte: tipo de recurso cunha letra maiúscula (se o nome do tipo contén dous dous puntos, cada parte do nome entre os dous puntos está en maiúscula), entón o nome do recurso entre corchetes (o caso do nome non cambia!). Non debería haber espazos; os corchetes escríbense inmediatamente despois do nome do tipo.

Exemplo:

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

Dependencias e notificacións

Documentación aquí.

Como se dixo anteriormente, as dependencias simples entre recursos son transitivas. Por certo, teña coidado ao engadir dependencias: podes crear dependencias cíclicas, o que provocará un erro de compilación.

A diferenza das dependencias, as notificacións non son transitivas. Para as notificacións aplícanse as seguintes regras:

  • Se o recurso recibe unha notificación, actualízase. As accións de actualización dependen do tipo de recurso − exec executa o comando, servizo reinicia o servizo, paquete reinstala o paquete. Se o recurso non ten unha acción de actualización definida, non pasa nada.
  • Durante unha execución de Puppet, o recurso non se actualiza máis dunha vez. Isto é posible porque as notificacións inclúen dependencias e o gráfico de dependencias non contén ciclos.
  • Se Puppet cambia o estado dun recurso, o recurso envía notificacións a todos os recursos subscritos a el.
  • Se se actualiza un recurso, envía notificacións a todos os recursos subscritos a el.

Manexo de parámetros non especificados

Como regra xeral, se algún parámetro de recurso non ten un valor predeterminado e este parámetro non se especifica no manifesto, Puppet non cambiará esta propiedade para o recurso correspondente no nodo. Por exemplo, se un recurso de tipo arquivo parámetro non especificado owner, entón Puppet non cambiará o propietario do ficheiro correspondente.

Introdución ás clases, variables e definicións

Supoñamos que temos varios nodos que teñen a mesma parte da configuración, pero tamén hai diferenzas; se non, poderiamos describilo todo nun bloque node {}. Por suposto, pode simplemente copiar partes idénticas da configuración, pero en xeral esta é unha mala solución: a configuración crece e, se cambia a parte xeral da configuración, terá que editar o mesmo en moitos lugares. Ao mesmo tempo, é fácil cometer un erro e, en xeral, o principio DRY (non te repitas) inventouse por unha razón.

Para resolver este problema existe un deseño como clase.

Clases

Clase é un bloque nomeado de código poppet. Necesítanse clases para reutilizar o código.

Primeiro hai que describir a clase. A propia descrición non engade ningún recurso en ningún lado. A clase descríbese en manifestos:

# Описание класса начинается с ключевого слова class и его названия.
# Дальше идёт тело класса в фигурных скобках.
class example_class {
    ...
}

Despois disto, a clase pódese usar:

# первый вариант использования — в стиле ресурса с типом class
class { 'example_class': }
# второй вариант использования — с помощью функции include
include example_class
# про отличие этих двух вариантов будет рассказано дальше

Un exemplo da tarefa anterior: movemos a instalación e configuración de nginx a unha 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

A clase do exemplo anterior non é flexible en absoluto porque sempre trae a mesma configuración nginx. Fagamos o camiño á variable de configuración, entón esta clase pódese usar para instalar nginx con calquera configuración.

Pódese facer utilizando variables.

Atención: as variables en Puppet son inmutables!

Ademais, só se pode acceder a unha variable despois de ser declarada, se non, o valor da variable será undef.

Exemplo de traballo 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}"

Títere ten espazos de nomes, e as variables, en consecuencia, teñen zona de visibilidade: unha variable co mesmo nome pódese definir en diferentes espazos de nomes. Ao resolver o valor dunha variable, búscase a variable no espazo de nomes actual, despois no espazo de nomes que se inclúe, etc.

Exemplos de espazos de nomes:

  • global: as variables fóra da descrición da clase ou do nodo van alí;
  • espazo de nomes do nodo na descrición do nodo;
  • espazo de nomes da clase na descrición da clase.

Para evitar ambigüidades ao acceder a unha variable, pode especificar o espazo de nomes no nome da variable:

# переменная без пространства имён
$var
# переменная в глобальном пространстве имён
$::var
# переменная в пространстве имён класса
$classname::var
$::classname::var

Imos de acordo en que o camiño para a configuración de nginx reside na variable $nginx_conf_source. Entón a clase quedará 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
}

Non obstante, o exemplo dado é malo porque hai un "coñecemento secreto" de que nalgún lugar dentro da clase se usa unha variable con tal ou tal nome. É moito máis correcto xerar este coñecemento: as clases poden ter parámetros.

Parámetros de clase son variables no espazo de nomes da clase, especifícanse na cabeceira da clase e pódense usar como variables regulares no corpo da clase. Os valores dos parámetros especifícanse cando se usa a clase no manifesto.

O parámetro pódese configurar cun valor predeterminado. Se un parámetro non ten un valor predeterminado e o valor non se define cando se usa, provocará un erro de compilación.

Imos parametrizar a clase a partir do exemplo anterior e engadir dous parámetros: o primeiro, obrigatorio, é o camiño á configuración, e o segundo, opcional, é o nome do paquete con nginx (en Debian, por exemplo, hai 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, escríbense as variables. Comer moitos tipos de datos. Os tipos de datos úsanse normalmente para validar os valores de parámetros pasados ​​a clases e definicións. Se o parámetro pasado non coincide co tipo especificado, producirase un erro de compilación.

O tipo escríbese inmediatamente antes do nome do parámetro:

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

Clases: inclúe nome de clase vs clase{'nome de clase':}

Cada clase é un recurso de tipo clase. Como con calquera outro tipo de recurso, non pode haber dúas instancias da mesma clase no mesmo nodo.

Se tentas engadir unha clase ao mesmo nodo dúas veces usando class { 'classname':} (sen diferenzas, con parámetros diferentes ou idénticos), producirase un erro de compilación. Pero se usas unha clase no estilo do recurso, podes establecer de forma explícita todos os seus parámetros no manifesto.

Non obstante, se usas include, entón a clase pódese engadir tantas veces como se desexe. O feito é que include é unha función idempotente que verifica se se engadiu unha clase ao directorio. Se a clase non está no directorio, engádea e, se xa existe, non fai nada. Pero en caso de usar include Non pode establecer parámetros de clase durante a declaración de clase; todos os parámetros necesarios deben establecerse nunha fonte de datos externa: Hiera ou ENC. Falaremos deles no próximo artigo.

Define

Como se dixo no bloque anterior, a mesma clase non pode estar presente nun nodo máis dunha vez. Non obstante, nalgúns casos cómpre poder usar o mesmo bloque de código con diferentes parámetros no mesmo nodo. Noutras palabras, é necesario un tipo de recurso propio.

Por exemplo, para instalar o módulo PHP, facemos o seguinte en Avito:

  1. Instala o paquete con este módulo.
  2. Imos crear un ficheiro de configuración para este módulo.
  3. Creamos unha ligazón simbólica para a configuración de php-fpm.
  4. Creamos unha ligazón simbólica á configuración para php cli.

Nestes casos, un deseño como definir (definir, tipo definido, tipo de recurso definido). Un Define é semellante a unha clase, pero hai diferenzas: primeiro, cada Define é un tipo de recurso, non un recurso; en segundo lugar, cada definición ten un parámetro implícito $title, onde vai o nome do recurso cando se declara. Do mesmo xeito que no caso das clases, primeiro debe describirse unha definición, despois de que se poida utilizar.

Un exemplo simplificado cun 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' }
}

A forma máis sinxela de detectar o erro de declaración duplicada é en Definir. Isto ocorre se unha definición ten un recurso cun nome constante e hai dúas ou máis instancias desta definición nalgún nodo.

É doado protexerse disto: todos os recursos dentro da definición deben ter un nome dependendo de $title. Unha alternativa é a adición idempotente de recursos; no caso máis sinxelo, abonda con mover os recursos comúns a todas as instancias da definición a unha clase separada e incluír esta clase na función definición -. include idempotente.

Hai outras formas de conseguir a idempotencia ao engadir recursos, é dicir, empregando funcións defined и ensure_resources, pero xa vos contarei no seguinte episodio.

Dependencias e notificacións para clases e definicións

As clases e definicións engaden as seguintes regras para xestionar dependencias e notificacións:

  • a dependencia dunha clase/define engade dependencias de todos os recursos da clase/define;
  • unha dependencia de clase/definir engade dependencias a todos os recursos de clase/definir;
  • a notificación class/define notifica todos os recursos da clase/define;
  • a subscrición class/define subscríbese a todos os recursos da clase/define.

Declaracións condicionais e selectores

Documentación aquí.

if

Aquí é sinxelo:

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

a menos que

a menos que sexa un if ao revés: o bloque de código executarase se a expresión é falsa.

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

caso

Aquí tampouco hai nada complicado. Podes usar valores regulares (cadeas, números, etc.), expresións regulares e tipos de datos como valores.

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

Selectores

Un selector é unha construción de linguaxe semellante a case, pero en lugar de executar un bloque de código, devolve un valor.

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

Módulos

Cando a configuración é pequena, pódese gardar facilmente nun único manifesto. Pero cantas máis configuracións describimos, máis clases e nodos hai no manifesto, crece e faise incómodo traballar.

Ademais, existe o problema da reutilización do código: cando todo o código está nun mesmo manifesto, é difícil compartir este código con outros. Para resolver estes dous problemas, Puppet ten unha entidade chamada módulos.

Módulos - estes son conxuntos de clases, definicións e outras entidades Puppet colocadas nun directorio separado. Noutras palabras, un módulo é unha peza independente da lóxica de Puppet. Por exemplo, pode haber un módulo para traballar con nginx, e conterá o que e só o que é necesario para traballar con nginx, ou pode haber un módulo para traballar con PHP, etc.

Os módulos son versionados e tamén se admiten dependencias dos módulos entre si. Hai un repositorio aberto de módulos - Forxa de Títeres.

No servidor títeres, os módulos están situados no subdirectorio módulos do directorio raíz. Dentro de cada módulo hai un esquema de directorio estándar: manifestos, ficheiros, modelos, lib, etc.

Estrutura de ficheiros nun módulo

A raíz do módulo pode conter os seguintes directorios con nomes descritivos:

  • manifests - contén manifestos
  • files - contén ficheiros
  • templates - contén modelos
  • lib — contén código Ruby

Esta non é unha lista completa de directorios e ficheiros, pero é suficiente para este artigo polo momento.

Nomes dos recursos e nomes dos ficheiros do módulo

Documentación aquí.

Os recursos (clases, definicións) nun módulo non se poden nomear como queiras. Ademais, existe unha correspondencia directa entre o nome dun recurso e o nome do ficheiro no que Puppet buscará unha descrición dese recurso. Se infrinxe as regras de nomeamento, Puppet simplemente non atopará a descrición do recurso e obterá un erro de compilación.

As regras son sinxelas:

  • Todos os recursos dun módulo deben estar no espazo de nomes do módulo. Se o módulo é chamado foo, entón todos os recursos nel deberían ser nomeados foo::<anything>, ou simplemente foo.
  • O recurso co nome do módulo debe estar no ficheiro init.pp.
  • Para outros recursos, o esquema de nomes de ficheiros é o seguinte:
    • descartarase o prefixo co nome do módulo
    • todos os dous dous puntos, se os hai, substitúense por barras inclinadas
    • engádese extensión .pp

Vou demostrar cun exemplo. Digamos que estou escribindo un módulo nginx. Contén os seguintes recursos:

  • clase nginx descrito no manifesto init.pp;
  • clase nginx::service descrito no manifesto service.pp;
  • definir nginx::server descrito no manifesto server.pp;
  • definir nginx::server::location descrito no manifesto server/location.pp.

templates

Seguro que vostede mesmo sabe o que son os modelos; aquí non os describirei en detalle. Pero vouno deixar por se acaso ligazón á Wikipedia.

Como usar modelos: O significado dun modelo pódese ampliar mediante unha función template, que se pasa o camiño ao modelo. Para recursos de tipo arquivo usado xunto co parámetro content. Por exemplo, así:

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

Ver camiño <modulename>/<filename> implica arquivo <rootdir>/modules/<modulename>/templates/<filename>.

Ademais, hai unha función inline_template — recibe o texto do modelo como entrada, non o nome do ficheiro.

Dentro dos modelos, pode usar todas as variables de Puppet no ámbito actual.

Puppet admite modelos en formato ERB e EPP:

Brevemente sobre ERB

Estruturas de control:

  • <%= ВЫРАЖЕНИЕ %> — inserir o valor da expresión
  • <% ВЫРАЖЕНИЕ %> — calcular o valor dunha expresión (sen inserila). As instrucións condicionais (if) e os bucles (cada un) adoitan ir aquí.
  • <%# КОММЕНТАРИЙ %>

As expresións en ERB están escritas en Ruby (ERB é en realidade Embedded Ruby).

Para acceder ás variables do manifesto, cómpre engadir @ ao nome da variable. Para eliminar un salto de liña que aparece despois dunha construción de control, cómpre utilizar unha etiqueta de peche -%>.

Exemplo de uso do modelo

Digamos que estou escribindo un módulo para controlar ZooKeeper. A clase responsable de crear a configuración ten un aspecto 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'),
  }
}

E o modelo correspondente zoo.cfg.erb -Entón:

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

Feitos e variables incorporadas

Moitas veces, a parte específica da configuración depende do que está a suceder actualmente no nodo. Por exemplo, dependendo da versión de Debian, cómpre instalar unha ou outra versión do paquete. Pode supervisar todo isto manualmente, reescribindo os manifestos se os nodos cambian. Pero este non é un enfoque serio; a automatización é moito mellor.

Para obter información sobre os nodos, Puppet ten un mecanismo chamado feitos. feitos - Esta é información sobre o nodo, dispoñible en manifestos en forma de variables ordinarias no espazo de nomes global. Por exemplo, o nome do servidor, a versión do sistema operativo, a arquitectura do procesador, a lista de usuarios, a lista de interfaces de rede e os seus enderezos, e moito máis. Os feitos están dispoñibles en manifestos e modelos como variables habituais.

Un exemplo de traballo con feitos:

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

Formalmente falando, un feito ten un nome (cadea) e un valor (dispoñen de varios tipos: cadeas, matrices, dicionarios). Comer conxunto de feitos incorporados. Tamén podes escribir o teu. Descríbense os colectores de feitos como funcións en Rubyou como ficheiros executables. Os feitos tamén se poden presentar no formulario ficheiros de texto con datos nos nodos.

Durante a operación, o axente monicreque copia primeiro todos os colectores de feitos dispoñibles do servidor pappet ao nodo, despois de que os lanza e envía os feitos recollidos ao servidor; Despois diso, o servidor comeza a compilar o catálogo.

Feitos en forma de ficheiros executables

Tales feitos colócanse en módulos do directorio facts.d. Por suposto, os ficheiros deben ser executables. Cando se executan, deben enviar información á saída estándar en formato YAML ou clave=valor.

Non esquezas que os feitos aplícanse a todos os nós que están controlados polo servidor poppet no que está implantado o teu módulo. Polo tanto, no script, teña coidado de comprobar que o sistema ten todos os programas e ficheiros necesarios para que o seu feito funcione.

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

Feitos Ruby

Tales feitos colócanse en módulos do 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

Feitos do texto

Tales feitos colócanse nos nodos do directorio /etc/facter/facts.d en Boneco vello ou /etc/puppetlabs/facts.d no novo Boneco.

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

Chegando aos feitos

Hai dúas formas de abordar os feitos:

  • a través do dicionario $facts: $facts['fqdn'];
  • usando o nome do feito como nome da variable: $fqdn.

É mellor usar un dicionario $facts, ou mellor aínda, indica o espazo de nomes global ($::facts).

Aquí está a sección relevante da documentación.

Variables incorporadas

Ademais dos feitos, tamén os hai algunhas variables, dispoñible no espazo de nomes global.

  • feitos fiables — variables que se toman do certificado do cliente (xa que o certificado adoita emitirse nun servidor poppet, o axente non pode simplemente tomar e cambiar o seu certificado, polo que as variables son “de confianza”): o nome do certificado, o nome do host e dominio, extensións do certificado.
  • datos do servidor —variables relacionadas coa información sobre o servidor: versión, nome, enderezo IP do servidor, ambiente.
  • feitos dos axentes — variables engadidas directamente polo axente títere, e non polo factor — nome do certificado, versión do axente, versión do monicreque.
  • variables mestras - Variables Pappetmaster (sic!). É case o mesmo que en datos do servidor, máis os valores dos parámetros de configuración están dispoñibles.
  • variables do compilador — variables do compilador que difiren en cada ámbito: o nome do módulo actual e o nome do módulo no que se accedeu ao obxecto actual. Pódense utilizar, por exemplo, para comprobar que as túas clases particulares non se están utilizando directamente desde outros módulos.

Adición 1: como executar e depurar todo isto?

O artigo contiña moitos exemplos de código de monicreques, pero non nos indicaba en absoluto como executar este código. Pois estoume corrixindo.

Un axente é suficiente para executar Puppet, pero na maioría dos casos tamén necesitarás un servidor.

Axente

Polo menos desde a versión XNUMX, os paquetes puppet-agent de repositorio oficial de Puppetlabs conteñen todas as dependencias (ruby e as xemas correspondentes), polo que non hai dificultades de instalación (falo de distribucións baseadas en Debian; non usamos distribucións baseadas en RPM).

No caso máis sinxelo, para usar a configuración do monicreque, abonda con lanzar o axente en modo sen servidor: sempre que se copie o código monicreque no 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

É mellor, por suposto, configurar o servidor e executar axentes nos nodos en modo daemon; despois, unha vez cada media hora, aplicarán a configuración descargada do servidor.

Podes imitar o modelo de traballo push: vai ao nodo que che interesa e comeza sudo puppet agent -t. Chave -t (--test) realmente inclúe varias opcións que se poden activar individualmente. Estas opcións inclúen as seguintes:

  • non se execute en modo daemon (por defecto o axente comeza en modo daemon);
  • apagar despois de aplicar o catálogo (por defecto, o axente seguirá traballando e aplicando a configuración unha vez cada media hora);
  • escribir un rexistro de traballo detallado;
  • mostrar cambios nos ficheiros.

O axente ten un modo de funcionamento sen cambios; pode usalo cando non estea seguro de que escribiu a configuración correcta e quere comprobar o que cambiará exactamente o axente durante a operación. Este modo está habilitado polo parámetro --noop na liña de comandos: sudo puppet agent -t --noop.

Ademais, pode activar o rexistro de depuración do traballo: nel, puppet escribe sobre todas as accións que realiza: sobre o recurso que está a procesar actualmente, sobre os parámetros deste recurso, sobre os programas que lanza. Por suposto, este é un parámetro --debug.

Servidor

Non vou considerar a configuración completa do servidor pappet e a súa implementación de código neste artigo; só direi que fóra da caixa hai unha versión totalmente funcional do servidor que non require configuración adicional para funcionar cun pequeno número de nós (por exemplo, ata cen). Un maior número de nodos requirirá axustes: de forma predeterminada, puppetserver non lanza máis de catro traballadores, para un maior rendemento, cómpre aumentar o seu número e non se esqueza de aumentar os límites de memoria, se non, o servidor recollerá o lixo a maior parte do tempo.

Implementación de código: se o necesitas de forma rápida e sinxela, mira (en r10k)[https://github.com/puppetlabs/r10k], para instalacións pequenas debería ser suficiente.

Anexo 2: Directrices de codificación

  1. Coloca toda a lóxica en clases e definicións.
  2. Manteña as clases e definicións en módulos, non en manifestos que describen nós.
  3. Use os feitos.
  4. Non fagas ifs baseados en nomes de host.
  5. Non dubides en engadir parámetros para as clases e definicións; isto é mellor que a lóxica implícita oculta no corpo da clase/definición.

Explicarei por que recomendo facelo no seguinte artigo.

Conclusión

Rematamos coa introdución. No seguinte artigo falareivos de Hiera, ENC e PuppetDB.

Só os usuarios rexistrados poden participar na enquisa. Rexístrate, por favor.

De feito, hai moito máis material: podo escribir artigos sobre os seguintes temas, votar sobre o que che interesaría ler:

  • 59,1%Construcións avanzadas de títeres: algunhas merdas de nivel seguinte: bucles, mapeo e outras expresións lambda, colectores de recursos, recursos exportados e comunicación entre hosts a través de Puppet, etiquetas, provedores, tipos de datos abstractos.13
  • 31,8%“Son a administradora da miña nai” ou como nós en Avito fixemos amizade con varios servidores poppet de diferentes versións e, en principio, a parte da administración do servidor poppet.7
  • 81,8%Como escribimos o código de monicreques: instrumentación, documentación, probas, CI/CD.18

Votaron 22 usuarios. 9 usuarios abstivéronse.

Fonte: www.habr.com