Illustration de l'article

Ansible partie 3 : Les rôles

Écrit par Sogilis

Dans notre précédent article, nous avons vu comment installer l’application ipfs sur notre serveur. Nous avons fait le tout de manière très simple, avec un playbook global. Cela est peu élégant si nous souhaitons déployer plusieurs services sur la même machine. Et comment faire pour découper une suite de tâches simples que nous voudrions pouvoir réutiliser ? La réponse à ces deux questions se trouve dans les rôles Ansible. Les rôles sont une manière un peu plus élégante d’inclure des tâches Ansible au sein d’autres tâches en déclarant des dépendances.

Transformer notre playbook en rôle

Commençons par créer un rôle simple correspondant exactement au playbook que nous avions la dernière fois :

---
- hosts: perrin
  sudo: yes
  tasks:
    - go-install: name=go-ipfs package=github.com/jbenet/go-ipfs/cmd/ipfs

Le rôle que nous allons créer va nécessiter de créer une arborescence dans un dossier roles/ qui va contenir notre role nommé ipfs :

  • roles/ipfs/tasks/main.yml :
      ---
      - go-install: name=go-ipfs package=github.com/jbenet/go-ipfs/cmd/ipfs
      

Notre nouveau playbook va maintenant contenir une dépendance envers ce rôle :

---
- hosts: perrin
  sudo: yes
  roles:
    - ipfs

Un second rôle : cjdns-docker

Ce qui est intéressant, c’est d’avoir plusieurs rôles. Nous allons donc voir comment déployer cjdns en utilisant docker avec un rôle.

  • Ce rôle définit des variables dans roles/cjdns-docker/vars/main.yml :

      ---
      name: cjdns
      

  • et se compose de tâches définies dans un fichier roles/cjdns-docker/tasks/main.yml :

      ---
      - shell: docker pull mildred/cjdns
      - systemd-docker-service: name='{{name}}'
      - service: name='{{name}}' enabled=yes state=restarted
      - docker-datadir: name='{{name}}' volume=/etc/cjdns file=/cjdroute.conf
        register: cjdroute_conf
      - file: src='{{cjdroute_conf.file}}' dest='/etc/docker-{{name}}.conf' state=link
      

La variable name permet de nommer différents éléments du système correspondant au rôle. Elle a une valeur par défaut, mais pourra se redéfinir au niveau du playbook principal. Pour provisionner le container cjdns, il nous faut :

  • récupérer le container avec un docker pull
  • déclarer un service systemd pour démarrer le container comme un service système
  • activer le service qui vient d’être créé
  • trouver le chemin vers le fichier de configuration cjdns qui se trouve dans un volume docker séparé
  • et créer un lien symbolique de ce fichier vers /etc.

Nous voyons avec cela comment utiliser les variables. Pour utiliser une variable, il existe la syntaxe {{ *variable_name* }}. Cette syntaxe correspond au langage de template Jinja2 et nécessite d’inclure les variables entre quotes pour rester valide YAML. Voir la documentation Ansible sur les variables.

Les modules peuvent définir des variables de leur propre chef, mais il est également possible d’enregistrer le résultat d’un module dans une variable avec la syntaxe register: *variable_name*. Pour visualiser les champs disponibles, il faut utiliser l’option -v sur la ligne de commande lorsqu’on exécute le playbook. Dans notre exemple, un champ file est disponible, et sera utilisé avec la syntaxe {{cjdroute_conf.file}}.

L’utilisation de ce rôle dans le playbook principal nécessite de définir une variable au moment de l’inclusion du rôle :

---
- hosts: perrin
  sudo: yes
  vars:
    name: perrin
  roles:
    - ipfs
    - role: cjdns-docker
      name: '{{name}}-cjdroute'

Si vous tentez d’exécuter ce role, il vous manquera les modules systemd-docker-service et docker-datadir. Leur conception n’implique rien de nouveau et voici leur code :

  • library/systemd-docker-service :

      #!/bin/bash
    
      changed=false
      failed=false
      res_code=0
      msg=Success
      exec 3>&1 >/dev/null 2>&1
      trap 'failed=true res_code=1 msg="Failed at line $LINENO"' ERR
    
      name=
      cmdline=
      after=
      . "$1"
    
      changed=true
    
      cat <<EOF >/tmp/$$.1
      [Unit]
      Description=Container $name
      Requires=docker.io.service
      After=docker.io.service $after
    
      [Service]
      Restart=always
      ExecStart=/usr/local/bin/docker-start-run $name $cmdline
      ExecStop=/usr/bin/docker stop -t 2 $name
    
      [Install]
      WantedBy=multi-user.target
      EOF
    
      :> /tmp/$$.2
      chmod +x /tmp/$$.2
      cat >>/tmp/$$.2 <<"EOF"
      #!/bin/bash
    
      name="$1"
      shift
    
      if ! id="$(/usr/bin/docker inspect --format="{{.ID}}" "$name-data" 2>/dev/null)"; then
        echo "Reusing $id"
        docker run --name "$name-data" --volumes-from "$name-data" --entrypoint /bin/true "$@"
      fi
    
      /usr/bin/docker rm "$name" 2>/dev/null
      set -x
      exec /usr/bin/docker run --name="$name" --volumes-from="$name-data" --rm --attach=stdout --attach=stderr "$@"
    
      if docker inspect --format="Reusing {{.ID}}" "$name" 2>/dev/null; then
        exec /usr/bin/docker start -a "$name"
      else
        exec /usr/bin/docker run --name="$name" --volumes-from="$name-data" --attach=stdout --attach=stderr "$@"
      fi
      EOF
    
      if ! cmp /tmp/$$.2 /usr/local/bin/docker-start-run; then
          mv /tmp/$$.2 /usr/local/bin/docker-start-run
          chmod +x /usr/local/bin/docker-start-run
          changed=true
      fi
    
      if ! cmp /tmp/$$.1 /etc/systemd/system/$name.service; then
          mv /tmp/$$.1 /etc/systemd/system/$name.service
          systemctl daemon-reload
          changed=true
      fi
    
      rm -f /tmp/$$.1 /tmp/$$.2
    
      cat <<EOF >&3
      {
          "failed":  $failed,
          "changed": $changed,
          "msg":     "$msg"
      }
      EOF
      exit $res_code
      

  • library/docker-datadir :

      #!/bin/bash
    
      changed=false
      failed=false
      res_code=0
      msg=Success
      

  ---
  - shell: docker pull mildred/cjdns
  - systemd-docker-service: name='{{name}}'
  - service: name='{{name}}' enabled=yes state=restarted
  - docker-datadir: name='{{name}}' volume=/etc/cjdns file=/cjdroute.conf
    register: cjdroute_conf
  - file: src='{{cjdroute_conf.file}}' dest='/etc/docker-{{name}}.conf' state=link
  
  exec 3>&1 >/dev/null 2>&1
  trap 'failed=true res_code=1 msg="Failed at line $LINENO"' ERR

  name=
  image_name=
  volume=
  file=
  variable=file
  . "$1"
  : ${image_name:="$name-data"}

  changed=false
  res="$(docker inspect -f "{{(index .Volumes "$volume")}}" "$image_name")$file"

  cat <<EOF >&3
  {
      "failed":    $failed,
      "changed":   $changed,
      "msg":       "$msg",
      "$variable": "$res"
  }
  EOF
  exit $res_code
  

Un dernier rôle pour accéder par ssh à notre container

Nous voudrions pouvoir accéder au container docker en utilisant SSH avec un utilisateur particulier. Ceci peut se faire de manière générique pour tout container Docker, et c’est comme cela que nous l’implémenterons. Nous définirons un rôle pour ajouter un accès ssh, et l’utiliseront pour le docker cjdns.

Nous aurons besoin de nsenter, donc nous l’installons avec docker. Ensuite, nous créons un utilisateur avec l’UID 0 (afin qu’il ait les permissions d’exécuter nsenter), et nous spécifions une clef ssh de login, avec la commande nsenter qui nous permettra d’entrer dans le container :

  • roles/docker-ssh/vars/main.yml :

      ---
      user: '{{name}}'
      shell: /bin/bash
      

  • roles/docker-ssh/tasks/main.yml :

      ---
      - command: docker run --rm -v /usr/local/bin:/target jpetazzo/nsenter
      - user: name="{{user}}" uid=0 createhome=yes shell=/usr/sbin/nologin home='/home/{{user}}'
      - authorized_key: key="{{ssh_key}}" user='{{user}}' key_options='command="nsenter --target $(docker inspect --format {{ "{{.State.Pid}}" }} {{name}}) --mount --uts --ipc --net --pid {{shell}}"'
      

Pour utiliser ce nouveau rôle, nous allons le déclarer comme dépendance dans roles/cjdns-docker/meta/main.yml :

---
dependencies:
  - role: docker-ssh
    when: "'{{ssh_key}}' != ''"

Et notre playbook principal va être augmenté afin de définir la variable ssh_key pour le rôle docker-cjdns (qui sera ensuite héritée par le rôle docker-ssh instancié par dépendance) :

---
- hosts: perrin
  sudo: yes
  vars:
    admin_sshkey: ssh-rsa AAA...vgcv
    name: perrin
  roles:
    - ipfs
    - role: cjdns-docker
      name: '{{name}}-cjdroute'
      ssh_key: '{{admin_sshkey}}'

Nous avons vu dans cet article comment organiser notre code Ansible afin qu’il soit plus maintenable. Les rôles définissent des unités de code comme pourraient l’être des fonctions dans un langage plus classique. Il n’est pas possible d’invoquer un rôle directement au milieu d’une liste de tâches dans ce cas, (la directive include et le module include vars existent), mais il est possible de définir des dépendances entre rôles. En guise d’exercice, il est laissé au soin du lecteur de décomposer le module systemd-docker-service en un rôle séparé.

Illustration de l'article
comments powered by Disqus