PyInfra

Parce que ton infrastructure mérite du vrai code en Python, pas une soupe de YAML

PyConFR 2026 - Biarritz
Samedi 31 octobre - 16 h 30 - Salle MARI/SUGGAR

Loïc "wowi42" Tosser
CTO chez Kalvad

github.com/wowi42
codeberg.org/wowi42
@wowi42

Qui ne sait PAS ce qu'est l'Infrastructure as Code ?

C'est le moment de le dire. Même ton IDE ne te jugera pas.

Et maintenant, qui fait du YAML ?

Vous êtes entre amis. Celui qui dit "c'est moins pire que XML" paie l'apéro.

Qui pense que le YAML est la pire plaie de l'informatique ?

(Après Kubernetes. Et l'imprimante du bureau.)

Mon combat

Que les Ops apprennent des devs, et que les devs apprennent des Ops.

Python peut être ce langage commun.

Le YAML reste ici uniquement pour les besoins de la démonstration.

Mon sport favori

Le YAML n'a qu'à bien se tenir.

# playbook.yml - 847 lignes. Personne n'ose y toucher. Même son auteur.
- name: "Déployer {{ app | d('le bordel') }} sur {{ env | upper if env is defined else 'PROD???' }}"
  hosts: "{{ lookup('env','HOSTS')|split(',')|reject('==','')|list if lookup('env','HOSTS')|d('')!='' else ['all'] }}"
  vars:
    norway: no               # false. Le fameux bug scandinave.
    enabled: yes              # true. Évidemment.
    disabled: off             # false. Bien sûr que c'est logique.
    tokyo: on                 # true. YAML fait aussi de la géographie.
    nope: n                   # false. CQFD.
    yep: y                    # true. Vous suivez ? Personne ne suit.
    port: "{{ (lookup('env','PORT')|d(8080)|int)+((groups['web']|d([])|length)*10) if groups['web'] is defined else 8080 }}"
    nested: "{{ '{{' }} lookup('template','templates/'+(('{{' }} item.key {{ '}}') if item.key is defined else 'default')+'.j2') {{ '}}' }}"
    &common:
      timeout: "{{ 300 if env=='prod' else 600 }}"
      retries: "{{ [1,2,3,5,8]|random }}"

  tasks:
    - name: "Installer {{ item.p | d('un truc') }}"
      become: "{{ yes if ansible_os_family!='Windows' else no }}"
      package: name="{{ item.p|regex_replace('^py[23]?-','python3-') if item.py else item.p }}" state=present
      loop: "{{ pkgs|d([])|union(extra_pkgs|d([]))|intersect(allowed|d(pkgs|d([])))|list }}"
      when:
        - pkgs is defined or extra_pkgs is defined
        - "{{ lookup('env','SKIP')|d('no')|bool == false }}"
        - ansible_distribution == 'Debian' and ansible_distribution_version is version('11','>=') or ansible_distribution == 'Ubuntu' and ansible_distribution_version is version('20.04','>=')

    - name: "Config {{ nested|count }} variables"
      template:
        src: "{{ 'templates/'+app+'/'+env+'/'+item.key+'.j2' if item.key is defined else 'templates/default/'+env+'.j2' }}"
        dest: "/etc/{{ app|d('app') }}/{{ item.key|d('default')|regex_replace('[^a-zA-Z0-9._-]','_') }}.conf"
        mode: "{{ '0644' if not item.secret|d(false) else '0600' }}"
      with_dict: "{{ configs|d({})|combine(more|d({})) }}"
      notify: "Restart {{ app }}"
      tags: "{{ item.value.tags|d(['config'])+['always-'+env] }}"

Le YAML et les nombres, une grande histoire d'amour

# Tes 7 premiers clusters Kubernetes déploient nickel.
# Le 8ème explose. Pourquoi ?
revisions:
  - 07   # -> 7      (nombre)
  - 08   # -> "08"   (chaîne. merci l'octal, au revoir la prod)

# Tu voulais écrire "4h30 du matin" ?
reveil: 04:30   # YAML entend : 16200 secondes depuis minuit

Le seul format où 07 et 08 n'ont pas le même type, et où 04:30 est un nombre.

Le YAML et les versions, même combat

versions:
  foo: 1.7     # même version que...
  bar: 1.70    # ...celle-ci. Évidemment.
  fizz: 1.7.0    # différent de...
  buzz: 1.70.0   # ...celle-là. Bien sûr.

deploy:
  revision: ${GIT_SHORT_SHA}          # string ~98% du temps
  revision_safe: "${GIT_SHORT_SHA}"   # string ~100% du temps

Un SHA court tout en chiffres (ça arrive 2% du temps), et ton pipeline typé explose. Bonne chance pour le reproduire.

Un fichier YAML est presque toujours "valide", même tronqué

"A YAML file is almost always still 'valid' even if it is trunca

— @colmmacc, tweet volontairement coupé (comme votre fichier)

  • 63 façons d'écrire une chaîne multi-lignes (source)
  • Chaque parser YAML est un parser YAML maison : un même fichier peut être "valide" différemment selon les outils (matrix.yaml.io)
  • Rite de passage DevOps : découvrir que Kubernetes parle YAML 1.1, où 0666 est une histoire d'octal

Toutes ces horreurs, et bien d'autres, sont répertoriées sur noyaml.com.

On nous avait promis de l'Infrastructure as Code

On a eu l'Infrastructure as YAML.

  • Code : "Un langage de programmation lisible par l'humain"
  • YAML : "Un fichier de définition lisible par la machine"

L'un se teste. L'autre se prie.

Il est écrit que seul un gnome des Forêts du Nord unijambiste dansant à la pleine lune au milieu des douze statuettes enroulées dans du jambon arrivera à lire des templates Helm en entier

Le cauchemar du développeur

  • L'enfer du templating → Du Jinja2 dans du YAML dans du Jinja2. Inception, sans le budget.
  • Pas de vraies fonctions → Copier-coller des rôles jusqu'à devenir un module
  • Du debug ? → "undefined variable", avec 47 fichiers suspects
  • Support IDE ? → Du feeling, des prières et un linter fatigué
  • YAML == URSSAF → Une ligne mal remplie, et tu paies

Les messages d'erreur Ansible

TASK [Deploy app] *******************
fatal: [web01]: FAILED! => {"msg": "The task includes
an option with an undefined variable. The error was:
'dict object' has no attribute 'nonexistent'\n\nThe
error appears to be in '/playbook.yml': line 42,
column 3, but may be elsewhere in the file depending
on the exact syntax problem."}

"quelque part dans le fichier" - Autrement dit: débrouille-toi.

La solution officielle: -vvvv, un café et 1000 lignes de logs.

Les tests Ansible

# 200 lignes de YAML pour tester 50 lignes de YAML, avec Tox et Molecule
# Plus Docker, plus Vagrant, plus un doctorat en souffrance

Tu voulais tester ton infra. Tu dois maintenant tester les tests de ton infra.

PyInfra entre en scène

Votre infrastructure. Du vrai Python.

Le seul YAML restant est dans les exemples de ce talk.

>>> import antigravity  # Pour enfin décoller du YAML
>>> from yaml import pain
ImportError: No more. We're done here.
>>> import pyinfra
>>> # Ça va déjà mieux.

C'est quoi PyInfra ?

Du Python en entrée, des commandes shell en sortie

  • Exécuter des commandes ad-hoc
  • Écrire des opérations déclaratives (diff checking, idempotence)
  • Cibler des serveurs SSH, la machine locale, des conteneurs Docker
  • Passe à l'échelle d'un à des milliers de serveurs

Pas besoin de Python sur les systèmes cibles: juste un accès shell POSIX !

Votre serveur n'a même pas besoin de savoir qu'il fait du Python.

Le concept de base

# Installer et configurer nginx
from pyinfra.operations import apt, systemd

apt.packages(
    name="Install nginx",
    packages=["nginx"],
)

systemd.service(
    name="Restart and enable the nginx service",
    service="nginx.service",
    running=True,
    restarted=True,
    enabled=True,
)

Du Python en entrée, du shell en sortie.

Le YAML peut sortir de la pièce.

Attendez... C'est juste du Python !

EXACTEMENT !

  • De vraies fonctions avec def, pas un rôle copié douze fois
  • De vraies boucles for sans gymnastique with_items
  • De vrais if/else sans spaghetti Jinja2
  • De vrais import et pytest
  • Type hints & autocomplétion → mypy approuve
  • List comprehensions → [server for server in fleet if server.is_alive()]

Votre IDE arrête enfin de faire semblant.

Connecteurs : ciblez n'importe quoi

# SSH vers des serveurs (le classique)
pyinfra @ssh/web1.example.com deploy.py

# Exécuter en local (casser chez toi d'abord)
pyinfra @local deploy.py

# Cibler des conteneurs Docker directement
pyinfra @docker/my-container deploy.py

# Mixer dans l'inventaire
pyinfra inventory.py deploy.py
# inventory.py - C'est juste du Python !
ssh_hosts = ["web1", "web2", "db1"]
docker_hosts = [("@docker/app", {"env": "staging"})]
local = ["@local"]

Un outil. Toutes les cibles. Même celle nommée "prod-ne-pas-toucher".

Totalement extensible

Profitez de tout l'écosystème de packages Python

import requests
hosts = requests.get("https://api/hosts").json()

import boto3
instances = boto3.client('ec2').describe_instances()

from slack_sdk import WebClient
slack = WebClient(token=TOKEN)
slack.chat_postMessage(channel="#ops", text="Deploy complete!")

Pas de plugins custom. Juste pip install. Plus de 400 000 packages sur PyPI.

Tu connais déjà la commande. Et les regrets qui vont avec.

Intégrez PyInfra partout

C'est une bibliothèque, pas juste un CLI

from pyinfra.api import Config, Inventory, State
from pyinfra.api.operations import run_ops
from pyinfra.operations import server

inventory = Inventory((["web1.example.com"], {}))
state = State(inventory, Config())

server.shell(commands=["echo 'Deployed from my Flask app!'"])
run_ops(state)

De l'automatisation d'infrastructure dans VOTRE appli. Intégrez dans Django, FastAPI, des outils CLI.

Un import de plus. Un outil de moins.

PyInfra vs Ansible

En quoi c'est mieux ?

Le combat que ton équipe plateforme attendait. Ou pas.

Performance : jusqu'à 10x plus rapide

Ansible :

  • Parallélisme à régler
  • Couches d'abstraction lourdes
  • Python requis sur la cible

PyInfra :

  • Multiplexage SSH efficace
  • Exécution parallèle intégrée
  • Commandes shell directes
  • Passe à l'échelle sur des milliers de serveurs

Jusqu'à 10x plus rapide en déploiement

Benchmark maison, donc parfait.

Face à face : installer 3 paquets

Ansible - 10 lignes

- name: Install packages
  hosts: webservers
  become: yes
  tasks:
    - name: Update apt cache
      apt: update_cache=yes

    - name: Install packages
      apt: name="{{ item }}" state=present
      with_items: [nginx, postgresql, redis]

PyInfra - 2 lignes

from pyinfra.operations import apt
apt.packages(packages=["nginx", "postgresql", "redis"], update=True)

Tu préfères lire du code ou résoudre une énigme ?

Face à face : logique conditionnelle

Ansible

- name: Configure based on environment
  template:
    src: "{{ 'prod.j2' if env == 'production'
             else 'dev.j2' }}"
    dest: /etc/app/config
  when: env is defined

