Aller au contenu

Incus booste ma productivité pour le développement de rôles Ansible

logo incus

Depuis longtemps, je développe des rôles Ansible en utilisant le couple Molecule/Vagrant pour créer des environnements de test reproductibles. Cependant, on ne peut que constater que la version 3.0 de Vagrant n’est plus une priorité pour HashiCorp. En plus de cela, les dernières versions commencent à poser des problèmes, notamment avec le plugin libvirt, qui est essentiel pour ceux qui, comme moi, travaillent régulièrement avec QEMU/KVM.

J’ai également tenté d’utiliser Proxmox pour créer ces ressources temporaires, malheureusement, l’expérience a été tout aussi décevante. Les performances n’étaient pas à la hauteur, et j’ai souvent rencontré des problèmes de gestion des VMs et des conteneurs.

Ces soucis, couplés à la lenteur au démarrage des machines virtuelles, m’ont poussé à chercher des alternatives plus efficaces. C’est ainsi que j’ai découvert Incus, qui annonce offrir une solution beaucoup plus rapide et légère pour déployer des ressources. Après quelques tests, je pense que le passage à Incus va complètement décupler ma production, en réduisant considérablement les temps de démarrage et de configuration de mes environnements de développement.

Voyons comment mettre en œuvre Incus dans mon cas d’usage.

C’est quoi Incus ?

Incus est une plateforme de gestion de conteneurs et de machines virtuelles, offrant une expérience utilisateur similaire à celle du cloud public. Contrairement aux machines virtuelles traditionnelles, Incus permet d’utiliser des conteneurs système, qui sont beaucoup plus légers, rapides à démarrer, et efficaces en ressources. Il supporte une grande variété de distributions Linux, ce qui en fait une solution idéale pour des environnements de test reproductibles.

Installation d’Incus et Configuration

Pour installer Incus sur une distribution Linux, utilisez le gestionnaire de paquets adapté à votre système :

  • Debian/Ubuntu :

    Terminal window
    sudo apt update
    sudo apt install incus
  • Arch Linux :

    Terminal window
    sudo pacman -S incus
  • Fedora :

    Terminal window
    sudo dnf install incus

Une fois l’installation terminée, vous devez ajouter l’utilisateur courant aux groupes nécessaires pour gérer Incus sans utiliser sudo :

Terminal window
sudo adduser $USER incus-admin
sudo adduser $USER incus

Initialisez Incus avec une configuration de base :

Terminal window
incus admin init --minimal

Les commandes essentielles d’Incus

Incus mets à disposition un certain nombre d’images pour les conteneurs et VMs. Pour lister toutes ces images, tapez la commande suivante :

Terminal window
incus image list images:

Pour lancer un conteneur basé sur l’image Ubuntu 22.04 :

Terminal window
incus launch images:ubuntu/22.04 first
incus launch images:ubuntu/22.04 second

Pour lancer une VM avec Ubuntu 22.04 :

Terminal window
incus launch images:ubuntu/22.04 ubuntu-vm --vm

Pour lister les instances :

Terminal window
incus list

Pour lancer ou arrêtez des conteneurs :

Terminal window
incus start third
incus stop second

Pour supprimer des conteneurs :

Terminal window
incus delete second
incus delete third --force

Pour lancer un conteneur avec des limites de ressources :

Terminal window
incus launch images:ubuntu/22.04 limited --config limits.cpu=1 --config limits.memory=192MiB

On peut exécuter des commandes dans une ressource :

Terminal window
incus exec first -- bash

On peut aussi se connecter :

Terminal window
incus shell firts

Pour plus de détails, consultez le guide officiel d’Incus.

Création d’une image personnalisée

