samedi 11 février 2017

Introduction au pipeline de Jenkins

J'utilise Jenkins depuis quelques années. Un peu comme tout le monde je suis passé à la 2 au moment de sa sortie. Comme beaucoup, je n'ai pas vraiment vu de grosse différence avec les versions 1.6xx (si ce n'est que mes jobs ne fonctionnait plus mais ça, c'est une autre histoire).
Au bout d'un moment je suis tombé sur une pépite : les pipeline/workflow de Jenkins.

Mais avant de parler de ça, je vais vous parler de ce que je faisais traditionnellement.

Création de job et parallélisation


Pour résumé, je gère énormément d'installations ou de compilation en tout genre et Jenkins me sert d'esclave personnel : il lance des playbooks Ansible, des shells Unix ou des compilations maven (beurk !). Je colle généralement ces scripts dans le repository Git des projets et je les appelle depuis Jenkins. Le petit problème est que j'ai quand même à créer ces jobs et que je dois gérer l'enchaînement de ces éléments.

Fonction de la difficulté de la tâche, je passe soit par des déclenchements de jobs (natif Jenkins) soit carrément des jobs permettant d'orchestrer plusieurs lancements (plugin Multijob).

Pour ceux qui ne le connaîtrait pas, le plugin Multijob est tout de même bien fait et permet de très facilement gérer des lancements d'opérations en parallèle assez complexe (cf capture d'écran).

Exemple de Multijob
Malgré tout, ça implique que pour chaque phase, vous aurez un job à définir (qui ne vont pas contenir grand chose si ce n'est un appel à un playbook Ansible) ainsi qu'un job supplémentaire permettant de piloter le lancement des autres jobs et ceci depuis l'interface graphique de Jenkins.

Vous l'aurez compris, ça fait beaucoup d'opérations manuelles à réaliser et vous n'avez pas vraiment de moyen de faire ça proprement (sauf à bricoler les fichiers XML de Jenkins). Autre problème, vous ne savez pas qui modifie quoi à quel moment. Vous n'avez aucun moyen de savoir si une modification a été réalisée par quelqu'un qui pourrait expliquer le gros plantage de compilation au moment de la release de votre projet.

C'est là qu'on va voir en quoi les pipelines/workflows Jenkins peuvent répondre à ce type de problème.

Les pipelines sous Jenkins


Première chose à savoir, vous n'avez plus à aller dans l'interface Jenkins pour mettre à jour vos jobs : vous créez votre Job dans l'interface Jenkins (vivement qu'on puisse scripter ça proprement ...), vous associez votre repository de code, vous rentrez le nom du fichier décrivant le pipeline et c'est à peu près tout. Et le reste me direz-vous ? On va voir que le pipeline remplace ça avantageusement :

  • Besoin de récupérer la dernière version du code ? Utilisez le mot clé checkout scm ;
  • Besoin de lancer une tâche ? Le mot clé sh est là pour ça ;
  • Lancement en parallèle ? La fonction parallel est là pour ça !
Vous l'aurez compris, ça permet de remplacer à la fois vos micro jobs ainsi que le job chapeau de pilotage des autres jobs.
Autre chose, c'est du groovy ce qui permet de faire des choses relativement propre par rapport à du shell Unix. Il est même possible de faire des librairies de fonction pour réutiliser du code. C'est magique je vous avais dit !

Exemple de pipeline


Avant d'aller plus loin, voici un petit exemple de pipeline :
node() {
  // Étape 1
  stage('etapes-1') {
    parallel(
      'etape-1.1': {
        echo "Job 1 de l'étape 1"
      },
      'etape-1.2': {
        echo "Job 2 de l'étape 1"
      },
    )
  }
  // Étape 2
  stage('etapes-2') {
    echo "Job 1 de l'étape 2"
  }
  // Étape 3
  stage('etapes-3') {
    parallel(
      'etape-3.1': {
        // Cette opération doit prendre moins de 10 secondes
        timeout(time: 10, unit: 'SECONDS') {
          echo "Job 1 de l'étape 3"
        }
      },
      'etape-3.2': {
        echo "Job 2 de l'étape 3"
      },
      'etape-3.3': {
        echo "Job 3 de l'étape 3"
      },
    )
  }
}
Les sections parallel vous permettent de lancer x jobs en même temps. Jenkins attendra gentiment que tous les jobs finissent leur lancement. Vous avez peur qu'un de ces petits coquins ne prenne trop de temps et ne vous rende jamais la main ? Ajoutez une section timeout et vous êtes sauvé !
Vous avez envie de gérer la reprise sur incident ? C'est du groovy ! Ajoutez une section try/catch !