PyInfra

if env == "production":
    template = "prod.j2"
else:
    template = "dev.j2"
files.template(src=template, dest="/etc/app/config")

Le même if que dans tes applis. Et il marche aussi ici.

Face à face : les boucles

Ansible - with_items hell

- name: Create users
  user:
    name: "{{ item.name }}"
    groups: "{{ item.groups }}"
  with_items:
    - { name: alice, groups: sudo }
    - { name: bob, groups: developers }
  when: item.name != 'root'

PyInfra - une boucle for

users = [{"name": "alice", "groups": ["sudo"]}, {"name": "bob", "groups": ["developers"]}]
for user in users:
    if user["name"] != "root":
        server.user(user=user["name"], groups=user["groups"])

Une boucle for. Tu la connais déjà.

Debug : retour instantané

Ansible

  • Ajouter le flag -vvvv et scroller des milliers de lignes
  • Tâches debug: msg="{{ variable }}"
  • "may be elsewhere in the file" en cas d'erreur

PyInfra

  • Output des commandes shell en temps réel
  • breakpoint() fonctionne directement
  • Stack traces Python : fichier exact, ligne, erreur
  • Support du debugger IDE avec des breakpoints

Arrête de déboguer tes outils de débogage.

Support IDE : développement moderne

Ansible

  • Coloration syntaxique YAML basique
  • Peut-être un peu de linting
  • Pas d'autocomplétion, pas de refactoring

PyInfra

  • Autocomplétion complète avec type hints
  • Documentation inline
  • Support du refactoring (renommer, extraire)
  • Aller à la définition

Ctrl+clic remplace Ctrl+F dans 47 fichiers.

Réutilisation de code : modules Python

Ansible

  • Copier-coller des rôles depuis Galaxy
  • Personnaliser avec 5 répertoires différents
  • Se battre avec les dépendances de rôles

PyInfra

  • import vos fonctions
  • Utilisez plus de 400 000 packages PyPI
  • Écrivez du code propre et modulaire
from my_company.infra import setup_monitoring, configure_firewall

Une fonction. Un seul endroit pour la casser.

Bénéfices

  • 70% de code en moins à maintenir
  • Vraie réutilisation de code via import
  • 5x plus rapide en développement avec le support IDE
  • De vrais tests unitaires avec pytest
  • Les juniors comprennent: ils ont appris Python, pas la syntaxe YAML du mois
  • Passe à l'échelle sur des milliers de serveurs
  • Intégrable dans vos applis

Chiffres mesurés au doigt mouillé. Mais avec beaucoup de conviction.

Mais et si...

Ansible a des milliers de modules !

PyInfra a plus de 400 000 packages Python. pip install requests > module Ansible custom.

Tout le monde connaît Ansible !

