Automatiser la gestion des serveurs avec Ansible

Ansible est un outil qui permet – entre autre choses – d’automatiser l’installation, le déploiement et la gestion de vos serveurs. Vous utilisez certainement ssh pour installer les programmes dont vous avez besoin et configurer vos serveurs. Peut-être même avez-vous créé des scripts pour que tout ça aille plus vite. Ansible permet de créer des « Playbooks », qui ne sont autre que des scripts à la sauce Ansible, et permettent de configurer vos serveurs.

Sa grande force est qu’il est agentless, autrement dit, rien n’est à placer sur vos serveurs. Vous installez Ansible sur votre laptop par exemple, et le tour est joué. Vous pouvez ensuite lancer l’install de vos 40 serveurs de base de données en une seule commande ! Ça vous émoustille ? Alors allons-y !

L’install

Vous vous doutez bien qu’il faut avant tout installer Ansible sur votre control machine. Rien de bien compliqué, ça tourne sur à peu près tout sauf Windows et il y a plusieurs procédures au choix : du git clone au apt-get. Elles sont pour la plupart détaillées sur la page d’install de Ansible.

En ce qui me concerne, j’ai voulu tenter de l’installer sur mon Mac via Macports, pour plus de facilité et des mises à jour en toute souplesse. L’install se passe assez simplement via un sudo port install ansible. Sur Mac, et c’est valable aussi pour l’install via Homebrew, au lieu d’avoir la config dans /etc/ansible/ tout se passe dans /opt/local/etc/ansible/, ça vous évitera de chercher.

Dans le cas où vous suivriez les instructions d’installation de la doc Ansible – laquelle suggère de procéder à l’installation via PIP – il n’y a pas de fichier de config par défaut. Pourtant, Ansible le cherchera dans /etc/ansible, de la même manière que le fichier hosts.

Deux solutions sont possibles. Soit vous créez un fichier .ansible.cfg dans votre HOME, et vous précisez bien dans celui-ci où se trouve le fichier hosts, soit vous créez /etc/ansible et vous y placez le fichier hosts.

Configuration des hôtes

Avant tout, il faut bien comprendre que Ansible repose sur le protocole ssh. Ainsi, c’est via ce protocole qu’il se connectera à vos serveurs. Par défaut sur le port 22 évidemment. Il tentera aussi par défaut de se connecter via une clef ssh de ~/.ssh/. De plus, l’utilisation d’OpenSSH permet de lire le fichier de configuration ~/.ssh/conf. Néanmoins, l’usage d’OpenSSH est conditionné par une version récente de ce dernier. Dans le cas contraire, Ansible fallback sur une librairie en Phyton.

Les hôtes se configurent dans /etc/ansible/hosts :

# les crochets permettent de définir des groupes
# on pourra ainsi appliquer la même conf à tous les serveurs de backup
# un serveur peut appartenir à plusieurs groupes 
[backup]
# on fait ici référence à un serveur présent dans le fichier conf de .ssh
jarjar

# définir un port non standard
www.buzut.fr:5309

# il est possible d'adresser plusieurs serveurs qui suivent un nommage spécifique
# en utilisant la notation intervalle des REGEX
www[01:50].buzut.fr

Beaucoup d’autres options sont disponibles, vous trouverez toutes les possibilités dans l’Inventory.

Une fois les hôtes configurés, vous pouvez tester une première commande afin de voir que la configuration fonctionne. Le module ping affiche simplement « pong », il n’a rien à voir avec le ping ICMP :

ansible all -m ping
buzut | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}

# il se peut que vous ayez un problème 
buzut | UNREACHABLE! => {
    "changed": false, 
    "msg": "SSH encountered an unknown error during the connection. We recommend you re-run the command using -vvvv, which will enable SSH debugging output to help diagnose the issue", 
    "unreachable": true
} 

# après le -vvv et quelque recherches, Ansible créé un dossier .ansible dans votre home
# il se peut que ce répertoire n'appartienne pas au bon utilisateur
# pour remédier à cela
sudo chown -R votre_user ~/.ansible

Dernier petit détail, nous avons entraperçu dans les commentaires qu’il y a de puissants moyens de sélectionner les hôtes. On peut en sélectionner un unique ou tout un groupe, appliquer des REGEX pour en exclure ou préciser un intervalle. Pour tirer toute la souplesse et la puissance de cette notation, lisez rapidement la page patterns de la doc.

La ligne de commande

Ansible possède une ligne de commande. Pourquoi donc ? Eh bien c’est la même chose que pour les scripts. Parfois vous faites un script parce que vous savez que vous aurez à refaire cette manipulation, parfois, c’est juste du one shot – ou c’est super court – donc vous tapez directement ce que vous voulez faire. Par exemple, admettons que vous vouliez redémarrer tous les serveurs de base de données, vous pourriez faire :

# database est notre groupe de serveur [database]
# l'option -a permet de préciser l'argument de la commande
ansible database -a "/sbin/reboot"

# par défaut, ansible lance la commande sur 5 serveurs en parallèle en faisant des forks de son processus
# si votre groupe contient beaucoup de serveurs – front[1-500] par exemple – il peut être judicieux de l'augmenter
# vous pouvez gérer cela avec le paramètre -f
ansible database -a "/sbin/reboot" -f 25

Vous pouvez utiliser l’option -u username pour exécuter une commande depuis un autre utilisateur, -k pour passer en root et entrer le mot de passe root

La CLI permet d’appeler des modules pour exécuter les commandes que nous voulons. Dans l’exemple ci-dessus, nous n’avons rien précisé car le module par défaut est command et c’est ce que nous voulions. Admettons maintenant que nous voulions copier un fichier sur l’ensemble de nos serveurs web :

# le module copy permet de transférer un fichier
ansible web -m copy -a "src=/Users/Buzut/Desktop/super-site.conf dest=/etc/apache2/sites-available/super-site.conf"

Vous pouvez en savoir plus sur les modules disponibles directement sur la page de docs dédiée.

Les playbooks

J’en ai rapidement parlé en introduction, les playbooks sont des fichiers en YAML qui décrivent les tâches qu’Ansible doit accomplir sur vos serveurs. Les playbooks utilisent une syntaxe très simple : on définit les hôtes, les variables éventuelles puis on créé des tâches. Chaque tâche possède un nom et appelle des modules (les mêmes qu’en CLI).

Sachez que vous pouvez lancer vos playbooks avec l’option --syntax-check, qui comme son nom l’indique, s’assure qu’il n’y a pas d’erreur dans la syntaxe de vos playbooks. Ensuite, vous pouvez également utiliser --check afin de simuler un play sans effectuer aucun changement. Dans cette dernière option, Ansible va se connecter au(x) serveur(s) et lancer les modules en leur demandant de ne pas effectuer de modifications (tous les modules ne sont pas compatibles, auquel cas, ils ne retourneront pas les changements potentiels).

---
# les documents YAML commencent toujours par "---"

# le nom de l'hôte ou groupe concerné
- hosts: webserver

  # on déclare les éventuelles variables
  vars:
    http_port: 80
    domain: buzut.fr

  # nom de l'utilisateur du compte (lance celui du .ssh/conf par défaut)
  remote_user: root

  # ici débute la liste des tâches
  tasks:

  # nom d'une tâche
  - name: ensure server is up to date
    # nom du module à utiliser
    apt:
      update_cache: yes
      upgrade: full

  # installons apache
  - name: install apache2
    # il existe une syntaxe alternative, plus condensée
    # apt: name=apache2 update_cache=yes state=latest

    apt:
      name: apache2
      update_cache: yes
      state: latest

  # s'assurer que le mod rewrite est actif (l'activer sinon)
  - name: enabled mod_rewrite
    apache2_module:
      name: rewrite
      state: present

  # le module template fonctionne de manière similaire à copy (vu plus haut)
  # mais il injecte dynamiquement les variables nécessaires
  - name: write the apache config file
    template:
      src: /Users/Buzut/.ansible/templates/vhost.conf
      dest: /etc/apache2/sites-available/buzut.conf

  - name: enable vhost
    command: a2ensite buzut

  - name: restart apache
    service:
      name: httpd
      state: restarted

Pour que vous compreniez bien le fonctionnement des variables, voici à quoi ressemble le fichier de template vhost.conf :

<VirtualHost *:{{ http_port }}>
    ServerAdmin webmaster@{{ domain }}
    ServerName {{ domain }}
    ServerAlias www.{{ domain }}
    DocumentRoot /var/www/{{ domain }}

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined

    <Directory /var/www/{{ domain }}/>
       Options -Indexes +FollowSymLinks
       AllowOverride All
    </Directory>
</VirtualHost>

Comme expliqué en commentaire, le module template charge le fichier de template et pour chaque {{ variable }}, injecte la valeur correspondante depuis les variables définies au début. Vous rendez-vous compte de la puissance de la chose ?!

Les variables

Nous avons rapidement pu voir les variables dans la partie précédente. Néanmoins, Ansible fourni de nombreuses variables sur l’environnement serveur. Le module setup nous renseigne sur ces dernières :

ansible buzut -m setup
buzut | SUCCESS => {
    "ansible_facts": {
        "ansible_all_ipv4_addresses": [
            "37.59.21.45"
        ], 
        "ansible_all_ipv6_addresses": [
            "2001:41d0:8:852d::", 
            "fe80::225:90ff:fe7c:fa36"
        ], 
        "ansible_architecture": "x86_64", 
        "ansible_bios_date": "01/03/2014", 
        "ansible_bios_version": "3.0a", 
        […]
        "ansible_lsb": {
            "codename": "trusty", 
            "description": "Ubuntu 14.04.4 LTS", 
            "id": "Ubuntu", 
            "major_release": "14", 
            "release": "14.04"
        }, 
       […]

Toutes ces variables peuvent être utilisées dans vos playbooks. On y accède de la manière suivante :

# variable simple
{{ ansible_architecture }}

# pour accéder à une propriété
{{ ansible_lsb.major_release }}

# pour accéder à un tableau (première propriété)
{{ ansible_all_ipv4_addresses[0] }}

Vous commencez à percevoir la puissance des variables. Mieux, on peut alimenter ces dernières directement depuis une commande exécutée sur le serveur ! Voyons donc cela :

---
- hosts: buzut
  tasks:
    - name: determine vhosts
      command: /bin/ls /etc/apache2/sites-enabled/
      register: vhosts    

vhosts contient maintenant la liste des éléments présents dans /etc/apache2/sites-enabled/ tel que l’affiche la commande ls.

Il y a un type de variable un peu particulier qui peut s’avérer très utile : ce sont les variables prompt. Au moment de l’exécution, l’utilisateur qui lance le script se voit demander la valeur qu’il veut attribuer à la variable. Par exemple dans le cas d’un playbook permettant d’installer un nouveau serveur, inutile – peut-être même dangereux – de l’appliquer à all s’il n’y a qu’un seul nouveau serveur à paramétrer. Par ailleurs, c’est fastidieux d’éditer le playbook avant exécution. Il suffit donc de procéder comme ceci :

    ---
    - hosts: "{{ servernames }}"
      vars_prompt:
        - name: "servernames"
          prompt: "Which hosts would you like to setup?"
          private: no
      tasks:
        […]

Vous noterez que je précise ici private: no. En effet, par défaut, ansible considère le prompt comme password-sensitive et n’affiche donc pas les caractères.

Enfin, sachez qu’il est également possible de passer des variables au playbook directement depuis la CLI au moment de l’invocation de ce dernier. Reprenons le même exemple que ci-dessus en enlevant le vars_prompt :

    ---
    - hosts: "{{ servernames }}"
      tasks:
        […]

Il suffit simplement d’appeler le playbook de cette manière : ansible-playbook baseServer.yml --extra-vars "servernames=db6". Sachez également que les variables prennent des guillemets lorsqu’elles débutent une ligne. Petite illustration :

    # ici la variable doit être entourée de guillemets
    - hosts: "{{ servernames }}"
      tasks:
        […]

    # mais pas dans ce cas là
    - name: Enable VirtualHost
      file:
        src: /etc/nginx/sites-available/{{ domain }}
        dest: /etc/nginx/sites-enabled/{{ domain }}
        state: link

Debug, le var_dump d’Ansible

Récupérer des variables c’est bien, savoir ce qu’il y a dedans, c’est mieux. C’est justement l’objet de debug. Reprenons l’exemple précédent :

---
- hosts: buzut
  tasks:
    - name: determine vhosts
      command: /bin/ls /etc/apache2/sites-enabled/
      register: vhosts
    - debug: msg="{{ vhosts }}"
---
# on lance le playbook
ansible-playbook test.yml 

PLAY ***************************************************************************

TASK [setup] *******************************************************************
ok: [buzut]

TASK [determine vhosts] ********************************************************
changed: [buzut]

TASK [debug] *******************************************************************
ok: [buzut] => {
    "msg": {
        "changed": true, 
        "cmd": [
            "/bin/ls", 
            "/etc/apache2/sites-enabled"
        ], 
        "delta": "0:00:00.014652", 
        "end": "2016-03-24 23:21:32.612755", 
        "rc": 0, 
        "start": "2016-03-24 23:21:32.598103", 
        "stderr": "", 
        "stdout": "buzut.conf\default.conf", 
        "stdout_lines": [
            "buzut.conf", 
            "default.conf"
        ], 
        "warnings": []
    }
}

PLAY RECAP *********************************************************************
cloud                      : ok=3    changed=1    unreachable=0    failed=0   

Nous voyons ici que si nous voulons accéder à la valeur du premier vhost, il faut faire {{ vhosts.stdout_lines[0] }}, tout simplement.

Les variables présentent bien d’autres possibilités, on peut par exemple récupérer des informations sur un autre hôte. Je vous laisse découvrir toute cette magie directement dans la doc.

Les boucles

Vous avez pu constater que les variables peuvent avoir des propriétés et contenir des tableaux. Qui dit tableau dit boucle. Nous allons reprendre notre exemple précédent et effacer tous les vhosts déjà présents avant d’ajouter le notre. C’est parti :

---
- hosts: webserver
  vars:
    http_port: 80
    domain: buzut.fr
  remote_user: root
  tasks:
    […]
    - name: determine vhosts
      command: /bin/ls /etc/apache2/sites-enabled/
      register: vhosts

    # on enlève le (ou les) vhosts par défaut
    - name: deregister default vhosts
      command: a2dissite {{ item }}
      with_items: 
        - "{{ vhosts.stdout_lines }}"

    # maintenant on peut rajouter notre vhost
    - name: enable vhost
      command: a2ensite buzut
    […]

Bien entendu, les boucles recèlent encore bien d’autres secrets, et comme à mon habitude, je vous laisse avec la doc si vous souhaiter creuser le sujet.

Le notify pattern

Qu’est-ce donc que cela me direz-vous ? On peut dire que le notify pattern est la programmation événementielle Ansible flavoured en quelque sorte. Ok, je m’explique. Plutôt que de lancer une action plusieurs fois – comme de redémarrer Apache – après l’activation d’un module ou la modification d’un fichier de config, les handlers ne sont lancés que si le fichier de conf change réellement. En outre, bien que plusieurs tâches puissent nécessiter une même action, l’action en question ne sera lancée qu’après l’exécution de tous les blocs tâches.

Illustrons ce comportement en reprenant notre exemple :

---
- hosts: webserver

  vars:
    http_port: 80
    domain: buzut.fr
  remote_user: root

  tasks:
    […]
    - name: determine vhosts
      command: /bin/ls /etc/apache2/sites-enabled/
      register: vhosts

    - name: deregister default vhosts
      command: a2dissite {{ item }}
      with_items: 
        - "{{ vhosts.stdout_lines }}"
      notify:
        - restart apache2

    - name: enabled mod_rewrite
      apache2_module:
        name: rewrite
        state: present
      notify:
        - restart apache2

    - name: enable vhost
      command: a2ensite buzut
      notify:
        - restart apache2
    […]

    handlers:
    - name: restart apache2
      service: 
        name: apache2 
        state: restarted

Ainsi, dès lors que nos anciens vhosts sont effacés, que le nouveau est installé et que le mod_rewrite est activé, Apache sera redémarré. Si nous avions référencé directement le module apache2 avec l’instruction de redémarrer dans chacun des blocs de tâche, nous aurions redémarré Apache 3 fois…

À ce stade, comme tout notre code est dans le même playbook, il est vrai que l’intérêt de notify s’avère plutôt limité. Dans un cas comme celui-ci, il nous suffit en effet de redémarrer Apache une fois en fin de fichier et le tour est joué. Cependant, lorsque nos playbooks sont plus complexes, on les sépares en plusieurs morceaux logiques, qui peuvent ainsi être réutilisables dans d’autres playbooks. Là, tout de suite vous percevez sans doute bien mieux l’intérêt de notify.

Puisque l’on parle de séparer nos playbooks en différentes parties, il est temps d’introduire les includes !

Meilleure organisation avec include

L’include dans Ansible, c’est exactement comme le include de PHP. Cela vous permet de scinder vos tâches en différents fichiers et de les importer au besoin dans vos playbooks. Imaginez par exemple que vous ayez une tâche qui se charge d’installer un dæmon de monitoring, vous voudrez certainement qu’elle s’exécute aussi bien sur vos serveurs de base de données, que sur vos serveurs front etc.

Sans l’include, vous devriez répéter ça dans tous vos playbooks, tandis qu’avec cette petite magie, une simple référence suffit.

On définit donc nos tâches dans un fichier dédié, on l’appelle pour l’exemple setupMonitoring.yml.

---
- name: Install monitoring agent
  apt:
    name: blabla

# on ajoute toutes les tâches que l'on veut

Comme ce fichier sera intégré directement dans un playbook, nous n’avons pas à référencer tasks:, nous plaçons directement notre liste de tâches.

Une fois ce fichier créé et enregistré, admettons pour l’exemple que nous enregistrions toutes nos tâches dans tasks et que les playbooks soient à la racine, voilà à quoi ressemblerait notre playbook.

---
- hosts: dbservers
  vars:
    email: mon@email.fr

  vars_prompt:
    - name: "dbrootpasswd"
      prompt: "Database root password"

  tasks:
    - include: tasks/commonSetup.yml
    - include: tasks/setupMonitoring.yml
    - include: tasks/installMySQL.yml

    # on peut bien entendu mélanger des includes et des tâches classiques
    - name: Install htop
      apt:
        name: htop

Il y a quelque chose que je ne vous ai pas dit concernant les includes. Nous avons vu comment inclure des tâches, mais l’on peut aussi inclure des playbooks. Tout dépend de l’endroit où l’include est utilisé.

Dans l’exemple précédent nous l’avons inséré après tasks:, il est donc évident qu’il ne peut inclure que des tâches. Cependant, s’il est inséré au premier niveau du playbook, il insèrera un playbook. Il est ainsi possible de créer des meta-playbooks. Attention cependant, car la syntaxe de ces fichiers devra être celle d’un playbook !

C’est simple mais redoutable de puissance ! Depuis sa version 1.2, Ansible a mis en place un mécanisme qui pousse encore plus loin cette logique afin de rendre les playbooks plus organisés, plus clairs et plus réutilisables, j’ai nommé : les rôles.

Les rôles constituent un moyen d’automatiser le chargement des variables, des tâches et des handlers grâce à une convention d’arborescence de fichiers. C’est une automatisation des includes qui permet une grande souplesse et une très bonne organisation des tâches complexes. J’y consacre un article entier !

Les objets de valeur au coffre

C’est une bonne pratique de versionner ses scripts Ansible. Cependant, qui dit versionning, dit souvent dépôt distant. Il va sans dire que certains éléments de configurations ne sont pas à laisser en clair sur n’importe quel dépôt : clef ssh, fichiers de conf avec mot de passe etc.

D’une part, tous les collaborateurs n’ont pas forcement à y avoir accès, d’autre part, un Gitlab ou Github, même avec un dépôt privé et même s’il est installé sur vos serveurs, peut toujours être compromis.

Bien entendu, séparer les fichiers sensibles est envisageable. Mais vous n’aurez pas le confort d’avoir l’intégralité de vos éléments d’install ou deploy d’un simple petit coup de git clone, ou équivalent avec SNV ou Mercurial.

Pour répondre à cette problématique, Ansible propose un coffre – Vault dans la langue de Shakespeare. Vous spécifiez un mot de passe et Ansible chiffre le fichier (par défaut en AES). Lors de l’édition de fichier, Ansible ouvrira votre l’éditeur définit dans la variable $EDITOR. Veillez à en définir un si ce n’est pas fait. Le cas échéant, votre fichier sera ouvert avec vi.

L’usage du vault est très simple. Vous n’aurez que quatre commandes à retenir :

# chiffrer des fichiers
ansible-vault encrypt fichierA [fichierB …]

# afficher un fichier
ansible-vault view fichierA [fichierB …]

# éditer un fichier déjà chiffré
ansible-vault edit fichierA

# si jamais vous avez envie de déchiffrer un fichier précédemment chiffré
ansible-vault decrypt ficherA [fichierB …]

Lorsque vous désirez lancer un playbook qui nécessitera d’utiliser des fichiers présents dans le Vault, il faudra passer l’option --ask-vault-pass.

Enfin, il est possible d’utiliser des mots de passe différents pour différents fichiers. Cependant, tous les fichiers utilisés au sein d’un même Playbook doivent partager le même mot de passe.

Performances

Négligeable quand vous n’avez que quelques serveurs à traiter, les réglages qui influent sur les performances peuvent avoir un impact conséquent en terme de temps d’exécution si vous gérer un parc de serveur important. Passons en revue les optimisations qui permettent de doper les performances d’Ansible. Tout va se passer dans le fichier de config /etc/ansible/ansible.cfg ou /opt/local/etc/ansible/ansible.cfg.default (qu’il faudra d’ailleurs renommer pour lui enlever .default) sur OS X.

Forks

Nous en avons déjà rapidement parlé, Ansible gère par défaut les hôtes 5 par 5. Ce qui veut dire qu’il attendra que l’exécution de vos instructions soit terminé sur vos 5 serveurs avant de poursuivre sur d’autres. Vous pouvez évidemment, c’est ce que nous avons vu, préciser autre chose avec l’option -f. Néanmoins, si vous souhaitez toujours vous addresser à plus de serveurs en parallèle, autant modifier ce paramètre dans le fichier de configuration et ne plus avoir à la spécifier manuellement à chaque fois. Nous sommes là pour automatiser nos tâches après tout ! Il n’y a pas de règle spécifique, les deux principaux facteurs limitant seront la charge CPU et la charge réseau engendrés. Le réglage de 5 par défaut est extrêmement conservateur, si votre machine est décemment récente et que vous avez autre chose qu’un modem 56K, vous pouvez tout à fait passer cette variable à 20 et ajuster après avoir testé.

forks=20

Pipelining

Le pipelining permet de réduire le nombre de connexions ssh nécessaires. Par conséquent, la vitesse d’exécution des playbooks s’en trouvera grandement améliorée. Il est désactivé par défaut car certaines configurations nécessitant requiretty ne sont pas compatible, auquel cas il est possible d’utiliser le mode accéléré. Pour activer le pipelining :

pipelining=True

Exécution asynchrone et polling

Ansible se connecte à vos serveurs en ssh et ne rend la connection qu’une fois toutes les actions effectuées. Ainsi, le comportement par défaut est bloquant, et maintenir de nombreuses connections ssh ouvertes « pour rien » impacte négativement les performances. Il est cependant possible de lancer des opérations et de faire du polling afin de controler à intervales régulier l’état des processus ainsi lancés. De cette manière, vous pourrez lancer vos tâches sur plus de serveurs en parallèle.

# compilation du codec video x264, qui peut être assez longue
- name: compile x264
  environment: ffmpeg_env
  command: "{{ item }}"
  args:
    chdir: "{{ ffmpeg_source_dir }}/x264"
    creates: "{{ ffmpeg_bin_dir }}/x264"
  with_items:
    - ./configure --prefix={{ ffmpeg_build_dir }} --bindir={{ ffmpeg_bin_dir }} --enable-static
    - make
    - make install
    - make distclean
  # on déclare le temps maximum d'exécution
  async: 120
  # et intervalle de temps auquel vérifier l'état de l'opération
  poll: 10

Il est également possible de spécifier poll à 0. Auquel cas on lance l’opération sans en vérifier le statut, en présupposant que le résultat sera celui auquel on s’attend.

Avant de conclure, puisque nous visons à automatiser au maximum la gestion de nos systèmes, j’ai écrit un petit script pre-ansible, dispo sur Github. Ce dernier permet de configurer l’ajout d’un nouveau serveur au système : ajout dans la config ssh, ajout au fichier hosts d’Ansible et paramétrage du ssh du serveur pour une connexion automatique par clef ssh.

En guise de conclusion, je vous laisse avec un exemple de playbook assez fourni. Il s’agit d’un article de Digital Ocean sur la configuration d’un serveur Apache avec Ansible.

Alors, est-ce qu’Ansible va révolutionner votre vie ? Quel est selon vous son plus gros atout ?

Il y a déjà une réponse à cet article :

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *