La mise en conteneur d’une application ou d’un service avec LXD

Magazine
Marque
Linux Pratique
HS n°
Numéro
45
Mois de parution
juin 2019
Spécialité(s)


Résumé

LXD est une suite d’outils visant à faciliter le management de la couche LXC (LinuX Containers) du noyau Linux. Les containers sont des environnements confinés où les applications peuvent s’exécuter sans attenter à l’intégrité du système hôte ainsi que des autres containers.


Body

Plusieurs raisons sont susceptibles de motiver la mise en container d’une application ou d’un service. On peut souhaiter les isoler par rapport au système et/ou limiter la quantité de ressources leur étant accessibles et/ou les rendre autosuffisants en terme de librairies (un peu à la manière de chroot qui, je le rappelle, n’a jamais été créé pour faire de la sécurité). LXD permet à l’administrateur de créer facilement l’enveloppe de ces applications ou services en orchestrant les couches LXC, cgroups et namespaces.

1. Les cgroups

Comme mentionné ci-dessus, il est possible de mettre une application ou un service dans un container afin de limiter leurs ressources accessibles sur le système. Je fais volontairement une distinction entre une application et un service. Une application est un fichier binaire qu’on exécute, cette exécution entraîne la création d’une entité active sur le système qu’on appelle le processus. C’est cette entité qui va consommer de la mémoire et du CPU pour faire tourner le code machine du binaire exécutable. Le cgroup est un moyen de créer des groupes de processus.

Dans un système UNIX, un processus peut avoir des fils. Par exemple, lorsque vous lancez votre navigateur web, chaque onglet correspond en réalité à un processus fils de votre navigateur. Dans ce cas-là, pas de problèmes pour grouper les processus : on part du père et on récupère tous les fils. Pas besoin d’une structure de données supplémentaire comme les cgroups. Cela se complique lorsqu’on parle de service.

Un service est une vision de plus haut niveau que l’application. On pourrait dire qu’un service regroupe X applications. Par exemple, si on considère un serveur web de e-commerce, on sait qu’il s’appuie sur trois composants : un serveur HTTP (Apache), un interpréteur de script pour du web dynamique (PHP) et une base de données (MySQL). Dans ce cas, les cgroups nous proposent une structure de donnée plus agile que le modèle père/fils pour grouper les processus.

Chaque ressource limitable par les cgroup est représentée par un subsystem. Il y en a un pour le CPU, les entrées/sorties disques, la mémoire, etc. On superpose une ou plusieurs politiques à chaque subsystem, les processus suivent ensuite une politique. Petit exemple avec la consommation de CPU. Nous allons créer deux politiques, l’une avec l’utilisation par défaut du CPU et une autre limitant volontairement le processus.

Commençons par installer ce qu’il faut pour travailler avec les cgroups :

[root@lxd ~]# yum install libcgroup libcgroup-tools

Créons une politique par défaut qui n’applique pas de limites particulières :

[root@lxd ~]# cgcreate -g cpu:/cpudefault

Créons une autre politique qui dit que lorsqu’un processus est positionné dans ce cgroup, il est dans un ratio de consommation de 2:1 sur cette ressource. Le subsystem associé au CPU associe une valeur arbitraire de 1024 à la totalité du CPU. Si on veut représenter un ratio de 2:1, il faut positionner cette valeur à 512.

[root@lxd ~]# cgcreate -g cpu:/cpulimited

[root@lxd ~]# cgset -r cpu.shares=512 cpulimited

Si on lance quatre fois de suite cette commande :

[root@lxd ~]# cgexec -g cpu:cpudefault stress -c 1 --timeout 200s &

La commande cgexec permet de lancer une commande en plaçant les processus dans un cgroup (-g). Les processus vont hériter des limitations de ce groupe. La commande stress est un utilitaire qui fait des calculs pour mettre le CPU au taquet. Ici, on lui fait utiliser 100 % d’un seul CPU (-c). On obtient la sortie suivante pour top :

PID   USER  PR  NI  VIRT  RES  SHR  S  %CPU  %MEM    TIME+  COMMAND

3669  root  20   0  7308   96    0  R  25.3   0.0  0:01.37   stress