Intégration dans Jenkins


On a vu à quoi ressemblait le code de notre pipeline. On va maintenant voir comment l'intégrer dans Jenkins :


On y voit plusieurs opérations :
  • Création du job pipeline dans Jenkins ;
  • Lancement du job et visualisation du résultat ;
  • Affichage dans la nouvelle interface Jenkins Blue Ocean.
Ici, le code a été intégré directement dans l'interface Jenkins. On peut utiliser ce mécanisme dans le cadre d'une mise au point ou de test mais si vous voulez travailler proprement, rien n'égale l'intégration dans un repository Git. Ça tombe bien, on va voir ça dans le chapitre suivant.

Stockage du pipeline dans Git


Première chose, nous allons créer un repository Git avec notre code pipeline :
Création du repository :

$ mkdir git/test
$ cd git/test
$ git init .

Intégration du code :

$ git add Jenkinsfile
$ git commit -m "Ajout fichier Jenkinsfile d'exemple."
[master (commit racine) ccc0630] Ajout fichier Jenkinsfile d'exemple.
 1 file changed, 34 insertions(+)
 create mode 100644 Jenkinsfile

Reste maintenant à intégrer notre code dans l'interface de Jenkins de la manière suivante :


NB : Si vous appelez votre fichier de pipeline autrement que Jenkinsfile, il faudra le préciser au moment de la création de votre job.

Pour finir


Voilà, ça sera tout pour l'instant. Ça donne déjà un bon aperçu de ce qu'il est possible de faire. Dans un prochain article, j'essaierai de vous présenter une autre fonction très sympathique des pipelines : l'écriture de librairie pipeline (avec le mot clé load). En attendant, bonne intégration continue !

samedi 28 janvier 2017

Chiffrement/déchiffrement des mots de passe de vos scripts gérés avec Ansible


Pour les paresseux, un exemple d'implémentation est disponible à l'adresse suivante : https://github.com/Yannig/yannig-ansible-playbooks/tree/master/scripts-vault

Mais pourquoi chiffrer d'abord ?

L'automatisation c'est bien mais des fois vous pouvez rencontrer quelques petits problèmes. Un que je rencontre régulièrement vient de la présence de mot de passe en clair dans mes scripts d'administration WebLogic/Oracle etc. Pour comprendre le problème, prenons l'extrait de playbook Ansible suivant :

- name: "Dépot d'un template de création"
  template:
    src: "template.sh.j2"
    dest: "/tmp/creation.sh"
    mode: "755"
  register: _

- name: "Exécution du template"
  shell: "/tmp/creation.sh"
  when: _.changed|d('no')|bool

Le déroulement est assez simple : on dépose un template sur la machine et on exécute le script si ce dernier a changé (suite à une mise à jour ou simplement parce que le script n'existait pas). Problème : si ce script contient un mot de passe, il est en clair sur la machine ce qui ne fait pas très sérieux.

Nous allons voir comment faire en sorte que les mots de passe soient à disposition sur la machine distante au moment de leur exécution sans pour autant les mettre en clair ou en tout cas rendre très difficile leur exploitation. Pour cela, nous passerons par une clé de chiffrement dans une variable d'environnement et nous verrons comment récupérer ça depuis différents langages (ici Python, Java et shell Unix).

Utilisation du 3DES pour chiffrer

Le principe va être le suivant : on génère une clé de chiffrement sur 24 caractères, nous chiffrons le mot de passe avec et nous la mettons à disposition sur la machine distante que nous déchiffrons via l'utilisation d'une variable d'environnement.

Cette clé peut-être positionné dans votre inventaire ou via un générateur de clé avec une graine connue (seed). Cette graine peut venir du nom de votre serveur, une variable déjà existante propre à votre plateforme, etc. J'en parlerai sûrement une prochaine fois pour voir ce qu'il est possible de faire.

Mais revenons à nos moutons. Première chose à faire : créer le programme qui nous permettra de faire le chiffrement à partir de cette clé. Ci-dessous un programme python permettant de faire du chiffrement 3DES :

import sys, os, base64
from Crypto.Cipher import DES3

BS    = 8
pad   = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS) 

key = os.getenv('DES_KEY')
des = DES3.new(key, DES3.MODE_ECB)
print(base64.b64encode(des.encrypt(pad(sys.argv[1]))))

La clé de chiffrement se trouve stockée dans la variable d'environnement DES_KEY. Ci-dessous un exemple d'utilisation de ce mécanisme :

$ DES_KEY=111111111111111111111111 ./chiffre.py abcdefghijklmnopqrstuvwxyz
eHPtqHbKD+oGdodkRUxdwo8Z4EcsTUBrrSeCYcBIQ0g=

