Ansible: améliorer la sortie écran

Ansible est un très bon ordonnanceur, avec une sortie relativement claire, néanmoins il se peut que vous ayez envie de l'améliorer.

La sortie standard Ansible utilise le plugin CallbackModule présent dans le répertoire lib/ansible/plugins/callback/default.py. Nous allons ici bénéficier de la possibilité de définir nos propres callback plugins et de l'héritage objet Python pour pouvoir améliorer facilement la sortie Ansible.

Configuration préliminaire

Dans un premier temps nous allons devoir établir quelques configurations Ansible dans le fichier ansible.cfg

callback_whitelist = timer
stdout_callback = customstdout
callback_plugins   = ./plugins/callback
  • La première ligne va activer le plugin timer d'Ansible, c'est un petit plugin très simple qui va en fin de playbook vous donner le temps d'exécution du playbook, pratique donc.
Playbook run took 0 days, 0 hours, 0 minutes, 37 seconds
  • La seconde ligne va définir le module customstdout comme plugin par défaut pour la sortie standard.
  • Enfin la 3ème ligne permet de définir un chemin relatif à l'exécution courante pour les callback plugins locaux à votre installation.

Création du plugin

Créez maintenant le fichier plugins/callback/customstdout.py dans votre arbre ansible courant et indiquez le contenu suivant, nous allons le détailler ensuite:

#
# Overrided version of Ansible default.CallbackModule
#
import datetime
from ansible import constants as C
from ansible.plugins.callback import default
import imp
import os

ANSIBLE_PATH = imp.find_module('ansible')[1]
DEFAULT_PATH = os.path.join(ANSIBLE_PATH, 'plugins/callback/default.py')
DEFAULT_MODULE = imp.load_source(
    'ansible.plugins.callback.default',
    DEFAULT_PATH
)

class CallbackModule(default.CallbackModule):  # pylint: disable=too-few-public-methods,no-init
    '''
    Override for the default callback module.

    Render std err/out outside of the rest of the result which it prints with
    indentation.
    '''
    CALLBACK_VERSION = 2.0
    CALLBACK_TYPE = 'stdout'
    CALLBACK_NAME = 'customstdout'

    def __init__(self):
        self._current_task_start = None
        super(default.CallbackModule, self).__init__()

    # Overrided functions from Ansible default.CallbackModule
    def v2_playbook_on_task_start(self, task, is_conditional):
        self._current_task_start = datetime.datetime.now()
        default.CallbackModule.v2_playbook_on_task_start(self, task, is_conditional)

    # This override show the task action where name is present. It's now the case by default
    def _print_task_banner(self, task):
        args = ''
        if not task.no_log and C.DISPLAY_ARGS_TO_STDOUT:
            args = u', '.join(u'%s=%s' % a for a in task.args.items())
            args = u' %s' % args

        if task._role and task.name and ("%s : " % task._role._role_name) not in task.name:
            task_name = "%s : %s | %s" % (task._role.get_name(), task.action.upper(), task.name)
        elif task.name:
            task_name = "%s | %s" % (task.action.upper(), task.name)
        else:
            if task._role:
                task_name = "%s : %s" % (task._role.get_name(), task.action)
            else:
                task_name = "%s" % (task.action,)

        self._display.banner(u"TASK [%s%s]" % (task_name.strip(), args))
        if self._display.verbosity >= 2:
            path = task.get_path()
            if path:
                self._display.display(u"task path: %s" % path, color=C.COLOR_DEBUG)

        self._last_task_banner = task._uuid

    def v2_runner_on_ok(self, result):

        if self._play.strategy == 'free' and self._last_task_banner != result._task._uuid:
            self._print_task_banner(result._task)

        self._clean_results(result._result, result._task.action)

        delegated_vars = result._result.get('_ansible_delegated_vars', None)
        self._clean_results(result._result, result._task.action)
        if result._task.action in ('include', 'include_role'):
            return
        elif result._result.get('changed', False):
            if delegated_vars:
                msg = "changed: [%s -> %s]" % (result._host.get_name(), delegated_vars['ansible_host'])
            else:
                msg = "changed: [%s]" % result._host.get_name()
            color = C.COLOR_CHANGED
        else:
            if delegated_vars:
                msg = "ok: [%s -> %s]" % (result._host.get_name(), delegated_vars['ansible_host'])
            else:
                msg = "ok: [%s]" % result._host.get_name()
            color = C.COLOR_OK

        if result._task.loop and 'results' in result._result:
            self._process_items(result)
        else:

            if (self._display.verbosity > 0 or '_ansible_verbose_always' in result._result) and not '_ansible_verbose_override' in result._result:
                msg += " => %s" % (self._dump_results(result._result),)
            td = datetime.datetime.now() - self._current_task_start
            msg += " (%fs)" % (td.microseconds / 1000000.0)
            self._display.display(msg, color=color)

        self._handle_warnings(result._result)

