L’inversion du modèle de connexion d’Ansible avec Ansible-pull : killer feature ?

le 06/11/2018 par Yohan Lascombe
Tags: Cloud & Platform

Il n’est plus utile de le présenter, Ansible s’est imposé comme un des outils standards pour gérer le déploiement d'infrastructures. Le modèle de connexion qu’il utilise (push), et son opposé (le pull) a suscité beaucoup de débats sur les avantages et inconvénients car c’est un élément clivant dans le choix d’un outil d’Infrastructure as Code. Nous allons parler aujourd’hui d’une fonctionnalité d’Ansible relativement peu connue même si elle existe depuis longtemps. Cette fonctionnalité, nous allons le voir, propose de passer en modèle pull sur lequel Ansible repose et permet ainsi d’annuler quelques contraintes associées.

Nous allons débuter par un petit rappel sur le modèle push, nous présenterons ensuite ce qu’est réellement Ansible-pull de manière théorique puis avec un exemple d’utilisation. Nous creuserons quelques points de détails sur le mode multi-machine et la gestion des secrets. Nous terminerons par un ensemble de limitations qui nous font dire qu’il faut réserver l’utilisation d’Ansible-pull à certains cas.

En prémisse, un petit rappel sur le modèle push d’Ansible

Les outils d’Infrastructure as Code, dont l’objectif est de définir sous forme de code des infrastructures, sont souvent classés selon différents modèles. Un des modèles régulièrement utilisé est un classement sur le modèle de connexion push versus pull :

  • dans un modèle push : c’est une machine tierce (la machine de contrôle) qui contient le code d’Infrastructure as Code et qui se connecte sur les machines pour vérifier et appliquer si besoin les actions pour obtenir l’état cible : c’est le modèle par défaut d’Ansible.
  • à l’inverse, dans le modèle pull, ce sont directement les machines contrôlées qui initient une connexion sur un serveur central pour récupérer l’état cible dans lequel elles doivent être et appliquer les changements nécessaires si besoin. Ce modèle impose l’installation d’un agent sur chaque machine à contrôler. Nous pouvons citer par exemple Puppet et Chef.

Le modèle de connexion push/pull détermine donc le fait que les machines contrôlées sont actrices ou non pour appliquer les changements.

Ci-dessous une illustration du modèle push :

La machine de contrôle est la machine depuis laquelle Ansible est exécuté. Elle contient donc une copie du code source qui définit l’état de l’infrastructure souhaitée. La machine de contrôle peut être un poste de travail (pour des phases de mise au point) ou un orchestrateur dans une usine de développement. Contrainte Ansible oblige, cette machine sera forcément sur un système d’exploitation Linux/BSD (à l’inverse des machines contrôlées qui peuvent aussi être sur Windows).

Quasiment aucun prérequis n’est nécessaire sur les machines contrôlées : la machine de contrôle doit pouvoir se connecter dessus (en SSH, WinRM, ...) et Python doit être installé (il est toutefois possible de l’installer si ce n’est pas le cas via Ansible avec le module raw). Aucun agent spécifique n’est nécessaire.

Voici quelques avantages et inconvénients qui découlent du modèle push. Nous avons fait le choix de ne pas être exhaustifs et de ne cibler que ceux intéressants pour la suite : + faible empreinte sur l’infrastructure : (quasiment) pas de prérequis sur les machines contrôlées (pas d’agent à installer) + modèle simple : une seule machine, la machine de contrôle, réalise l’orchestration des tâches : cela permet de réaliser simplement ce qu’on effectue depuis les postes de travail.

- nécessité d’avoir un accès SSH/WinRM depuis la machine de contrôle sur les machines contrôlées. Cela impose donc d’avoir un flux ouvert entre la machine de contrôle et la machine contrôlée et également d’avoir un compte applicatif avec lequel se connecter.

Cela paraît anodin mais par exemple dans une approche où l’on se positionne comme un cloud provider (cloud interne par exemple), si l’équipe sécurité souhaite utiliser Ansible pour effectuer le durcissement des VMs ou d’installer des agents, cela signifie qu’elle doit avoir accès aux VMs : ce qui ne sera pas forcément accepté par les équipes projet.

Et concrètement, Ansible-pull, quesaco ?

Ansible-pull est simplement une commande dans la galaxie des commandes Ansible et non un agent qui s’exécute en tâche de fond sur le système d’exploitation.

Liste des binaires présents dans l’installation du paquet Ansible :

Liste des commandes AnsibleListe des commandes Ansible

À son lancement, Ansible-pull va télécharger le code qui décrit l’état cible attendu depuis un dépôt de code. La commande se terminera à la fin de l’exécution. S’il n’y a pas un élément (orchestrateur) externe qui relance la commande, Ansible-pull ne se ré-exécutera pas. Nous verrons plus loin les limites inhérentes à ce modèle.

Les machines qui exécutent la commande Ansible-pull deviennent donc « actrices » de leur changement.

Illustration avec Ansible-pull :

Illustration ansible-pull

Dans ce modèle, le binaire Ansible-pull doit être installé sur chaque machine contrôlée.

Comme vous l’avez certainement noté, il y a deux points qui peuvent être relevés :

  • la machine de contrôle est facultative : Ansible-pull est capable d’exécuter lui même le code de manière autonome. Rien ne vous oblige à utiliser une machine de contrôle dans la plupart des cas.
  • selon la documentation Ansible, il n’est pas possible d’utiliser Windows en tant que machine de contrôle. Il doit pouvoir être possible d’installer Ansible sur Windows avec pip mais le comportement obtenu n’est pas du tout garanti. Nous déconseillons son utilisation sur Windows.

Prenons un exemple pour illustrer l’utilisation d’Ansible-pull

Nous allons provisionner des machines virtuelles sur un cloud provider (AWS ici) puis installer Ansible-pull et configurer une exécution régulière via une tâche planifiée (crontab).

Illustration de l’exemple : Illustration de l'exemple

  1. Depuis notre poste de travail (ou depuis une Usine de Développement en mode industrialisé), lancement du code Terraform pour provisionner sur AWS les instances.
  2. Chaque instance exécute un script cloud-init au démarrage : ce script installe Git et Ansible-pull (en installant Ansible) puis ajoute un fichier crontab pour demander une exécution planifiée pour lancer la commande Ansible-pull régulièrement

Quelques extrait de code : Template Terraform qui permet de générer le fichier de configuration cloud-init exécuté au démarrage (user_data.tpl):


#cloud-config
apt_upgrade: true
apt_sources:
# PPA shortcut:
#  * Setup correct apt sources.list line
#  * Import the signing key from LP
#
#  See https://help.launchpad.net/Packaging/PPA for more information
#  this requires ‘add-apt-repository’
– source: « ppa:ansible/ansible »    # Quote the string
packages:
 – software-properties-common
 – ansible
 – git
write_files:
 – path: /etc/ansible-pull/cron.sh
   owner: root:root
   permissions: ‘0744’
   content: |
       ansible-pull provision.yml –url ${git_repository} –checkout step1 –accept-host-key -i localhost -e node_type=${node_type} -e env=${env}
runcmd:
 # do not create this file in write_files part since /etc/cron.d does not yet exists
 – [ sh, -c, « echo ‘*/30 * * * * root /etc/ansible-pull/cron.sh > /var/log/ansible-pull.log 2>&1’ > /etc/cron.d/ansible_pull_cronjob »]

Dans ce fichier, les variables incluses dans les marqueurs ${} seront résolues par Terraform lors de la création des machines virtuelles.

Analysons les paramètres fournis à la commande Ansible-pull : Détail de la commande ansible-pull

Le code Terraform qui permet de créer les instances est très classique. Ci-dessous l’extrait de code qui permet de définir le template du cloud-init et l’instance AWS qui utilise ce template en tant que via les user_data :


data "template_file" "script-web" {
  template = "${file("${path.module}/user_data.tpl")}"

  vars {
    node_type = "web"
    env = "${var.env}"
    git_repository = "${var.git_repository}"
  }
}

resource "aws_instance" "puller-web" {
  ami = "ami-4d46d534"
  instance_type = "t2.micro"
  key_name = "${var.aws_keypair}"

  vpc_security_group_ids = ["${aws_security_group.ansible-pull-demo-almost-openbar.id}"]
  associate_public_ip_address = true
  source_dest_check = false
  user_data = "${data.template_file.script-web.rendered}"

  subnet_id = "${aws_subnet.main.id}"

  tags {
    Trigramme = "${var.aws_trigram}"
    Topic = "ansiblepulldemo"
    Name = "puller-web"
  }
}

Contenu du playbook qui est exécuté par Ansible-pull :


- hosts: localhost
  tasks:
  - name: Write env_infos file in /etc folder
    copy:
      content: |
        node_role: {{ node_type }}
        env: {{ env }}
       dest: /etc/env_infos

Le code complet de l’exemple se trouve dans le dépôt https://github.com/ylascombe/ansible-pull-demo sous le tag step1.

Dans cet exemple, le dépôt de code qui contient ce playbook est constitué seulement de ce fichier mais la structure du dépôt cible possédera généralement la structure classique de dépôt de code Ansible :


site.yml                  # playbook principal

roles/
    common/               # rôle qui effectue les tâches communes
        tasks/            #
            main.yml      #  <-- fichier contenant les tâches
        handlers/         #
            main.yml      #  <-- handlers 
        templates/        #  <-- fichiers templates
            ntp.conf.j2   #  <------- templates end in .j2
        defaults/         #
            main.yml      #  <-- Variables propres aux rôle
   
    webtier/              # rôle qui effectue les tâches spécifiques aux VMs “web”

Possibilité d’utiliser Ansible-pull pour faire du multi-machine ?

Comme Ansible-pull est fait pour travailler en local, le mode de connection qu’il utilise par défaut est “local”. Cela évite ainsi d’ouvrir une connection SSH sur la machine elle-même. Faute de documentation claire à ce sujet, nous avons vérifié ce comportement dans des tests et c’est le cas.

Cela signifie que sans précisions, chaque machine décrite dans l’inventaire sera considérée comme locale. Ainsi, si vous souhaitez exécuter Ansible-pull sur des machines distantes, il est nécessaire de préciser pour ces machines distantes le paramètre ansible_connection=ssh. Hormis cette précision, somme toute importante, il n’est pas nécessaire de modifier votre code avec des local_action ou delegate_to pour le faire fonctionner.

Illustration par du code :

Fichier d’inventaire de test


[remote_vms]
10.20.30.40 ansible_connection=ssh ansible_user=ubuntu

[remote_without_ssh_cnx]
10.20.30.41

Pour le playbook ci-dessous :


- hosts: remote_vms remote_without_ssh_cnx
  tasks:
    - name: Write test-file.txt in /tmp folder
      copy:
        content: "file content for instance {{ ansible_hostname }}"
        dest: /tmp/test-file-{{ ansible_hostname }}.txt

Le même code aura 2 comportements différents pour la machine du groupe remote_vms et celle du groupe remote_without_ssh_cnx :

  • Le fichier /tmp/test-file-10.20.30.40.txt sera créé sur la machine avec l’IP 10.20.30.40
  • Le fichier /tmp/test-file-10.20.30.41.txt sera créé sur la machine localhost (celle qui exécute Ansible-pull)

Cela nous a amené à nous demander s’il est possible d’utiliser les delegate_to avec Ansible-pull. Ici aussi, la réponse est oui mais ce ne sera pas aussi facile qu’en mode Ansible normal. Nous pouvons distinguer 4 cas : le schéma permet de les illustrer : Illustration des 4 cas de délégation

Détail du cas 2 : Depuis une machine distante, délégation à la machine qui exécute Ansible-pull (localhost donc) :

Aucun problème, cela fonctionne. Exemple de playbook :


- hosts: remote_vms[0]
  tasks:
    - name: Create a test file with a delegate_to localhost
      delegate_to: localhost
      copy:
        content: "file content generated with delegate_to localhost"
        dest: /tmp/test-file-delegate-to.txt

Sans surprise, le fichier /tmp/test-file-delegate-to.txt sera généré sur la machine qui exécute Ansible-pull.

Détail du cas 3 et cas 4 : Délégation à une machine distante (depuis localhost ou une machine distante)

Ici, sans aide pour lui indiquer comment établir la connection SSH, Ansible rencontrera une erreur : “Unreachable ERROR! SSH…”

Exemple de playbook :


- hosts: localhost
  tasks:
    - name: Create a test file with a delegate_to remote_vms[0]
      delegate_to: remote_vms[0]
      copy:
        content: "file content generated with delegate_to remote_vms[0]"
        dest: /tmp/test-file-delegate-to.txt

Même si nous venons de voir qu’il est possible de faire de la gestion multi-machines avec Ansible-pull en précisant à Ansible comment le faire, il est important de noter que ce n’est clairement pas le cas d’utilisation attendu : nous vous invitons à challenger la raison pour laquelle vous souhaitez faire cela et essayer de trouver une solution plus simple.

Et comment gérer mes secrets ?

Dans le mode push par défaut d’ansible, ansible-vault est utilisé sur la machine de contrôle pour chiffrer et déchiffrer l’ensemble des secrets. À l’exécution, seul le sous ensemble de secret concernant une machine est envoyé sur les machines. La machine de contrôle constitue donc l’élément principal à sécuriser.

Ansible-pull continue d’offrir la possibilité d’utiliser ansible-vault : il n’y a donc pas de problème technique.

Par contre, le changement de modèle de connection implique que la clé de déchiffrement doit être distribuée sur chaque instance qui exécute Ansible-pull : toutes les machines qui utilisent Ansible-pull doivent donc avoir la clé de déchiffrement.

A minima, il est donc nécessaire de découper le fichier vault qui contenait l’ensemble des secrets en autant de fichiers que de typologies de machine possible, avec bien entendu une clé de chiffrement différente pour chaque fichier. Ce découpage entraînera également des duplication de secrets dans plusieurs fichiers, car plusieurs typologies de machines peuvent avoir besoin du même secret (token de connection au dépôt de binaire par exemple). La duplication de code n’est pas élégante, et implique d’être rigoureux au moment de la mise à jour d’un secret pour ne pas en oublier.

Dans le code source de l’exemple que nous avons déroulé plus haut, les modifications suivantes ont été effectuées pour implémenter un cas fonctionnel :

  1. 1. modification du template de cloud-init en ajoutant deux fichiers sur chaque machine qui exécutera du Ansible-pull :
    • /etc/ansible-pull/.secret : contient le mot de passe de déchiffrement pour ansible-vault : Il sera référencé au lancement de la commande Ansible-pull avec le paramètre --vault-password-file
    • /etc/ansible-pull/inventory : fichier d’inventaire : il contient 2 groupes qui seront nommés en fonction des variables node_type et env. Cela permettra de référencer les group_vars associées (group_vars/web pour les machines web, group_vars/db pour les machines db), et notamment les fichiers chiffrés avec ansible-vault qu’ils contiennent.

Modification de code dans le fichier de configuration de cloud-init:


write_files:
 - path: /etc/ansible-pull/.secret
   owner: root:root
   permissions: '0600'
   content: |
     ${secret}
 - path: /etc/ansible-pull/inventory
   owner: root:root
   permissions: '0600'
   content: |
     [${node_type}]
     localhost
 
     [${env}]
     localhost
 - path: /etc/ansible-pull/cron.sh
   owner: root:root
   permissions: '0744'
   content: |
       ansible-pull provision.yml --url ${git_repository} --accept-host-key -i /etc/ansible-pull/inventory -e node_type=${node_type} -e env=${env} --vault-password-file=/etc/ansible-pull/.secret
  1. 2. génération d'un template-file différent pour chaque type de machine (rôle)

data "template_file" "script-web" {
 template = "${file("${path.module}/user_data.tpl")}"
 
 vars {
   node_type = "web"
   env = "${var.env}"
   git_repository = "${var.git_repository}"
   secret = "${var.web_role_secret}"
 }
}
 