3667  root  20   0  7308  100    0  R  25.0   0.0  0:01.96   stress

3671  root  20   0  7308  100    0  R  25.0   0.0  0:00.90   stress

3665  root  20   0  7308  100    0  R  24.7   0.0  0:03.52   stress

Chaque processus est dans un rapport 1:1, donc tout le monde se partage équitablement le CPU disponible.

Lançons maintenant cette commande :

[root@lxd ~]# cgexec -g cpu:cpudefault stress -c 1 --timeout 200s &

Et trois fois la même commande, mais positionnée dans notre cgroup limité au niveau CPU :

[root@lxd ~]# cgexec -g cpu:cpulimited stress -c 1 --timeout 200s &

Le résultat de top est différent :

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND

3689 root 20 0 7308 100 0 R 66.8 0.0 0:35.62 stress

3693 root 20 0 7308 96 0 R 11.3 0.0 0:05.52 stress

3703 root 20 0 7308 100 0 R 11.3 0.0 0:01.70 stress

3691 root 20 0 7308 100 0 R 11.0 0.0 0:09.15 stress

On voit que le processus lancé avec la politique cpudefault prend 66 % du CPU tandis que les 3 autres plafonnent à 11 %. Or, 11 x 3 = 33 et 33, c’est bien la moitié de 66. Le ratio 2:1 est donc respecté.

2. Les namespaces

Les rôles des namespaces est de créer un nouveau contexte système isolé pour le processus ciblé. Cette isolation se fait sur trois axes :

  1. Un nouvel espace de PID est initialisé et votre processus prend le numéro 1, tous les autres processus s'en sont allés ;
  2. Une nouvelle pile réseau est allouée au processus, aucun conflit possible avec les services réseaux de l'hôte ;
  3. Les systèmes de fichiers sont indépendants, on peut monter et démonter des volumes sans que cela ait d'incidences sur l'hôte.

Nous allons illustrer les namespaces avec la commande unshare. Cette commande permet de lancer un programme en se détachant du namespace parent. Nous allons l'utiliser pour lancer bash dans un nouveau namespace.

[root@lxd ~]# unshare --fork --pid --mount-proc bash

[root@lxd ~]# ps -aux

USER  PID  %CPU  %MEM     VSZ  RSS     TTY  STAT  START  TIME  COMMAND

root    1   0.0   0.1  115440  2024  pts/0     S  21:32  0:00  bash

root   10   0.0   0.1  155360  1872  pts/0    R+  21:32  0:00  ps -aux