Description du plugin

Ce plugin est callback plugin dont l'objet père est default.CallbackModule, à savoir le module gérant la sortie par défaut Ansible. Grâce à l'héritage nous allons pouvoir récupérer le comportement par défaut d'Ansible et modifier les parties qui nous intéressent. Nous avons repris ici 2 fonctions du plugin pour les améliorer, l'affichage des noms de tâches (__print_taskbanner) et l'affichage du statut de changement de la tâche sur un node (_v2_runner_onok). Ces fonctions sont copiés directement depuis l'objet parent (le module default d'Ansible) et modifiées ici. Par l'héritage objet Python elles seront appelées plutôt que celles de l'objet parent.

Apports ajoutés par le module

Le premier apport est sur l'affichage du nom des tâches. En effet, lorsque vous spécifiez le module name avant d'appeler vos tâches, celui-ci remplace le nom du module utilisé. Notre amélioration va afficher le module de manière inconditionnelle, en plus du nom arbitraire attaché à la tâche en cours. Ainsi, au lieu d'avoir:

TASK [dovecot2 : Configure dovecot (conf.d files)] ******************

Vous obtiendrez:

TASK [dovecot2 : TEMPLATE | Configure dovecot (conf.d files)] ******************

C'est une fonctionnalité pratique lorsque vous lisez la sortie de vos tâches. Second apport, l'ajout du temps d'exécution des tâches par notre. Habituellement vous avez:

ok: [192.168.2.1]

Et maintenant vous aurez la sortie suivante:

ok: [192.168.2.11] (0.633523s)

Conclusion

Grâce à ce tutoriel vous savez désormais comment ajouter un callback plugin à Ansible et changer la sortie standard pour afficher la sortie comme bon vous semble.

Ansible: gérer les clefs SSH de ses serveurs

Ansible est un outil d'industrialisation puissant. Il permet notamment de gérer les clefs publiques SSH de vos utilisateurs en les déployant sur un ensemble de machines.

Nous allons ici étudier le module authorized_key fourni par ansible.

Déploiement de clef SSH

Dans un premier temps nous allons parler de la tâche permettant de déployer les clefs SSH.

Pour déployer une clef SSH dans son plus simple appareil voici la syntaxe à adopter:

- authorized_key: user=root key="ssh-rsa AAAA....= ansible@ansiblehost"

Si vous souhaitez définir que la clef SSH de l'utilisateur est la seule qui doit exister pour cet utilisateur, il suffit d'appeler le paramètre exclusive:

- authorized_key:
  user: "root"
  key: "ssh-rsa AAAA....= ansible@ansiblehost"
  exclusive: yes

Suppression de clef SSH

Pour supprimer une clef SSH, il suffit de définir la state de notre tâche à absent

- authorized_key:
  user: "root"
  key: "ssh-rsa AAAA....= ansible@ansiblehost"
  state: absent

Déploiement de clefs SSH multiples par hôtes/groupes d'hôtes

Nous allons corser un peu la tâche ici en lui ajoutant une notion de boucle afin de définir des ensembles de clefs à déployer sur des hôtes:

- authorized_key:
  user: "{{ item.user }}"
  key: "{{ item.key }}"
  exclusive: "{{ item.exclusive | default('no') }}"
  state: "{{ item.state | default('present') }}"
  with_items: "{{ ssh_authorized_keys }}"

Nous avons ici une boucle lisant la variable ssh_authorized_keys. Chaque élément de ce tableau nécessitera le champ user et key. Les champs exclusive et state sont optionnels avec des valeurs par défaut étant le standard du module.

Admettons maintenant que nous ayons un groupe linux_hosts dans notre inventaire ansible, nous allons éditer le fichier group_vars/linux_hosts.yml afin de lui attribuer des clefs SSH au moyen de notre variable:

ssh_authorized_keys:
  - user: root
    key: "ssh-rsa AAAAB...aB= ansible@bastion"
  - user: cafecreme
    key: "ssh-rsa AAAAA...qpX= cafecreme@restau"

Lorsque nous exécutons notre tâche sur notre groupe linux_hosts, nous allons déployer simultanement les deux clefs. Les hôtes n'étant pas dans ce groupe ne seront pas affectés si on appèle, par exemple, le groupe d'hôtes embarqué all.

Conclusion

Cette méthode est très pratique et permet de faire des changements de clefs SSH autorisées sur un nombre d'hôtes important et de le faire varier par groupe d'hôtes sans se compliquer la tâche.

AWS + Ansible: scheduler automatisé d’allumage/extinction d’instances

Amazon Web Services (AWS) est très bien interfacé avec Ansible. Nous allons voir dans cet article comment créer quelques économies d'infrastructure en designant un planficateur de démarrage et d'arrêt d'instances. Nous partirons du principe que vous avez déjà utilisé AWS et que vous avez déjà vu les bases d'Ansible. Nous utiliserons ici une syntaxe ansible 2.0

Architecture

L'architecture de notre scheduler est simple, un cron va faire tourner périodiquement ansible pour envoyer des ordres d'arrêt et extinction à la plateforme EC2 d'AWS en fonction d'un tag sur une instance. Les instances qui devront être démarrées à une heure précise auront le tag starthour, les instances qui devront être éteintes à une heure précise le tag stophour

Playbook Ansible

Nous allons créer un playbook qui va se baser sur un rôle que nous nommerons amazon_scheduler. Editons notre fichier de playbook /etc/ansible/playbooks/amazon_scheduler.yml

---
- hosts: localhost
  connection: local
  become: false
  roles:
    - amazon_scheduler

La configuration de ce playbook permet de la lancer avec un utilisateur non privilégié sur la machine locale.

Role Ansible

Créons maintenant notre role ansible

mkdir -p /etc/ansible/roles/amazon_scheduler/{tasks,vars,default}

Variables

Peuplons le fichier de variables par défaut du rôle (default/main.yml) avec une seule variable correspondant à la region AWS sur laquelle nous allons travailler. Note: Vous pourrez bien entendu externaliser cette région afin d'agir sur plusieurs points du globe facilement.

---
region: 'eu-west-1'

Dans le répertoire de variables du rôle, créez un fichier _vars/defaulthost.yml contenant le hash vide suivant:

---
aws_env: {}

Et un fichier au nom d'hôte de la machine faisant tourner le cron contenant les informations d'authentification AWS réelles.

---
 aws_env:  AWS_ACCESS_KEY_ID: KAMLD8ELAM64AD
 AWS_SECRET_ACCESS_KEY=MSLds51A54cdsq+sdf1s+d2d+dqs59

Note: je vous conseille de sceller le fichier contenant les informations AWS dans une vault ansible chiffrée. Cette méthode permettra ainsi d'avoir une configuration statique pour votre runner automatique et, par défaut d'utiliser la configuration de votre machine personnelle pour vos tests ou actions manuelles.

Actions

Editez maintenant le fichier tasks/main.yml et indiquez le contenu suivant:

---
- name: "Initialize some host specific configuration"
  include_vars: "{{ item }}"
  with_first_found:
    - "{{ ansible_hostname }}.yml"
    - "default_host.yml"
  tags: [stop, start]

- include: stop.yml
  environment: "{{ aws_env }}"
  tags:
    - stop

- include: start.yml
  environment: "{{ aws_env }}"
  tags:
    - start

Notre point d'entrée va dans un premier temps charger nos variables dynamiques par host avant de déclarer 2 sous actions, une pour démarrer et une pour stopper des machines. Chaque sous action dispose en prime d'un accès aux variables d'environnement AWS chargées dynamiquement. Passons maintenant à l'action stop (tasks/stop.yml)

---
- name: EC2 | Stopping instances
  ec2:
    instance_ids: []
    instance_tags:
      stophour: "{{ stophour | default(ansible_date_time.hour) }}"
      region: '{{ region }}'
      state: stopped
      wait: True

Cette action est très simple, on demande au module ec2 d'arrêter toutes les machines avec le tag stophour correspondant à une variable ansible stophour arbitraire. Cette action est appliquée sur notre région statique, spécifiée dans default/main.yml. On attend également que les machines soient éteintes avant de rendre la main. De manière analogue,

---
- name: EC2 | Starting instances
  ec2:
    instance_ids: []
    instance_tags:
      starthour: "{{ starthour | default(ansible_date_time.hour) }}"
    region: '{{ region }}'
    state: running
    wait: True

Arborescence Ansible finale

Vous devriez maintenant avoir l'arborescence ansible suivante:

  • playbooks
    • amazon_scheduler.yml
  • roles
    • amazon_scheduler
      • defaults
        • main.yml
      • tasks
        • main.yml
        • start.yml
        • stop.yml
      • vars
        • default_host.yml
        • ansible_launcher_hostname.yml

Test du playbook

Vous pouvez maintenant tester un scheduler avec une heure précise en invoquant le playbook avec une heure précise et une action précise:

ansible-playbook /etc/ansible/playbooks/amazon_scheduler.yml --tags stop -e stophour=21

Planification cron

Vos schedulers ansible sont prêts. Vous n'avez plus qu'à aller sur votre machine hôte de déploiement Ansible et créer le cron suivant:

0 * * * * ansible ansible-playbook /etc/ansible/playbooks/amazon_scheduler.yml

Configuration AWS

Allez maintenant sur votre console AWS et ajoutez les tags stophour et starthour sur les instances que vous souhaitez allumer et éteindres à horaires fixes. AWS EC2 TagsNote: vous pouvez bien entendu utiliser le module _ec2_tags_ d'Ansible pour industrialiser cette action

Conclusion

Vous pouvez désormais économiser vos ressources AWS (et accessoirement faire quelques économies) en planifiant simplement l'allumage et l'extinction de vos machines. Quelques idées intéressantes en vous basant ce scheduler:

  • Allumer des machines uniquement sur des périodes ouvrées pour vos développeurs
  • Activer certaines machines de production à certaines heures afin d'absorber les pics de charge (ce n'est pas la seule solution, loin de là).

Ansible: dump & injection de dump

Ansible est un excellent orchestrateur d'infrastructure agent-less. Nous n'avions pas encore eu le temps de le couvrir sur ce blog, malgré que je l'utilise beaucoup au quotidien pour orchestrer, mettre en production, maintenir une infrastructure de quelques centaines de serveurs.

Ansible est capable de faire des dumps et restaurations de fichiers SQL de façon très simple et, depuis la version 2.0, ceux-ci peuvent être compressés.

Injecter un dump SQL compressé

hosts: mysql_server_01
tasks:
  - mysql_db: name="translations" state=import target="/nfs/backup/translations.sql.gz"

Ansible va ici se connecter sur le serveur mysql_server_01 renseigné dans votre fichier hosts et injecter le fichier translations.sql.gz directement dans la base translations. Si elle n'existe pas il la créera. Si vous utilisez ansible 1.X, vous ne pouvez spécifier qu'un fichier non compressé, il faudra une étape unarchive intermédiaire.

Dumper une base de données en SQL

hosts: mysql_server_01
tasks:
  - mysql_db: name="translations" state=dump target="/nfs/backup/translations.sql.gz"

Ansible exporte ici notre base translations vers le fichier translations.sql.gz

Si vous le souhaitez vous pouvez également utiliser les paramètres _loginhost, _login_password, loginport et _loginhost afin de configurer votre connexion MySQL. Dans le cas contraire le fichier $HOME/.my.cnf sera lu.

Backup multi bases

Voici en extra de cet article, un cas d'usage qui permet de faire un backup multi bases en utilisant une boucle ansible

hosts: mysql_server_01
tasks:
  - mysql_db: name="{{ item }}" state=export target="/nfs/backup/{{ item }}.sql.gz"
    with_items:
      - translations
      - movies
      - mysql
      - owncloud