Affichage des articles dont le libellé est DevOps. Afficher tous les articles
Affichage des articles dont le libellé est DevOps. Afficher tous les articles

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 !

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