Introduction à Puppet

Puppet est un système de gestion de configuration. Il est utilisé pour ramener les hôtes à l'état désiré et maintenir cet état.

Je travaille avec Puppet depuis plus de cinq ans. Ce texte est essentiellement une compilation retravaillée et traduite des points clés de la documentation officielle, qui permettra aux débutants de comprendre rapidement les fondamentaux de Puppet.

Introduction à Puppet

Informations de base

Le fonctionnement de Puppet est basé sur un modèle client-serveur, bien qu'une option de fonctionnement sans serveur avec des fonctionnalités limitées soit également supportée.

Un modèle de fonctionnement par pull est utilisé : par défaut, toutes les demi-heures, les clients se connectent au serveur pour obtenir la configuration et l'appliquent. Si vous avez travaillé avec Ansible, vous savez que celui-ci utilise un modèle push : l'administrateur initie le processus d'application de la configuration, les clients ne feront rien d'eux-mêmes.

Pour l'interaction réseau, un chiffrement TLS bidirectionnel est utilisé : le serveur et le client possèdent chacun des clés privées et les certificats correspondants. En général, le serveur délivre des certificats aux clients, mais l'utilisation d'une CA externe est également possible.

Introduction aux manifestes

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

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

  • Le fichier existe et a un contenu spécifique.
  • Le paquet est installé.
  • Le service est en cours d'exécution.

Les ressources peuvent être interconnectées :

  • Il existe des dépendances qui impactent l'ordre d'application des ressources.
    Par exemple, « d'abord installe le paquet, ensuite modifie le fichier de configuration, après cela démarre le service ».
  • Il existe des notifications : si une ressource change, elle envoie des notifications aux ressources abonnées.
    Par exemple, si le fichier de configuration est modifié, le service peut être automatiquement redémarré.

De plus, dans Puppet DSL, il y a des fonctions et des variables, ainsi que des opérateurs conditionnels et des sélecteurs. Divers mécanismes de modélisation sont également pris en charge : EPP et ERB.

Puppet est écrit en Ruby, donc de nombreuses constructions et termes proviennent de là. Ruby permet d'étendre Puppet : d'écrire une logique complexe, de nouveaux types de ressources et des fonctions.

Lors de l'exécution de Puppet, les manifestes pour chaque nœud spécifique sur le serveur sont compilés dans un répertoire. Le répertoire — il s'agit d'une liste de ressources et de leurs relations après calcul des valeurs des fonctions, des variables et des opérateurs conditionnels.

Syntaxe et style de code

Voici les 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 un manifeste :

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

