Coder à l'endroit. Pour nous, l'ordre normal des choses, c'est identifier un besoin, le tester, s'il n'est pas rempli, le coder et enfin vérifier qu'il est bien présent. Pour tous les adeptes du Test Driven Developpement, c'est l'enchainement logique des phases de test et de codage. Seulement voilà, comment faire pour dérouler ces étapes pour réaliser un conteneur Docker.

L'ensemble du code présenté dans cet article est consultable dans un projet github

Une histoire de services

Après la lecture de Twelve Factor Apps, il apparait évident qu'un conteneur est une application ou une ressource distante. Donc pour tester ce service, il suffit de se connecter sur le port réseau exposé et de parler la même langue que le service. Aujourd'hui, il est possible de ranger les applications dans 3 grandes catégories :

  • les applications web : exposent de l'http et fournissent html, css et javascript
  • les services web : exposent de l'http et fournissent des objets sérialisés principalement en json et xml
  • les autres services : base de données, xmpp, etc.

On peut affirmer, sans prendre trop de risque, qu'il est possible de tester toutes ces applications dans nos langages de développement. Capybara permet par exemple de naviguer sur des applications web, il est aussi très simple de se brancher sur des services SOAP ou REST, enfin les pilotes pour accéder à des bases de données, des files de messages où d'autres services sont quasiment tous présents dans les langages populaires.

Pour l'exemple, j'ai choisi ruby, mais je vous invites à faire de même avec votre langage favori.

Extreme Startup

C'est un petit jeu que j'utilise de temps en temps en cours. Il permet de découvrir en quelques heures quelques bonnes pratiques du développement logiciel. Pour en savoir plus je vous invite à visiter la page github

C'est l'application que nous allons "dockerifier"

Un peu d'organisation

Pour nos conteneurs, nous avons pris l'habitude de ranger notre dossier de travail de la façon suivante :

  • Le fichier Dockerfile qui permet la construction automatique du conteneur
  • Un fichier Rakefile pour automatiser l'ensemble des taches
  • Un fichier Gemfile pour gérer nos dépendances ruby
  • un dossier src pour héberger les fichiers utile à la construction du conteneur et les sources de l'application si vous en êtes l'auteur.
  • un dossier test pour ranger tous nos tests
  • un dossier .target qui sert de répertoire de travail

Des dépendances

Les fichiers Gemfile et Rakefile sont là pour automatiser les taches récurrentes, typiquement :

  • Préparer l'environnement de travail
  • Construire le conteneur
  • Lancer le conteneur
  • Lancer les tests

Les 3 premières opérations se font avec la gem rake-docker qui automatise toutes les opérations de base sur les conteneurs docker. La dernière est gérée par la gem docker-tdd. Bien évidemment dans un but pédagogique, nous aurions pu tout réécrire en plus simple. Ce sera certainement le sujet d'un nouvel article.

Rakefile en détail

require 'rake/docker_lib'

Permet de charger rake-docker

directory '.target/app'  => '.target' do
  sh "git clone git@github.com/rchatley/extreme_startup.git .target/app"
end

.target/app est le répertoire avec les sources du projet amont, cloné à la création du répertoire.

Rake::DockerLib.new("tclavier/extreme-startup") do
  prepare do
     sh "cd app/ && git pull"
  end
end

On force un pull à chaque préparation. Cela implique de toujours avoir le réseau.

task test: :build
task prepare: '.target/app'

Quelques dépendances forcées.

TDD power !

Pour que docker ne hurle pas durant la phase de build, nous pouvons faire un premier Dockerfile presque vide

from deliverous/wheezy
cmd tail -f /var/log/*

Notez la présence du tail dans la commande à démarrer par défaut, il faut en effet être certain que le conteneur se lance et reste actif.

Premier test

Pour ce premier test, on va travailler dans le fichier test/test_extreme_startup.rb. Pour lancer le test, il faut lancer le conteneur ça se fait dans la fonction containers :

describe "Extreme-startup" do
  include DockerTdd::ContainerPlugin
  def containers
    @xs = DockerTdd::Container.new "tclavier/extreme-startup", boottime: 1
  end
  ...
end

L'application doit écouter en http sur le port 3000.

it "must listen in http on port 3000" do
  open("http://#{@xs.address}:3000/")).status[0].must_equal '200'
end

Notez que l'attribut @xs contient quelques attributs fort pratique entre autres l'adresse IP attribué par docker au conteneur.

Le "rake test" nous dit que le test est bien en échec, tout va bien.

Première implémentation

Passons à l'implémentation. Donc retour dans le fichier Dockerfile. On installe ruby et unicorn

run apt-get update && \
    apt-get install -y --no-install-recommends ruby && \
    apt-get clean

On installe les dépendances ruby

run apt-get update && \
    apt-get install -y bundler sudo libxslt-dev libxml2-dev && \
    cd /opt/extreme_startup && bundle update && \
    apt-get remove -y bundler libxslt-dev libxml2-dev &&\
    apt-get autoremove -y &&\
    apt-get clean

Je fais un bundle update et pas un bundle install, en effet la version de nokogiri présente dans le Gemfile.lock ne compile plus sous Debian stable.

Et on lance le service

cmd cd /opt/extreme_startup && ruby web_server.rb

Puis on relance le test

rake build test

Et voilà ! Premier test OK.

Warmup

Pour que le premier sprint ne génère pas d'inégalité technique, il y a une option warmup qui permet d'avoir toujours la même question.

Pour faire ce test, comme le conteneur est lancé durant le setup il faut faire une seconde classe de test pour passer les bonnes options au conteneur.

@xs = DockerTdd::Container.new "tclavier/extreme-startup", env: ['WARMUP=1'], boottime: 1

Une fois encore le conteneur doit écouter sur le port 3000, mais il doit aussi déclencher une erreur au changement de round

it "must listen in http on port 3000" do
  open(url('/')).status[0].must_equal '200'
end

it "must restart to change round" do
  params = {'param1' => 'value1'}
  url = URI.parse(url('/advance_round'))
  resp = Net::HTTP.post_form(url, params)
  resp.code.must_equal '500'
end

On lance les tests et tout est vert.

Dernier test

Nous avons bien testé que le changement de round déclenche une erreur en cas de warmup. Mais dans le cas nominal que ce passe-t-il ? Nous allons donc ajouter le test suivant dans test/test_extreme_startup.rb.

it "can change round" do
  params = {'param1' => 'value1'}
  url = URI.parse(url('/advance_round'))
  resp = Net::HTTP.post_form(url, params)
  resp.code.must_equal '200'
end

Il suffit de lancer le test pour voir que les 4 tests sont vert.

Conclusion

Nous avons fait 4 tests dans 2 classes avec une implémentation simple. Ce qui nous a permis de voir une bonne partie de notre environnement de test. Nous avons en particulier appris à :

  • déclarer un conteneur dans le fichier Rakefile
  • aller chercher le code source dans un autre projet durant la phase de préparation
  • déclarer des variables d'environnement à l'exécution du conteneur
  • utiliser la lib Net::HTTP pour interroger le conteneur published Ce qui couvre une grande partie de nos tests.

Photo par Med


Comments

comments powered by Disqus