1. Pourquoi ce retour en enfance ?
Je suis un fainéant. Non, ce n'est pas une blague, c'est un constat. Je déteste refaire dix fois la même chose : les tâches bêtes et méchantes m'horripilent au plus haut point. Par contre, j'aime savoir que tous mes serveurs tournent comme des horloges, que mon Nagios est au vert partout, que les lutins courent dans les câbles sans se prendre les pieds dans le tapis tout ça parce que la femme de ménage l'a mal replacé. Bref, je suis un sysadmin comme il en existe des milliers. Sans avoir des milliers de serveurs à gérer, la centaine est presque atteinte et, mine de rien, ça occupe. Au début, comme tout le monde, j'ai commencé à faire des boucles for avec un ssh dedans, mais je me suis vite aperçu que ce système a ses limites, tant en termes de performances que d'homogénéité du parc. J'ai donc cherché des outils pour gérer les configurations de mes machines, de façon efficace, simple et surtout, surtout, qui ne me prenne pas tout mon temps. Au final de ces recherches, j'ai retenu Puppet [PUPPET].
2. Présentation de Puppet
Commençons par ce que Puppet n'est pas : ce n'est pas un outil de remplacement pour une boucle avec un ssh dedans. C'est un outil permettant de définir et de mettre en œuvre des configurations. Tout d'abord, qu'est ce qu'on appelle une configuration ? C'est un ensemble de paramètres qui peut se constituer de fichiers de configuration, de packages, de services, etc. La liste n'est pas exhaustive, puisqu'on peut définir ses propres types.
Il faut installer et configurer sur chaque machine que l'on veut « puppetiser » un client, qui se réfèrera au puppetmaster, machine sur laquelle seront stockés les manifests, qui contiennent les spécifications. Enfin, la communication entre les clients puppet et le puppetmaster se fait à travers une couche SSL, en utilisant des certificats.
Pour la mise en œuvre des configurations, Puppet sait prendre en compte l'environnement dans lequel il se trouve, grâce à une de ses dépendances, facter. Ainsi, dans un environnement Debian, Puppet utilisera apt pour installer un package, et pkg_add sur un FreeBSD par exemple. Toutefois, tous les OS ne sont pas égaux niveau support de leurs spécificités, même si ce point s'étoffe avec le temps, fort heureusement. Cette prise en compte des fonctionnalités se fait par l'intermédiaire des providers, qui sont tout simplement des gestionnaires locaux à la machine client.
Puppet fonctionne avec une philosophie objet : on y définit des classes, qui possèdent des méthodes, peuvent hériter d'autres classes. Chaque classe possède des ressources, qui sont en fait les spécifications de ce que l'on veut obtenir.
3. Installation
Puppet est écrit en Ruby, ce qui le rend disponible sur de nombreuses plateformes, soit sous la forme d'un gem (les gems sont au Ruby ce que le CPAN est à Perl), soit sous la forme d'un package ou encore d'un très classique tarball.
3.1 Via les gems
L'installation par les gems se fait de façon tout à fait classique :
# gem install puppet
Bien penser à vérifier que la dépendance facter de Puppet a été installée, sinon l'installer elle aussi avec un petit coup de :
# gem install facter
3.2 Via le tarball
Une fois décompressé, le tarball contient un fichier install.rb, qu'on lancera en tant que root :
# ruby install.rb
3.3 Sur Debian
L'installation via le système de packages Debian ne comporte aucune difficulté particulière et se fait avec apt-get/aptitude. Toutefois, au moment où sont écrites ces lignes, la version de Puppet disponible dans la release « Etch » n'est que la 0.20.1. Celle présente dans la branche « Lenny » est la 0.24.4-8 (et comporte des fonctionnalités de la 0.24.5). La version stable est relativement ancienne (j'entends déjà rire au fond) et manque de certaines fonctionnalités : je vous encourage donc à passer en Lenny si vous le pouvez, ou alors à utiliser la version de Puppet disponible sur backports.org [BACKPORTS] pour éviter certains petits tracas.
3.4 Sur FreeBSD & OpenBSD
J'ai opté pour un package pour installer Puppet sur FreeBSD et OpenBSD, les versions packagées étant assez récentes sur chacun de ces OS. Pas plus de détails, vous êtes grand :)
3.5 Sur Solaris
Puppet ne fait pas partie des packages fournis de base avec Solaris (ou comme pour moi, OpenSolaris), mais il est disponible via le fameux Blastwave [BLASTWAVE] et son utilitaire pkg-get. Je vous laisse vous référer à la documentation disponible sur le site pour l'installer, ainsi qu'à la page correspondante que le wiki du projet [PUPPETSOLARIS].
3.6 Les autres plateformes
La liste précédente n'est pas exhaustive, mais je n'ai pas testé l'installation sur d'autres plateformes. Toutefois, si Ruby est disponible dans une version assez récente sur une plateforme, il n'y a pas de raison pour que Puppet ne fonctionne pas dessus.
4. Paramétrage basique
Pour cet article, je vais utiliser un puppetmaster en Lenny et des clients en Etch, sans backports. J'ai donc installé mes machines avec des packages de la distribution.
Première étape, il faut configurer le puppetmaster (vous pouvez lancer la B.O. de Ghost in the shell en fond).
# /etc/puppet/puppet.conf
[puppetmasterd]
templatedir=/etc/puppet/templates
[main]
logdir=/var/log/puppet
vardir=/var/lib/puppet
ssldir=/var/lib/puppet/ssl
rundir=/var/run/puppet
factpath=$vardir/lib/facter
pluginsync=true
et le démarrer, via le script de la distribution :
master# /etc/init.d/puppetmaster start
Une fois cette étape franchie, il faut que les clients se connectent au serveur, et que celui-ci valide leur certificat, à l'aide de l'utilitaire puppetca.
Sur le client, on a un fichier /etc/puppet/puppet.conf qui a la forme suivante :
[puppet]
server = puppet.mondomaine.com
[main]
....
Attention, puisqu'on utilise des certificats, Puppet est très sensible au DNS. Assurez-vous que le vôtre soit bien configuré !
Une fois ce réglage fait, on contacte une première fois le serveur :
client# puppetd --test --waitforcert 60
Un petit [Ctrl-C] plus tard, le client arrête de se plaindre.
Sur le puppetmaster, on peut voir le certificat en attente de signature :
master# puppetca --list
+ client.mondomaine.com
On signe le certificat avec la commande :
master# puppetca --sign client.mondomaine.com
Hint pour fainéants : le switch --all permet de voir tous les certificats (signés ou non) ou de les signer tous d'un seul coup. Pratique lors du déploiement en conditions réelles.
Nous voilà prêts à écrire notre premier manifest !
5. Premier manifest
Pour commencer et appréhender la bête, je vais commencer par une classe qui ne fait rien, mais qui le fait bien.
# /etc/puppet/manifests/site.pp sur master
class information_class {
Exec { path => "/usr/bin:/bin:/usr/sbin:/sbin" }
exec { "echo running on $fqdn is a $operatingsystem with ip $ipaddress. Message is '$mavariable'": }
}
node "client.mondomaine.com" {
include information_class
$mavariable="pinpin vous aime"
}
On relance le puppetmaster pour être sûr que les modifications côté serveur ont bien été prises en compte. Et sur le client, on utilisera le client de façon « standalone » pour les tests avec la commande :
client# puppetd --test
notice: Ignoring cache
info: Caching catalog at /var/lib/puppet/state/localconfig.yaml
notice: Starting catalog run
notice: //information_class/Exec[echo running on client.mondomomaine.com is a Debian with ip 192.168.0.3. Message is 'pinpin vous aime']/returns: executed successfully
notice: Finished catalog run in 0.45 seconds
L'option --test permet de revalider le manifest à chaque fois sans « daemoniser » le client. Il est aussi possible d'affecter des variables pour un nœud donné, qu'on pourra réutiliser pour la suite pour réaliser des manifests plus complexes.
Maintenant, nous allons manipuler un fichier. On ajoute au fichier site.pp la classe suivante :
class glmf {
file { "/tmp/glmf":
owner => "nico",
group => "staff",
mode => 644,
content => "Vive pinpin !"
}
}
et on affecte cette classe au client (qui est appelé un nœud) en ajoutant la ligne suivante dans la définition vue précédemment :
node "client.mondomaine.com" {
include information_class
include glmf
}
On relance le client, et on vérifie que notre fichier est bien là :
client# puppetd --test
[...]
client# ls -l /tmp/glmf
-rw-r--r-- 1 nico staff 0 2008-10-08 15:01 /tmp/glmf
client# cat /tmp/glmf
Vive pinpin!
6. Gérer la diversité
Une fois vu le fonctionnement de base entre le client et le serveur, passons à quelque chose d'un petit plus compliqué. Je possède un parc hétérogène, composé de différents OS qui ont parfois des chemins différents pour certains fichiers. Même si Puppet sait différencier les OS sur lesquels il s'exécute, il n'a pas non plus la science infuse, et des mécanismes sont à notre disposition pour gérer ça. Pour étudier ça, je vais utiliser un manifest et une classe utilisés dans mon architecture :
#sudo.pp
class sudo
{
file { "sudoers":
name => $operatingsystem? {
solaris => "/opt/csw/etc/sudoers",
freebsd => "/usr/local/etc/sudoers",
default => "/etc/sudoers"
},
owner => root,
mode => 440,
# [snip]
}
}
Ici c'est la variable operatingsystem qui permet de différencier la plateforme sur laquelle on se trouve. Pour avoir la liste des variables disponibles par défaut, c’est-à-dire non définies par vous, vous pouvez lancer la commande facter. Cette variable est donc testée comme dans une instruction case (également disponible) afin de modifier la valeur concernant le chemin du fichier sudoers pour correspondre à chaque OS.
7. Distribuer des fichiers
Nous avons donc défini les paramètres du fichiers pour les sudoers, mais il serait quand même plus plaisant de le compléter aussi par la même occasion. Nous allons donc modifier notre classe de façon à avoir un contenu correspondant à nos attentes. Puppet possède un mécanisme de distribution de fichiers intégré.
Il faut modifier le fichier fileserver.conf afin de configurer ce système de distribution.
#/etc/puppet/fileserver.conf
[files]
path /etc/puppet/files
allow 192.168.0.0/16
Penser à redémarrer le service puppetmaster pour que les changements soient pris en compte. On va se servir de ce système pour fournir un fichier sudoers qui contient ce que l'on veut. Notre classe va donc ressembler à ceci :
#sudo.pp
class sudo
{
file { "sudoers":
name => $operatingsystem? {
solaris => "/opt/csw/etc/sudoers",
freebsd => "/usr/local/etc/sudoers",
default => "/etc/sudoers"
},
owner => root,
mode => 440,
source => "puppet://master.mondomaine.com/files/apps/sudo/sudoers"
}
}
Puppet utilise un MD5 du fichier pour déterminer si le fichier sur le client est le même que sur le serveur. Si le MD5 ne correspond pas, il retransfère le fichier depuis le serveur.
8. Les templates
Néanmoins, certains fichiers, même s’ils sont grandement similaires, peuvent varier d'une seule ligne entre chaque serveur. Un certain nombre de mes serveurs possèdent un Postfix installé, et ils sont tous configurés de la même façon, mais le main.cf diffère sur chaque serveur... d'une seule ligne. Pour info, ce fichier contient par exemple le relay SMTP utilisé. S’il change, il faut le changer sur tous les serveurs à la fois. Ici, le filebucket n'est pas le système adapté, on ne va pas créer un fichier par serveur ! Pour cette tâche, Puppet possède un système de modèles, appelés « templates », basé sur Erb [ERB].
On a donc la définition suivante :
file { "/etc/postfix/main.cf":
owner => root,
group => root,
mode => 644,
content => template("main.cf.erb")
}
et le fichier main.cf.erb suivant :
# See /usr/share/postfix/main.cf.dist for a commented, more complete version
smtpd_banner = $myhostname ESMTP $mail_name (Debian/GNU)
biff = no
# appending .domain is the MUA's job.
append_dot_mydomain = no
myhostname =
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
myorigin = /etc/mailname
mydestination = localhost.localdomain, localhost
relayhost = smtp.mondomaine.com
mynetworks = 127.0.0.0/8
mailbox_command =
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = all
inet_protocols = all
C'est la ligne de définition myhostname qui diffère sur chaque serveur, et ici la valeur insérée est celle du hostname du client, telle que donnée par facter. Les templates font plus que de banales substitutions, ils permettent aussi d'utiliser des structures de contrôle comme des if, des boucles itératives...
9. Utiliser un backend externe
Vous aurez sûrement remarqué que la façon d'affecter les classes est relativement fastidieuse. Heureusement, il existe un moyen de stocker ces informations dans un backend externe. En effet, Puppet accepte aussi ces informations au format YAML (Yet Another Markup Language). Voici la façon, probablement améliorable, que j'utilise : une base de données (MySQL) et un script Ruby pour sortir le flux YAML. Le script reçoit en paramètre le FQDN du client.
On ajoute donc dans la section [main] du puppet.conf du master :
node_terminus=exec
external_nodes=/opt/puppet/ext_node.rb
Bien s'assurer que les droits sur le script sont corrects.
Voici le schéma de création de la table contenant les données :
CREATE TABLE `puppets` (
`hostname` varchar(100) NOT NULL,
`classes` varchar(100) NOT NULL,
`parameters` varchar(100) NOT NULL,
`id` int(11) NOT NULL auto_increment,
PRIMARY KEY (`id`) );
Et la source du script : il ne fait pas de vérification sur les arguments, libre à vous de l'améliorer.
#!/usr/bin/env ruby
require "dbi"
dbh = DBI.connect("DBI:Mysql:mabase:mysql.mondomaine.com", "USER", "PASSWORD")
# classes
query = dbh.prepare("SELECT COUNT(*) FROM puppets WHERE hostname ='#{ARGV[0]}' AND classes <> '';")
query.execute
row=query.fetch
nbresults=row[0]
if nbresults > 0
puts "---"
puts "classes:"
query = dbh.prepare("SELECT classes FROM puppets WHERE hostname ='#{ARGV[0]}' AND classes <> '';")
query.execute
while row=query.fetch do
puts " - #{row['classes']}"
end
end
# parameters
query = dbh.prepare("SELECT COUNT(*) FROM puppets WHERE hostname ='#{ARGV[0]}' AND parameters <> '';")
query.execute
row=query.fetch
nbresults=row[0]
if nbresults > 0
puts "parameters:"
query = dbh.prepare("SELECT parameters FROM puppets WHERE hostname ='#{ARGV[0]}' AND parameters <> '';")
query.execute
while row=query.fetch do
puts " #{row['parameters']}"
end
end
# the end
query.finish
dbh.disconnect
exit(0)
La sortie produite par le script est donc un flux YAML, exemple :
master:~$ /opt/puppet/ext_node.rb client.mondomaine.com
---
classes:
- debian
- staff
- snmpd
parameters:
env_type: ipbx
deb_release: etch
Notre nœud se verra donc affecter les classes debian, staff et snmpd ainsi que les variables env_type et deb_release lorsqu'il interrogera le serveur. À vous de choisir quel backend vous préférez et comment vous le gérerez.
10. The end
Vous voilà arrivés à la fin de cette introduction à Puppet. Elle ne couvre pas, et de loin, l'intégralité des possibilités de Puppet. Il vous reste encore beaucoup de possibilités à découvrir, de problèmes sur lesquels travailler si le cœur vous en dit. Le développement de l'outil évolue vite et est très réactif. En guise de conclusion, je vous propose une modeste « interview » de Luke Kanies, le project leader de Puppet.
Bibliographie
- PUPPET : http://reductivelabs.com/trac/puppet
- BACKPORTS : http://backports.org/dokuwiki/doku.php
- BLASTWAVE : http://www.blastwave.org/ | http://www.opencsw.org/ (il y a eu scission suite à des problèmes « politiques »)
- PUPPETSOLARIS : http://reductivelabs.com/trac/puppet/wiki/PuppetSolaris