Aller au contenu

Infrastructure automatisée avec PyInfra

Mise à jour :

logo PyInfra

PyInfra est un outil d’automation d’infrastructure écrit en Python, conçu pour gérer des serveurs via SSH de manière simple, rapide et idempotente. Contrairement à des solutions comme Ansible qui utilisent YAML, PyInfra propose une approche déclarative en Python, permettant une meilleure intégration dans des workflows DevOps existants. Il s’adresse à ceux qui veulent automatiser le déploiement, la configuration et la gestion de serveurs sans agents, avec une syntaxe claire et des performances optimisées.

Concepts clés de PyInfra

PyInfra repose sur une architecture modulaire et une approche déclarative pour gérer des serveurs à distance. Son fonctionnement s’articule autour de plusieurs concepts fondamentaux : l’inventaire, les opérations, les facts et les connecteurs. Ces éléments permettent de modéliser une infrastructure de manière structurée tout en garantissant l’idempotence des actions.

  • Inventaire : L’inventaire est au cœur de PyInfra. Il définit les hôtes que l’on souhaite gérer, qu’il s’agisse de serveurs physiques, virtuels, conteneurs ou autres. Cet inventaire peut être statique (fichier texte ou Python) ou dynamique (généré à partir d’une API ou d’un script). Il permet aussi de regrouper les hôtes par environnement ou rôle, facilitant la réutilisation des configurations. Chaque hôte peut contenir des données spécifiques (comme des variables d’environnement ou des secrets), ce qui permet de personnaliser les opérations en fonction du contexte.
  • Opérations : Les opérations sont les actions exécutées sur les hôtes, comme installer un paquet, modifier un fichier, ou créer un utilisateur. Elles sont déclarées de manière déclarative, c’est-à-dire que l’on décrit l’état désiré plutôt que les étapes pour y arriver. Chaque opération est conçue pour être idempotente : elle ne modifie le système que si nécessaire. Cela permet d’exécuter plusieurs fois un même déploiement sans provoquer d’effets secondaires.
  • Facts : Les facts sont des données collectées depuis les hôtes avant l’exécution des opérations. Par exemple, la version d’un paquet, le type de distribution Linux, ou la présence d’un fichier. Ces informations permettent d’adapter dynamiquement les actions à l’état réel du système distant. PyInfra fournit une collection de facts intégrés, mais il est également possible d’en définir sur mesure, pour des besoins spécifiques.
  • Connecteurs : PyInfra fonctionne sans agent installé sur les machines gérées. Il utilise des connecteurs pour interagir avec elles. Le plus courant est le connecteur SSH, qui permet de piloter n’importe quel serveur disposant d’un accès SSH. D’autres connecteurs existent, comme ceux pour Docker ou Chroot, élargissant les possibilités à d’autres types d’environnements.

Comment fonctionne PyInfra ?

PyInfra utilise des connecteurs pour se connecter aux hôtes et exécuter des commandes déclarées dans des fichiers Python. Il suit un processus en plusieurs étapes :

  1. Chargement de l’inventaire : PyInfra charge l’inventaire défini dans un fichier Python, qui peut contenir des groupes d’hôtes et des données spécifiques.
  2. Collecte des facts : Avant d’exécuter les opérations, PyInfra collecte les facts de chaque hôte. Cela permet de connaître l’état actuel du système et d’adapter les actions en conséquence.
  3. Exécution des opérations : Les opérations sont exécutées de manière déclarative. PyInfra compare l’état actuel du système avec l’état désiré défini dans les opérations. Si l’état est déjà conforme, aucune action n’est effectuée. Sinon, les modifications nécessaires sont appliquées.
  4. Affichage des résultats : Après l’exécution, PyInfra affiche un résumé des actions effectuées, indiquant les changements appliqués et ceux qui n’ont pas nécessité de modifications. Cela permet de suivre l’évolution de l’infrastructure et de vérifier que les opérations ont été appliquées correctement.

Installation de PyInfra

L’installation de PyInfra est simple et rapide grâce à sa distribution via pip. Il peut être installé globalement, dans un environnement virtuel, ou via pipx pour un usage isolé sans perturber les dépendances du système.

L’utilisation de pipx est particulièrement adaptée pour les outils en ligne de commande Python. Elle permet d’installer PyInfra dans un environnement isolé tout en gardant la commande disponible globalement.

  1. Installer pipx (si ce n’est pas déjà fait) :

    Terminal window
    python3 -m pip install --user pipx
    python3 -m pipx ensurepath
  2. Installer pyinfra avec pipx :

    Terminal window
    pipx install pyinfra
  3. Vérifier l’installation :

    Terminal window
    pyinfra --version
    pyinfra: v3.3.1

Cette méthode assure une séparation propre entre les outils et les bibliothèques Python du système, ce qui évite les conflits de dépendances.

Premiers pas avec PyInfra

PyInfra est conçu pour être simple à prendre en main, même pour ceux qui n’ont pas d’expérience préalable en automation d’infrastructure. Voici les étapes pour commencer à utiliser PyInfra efficacement.

Création d’un inventaire

Un inventaire est un fichier Python qui liste les hôtes à gérer. Il peut être statique (une liste d’adresses IP ou de noms de domaine) ou dynamique (généré par un script ou une API). Voici un exemple d’inventaire statique :

inventory.py
my_hosts = [
"my-server.net",
"@docker/ubuntu:18.04", # Conteneur Docker
"@local", # Machine locale
]

Cet inventaire peut être utilisé pour exécuter des commandes ou des opérations sur les hôtes listés. Les hôtes peuvent être regroupés par environnement ou par rôle, facilitant ainsi la gestion de configurations complexes. Nous verrons plus loin comment structurer les inventaires pour différents environnements.

Vous remarquerez que les hôtes peuvent être désignés par leur nom de domaine, leur adresse IP, ou par des alias comme @docker/ubuntu:18.04 pour indiquer un conteneur Docker ou @local pour la machine locale. Ces alias utilisent des connecteurs spécifiques pour interagir avec les cibles. Parmis les connecteurs disponibles, on trouve :

  • @ssh : pour les serveurs distants accessibles via SSH.
  • @docker : pour les conteneurs Docker.
  • @local : pour exécuter des commandes sur la machine locale.
  • @terraform : pour exécuter des commandes dans un environnement Terraform.
  • @vagrant : pour les machines virtuelles gérées par Vagrant.

Il est possible d’écrire son propre connecteur pour des besoins spécifiques, mais les connecteurs fournis couvrent déjà la plupart des cas d’usage courants.

Exécution de commandes simples (ad-hoc)

Pour exécuter des commandes simples sur des hôtes distants, PyInfra propose la commande exec.

Terminal window
pyinfra INVENTAIRE exec -- commande

Elle permet d’envoyer des commandes shell à un ou plusieurs hôtes définis dans un inventaire. Voici quelques exemples :

Terminal window
# Exemple : via SSH
pyinfra my-server.net exec -- echo "hello world"
# Exemple : dans un conteneur Docker
pyinfra @docker/ubuntu:18.04 exec -- echo "hello world"
# Exemple : sur la machine locale
pyinfra @local exec -- echo "hello world"

Cette méthode utilise le connecteur SSH, Docker ou local selon la cible et affiche le résultat en temps réel.

Utilisation des opérations

Les opérations sont le cœur de PyInfra. Elles permettent de déclarer des actions à exécuter sur les hôtes, comme installer des paquets, modifier des fichiers, ou gérer des utilisateurs. Contrairement aux commandes ad-hoc, les opérations sont déclaratives et idempotentes, ce qui signifie qu’elles ne modifient l’état du système que si nécessaire.

Voici un exemple d’opération pour installer un paquet avec PyInfra :

Terminal window
# Installer le paquet vim sur une machine distante
pyinfra dev apt.packages vim _sudo=True

Dans cet exemple, dev est l’hôte défini dans l’inventaire, apt.packages est l’opération pour gérer les paquets sur les systèmes basés sur Debian/Ubuntu, et _sudo=True indique que l’opération doit être exécutée avec des privilèges élevés (sudo).

A la première exécution, PyInfra va installer le paquet vim si nécessaire. Si le paquet est déjà installé, l’opération ne fera rien, ce qui garantit l’idempotence.

Il existe de nombreuses opérations prédéfinies pour gérer les paquets, les utilisateurs, les fichiers, les services, etc.

Vous pouvez consulter les opérations disponibles dans la documentation officielle de PyInfra.

Arguments Globaux

PyInfra propose plusieurs arguments globaux pour personnaliser le comportement des opérations :

  • --sudo : exécute les opérations avec des privilèges élevés (sudo).
  • --data : permet de passer des données supplémentaires aux opérations.
  • --chdir : change le répertoire de travail avant d’exécuter les opérations.
  • --group-data : spécifie un répertoire contenant des données de groupe supplémentaires.

Ces arguments peuvent être combinés pour adapter le comportement des opérations à vos besoins spécifiques. Par exemple, pour exécuter des opérations avec sudo et passer des données personnalisées :

Terminal window
pyinfra inventory.py deploy.py --sudo --data key=value

Créer un déploiement

Un deploy regroupe inventaires et opérations dans des fichiers Python pour être versionné et réutilisé :