Les espaces et les sauts de ligne ne sont pas une partie obligatoire du manifeste, cependant, il existe un guide de style. Résumé :

  • Des retraits à deux espaces, les tabulations ne sont pas utilisées.
  • Les accolades sont séparées par un espace, le deux-points n'est pas séparé par un espace.
  • Des virgules après chaque paramètre, y compris le dernier. Chaque paramètre doit être sur une ligne séparée. Une exception est faite pour le cas sans paramètres et un seul paramètre : il peut être écrit sur une ligne sans virgule (c'est-à-dire, resource { 'title': } et resource { 'title': param => value }).
  • Les flèches des paramètres doivent être alignées sur le même niveau.
  • Les flèches de relation entre ressources sont écrites avant celles-ci.

Emplacement des fichiers sur le puppetserver

Pour de plus amples explications, j'introduirai le concept de "répertoire racine". Le répertoire racine est celui dans lequel se trouve la configuration Puppet pour un nœud spécifique.

Le répertoire racine varie en fonction de la version de Puppet et de l'utilisation des environnements. Les environnements sont des ensembles de configurations indépendants, qui sont stockés dans des répertoires séparés. Ils sont généralement utilisés en combinaison avec Git, dans ce cas, les environnements sont créés à partir de branches Git. En conséquence, chaque nœud se trouve dans l'un ou l'autre environnement. Cela se configure sur le nœud lui-même, ou dans l'ENC, dont je parlerai dans l'article suivant.

  • Dans la troisième version ("ancien Puppet"), le répertoire de base était /etc/puppet. L'utilisation des environnements est optionnelle – 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 de l'environnement. Si des environnements ne sont pas utilisés, le répertoire racine sera le répertoire de base.
  • À partir de la quatrième version ("nouveau Puppet"), l'utilisation des environnements est devenue obligatoire, et le répertoire de base a été déplacé dans /etc/puppetlabs/code. Ainsi, les environnements sont stockés dans /etc/puppetlabs/code/environments, le répertoire racine est le répertoire de l'environnement.

Dans le répertoire racine, il doit y avoir un sous-répertoire manifests, où se trouvent un ou plusieurs manifestes décrivant les nœuds. De plus, il doit y avoir un sous-répertoire modules, où se trouvent les modules. Je vous expliquerai plus tard ce que sont les modules. De plus, dans l'ancienne version de Puppet, il peut également y avoir un sous-répertoire files, où sont placés divers fichiers que nous copions sur les nœuds. Dans la nouvelle version de Puppet, tous les fichiers ont été déplacés dans des modules.

Les fichiers de manifeste ont l'extension .pp.

Exemples concrets

Description du nœud et de la ressource sur celui-ci

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

Écrivons le manifeste :

node 'server1.testdomain' {   # bloc de configuration relatif au nœud server1.testdomain
    file { '\/etc\/issue':   # description du fichier \/etc\/issue
        ensure  => present,   # ce fichier doit exister
        content => 'Debian GNU\/Linux n l',   # il doit avoir ce contenu
        owner   => root,   # utilisateur propriétaire
        group   => root,   # groupe propriétaire
        mode    => '0644',   # permissions sur le fichier. Elles sont définies sous forme de chaîne (entre guillemets), car sinon un nombre commençant par 0 sera interprété comme écrit en système octal, et tout ira mal comme prévu
    }
}

Relations des ressources sur le nœud

Sur le nœud server2.testdomain doit avoir nginx en cours d'exécution avec une configuration préparée à l'avance.

Décomposons la tâche :

  • Il faut que le paquet soit installé nginx.
  • Il faut que les fichiers de configuration soient copiés depuis le serveur.
  • Il faut que le service soit en cours d'exécution nginx.
  • En cas de mise à jour de la configuration, il faut redémarrer le service.

Écrivons le manifeste :

noeud 'server2.testdomain' {   # bloc de configuration pour le nœud server2.testdomain
    paquet { 'nginx':   # description du paquet nginx
        ensure => installé,   # il doit être installé
    }
  # La flèche directe (->) indique que la ressource ci-dessous doit
  # être créée après la ressource décrite ci-dessus.
  # Ces dépendances sont transitives.
    -> fichier { '/etc/nginx':   # description du fichier /etc/nginx
        ensure => répertoire,   # cela doit être un répertoire
        source => 'puppet:///modules/example/nginx-conf',   # son contenu doit être pris sur le serveur Puppet à l'adresse indiquée
        recurse => vrai,   # copier les fichiers de manière récursive
        purge => vrai,   # supprimer les fichiers inutiles (ceux qui ne sont pas dans la source)
        force => vrai,   # supprimer les répertoires inutiles
    }
  # La flèche ondulée (~>) indique que la ressource ci-dessous doit
  # s'abonner aux modifications de la ressource décrite ci-dessus.
  # La flèche ondulée inclut la directe (->).
    ~> service { 'nginx':   # description du service nginx
        ensure => en cours d'exécution,   # il doit être en cours d'exécution
        enable => vrai,   # il doit être lancé automatiquement au démarrage du système
    }
  # Lorsqu'une ressource de type service reçoit une notification,
  # le service correspondant est redémarré.
}

Pour que cela fonctionne, il faut un agencement des fichiers sur le serveur Puppet comme suit :

/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

La liste complète des types de ressources supportés se trouve dans la documentation, ici je vais décrire cinq types de base, qui dans ma pratique suffisent pour résoudre la plupart des tâches.

fichier

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

Paramètres :

  • nom de la ressource — chemin du fichier (facultatif)
  • chemin — chemin du fichier (s'il n'est pas spécifié dans le nom)
  • ensure — type de fichier :
    • absent — supprimer le fichier
    • présent — doit être un fichier de n'importe quel type (si le fichier n'existe pas, un fichier standard sera créé)
    • fichier — fichier standard
    • répertoire — répertoire
    • lien — lien symbolique
  • contenu — contenu du fichier (ne s'applique qu'aux fichiers standards, ne peut pas être utilisé avec source ou cible)
  • source — lien vers le chemin d'où copier le contenu du fichier (ne peut pas être utilisé avec contenu ou cible). Peut être spécifié soit sous forme d'URI avec le schéma puppet: (alors des fichiers du serveur Puppet seront utilisés), soit avec le schéma http: (j'espère que c'est clair ce qu'il en sera dans ce cas), et même avec le schéma file: ou sous forme de chemin absolu sans schéma (alors le fichier sera utilisé à partir du système de fichiers local sur le nœud)
  • cible — où le lien symbolique doit pointer (ne peut pas être utilisé avec contenu ou source)
  • propriétaire — l'utilisateur auquel le fichier doit appartenir
  • groupe — le groupe auquel le fichier doit appartenir
  • mode — les permissions du fichier (sous forme de chaîne)
  • récursif — inclut le traitement récursif des répertoires
  • purger — inclut la suppression des fichiers non décrits dans Puppet
  • forcer — inclut la suppression des répertoires non décrits dans Puppet

package

Installe et supprime des paquets. Peut gérer les notifications — réinstalle le paquet si le paramètre est spécifié réinstaller_en_rafraîchissant.

Paramètres :

  • nom de la ressource — nom du paquet (optionnel)
  • nom — nom du paquet (si non spécifié dans le nom)
  • fournisseur — le gestionnaire de paquet à utiliser
  • ensure — état souhaité du paquet :
    • présent, installé — toute version installée
    • latest — dernière version installée
    • absent — supprimé (apt-get remove)
    • purged — supprimé avec les fichiers de configuration (apt-get purge)
    • retenu — version du paquet bloquée (apt-mark hold)
    • toute autre chaîne — version spécifiée installée
  • réinstaller_en_rafraîchissant — si true, lors de la réception d'une notification, le paquet sera réinstallé. Utile pour les distributions basées sur source, où la reconstruction des paquets peut être nécessaire lors de la modification des paramètres de construction. Par défaut faux.

service

Gère les services. Peut gérer les notifications — redémarre le service.

Paramètres :

  • nom de la ressource — service à gérer (optionnel)
  • nom — service à gérer (si non spécifié dans le nom)
  • ensure — état souhaité du service :
    • en cours d'exécution — en cours d'exécution
    • arrêté — arrêté
  • activer — gère la possibilité de démarrage du service :
    • true — démarrage automatique activé (systemctl enable)
    • masquer — masqué (systemctl mask)
    • faux — démarrage automatique désactivé (systemctl disable)
  • redémarrer — commande pour redémarrer le service
  • statut — commande pour vérifier l'état du service
  • hasrestart — indique si le script d'initialisation du service prend en charge le redémarrage. Si faux et le paramètre est spécifié redémarrer — la valeur de ce paramètre est utilisée. Si faux et le paramètre redémarrer n'est pas spécifié — le service est arrêté et redémarré (mais dans systemd, la commande utilisée est systemctl restart).
  • hasstatus — indique si le script d'initialisation du service prend en charge la commande statut. Si faux, alors la valeur du paramètre est utilisée statut. Par défaut true.

exec

Exécute des commandes externes. Si aucun paramètre n'est spécifié crée, onlyif, unless ou refreshonly, la commande sera exécutée à chaque fois que Puppet s'exécute. Peut gérer les notifications — exécute la commande.

Paramètres :

  • nom de la ressource — commande à exécuter (facultatif)
  • commande — commande à exécuter (si elle n'est pas spécifiée dans le nom)
  • chemin — chemins dans lesquels chercher le fichier exécutable
  • onlyif — si la commande spécifiée dans ce paramètre se termine par un code de retour nul, la commande principale sera exécutée
  • unless — si la commande spécifiée dans ce paramètre se termine par un code de retour non nul, la commande principale sera exécutée
  • crée — si le fichier précisé dans ce paramètre n'existe pas, la commande principale sera exécutée
  • refreshonly — si true, la commande ne sera exécutée que si cet exec reçoit une notification d'autres ressources
  • cwd — répertoire à partir duquel lancer la commande
  • user — utilisateur à partir duquel lancer la commande
  • fournisseur — avec quoi exécuter la commande :
    • posix — crée simplement un processus fils, doit être spécifié chemin
    • shell — la commande s'exécute dans un shell /bin/sh, peut ne pas être spécifié chemin, il est possible d'utiliser le globbing, les pipes et d'autres fonctionnalités du shell. Déterminé automatiquement si des caractères spéciaux sont présents (|, ;, &&, || etc.

cron

Gère les tâches cron.

Paramètres :

  • nom de la ressource — simplement un identifiant
  • ensure — état de la tâche cron :
    • présent — créer si elle n'existe pas
    • absent — supprimer si elle existe
  • commande — quelle commande exécuter
  • environment — dans quel environnement exécuter la commande (liste des variables d'environnement et de leurs valeurs séparées par =)
  • user — de quel utilisateur exécuter la commande
  • minute, heure, jour de la 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 si supprimé hors de la boîte dans puppetserver, donc il n'y a pas de documentation sur le site général. Mais il est dans la boîte dans puppet-agent, donc il n'est pas nécessaire de l'installer séparément. La documentation à son sujet peut être consultée dans la documentation de la cinquième version de Puppet, ou bien sur GitHub.

À propos des ressources en général

Exigences d'unicité des ressources

L'erreur la plus courante que nous rencontrons est Déclaration dupliquée. Cette erreur se produit lorsque deux ressources ou plus du même type avec le même nom se retrouvent dans un répertoire.

C'est pourquoi je le répète : dans les manifestes d'un nœud, il ne doit pas y avoir de ressources du même type avec le même nom (title) !

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

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

Dans d'autres types de ressources, il existe des paramètres analogues qui aident à éviter la duplication — nom au service, commande au exec, et ainsi de suite.

Métaparamètres

Certains paramètres spéciaux sont présents pour chaque type de ressource, quelle que soit sa nature.

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

Liste succincte :

  • require — ce paramètre spécifie de quelles ressources dépend cette ressource.
  • before — ce paramètre indique quelles ressources dépendent de cette ressource.
  • subscribe — ce paramètre indique de quelles ressources cette ressource reçoit des notifications.
  • notify — ce paramètre précise quelles ressources reçoivent des notifications de cette ressource.

Tous les métaparamètres énumérés acceptent soit une seule référence à une ressource, soit un tableau de références entre crochets.

Références aux ressources

Une référence à une ressource est simplement une mention de la ressource. Elles sont principalement utilisées pour indiquer des dépendances. Une référence à une ressource non existante provoquera une erreur de compilation.

La syntaxe d'une référence est la suivante : type de ressource avec une majuscule (si le nom du type contient des doubles deux-points, chaque partie du nom entre les deux-points est écrite avec une majuscule), suivie, entre crochets, du nom de la ressource (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

La documentation est ici.

Comme déjà mentionné, les dépendances simples entre ressources sont transitives. D'ailleurs, faites attention lors de l'établissement des dépendances — vous pouvez créer des dépendances circulaires, 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 une ressource reçoit une notification, elle est mise à jour. Les actions lors de la mise à jour dépendent du type de ressource — exec elle exécute une commande, service elle redémarre un service, package elle réinstalle un paquet. Si aucune action de mise à jour n'est définie pour la ressource, rien ne se passe.
  • Lors d'un seul passage, la ressource Puppet est mise à jour au maximum une 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, cette ressource envoie des notifications à toutes les ressources qui sont abonnées à elle.
  • Si une ressource est mise à jour, elle envoie des notifications à toutes les ressources qui sont abonnées à elle.

Traitement des paramètres non spécifiés

En général, si un paramètre d'une 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 fichier n'a pas le paramètre propriétaire, 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 partagent une partie identique de la configuration, mais il y a aussi des différences - sinon, nous pourrions décrire tout cela dans un seul bloc node {}. Bien sûr, il est possible de copier simplement les parties identiques de la configuration, mais en général, c'est une mauvaise solution - la configuration devient encombrante, et lors de la modification de la partie partagée, il faudra corriger la même chose à plusieurs endroits. Il est facile de faire une erreur dans ce cas, de plus, le principe DRY (don’t repeat yourself) n'a pas été inventé pour rien.

Pour résoudre ce problème, il existe une construction appelée classe.

Classes

Classe — est un bloc de code Puppet nommé. Les classes sont nécessaires pour la réutilisation du code.

D'abord, il faut décrire la classe. La description seule ne crée aucun ressource. 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
# про отличие этих двух вариантов будет рассказано дальше

Prenons l'exemple de la tâche précédente - extrayons 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 très flexible, car elle apporte toujours la même configuration pour nginx. Faisons en sorte que le chemin vers la configuration devienne une variable, alors cette classe pourra être utilisée pour installer nginx avec n'importe quelle configuration.

Cela peut être fait à l'aide de variables.

Attention : les variables dans Puppet sont immuables !

De plus, une variable ne peut être accédée qu'après avoir été déclarée, sinon sa valeur sera undef.

Exemple de manipulation 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}"

Dans Puppet, il existe des espaces de noms, et les variables ont donc une portée: une variable ayant 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, celle-ci est recherchée dans l'espace de noms actuel, puis dans l'englobant, et ainsi de suite.

Exemples d'espaces de noms :

  • global — les variables y sont placées en dehors de la description de la classe ou du nœud ;
  • espace de noms du nœud dans la description du nœud ;
  • espace de noms de la classe dans la description de la classe.

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

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

Convenons que le chemin de la configuration nginx se trouve dans la variable $nginx_conf_source. La classe aura alors la structure suivante :

class nginx_example {
    package { 'nginx':
        ensure => installed,
    }
    -> file { '/etc/nginx':
        ensure => directory,
        source => $nginx_conf_source,   # ici nous utilisons la variable à la place d'une chaîne fixe
        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 y a une sorte de «connaissance secrète» que quelque part à l'intérieur de la classe, une variable avec un tel nom est utilisée. Il est bien plus approprié de rendre cette connaissance commune – les classes peuvent avoir des paramètres.

Les paramètres de classe sont des variables dans l'espace de noms de la classe, définies dans l'en-tête de la classe et pouvant être utilisées comme des variables ordinaires 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.

Un paramètre peut avoir une valeur par défaut. Si un paramètre n'a pas de valeur par défaut et qu'aucune valeur n'est spécifiée lors de l'utilisation, cela entraînera une erreur de compilation.

Paramétrons la classe de l'exemple précédent et ajoutons deux paramètres : le premier, obligatoire – le chemin vers la configuration, et le second, facultatif – le nom du paquet avec nginx (par exemple, dans Debian, il existe 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 typées. Il existe beaucoup de types de données. Les types de données sont généralement utilisés pour la validation des valeurs des paramètres passés dans les classes et les définitions. Si le paramètre passé ne correspond pas au type spécifié, une erreur de compilation se produira.

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

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

Classes : include classname vs class{'classname':}

Chaque classe est une ressource de type class. Comme pour tout autre type de ressource, deux instances de la même classe ne peuvent pas coexister sur un même nœud.

Si vous essayez d'ajouter une classe au même nœud deux fois à l'aide de class { 'classname':} (qu'elle soit avec des paramètres différents ou identiques), cela entraînera une erreur de compilation. En revanche, dans le cas de l'utilisation d'une classe en style ressource, vous pouvez spécifier immédiatement tous ses paramètres dans le manifeste.

Cependant, si vous utilisez include, la classe peut être ajoutée autant de fois que vous le souhaitez. Le fait est que include est une fonction idempotente qui vérifie si la classe est présente dans le répertoire. Si la classe n'est pas dans le répertoire, elle l'ajoute ; sinon, elle ne fait rien. Mais dans le cas de l'utilisation de include vous ne pouvez pas définir les paramètres de la classe lors de sa déclaration - tous les paramètres obligatoires doivent être spécifiés dans une source de données externe - Hiera ou ENC. Nous en discuterons dans l'article suivant.

Définitions

Comme indiqué dans le bloc précédent, la même classe ne peut être présente qu'une seule fois sur un nœud. Cependant, dans certains cas, il est nécessaire de pouvoir appliquer un même bloc de code avec des paramètres différents sur un même nœud. Autrement dit, il y a un besoin pour un type de ressource propre.

Par exemple, pour installer un module PHP, nous faisons ce qui suit chez Avito :

  1. Nous installons le paquet contenant ce module.
  2. Nous 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 ces cas, une construction telle que définition (define, defined type, defined resource type) est utilisée. Une définition ressemble à une classe, mais il y a des différences : premièrement, chaque définition est un type de ressource, et non une ressource ; deuxièmement, chaque définition a un paramètre implicite $title, dans lequel le nom de la ressource entre lors de sa déclaration. Tout comme avec les classes, une définition doit d'abord être décrite avant de pouvoir être utilisée.

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'],  # Les triggers des paquets PHP Debian créent eux-mêmes des liens symboliques et redémarrent le service php-fpm - ce n'est pas nécessaire pour nous, car nous gérons à la fois les liens symboliques et le service avec 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' }
}

Il est le plus facile de détecter une erreur de déclaration dupliquée dans le define. Cela se produit s'il y a une ressource avec un nom constant dans le define, et qu'il y a deux ou plusieurs instances de ce define sur un nœud.

Se protéger contre cela est simple : toutes les ressources à l'intérieur du define doivent avoir un nom dépendant de $title. En alternative, on peut ajouter des ressources de manière idempotente, dans le cas le plus simple, il suffit de sortir les ressources communes à toutes les instances du define dans une classe séparée et d'inclure cette classe dans le define - la fonction include est idempotente.

Il existe d'autres moyens d'atteindre l'idempotence lors de l'ajout de ressources, notamment en utilisant les fonctions defined et ensure_resources, mais j'en parlerai dans la prochaine série.

Dépendances et notifications pour les classes et les defines

Les classes et les defines ajoutent les règles suivantes au traitement des dépendances et des notifications :

  • Une dépendance de classe/define ajoute des dépendances à toutes les ressources de la classe/define ;
  • Une dépendance de classe/define ajoute des dépendances à toutes les ressources de la classe/define ;
  • Une notification de classe/define notifie toutes les ressources de la classe/define ;
  • Une souscription à une classe/define s'abonne à toutes les ressources de la classe/define.

Opérateurs conditionnels et sélecteurs

La documentation est ici.

if

Ici, tout est simple :

if EXPRESSION1 {
  ...
} elsif EXPRESSION2 {
  ...
} else {
  ...
}

unless

unless — c'est l'inverse de if : le bloc de code sera exécuté si l'expression est fausse.

unless EXPRESSION {
  ...
}

case

Ici, il n'y a aussi rien de compliqué. Comme valeurs, on peut utiliser des valeurs ordinaires (chaînes, nombres, etc.), des expressions régulières, ainsi que des types de données.

cas EXPR {
  VALEUR1: { ... }
  VALEUR2, VALEUR3: { ... }
  par défaut: { ... }
}

Sélecteurs

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

$var = $othervar ? { 'val1' => 1, 'val2' => 2, par défaut => 3 }

Modules

Lorsque la configuration est petite, elle peut facilement être contenue 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 s'étend et devient difficile à gérer.

De plus, il y a un problème de réutilisation du code - lorsque tout le code est dans un seul manifeste, il est difficile de partager ce code avec d'autres. Pour résoudre ces deux problèmes, Puppet possède une entité appelée modules.

Modules — ce sont des ensembles de classes, de définitions et d'autres entités Puppet, extraites dans un répertoire séparé. En d'autres termes, un module est un morceau indépendant de logique Puppet. Par exemple, il peut y avoir un module pour travailler avec nginx, et il contiendra uniquement ce qui est nécessaire pour travailler avec nginx, et il peut y avoir un module pour travailler avec PHP, etc.

Les modules sont versionnés et supportent également des dépendances entre eux. Il existe un dépôt ouvert de modules — Puppet Forge.

Sur le serveur Puppet, les modules sont situés dans le sous-répertoire modules du répertoire racine. À l'intérieur de chaque module, il y a un schéma de répertoire standard — manifests, files, templates, lib, etc.

Structure des fichiers dans un module

À la racine du module, il peut y avoir les répertoires suivants avec des noms explicites :

  • manifests — elle contient les manifestes
  • files — elle contient les fichiers
  • templates — elle contient les modèles
  • lib — elle contient du code Ruby

Ce n'est pas une liste complète de répertoires et de fichiers, mais c'est suffisant pour cet article pour l'instant.

Noms des ressources et noms de fichiers dans un module

La documentation est ici.

Les ressources (classes, définitions) dans un module ne peuvent pas être nommées n'importe comment. De plus, il y a une correspondance directe entre le nom de la ressource et le nom du fichier dans lequel Puppet cherchera la description de cette ressource. Si les règles de nommage sont enfreintes, Puppet ne trouvera tout simplement pas la description des ressources, et une erreur de compilation se produira.

Les règles sont simples :

  • Toutes les ressources dans un module doivent appartenir à l'espace de noms du module. Si le module s'appelle foo, alors toutes les ressources en lui doivent être nommées foo::, ou simplement foo.
  • Une ressource portant le nom du module doit se trouver dans le fichier init.pp.
  • Pour les autres ressources, le schéma de nommage des fichiers est le suivant :
    • le préfixe avec le nom du module est supprimé
    • tous les doubles deux-points, s'ils existent, sont remplacés par des barres obliques
    • une extension est ajoutée .pp

Je vais montrer par un exemple. Supposons que j'écrive 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éfinition nginx::server décrit dans le manifeste server.pp;
  • définition nginx::server::location décrit dans le manifeste server/location.pp.

Modèles

Vous savez sûrement déjà ce que sont les modèles, je ne vais pas m'étendre ici. Mais pour laisser un lien vers Wikipédia.

Comment utiliser les modèles : la valeur du modèle peut être révélée à l'aide de la fonction template, à laquelle on passe le chemin vers le modèle. Pour les ressources de type fichier utilisé avec le paramètre contenu. Par exemple, comme ça :

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

Le chemin au format / implique le fichier /modules//templates/.

De plus, il existe une fonction inline_template — elle prend en entrée le texte du modèle, et non le nom du fichier.

À l'intérieur des modèles, toutes les variables Puppet dans l'espace de noms actuel peuvent être utilisées.

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

En résumé sur ERB

Structures de contrôle :

  • <%= ВЫРАЖЕНИЕ %> — insérer la valeur de l'expression
  • <% ВЫРАЖЕНИЕ %> — évaluer la valeur d'une expression (sans l'insérer). Ici, les opérateurs conditionnels (if) et les boucles (each) sont généralement utilisés.
  • <%# КОММЕНТАРИЙ %>

Les expressions en ERB sont écrites en Ruby (en fait, ERB signifie Embedded Ruby).

Pour accéder aux variables du manifeste, il faut ajouter @ au nom de la variable. Pour éliminer le retour à la ligne qui apparaît après la structure de contrôle, il faut utiliser la balise de fermeture -%>.

Exemple d'utilisation d'un modèle

Supposons que j'écrive un module pour gérer ZooKeeper. La classe responsable de la création de la configuration ressemble à peu près à cela :

class zookeeper::configure (\n  Array[String] $nodes,\n  Integer $port_client,\n  Integer $port_quorum,\n  Integer $port_leader,\n  Hash[String, Any] $properties,\n  String $datadir,\n) {\n  file { '/etc/zookeeper/conf/zoo.cfg':\n    ensure  => present,\n    content => template('zookeeper/zoo.cfg.erb'),\n  }\n}

Et le modèle correspondant zoo.cfg.erb — est comme ceci :

0 -%>\n\nserver.=:::\n\n\n\ndataDir=\n\n\n=\n

Faits et variables intégrées

Souvent, une partie spécifique de la configuration dépend de ce qui se passe à ce moment-là sur le nœud. Par exemple, selon la version de Debian en cours, il est nécessaire d'installer telle ou telle version de paquet. On peut tout suivre manuellement en réécrivant les manifestes en cas de changement de nœud. Mais c'est une approche peu sérieuse, l'automatisation est bien meilleure.

Pour obtenir des informations sur les nœuds dans Puppet, il existe un mécanisme appelé faits. Les faits sont des informations sur le nœud, disponibles dans les manifestes sous forme de variables normales 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 d'autres. Les faits sont disponibles dans les manifestes et les modèles comme des variables normales.

Exemple d'utilisation des faits :

notify { "Exécution de l'OS ${facts['os']['name']} version ${facts['os']['release']['full']}" : } 
# Le type de ressource notify affiche simplement un message dans le journal

Pour parler formellement, un fait a un nom (chaîne) et une valeur (différents types sont disponibles : chaînes, tableaux, dictionnaires). Il existe un ensemble de faits intégrés. On peut aussi écrire ses propres faits. Les collecteurs de faits sont décrits comme des fonctions en Ruby, ou comme des fichiers exécutables. De plus, les faits peuvent être présentés sous forme de fichiers texte contenant des données sur les nœuds.

Lors de son fonctionnement, l'agent Puppet copie d'abord tous les collecteurs de faits disponibles du serveur Puppet vers le nœud, puis les exécute et envoie les faits collectés au serveur ; ce n'est qu'après cela que le serveur commence la compilation du catalogue.

Faits sous forme de fichiers exécutables

Ces faits sont placés dans des modules dans le répertoire facts.d. Bien sûr, les fichiers doivent être exécutables. Lors de l'exécution, ils doivent afficher sur la sortie standard des informations soit au format YAML, soit au format "clé=valeur".

N'oubliez pas que les faits s'appliquent à tous les nœuds sous la gestion du serveur Puppet, sur lequel votre module est déployé. Donc, dans le script, veillez à vérifier que tous les programmes nécessaires au fonctionnement de votre fait et les fichiers sont présents dans le système.

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

Faits en Ruby

Ces faits sont placés dans des modules dans le 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 texte

Ces faits sont placés sur les nœuds dans le répertoire /etc/facter/facts.d dans l'ancien Puppet ou /etc/puppetlabs/facts.d dans le nouveau Puppet.

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

Accès aux faits

Vous pouvez accéder aux faits de deux manières :

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

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

Voici la section de documentation nécessaire.

Variables intégrées

En plus des faits, il existe également certaines variables, disponibles dans l'espace de noms global.

  • trusted facts — variables qui viennent du certificat client (étant donné que le certificat est généralement émis sur le serveur Puppet, l'agent ne peut pas simplement changer son certificat, donc les variables sont considérées comme « fiables ») : le nom du certificat, le nom d'hôte et le domaine, les extensions du certificat.
  • server facts — variables concernant les informations du serveur — version, nom, adresse IP du serveur, environnement.
  • agent facts — variables ajoutées directement par puppet-agent, et non par facter — nom du certificat, version de l'agent, version de Puppet.
  • master variables — variables du puppetmaster (sic!). Il y a à peu près la même chose que dans server facts, plus des valeurs des paramètres de configuration disponibles.
  • compiler variables — variables du compilateur, qui varient dans chaque portée : nom du module actuel et nom du module où l'objet actuel a été appelé. Elles peuvent être utilisées, par exemple, pour vérifier que vos classes privées ne sont pas utilisées directement par d'autres modules.

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

L'article contenait de nombreux exemples de code Puppet, mais n'expliquait pas comment exécuter ce code. Eh bien, je corrige cela.

Pour faire fonctionner Puppet, il suffit d'un agent, mais dans la plupart des cas, un serveur sera également nécessaire.

L'agent

Au moins à partir de la cinquième version, les paquets puppet-agent de le dépôt officiel de Puppetlabs contiennent toutes les dépendances (ruby et les gemmes correspondantes), donc il n'y a aucune difficulté d'installation (je parle des distributions basées sur Debian — nous n'utilisons pas les distributions basées sur RPM).

Dans le cas le plus simple, pour appliquer la configuration puppet, il suffit de lancer l'agent en mode sans serveur : à condition que le code Puppet soit copié sur le nœud, vous lancez puppet apply:

atikhonov@atikhonov ~\/puppet-test $ cat helloworld.pp 
node default {
    notify { 'Hello world!': }
}
atikhonov@atikhonov ~\/puppet-test $ puppet apply helloworld.pp 
Avis : catalogue compilé pour atikhonov.localdomain dans l'environnement de production en 0.01 secondes
Avis : Bonjour le monde !
Avis : \/Stage[main]\/Main\/Node[default]\/Notify[Hello world!]\/message : message défini comme 'Bonjour le monde !'
Avis : Catalogue appliqué en 0.01 secondes

Il est préférable d'élever le serveur et de lancer des agents sur les nœuds en mode démon — ainsi, toutes les demi-heures, ils appliqueront la configuration téléchargée depuis le serveur.

Vous pouvez simuler un modèle de travail push — entrez dans le nœud qui vous intéresse et lancez sudo puppet agent -t. L'option -t (--test) inclut en réalité plusieurs options qui peuvent être activées individuellement. Parmi ces options, on trouve :

  • ne pas fonctionner en mode démon (par défaut, l'agent est lancé en mode démon) ;
  • terminer le travail après l'application du catalogue (par défaut, l'agent continuera à fonctionner et appliquera la configuration toutes les demi-heures) ;
  • écrire un journal de travail détaillé ;
  • afficher les modifications dans les fichiers.

L'agent a un mode de fonctionnement sans modifications — il peut être utilisé dans les cas où vous n'êtes pas sûr d'avoir écrit une configuration correcte et que vous voulez vérifier ce que l'agent changera pendant son fonctionnement. Ce mode est activé par l'option --noop dans la ligne de commande : sudo puppet agent -t --noop.

En outre, vous pouvez activer un journal de débogage — il écrit toutes les actions qu'il effectue : concernant la ressource qu'il traite actuellement, les paramètres de cette ressource, et les programmes qu'il lance. Évidemment, cette option est --debug.

Serveur

Je ne couvrirais pas la configuration complète du puppetserver et le déploiement du code sur celui-ci dans cet article, je dirai juste qu'une version fonctionnelle du serveur est fournie prête à l'emploi, sans nécessiter 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 déjà un ajustement — par défaut, puppetserver démarre pas plus de quatre travailleurs, pour une plus grande performance, il faut augmenter leur nombre et ne pas oublier d'augmenter les limites de mémoire, sinon le serveur passera la majeure partie de son temps à faire du garbage collecting.

Le déploiement de code — si vous avez besoin de faire rapidement et simplement, regardez (sur r10k)[https://github.com/puppetlabs/r10k], pour de petites installations, cela devrait suffire.

Complément 2 : recommandations pour l'écriture de code

  1. Rassemblez toute la logique dans des classes et des définitions.
  2. Gardez les classes et les définitions dans des modules, et non dans des manifestes décrivant les nœuds.
  3. Utilisez des faits.
  4. Évitez les conditions if basées sur les noms d'hôtes.
  5. N'hésitez pas à ajouter des paramètres pour les classes et les définitions – c'est mieux que d'avoir une logique implicite cachée dans le corps de la classe ou de la définition.

Pourquoi je recommande cela, je l'expliquerai dans l'article suivant.

Conclusion

Nous terminerons ici l'introduction. Dans l'article suivant, je parlerai de Hiera, ENC et PuppetDB.

Seuls les utilisateurs enregistrés peuvent participer au sondage. Connectez-vous, s'il vous plaît.

En réalité, il y a beaucoup plus de matériel – je peux écrire des articles sur les sujets suivants, votez pour ceux qui vous intéressent :

  • 59,1%Constructs avancés de Puppet – des sujets de niveau supérieur : boucles, mappage et autres expressions lambda, collecteurs de ressources, ressources exportées et interaction inter-hôtes via Puppet, tags, fournisseurs, types de données abstraits.
  • 31,8%« Je suis admin de maman » ou comment nous avons relié plusieurs serveurs Puppet de différentes versions chez Avito, et en fait, une partie sur l'administration du serveur Puppet.
  • 81,8%Comment nous écrivons le code Puppet : environnement d'outils, documentation, tests, CI/CD.

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

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster