Migration Dagster → Airflow 3 (Astro)
Migrer un projet Dagster vers Airflow 3 sur Astro Runtime, honnêtement. La migration est asset-first (les graphes d'assets Dagster se traduisent en assets Airflow et schedules asset-aware, pas en DAGs aplatis), incrémentale (domaine par domaine, Dagster reste autoritaire jusqu'à la parité), et honnête (chaque définition reçoit une disposition explicite ; les déltas sémantiques sont documentés, jamais occultés).
Tu conduis ça pour la première fois ? Lis d'abord reference/quickstart.md : les commandes de première heure, le glossaire, et ce qui peut et ne peut pas casser.
Migration en un coup d'œil
- Baseline la suite de tests du projet source, puis l'inventorie en lecture seule (
scripts/inventory.py→ manifest). - Revois les classifications (MECH/JUDG/REDESIGN/NONE par
reference/mapping.md) ; fais l'appel go/no-go (trois résultats ; migrate-with-conditions est le cas courant, stay est le cas rare) ; planifie les frontières de DAG, les décisions IO par arête, et les attentes Gate 3 dans le manifest. - Effectue un essai de migration 2-3 unités représentatives de bout en bout à travers chaque gate de validation.
- Migre domaine par domaine par l'échelle six-gate (
reference/validation.md), en suivant l'état par unité (scripts/status.py) ; corrige les classes d'échec viareference/troubleshooting.md, jamais de stubs. - Mappe la couche plateforme (secrets, alertes, CI/CD, Deployments) par
reference/astro-deployment.md. - Exécute en parallèle, puis bascule par domaine (les consommateurs reprennent d'abord ; voir la checklist), en gardant le rollback à un pas.
- Livre le rapport de migration : chaque définition disposée, une ligne d'équivalence par déclencheur, les pertes énoncées clairement.
Dérive de version
Vérifié contre Airflow 3.3.0 / Astro Runtime 3.3-2 / astronomer-cosmos 1.15 / Dagster 1.13 (2026-07). Les lignes sensibles aux versions dans les références portent leur étage (notamment la surface partition 3.2-vs-3.3). Avant de te fier à une affirmation version-gated : vérifie la cible (airflow version, astro deployment inspect), sonde les imports pour la surface SDK (python3 -c "from airflow.sdk import X"), et préfère --help / découverte API spec aux contrats CLI/REST verbatim supposés sur les versions plus récentes. Les entrées du playbook sont version-scoped par entrée.
Exigences
- Target Astro Runtime 3.3+ (Airflow 3.3+) ; la surface asset-partition native l'exige. Sous 3.2 le mapping se dégrade mal ; dis-le et recommande une mise à niveau avant migration.
- Le repo Dagster, et idéalement une instance Dagster en cours d'exécution (ses métadonnées de matérialisation fournissent les fixtures de test de parité).
- CLI
astropour le projet cible.
Règles dures
- Jamais de stubs. Une unité traduite fonctionne soit par sa gate de validation, soit est reportée avec raison écrite. Les corps fake-success et code workaround avec longs commentaires justificatifs sont des échecs.
- Aucune omission silencieuse. Chaque enregistrement du manifest d'inventaire se termine par
completeoudeferred (reason).scripts/status.py summarysort nonzero sinon ; lance-le avant de déclarer done. - Lignes d'équivalence pour chaque déclencheur. Chaque schedule/sensor/automation condition obtient une ligne de rapport : orthographe source, orthographe cible, delta en une phrase. Les deltas sémantiques existent (catchup, inversion on_cron, garanties eager) ; le péché n'est pas le delta, c'est le delta indocumenté.
- Corrige les classes, pas les instances. Quand un pattern de traduction échoue la validation, corrige le pattern (et enregistre-le dans
reference/troubleshooting.md), puis réapplique ; ne fais pas de patch manuel sur une unité. - N'invente pas d'APIs. Les références ne contiennent que des noms vérifiés. Tout ce qui n'y est pas couvert est vérifié contre les docs officielles avant utilisation.
Workflow
Phase 0 : Preflight
Confirme la version Runtime cible, la présence CLI astro, et l'accès au repo. Détecte la disposition du projet : classique (@repository/workspace.yaml), moderne (Definitions), ou Components (pyproject.toml [tool.dg], fichiers defs.yaml) ; les trois se produisent, parfois ensemble. Baseline la suite de tests du projet source maintenant : les échecs préexistants sont enregistrés et exclus du blâme de migration.
Phase 1 : Inventory (lecture seule)
python3 scripts/inventory.py <dagster_repo> --out manifest.json # static scan
python3 scripts/inventory.py <dagster_repo> --runtime --out manifest.json # + runtime introspection when the project imports
Le manifest liste chaque définition avec file:line, paramètres capturés, orthographe current-vs-deprecated, et arêtes de dépendance avec leur IO manager ; chaque enregistrement commence par classification: "pending". Classer est TA première tâche de jugement : assigne à chaque enregistrement MECH / JUDG / REDESIGN / NONE depuis sa ligne dans reference/mapping.md et écris-la dans le manifest. Le scanner énumère (complétude déterministe) ; l'agent classe (jugement). Un enregistrement que tu ne peux pas mapper à une ligne mapping.md est lui-même un finding : enregistre-le, ne devines pas. Grep aussi pour DAGSTER_CLOUD_ et EnvVar( (couche plateforme, Phase 5).
Conventions de manifest : le manifest canonique vit dans le répertoire d'exécution de migration. Une fois le projet Astro créé (Phase 2 scaffold), copie le manifest à son include/inventory/manifest.json pour que Gate 3 pytest et les défauts status.py le trouvent ; jusque-là il reste juste dans le run dir (garde les deux en sync après, la copie run-dir gagne). Les enregistrements statiques sont les unités canoniques de migration ; l'enrichissement mode runtime fusionne en eux, et seules les définitions genuinely runtime-only (factory-generated) deviennent de nouvelles unités.
Émets le squelette du rapport de migration maintenant : une section par enregistrement manifest, plus la carte de nommage secrets/env de reference/astro-deployment.md. Scale le squelette au projet : un projet local sans secrets obtient une note one-line "no secrets/platform layer", pas des sections boilerplate vides.
Phase 1.5 : Go/no-go (la gate honnête)
Avant de traduire quoi que ce soit, réponds à la question au niveau du projet que l'inventaire rend répondable : qu'est-ce que cette équipe abandonne en migrant, et chaque perte a-t-elle une réponse acceptable ? Évalue les lignes NONE et REDESIGN contre ce qui est load-bearing pour CETTE équipe, en évaluant la mitigation, pas juste la perte :
| Si load-bearing | La réponse Airflow-world | Signal stay seulement si |
|---|---|---|
dbt rebuild-on-code-change (code_version_changed()) |
Builds dbt state-aware sur un cron (Fusion / dbt State skip unchanged models par run, donc le tick post-deploy rebâtit exactement ce qui a changé), et/ou dbt build déclenché par CI en merge (PR-gated, souvent une upgrade) |
L'équipe ne peut ni exécuter une stack dbt state-aware ni dbt depuis CI |
| Freshness driving materialization | Astro Observe freshness SLAs / Timeliness alerts + scheduled runs dimensionnées à la SLA | Freshness-triggered compute est genuinely irremplaçable par schedule+alerting |
| Per-asset cost accounting (Insights) | Astro Observe pipeline-level warehouse cost management ; la granularité per-asset est perdue | La chargeback par ASSET est une exigence contractuelle/organisationnelle |
| Asset catalog / column-level lineage comme outils quotidiens | Airflow 3 asset views + OpenLineage/Astro lineage (asset-level) | La column-level lineage est intégrée aux workflows quotidiens sans catalog externe |
Deep AutomationCondition compositions, can_subset, selective per-partition materialization |
La plupart se décomposent en cron/asset schedules (voir reference/automation.md) ; le résidu est redesigné par domaine |
Multiple domains dépendent de compositions qui se décomposent à rien |
| Sensor cursor transactionality, run-scoped teardown | Idempotent consumers + max_active_runs ; context managers dans les corps de tâche |
L'exactly-once event coalescing est une exigence de correctness que l'idempotency ne peut pas absorber |
Une règle que la table implique, énoncée clairement : **aucune condition dbt-only n'atteint « stay ». Entre Cosmos, les builds dbt state-aware, et les builds déclenché par CI, chaque perte de workflow dbt a une mitigation de pratique acceptée (execution-proven dans le programme eval de cette skill, y compris sur un vrai warehouse) ; les items dbt sont des conditions à enregistrer, jamais des blockers. Les lignes d'observabilité (per-asset cost, column-level lineage) sont des conditions séparées et sont évaluées sur leurs propres, même pour les équipes dbt-heavy.
L'issue de la gate est trois-valué, et le milieu est le cas courant :
- Migrate : aucun stay-signal ; procède à Phase 2.
- Migrate with conditions (la plupart des vrais projets) : les pertes existent, les mitigations sont nommées et acceptées par écrit dans la première section du rapport, les domaines spécifiques peuvent porter du travail REDESIGN ; procède à Phase 2 avec ces conditions enregistrées.
- Stay on Dagster, today : réservé au cas où MULTIPLE conditions stay-signal dans la colonne droite tiennent genuinely à la fois et la migration n'est pas mandatée en externe. Alors le livrable honnête est cette recommandation, par écrit, avec les pertes spécifiques non-mitigées nommées, et l'exécution s'arrête là. Un guide de migration qui ne peut pas dire « ne fais pas » ne peut pas être de confiance quand il dit « fais », mais « ne fais pas » est gagné par les pertes non-mitigables, pas par la simple existence de deltas.
Phase 2 : Plan
- DAG boundaries : décide quelles arêtes de dépendance d'assets deviennent des schedules asset-aware (cross-DAG) vs task ordering (intra-DAG). Groupe par domaine/schedule cadence/team ownership ; les sélections
define_asset_jobnomment généralement les domaines naturels. - Per-edge IO decisions via l'arbre dans
reference/io-and-data-passing.md(fuse / explicit storage / XCom). - Order : domaines feuilles d'abord, ordre dépendance après ; la couche plateforme en dernier.
- Remplis les attentes cible de chaque unité planifiée dans le manifest :
dag_id,task_count,edges,schedule,asset_outletspar unité. Gate 3 asserte contre exactement ces champs ; une unité sans eux est skippée par la validation, donc un manifest non-enrichi veut dire que Gate 3 ne vérifie rien (validate_dag rapporte les counts skipped bruyamment, ne les ignore pas). - Scaffold la cible :
astro dev init, helpers partagés sousinclude/. Les conventions de maison que le scaffold impose (p. ex. un test exigeantretries >= 2) N'OVERRIDIENT PAS la source fidelity : le comportement source gagne ; l'adoption de convention est une amélioration post-cutover listée dans le rapport, et le test scaffold est skippé avec une raison explicite.
Phase 3 : Trial
Migre 2-3 unités représentatives de bout en bout à travers chaque gate avant de fanner out. Choisis un asset MECH, un asset partitionné, un cas JUDG, ou le mix le plus proche disponible (les petits projets peuvent n'avoir aucun asset partitionné ou aucun asset MECH ; choisis un chemin complet à travers un vrai DAG à la place). Ce que l'essai enseigne va dans reference/troubleshooting.md avant le scaling ; si l'essai échoue structurellement, arrête-toi et retravaille le plan, pas les unités.
Phase 4 : Migrate, domaine par domaine
Par unité, la machine d'état (suivie dans le manifest) :
pending → translate → fix-import → fix-lint → fix-tests → verify-parity → complete
↘ deferred (reason required)
- Traduis en utilisant le fichier de référence pour la construction (table de routage ci-dessous). Le contexte riche bat la cleverness : lis l'unité source, ses lignes mapping, et un exemple déjà migré à proximité.
- Valide à travers les gates :
python3 scripts/validate_dag.py <astro_project> --manifest manifest.json(gates 1-3), puis exécution et parité parreference/validation.md. - En cas d'échec de gate, réessaie avec la dernière sortie du validateur en contexte (cap ~10 attempts, puis defer avec la classe d'échec).
- Avance l'état seulement au passage de gate :
python3 scripts/status.py advance <unit-id> .... Une disposition incorrecte se corrige avecstatus.py reopen <unit-id> --reason .... La règle no-hand-editing s'applique au champ STATE (status) seulement ; les champs PLAN (dag_id,task_count,edges,schedule,asset_outlets,target) sont à écrire du planificateur en Phase 2. - Les unités qui se traduisent délibérément en NO DAG de leur propre (helpers absorbés dans les tâches, policies devenues alerts, resources devenues connections) sont disposées
completeavectarget: "none"et evidence nommant où elles sont allées ; Gate 3 les saute par design. - Commit par unité, atomiquement.
Phase 5 : Couche plateforme
reference/astro-deployment.md : topologie Deployments, carte de nommage secrets/connection, CI/CD et Deployments preview, mapping de policy d'alerte, attentes Observe/lineage, la checklist rewrite in-code DAGSTER_CLOUD_*.
Phase 6 : Side-by-side et cutover
Dagster reste autoritaire. Exécute les DAGs migrés shadowed/paused ; compare les outputs sur la même fenêtre logique (row counts + checksums ; recalcule les valeurs attendues depuis l'output produit par Dagster lui-même, en utilisant les métadonnées de matérialisation enregistrées seulement opportunistically, par reference/validation.md Gate 5). Flip les schedules un domaine par change window : pause la schedule Dagster, unpause le DAG Airflow ; rollback est l'inverse. Garde Dagster lisible après cutover (la run history ne migre pas).
Phase 7 : Rapport final
scripts/status.py summary doit passer. Le rapport contient : l'évaluation go/no-go (Phase 1.5) et sa raison, la table de disposition pour chaque définition, tous les lignes d'équivalence, les pertes NONE/REDESIGN énoncées clairement (lineage depth, triggers code_version, Insights cost accounting, sensor cursor transactionality), la carte des secrets, et la liste deferred avec raisons. Spot-check dix affirmations complete avant de la livrer.
Routage de référence
| Construction rencontrée | Lis |
|---|---|
| Première heure, glossaire, ce qui peut casser | reference/quickstart.md |
| N'importe quoi (première étape : une ligne par construction) | reference/mapping.md |
| Asset-key → convention URI, granularité traduction, assets external/observable | reference/assets.md |
| Asset deps, IO managers, XCom, décisions storage | reference/io-and-data-passing.md |
N'importe quel partitions_def, mappings partition, backfills |
reference/partitions.md |
| Schedules, sensors, AutomationCondition, freshness | reference/automation.md |
@dbt_assets, translators, dbt Cloud |
reference/dbt.md |
| Components (defs.yaml), custom Component subclasses, dynamic generation | reference/components.md |
| dagster_cloud.yaml, secrets, alerts, CI/CD, cutover | reference/astro-deployment.md |
| Gates, parity testing, machine d'état | reference/validation.md |
| Classes d'échec vues avant | reference/troubleshooting.md |
Scripts
| Script | Objectif |
|---|---|
scripts/inventory.py |
Scanne le repo Dagster → manifest JSON (mode statique + optional runtime) |
scripts/validate_dag.py |
Gates 1-3 contre le projet Astro généré |
scripts/status.py |
Machine d'état par unité + gate de complétude |