Incus booste ma productivité pour le développement de rôles Ansible
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 updatesudo 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 :
sudo adduser $USER incus-adminsudo adduser $USER incus
Initialisez Incus avec une configuration de base :
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 :
incus image list images:
Pour lancer un conteneur basé sur l’image Ubuntu 22.04 :
incus launch images:ubuntu/22.04 firstincus launch images:ubuntu/22.04 second
Pour lancer une VM avec Ubuntu 22.04 :
incus launch images:ubuntu/22.04 ubuntu-vm --vm
Pour lister les instances :
incus list
Pour lancer ou arrêtez des conteneurs :
incus start thirdincus stop second
Pour supprimer des conteneurs :
incus delete secondincus delete third --force
Pour lancer un conteneur avec des limites de ressources :
incus launch images:ubuntu/22.04 limited --config limits.cpu=1 --config limits.memory=192MiB
On peut exécuter des commandes dans une ressource :
incus exec first -- bash
On peut aussi se connecter :
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.
-
Lancer un conteneur avec Incus :
Terminal window incus launch images:ubuntu/22.04 ubuntu-ansible -
Configuration de l’image : Connectez-vous au conteneur et installez les paquets nécessaires :
Terminal window incus shell ubuntu-ansibleapt updateapt install -y openssh-server python3 sudo python3-pipuseradd -m -s /bin/bash ansiblemkdir -p /home/ansible/.sshecho 'ssh-ed25519 xxxxxxxxxx' > /home/ansible/.ssh/authorized_keyschown -R ansible:ansible /home/ansible/.sshchmod 600 /home/ansible/.ssh/authorized_keysecho 'ansible ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/ansiblePASSWD=$(date | md5sum | cut -c1-8)echo "ansible:$PASSWD" | chpasswd -
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_ed25519Warning: 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/proThe programs included with the Ubuntu system are free software;the exact distribution terms for each program are described in theindividual files in /usr/share/doc/*/copyright.Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted byapplicable law.ansible@ubuntu-ansible:~$ -
Vérifiez aussi que vous pouvez utiliser sudo sans mot de passe avec
sudo -i
par exemple. Quittez le conteneur en tapant [CTRL] + [D] -
Créons l’image :
Terminal window incus stop ubuntu-ansibleincus publish --alias ubuntu_ansible ubuntu-ansibleincus 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
:
ansible-galaxy collection init steph.cloudcd steph/cloud/rolesansible-galaxy role init my_rolecd 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 :
molecule init scenario
Editez le fichier molecule/default/molecule.yml
:
---dependency: name: galaxydriver: name: defaultplatforms: - name: sshd-test image: ubuntu_ansible memory: 2048 cpus: 2provisioner: name: ansibleverifier: 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 :
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 > prepareWARNING Skipping, prepare playbook not configured.
Vérifions que l’instance est bien créée :
ì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 :
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 :
molecule loginINFO Running default > loginWelcome 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/proLast login: Tue Oct 15 20:12:51 2024 from 10.150.61.1ansible@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 :
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 :
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…