Introduction à la marionnette

Puppet est un système de gestion de configuration. Il est utilisé pour amener les hôtes à l’état souhaité et maintenir cet état.

Je travaille avec Puppet depuis plus de cinq ans maintenant. Ce texte est essentiellement une compilation traduite et réorganisée de points clés de la documentation officielle, qui permettra aux débutants de comprendre rapidement l'essence de Puppet.

Introduction à la marionnette

Informations de base

Le système d'exploitation de Puppet est client-serveur, bien qu'il prenne également en charge un fonctionnement sans serveur avec des fonctionnalités limitées.

Un modèle de fonctionnement pull est utilisé : par défaut, une fois toutes les demi-heures, les clients contactent le serveur pour une configuration et l'appliquent. Si vous avez travaillé avec Ansible, alors ils utilisent un modèle push différent : l'administrateur lance le processus d'application de la configuration, les clients eux-mêmes n'appliqueront rien.

Lors de la communication réseau, un cryptage TLS bidirectionnel est utilisé : le serveur et le client disposent de leurs propres clés privées et des certificats correspondants. Généralement, le serveur délivre des certificats pour les clients, mais en principe il est possible d'utiliser une autorité de certification externe.

Introduction aux manifestes

Dans la terminologie des marionnettes au serveur de marionnettes connecter nœuds (nœuds). La configuration des nœuds est écrite dans les manifestes dans un langage de programmation spécial - Puppet DSL.

Puppet DSL est un langage déclaratif. Il décrit l'état souhaité du nœud sous forme de déclarations de ressources individuelles, par exemple :

  • Le fichier existe et il a un contenu spécifique.
  • Le paquet est installé.
  • Le service a commencé.

Les ressources peuvent être interconnectées :

  • Il existe des dépendances, elles affectent l'ordre dans lequel les ressources sont utilisées.
    Par exemple, « installez d’abord le package, puis modifiez le fichier de configuration, puis démarrez le service ».
  • Il existe des notifications - si une ressource a changé, elle envoie des notifications aux ressources qui y sont abonnés.
    Par exemple, si le fichier de configuration change, vous pouvez redémarrer automatiquement le service.

De plus, le Puppet DSL possède des fonctions et des variables, ainsi que des instructions conditionnelles et des sélecteurs. Divers mécanismes de création de modèles sont également pris en charge : EPP et ERB.

Puppet est écrit en Ruby, donc de nombreuses constructions et termes en sont tirés. Ruby vous permet d'étendre Puppet - d'ajouter une logique complexe, de nouveaux types de ressources et des fonctions.

Pendant l'exécution de Puppet, les manifestes de chaque nœud spécifique du serveur sont compilés dans un répertoire. annuaire est une liste de ressources et de leurs relations après calcul de la valeur des fonctions, des variables et développement des instructions conditionnelles.

Syntaxe et style de code

Voici des sections de la documentation officielle qui vous aideront à comprendre la syntaxe si les exemples fournis ne suffisent pas :

Voici un exemple de ce à quoi ressemble le manifeste :

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