inventory.py
my_hosts = ["dev"]
deploy.py
from pyinfra.operations import apt
apt.packages(
name="Installer le paquet vim",
packages=["vim"],
_sudo=True,
)

Exécution :

Terminal window
pyinfra inventory.py deploy.py
--> Loading config...
--> Loading inventory...
--> Connecting to hosts...
[dev] Connected
--> Preparing operation files...
Loading: deploy.py
[dev] Ready: deploy.py
--> Detected changes:
Operation Change Conditional Change
Installer le paquet vim - -
Detected changes may not include every change pyinfra will execute.
Hidden side effects of operations may alter behaviour of future operations,
this will be shown in the results. The remote state will always be updated
to reflect the state defined by the input operations.
Detected changes displayed above, skip this step with -y
--> Beginning operation run...
--> Starting operation: Installer le paquet vim
[dev] No changes
--> Results:
Operation Hosts Success Error No Change
Installer le paquet vim 1 - - 1
Grand total 1 - - 1
--> Disconnecting from hosts...

Cette approche, similaire à un playbook Ansible, facilite la maintenance et le partage de vos procédures.

Si vous souhaitez désinstaller un paquet, vous pouvez utiliser l’option present=False :

apt.packages(
name="Désinstaller le paquet vim",
packages=["vim"],
_uninstall=True,
present=False,
)

Chaque opération possède différents arguments pour personnaliser son comportement, comme mode pour définir les permissions d’un fichier. Référez-vous à la documentation officielle de PyInfra pour une liste complète des opérations et de leurs arguments.

Syntaxe Avancée de PyInfra

Pour aller plus loin, explorons les mécanismes avancés de PyInfra qui offrent un contrôle précis sur le déroulement des opérations et la gestion de l’inventaire.

Arguments Globaux

PyInfra propose plusieurs arguments globaux pour personnaliser le comportement d’une opération.

  • _sudo : exécute les opérations avec des privilèges élevés (sudo).
  • _sudo_user : spécifie un utilisateur pour sudo.
  • _chdir : change le répertoire de travail avant d’exécuter les opérations.
  • _env : permet de passer des variables d’environnement aux opérations.
  • _timeout : spécifie un délai d’attente pour les opérations.
  • _ignore_errors : ignore les erreurs lors de l’exécution des opérations.
  • _if : conditionne l’exécution d’une opération en fonction du résultat d’une autre opération.

Ces arguments peuvent être combinés pour adapter le comportement des opérations à vos besoins spécifiques. Par exemple, pour exécuter des opérations avec sudo et passer des données personnalisées :

server.user(
name="Create pyinfra user using sudo",
user="pyinfra",
_sudo=True,
_sudo_password="my-secret-password",
)

Pour plus de détails sur les arguments globaux, consultez la documentation officielle de PyInfra.

L’objet host et inventory

Comment récupérer des données dynamiques ? Grâce à l’objet host, vous pouvez interroger :

  • host.name et host.groups pour cibler des machines précises
  • host.data pour accéder aux variables d’inventaire
  • host.get_fact(...) pour obtenir des facts (distribution, version, etc.)
from pyinfra import host, inventory
from pyinfra.facts.server import LinuxName
from pyinfra.operations import server
if host.get_fact(LinuxName) == "Ubuntu":
server.apt.update(
name="Mettre à jour les paquets sur Ubuntu",
_sudo=True,
)
server.apt.upgrade(
name="Mettre à niveau les paquets sur Ubuntu",
_sudo=True,
)

Pour plus d’informations sur les facts disponibles, consultez la documentation officielle de PyInfra.

De son côté, l’objet inventory facilite l’accès aux autres hôtes, idéal pour générer des configurations croisées.

from pyinfra import inventory
from pyinfra.operations import server
# Créer un utilisateur sur tous les hôtes du groupe "web"
for host in inventory.get_group("web"):
server.user(
name="Créer l'utilisateur web sur {}".format(host.name),
user="web",
shell="/bin/bash",
home="/home/web",
_sudo=True,
)

Opérations conditionnelles

Les opérations conditionnelles permettent de n’exécuter une action que si certaines conditions sont remplies. Par exemple, vous pouvez conditionner l’exécution d’une opération en fonction du résultat d’une autre opération.

from pyinfra import host
from pyinfra.operations import server, files
template = files.template(
name="Déployer le fichier de configuration",
src="templates/myapp.conf.j2",
dest="/etc/myapp/myapp.conf",
mode="644",
user="root",
group="root",
_sudo=True,
)
server.service(
name="Redémarrer le service si le fichier a changé",
service="myapp",
_if=template.did_change,
_sudo=True,
)

Dans cet exemple, le service myapp ne sera redémarré que si le fichier de configuration a été modifié par l’opération files.template. Cela permet d’éviter des redémarrages inutiles et de gagner du temps lors des déploiements.

Inclusion de fichiers multiples

Pour un déploiement modulaire, local.include permet d’importer d’autres scripts pyinfra et de leur passer des données via l’argument data. Cela facilite les rôles et les tâches partagées :

from pyinfra import local
local.include("tasks/setup_db.py", data={"db_user": "app"})

Objet config

Enfin, l’objet config offre des defaults globaux pour toutes les opérations du deploy :

from pyinfra import config
config.SUDO = True

Tous les appels suivants utiliseront sudo par défaut et vérifieront la version de pyinfra exigée.

Structure d’un projet PyInfra

Un projet PyInfra se compose de quelques fichiers et répertoires bien identifiés ; leur agencement conditionne la façon dont l’outil charge les cibles, les variables et les opérations.

Arborescence minimale conseillée

proj_PyInfra/
├─ deploy.py # scénario principal (opérations)
├─ inventories/
│ ├─ production.py # inventaire statique ou dynamique
│ └─ staging.py
└─ group_data/
├─ all.py # variables communes
├─ production.py # données propres à l’environnement de production
└─ staging.py # données propres à l’environnement de staging

Cet exemple, repris de la documentation officielle, illustre la séparation entre inventaires et données de groupe pour plusieurs environnements ; chaque fichier d’inventaire devient automatiquement un groupe du même nom ( production, staging ) en plus du groupe spécial all qui englobe toutes les machines.

Inventaires : fichiers Python

  • Les fichiers d’inventaire sont des scripts Python qui définissent les hôtes à gérer.
  • Ils sont placés dans le répertoire inventories/ et peuvent être nommés comme production.py, staging.py, etc.
  • Chaque fichier d’inventaire peut contenir des groupes d’hôtes, des variables spécifiques, et des données d’environnement.
  • Il est possible de spécifier un inventaire différent avec l’option --inventory ou -i

group_data : variables par groupe

  • Tout fichier group_data/<nom_du_groupe>.py expose des variables accessibles via host.data dans les opérations.
  • Le répertoire group_data est résolu relativement au répertoire courant ; il peut être déplacé avec l’option --group-data ou --chdir.

Hiérarchie des données

Lorsqu’une clé est définie à plusieurs niveaux, PyInfra applique cette priorité (de la plus forte à la plus faible) :

  1. Override fourni en ligne de commande (--data key=value)
  2. host_data dans l’inventaire
  3. group_data normaux
  4. all.py (valeurs par défaut)

Ainsi, une même variable peut être définie globalement puis surchargée finement par environnement ou par hôte.

Comparaison avec Ansible

  • Langage et flexibilité
    • pyinfra : tout l’inventaire et les opérations sont écrits en Python, permettant d’intégrer boucles, conditions et fonctions complexes au besoin.
    • Ansible : playbooks en YAML enrichis de Jinja2, plus accessible pour les débutants mais moins souple dès qu’il faut gérer de la logique avancée.
  • Performance
    • pyinfra exécute les commandes shell en parallèle, sans couche JSON intermédiaire, et ne modifie l’hôte que si nécessaire (idempotence).
  • Transparence et débogage
    • pyinfra affiche en temps réel la sortie des commandes et propose un diff avant/après chaque opération.
    • Ansible passe par des modules encapsulés et un échange JSON, rendant parfois le débogage moins direct
  • Modèle déclaratif vs impératif
    • Ansible impose un modèle strictement déclaratif (playbooks YAML).
    • pyinfra reste déclaratif pour la plupart des opérations, mais autorise l’insertion de code Python impératif (via host.get_fact, callbacks, etc.) pour les cas complexes
  • Inventaire et variables
    • pyinfra : inventaire en Python (listes/tuples), enrichi par host_data et group_data pour définir variables et secrets.
    • Ansible : inventaire en INI ou YAML, variables réparties dans les répertoires group_vars/ et host_vars/
  • Écosystème et modules
    • Ansible dispose de milliers de modules officiels (ansible-doc -l) couvrant une large gamme de cas d’usage.
    • pyinfra s’appuie sur ses operations Python et la richesse de l’écosystème des packages Python pour étendre ses fonctionnalités

Conclusion

Comparé à Fabric, PyInfra offre une approche plus moderne et performante pour l’automatisation d’infrastructure, tout en restant accessible aux développeurs Python. Contrairement à Fabric, les modules Pyinfra garantissent l’idempotence.

PyInfra est donc un outil puissant et flexible pour l’automatisation d’infrastructure écrit en Python. Sa syntaxe claire et sa capacité à gérer des opérations idempotentes en font un choix idéal pour les équipes DevOps qui cherchent à simplifier la gestion de leurs serveurs.