CapallMara.io - Architecture & Solutions Data
Accueil Le parcours Le carnet Prenons contact
← Retour au carnet
Orchestration 1 juillet 2026 · 12 min de lecture

Ce qu'un point-virgule m'a appris sur la conception d'une orchestration

Un bug de production chez une banque française m'a fait repenser ce qui relève de Python et ce qui relève de l'orchestrateur.

Note : traduction française d'un article publié à l'origine en anglais sur Medium, pour la publication Kestra Engineering.
Frontière entre code Python et orchestration déclarative
Le code custom d'un côté, le contrat déclaratif de l'autre : déplacer la frontière.

Je travaillais sur des plateformes data pour une banque française en transition vers le cloud, en orchestrant des flux entre des systèmes legacy DB2/AS400 et des services plus récents comme Snowflake, Azure Blob Storage et Airbyte.

Nous avions choisi Kestra Community Edition pour des raisons pratiques : pas de budget, et aucune certitude à l'époque que cet outil deviendrait une plateforme durable.

Dans un environnement de ce genre, rien n'est jamais totalement déclaratif ni totalement custom. Alors, dès le début, je me suis fortement appuyé sur des tâches Python. Elles étaient rapides, flexibles, et géraient les parties les plus délicates : cas particuliers legacy, règles de validation, conventions de nommage de fichiers, connecteurs manquants. Ça fonctionnait. Mais avec le temps, mes flows se sont remplis de code qui ne relevait plus vraiment de la logique métier. Notifications. Manipulation de fichiers. Templating. Parsing des destinataires. Tout ça enfoui dans des scripts que j'étais à peu près le seul à comprendre.

Un moment petit, et un peu embarrassant

Nous faisions tourner nos premiers flows en production depuis environ six mois. Un flow interrogeait Snowflake pour détecter des conditions techniques critiques, puis envoyait une alerte par email. Le serveur Kestra n'avait pas d'accès SMTP, et la version que nous utilisions n'avait pas de plugin natif Azure Communication Services Email. J'ai donc fait ce que beaucoup d'ingénieurs font sous pression de livraison : j'ai tout écrit en Python.

Une tâche parsait les destinataires, générait le corps de l'email, construisait le payload Azure Communication Services, signait la requête REST, et envoyait l'email. Une version simplifiée ressemblait à ceci :

def parse_recipients(raw_value: str) -> list[str]:
    # Expected: "ops@example.com,data@example.com"
    # One day the configuration contained:
    # "ops@example.com;data@example.com"
    return [
        item.strip()
        for item in raw_value.split(",")
        if item.strip()
    ]


def send_email(raw_recipients: str, rows: list[dict]) -> None:
    recipients = parse_recipients(raw_recipients)
    payload = {
        "senderAddress": ACS_SENDER,
        "content": {
            "subject": "Kestra monitoring alert",
            "html": build_html(rows),
        },
        "recipients": {
            "to": [{"address": e} for e in recipients],
        },
    }
    body = json.dumps(payload).encode("utf-8")
    path = "/emails:send?api-version=2023-03-31"
    headers = sign_request("POST", path, body)
    requests.post(f"{ACS_ENDPOINT}{path}",
                  headers=headers, data=body, timeout=30)

Puis, un jour, la moitié des destinataires a cessé de recevoir les alertes. Aucune erreur. L'exécution était verte. Succès.

J'ai passé deux heures à soupçonner les droits Snowflake et à vérifier la requête elle-même. La cause réelle était un point-virgule là où mon code attendait une virgule, hérité d'un vieux fichier Excel utilisé comme configuration des années auparavant.

Quinze secondes pour corriger. La partie inconfortable est venue après.

Deux moteurs de template sur le même texte

En étant dans le code, j'ai remarqué que je mélangeais deux moteurs de template sans m'en rendre compte. À l'époque, le Python était embarqué directement dans le bloc script: | de Kestra, pas conservé comme fichier séparé. Ce détail comptait, parce que Kestra rendait les expressions Pebble dans la définition de la tâche avant même que le processus Python ne démarre.

Dans ce code Python, j'avais des chaînes comme {‍{ flow.id }‍} et {‍{ execution.id }‍}. Pire, le template HTML embarqué utilisait aussi des blocs de contrôle façon Jinja comme {% for row in rows %}. En tant que développeur Python connaissant Jinja2, ça me paraissait naturel. Mais dans Kestra, cette syntaxe est aussi suffisamment proche de Pebble pour devenir dangereuse une fois placée directement dans une définition de tâche rendue.

Le script n'était donc plus vraiment un simple script Python. Il était d'abord traité par Kestra, puis exécuté par Python, puis partiellement rendu à nouveau par Jinja2. Quand ça fonctionnait, c'était pratique. Quand ça échouait, c'était difficile à raisonner.

Mon premier réflexe a été de réécrire le Python plus soigneusement. Un parsing plus rigoureux. Plus de tests. Du code plus défensif. Mais c'était exactement le réflexe qui avait créé le problème. Tout résoudre avec du code custom. Posséder chaque ligne. Posséder chaque bug.