Nous avons lancé bash comme fils de unshare (--fork) avec son propre espace de PID (--pid) et sa propre instance de /proc (--mount-proc). Dans ce nouveau namespace, bash a le PID 1 et il est donc le père de tous les processus et il est seul (enfin avec la commande ps qu'on a lancé). Voyons ce que ça donne depuis le système hôte avec un pstree :

[root@lxd ~]# pstree

systemd─┬─NetworkManager─┬─2*[dhclient]

│ └─2*[{NetworkManager}]

...

├─login───bash───unshare───bash

...

On voit bien la ligne de parenté systemd → login → bash → unshare → bash. Donc du point de vue de l'hôte, le processus bash issu du unshare est un processus comme un autre. Il a un PID tout à fait standard. Vous avez maintenant les clés de compréhension pour la couche LXC/LXD.

3. Les containers LXD

Avant LXD, il y eu LXC (LinuX Containers). Pour lever tout de suite l’ambiguïté, il ne s'agit pas de choisir LXD ou LXC c'est plutôt LXC et LXD. En effet, LXC est un ensemble d'outils et de librairies en espace utilisateur faisant le lien entre les namespaces et les cgroups pour unifier ces deux technologies afin d'arriver à fournir des containers utilisables. LXD complète LXC notamment en ajoutant un accès par webservice qui rend les containers orchestrables par des systèmes type OpenStack et des outils plus haut niveau pour les gérer type gestion de profil de containers, etc.

Dernière précision sur les containers avant de passer à la pratique, on distingue deux types de containers : fat (gras) et thin (mince). Cette terminologie est empruntée aux jails BSD. Le thin jail revient à containeriser juste une application (comme on l’a fait ci-dessus avec bash). Un fat jail consiste à faire tourner un système entier dans un container. De façon schématique, on prend ce qu'on a fait au-dessus sauf qu'à la place de bash, on exécute systemd (processus de démarrage des Linux modernes). Par contre, contrairement à ce qu'on a vu précédemment dans le cas de la virtualisation complète, on ne fera tourner que du Linux dans un fat jail, car il n’y a qu'un seul noyau qui tourne sur la machine.

3.1 Installation de la couche LXD

Comme sur beaucoup de distributions (mis à part Ubuntu), LXD n'est pas disponible directement sous forme de package. Ici, on doit installer les dépôts epel et copr (dont les paquets sont de qualité assez variable) pour obtenir snapd. Les snap sont des paquets d'installation qui viennent compléter le système de gestion de paquets standard avec des paquets plus portables d'une distribution à l'autre dans la mesure où ils sont fournis avec leurs dépendances binaires (par conséquent, ils s'appuient beaucoup moins sur la distribution sous-jacente). Installons tout cela :

[root@lxd ~]# yum install yum-plugin-copr epel-release

[root@lxd ~]# yum copr enable ngompa/snapcore-el7

[root@lxd ~]# yum install snapd

[root@lxd ~]# systemctl enable --now snapd.socket

[root@lxd ~]# systemctl enable snapd

Nous avons maintenant snapd d'installé sur notre CentOS. Nous pouvons configurer le noyau pour expliciter le chargement des namespaces au démarrage du noyau (user_namespace.enable=1) et nous ménager la possibilité de lancer des containers en tant que non-root (namespace.unpriv_enable=1). Toutes ces modifications se font dans le fichier /etc/default/grub :

[root@lxd ~]# vi /etc/default/grub

...

GRUB_CMDLINE_LINUX="... user_namespace.enable=1 namespace.unpriv_enable=1 ..."

...

[root@lxd ~]# grub2-mkconfig >/boot/grub2/grub.cfg

Enfin, on passe un nombre de namespaces maximum instantiables par l'utilisateur :

[root@lxd ~]# echo "user.max_user_namespaces=3883" > /etc/sysctl.d/99-userns.conf

 

On redémarre la machine et on peut passer à l'installation de LXD :

[root@lxd ~]# snap install lxd

2019-03-07T22:43:22+01:00 INFO Waiting for restart...

lxd 3.10 from Canonical✓ installed

3.2 Configuration initiale de LXD

La configuration des containers passe par une commande d'initialisation qui va nous permettre de créer un profil par défaut. Sauf que nous allons tout faire nous même, donc nous allons répondre non partout.

[root@lxd ~]# lxd init

Would you like to use LXD clustering? (yes/no) [default=no]: no

Créer un emplacement où stocker les containers :

Do you want to configure a new storage pool? (yes/no) [default=yes]: no

Connexion à un serveur MAAS (produit Ubuntu pour déployer des machines automatiquement) :

Would you like to connect to a MAAS server? (yes/no) [default=no]: no

Création d'un bridge (on y reviendra) :

Would you like to create a new local network bridge? (yes/no) [default=yes]: no

Utilisation de ce bridge (on y reviendra) :

Would you like to configure LXD to use an existing bridge or host interface? (yes/no) [default=no]: no

Peut-on administrer les containers par le réseau :

Would you like LXD to be available over the network? (yes/no) [default=no]: no

Mise à jour automatique des images disponibles :

Would you like stale cached images to be updated automatically? (yes/no) [default=yes] yes

Afficher la configuration (en fait toutes les configurations de LXD se font au format YAML) :

Would you like a YAML "lxd init" preseed to be printed? (yes/no) [default=no]: no

Pour au moins tester notre installation de LXD, il va falloir créer un storage pool. En fait, c'est juste un répertoire qui va contenir nos containers. Pour créer ces choses-là à la main, on va repasser sur les commandes lxc :

[root@lxd ~]# lxc storage create store_lxd dir source=/store/lxd

Ici, on crée un pool de stockage nommé store_lxd qui est de type fichier (on peut aussi mettre des partitions en mode bloc, volume LVM, ZFS, etc.) et qui se trouve localisé physiquement dans /store/lxd. Nous allons pouvoir déployer notre première image dans ce pool fraîchement créé. Voyons si on peut descendre une image :

[root@lxd ~]# lxc launch -s store_lxd images:centos/7/amd64 apollo

Creating apollo

 

The container you are starting doesn't have any network attached to it.

To create a new network, use: lxc network create

To attach a network to a container, use: lxc network attach

 

Starting apollo

Cette commande va créer un container basé sur une image CentOS 7. Le système nous informe que ce container n'a pas de réseau, c'est normal nous allons le créer. Listons maintenant les containers en cours d’exécution :

[root@lxd ~]# lxc list

+--------+---------+------+------+------------+-----------+

|  NAME  |  STATE  | IPV4 | IPV6 |    TYPE    | SNAPSHOTS |

+--------+---------+------+------+------------+-----------+

| apollo | RUNNING |      |      | PERSISTENT |           |

+--------+---------+------+------+------------+-----------+

Nous retrouvons bien le container apollo. Mettons un petit mot de passe pour root. La sous-commande exec de la commande lxc lance une commande dans le container ciblé :

[root@lxd ~]# lxc exec apollo passwd root

Changing password for user root.

New password:

Retype new password:

passwd: all authentication tokens updated successfully.

Quant à lxc console, cela ouvre une console dans le container :

[root@lxd ~]# lxc console apollo

To detach from the console, press: <ctrl>+a q

 

CentOS Linux 7 (Core)

Kernel 3.10.0-957.5.1.el7.x86_64 on an x86_64

 

apollo login: root

Password:

[root@apollo ~]#

Sauf que parfois on n’arrive pas à en sortir… Dans ces cas-là, on peut exécuter un bash dans le container :

[root@lxd ~]# lxc exec apollo bash

[root@apollo ~]#

Le but maintenant c'est que ce container puisse aller sur le Web. Nous allons donc nous atteler au réseau.

4. Configuration réseau

État des lieux du réseau sur Apollo :

[root@apollo ~]# ip a

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000

...

Il n'y pas grand-chose, juste une interface de rebouclage (loopback). Notre premier travail va être de préparer l'hôte pour connecter apollo au monde extérieur. C'est ici que les fameux bridges entrent en jeu. Dans l’article page XX, nous avons dit que VirtualBox distribuait les adresses réseau en DHCP. L'idée est d'alimenter apollo avec un bail DHCP fourni par VirtualBox comme on le ferait dans un réseau domestique servi par une box ou dans un réseau d'entreprise embarquant du DHCP.

L'astuce va être de configurer un bridge sur l'hôte. Un bridge est une sorte de commutateur virtuel qu'on va monter sur un système d'exploitation. Pour mémoire, un commutateur est un concentrateur permettant aux machines qui y sont connectées de communiquer au niveau liaison (niveau 2 du modèle OSI). C'est-à-dire que les cartes réseau de ces machines peuvent s'envoyer et recevoir des trames, car elles font partie du même domaine de broadcast Ethernet (en conséquence, ils pourront recevoir les bails DHCP et contacter la passerelle par défaut ainsi que le cache DNS). Donc nous allons créer un bridge sur l'hôte et y « brancher » l'interface réseau réelle de l'hôte. Ensuite, nous allons créer une interface virtuelle pour apollo et la « brancher » sur ce bridge.

Sur l'hôte, il faut d'abord installer le paquet bridge-utils :

[root@apollo ~]# yum install bridge-utils

Ensuite, il faut configurer le bridge :

[root@lxd ~]# cat /etc/sysconfig/network-scripts/ifcfg-virbr0

DEVICE="virbr0"

BOOTPROTO="dhcp"

ONBOOT="yes"

TYPE="Bridge"

NM_CONTROLLED="no"

Les paramètres intéressants sont qu’on crée un bridge (TYPE='Bridge') correspondant au device /dev/virbr0 (DEVICE), configuré en DHCP (BOOTPROTO), monté au démarrage (ONBOOT='yes'). Ensuite, nous raccordons l'interface réseau au bridge :

[root@lxd ~]# cat /etc/sysconfig/network-scripts/ifcfg-enp0s3

TYPE=Ethernet

BOOTPROTO=none

NAME=enp0s3

UUID=dc83eab1-b18a-4c24-9ad9-818479905817

DEVICE=enp0s3

NM_CONTROLLED=no

BRIDGE=virbr0

ONBOOT=yes

Il s'agit d'une interface Ethernet (TYPE), dont le nom est enp0s3 (NAME) correspondant au device /dev/enp0s3 (DEVICE) et surtout membre du bridge virbr0 (BRIDGE). À ce moment-là, le bridge est arrosé par le réseau externe via l'interface enp0s3 de l'hôte.

La dernière étape est de créer une interface réseau sur apollo liée au bridge de l'hôte. Sur l'hôte :

[root@lxd ~]# lxc config device add apollo eth0 nic name=eth0 nictype=bridged parent=virbr0

Device eth0 added to apollo

Cette commande ajoute une carte réseau (nic) nommée eth0 (name) qui va s'attacher sur un bridge (nictype=bridged) nommé virbr0 (parent=virbr0). Retournons sur apollo :

[root@apollo ~]# ip a

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000

...

5: eth0@if6: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000

...

Tentons de récupérer un bail DHCP et de pinger Google :

[root@apollo ~]# dhclient

[root@apollo ~]# ping www.google.fr

PING www.google.fr (172.217.19.227) 56(84) bytes of data.

64 bytes from par21s11-in-f3.1e100.net (172.217.19.227): icmp_seq=1 ttl=52 time=2.48 ms

64 bytes from par21s11-in-f3.1e100.net (172.217.19.227): icmp_seq=2 ttl=52 time=2.16 ms

5. Et pour finir…

Quelques petites commandes bien utiles : comment stopper et arrêter les containers :

[root@apollo ~]# lxc stop apollo

[root@apollo ~]# lxc start apollo

Pour lancer automatiquement apollo :

[root@apollo ~]# lxc config set apollo boot.autostart=true

Afficher/modifier les valeurs par défaut lors de la création du container :

[root@lxd ~]# lxc profile edit default

config: {}

description: Default LXD profile

devices: {}

name: default

used_by:

- /1.0/containers/apollo

Et enfin, afficher la configuration du container :

[root@lxd ~]# lxc config edit apollo

 

architecture: x86_64

config:

   image.architecture: amd64

   image.description: Centos 7 amd64 (20190307_07:08)

   image.os: Centos

   image.release: "7"

   image.serial: "20190307_07:08"

   volatile.base_image: 6f065bff901509e641cb74436075d37bb5fcfde14539d74363b71911b33b9fe2

   volatile.eth0.hwaddr: 00:16:3e:0f:7b:eb

   volatile.idmap.base: "0"

   volatile.idmap.next: '[{"Isuid":true,"Isgid":true,"Hostid":1000000,"Nsid":0,"Maprange":1000000000}]'

   volatile.last_state.idmap: '[{"Isuid":true,"Isgid":true,"Hostid":1000000,"Nsid":0,"Maprange":1000000000}]'

   volatile.last_state.power: STOPPED

devices:

   eth0:

      name: eth0

      nictype: bridged

      parent: virbr0

      type: nic

   root:

      path: /

      pool: store_lxd

      type: disk

ephemeral: false

profiles:

- default

stateful: false

description: ""

On y retrouve bien tous nos éléments configurés à la main.

Conclusion

Cet article vous a présenté les bases pour appréhender les containers sous Linux. Il est très important de faire l'effort d'apprivoiser LXD dès maintenant, car tous les systèmes de cloud libres majeurs tels qu’OpenStack et OpenNebula proposent des drivers LXD pour déployer des containers sur leurs nœuds physiques. Autant partir dès maintenant sur de bonnes bases !

 



Article rédigé par

Par le(s) même(s) auteur(s)

Intégration d’ownCloud pour servir un partage de fichiers existant

Magazine
Marque
Linux Pratique
Numéro
136
Mois de parution
mars 2023
Spécialité(s)
Résumé

Le logiciel libre ownCloud est une solution de partage et de stockage des fichiers en mode web. Avec la profusion des plateformes commerciales de ce type (Google Drive, iCloud, Dropbox, etc.), l’accès web aux fichiers est devenu le nouveau standard. Dans cet article, nous allons explorer une méthode pour interfacer cette méthode d’accès aux fichiers avec un serveur NFS (Network File System) existant.

Équilibrage de charge avec IPVS

Magazine
Marque
Linux Pratique
HS n°
Numéro
55
Mois de parution
octobre 2022
Spécialité(s)
Résumé

IP Virtual Server (IPVS) est un équilibreur de charge agissant au niveau 4 du modèle OSI. Il est implémenté sous forme d’un module noyau s’appuyant sur le framework Netfilter, ce qui le rend efficace sur l’équilibrage des services par rapport à leurs ports TCP/UDP, mais totalement agnostique aux protocoles applicatifs transportés (LDAP, HTTP, etc.).

Libre-service de machines virtuelles avec OpenNebula

Magazine
Marque
Linux Pratique
Numéro
130
Mois de parution
mars 2022
Spécialité(s)
Résumé

Dans un article précédent, nous avons mis en place une infrastructure OpenNebula basique [1]. Nous allons maintenant configurer cette plateforme pour proposer aux utilisateurs un guichet de libre-service de machines virtuelles. L’idée est que les utilisateurs puissent créer eux-mêmes leurs machines virtuelles.

Les derniers articles Premiums

Les derniers articles Premium

La place de l’Intelligence Artificielle dans les entreprises

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

L’intelligence artificielle est en train de redéfinir le paysage professionnel. De l’automatisation des tâches répétitives à la cybersécurité, en passant par l’analyse des données, l’IA s’immisce dans tous les aspects de l’entreprise moderne. Toutefois, cette révolution technologique soulève des questions éthiques et sociétales, notamment sur l’avenir des emplois. Cet article se penche sur l’évolution de l’IA, ses applications variées, et les enjeux qu’elle engendre dans le monde du travail.

Petit guide d’outils open source pour le télétravail

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Ah le Covid ! Si en cette période de nombreux cas resurgissent, ce n’est rien comparé aux vagues que nous avons connues en 2020 et 2021. Ce fléau a contraint une large partie de la population à faire ce que tout le monde connaît sous le nom de télétravail. Nous avons dû changer nos habitudes et avons dû apprendre à utiliser de nombreux outils collaboratifs, de visioconférence, etc., dont tout le monde n’était pas habitué. Dans cet article, nous passons en revue quelques outils open source utiles pour le travail à la maison. En effet, pour les adeptes du costume en haut et du pyjama en bas, la communauté open source s’est démenée pour proposer des alternatives aux outils propriétaires et payants.

Sécurisez vos applications web : comment Symfony vous protège des menaces courantes

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Les frameworks tels que Symfony ont bouleversé le développement web en apportant une structure solide et des outils performants. Malgré ces qualités, nous pouvons découvrir d’innombrables vulnérabilités. Cet article met le doigt sur les failles de sécurité les plus fréquentes qui affectent même les environnements les plus robustes. De l’injection de requêtes à distance à l’exécution de scripts malveillants, découvrez comment ces failles peuvent mettre en péril vos applications et, surtout, comment vous en prémunir.

Bash des temps modernes

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Les scripts Shell, et Bash spécifiquement, demeurent un standard, de facto, de notre industrie. Ils forment un composant primordial de toute distribution Linux, mais c’est aussi un outil de prédilection pour implémenter de nombreuses tâches d’automatisation, en particulier dans le « Cloud », par eux-mêmes ou conjointement à des solutions telles que Ansible. Pour toutes ces raisons et bien d’autres encore, savoir les concevoir de manière robuste et idempotente est crucial.

Les listes de lecture

8 article(s) - ajoutée le 01/07/2020
Découvrez notre sélection d'articles pour faire vos premiers pas avec les conteneurs, apprendre à les configurer et les utiliser au quotidien.
11 article(s) - ajoutée le 02/07/2020
Si vous recherchez quels sont les outils du DevOps et comment les utiliser, cette liste est faite pour vous.
8 article(s) - ajoutée le 02/07/2020
Il est essentiel d'effectuer des sauvegardes régulières de son travail pour éviter de perdre toutes ses données bêtement. De nombreux outils sont disponibles pour nous assister dans cette tâche.
Voir les 60 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous