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.

mardi 3 mars 2015

Écriture de module avec Ansible

Depuis quelques temps, je m'étais mis en tête de vouloir tester l'espace disque disponible. En effet, il m'arrive assez régulièrement d'avoir un bon gros message d'erreur plus d'espace disponible alors que je viens de faire la moitié de l'installation.

Partant de ce constat, je me suis dit qu'il vaudrait mieux avoir une erreur franche en début d'exécution plutôt qu'au milieu de mon installation (sachant que généralement je peux avoir un truc à moitié bancal que je dois supprimé pour avoir une installation correcte). J'ai donc cherché un moyen de tester la quantité de disque restant pour un emplacement donné évitant ainsi ce type de désagrément.

Petit problème, je n'ai rien trouvé me permettant de faire ce test simplement. N'étant pas non plus emballé de faire ça avec un bout de shell, j'ai donc cherché à écrire le module Ansible qui me permettrait de gérer ce test.

Fichier playbook

Prenons un playbook check_tmp.yml qui va s'assurer que j'ai au moins 40 Mo d'espace disque dans /tmp. Pour se faire, il va faire appel à un module space_available :

---
# Playbook de test

- name: "Test"
  hosts: "localhost"
  gather_facts: no
  tasks:
    - space_available: path=/tmp size=40M

Fichier du module (library/space_available)

Dans le même répertoire que le fichier check_tmp.yml, créer un répertoire library. Dans ce répertoire, il nous suffit de créer un fichier space_available que nous allons alimenter avec le contenu suivant :

#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Author: Yannig Perre 
#
# Check for available space on a given path.
#
 
# Documentation
DOCUMENTATION = '''
---
version_added: "1.8"
module: space_available
short_description: space_available
description:
  - This module check if there's enough space available
options:
  path:
    description:
      path to check
  size:
    description:
      size needed.
notes:
requirements: []
author: Yannig Perre
'''
 
EXAMPLES = '''
- name: "40M space available on /tmp"
  space_available: path=/tmp size=40M
'''
 
# static hash map converter
size_convertion = { "k": 1024, "m": 1024*1024, "g": 1024*1024*1024, "t": 1024*1024*1024*1024, "p": 1024*1024*1024*1024*1024 }

# Here we go...
def main():
    # module declaration
    module = AnsibleModule(
        argument_spec=dict(
            path=dict(required=True),
            size=dict(required=True),
        ),
        supports_check_mode=True
    )
    # Retrieving parameters value
    path = module.params['path']
    size = module.params['size']
    # Retrieve size (code part stolen from lvol module)
    if size[-1].isalpha():
        if size[-1].lower() in 'kmgtpe':
            size_unit = size[-1]
            if not size[0:-1].isdigit():
                module.fail_json(msg="Bad size specification for unit %s" % size_unit)
            size_opt = size[-1]
            size = int(size[0:-1])
    elif not size.isdigit():
        module.fail_json(msg="Bad size specification")
    else:
        size = int(size)
        size_opt = 'm'

    # Convert everything in bytes please
    unit_size = size_convertion[size_opt.lower()]
    size = size * unit_size
    # Try to get ...
    try:
        # ... size available
        statvfs = os.statvfs(path)
    except OSError, e:
        # if we fail dying with exception message
        module.fail_json(msg="%s" % e)
    space_available = statvfs.f_bavail * statvfs.f_frsize

    # If not enough room => fail
    if space_available < size:
        module.fail_json(msg="Not enough space avaible on %s (found = %0.2f%s, needed = %0.2f%s)" % (path, space_available / unit_size, size_opt, size / unit_size, size_opt))
    module.exit_json(msg=path)
 
# Import Ansible Utilities
from ansible.module_utils.basic import *
main()

Premier test

Lançons maintenant notre playbook avec la commande suivante :
ansible-playbook space_available.yml 

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

TASK: [space_available path=/tmp size=40M] ********************** 
ok: [localhost]

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

\o/ Youpi ! Il semblerait que mon poste dispose de plus de 40 Mo de disque dans /tmp !

Quelques tests supplémentaires

Changeons maintenant notre playbook pour voir le comportement de notre module :

---
# Playbook de test

- name: "Test"
  hosts: "localhost"
  gather_facts: no
  tags: "poste-packaging"
  tasks:
    - space_available: path=/does/not/exist size=400M
      ignore_errors: yes
    - space_available: path=/root/.aptitude size=400M
      ignore_errors: yes
    - space_available: path=/tmp size=400M
    - space_available: path=/tmp size=1G
    - space_available: path=/tmp size=37G
      ignore_errors: yes
    - space_available: path=/tmp size=40M
    - space_available: path=/ size=3000M

Lançons notre playbook :

ansible-playbook space_available.yml 

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

TASK: [space_available path=/does/not/exist size=400M] ********* 
failed: [localhost] => {"failed": true}
msg: [Errno 2] No such file or directory: '/does/not/exist'
...ignoring

TASK: [space_available path=/root/.aptitude size=400M] ********* 
failed: [localhost] => {"failed": true}
msg: [Errno 13] Permission denied: '/root/.aptitude'
...ignoring

TASK: [space_available path=/tmp size=400M] ******************** 
ok: [localhost]

TASK: [space_available path=/tmp size=1G] ********************** 
ok: [localhost]

TASK: [space_available path=/tmp size=37G] ********************* 
failed: [localhost] => {"failed": true}
msg: Not enough space avaible on /tmp (found = 9.00G, needed = 37.00G)
...ignoring

TASK: [space_available path=/tmp size=40M] ********************* 
ok: [localhost]

TASK: [space_available path=/ size=3000M] ********************** 
ok: [localhost]

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

Nous avons bien des indications sur :

  • L'absence d'un fichier sur la machine ;
  • Un manque de permission ;
  • Et enfin, un manque de place.
A noter que les instructions en erreur sont passées du fait de la présence du mot clé ignore_errors: yes.

Pour aller plus loin

Pour plus de détail sur le fonctionnement des modules, je vous invite à vous reporter à la documentation sur le sujet présente sur ansible.com (http://docs.ansible.com/developing_modules.html).

jeudi 9 octobre 2014

Comparaison de la gestion des FS avec Puppet et Ansible

Suite de mes aventures avec Ansible : aujourd'hui c'est du système Linux.

Les données du problème : je veux pouvoir gérer le LVM Linux et retailler mes FS à la demande.

La recette sous Puppet

Sous Puppet, en utilisant un fichier yaml/Hiera, l'écriture de tout ceci se ferait avec quelque chose de ce genre :

---
classes: ['lvm']

lvm::volume_groups:
  rootvg:
    physical_volumes:
      - /dev/sda2
    logical_volumes:
      tmp:
        size: 2G
        mountpath: /tmp
  datavg:
    physical_volumes:
      - /dev/sdb
    logical_volumes:
      test:
        size: 128
        mountpath: /test

Ne pas oublier non plus d'inclure le code suivant dans le fichier site.pp :

hiera_include('classes','')

Bien penser également à installer le module lvm, sinon ça ne marchera pas (puppetlabs-lvm).

Bon, sous Ansible, toutes ces belles choses n'existent pas (encore) en l'état mais sous forme de brique à assembler. On va voir comment recréer un mécanisme à peu près similaire.

La recette sous Ansible

Je me place dans le cadre d'un playbook Ansible avec un fichier system.yml se trouvant dans un répertoire de travail ~/ansible avec le contenu suivant :

---
# Playbook de gestion du système

- name: Propriétés communes système
  hosts: all
  remote_user: root

  roles:
    - system

Autre fichier présent : group_vars/all avec le contenu suivant :

---
# Variable applicable à tous

vgs:
  rootvg: "/dev/sda2"
  datavg: "/dev/sdb"

lvs:
  test:
    vg: rootvg
    mountpoint: /test
    size: 256

Ce fichier sera a spécialiser par groupe de machine mais est l'équivalent de notre fichier yaml/Hiera. Reste maintenant à écrire notre liste de tâche qui nous permettra de gérer ces FS (qu'on mettra dans le fichier roles/system/tasks/main.yml) :

---
- name: VG/PV Configuration
  lvg: vg={{ item.key }} pvs={{ item.value }} state=present
  with_dict: vgs

- name: LV creation
  lvol: vg={{ item.value.vg }} lv={{item.key}} size={{item.value.size}}
  register: task
  with_dict: lvs

- name: LV format
  filesystem: fstype=ext4 dev=/dev/mapper/{{item.value.vg}}-{{item.key}}
  with_dict: lvs