Relançons notre programme en changeant notre clé :

$ DES_KEY=111111111111111111111112 ./chiffre.py abcdefghijklmnopqrstuvwxyz
vD37nk4ME9n5BUa9pLH0zrHGWl2xUkLRlLhEwfkFmos=

Parfait, on voit bien que le chiffrement de notre mot de passe change complètement lorsqu'on change la clé.

Déchiffrons notre message

Nous arrivons à chiffrer. Écrivons maintenant le petit bout de code python qui va nous permettre de récupérer le mot de passe :

import sys, os, base64
from Crypto.Cipher import DES3

BS    = 8
unpad = lambda s: s[0:-ord(s[-1])]

key = os.getenv('DES_KEY')
des = DES3.new(key, DES3.MODE_ECB)
print(unpad(des.decrypt(base64.b64decode(sys.argv[1]))))

Faisons maintenant quelques tests :

$ DES_KEY=111111111111111111111112 ./dechiffre.py vD37nk4ME9n5BUa9pLH0zrHGWl2xUkLRlLhEwfkFmos=
abcdefghijklmnopqrstuvwxyz

Youpi ! On récupère notre mot de passe !

Maintenant que nous avons notre code python, voyons comment récupérer ce mot de passe depuis un autre langage.

Déchiffrement Java

Si comme moi vous n'avez pas été sage et que vous faites de l'administration Java, vous aurez peut-être besoin de récupérer ce mot de passe depuis un programme en Java. Le bout de code suivant devrait nous aider à réaliser cette opération :

// Récupère un mot de passe (ou autre chose) chiffré avec une clé 3DES et renvoie
// la chaîne à l'aide de la clé contenue dans la variable d'environnement DES_KEY

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

// Decrypt a password
class dechiffre {
  public static void main(String [] args) throws Exception {
    System.out.println(decrypt(args[0]));
  }
  public static String decrypt(String password) throws Exception {
    String env_key = System.getenv("DES_KEY");

    Cipher out_cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
    SecretKeySpec key = new SecretKeySpec(env_key.getBytes(), "DESEDE");
    out_cipher.init(Cipher.DECRYPT_MODE, key);
    return new String(out_cipher.doFinal(Base64.getDecoder().decode(password)), "UTF-8");
  }
}

Notons au passage la qualité première de Java : sa concision.

Lançons maintenant notre petit programme et voyons ce que ça donne :

$ DES_KEY=111111111111111111111112 java dechiffre vD37nk4ME9n5BUa9pLH0zrHGWl2xUkLRlLhEwfkFmos=
abcdefghijklmnopqrstuvwxyz

C'est pas trop mal. Voyons ce que ça donne en changeant la clé :

DES_KEY=111111111111111111111111 java dechiffre vD37nk4ME9n5BUa9pLH0zrHGWl2xUkLRlLhEwfkFmos=
Exception in thread "main" javax.crypto.BadPaddingException: Given final block not properly padded
       at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:975)
       at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:833)
       at com.sun.crypto.provider.DESedeCipher.engineDoFinal(DESedeCipher.java:294)
       at javax.crypto.Cipher.doFinal(Cipher.java:2165)
       at dechiffre.decrypt(dechiffre.java:19)
       at dechiffre.main(dechiffre.java:11)


Magnifique, une superbe stacktrace Java pour nous indiquer que ça ne fonctionne pas bien !

Et si on faisait la même chose en shell ?

Pas mal mais j'écris rarement mes programmes de création d'environnement en Java. Je me suis donc posé la question s'il serait possible de faire la même chose en shell. En effet, j'ai régulièrement besoin de déposer des scripts shell afin de procéder à la création d'objets divers avec des morceaux de mot de passe dedans. Là aussi, j'aurai bien aimé pouvoir chiffrer mes mots de passe sur la machine distante.

Cette fois ci, la solution vient d'openssl. Petit bonus, il s'agit d'un programme relativement courant à trouver sur une machine Linux (sauf si vous êtes sur une image Docker où en général il est supprimé). Nous allons voir comment l'utiliser dans notre contexte :

$ export DES_KEY=111111111111111111111112
$ echo vD37nk4ME9n5BUa9pLH0zrHGWl2xUkLRlLhEwfkFmos= | \
    openssl des-ede3 -d -a -K $(echo -n "$DES_KEY" | xxd -pu)
abcdefghijklmnopqrstuvwxyz