data "template_file" "script-db" {
 template = "${file("${path.module}/user_data.tpl")}"
 
 vars {
   node_type = "db"
   env = "${var.env}"
   git_repository = "${var.git_repository}"
   secret = "${var.db_role_secret}"
 }

Chaque template_file contient les variables qui sont propres au rôle de machine associé.

  1. 3. Ainsi, nous avons pu modifier notre playbook pour qu’il utilise ces nouvelles variables:

  - hosts: localhost
    tasks:
      - name: Write env_infos file in /etc folder
        copy:
          content: |
            node_role: {{ node_type }}
            env: {{ env }}
            my_secret: {{ vault_example_secret }}
           dest: /etc/env_infos

Le code complet de l’exemple se trouve dans le dépôt https://github.com/ylascombe/ansible-pull-demo sous le tag step3.

Une approche basée sur l’utilisation d’un tiers de sécurité comme Hashicorp Vault (à ne pas confondre avec ansible-vault) reste en revanche tout à fait pertinente : chaque instance porte sa propre identité auprès de Vault et peut donc accéder uniquement aux secrets qui la concerne. La mise en place d’un tel outil dépasse largement le cadre de cet article, nous ne détaillerons donc pas ce point.

Au final, killer feature ?

Le fait qu’Ansible-pull soit relativement peu connu n’est peut-être pas anodin : il constitue un réel apport mais présente plusieurs limites importantes :

  • C’est un modèle qui permet d’inverser le modèle de connexion mais le fait que ce soit une simple commande et pas un agent ne lui permet pas d’être aussi abouti que les outils d’Infrastructure as Code qui sont basés sur le modèle pull avec agent.
      • Le modèle d'authentification est uniquement porté par le dépôt de code, il n’y pas de gestion des rôles.
        • Ansible-pull n'apporte pas de solution pour l’auditabilité et le reporting : les logs d’exécution ne sont pas centralisés sur un serveur, c’est à la charge des utilisateurs de mettre en place ce mécanisme.En inversant le sens d’initiation de la connection, les agents permettent de traverser les firewall et zones réseaux de niveaux d’isolation différents : cette aptitude est parfois importante.
  • L’empreinte sur les machines contrôlées est plus forte : il est nécessaire d’installer le paquet Ansible, qui est un ensemble de scripts Python.
  • La distribution des secrets sur les différentes machines impose d’avoir une vraie stratégie de découpage et de la rigueur dans son application.
  • Une des grandes forces d’Ansible en mode push est qu’il permet de gérer facilement l’orchestration de tâches sur plusieurs machines. Cette capacité d’orchestration est souvent mise en avant dans le choix d’un outil d’Infra as Code lorsque celui-ci doit être capable de faire du déploiement applicatif en plus de la gestion de configuration. Ici, avec le mode pull, cette aptitude est annulée.
  • Plus généralement, la plupart des cas d’utilisations (hardening, installation d’agent…) que l’on peut imaginer peuvent être traités autrement : il faut donc porter attention à ne pas tomber dans la facilité supposé de tout faire avec Ansible-pull.

L’utilisation du mode pull d’Ansible n’est pas aussi simple qu’avec des outils nativement prévu pour cela. En revanche, il permet tout de même de répondre à des besoins spécifiques et précis, nous vous proposons 3 exemples d’utilisation :

Use Case 1 : en remplacement/complément du cloud-init pour les tâches avancées

cloud-init est souvent utilisé par les cloud providers pour offrir aux utilisateurs la possibilité de modifier/configurer les instances à leurs création et/ou démarrage. cloud-init permet d’effectuer de nombreuses tâches simple (création d’utilisateurs, de fichiers, configuration réseau, dépôt de clés SSH, …) ou plus complexes (installer certains utilitaires ou renforcer certains paramètres de sécurité). Les tâches les plus complexes sont souvent effectuées via des scripts shell appelés depuis le cloud-init.

Avec les différents systèmes d’exploitations et versions, il est assez difficile de maintenir et tester l’ensemble des scripts cloud-init/shell. Effectuer ces tâches avec du code Ansible plutôt que cloud-init permet d’améliorer la maintenabilité. Ici, utiliser Ansible-pull via cloud-init pour effectuer les tâches plus complexes est un bon cas d’utilisation.

Use Case 2 : gérer la conformité des noeuds de manière récurrente sur les machines non accessibles

S’il n’est pas possible de donner accès à des instances mais qu’il est tout de même nécessaire d’assurer la conformité avec un ensemble de règles, Ansible-pull peut-être installé et configuré au démarrage de la VM pour effectuer ces tâches.

Use Case 3 : permettre à une équipe qui ne dispose pas d’un compte applicatif sur les VMs de proposer un service un minimum managé

Par exemple, dans un entreprise où l’équipe qui gère un middleware (agent de logs par exemple) souhaite effectuer l’installation et la configuration de cet agent sans avoir de compte applicatif sur celles-ci : ces actions peuvent être effectuées avec Ansible-pull.

En synthèse

C’est indéniable, Ansible possède de super atouts, et cette fonctionnalité contribue à en réduire ses points faibles. Cependant, nous pensons qu’il faut toutefois essayer de limiter l’utilisation d’Ansible-pull pour des cas précis et ne pas essayer de systématiser son utilisation en challengeant chaque cas d'utilisation potentiel.

L’utilisation du mode pull implique également de perdre la facilité d’orchestrer des tâches entre plusieurs machines, qui est une réelle force du mode push d’Ansible.

Liens

Lien vers le dépôt de code pullé par Ansible-pull

https://github.com/ylascombe/ansible-pull-demo