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 Rakefile1. 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 :

  1. Pour chaque tâche, on assigne la valeur --format json --out ./reports/current/#{target}.json à rspec_opts. La sous-classe ServerspecTask 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.

  2. 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.

Exemple de rapport
Exemple d'un court rapport affiché via l'interface web

En cliquant sur un test, les informations détaillées s’affichent : description du test, code, message d’erreur et trace d’exécution.

Visualistion d'une erreur
Informations détaillées sur un test en échec

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.


  1. Un Rakefile est l’équivalent d’un Makefile. Les tâches et leurs dépendances sont décrites en Ruby et seront exécutées dans l’ordre par l’utilitaire rake↩︎

  2. Cette interface est disponible sur le dépôt Git dans le répertoire viewer/↩︎