A noter que j'ai un peu tâtonné pour trouver le bon algo (ici des-ede3). En effet, avec l'algo des3, ça fonctionnait presque mais ça plantait sur des chaînes de plus de 8 caractères. Autre problème : openssl veut une chaîne au format hexadécimal. Il faut donc faire cette transformation avec la chaîne de la clé avec xxd (et l'option -pu). Autre point d'attention lors de la transformation : faire attention à ne pas rajouter de retour à la ligne à xxd sinon vous fausserez votre clé.

En dehors de ça, mission accomplie, nous avons notre principe.

Pour conclure

Nous avons vu comment faire le chiffrement reste maintenant à combiner ça avec votre outil de gestion de conf préféré (Ansible, Puppet etc.). L'astuce consistera à passer la variable d'environnement au moment du lancement du script. Ci-dessous un exemple de passage de variable :

- name: "Dépot script"
  template:
    scr: "test.sh.j2"
    dest: "/tmp/monscript.sh"
  vars:
    encrypted_value: "vD37nk4ME9n5BUa9pLH0zrHGWl2xUkLRlLhEwfkFmos="
- name: "Rafraichir configuration"
  shell: "/tmp/monscript.sh"
  environment:
    DES_KEY: "{{des_key}}"

Ci-dessous le contenu du template test.sh.j2 :

#!/bin/bash
mdp=$(echo {{encrypted_value}} | openssl des-ede3 -d -a -K $(echo -n "$DES_KEY" | xxd -pu) &> /dev/null) && echo $mdp

Depuis Ansible, tout va bien, le shell se lance bien. Essayons de lancer maintenant ce script depuis la machine :

$ ./test.sh

Le script n'affiche rien. Si on vérifie la valeur du code retour, on se rend compte qu'il y a eu un problème lors du lancement du script :
$ echo $?
1

Parfait ! Nous sommes incapable d'obtenir le mot de passe directement depuis la machine : mission accomplie ! Reste maintenant à généraliser ce mécanisme.

Petite astuce au passage : passer par l'utilisation de filtre Ansible pour gérer le chiffrement de vos mots de passe. Ça simplifiera grandement votre travail.

mercredi 25 mai 2016

Ecriture de filtre Ansible

Écriture de filtre avec Ansible

Ansible est un merveilleux outils pour gérer la mise à jour de vos serveurs. Un gros avantage de ce produit et de permettre d'étendre ses capacités assez facilement à l'aide d'un mécanisme de plugin. Vous pourrez retrouver un petit article que j'avais écrit sur la notion de module à l'emplacement suivant : Écriture de module avec ansible.

Aujourd'hui, je vais vous présenter un autre mécanisme Ansible : les filtres.

Pour info, la notion de filtre vient du monde Jinja. Comme ce moteur est au coeur du fonctionnement d'Ansible, il est donc très facile d'utiliser les mêmes mécanismes chez ce dernier.

Dans ce qui va suivre, nous allons voir comment écrire un filtre spécifique et comment l'utiliser au sein d'Ansible.

Écriture du filtre

Avant toute chose, nous allons devoir écrire une fonction python qui prendra un argument (on peut en mettre plusieurs mais il faudra les rajouter à la suite du filtre entre parenthèse). Cet argument va contenir la chaîne en entrée à transformer. Ci-dessous un exemple de fonction permettant de formater la date du jour à l'aide d'une chaîne de caractère (un peu comme le ferait la commande Unix date) :

def strftime(string_format):
    '''
    Renvoie une chaîne formatée par strftime (ex : strftime('%Y') => 2016)
    '''
    return time.strftime(string_format, time.localtime())

Intégration dans Ansible

Rajoutons maintenant le nécessaire pour qu'Ansible puisse utiliser cette fonction :

# -*- coding: utf-8 -*-
# Filtre strftime

from __future__ import absolute_import
from ansible import errors

import time

def strftime(string_format):
    '''
    Renvoie une chaîne formatée par strftime (ex : strftime('%Y') => 2016)
    '''
    return time.strftime(string_format, time.localtime())

class FilterModule(object):
    ''' On renvoie tout ça dans un objet filtre '''

    def filters(self):
        return { 'strftime': strftime }

Prise en compte

Mettez tout ceci dans le fichier strftime.py (par exemple). Il ne nous reste plus qu'à déclarer l'emplacement du chemin des plugins custom dans le fichier /etc/ansible/ansible.cfg (ou sinon dans le fichier .ansible.cfg dans votre répertoire home) :

filter_plugins = /emplacement/de/mes/filtres/custom

Tout est prêt, nous allons pouvoir passer à l'utilisation du filtre.

Et maintenant, les tests !

Lançons maintenant un petit test pour vérifier que tout ceci fonctionne avec la date du jour par exemple :

ansible -m debug -a msg="{{'%Y%m%d'|strftime}}" localhost
localhost | SUCCESS => {
   "msg": "20160525"
}

Rajoutons maintenant l'heure :

ansible -m debug -a msg="{{'%Y%m%d %H:%M'|strftime}}" localhost
localhost | SUCCESS => {
    "msg": "20160525 11:06"
}

Ça semble plutôt bien marcher !

Le petit mot de la fin

Vous pouvez maintenant utiliser ce mécanisme où bon vous semblera (déclaration de variable, contenu des paramètres Ansible). Attention toutefois à ne pas oublier la déclaration du répertoire des filtres.

Voilà, ça sera tout pour aujourd'hui !

lundi 9 mai 2016

Prototypage d'infra à l'aide de Docker et Ansible

J'utilise depuis quelques temps l'outil Ansible pour gérer mes environnements. Il y a peu, j'ai eu besoin de mettre en place des tests automatiques. Comme toujours, c'est posé la question de savoir où lancer ces fameux tests.

La réponse classique aurait de se baser sur des VM mais comme d'habitude, j'avais des difficultés à avoir ces machines :

  • Il faut demander une entrée DNS ;
  • Il me faut une adresse IP ;
  • Il faut l'installer ;
  • Attention, l'hyperviseur est bientôt saturé ;
  • Mais qui va payer ?
  • Mais qui va sauvegarder ?

Bref, comme toujours, la VM de test est presque aussi compliquée à installer qu'une VM de production et je n'arrive pas à expliquer aux gens qu'il s'agit d'un besoin jetable.

Dans ce cadre, j'ai repensé aux quelques tests que j'avais réalisé sur Docker il y a quelques temps et je me suis dit que ce produit pourrait très bien répondre à mon besoin.

Disclaimer : Ce qui va suivre n'est absolument pas conseillé dans le cadre d'une production. Ce besoin s'inscrit dans un besoin de prototypage d'infra. Ce mode de fonctionnement n'est pas assez stable - du moins pour l'instant - dans des besoins sérieux.

Bref, je demande une VM avec Docker dessus et partant de là, je peux commencer à travailler.

Le principe

Mon besoin est de pouvoir me connecter à un container en faisant mes opérations comme si j'étais dans une vraie machine. Pour se faire, je suis quand même obligé de lancer un process pour faire croire à Docker et Ansible que quelque chose tourne. Dans un premier temps, j'ai fait quelques tests en lançant avec des commandes sleep ou tail -f /dev/null. Le problème est que lorsque j'ai voulu démarrer mes services à l'aide de systemd, j'ai eu quelques plantages.

Problème systemd dans Docker

L'un des premiers problèmes que j'ai abordé a été la gestion des spécificités de systemd. En effet, dans le cas des distributions modernes (centos 7, ubuntu 15.10 ou 16.04), le démarrage est entièrement géré par systemd. Le seul petit hic étant que ce dernier change entièrement la façon de gérer les services. C'est assez marrant d'ailleurs, ce changement m'a fait pensé à la refonte des services dans Solaris 10 (attention, un troll c'est glissé dans ce paragraphe).