L'indentation et les sauts de ligne ne sont pas une partie obligatoire du manifeste, mais il existe une recommandation guide de style. Résumé:

  • Retrait à deux espaces, les tabulations ne sont pas utilisées.
  • Les accolades sont séparées par un espace ; les deux-points ne sont pas séparés par un espace.
  • Des virgules après chaque paramètre, y compris le dernier. Chaque paramètre est sur une ligne distincte. Une exception est faite pour le cas sans paramètres et avec un paramètre : vous pouvez écrire sur une seule ligne et sans virgule (c'est-à-dire resource { 'title': } и resource { 'title': param => value }).
  • Les flèches sur les paramètres doivent être au même niveau.
  • Des flèches de relation entre les ressources sont écrites devant eux.

Emplacement des fichiers sur Pappetserver

Pour plus d'explications, j'introduirai le concept de « répertoire racine ». Le répertoire racine est le répertoire qui contient la configuration Puppet pour un nœud spécifique.

Le répertoire racine varie en fonction de la version de Puppet et des environnements utilisés. Les environnements sont des ensembles de configuration indépendants stockés dans des répertoires distincts. Généralement utilisé en combinaison avec git, auquel cas les environnements sont créés à partir de branches git. En conséquence, chaque nœud est situé dans un environnement ou un autre. Cela peut être configuré sur le nœud lui-même, ou en ENC, dont je parlerai dans le prochain article.

  • Dans la troisième version ("ancienne Puppet") le répertoire de base était /etc/puppet. L'utilisation des environnements est facultative - par exemple, nous ne les utilisons pas avec l'ancien Puppet. Si des environnements sont utilisés, ils sont généralement stockés dans /etc/puppet/environments, le répertoire racine sera le répertoire d'environnement. Si les environnements ne sont pas utilisés, le répertoire racine sera le répertoire de base.
  • A partir de la quatrième version (« new Puppet »), l'utilisation des environnements est devenue obligatoire, et le répertoire de base a été déplacé vers /etc/puppetlabs/code. En conséquence, les environnements sont stockés dans /etc/puppetlabs/code/environments, le répertoire racine est le répertoire d'environnement.

Il doit y avoir un sous-répertoire dans le répertoire racine manifests, qui contient un ou plusieurs manifestes décrivant les nœuds. De plus, il devrait y avoir un sous-répertoire modules, qui contient les modules. Je vous dirai quels sont les modules un peu plus tard. De plus, l'ancien Puppet peut également avoir un sous-répertoire files, qui contient divers fichiers que nous copions sur les nœuds. Dans le nouveau Puppet, tous les fichiers sont placés dans des modules.

Les fichiers manifestes ont l'extension .pp.

Quelques exemples de combats

Description du nœud et de la ressource qu'il contient

Sur le nœud server1.testdomain un fichier doit être créé /etc/issue avec du contenu Debian GNU/Linux n l. Le fichier doit appartenir à un utilisateur et à un groupe root, les droits d'accès doivent être 644.

Nous écrivons un manifeste :

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

Relations entre les ressources sur un nœud

Sur le nœud server2.testdomain nginx doit être en cours d'exécution et fonctionner avec une configuration préalablement préparée.

Décomposons le problème :

  • Le paquet doit être installé nginx.
  • Il est nécessaire que les fichiers de configuration soient copiés depuis le serveur.
  • Le service doit être exécuté nginx.
  • Si la configuration est mise à jour, le service doit être redémarré.

Nous écrivons un manifeste :

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

Pour que cela fonctionne, vous avez besoin d'environ l'emplacement de fichier suivant sur le serveur Puppet :

/etc/puppetlabs/code/environments/production/ # (это для нового Паппета, для старого корневой директорией будет /etc/puppet)
├── manifests/
│   └── site.pp
└── modules/
    └── example/
        └── files/
            └── nginx-conf/
                ├── nginx.conf
                ├── mime.types
                └── conf.d/
                    └── some.conf

Types de ressources

Une liste complète des types de ressources pris en charge peut être trouvée ici dans les documents, je vais décrire ici cinq types de base qui, dans ma pratique, suffisent à résoudre la plupart des problèmes.

filet

Gère les fichiers, les répertoires, les liens symboliques, leur contenu et les droits d'accès.

options:

  • nom de la ressource — chemin d'accès au fichier (facultatif)
  • chemin — chemin d'accès au fichier (s'il n'est pas spécifié dans le nom)
  • assurer - Type de fichier:
    • absent - supprimer un fichier
    • present — il doit y avoir un fichier de n'importe quel type (s'il n'y a pas de fichier, un fichier normal sera créé)
    • file - fichier régulier
    • directory - répertoire
    • link - lien symbolique
  • contenu — contenu du fichier (convient uniquement aux fichiers normaux, ne peut pas être utilisé avec la source ou l'objectif)
  • la source — un lien vers le chemin à partir duquel vous souhaitez copier le contenu du fichier (ne peut pas être utilisé avec contenu ou l'objectif). Peut être spécifié comme URI avec un schéma puppet: (alors les fichiers du serveur Puppet seront utilisés), et avec le schéma http: (J'espère que ce qui va se passer dans ce cas est clair), et même avec le schéma file: ou comme chemin absolu sans schéma (alors le fichier du FS local sur le nœud sera utilisé)
  • l'objectif — où le lien symbolique doit pointer (ne peut pas être utilisé avec contenu ou la source)
  • propriétaire — l'utilisateur qui doit posséder le fichier
  • groupe — le groupe auquel le fichier doit appartenir
  • mode — autorisations de fichier (sous forme de chaîne)
  • recurse - permet le traitement récursif des répertoires
  • purge - permet de supprimer des fichiers qui ne sont pas décrits dans Puppet
  • forcer - permet de supprimer des répertoires qui ne sont pas décrits dans Puppet

paquet

Installe et supprime des packages. Capable de gérer les notifications - réinstalle le package si le paramètre est spécifié réinstaller_on_refresh.

options:

  • nom de la ressource — nom du package (facultatif)
  • prénom — nom du package (s'il n'est pas spécifié dans le nom)
  • de voiture. — gestionnaire de paquets à utiliser
  • assurer — état souhaité du colis :
    • present, installed - n'importe quelle version installée
    • latest - dernière version installée
    • absent - supprimé (apt-get remove)
    • purged — supprimé avec les fichiers de configuration (apt-get purge)
    • held - la version du package est verrouillée (apt-mark hold)
    • любая другая строка — la version spécifiée est installée
  • réinstaller_on_refresh - si un true, puis dès réception de la notification, le package sera réinstallé. Utile pour les distributions basées sur les sources, où la reconstruction des packages peut être nécessaire lors de la modification des paramètres de construction. Défaut false.

service

Gère les services. Capable de traiter les notifications - redémarre le service.

options:

  • nom de la ressource — service à gérer (facultatif)
  • prénom — le service qui doit être géré (s'il n'est pas spécifié dans le nom)
  • assurer — état souhaité du service:
    • running - lancé
    • stopped - arrêté
  • permettre — contrôle la possibilité de démarrer le service :
    • true — l'exécution automatique est activée (systemctl enable)
    • mask - déguisé (systemctl mask)
    • false — l'exécution automatique est désactivée (systemctl disable)
  • recommencer - commande pour redémarrer le service
  • statuts — commande pour vérifier l'état du service
  • a redémarré — indique si le script d'initialisation du service prend en charge le redémarrage. Si false et le paramètre est spécifié recommencer — la valeur de ce paramètre est utilisée. Si false et paramètre recommencer non spécifié - le service est arrêté et commence à redémarrer (mais systemd utilise la commande systemctl restart).
  • a un statut — indique si le script d'initialisation du service prend en charge la commande status. Si false, alors la valeur du paramètre est utilisée statuts. Défaut true.

exec

Exécute des commandes externes. Si vous ne spécifiez pas de paramètres crée des, seulement si, à moins que ou actualiser uniquement, la commande sera exécutée à chaque fois que Puppet sera exécuté. Capable de traiter les notifications - exécute une commande.

options:

  • nom de la ressource — commande à exécuter (facultatif)
  • commander — la commande à exécuter (si elle n'est pas précisée dans le nom)
  • chemin — chemins dans lesquels rechercher le fichier exécutable
  • seulement si — si la commande spécifiée dans ce paramètre s'est terminée par un code retour zéro, la commande principale sera exécutée
  • à moins que — si la commande spécifiée dans ce paramètre s'est terminée par un code retour non nul, la commande principale sera exécutée
  • crée des — si le fichier spécifié dans ce paramètre n'existe pas, la commande principale sera exécutée
  • actualiser uniquement - si un true, alors la commande ne sera exécutée que lorsque cet exécutable recevra une notification d'autres ressources
  • CWD — répertoire à partir duquel exécuter la commande
  • utilisateur — l'utilisateur à partir duquel exécuter la commande
  • de voiture. - comment exécuter la commande :
    • posix — un processus enfant est simplement créé, veillez à préciser chemin
    • coquille - la commande est lancée dans le shell /bin/sh, ne peut pas être spécifié chemin, vous pouvez utiliser le globbing, les tuyaux et d'autres fonctionnalités du shell. Généralement détecté automatiquement s'il y a des caractères spéciaux (|, ;, &&, || etc).

cron

Contrôle les tâches cron.

options:

  • nom de la ressource - juste une sorte d'identifiant
  • assurer — état de travail de la couronne :
    • present - créer si n'existe pas
    • absent - supprimer si existe
  • commander - quelle commande exécuter
  • sûr, heureux et sain — dans quel environnement exécuter la commande (liste des variables d'environnement et leurs valeurs via =)
  • utilisateur - à partir de quel utilisateur exécuter la commande
  • minute, heure, en semaine, mois, jour du mois - quand exécuter cron. Si l'un de ces attributs n'est pas spécifié, sa valeur dans la crontab sera *.

Dans Puppet 6.0 cron comme retiré de la boîte dans puppetserver, il n'y a donc pas de documentation sur le site général. Mais il est dans la boîte dans puppet-agent, il n'est donc pas nécessaire de l'installer séparément. Vous pouvez voir la documentation à ce sujet dans la documentation de la cinquième version de PuppetOu sur GitHub.

À propos des ressources en général

Exigences relatives à l'unicité des ressources

L'erreur la plus courante que nous rencontrons est Déclaration en double. Cette erreur se produit lorsque deux ou plusieurs ressources du même type portant le même nom apparaissent dans le répertoire.

Par conséquent, j'écrirai à nouveau : les manifestes pour le même nœud ne doivent pas contenir de ressources du même type avec le même titre !

Parfois, il est nécessaire d'installer des packages portant le même nom, mais avec des gestionnaires de packages différents. Dans ce cas, vous devez utiliser le paramètre namepour éviter l'erreur :

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

D'autres types de ressources ont des options similaires pour éviter la duplication - name у service, command у exec, et ainsi de suite.

Métaparamètres

Chaque type de ressource possède des paramètres spéciaux, quelle que soit sa nature.

Liste complète des métaparamètres dans la documentation de Puppet.

Liste restreinte :

  • exigent — ce paramètre indique de quelles ressources dépend cette ressource.
  • before - Ce paramètre précise quelles ressources dépendent de cette ressource.
  • inscrire — ce paramètre spécifie de quelles ressources cette ressource reçoit des notifications.
  • notifier — Ce paramètre spécifie quelles ressources reçoivent des notifications de cette ressource.

Tous les métaparamètres répertoriés acceptent soit un seul lien de ressource, soit un tableau de liens entre crochets.

Liens vers des ressources

Un lien de ressource est simplement une mention de la ressource. Ils sont principalement utilisés pour indiquer des dépendances. Référencer une ressource inexistante provoquera une erreur de compilation.

La syntaxe du lien est la suivante : type de ressource avec une majuscule (si le nom du type contient des doubles deux-points, alors chaque partie du nom entre les deux-points est en majuscule), puis le nom de la ressource entre crochets (la casse du nom ne change pas!). Il ne doit y avoir aucun espace ; les crochets sont écrits immédiatement après le nom du type.

Exemple:

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

Dépendances et notifications

Documentation ici.

Comme indiqué précédemment, les dépendances simples entre ressources sont transitives. À propos, soyez prudent lorsque vous ajoutez des dépendances : vous pouvez créer des dépendances cycliques, ce qui provoquera une erreur de compilation.

Contrairement aux dépendances, les notifications ne sont pas transitives. Les règles suivantes s'appliquent aux notifications :

  • Si la ressource reçoit une notification, elle est mise à jour. Les actions de mise à jour dépendent du type de ressource - exec exécute la commande, service redémarre le service, paquet réinstalle le package. Si aucune action de mise à jour n'est définie pour la ressource, rien ne se passe.
  • Au cours d'une exécution de Puppet, la ressource n'est mise à jour qu'une seule fois. Cela est possible car les notifications incluent des dépendances et le graphique des dépendances ne contient pas de cycles.
  • Si Puppet modifie l'état d'une ressource, la ressource envoie des notifications à toutes les ressources qui y sont abonnés.
  • Si une ressource est mise à jour, elle envoie des notifications à toutes les ressources qui y sont abonnés.

Gestion des paramètres non spécifiés

En règle générale, si un paramètre de ressource n'a pas de valeur par défaut et que ce paramètre n'est pas spécifié dans le manifeste, Puppet ne modifiera pas cette propriété pour la ressource correspondante sur le nœud. Par exemple, si une ressource de type filet paramètre non spécifié owner, alors Puppet ne changera pas le propriétaire du fichier correspondant.

Introduction aux classes, variables et définitions

Supposons que nous ayons plusieurs nœuds qui ont la même partie de la configuration, mais qu'il existe également des différences - sinon nous pourrions tout décrire en un seul bloc. node {}. Bien sûr, vous pouvez simplement copier des parties identiques de la configuration, mais en général, c'est une mauvaise solution - la configuration s'agrandit, et si vous modifiez la partie générale de la configuration, vous devrez modifier la même chose à plusieurs endroits. En même temps, il est facile de se tromper, et en général, le principe DRY (ne vous répétez pas) a été inventé pour une raison.

Pour résoudre ce problème, il existe une conception telle que classe.

Классы

classe est un bloc nommé de code poppet. Des classes sont nécessaires pour réutiliser le code.

Il faut d’abord décrire la classe. La description elle-même n’ajoute aucune ressource nulle part. La classe est décrite dans les manifestes :

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

Après cela, la classe peut être utilisée :

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

Un exemple de la tâche précédente : déplaçons l'installation et la configuration de nginx dans une classe :

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 classe de l'exemple précédent n'est pas du tout flexible car elle apporte toujours la même configuration nginx. Créons le chemin vers la variable de configuration, cette classe peut alors être utilisée pour installer nginx avec n'importe quelle configuration.

Ça peut être fait utiliser des variables.

Attention : les variables dans Puppet sont immuables !

De plus, une variable n'est accessible qu'après avoir été déclarée, sinon la valeur de la variable sera undef.

Exemple de travail avec des 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 marionnette a espaces de noms, et les variables, en conséquence, ont zone de visibilité: Une variable portant le même nom peut être définie dans différents espaces de noms. Lors de la résolution de la valeur d'une variable, la variable est recherchée dans l'espace de noms actuel, puis dans l'espace de noms englobant, et ainsi de suite.

Exemples d'espaces de noms :

  • global - les variables en dehors de la description de la classe ou du nœud y vont ;
  • espace de noms du nœud dans la description du nœud ;
  • espace de noms de classe dans la description de la classe.

Pour éviter toute ambiguïté lors de l'accès à une variable, vous pouvez spécifier l'espace de noms dans le nom de la variable :

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

Admettons que le chemin d'accès à la configuration nginx réside dans la variable $nginx_conf_source. La classe ressemblera alors à ceci :

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
}

Cependant, l'exemple donné est mauvais car il existe une « connaissance secrète » selon laquelle quelque part à l'intérieur de la classe une variable portant tel ou tel nom est utilisée. Il est beaucoup plus correct de généraliser ces connaissances - les classes peuvent avoir des paramètres.

Paramètres de classe sont des variables dans l'espace de noms de la classe, elles sont spécifiées dans l'en-tête de la classe et peuvent être utilisées comme des variables normales dans le corps de la classe. Les valeurs des paramètres sont spécifiées lors de l'utilisation de la classe dans le manifeste.

Le paramètre peut être défini sur une valeur par défaut. Si un paramètre n'a pas de valeur par défaut et que la valeur n'est pas définie lors de son utilisation, cela provoquera une erreur de compilation.

Paramétrons la classe de l'exemple ci-dessus et ajoutons deux paramètres : le premier, obligatoire, est le chemin d'accès à la configuration, et le second, facultatif, est le nom du paquet avec nginx (dans Debian par exemple, il y a des paquets 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',   # задаём параметры класса точно так же, как параметры для других ресурсов
  }
}

Dans Puppet, les variables sont saisies. Manger de nombreux types de données. Les types de données sont généralement utilisés pour valider les valeurs de paramètres transmises aux classes et aux définitions. Si le paramètre transmis ne correspond pas au type spécifié, une erreur de compilation se produira.

Le type est écrit juste avant le nom du paramètre :

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

Classes : inclure le nom de la classe par rapport à la classe{'classname' :}

Chaque classe est une ressource de type classe. Comme pour tout autre type de ressource, il ne peut pas y avoir deux instances de la même classe sur le même nœud.

Si vous essayez d'ajouter une classe au même nœud deux fois en utilisant class { 'classname':} (pas de différence, avec des paramètres différents ou identiques), il y aura une erreur de compilation. Mais si vous utilisez une classe dans le style de ressource, vous pouvez immédiatement définir explicitement tous ses paramètres dans le manifeste.

Cependant, si vous utilisez include, la classe peut alors être ajoutée autant de fois que vous le souhaitez. Le fait est que include est une fonction idempotente qui vérifie si une classe a été ajoutée au répertoire. Si la classe n'est pas dans le répertoire, elle l'ajoute, et si elle existe déjà, elle ne fait rien. Mais en cas d'utilisation include vous ne pouvez pas définir les paramètres de classe lors de la déclaration de classe - tous les paramètres requis doivent être définis dans une source de données externe - Hiera ou ENC. Nous en parlerons dans le prochain article.

Définit

Comme cela a été dit dans le bloc précédent, la même classe ne peut pas être présente plus d’une fois sur un nœud. Cependant, dans certains cas, vous devez pouvoir utiliser le même bloc de code avec des paramètres différents sur le même nœud. En d’autres termes, il est nécessaire de disposer d’un type de ressource propre.

Par exemple, afin d'installer le module PHP, nous procédons comme suit dans Avito :

  1. Installez le package avec ce module.
  2. Créons un fichier de configuration pour ce module.
  3. Nous créons un lien symbolique vers la configuration pour php-fpm.
  4. Nous créons un lien symbolique vers la configuration pour php cli.

Dans de tels cas, une conception telle que définir (définir, type défini, type de ressource défini). Un Define est similaire à une classe, mais il existe des différences : premièrement, chaque Define est un type de ressource, pas une ressource ; deuxièmement, chaque définition a un paramètre implicite $title, où va le nom de la ressource lorsqu'elle est déclarée. Tout comme dans le cas des classes, une définition doit d’abord être décrite, après quoi elle peut être utilisée.

Un exemple simplifié avec un module pour 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' }
}

Le moyen le plus simple de détecter l’erreur de déclaration en double est de définir. Cela se produit si une définition a une ressource avec un nom constant et qu'il existe deux instances ou plus de cette définition sur un nœud.

Il est facile de s'en protéger : toutes les ressources contenues dans la définition doivent avoir un nom en fonction de $title. Une alternative est l'ajout idempotent de ressources ; dans le cas le plus simple, il suffit de déplacer les ressources communes à toutes les instances de définition dans une classe distincte et d'inclure cette classe dans la définition - fonction include idempotent.

Il existe d'autres moyens d'atteindre l'idempotence lors de l'ajout de ressources, notamment en utilisant des fonctions defined и ensure_resources, mais je vous en parlerai dans le prochain épisode.

Dépendances et notifications pour les classes et les définitions

Les classes et définitions ajoutent les règles suivantes à la gestion des dépendances et des notifications :

  • la dépendance sur une classe/définir ajoute des dépendances sur toutes les ressources de la classe/définir ;
  • une dépendance class/define ajoute des dépendances à toutes les ressources class/define ;
  • La notification class/define informe toutes les ressources de la classe/define ;
  • L'abonnement class/define s'abonne à toutes les ressources de la classe/define.

Instructions conditionnelles et sélecteurs

Documentation ici.

if

Ici, tout est simple:

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

à moins que

except est un if inversé : le bloc de code sera exécuté si l'expression est fausse.

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

maisons

Il n'y a rien de compliqué ici non plus. Vous pouvez utiliser des valeurs régulières (chaînes, nombres, etc.), des expressions régulières et des types de données comme valeurs.

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

Sélecteurs

Un sélecteur est une construction de langage similaire à case, mais au lieu d'exécuter un bloc de code, il renvoie une valeur.

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

Modules

Lorsque la configuration est petite, elle peut facilement être conservée dans un seul manifeste. Mais plus nous décrivons de configurations, plus il y a de classes et de nœuds dans le manifeste, il grandit et il devient peu pratique de travailler avec.

À cela s'ajoute le problème de la réutilisation du code : lorsque tout le code se trouve dans un seul manifeste, il est difficile de partager ce code avec d'autres. Pour résoudre ces deux problèmes, Puppet dispose d'une entité appelée modules.

Modules - ce sont des ensembles de classes, définitions et autres entités Puppet placées dans un répertoire séparé. En d’autres termes, un module est un élément indépendant de la logique Puppet. Par exemple, il peut y avoir un module pour travailler avec nginx, et il contiendra ce qui est nécessaire et seulement ce qui est nécessaire pour travailler avec nginx, ou il peut y avoir un module pour travailler avec PHP, et ainsi de suite.

Les modules sont versionnés et les dépendances des modules les uns sur les autres sont également prises en charge. Il existe un référentiel ouvert de modules - Forge de marionnettes.

Sur le serveur Puppet, les modules se trouvent dans le sous-répertoire modules du répertoire racine. À l'intérieur de chaque module, il existe un schéma de répertoires standard : manifestes, fichiers, modèles, lib, etc.

Structure de fichier dans un module

La racine du module peut contenir les répertoires suivants avec des noms descriptifs :

  • manifests - il contient des manifestes
  • files - il contient des fichiers
  • templates - il contient des modèles
  • lib — il contient du code Ruby

Il ne s'agit pas d'une liste complète des répertoires et des fichiers, mais elle suffit pour l'instant pour cet article.

Noms des ressources et noms des fichiers dans le module

Documentation ici.

Les ressources (classes, définitions) d'un module ne peuvent pas être nommées comme bon vous semble. De plus, il existe une correspondance directe entre le nom d'une ressource et le nom du fichier dans lequel Puppet cherchera une description de cette ressource. Si vous enfreignez les règles de dénomination, Puppet ne trouvera tout simplement pas la description de la ressource et vous obtiendrez une erreur de compilation.

Les règles sont simples:

  • Toutes les ressources d'un module doivent se trouver dans l'espace de noms du module. Si le module est appelé foo, alors toutes les ressources qu'il contient doivent être nommées foo::<anything>ou juste foo.
  • La ressource avec le nom du module doit être dans le fichier init.pp.
  • Pour les autres ressources, le schéma de dénomination des fichiers est le suivant :
    • le préfixe avec le nom du module est supprimé
    • tous les doubles deux-points, le cas échéant, sont remplacés par des barres obliques
    • l'extension est ajoutée .pp

Je vais le démontrer avec un exemple. Disons que j'écris un module nginx. Il contient les ressources suivantes :

  • classe nginx décrit dans le manifeste init.pp;
  • classe nginx::service décrit dans le manifeste service.pp;
  • définir nginx::server décrit dans le manifeste server.pp;
  • définir nginx::server::location décrit dans le manifeste server/location.pp.

Modèles

Vous savez sûrement vous-même ce que sont les modèles, je ne les décrirai pas en détail ici. Mais je vais le laisser juste au cas où lien vers Wikipédia.

Comment utiliser les modèles : la signification d'un modèle peut être étendue à l'aide d'une fonction template, auquel est transmis le chemin d’accès au modèle. Pour les ressources de type filet utilisé avec le paramètre content. Par exemple, comme ceci :

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

Afficher le chemin <modulename>/<filename> implique un fichier <rootdir>/modules/<modulename>/templates/<filename>.

De plus, il existe une fonction inline_template - il reçoit le texte du modèle en entrée, pas le nom du fichier.

Dans les modèles, vous pouvez utiliser toutes les variables Puppet dans la portée actuelle.

Puppet prend en charge les modèles au format ERB et EPP :

En bref sur l'ERB

Structures de contrôle:

  • <%= ВЫРАЖЕНИЕ %> — insérer la valeur de l'expression
  • <% ВЫРАЖЕНИЕ %> — calculer la valeur d'une expression (sans l'insérer). Les instructions conditionnelles (if) et les boucles (chacune) se trouvent généralement ici.
  • <%# КОММЕНТАРИЙ %>