En vérifiant, le contenu de l’image ubuntu, j’ai pu me rendre compte que les services systemd fonctionnaient. Par contre OpenSSH n’est pas installé. Pour Ansible, il faut donc créer une image personnalisée.

  1. Lancer un conteneur avec Incus :

    Terminal window
    incus launch images:ubuntu/22.04 ubuntu-ansible
  2. Configuration de l’image : Connectez-vous au conteneur et installez les paquets nécessaires :

    Terminal window
    incus shell ubuntu-ansible
    apt update
    apt install -y openssh-server python3 sudo python3-pip
    useradd -m -s /bin/bash ansible
    mkdir -p /home/ansible/.ssh
    echo 'ssh-ed25519 xxxxxxxxxx' > /home/ansible/.ssh/authorized_keys
    chown -R ansible:ansible /home/ansible/.ssh
    chmod 600 /home/ansible/.ssh/authorized_keys
    echo 'ansible ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/ansible
    PASSWD=$(date | md5sum | cut -c1-8)
    echo "ansible:$PASSWD" | chpasswd
  3. Vérifions que l’on peut se connecter à l’instance :

    Terminal window
    incus list
    +----------------+---------+----------------------+-----------------------------------------------+-----------+-----------+
    | NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS |
    +----------------+---------+----------------------+-----------------------------------------------+-----------+-----------+
    | ubuntu-ansible | RUNNING | 10.150.61.126 (eth0) | fd42:7644:95eb:bad8:216:3eff:fe6d:1da6 (eth0) | CONTAINER | 0 |
    +----------------+---------+----------------------+-----------------------------------------------+-----------+-----------+
    ssh ansible@10.150.61.126 -i ~/.ssh/id_ed25519
    Warning: Permanently added '10.150.61.126' (ED25519) to the list of known hosts.
    Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.8.0-45-generic x86_64)
    * Documentation: https://help.ubuntu.com
    * Management: https://landscape.canonical.com
    * Support: https://ubuntu.com/pro
    The programs included with the Ubuntu system are free software;
    the exact distribution terms for each program are described in the
    individual files in /usr/share/doc/*/copyright.
    Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
    applicable law.
    ansible@ubuntu-ansible:~$
  4. Vérifiez aussi que vous pouvez utiliser sudo sans mot de passe avec sudo -i par exemple. Quittez le conteneur en tapant [CTRL] + [D]

  5. Créons l’image :

    Terminal window
    incus stop ubuntu-ansible
    incus publish --alias ubuntu_ansible ubuntu-ansible
    incust image list -c l
    +----------------------+
    | ALIAS |
    +----------------------+
    | ubuntu_ansible |
    +----------------------+

Notre image est prête !

Maj : J’ai écrit un guide sur l’automatisation de la création d’image Incus avec Ansible ou Packer.

Création du code Molecule

Commencez par initialiser une collection et un rôle avec ansible-galaxy :

Terminal window
ansible-galaxy collection init steph.cloud
cd steph/cloud/roles
ansible-galaxy role init my_role
cd my_role

Ajoutez une tache dans le fichier tasks/main.yml :

---
- name: Task is running from within the role
ansible.builtin.debug:
msg: "This is a task from my_role."

Pour créer le scenario, tapez les commandes suivantes :

Terminal window
molecule init scenario

Editez le fichier molecule/default/molecule.yml :

---
dependency:
name: galaxy
driver:
name: default
platforms:
- name: sshd-test
image: ubuntu_ansible
memory: 2048
cpus: 2
provisioner:
name: ansible
verifier:
name: ansible

Modifiez le fichier create.yml pour créer et gérer des conteneurs avec Incus :

---
- name: Create
hosts: localhost
connection: local
gather_facts: false
no_log: "{{ molecule_no_log }}"
vars:
default_private_key_path: "{{ lookup('env', 'HOME') }}/.ssh/id_ed25519"
default_public_key_path: "{{ default_private_key_path }}.pub"
default_ssh_user: ansible
default_ssh_port: 22
default_memory: 1024
default_cpus: 1
platform_defaults:
private_key_path: "{{ default_private_key_path }}"
public_key_path: "{{ default_public_key_path }}"
ssh_user: "{{ default_ssh_user }}"
ssh_port: "{{ default_ssh_port }}"
image_name: ""
name: ""
memory: "{{ default_memory }}"
cpus: "{{ default_cpus }}"
# Merging defaults into a list of dicts is, it turns out, not straightforward
platforms: >-
{{ [platform_defaults | dict2items]
| product(molecule_yml.platforms | map('dict2items') | list)
| map('flatten', levels=1)
| list
| map('items2dict')
| list }}
tasks:
- name: Create
ansible.builtin.include_tasks: incus-create.yml
loop: '{{ platforms }}'
loop_control:
loop_var: platform

Créons ensuite le fichier incus-create.yml :

---
- name: Test if VM already Exist
ansible.builtin.shell:
cmd: "incus list -f json {{ platform.name }}"
register: instance_exist
- name: Convert to JSON
ansible.builtin.set_fact:
instance: "{{ instance_exist.stdout | from_json }}"
- block:
- name: Provision instance {{ platform.name }}
ansible.builtin.shell:
cmd: "incus launch {{ platform.image }} {{ platform.name }} --config limits.cpu={{ platform.cpus }} --config limits.memory={{ platform.memory }}MiB"
- name: Pause 5s
ansible.builtin.pause:
seconds: 5
when: instance | length == 0
- name: Test if VM already Exist
ansible.builtin.shell:
cmd: "incus list -f json {{ platform.name }}"
register: instance_exist
- name: Convert to JSON
ansible.builtin.set_fact:
state: "{{ instance_exist.stdout | from_json }}"
- name: Create instance config file
ansible.builtin.file:
path: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/instance_config.yml"
state: touch
mode: "0644"
- name: Get IP
vars:
jmesquery: "[*].state.network.eth0.addresses[?family==`inet`].address[]"
ansible.builtin.set_fact:
ip: "{{ state | community.general.json_query(jmesquery) | first }}"
- name: Register instance config for VM {{ platform.name }}
ansible.builtin.blockinfile:
path: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/instance_config.yml"
block: "- { address: {{ ip }} , identity_file: {{ platform.private_key_path }}, instance: {{ platform.name }}, port: {{ platform.ssh_port }}, user: {{ platform.ssh_user }} }"
marker: '# {mark} Instance : {{ platform.name }}'
marker_begin: 'BEGIN'
marker_end: 'END'
- name: Add to group molecule_hosts {{ platform.name }}
ansible.builtin.add_host:
name: "{{ ip }}"
groups: molecule_hosts

On peut vérifier que molecule peut créer l’image :

Terminal window
molecule create
...
TASK [Get IP] ******************************************************************
ok: [localhost]
TASK [Register instance config for VM sshd-test] *******************************
changed: [localhost]
TASK [Add to group molecule_hosts sshd-test] ***********************************
changed: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=12 changed=6 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
INFO Running default > prepare
WARNING Skipping, prepare playbook not configured.

Vérifions que l’instance est bien créée :

Terminal window
ìncus list
+----------------+---------+----------------------+-----------------------------------------------+-----------+-----------+
| NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS |
+----------------+---------+----------------------+-----------------------------------------------+-----------+-----------+
| sshd-test | RUNNING | 10.150.61.8 (eth0) | fd42:7644:95eb:bad8:216:3eff:feb1:cbee (eth0) | CONTAINER | 0 |
+----------------+---------+----------------------+-----------------------------------------------+-----------+-----------+

L’instance s’appelle sshd-test. Un petit test on lance le role :

Terminal window
molecule converge
..
PLAY [Converge] ****************************************************************
TASK [Gathering Facts] *********************************************************
ok: [test]
TASK [Include role] ************************************************************
included: my_role for test
TASK [my_role : Task is running from within the role] **************************
ok: [test] => {
"msg": "This is a task from my_role."
}
PLAY RECAP *********************************************************************
test : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

On vérifie que la commande molecule login fonctionne :

Terminal window
molecule login
INFO Running default > login
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.8.0-45-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
Last login: Tue Oct 15 20:12:51 2024 from 10.150.61.1
ansible@test:~$

Tout fonctionne. Reste à créer le code pour détruire l’instance.

Modifions le fichier destroy.yml :

---
- name: Destroy
hosts: localhost
connection: local
gather_facts: false
no_log: "{{ molecule_no_log }}"
tasks:
- name: Check instance config file exist
ansible.builtin.stat:
path: "{{ molecule_instance_config }}"
register: file_exist
- name: Destroy VM
when: file_exist.stat.exists
block:
- name: Load Instance config File
ansible.builtin.set_fact:
instance_conf: "{{ lookup('file', molecule_instance_config) | from_yaml }}"
- name: Destroy
ansible.builtin.include_tasks: incus-destroy.yml
loop: '{{ instance_conf }}'
loop_control:
loop_var: platform

Et le fichier incus-destroy.yml :

---
- name: Destroy VM
ansible.builtin.shell:
cmd: "incus delete {{ platform.instance }} --force"

On lance la destruction de l’instance :

Terminal window
molecule destroy
...
TASK [Destroy] *****************************************************************
included: /home/bob/Projets/outscale_srt20/cloud/roles/my_role/molecule/default/incus-destroy.yml for localhost => (item=(censored due to no_log))
TASK [Destroy VM] **************************************************************
changed: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
INFO Pruning extra files from scenario ephemeral directory

On vérifie que l’instance est bien détruite :

Terminal window
incus list

Tout est ok.

Plus loin

Cette solution, basée sur Incus, s’est révélée extrêmement efficace et bien plus rapide que l’utilisation de Vagrant ou Docker. Grâce à Incus, nous bénéficions d’une mise en place simple et fluide de conteneurs, évitant les lourdeurs des machines virtuelles tout en supportant pleinement les services comme systemd, ce qui peut être problématique avec Docker. De plus, le démarrage des conteneurs est beaucoup plus rapide, rendant les tests de rôles Ansible bien plus efficients

Bien que tout fonctionne, il reste plusieurs améliorations possibles. Par exemple, il serait pertinent d’automatiser complètement la création d’images compatibles Ansible pour éviter des configurations manuelles répétitives. De plus, je n’ai pas vérifié la gestion de plusieurs instances simultanément. L’intégration de l’API Incus au lieu des commandes shell serait également une bonne chose pour rendre le processus idempotent.

Cela me donne vraiment de pousser plus loin mon expérience avec Incus, comme la création de clusters. Une section complète dédiée à venir…