Déplacer la frontière

Ce qui a réellement fonctionné, c'est de déplacer la frontière. Il n'y avait toujours pas de plugin ACS Email natif pour me sauver, et l'appel ACS avait toujours besoin de Python. Ce qui a sécurisé les choses n'était pas un plugin spécifique à ACS. La primitive Kestra qui a changé la conception, c'est io.kestra.plugin.scripts.python.Script, utilisée avec des env, inputFiles et outputFiles explicites.

Le plugin n'envoyait pas l'email à ma place. Il rendait la frontière d'exécution explicite. Le reste de la tâche n'avait plus besoin d'être un seul bloc Python opaque :

- id: notify_with_acs
  type: io.kestra.plugin.scripts.python.Script
  taskRunner:
    type: io.kestra.plugin.core.runner.Process
  env:
    ACS_EMAIL_ENDPOINT: "{‍{ secret('ACS_EMAIL_ENDPOINT') }‍}"
    ACS_EMAIL_KEY: "{‍{ secret('ACS_EMAIL_KEY') }‍}"
    ACS_EMAIL_SENDER: "{‍{ secret('ACS_EMAIL_SENDER') }‍}"
    ALERT_RECIPIENTS: "{‍{ vars.alert_recipients }‍}"
    FLOW_ID: "{‍{ flow.id }‍}"
    EXECUTION_ID: "{‍{ execution.id }‍}"
  inputFiles:
    rows.json: "{‍{ outputs.query_alerts.uri }‍}"
    send_acs_email.py: "{‍{ read('scripts/send_acs_email.py') }‍}"
  outputFiles:
    - "acs_response.json"
  script: |
    python send_acs_email.py

C'est le moment que je résume par « quarante lignes de Python sont devenues une dizaine de lignes de YAML ». Le ratio exact importe moins que ce qui a changé : des dizaines de lignes de code embarqué dans le workflow sont devenues un petit contrat déclaratif plus un adaptateur Python ciblé.

La responsabilité s'est déplacée. Kestra portait la plomberie d'orchestration : secrets, entrées, sorties, retries, logs, cycle de vie. Python ne portait plus que ce que seul Python pouvait faire : construire le payload ACS, signer la requête, l'envoyer, écrire la réponse. Le helper Python a changé de forme lui aussi. Il ne contenait plus d'expressions Pebble - il lisait des variables d'environnement et des fichiers à la place.

Le problème n'a jamais été que le templating soit une mauvaise idée. Le problème était de cacher un moteur de template à l'intérieur d'un autre et de laisser les deux opérer sur le même texte.

Le même schéma, partout

Cet incident n'était pas isolé. Dans les mois qui ont suivi, j'ai repassé nos flows un par un et j'ai retrouvé le même schéma partout. Du code custom gérait des problèmes que Kestra avait déjà résolus, ou pouvait rendre explicites : parsing des sorties de trigger, transmission de fichiers, retries, frontières de notification.

Python restait la bonne réponse pour certaines choses, comme streamer des fichiers volumineux ou appeler des procédures Snowflake. Mais même là, Kestra devenait le control plane et Python devenait le worker, pas le workflow. Chaque contrainte de production que nous avons rencontrée - pulls Docker, déclarations de sorties, limites mémoire - renforçait la même leçon : garder l'orchestration explicite, le code custom étroit, et le cycle de vie porté par la plateforme.

À l'été 2025, nous avions un an d'usage réel en production derrière nous. Kestra avait porté nos flows les plus lourds de manière fiable, sous des contraintes réelles. La décision de passer en Enterprise ne reposait pas sur une évaluation formelle. Elle a été validée par un an de confiance construite progressivement, flow après flow.

Ce qui m'a surpris, c'est ce que ça a fait ressentir. Je pensais que céder mon code à de la configuration allait donner une sensation de perte de contrôle. Ça a donné une sensation de soulagement. Le Python n'a pas disparu, et il ne devait pas disparaître. Mais j'ai arrêté d'en écrire pour des problèmes que Kestra avait déjà mieux résolus que moi.

C'est ça, la vraie leçon. Un bon orchestrateur n'impose pas de choisir entre code et configuration dès le premier jour. On commence par du code tant qu'on est encore en train de comprendre le problème, puis on déplace les parties stables vers des tâches déclaratives une fois que la plateforme et nous connaissons tous les deux la forme du workflow.

À un moment, Kestra a cessé d'être un scheduler. C'est devenu un control plane. Et j'ai arrêté de tout porter dans ma propre tête.

#Kestra #Orchestration #DataEngineering #Python #Snowflake #Airbyte #Azure

Une frontière code / config à repenser ?

Orchestration, Kestra, plateformes data - partageons nos retours.

Prenons contact
CapallMara.io - Architecture & Solutions Data
© 2026 CapallMara.io - Architecture & Solutions Data