Les expressions dans ERB sont écrites en Ruby (ERB est en fait Embedded Ruby).

Pour accéder aux variables du manifeste, vous devez ajouter @ au nom de la variable. Pour supprimer un saut de ligne qui apparaît après une construction de contrôle, vous devez utiliser une balise de fermeture -%>.

Exemple d'utilisation du modèle

Disons que j'écris un module pour contrôler ZooKeeper. La classe responsable de la création de la configuration ressemble à ceci :

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'),
  }
}

Et le modèle correspondant zoo.cfg.erb - Donc:

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

Faits et variables intégrées

Souvent, la partie spécifique de la configuration dépend de ce qui se passe actuellement sur le nœud. Par exemple, selon la version de Debian, vous devez installer l'une ou l'autre version du paquet. Vous pouvez surveiller tout cela manuellement, en réécrivant les manifestes si les nœuds changent. Mais ce n’est pas une approche sérieuse : l’automatisation est bien meilleure.

Pour obtenir des informations sur les nœuds, Puppet dispose d'un mécanisme appelé faits. Faits - il s'agit d'informations sur le nœud, disponibles dans des manifestes sous forme de variables ordinaires dans l'espace de noms global. Par exemple, le nom d'hôte, la version du système d'exploitation, l'architecture du processeur, la liste des utilisateurs, la liste des interfaces réseau et leurs adresses, et bien plus encore. Les faits sont disponibles dans des manifestes et des modèles sous forme de variables régulières.