- name: Mount
  mount: name={{item.value.mountpoint}} src=/dev/mapper/{{item.value.vg}}-{{item.key}} dump=1 passno=2 fstype=ext4 state=mounted
  when: item.value.has_key('mountpoint')
  with_dict: lvs

- name: FS resize
  command: resize2fs /dev/mapper/{{item.item.value.vg}}-{{item.item.key}}
  with_items: task.results
  when: item.changed == True

NB : cette recette ne gère que des fs de type ext4. Si vous voulez gérer du xfs/btrfs/fegafatfs, il faudra changer la commande resize2fs par autre chose.

Le fonctionnement de la recette est assez simple : on enchaine les opérations (création du PV/VG, création du LV, formatage et montage) et on exécute la commande resize2fs qu'en cas de changement sur le LV (cf FS resize, ligne when: item.changed == True).

Reste maintenant à lancer notre playbook avec la commande ansible-playbook -i ./hosts ./system.yml. Vous devriez obtenir le résultat suivant :

PLAY [Propriétés communes système] ******************************

GATHERING FACTS *************************************************
ok: [machine1]

TASK: [common | VG/PV Configuration] ****************************
ok: [machine1] => (item={'key': 'datavg', 'value': '/dev/sdb'})
ok: [machine1] => (item={'key': 'rootvg', 'value': '/dev/sda2'})

TASK: [common | LV creation] ************************************
changed: [machine1] => (item={'key': 'test', 'value': ...})

TASK: [common | LV format] **************************************
changed: [machine1] => (item={'key': 'test', 'value': ...})

TASK: [common | Mount] ******************************************
changed: [machine1] => (item={'key': 'test', 'value': ...})

TASK: [common | FS resize] **************************************
changed: [machine1] => (item={u'msg': u'', 'item': ...})

PLAY RECAP ******************************************************
machine1        : ok=6    changed=4    unreachable=0    failed=0   

Changeons la taille de notre FS pour autre chose et relançons ansible pour vérifier la bonne prise en compte de tout ceci :

PLAY [Propriétés communes système] ******************************

GATHERING FACTS *************************************************
ok: [machine1]

[...]

TASK: [common | LV creation] ************************************
changed: [machine1] => (item={'key': 'test', 'value': ...})

[...]

TASK: [common | FS resize] **************************************
changed: [machine1] => (item={u'msg': u'', 'item': ...})

PLAY RECAP ******************************************************
machine1        : ok=6    changed=2    unreachable=0    failed=0   

Tout c'est bien passé. Il ne me reste plus qu'à vous souhaiter une bonne gestion de vos LV !

mardi 7 octobre 2014

Premier pas avec Ansible et gestion du sudo

Et voilà, à peine je commence à maîtriser le langage autour de Puppet qu'on me demande de me pencher sur Ansible. Pour les personnes qui se sauraient pas ce que font ces deux produits, il faut savoir qu'ils appartiennent à la mouvance du DevOps qui consiste à gérer son infrastructure par du code (Dev) plutôt que par un opérateur (Ops) également appelé être humain (ou bipède, interface chaise-clavier etc.).

Installation de Ansible sur votre machine

La différence entre les deux produits se trouve au niveau de leur mode de fonctionnement : Puppet fonctionne à l'aide d'agent alors que Ansible s'appuie sur le protocole SSH et l'interpréteur python. Un petit bémol tout de même, si vous utilisez des versions antérieurs à Python 2.5, vous aurez besoin d'une librairie de gestion du json dans python. Comme il se trouve que les versions de RHEL 5.x sont concernées, ça pourrait vous arriver.

Pour les autres, c'est pour ainsi dire la fête : pas de package à installer et à maintenir sur vos machines. Vous échangez vos clés SSH avec les machines que vous voulez gérer et c'est parti.

Pour le serveur Ansible, l'installation se fait en activant le support des EPEL pour les machines à base de RHEL/CentOS (cf https://fedoraproject.org/wiki/EPEL) et en lançant un bon vieux yum install ansible. Pour Ubuntu, vous pouvez vous appuyer sur le ppa suivant : ppa:rquillo/ansible (l'ajout se fait avec la commande sudo add-apt-repository ppa:rquillo/ansible suivi d'un sudo aptitude update ; sudo aptitude install ansible).

Et voilà, c'est tout !

Prenons un exemple

Maintenant que nous avons planté le décor, nous allons créer notre première recette. Créer un répertoire de travail (~/ansible/test par exemple) et, dans ce répertoire, créer un fichier hosts avec le contenu suivant :

[test]
machine1
machine2

Ce fichier va nous servir d'inventaire des différentes machines pour nos déploiements et de mécanisme de regroupement (avec ici une section [test]). Pour tester la communication ssh, il suffit de lancer la commande ansible -i ./hosts all -m ping. Ci-dessous un exemple de résultat :

ansible -i ./hosts all -m ping
machine1 | success >> {
    "changed": false, 
    "ping": "pong"
}
machine2 | success >> {
    "changed": false, 
    "ping": "pong"
}

En cas de pépin, il faudra bien-sûr échanger les clés SSH avec un ssh-copy-id MACHINE.

Maintenant que nous avons la liste de nos machines, lançons la création du fichier site.yml et ajoutons-y le contenu suivant :

---
# Playbook de test

- name: Test
  hosts: test
  tasks:
    - name: Création d'un fichier /tmp/test
      copy: dest=/tmp/test content="Ceci est test\n" owner=root group=root

Nous avons ici associé le group de machine test (ligne hosts: test) avec la création d'un fichier de test appartenant à root.

On enregistre, et on lance maintenant l'application de cette recette avec la commande ansible-playbook -i ./hosts ./site.yml. On devrait obtenir la sortie suivante :

yannig@pupuce ~/ansible/test $ ansible-playbook -i ./hosts ./site.yml

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

GATHERING FACTS *****************************************************
ok: [machine1]
ok: [machine2]

TASK: [Création d'un fichier /tmp/test] *****************************
changed: [machine1]
changed: [machine2]

PLAY RECAP **********************************************************
machine1             : ok=2    changed=1    unreachable=0    failed=0
machine2             : ok=2    changed=1    unreachable=0    failed=0

Ça marche : j'ai maintenant des fichiers /tmp/test sur mes deux machines :).

Et puis vint la sécurité

C'est bien beau mais le test que je viens de faire, je l'ai fait sur des machines que je maîtrise totalement. Malheureusement sur la prod sur laquelle je vais devoir travailler, il est interdit de se connecter directement en tant que root. Il faut donc passer par un mécanisme d'escalade sudo. Et comme un bonheur ne vient jamais seul, il faut également ressaisir le mot de passe pour lancer le sudo ...

Et c'est là où intervient la notion de variable dans le fichier hosts. Ci-dessous un exemple permettant de gérer l'utilisation d'un mot de passe sudo pour passer en tant qu'utilisateur root :

[machineexterne:vars]
ansible_sudo_pass=xxx
ansible_remote_user=userxxx
ansible_sudo=yes
ansible_sudo_user=root

[machineexterne]
machineexterne1
machineexterne2

[test]
machine1
machine2

Il est bien sûr entendu que le compte remote_user devra disposer des droits suffisants pour faire un sudo vers root.

Pour conclure

Vous l'aurez vu, il s'agissait juste de faire ses premiers pas avec ce merveilleux outils. Les difficultés habituelles sont bien gérées (sudo, ssh) et sa légéreté de déploiement initiale devrait en convaincre plus d'un.

La documentation du site est bien organisée. Je vous laisse consulter ça à l'adresse suivante : http://docs.ansible.com/.

samedi 13 septembre 2014

Installation et présentation de puppetboard

Mais au fait, pourquoi puppetboard ?

Il était une fois un royaume dans lequel les devops étaient roi et n'avaient (presque plus) rien à faire. Passant par là, le développeur opensource du dit produit se dit qu'il fallait réveiller se peuple au bios dormant. C'est ainsi que naquis la notion de PuppetDB (bon OK, y'a d'autres raisons notamment le besoin d'avoir un mécanisme de stockage des faits un peu plus propre que ce qui était fait).

Concernant PuppetDB en lui même - dans les grandes lignes - sachez qu'il s'agit d'une base type NoSQL (notion à la mode) qu'on peut interroger à l'aide de requête de type REST (autre notion à la mode).

Pour la version un peu plus longue, sachez que le premier désigne un mécanisme de stockage d'objet au format JSON (et c'est là où je me rends compte que c'est pas forcément plus clair ...). En gros, c'est comme le SQL sauf qu'il n'y a pas de table et que les requêtes se font en javascript à la place du SQL. Si vous ne savez pas de quoi je parle là non plus, je ne peux plus rien faire pour vous.

Pour les requêtes REST (ou restfull), tout ceci désigne un moyen de faire des appels via l'utilisation de requête HTTP (et que c'est marrant de se rendre compte qu'on avait déjà fait du REST avant le verbe mais je m'égare). En gros, vous voulez la liste des machines, vous faîtes une requête sur l'URL http://localhost/nodes, ce dernier vous renverra alors la liste des nœuds sous la forme d'un hash map (autrement appelé JSON). Bon d'accord, c'est pas très clair. En gros, ça vous renvoie du texte qui devrait ressembler à un truc de ce genre :

{
  "node1": {"address": "192.168.0.1", "macadress": "00:00:01:00:00:01"},
  "node2": {"address": "192.168.0.2", "macadress": "00:00:01:00:00:02"},
}

Ne reste plus qu'à le lire et l'utiliser dans votre interface web, votre script perl etc.

Pour revenir à nos moutons, PuppetDB à donc pour rôle de stocker les informations qu'on va récolter suite aux applications de catalogue sur nos machines. Seul petit hic, le plugin puppet dashboard n'est plus trop compatible avec tout ceci et je me retrouve donc avec un superbe outil de présentation des données puppet vide et, de l'autre côté, une interface en JSON/Restfull absolument inutilisable pour mes utilisateurs (ainsi que moi-même étant donné que je ne lis pas encore le JSON dans le texte).

C'est donc à ce moment que je me suis mis en quête d'une alternative afin de rendre le sourire à mes utilisateurs et moi-même. Après quelques requêtes sur le moteur dont on ne doit pas prononcer le nom, je tombe sur le jeune et prometteur puppetboard (https://github.com/nedap/puppetboard).

Mais assez de blabla, passons maintenant à l'installation de ce merveilleux outils.

Installation

Autant vous le dire tout de suite, si vous voulez installer ce produit, c'est que vous utilisez puppet. Si vous ne voulez pas utiliser puppet pour l'installation, c'est que vous êtes maso. Comme il se trouve qu'en plus, les auteurs ont fait une recette puppet directement récupérable depuis le puppet forge, on ne va pas s'en priver.

En premier lieu, assurez-vous d'avoir déjà les modules puppet apache et puppetboard (puppet module install/list/etc.).

Ouvrez la structure du node puppet correspondant à la machine sur laquelle vous voulez installer le puppetboard et ajoutez-y la recette suivante :

  class { 'apache': }
  class { 'apache::mod::wsgi': }
  # Configure Puppetboard
  class { 'puppetboard':
    manage_virtualenv => true,
    puppetdb_port     => "9090",
    puppetdb_host     => "127.0.0.1",
  }
  # Access Puppetboard through pboard.example.com
  class { 'puppetboard::apache::vhost':
    vhost_name => 'puppetboard',
    port       => 8888,
  }

De là, je vous invite à lancer la commande d'application de votre recette sur la machine puppetboard :

puppet agent -t

Tada ! Vous avez un puppetboard fonctionnel.

Sortez maintenant votre navigateur préféré (firefox, chrome, Internet E nan, j'déconne) et pointez sur l'URL http://puppetboard:8888 (ou ce que votre cerveau malade aura décidé de choisir) et vous devriez obtenir l'interface suivante :
Un petit tour du propriétaire s'impose :
  • Le détail sur un serveur :
  • Consultation du résultat de l'application d'un catalogue sur une machine :
  • Requêtage des faits en base :

Pour finir

Autant le dire tout de suite, l'outil est encore un peu jeune. A noter un petit soucis que j'ai rencontré lors du déploiement : les librairies Javascript sont référencées sur Internet et non sur la machine. Il faut donc un navigateur avec un proxy valide pour que ça fonctionne. Je me fendrai peut-être d'un patch pour corriger ça ...

Autre chose, je n'ai plus mes courbes de tendance sur le temps d'application du catalogue sur mes machines. Argh ! ça va crier ...

Mais bon, en dehors de ça, l'interface est assez réactive et simple à comprendre. Je recommande donc !

jeudi 13 février 2014

Couplage nxlog et stack ElasticSearch

Je suis actuellement en train de tester le couple Logstash/ES/Kibana. Dans ce cadre, j'ai voulu regarder différents mécanismes pour remonter des informations venant de mes différentes machines sans avoir pour autant à installer un agent Logstash partout.

J'ai donc commencé à regarder le fonctionnement de nxlog qui permet de faire ce genre de chose.

Mise en place de nxlog

Dans ce qui va suivre, on va remonter les events venant de notre nœud Windows (ici un 2003) dans le serveur Logstash. Le côté intéressant de ce produit est qu'il fonctionne aussi bien sous Windows que Linux et que ce dernier fait de la rétention de message le temps que le serveur Logstash soit de nouveau disponible.

Pour en revenir à nos moutons, ci-dessous un exemple de configuration :

define ROOT C:\Program Files\nxlog

Moduledir %ROOT%\modules
CacheDir %ROOT%\data
Pidfile %ROOT%\data\nxlog.pid
SpoolDir %ROOT%\data
LogFile %ROOT%\data\nxlog.log

<Input in>
    # Module      im_msvistalog
# For windows 2003 and earlier use the following:
    Module      im_mseventlog
</Input>

<Extension json>
    Module      xm_json
</Extension>

<Output logstash>
    Module      om_tcp
    Port        5140
    Host        IP_SERVEUR_LOGSTASH
    Exec        to_json();
</Output>

# Let's tie all pieces together with a NXlog route
<Route local_to_logstash>
    Path        in => logstash
</Route>
En substance, on charge le module im_mseventlog, on indique un point d'envoi (Output logstash) et enfin, on précise un chemin (in => logstash).

Configuration de Logstash

Nous allons maintenant configurer Logstash afin de récupérer les messages de notre ami nxlog.

Pour se faire, nous allons créer un fichier logstash.conf avec une entrée (précisant l'encodage) :

input {
  tcp {
    port => 5140
    type => "nxlog"
    codec => line {
      charset => "CP1252"
    }
  }
}

NB : il est possible d'utiliser le codec json directement dans l'input tcp. Le problème est que l'agent nxlog peut renvoyer plusieurs messages JSON d'un seul coup (notamment dans le cas d'un arrêt/relance du serveur Logstash). Dans ce cas, Logstash n'arrive pas à bien l'interpréter et ne récupère les différents champs.

On va ensuite récupérer le contenu et le traiter comme étant du json :

filter {
  if [type] == "nxlog" {
    json {
      source => "message"
    }
    mutate {
      rename => [ "Message", "message" ]
      # remove_field => [ "champ1", "champ2" ]
    }
  }
}

A noter que nxlog renvoie pas mal de champ. Si vous voulez en supprimer quelques uns, décommenter le champ remove_field ci-dessus.

Il ne nous reste plus qu'à renvoyer le contenu sur la sortie standard pour débogage :

output {
  stdout { debug => true }
}

Reste maintenant à lancer notre agent logstash :

java -jar logstash.jar --conf logstash.conf agent

Exemple de message

Une fois lancé, nous pouvons recevoir nos premiers messages :

{
              "message" => "xxx",
             "@version" => "1",
           "@timestamp" => "2014-02-13T14:53:57.156Z",
                 "type" => "nxlog",
                 "host" => "xxxx:xxx",
            "EventTime" => "2014-02-13 15:53:57",
     "EventTimeWritten" => "2014-02-13 15:53:57",
             "Hostname" => "XXX",
            "EventType" => "AUDIT_SUCCESS",
        "SeverityValue" => 2,
             "Severity" => "INFO",
           "SourceName" => "Security",
             "FileName" => "Security",
              "EventID" => 538,
       "CategoryNumber" => 2,
             "Category" => "Ouverture/Fermeture de session ",
         "RecordNumber" => 410222,
               "Domain" => "XXX",
          "AccountName" => "Administrateur",
          "AccountType" => "User",
    "EventReceivedTime" => "2014-02-13 15:53:57",
     "SourceModuleName" => "in",
     "SourceModuleType" => "im_mseventlog"
}

Il ne reste plus qu'à envoyer tout ça dans le moteur ElasticSearch et le tour est joué !

dimanche 26 janvier 2014

Installation de razor (ainsi que puppet, postgresql, tftp et dhcp)

Il y a quelques années, j'avais mis en place un serveur d'installation Linux. C'était basé sur le classique dhcp, tftp et kickstart. Un de mes collègues m'a parlé dernièrement d'un projet relativement intéressant : razor. C'est un projet qui est toujours en cours de développement mais est tout à fait utilisable.

Son principe est assez simple : il se base sur des briques classiques pour gérer la partie boot réseau (DHCP + TFTP + kickstart) et utilise un serveur JBoss couplé à postgresql pour la partie gestion de la configuration.

Pour mémoire, l'installation se fera sur une CentOS 6.5 mais devrait s'adapter très facilement sur une autre distribution.

Mais trêve de bavardage, passons à la mise en oeuvre.

Installation et configuration de postgres

Pour faire fonctionner razor, nous allons avoir besoin d'une version très récente de posgresql (la 9.3). Le repository pour RHEL/CentOS contenant cette version de postgres s'installe avec la commande suivante :

yum install http://yum.postgresql.org/9.3/redhat/rhel-6-x86_64/pgdg-centos93-9.3-1.noarch.rpm

L'installation de postgresql à proprement parlé se fait ensuite simplement avec yum :

yum install postgresql93-server

De là, il nous faut initialiser le contenu de la base avec la commande suivante :

/etc/init.d/postgresql93 initdb

Ceci fait, nous pouvons démarrer la base :

/etc/init.d/postgresql93 start

Enfin, nous allons créer notre utilisateur ainsi que notre schéma :

psql
create user razor;
create database razor_prd owner razor;
\password razor

Modifier ensuite le fichier /var/lib/pgsql/9.3/data/pg_hba.conf afin de faire apparaître la ligne suivante (vers la fin du fichier) :

local   all         all     127.0.0.1/32        md5

Un arrêt/relance de postgresql plus tard :

/etc/init.d/postgresql restart

Nous voici prêt pour la suite.

Installation de puppet

Par la suite, je m'appuierai énormément sur puppet pour mettre en oeuvre le produit. Je vous invite donc à l'installer avec la série de commandes suivante :

yum install http://yum.puppetlabs.com/puppetlabs-release-el-6.noarch.rpm
yum install puppet

Installation de razor

Le plus simple et de se servir de puppet pour faire le gros du travail. Pour se faire, lançons l'installation du module razor :

puppet module install puppetlabs-razor

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

Notice: Preparing to install into /etc/puppet/modules ...
Notice: Downloading from https://forge.puppetlabs.com ...
Notice: Installing -- do not interrupt ...
/etc/puppet/modules
└─┬ puppetlabs-razor (v0.11.0)
  ├─┬ puppetlabs-java (v1.0.1)
  │ └── puppetlabs-stdlib (v4.1.0)
  └─┬ puppetlabs-tftp (v0.2.1)
    └── puppetlabs-xinetd (v1.2.0)

Passons maintenant à l'installation de razor :

export FACTER_server=ADDRESSE_IP_RAZOR
puppet apply -e "include razor"

Au bout d'un petit moment, vous devriez avoir un process Java JBoss pour héberger l'application razor ainsi qu'un serveur tftpboot fonctionnel (ou presque).

Rendez-vous maintenant dans le répertoire /opt/razor afin de recopier le fichier config.yam.sample en config.yaml :

cd /opt/razor
cp config.yaml.sample config.yaml

Adapter ensuite le contenu du fichier config.yaml à votre plateforme et lancer la commande suivante :

razor-admin -e production migrate-database

De là, il ne nous reste plus qu'à installer une version valide du client razor avec la commande suivante :

puppet resource package razor-client provider=gem ensure=latest

Si vous avez un parefeu sur votre machine, il vous faudra ouvrir le port 8080.

Configuration du tftp

Rien de particulier à signaler si ce n'est de bien faire attention d'ouvrir le port udp 69 sur votre parefeu.

Récupération du microkernel

Nous allons déposer le micro kernel dans un sous répertoire de razor (/var/lib/razor/repo-store). Ci-dessous les commandes à lancer pour le faire :

wget http://links.puppetlabs.com/razor-microkernel-003.tar
tar xfv razor-microkernel-003.tar -C /var/lib/razor/repo-store

Configuration du serveur DHCP

Il existe un fichier d'exemple dans le module puppet que vous pouvez récupérer avec la commande suivante :

cp /etc/puppet/modules/razor/examples/isc-dhcpd-example.conf /etc/dhcp/dhcpd.conf

Reste ensuite à adapter le fichier /etc/dhcp/dhcpd.conf et démarrer notre serveur dhcp en s'assurant qu'il soit démarrer au lancement de la machine :

puppet resource service dhcpd ensure=running enable=true

Nous allons pouvoir commencer quelques tests ...

Installation d'une CentOS 6.5

Création d'un broker par défaut

Avec razor, il est possible de faire tout un tas de chose en post installation. Dans notre cas, on va se contenter quelque chose de simple avant d'aller plus loin :

razor create-broker --name=noop --broker-type=noop

Création d'un repository

Nous allons récupérer un repository CentOS. Cette opération se fait avec la commande razor create-repo :

razor create-repo --name centos-6.5 --iso-url=http://.../CentOS-6.5-x86_64-minimal.iso

Nous pouvons ensuite vérifier la présence de notre repository avec la commande razor repos :

# razor repos
From http://192.168.122.3:8080/api/collections/repos:

    id: "http://...:8080/api/collections/repos/centos-6.5"
  name: "centos-6.5"
  spec: "/razor/v1/collections/repos/member"

Ainsi que le détail du repos en ajoutant son nom :

# razor repos centos-6.5
From http://192.168.122.3:8080/api/collections/repos/centos-6.5:

       id: "http://...:8080/api/collections/repos/centos-6.5"
     name: "centos-6.5"
     spec: "/razor/v1/collections/repos/member"
  iso_url: "http://.../CentOS-6.5-x86_64-minimal.iso"

Création d'un tag

Nous allons procéder à la création d'un tag testant si la machine est virtuelle ou non. Pour se faire, alimenter le fichier virtual.json avec le contenu suivant :

# cat virtual.json
{
  "name": "virtual",
  "rule": ["=", ["fact", "is_virtual"], "true"]
}

Donnons ça à manger à razor avec la commande razor create-tag :

# razor create-tag --json virtual.json
From http://192.168.122.3:8080/api:

    id: "http://localhost:8080/api/collections/tags/virtual"
  name: "virtual"
  spec: "/razor/v1/collections/tags/member"

Création d'une policy

Alimentons maintenant le fichier centos-6.5.json avec le contenu suivant :

# cat centos-6.5.json
{
  "name": "centos-6.5",
  "repo": { "name": "centos-6.5" },
  "task": { "name": "centos" },
  "broker": { "name": "noop" },
  "enabled": true,
  "hostname": "host${id}.local",
  "root_password": "ChangezMoi",
  "max_count": "100",
  "rule_number": "100",
  "tags": [{ "name": "virtual"}]
}

Le contenu du fichier va donner un minimum d'information pour l'installation à savoir :
  • Le nom de la police (centos-6.5) ;
  • Le nom du repository (idem) ;
  • Le type de tâche d'installation (ici une CentOS). Pour connaître la liste complète, on peut utiliser la commande razor tasks ;
  • Le type de broker : Il s'agit d'une espèce de post install. Pour l'instant, l'installation s'arrête là ;
  • Enabled : pour savoir si la police est active ou non ;
  • Un template pour le nom de la machine ;
  • Le mot de passe root ;
  • Le nombre max de création et son numéro ;
  • Enfin, les tags sur lesquels cette police s'appliquera (ici virtual).
Donnons ça à manger à notre ami razor avec la commande razor create-policy :

razor create-policy --json centos-6.5.json

Dès que nous démarrerons une machine virtuelle sur notre réseau, cette dernière commencera son installation. Si vous aviez une machine en attente, cette dernière redémarrera et commencera son installation.

Le mot de la fin

Vous l'avez vu, il s'agit vraiment d'une aide pour mettre en oeuvre razor le plus rapidement possible sans forcément chercher à personnaliser les choses.

J'essaierai dans un second temps de revenir avec un nouvel article pour vous présenter le mécanisme permettant de personnaliser vos installations.

A noter que le produit est en pleine refonte et que certaines parties de ce document vont sûrement être amené à évoluer ou devenir un peu obsolète. Je vous laisse ci-dessous une bibliographie m'ayant permis de mettre en oeuvre le produit.

Bon courage et bon déploiement !

Bibliographie

http://technodrone.blogspot.com/2013/11/installing-razor-yes-new-version.html
http://technodrone.blogspot.com/2013/11/installing-razor-client-and-creating.html