Il faut donc embarquer systemd dans votre container Docker. Mais d'un autre côté, ça me permet de virer la commande tail -f.

Ce qu'il y a de rigolo dans tout ça, c'est qu'il est plus simple de gérer un centos 6 dans Docker qu'une version 7.

Problème systemd dans Docker (suite)

Vous allez me dire, dépose un fichier /etc/init.d lance le avec un shell et ça fonctionnera pareil. Et bien non puisque même si vous passez par le script directement, systemd l'intercepte et vous vous retrouvez avec le magnifique message d'erreur suivant :

Failed to get D-Bus connection: Operation not permitted

Le problème vient du fait que systemd a besoin d'accéder à tout un tas de chose pour gérer le contexte de lancement des process. La solution est donc de lancer le container en mode privileged avec les problèmes de sécurité que ça peut entraîner. Encore une fois, il ne s'agit pas d'un problème dans mon contexte mais je vous le déconseille vivement dans un autre contexte (en production par exemple).

Problème systemd dans Docker (suite)

Le lancement du premier container se passe très bien mais rapidement, je constate que certains process getty se lancent en se goinfrant 100% de CPU. En creusant un peu, je me rends compte que systemd (sous centos 7) lance automatiquement le service getty@tty1. Le premier lancement n'a pas de conséquence mais le problème apparaît au lancement des containers suivants. Ici, la solution est relativement simple : désactivation du service à la construction de l'image Docker. Ci-dessous l'instruction dans Dockerfile permettant de le réaliser :

RUN systemctl disable getty@tty1

Redémarrage des containers

Ayant fini par faire fonctionner mes playbooks, je me suis dit qu'il était temps de tester un arrêt/relance du container pour voir comment ça se comporte. Là encore, ça ne fonctionne pas vraiment bien. Ne comptez donc pas (pour l'instant) utiliser systemd pour gérer le démarrage des services de vos containers.

Connexion au container

J'en ai parlé il y a peu mais là, pas de problème : Ansible dispose d'un connecteur docker natif. Il faut donc se débrouiller pour affecter la valeur docker à la variable ansible_connection et le tour est joué.

Modélisation du réseau

Un autre truc qui a bien fonctionné est la capacité de Docker à manipuler des réseaux virtuels. Par le passé, sous Linux, cette partie pouvait être vraiment une gageure. J'avais écrit un article sur le sujet à l'époque. Il vous fallait gérer l'ajout de bridge, gérer la création des interfaces virtuelles de vos VM. Ici, rien de ce genre, vous créez votre réseau et Docker se charge du reste :

  • Création du bridge ;
  • Visibilité des containers dans le même réseau.

Ci-dessous un exemple de lancement de la création d'un réseau Docker :

docker network create demo

Tous les containers que nous rajouteront dans ce réseau par la suite auront automatiquement une résolution DNS disponible.

Pour conclure

Vous l'aurez compris, systemd et Docker ne sont pas les meilleurs amis du monde. Je trouve ça d'autant plus dommage que je pense qu'il y aurait clairement quelque chose d'intéressant à avoir si le support de ces deux produits avaient été meilleurs. Si comme moi vous avez utilisé des solutions à base de zone Solaris, vous pouvez passer votre chemin, il ne s'agit pas du même niveau de stabilité.

Une autre leçon à retenir de tout ceci est que Docker n'est clairement pas un remplacement d'une virtualisation classique. Ne partez surtout dans l'idée de réutiliser les mêmes outils sans passer du temps sur les adaptations préalables. Cette bascule doit absolument se faire dans le cadre d'une refonte de votre application et de sa gestion.

Pour le reste, l'outil Docker est très pratique à utiliser et vous permettra de prototyper très rapidement votre infra sans trop d'effort.

Un autre outil intéressant que je suis en train de creuser semble être Packer. En effet, cet outil permet de réutiliser vos playbooks pour la création de vos images Docker. Le gros avantage est que vous n'avez donc qu'un travail minimum pour créer indifféremment des machines classiques ou des containers et en vous garantissant d'avoir la même chose partout.

Attention toutefois, il s'agit pour moi d'une situation transitoire si vous avez décidé de partir sous Docker. Il reste très important de mon point de vue de simplifier vos installations et de passer par du Dockerfile. Idéalement, Ansible ne devrait vous servir que pour l'enchaînement de vos différentes tâches ou dans la gestion des machines qui ne sont pas dans le cloud (base de données par exemple) ou pour la gestion de l'infra Docker.

Concernant le code Ansible/Docker, vous pourrez retrouver des exemples dans le repository sur github du repository Meetup Ansible Paris #9. Bonne lecture !

dimanche 24 janvier 2016

Installation de docker et utilisation avec Ansible

Pour fêter la sortie d'Ansible v2, je vous propose un petit article sur l'utilisation de mes deux passe-temps du moment : Docker et Ansible

Le premier est très à la mode dans le monde de l'intégration continue. Sans rentrer dans les détails, j'y ai vu un moyen assez simple de faire des tests sur plusieurs types de distribution sans avoir à trop me casser la tête. D'autant que la dernière version d'Ansible embarque un connecteur natif docker.

Nous allons donc voir comment faire fonctionner tout ceci ensemble.

Installation de docker

De ce côté, rien de particulier à signaler. Sous Ubuntu, il vous faudra rajouter une source (en fonction de votre version d'Ubuntu) et sous CentOS, il vous faudra rajouter le repository extras dans vos dépendances.

Ci-dessous les instructions sous Ubuntu :

  • Suivre les instructions dans l'article d'installation de docker sous Ubuntu.
  • Installation de la librairie docker-py (nécessaire pour la communication d'Ansible avec docker) : pip install docker-py

Sous CentOS :

Voilà, passons à la suite.

Installation d'Ansible

Il vous faudra tout d'abord une version installée d'Ansible v2 (par la suite, j'utiliserai la 2.0.0.2). Pour vous en assurer, vous pouvez lancer la commande suivante : ansible --version

Pour installer une version, je vous laisse consulter l'article d'installation d'Ansible.

Création de container docker

Nous allons tout d'abord créer un playbook qui va nous permettre de créer nos images docker :

- name: "create docker container"
  hosts: docker-server
  gather_facts: no
  tasks:
    - docker: name={{item}} image=centos
              command="sleep 3600" state=started
      with_items: groups['docker-container']

NB : tous les exemples dans cet article sont récupérables à l'adresse suivante

L'astuce de ce playbook est de s'appuyer sur la variable groups['docker-container'] avec un with_items pour indiquer la liste des containers docker à créer.

Nous nous appuierons également sur le fichier d'inventaire suivant :

[docker-server]
localhost

[docker-container]
docker1
docker2

[docker-container:vars]
ansible_connection=docker
ansible_remote_user=root

Nous allons ensuite lancer la création de nos containers :

ansible-playbook -i hosts docker-example/create-docker.yml

Vous devriez obtenir, si tout se passe bien, le résultat suivant :

PLAY [create docker container] *************************

TASK [docker] ******************************************
changed: [localhost] => (item=docker1)
changed: [localhost] => (item=docker2)

PLAY RECAP ********************************************
localhost    : ok=1    changed=1    unreachable=0    failed=0   

Un rapide coup d'oeil avec docker ps nous indiquera ce qui vient d'être créé :

CONTAINER ID  IMAGE   COMMAND       CREATED         [...]   NAMES
d8bbda43b241  centos  "sleep 3600"  50 seconds ago  [...]   docker2
e34ed4b637b9  centos  "sleep 3600"  50 seconds ago  [...]   docker1

Utilisation des containers dans Ansible

Pour ainsi dire, le travail est terminé. Prenons tout de même un exemple de playbook :

- name: "create docker container"
  hosts: docker-container
  gather_facts: no
  tasks:
    - copy: dest=/tmp/{{item}} content=test
      with_items: [ test1, test2, test3 ]

Lançons ce playbook pour voir ce que nous allons obtenir :

PLAY [create docker container] *******************************

TASK [copy] **************************************************
changed: [docker2] => (item=test1)
changed: [docker2] => (item=test2)
changed: [docker2] => (item=test3)
changed: [docker1] => (item=test1)
changed: [docker1] => (item=test2)
changed: [docker1] => (item=test3)

PLAY RECAP **************************************************
docker1   : ok=1    changed=1    unreachable=0    failed=0   
docker2   : ok=1    changed=1    unreachable=0    failed=0   

Voilà, ça sera tout pour aujourd'hui !

vendredi 13 mars 2015

Gestion de l'option changed_when dans Ansible

Petite astuce rapide. Sous Ansible, j'ai voulu supprimer le contenu d'un répertoire (suite à la décompression d'une archive) et je voulais faire le ménage dedans sauf une certaine liste de fichier.

Pour se faire, je passe pas une commande shell et un register pour pouvoir ensuite itérer sur ces fichiers.

Le playbook

Ci-dessous mon playbook :

- name: "Récupération fichier à supprimer du répertoire"
  shell: ls -1 /tmp/emplacement/*.xml | grep -v un-fichier.xml | cat
  register: files_to_remove

- name: "Ménage dans le répertoire."
  file: name={{item}} state=absent
  with_items: files_to_remove.stdout_lines

Seul petit problème, la tache Récupération fichier à supprimer du répertoire m'indique tout le temps qu'il y a eu un changement alors qu'en réalité non.

Ci-dessous un exemple pour illustrer ça :

TASK: [task | Récupération fichier à supprimer du répertoire] **
changed: [localhost]

TASK: [task | Ménage dans le répertoire.] ********************** 
ok: [localhost]

PLAY RECAP *****************************************************
localhost    : ok=1    changed=1    unreachable=0    failed=0   

Bloquer ce faux changement

Dans ce cas, il est intéressant d'utiliser l'option changed_when avec une condition afin de dire à Ansible que ma commande n'a rien modifiée (ici, ça sera False systématiquement).

Si on l'applique à notre cas, notre main.yml ressemble maintenant à la chose suivante :

- name: "Récupération fichier à supprimer du répertoire"
  shell: ls -1 /tmp/emplacement/*.xml | grep -v un-fichier.xml | cat
  register: files_to_remove
  changed_when: False

- name: "Ménage dans le répertoire."
  file: name={{item}} state=absent
  with_items: files_to_remove.stdout_lines

Dorénavant mes appels ne marquent plus cette tâche comme ayant changée quelque chose.

TASK: [task | Récupération fichier à supprimer du répertoire] ** 
ok: [localhost]

TASK: [task | Ménage dans le répertoire.] ********************** 
ok: [localhost]

PLAY RECAP **************************************************
localhost    : ok=2    changed=0    unreachable=0    failed=0   

mardi 10 mars 2015

Activation du cache Ansible par fichier JSON

Lorsque vous lancez Ansible, vous avez déjà dû remarquer que le démarrage peut prendre un certain temps : il s'agit de la phase de récupération des informations sur la machine. Ceci se manifeste au début du lancement d'un playbook sous la forme suivante :

GATHERING FACTS ******************************************* 
ok: [localhost]

Pendant cette phase, Ansible va collecter tout un tas d'indicateur qui vont caractériser votre machine. On va y retrouver pêle-mêle le type de processeur, les disques de la machine, les adresses IP etc. Seul petit problème cette phase prend quelques secondes (voir plus certaines fois) et peut devenir vite agaçante dans le cas où vous mettez au point vos procédures d'installation.

Heureusement, les petits gars d'Ansible ont introduit un petit mécanisme de cache de ces éléments (des facts dans le jargon Ansiblien). Vous pouvez effectivement stocker ça dans un serveur redis.

Comme je n'ai pas trop l'habitude d'utiliser redis et que je n'avais pas envie de passer du temps à faire l'installation de ce nouvel élément sur mon poste perso, je me suis posé la question s'il ne serait pas possible d'utiliser quelque chose de plus simple ... Et la réponse est oui, avec des fichiers au format JSON.

Activation du cache via fichier JSON

Ouvrons notre fichier /etc/ansible/ansible.cfg et allons modifier la valeur du paramètre gathering pour le passer à la valeur smart :

# smart - gather by default, but don't regather if already gathered
# implicit - gather by default, turn off with gather_facts: False
# explicit - do not gather by default, must say gather_facts: True
gathering = smart

Si vous lancez maintenant Ansible, vous ne verrez pas trop de différence. Ce paramètre peut néanmoins être avantageux si vous lancez plusieurs rôles sur les mêmes machines. Dans ce cas, Ansible ne récupérera les caractéristiques de vos machines qu'une seule fois.

Allons un peu plus loin et modifions maintenant les valeurs de fact_caching :

# if set to a persistant type (not 'memory', for example 'redis') fact values
# from previous runs in Ansible will be stored.  This may be useful when
# wanting to use, for example, IP information from one group of servers
# without having to talk to them in the same playbook run to get their
# current IP information.
# fact_caching = memory
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible

Nous avons activé le stockage des fichiers d'inventaire dans le répertoire /tmp/ansible. Chaque serveur verra ses caractéristiques stockées au format JSON.

A noter l'existence d'un paramètre fact_caching_timeout qui vous permettra de contrôler la fraîcheur de vos inventaires.

Différence de vitesse d'exécution dû au cache

Voyons maintenant l'impact des performances avec le test tout simple suivant :

---
# Playbook de test

- name: "Test"
  hosts: "localhost"
  tasks:
    - debug: msg=Lancement réussi

On ne fait qu'afficher un message. Ansible procédera néanmoins à la phase d'inventaire ce qui nous permettra de mesurer ce temps.

Premier lancement

Lançons une première fois notre playbook à froid :

$ time ansible-playbook test.yml 

PLAY [Test] ************************************************* 

GATHERING FACTS ********************************************* 
ok: [localhost]

TASK: [debug msg=Lancement réussi] ************************** 
ok: [localhost] => {
    "msg": "Lancement"
}

PLAY RECAP ************************************************** 
localhost    : ok=2    changed=0    unreachable=0    failed=0   


real    0m2.300s
user    0m1.531s
sys     0m0.708s

Un coup d'oeil dans le répertoire /tmp/ansible :

$ ls /tmp/ansible/
localhost

Ansible a créé un fichier de cache. Un coup d'oeil nous permet de voir qu'il s'agit d'un fichier au format JSON.

Lancement avec cache

Maintenant que nous disposons d'un cache, mesurons à nouveau ce temps d'exécution :

$ time ansible-playbook test.yml 

PLAY [Test] ************************************************* 

TASK: [debug msg=Lancement réussi] ************************** 
ok: [localhost] => {
    "msg": "Lancement"
}

PLAY RECAP ************************************************** 
localhost    : ok=1    changed=0    unreachable=0    failed=0   


real    0m0.303s
user    0m0.242s
sys     0m0.044s

Nous venons de passer de 2,3 secondes à 0,3 seconde soit un gain de 2 secondes.

Pour conclure

Comme nous l'avons vu, le gain n'est pas négligeable (surtout en phase de mise au point) et surtout introduit une notion de persistance de l'information d'une exécution à l'autre. En effet, vous pouvez maintenant récupérer une information venant d'un autre serveur sans pour autant avoir à vous y connecter à chaque fois.