Un exemple de travail avec des faits :

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

Formellement, un fait a un nom (chaîne) et une valeur (différents types sont disponibles : chaînes, tableaux, dictionnaires). Manger ensemble de faits intégrés. Vous pouvez également écrire le vôtre. Les collecteurs de faits sont décrits comme les fonctions dans Rubyou comment fichiers exécutables. Les faits peuvent également être présentés sous la forme fichiers texte avec des données sur les nœuds.

Pendant le fonctionnement, l'agent marionnette copie d'abord tous les collecteurs de faits disponibles du serveur Pappet vers le nœud, après quoi il les lance et envoie les faits collectés au serveur ; Après cela, le serveur commence à compiler le catalogue.

Faits sous forme de fichiers exécutables

Ces faits sont placés dans des modules du répertoire facts.d. Bien entendu, les fichiers doivent être exécutables. Lorsqu'ils sont exécutés, ils doivent afficher les informations sur la sortie standard au format YAML ou clé=valeur.

N'oubliez pas que les faits s'appliquent à tous les nœuds contrôlés par le serveur poppet sur lequel votre module est déployé. Par conséquent, dans le script, veillez à vérifier que le système dispose de tous les programmes et fichiers nécessaires au bon fonctionnement de votre fait.

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

Faits sur Ruby