Ici, tout le monde connaît Python. Et personne ne veut relire un when de quatre lignes.

Le déclaratif c'est mieux !

PyInfra EST déclaratif. Ce n'est pas le YAML qui fait la magie, c'est le diff.

Mais et si... (partie 2)

Et les dépendances sur les systèmes cibles ?

Zéro. Sans agent. N'importe quel shell POSIX-ish.

Le changement fait peur !

Votre playbook de 5000 lignes que personne n'ose toucher aussi.

La peur est déjà là. Autant qu'elle lise du Python.

Prêt pour la production ?

Lancez-vous aujourd'hui !

uv tool install pyinfra  # Tu peux passer à la partie où ça marche.
# deploy.py
from pyinfra.operations import apt, systemd

apt.packages(name="Install nginx", packages=["nginx"])

systemd.service(
    name="Start nginx",
    service="nginx.service",
    running=True,
    enabled=True,
)
pyinfra @local deploy.py  # Et voilà.

C'est tout. De l'IaC avec du vrai code. Aucun sacrifice sous la pleine lune.

Arrêtez le YAML, passez au Python

Venez me voir après pour discuter de :

  • Vos histoires d'horreur Ansible (thérapie de groupe, première séance gratuite)
  • Stratégies de migration sans big bang du vendredi soir
  • Pourquoi le YAML vous a fait du mal
  • Comment import pyinfra peut vous guérir

pyinfra.com - github.com/pyinfra-dev/pyinfra - uv tool install pyinfra

Merci !

return "Allez libérer votre infrastructure !"

pyinfra.com → Votre futur sans YAML commence maintenant

if encore_du_ansible_apres_ce_talk:
    raise FriendshipError("Faut qu'on parle")

Petite bière ?

YAML: le seul langage où une espace en trop transforme ton firewall en passoire et ta garde en retraite spirituelle.