Tester son infrastructure avec serverspec
Vincent Bernat
La configuration d’une ferme de serveurs peut être vérifiée par des outils de déploiement tels que Puppet, Chef, Ansible ou Salt. Ils permettent à un administrateur de décrire la configuration cible et de s’assurer que celle-ci est correctement appliquée. Il est également possible d’obtenir des rapports sur les serveurs dont la configuration n’est pas conforme.
serverspec exploite quant à lui RSpec, un outil de test pour le langage Ruby fréquemment utilisé dans le développement piloté par les tests. Il permet de tester l’état des serveurs à travers une connexion SSH.
Pourquoi utiliser un tel outil supplémentaire ? Certaines choses sont plus faciles à tester qu’à décrire via un changement de configuration, comme par exemple le bon fonctionnement d’un applicatif que l’on teste en vérifiant qu’il écoute sur un port donné.
Premiers pas#
Une bonne connaissance de Ruby peut aider mais n’est pas essentielle pour utiliser serverspec. L’écriture des tests se fait généralement dans un langage presque naturel. Toutefois, voici deux ressources intéressantes pour apprendre rapidement l’essentiel de Ruby :
Le site web de serverspec contient une courte introduction présentant l’essentiel. je vous invite à la parcourir rapidement. À titre d’exemple, voici un test qui vérifie qu’un service est bien en écoute sur le port 80 :
describe port(80) do it { should be_listening } end
Si vous voulez repérer les serveurs qui n’ont pas été mis à jour en Debian Wheezy, le test suivant fera l’affaire :
describe command("lsb_release -d") do it { should return_stdout /wheezy/ } end
Le test suivant permet de vérifier la bonne valeur du paramètre
miimon
de l’interface bond0
, uniquement si cette dernière existe :
has_bond0 = file('/sys/class/net/bond0').directory? # miimon should be set to something other than 0, otherwise, no checks # are performed. describe file("/sys/class/net/bond0/bonding/miimon"), :if => has_bond0 do it { should be_file } its(:content) { should_not eq "0\n" } end
serverspec dispose d’une
documentation complète des resources disponibles (telles que
port
et command
) qui peuvent être utilisées après le mot-clé
describe
.
Si un test est trop complexe pour s’exprimer de cette manière, il est
possible d’utiliser des commandes arbitraires. Dans l’exemple
ci-dessous, nous vérifions que memcached
est configuré pour utiliser
quasiment toute la mémoire disponible sur le système :
# We want memcached to use almost all memory. With a 2GB margin. describe "memcached" do it "should use almost all memory" do total = command("vmstat -s | head -1").stdout # ❶ total = /\d+/.match(total)[0].to_i total /= 1024 args = process("memcached").args # ❷ memcached = /-m (\d+)/.match(args)[1].to_i (total - memcached).should be > 0 (total - memcached).should be < 2000 end end
Bien que plus complexe, ce test est toujours assez simple à
comprendre. En ❶, le résultat de la commande vmstat
est récupérée
pour être comparé avec une valeur obtenue en ❷ via une autre ressource
fournie par serverspec.
Utilisation avancée#
La documentation de serverspec fournit quelques exemples d’utilisation plus avancée, comme la possibilité de partager des tests dans une ferme de serveurs ou d’exécuter plusieurs tests en parallèle.
J’ai mis en place un dépôt GitHub destiné à être utilisé comme base pour obtenir les fonctionnalités suivantes :
- assigner des rôles à chaque serveur et des tests à chaque rôle,
- exécution parallèle,
- génération et visualisation de rapports de test.
Taxinomie des serveurs#
Par défaut, serverspec-init
fournit une base où chaque serveur
dispose de son répertoire avec son propre ensemble de
tests. Toutefois, rien ne nous empêche de grouper les tests selon des
rôles. serverspec se concentrant sur l’exécution des tests sur un
serveur donné, la décision des serveurs et des tests à exécuter sur
ceux-ci est déléguée à un Rakefile
1. Ainsi, au lieu de se
baser sur le contenu d’un répertoire, la liste des serveurs peut être
extraite depuis un fichier (ou un serveur LDAP ou toute autre
source). Pour chacun d’eux, la liste des rôles associés est
calculée via une fonction :
hosts = File.foreach("hosts") .map { |line| line.strip } .map do |host| { :name => host.strip, :roles => roles(host.strip), } end
La fonction roles()
est chargée de fournir les rôles assignés à un
serveur. Elle peut par exemple se baser sur le nom du serveur :
def roles(host) roles = [ "all" ] case host when /^web-/ roles << "web" when /^memc-/ roles << "memcache" when /^lb-/ roles << "lb" when /^proxy-/ roles << "proxy" end roles end
Le code ci-dessous crée ensuite une tâche pour chaque serveur. Voyez,
en ❷, comment les rôles associés au serveurs vont influencer les tests
exécutés. De plus, en ❶, une tâche server:all
va permettre
de lancer les tests sur tous les serveurs.
namespace :server do desc "Run serverspec to all hosts" task :all => hosts.map { |h| h[:name] } # ❶ hosts.each do |host| desc "Run serverspec to host #{host[:name]}" ServerspecTask.new(host[:name].to_sym) do |t| t.target = host[:name] # ❷: Build the list of tests to execute from server roles t.pattern = './spec/{' + host[:roles].join(",") + '}/*_spec.rb' end end end
La commande rake -T
permet de lister les tâches ainsi créées :
$ rake -T rake check:server:all # Run serverspec to all hosts rake check:server:web-10 # Run serverspec to host web-10 rake check:server:web-11 # Run serverspec to host web-11 rake check:server:web-12 # Run serverspec to host web-12
Enfin, il convient de modifier spec/spec_helper.rb
pour expliquer à
serverspec que le serveur à tester se trouve dans la variable
d’environnement TARGET_HOST
.
Exécution parallèle#
Par défaut, chaque tâche est exécutée une fois la précédente
terminée. Avec de nombreux serveurs à tester, cela peut prendre un
certain temps. rake
permet d’exécuter des tests en parallèle en
combinant l’option -j
et l’option -m
:
$ rake -j 10 -m check:server:all
Rapports de test#
Pour chaque serveur, rspec
est exécuté. Par défaut, la sortie
produite ressemble à cela :
$ rake spec env TARGET_HOST=web-10 /usr/bin/ruby -S rspec spec/web/apache2_spec.rb spec/all/debian_spec.rb ...... Finished in 0.99715 seconds 6 examples, 0 failures env TARGET_HOST=web-11 /usr/bin/ruby -S rspec spec/web/apache2_spec.rb spec/all/debian_spec.rb ...... Finished in 1.45411 seconds 6 examples, 0 failures
Avec plusieurs dizaines ou centaines de serveurs, la lisibilité est
plutôt médiocre, d’autant qu’en cas d’exécution en parallèle des
tests, les lignes se retrouvent mélangées. Heureusement, rspec
permet de sauvegarder le résultat au format JSON. Il est
ensuite possible de consolider ces résultats en un unique
fichier. Tout ceci peut se faire dans le Rakefile
:
-
Pour chaque tâche, on assigne la valeur
--format json --out ./reports/current/#{target}.json
àrspec_opts
. La sous-classeServerspecTask
se charge de cette partie ainsi que de la transmission du nom du serveur et de la production d’une sortie plus compacte en en couleurs sur la console lors du déroulement du test. -
On ajoute une tâche pour consolider les fichiers JSON en un seul rapport. Le code source de tous les tests est également ajouté à ce rapport afin de le rendre indépendant des changements qui peuvent survenir par la suite. Cette tâche est exécutée automatiquement après la dernière tâche liée à serverspec.
N’hésitez pas à jeter un œil au fichier Rakefile
complet
pour obtenir plus de détails.
Enfin, une interface web minimaliste permet d’afficher le rapport ainsi généré2. Elle montre le résultat des tests sous la forme d’une matrice où les tests qui ont échoué sont représentés par une pastille rouge.
En cliquant sur un test, les informations détaillées s’affichent : description du test, code, message d’erreur et trace d’exécution.
J’espère que ces quelques ajouts au-dessus de serverspec permettent d’en faire une corde supplémentaire à l’arc des outils IT. Elle se situerait à mi-chemin entre l’outil de déploiement et l’outil de supervision.