Ces faits sont placés dans des modules du répertoire 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

Faits textuels

Ces faits sont placés sur les nœuds du répertoire /etc/facter/facts.d dans la vieille marionnette ou /etc/puppetlabs/facts.d dans la nouvelle marionnette.

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

Arriver aux faits

Il existe deux manières d’aborder les faits :

  • à travers le dictionnaire $facts: $facts['fqdn'];
  • en utilisant le nom du fait comme nom de variable : $fqdn.

Il est préférable d'utiliser un dictionnaire $facts, ou mieux encore, indiquez l'espace de noms global ($::facts).

Voici la section pertinente de la documentation.

Variables intégrées

Outre les faits, il y a aussi quelques variables, disponible dans l'espace de noms global.

  • faits fiables — les variables extraites du certificat du client (puisque le certificat est généralement émis sur un serveur poppet, l'agent ne peut pas simplement prendre et modifier son certificat, les variables sont donc « fiables ») : le nom du certificat, le nom du hôte et domaine, extensions du certificat.
  • faits sur le serveur —variables liées aux informations sur le serveur—version, nom, adresse IP du serveur, environnement.
  • faits sur les agents — variables ajoutées directement par puppet-agent, et non par facter — nom du certificat, version de l'agent, version marionnette.
  • variables principales - Variables Pappetmaster (sic !). C'est à peu près la même chose que dans faits sur le serveur, ainsi que les valeurs des paramètres de configuration sont disponibles.
  • variables du compilateur — des variables du compilateur qui diffèrent dans chaque portée : le nom du module actuel et le nom du module dans lequel l'objet actuel a été accédé. Ils peuvent être utilisés, par exemple, pour vérifier que vos cours privés ne sont pas utilisés directement depuis d'autres modules.

Ajout 1 : comment exécuter et déboguer tout cela ?

L'article contenait de nombreux exemples de code marionnette, mais ne nous expliquait pas du tout comment exécuter ce code. Eh bien, je me corrige.

Un agent suffit pour exécuter Puppet, mais dans la plupart des cas, vous aurez également besoin d'un serveur.

Agent

Au moins depuis la version XNUMX, les packages puppet-agent de dépôt officiel de Puppetlabs contiennent toutes les dépendances (ruby et les gems correspondantes), donc il n'y a pas de difficultés d'installation (je parle des distributions basées sur Debian - nous n'utilisons pas de distributions basées sur RPM).

Dans le cas le plus simple, pour utiliser la configuration marionnette, il suffit de lancer l'agent en mode sans serveur : à condition que le code marionnette soit copié sur le nœud, lancez 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

Il est bien sûr préférable de configurer le serveur et d'exécuter des agents sur les nœuds en mode démon - puis une fois toutes les demi-heures, ils appliqueront la configuration téléchargée depuis le serveur.

Vous pouvez imiter le modèle de travail push - accédez au nœud qui vous intéresse et commencez sudo puppet agent -t. Clé -t (--test) comprend en fait plusieurs options qui peuvent être activées individuellement. Ces options incluent les éléments suivants :

  • ne pas s'exécuter en mode démon (par défaut l'agent démarre en mode démon) ;
  • s'arrêter après avoir appliqué le catalogue (par défaut, l'agent continuera à travailler et appliquera la configuration une fois toutes les demi-heures) ;
  • rédiger un journal de travail détaillé ;
  • afficher les modifications dans les fichiers.

L'agent a un mode de fonctionnement sans modification - vous pouvez l'utiliser lorsque vous n'êtes pas sûr d'avoir écrit la configuration correcte et que vous souhaitez vérifier ce que l'agent changera exactement pendant le fonctionnement. Ce mode est activé par le paramètre --noop sur la ligne de commande : sudo puppet agent -t --noop.

De plus, vous pouvez activer le journal de débogage du travail - dans celui-ci, Puppet écrit sur toutes les actions qu'il effectue : sur la ressource qu'il traite actuellement, sur les paramètres de cette ressource, sur les programmes qu'il lance. Bien sûr, c'est un paramètre --debug.

Serveur

Je ne considérerai pas la configuration complète du pappetserver et le déploiement du code dans cet article ; je dirai seulement qu'il existe une version entièrement fonctionnelle du serveur qui ne nécessite pas de configuration supplémentaire pour fonctionner avec un petit nombre de nœuds (disons, jusqu'à une centaine). Un plus grand nombre de nœuds nécessitera un réglage - par défaut, puppetserver ne lance pas plus de quatre travailleurs, pour de plus grandes performances, vous devez augmenter leur nombre et n'oubliez pas d'augmenter les limites de mémoire, sinon le serveur fera la collecte des ordures la plupart du temps.

Déploiement de code - si vous en avez besoin rapidement et facilement, regardez (r10k)[https://github.com/puppetlabs/r10k], pour les petites installations, cela devrait suffire.

Addendum 2 : Directives de codage

  1. Placez toute la logique dans les classes et les définitions.
  2. Conservez les classes et les définitions dans des modules, pas dans des manifestes décrivant les nœuds.
  3. Utilisez les faits.
  4. Ne faites pas de if en fonction des noms d'hôtes.
  5. N'hésitez pas à ajouter des paramètres pour les classes et les définitions - c'est mieux que la logique implicite cachée dans le corps de la classe/define.

J'expliquerai pourquoi je recommande de faire cela dans le prochain article.

Conclusion

Terminons par l'introduction. Dans le prochain article, je vous parlerai de Hiera, ENC et PuppetDB.

Seuls les utilisateurs enregistrés peuvent participer à l'enquête. se connecters'il te plait.

En fait, il y a beaucoup plus de matériel - je peux écrire des articles sur les sujets suivants, voter sur ce qui vous intéresserait :

  • 59,1%Constructions de marionnettes avancées - des conneries de niveau supérieur : boucles, mappages et autres expressions lambda, collecteurs de ressources, ressources exportées et communication inter-hôtes via Puppet, balises, fournisseurs, types de données abstraits.13
  • 31,8%« Je suis l'administrateur de ma mère » ou comment chez Avito nous nous sommes liés d'amitié avec plusieurs serveurs poppet de différentes versions et, en principe, la partie concernant l'administration du serveur poppet.7
  • 81,8%Comment nous écrivons le code marionnette : instrumentation, documentation, tests, CI/CD.18

22 utilisateurs ont voté. 9 utilisateurs se sont abstenus.

Source: habr.com