PD-47 — Décomposition en tâches agents¶
1. Matrice des dépendances¶
| ID | Agent | Description | Produit | Consomme | Dépend de | Niveau |
|---|---|---|---|---|---|---|
| T1 | agent-developer | Entities, Types partagés, Migration TypeORM | backup-execution.entity.ts, backup-journal-event.entity.ts, types/index.ts, migration | — | [] | 0 |
| T2 | agent-developer | Services utilitaires (Validation + Config + Hash) | backup-validation.service.ts, backup-config.service.ts, backup-hash.service.ts | — | [] | 0 |
| T3 | agent-developer | Terraform KMS + IAM + S3 | kms-backup.tf, iam-backup.tf, storage-aws-core.tf (modification) | — | [] | 0 |
| T4 | agent-developer | Ansible backup provisioning | postgresql_backup/ role complet | — | [] | 0 |
| T5 | agent-developer | BackupStateService (machine d'états §5.7) | backup-state.service.ts | Entities (T1) | [T1] | 1 |
| T6 | agent-developer | BackupEncryptionService (AES-256-GCM + Vault Transit) | backup-encryption.service.ts | Types (T1), Config (T2) | [T1, T2] | 1 |
| T7 | agent-developer | BackupJournalService (append-only + dédup) | backup-journal.service.ts | Entities (T1), Validation (T2) | [T1, T2] | 1 |
| T8 | agent-developer | BackupS3Service (upload SSE-KMS + vérification SHA3-256) | backup-s3.service.ts | Types (T1), Config (T2), Encryption (T6), Hash (T2) | [T1, T2, T6] | 1 |
| T9 | agent-developer | BackupOrchestratorService (flux F1/F3, retry, circuit-breaker) | backup-orchestrator.service.ts | State (T5), Encryption (T6), S3 (T8), Hash (T2), Journal (T7), Validation (T2), Config (T2) | [T5, T6, T7, T8] | 2 |
| T10 | agent-developer | BackupWatchdogService + BackupRelaunchConsumer | backup-watchdog.service.ts, backup-relaunch.consumer.ts | S3 (T8), Config (T2), AlertService (existant) | [T8] | 2 |
| T11 | agent-developer | BackupReconciliationService + BackupRestorationService | backup-reconciliation.service.ts, backup-restoration.service.ts | State (T5), S3 (T8), Journal (T7), Encryption (T6), Hash (T2) | [T5, T6, T7, T8] | 2 |
| T12 | agent-developer | BackupModule (wiring DI, cron, BullMQ) | backup.module.ts, index.ts | Tous services (T5-T11) | [T9, T10, T11] | 3 |
| T13 | agent-developer | Tests unitaires + intégration (TC-NOM/ERR/INV/NEG/NR) | src/backup/__tests__/**/*.spec.ts | Tous services + module (T1-T12) | [T12] | 3 |
2. Bloc parallelization¶
parallelization:
strategy: by_level
levels:
- level: 0
tasks: [1, 2, 3, 4]
agents: [agent-developer, agent-developer, agent-developer, agent-developer]
branches:
- "feature/PD-47-l0-entities-migration"
- "feature/PD-47-l0-utility-services"
- "feature/PD-47-l0-terraform-infra"
- "feature/PD-47-l0-ansible-infra"
estimated_time: "2h"
- level: 1
tasks: [5, 6, 7, 8]
agents: [agent-developer, agent-developer, agent-developer, agent-developer]
branches:
- "feature/PD-47-l1-state-service"
- "feature/PD-47-l1-encryption-service"
- "feature/PD-47-l1-journal-service"
- "feature/PD-47-l1-s3-service"
estimated_time: "2h"
- level: 2
tasks: [9, 10, 11]
agents: [agent-developer, agent-developer, agent-developer]
branches:
- "feature/PD-47-l2-orchestrator"
- "feature/PD-47-l2-watchdog-relaunch"
- "feature/PD-47-l2-reconciliation-restoration"
estimated_time: "3h"
- level: 3
tasks: [12, 13]
agents: [agent-developer, agent-developer]
branches:
- "feature/PD-47-l3-module-wiring"
- "feature/PD-47-l3-tests"
estimated_time: "4h"
total_sequential_time: "25.5h"
total_parallel_time: "11h"
speedup_factor: 2.32
git_strategy: branch_per_level
3. Détail de chaque tâche¶
Tâche 1 — Entities, Types partagés et Migration TypeORM¶
- Agent : agent-developer
- Niveau : 0
- Dépend de : []
- Contracts : CC-47-02 (entity), CC-47-06 (entity), CC-47-13
- Projet : backend
- Fichiers :
src/backup/entities/backup-execution.entity.tssrc/backup/entities/backup-journal-event.entity.tssrc/backup/types/index.tssrc/migrations/XXXXXXXXXX-create-backup-tables.ts- Description : Produire les deux entities TypeORM (
BackupExecution,BackupJournalEvent) avec tous les champs définis dans CC-47-02 et CC-47-06, les enums partagés (BackupStatus,BackupType), les DTOs et interfaces (EncryptedPayload,ValidationResult,BackupExecutionResult,WatchdogResult,ReconciliationResult,RestorationResult,BackupConfig,S3BackupMetadata,S3UploadResult,BackupJournalEventDto), et la migration TypeORM complète.
Migration : créer les tables backup_executions et backup_journal_events avec : - Contrainte CHECK sur status enum - Index UNIQUE sur backup_id seul dans journal (pas backup_id + event_type) [CORR M-03] - Colonnes wrapped_dek, iv, auth_tag dans backup_executions [CORR B-03] - RLS policy INSERT-only sur backup_journal_events - Ne JAMAIS utiliser ALTER TYPE ADD VALUE dans la même transaction que WHERE (cf. PD-282)
Decision trace : Les types partagés sont extraits dans src/backup/types/index.ts plutôt que co-localisés dans chaque service. Cela permet à tous les agents de niveaux suivants d'importer depuis un point unique, réduisant les risques d'incompatibilité d'interface au merge.
Tâche 2 — Services utilitaires (Validation + Config + Hash)¶
- Agent : agent-developer
- Niveau : 0
- Dépend de : []
- Contracts : CC-47-09, CC-47-10, CC-47-05
- Projet : backend
- Fichiers :
src/backup/services/backup-validation.service.tssrc/backup/services/backup-config.service.tssrc/backup/services/backup-hash.service.ts- Description : Trois services purement fonctionnels sans dépendance sur les entities ou autres services PD-47 :
BackupValidationService (CC-47-09) : validation regex/type/bornes pour tous les champs §5.1 (backup_id, hash_sha3_256, s3_object_key_backup, s3_object_key_wal, size_bytes, timestamp). Rejet avec code FAILED_FORMAT (hors enum status). Valider size_bytes <= Number.MAX_SAFE_INTEGER même si regex OK (cf. R-05 spec review).
BackupConfigService (CC-47-10) : chargement config depuis env/Vault. Distinction stricte rejet vs clamp selon §5.8 colonne "Hors bornes" [CORR M-02]. Validation croisée delay_initial < 100 && factor <= 1 → warning (cf. R-08). Les constantes non configurables (cleartextTtlSeconds=300, restorationPeriodicityDays=90) sont en dur.
BackupHashService (CC-47-05) : calcul SHA3-256 via crypto.createHash('sha3-256'). Hash TOUJOURS sur le clair compressé AVANT chiffrement (cohérence F1/F2/F3, cf. R-02 spec review). Ne JAMAIS utiliser SHA-256 à la place de SHA3-256.
Decision trace : Regroupement de 3 services petits et indépendants en une seule tâche pour optimiser le ratio overhead/production. Alternative : 3 tâches séparées (rejetée — speedup marginal, overhead de merge accru).
Tâche 3 — Terraform KMS + IAM + S3¶
- Agent : agent-developer
- Niveau : 0
- Dépend de : []
- Contract : CC-47-15
- Projet : infra
- Fichiers :
terraform/kms-backup.tf(nouveau)terraform/iam-backup.tf(nouveau)terraform/storage-aws-core.tf(modification)- Description : Provisionner l'infrastructure AWS pour le backup :
kms-backup.tf : clé KMS symétrique dédiée backup (aws_kms_key.backup_key), rotation automatique activée, alias alias/pv-backup. Séparée de la clé maître existante.
iam-backup.tf : user IAM backup_user isolé du backend user. Policy : s3:PutObject, s3:GetObject, s3:ListBucket, s3:HeadObject uniquement sur le bucket backups. Policy kms:Encrypt, kms:Decrypt, kms:GenerateDataKey uniquement sur la clé backup. Aucun accès cross-bucket. Access key stockée dans Vault (kv/data/ci/aws-backup).
storage-aws-core.tf (modification) : upgrade SSE du bucket backups de AES256 vers aws:kms avec SSEKMSKeyId pointant vers la clé backup. Ne PAS modifier le block_public_acls existant. Ne PAS modifier le lifecycle 30 jours existant. Ajouter bucket policy HTTPS-only si absente.
Decision trace : User IAM dédié plutôt que rôle IAM. Alternative considérée : IAM role avec assume-role (rejetée — le backend tourne sur VPS OVH, pas sur EC2/ECS, donc pas de metadata service pour STS).
Tâche 4 — Ansible Backup Provisioning¶
- Agent : agent-developer
- Niveau : 0
- Dépend de : []
- Contract : CC-47-16
- Projet : infra
- Fichiers :
ansible/roles/postgresql_backup/tasks/main.ymlansible/roles/postgresql_backup/templates/pg-receivewal.service.j2ansible/roles/postgresql_backup/templates/watchdog-check.sh.j2ansible/roles/postgresql_backup/defaults/main.ymlansible/roles/postgresql_backup/handlers/main.yml- Description : Nouveau rôle Ansible
postgresql_backup:
tasks/main.yml : installation postgresql-client-15 (pg_dump, pg_basebackup, pg_receivewal), déploiement service systemd, déploiement cron watchdog fallback.
pg-receivewal.service.j2 : unit systemd pour pg_receivewal --slot=pv_backup --directory=/var/wal-archive/. Restart=always, After=network-online.target. Connection string récupérée depuis Vault (pas en clair dans le template).
watchdog-check.sh.j2 : script shell fallback indépendant du process NestJS. Vérifie aws s3 ls s3://{{ bucket }}/YYYY/MM/ pour la présence du backup du jour. Si absent → alerte via webhook/email. Ce cron Ansible est la défense en profondeur pour INV-47-06.
defaults/main.yml : variables par défaut (slot name, wal directory, bucket name, watchdog cron expression). Aucun secret en clair — références Vault uniquement.
Tâche 5 — BackupStateService¶
- Agent : agent-developer
- Niveau : 1
- Dépend de : [T1]
- Contract : CC-47-02 (service)
- Projet : backend
- Fichiers :
src/backup/services/backup-state.service.ts- Description : Machine d'états stricte §5.7. Table de transitions autorisées en constante :
SCHEDULED → RUNNING
RUNNING → SUCCESS
RUNNING → FAILED
SUCCESS → EXPIRED
EXPIRED → DELETED
FAILED → SCHEDULED (si replanificationCount < max)
Toute transition hors table → throw BackupStateTransitionError + journalisation du refus (from, to, backupId, timestamp).
Méthode create() : crée une BackupExecution en état SCHEDULED. Méthode transition() : valide la transition, met à jour le status, incrémente replanificationCount si FAILED→SCHEDULED, sauvegarde via TypeORM repository.
FAILED_FORMAT n'est PAS un état — ne doit JAMAIS apparaître dans BackupStatus.
Circuit-breaker : transition FAILED→SCHEDULED refusée si replanificationCount >= maxReplanificationCycles (DI-01).
Tâche 6 — BackupEncryptionService¶
- Agent : agent-developer
- Niveau : 1
- Dépend de : [T1, T2]
- Contract : CC-47-03
- Projet : backend
- Fichiers :
src/backup/services/backup-encryption.service.ts- Description : Envelope encryption AES-256-GCM avec Vault Transit :
encrypt() : 1. Générer DEK : crypto.randomBytes(32) 2. Générer IV : crypto.randomBytes(12) 3. Chiffrer : crypto.createCipheriv('aes-256-gcm', dek, iv) 4. Wrap DEK via Vault Transit (transit/encrypt/pv-backup) 5. Zéroïser DEK : dekBuffer.fill(0) immédiatement après usage 6. Retourner {ciphertext, iv, authTag, wrappedDek, kBackupVersion}
decrypt() : 1. Unwrap DEK via Vault Transit (transit/decrypt/pv-backup, version spécifiée) 2. Déchiffrer : crypto.createDecipheriv('aes-256-gcm', dek, iv) 3. Vérifier authTag 4. Zéroïser DEK
Fail-closed [CORR M-05] : si Vault Transit est indisponible, throw BACKUP_ENCRYPTION_FAILED. Aucun fallback HKDF local.
Ne JAMAIS réutiliser une DEK entre artefacts. Ne JAMAIS réutiliser un IV avec la même clé. Ne JAMAIS utiliser crypto.createCipher (deprecated). Ne JAMAIS utiliser createVerify() — utiliser crypto.verify(null, ...) si signature (cf. PD-282).
Tâche 7 — BackupJournalService¶
- Agent : agent-developer
- Niveau : 1
- Dépend de : [T1, T2]
- Contract : CC-47-06 (service)
- Projet : backend
- Fichiers :
src/backup/services/backup-journal.service.ts- Description : Journal append-only conforme INV-47-05 :
writeEvent() : 1. Valider tous les champs via BackupValidationService (§5.1) 2. INSERT avec ON CONFLICT (backup_id) DO NOTHING [CORR M-03] 3. Aucun UPDATE, aucun DELETE (garanti par RLS au niveau DB)
getEvents() : lecture seule par backup_id.
Le service est consommé par l'orchestrateur (flux F1/F3), le watchdog (indirectement via journalisation d'alerte), la réconciliation, et la restauration.
La déduplication est sur backup_id seul (pas backup_id + event_type), alignée sur la spec INV-47-05.
Tâche 8 — BackupS3Service¶
- Agent : agent-developer
- Niveau : 1
- Dépend de : [T1, T2, T6]
- Contract : CC-47-04
- Projet : backend
- Fichiers :
src/backup/services/backup-s3.service.ts- Description : Interface S3 avec SSE-KMS et vérification d'intégrité :
upload() : PutObject avec ServerSideEncryption: 'aws:kms' + SSEKMSKeyId. Metadata S3 : wrapped_dek, iv, auth_tag, k_backup_version en copie redondante [CORR B-03]. Multipart pour artefacts > 100 MB. Credentials du backup IAM user dédié (pas du backend user).
verifyPostUpload() [CORR M-04] : download l'objet, déchiffre via BackupEncryptionService.decrypt(), recalcule SHA3-256 via BackupHashService.computeSHA3_256(), compare avec expectedHash. Ne JAMAIS se fier à l'ETag S3 (MD5/multipart, non lié à SHA3-256).
checkObjectExists() : HeadObject pour le watchdog.
generateBackupKey() : génère la clé S3 conforme aux patterns §5.1 (YYYY/MM/backup_YYYYMMDD.enc pour pg_dump/basebackup, wal/{WAL_SEGMENT_NAME}.enc pour WAL).
Decision trace : injection de BackupEncryptionService dans S3Service pour verifyPostUpload. Alternative : déplacer la vérification dans l'orchestrateur (rejetée — le contrat CC-47-04 attribue cette responsabilité à S3Service).
Tâche 9 — BackupOrchestratorService¶
- Agent : agent-developer
- Niveau : 2
- Dépend de : [T5, T6, T7, T8]
- Contract : CC-47-01
- Projet : backend
- Fichiers :
src/backup/services/backup-orchestrator.service.ts- Description : Service le plus complexe — orchestre les flux F1 et F3 :
runDailyBackup() (flux F1, cron 03:00 Europe/Paris) : Séquence complète en 12 étapes (cf. plan §2.1). Wrappé dans try/finally garantissant au minimum un événement journal [CORR M-06]. Timer cleartext strict : Date.now() - exportEndTs < 300_000 (INV-47-01).
runWeeklyBasebackup() (flux F3, cron dimanche 02:00) : Identique à F1 sauf commande (pg_basebackup -D /tmp/basebackup -Ft -z), type (basebackup), et cron.
replanify() : transition FAILED→SCHEDULED avec vérification du circuit-breaker max_replanification_cycles [CORR M-01]. Si compteur atteint → refus + escalade critique.
Gestion des erreurs : - Échec export (ERR-47-01) : retry max 3, backoff exponentiel delay = min(delay_initial × factor^attempt, timeout_upload) - Échec chiffrement (ERR-47-02) : abort immédiat, purge, alerte - Échec upload (ERR-47-03) : retry dans la même tentative (pas de FAILED→SCHEDULED), puis FAILED - Hash invalide post-upload (ERR-47-04) : objet marqué invalide
child_process.spawn pour pg_dump/pg_basebackup. Ne JAMAIS utiliser Math.random() — crypto.randomUUID() pour tout identifiant.
Tâche 10 — BackupWatchdogService + BackupRelaunchConsumer¶
- Agent : agent-developer
- Niveau : 2
- Dépend de : [T8]
- Contracts : CC-47-07, CC-47-07b
- Projet : backend
- Fichiers :
src/backup/services/backup-watchdog.service.tssrc/backup/consumers/backup-relaunch.consumer.ts- Description : BackupWatchdogService (CC-47-07) : Cron 04:00 Europe/Paris. Vérifie la présence de
backup_YYYYMMDD.encen S3 viaBackupS3Service.checkObjectExists(). Si absent : - Émettre alerte via
AlertService(existant) - Envoyer job dans queue BullMQ
backup-relaunch[CORR B-02] - Attendre le résultat du job (timeout configurable)
- Si relance échoue → escalade critique obligatoire
Indépendance totale : ne JAMAIS injecter BackupOrchestratorService. Ne JAMAIS lire backup_executions. Lire S3 directement. Le watchdog est un producteur de queue, pas un appelant direct.
BackupRelaunchConsumer (CC-47-07b) : Consommateur BullMQ pour la queue backup-relaunch. Injecte BackupOrchestratorService, appelle runDailyBackup(). Si la relance échoue, émet escalade critique via AlertService. Ce consumer est le SEUL pont entre watchdog et orchestrateur (découplage CC-07).
Tâche 11 — BackupReconciliationService + BackupRestorationService¶
- Agent : agent-developer
- Niveau : 2
- Dépend de : [T5, T6, T7, T8]
- Contracts : CC-47-08, CC-47-11
- Projet : backend
- Fichiers :
src/backup/services/backup-reconciliation.service.tssrc/backup/services/backup-restoration.service.ts- Description : BackupReconciliationService (CC-47-08) : Implémente
OnApplicationBootstrap. Au démarrage : - Chercher
BackupExecutionen état SCHEDULED ou RUNNING [CORR M-07] (pas uniquement RUNNING) - Pour RUNNING : vérifier présence objet S3. Si absent → transition vers FAILED + journal
- Pour SCHEDULED : vérifier timestamp dépassé. Si timeout → transition vers FAILED + journal
- Journaliser chaque réconciliation (observable — trace/log/ événement d'audit) [CORR E-09]
- Ne JAMAIS créer un état SUCCESS si l'objet S3 est absent
BackupRestorationService (CC-47-11) : Flux F4 en 10 étapes (cf. plan §2.4) : 1. Sélection du backup logique le plus récent avant targetTimestamp 2. Download + decrypt 3. Vérification hash SHA3-256 AVANT pg_restore (INV-47-08) 4. pg_restore en staging 5. Download + decrypt WAL segments intermédiaires 6. Rejouage WAL (recovery_target_time) 7. Validation cohérence schéma/données (jeu contractuel minimal) 8. Enregistrement preuve 9. Si échec → ouverture incident conformité [CORR B-01], clôture bloquée sans plan d'action
Tâche 12 — BackupModule (wiring DI)¶
- Agent : agent-developer
- Niveau : 3
- Dépend de : [T9, T10, T11]
- Contract : CC-47-12
- Projet : backend
- Fichiers :
src/backup/backup.module.tssrc/backup/index.ts- Description : Module NestJS
@Moduleavec : imports:TypeOrmModule.forFeature([BackupExecution, BackupJournalEvent]),ScheduleModule,BullModule.registerQueue({ name: 'backup-relaunch' })[CORR B-02]providers: tous les services CC-47-01 à CC-47-11 +BackupRelaunchConsumerexports: services nécessaires aux consommateurs externes (si applicable)
Le fichier index.ts réexporte les types publics du module.
Enregistrement dans AppModule : ajouter BackupModule aux imports de app.module.ts (modification d'un fichier existant hors périmètre backup — à coordonner avec l'orchestrateur).
Tâche 13 — Tests unitaires + intégration¶
- Agent : agent-developer
- Niveau : 3
- Dépend de : [T12]
- Contract : CC-47-14
- Projet : backend
- Fichiers :
src/backup/__tests__/backup-validation.service.spec.tssrc/backup/__tests__/backup-config.service.spec.tssrc/backup/__tests__/backup-hash.service.spec.tssrc/backup/__tests__/backup-state.service.spec.tssrc/backup/__tests__/backup-encryption.service.spec.tssrc/backup/__tests__/backup-journal.service.spec.tssrc/backup/__tests__/backup-s3.service.spec.tssrc/backup/__tests__/backup-orchestrator.service.spec.tssrc/backup/__tests__/backup-watchdog.service.spec.tssrc/backup/__tests__/backup-reconciliation.service.spec.tssrc/backup/__tests__/backup-restoration.service.spec.tssrc/backup/__tests__/backup-integration.spec.ts- Description : Test runner : Jest [CORR M-08].
Tests unitaires : chaque service reçoit ses propres tests.
Couverture contractuelle : - TC-NOM-01 à TC-NOM-08 (flux nominaux) - TC-ERR-01 à TC-ERR-08 (cas d'erreur) - TC-INV-01 à TC-INV-09 (invariants — GIVEN/WHEN/THEN formalisés dans le plan §5.3 suite à R-03) - TC-NEG-01 à TC-NEG-08 (négatifs/adversariaux) - TC-NR-01 à TC-NR-05 (non-régression)
Tests critiques : - Roundtrip encrypt-decrypt obligatoire (JAMAIS tester encrypt et decrypt isolément — cf. PD-282) - State machine exhaustive : 6×6 = 36 combinaisons de transitions - Journal append-only : tentative UPDATE → rejet RLS - Déduplication backup_id : insertion doublon → DO NOTHING - Timer cleartext < 300s - Circuit-breaker replanification (max cycles atteint)
Prérequis : - TC-NOM-02 (WAL) : conditionné à H-TECH-01. Si slot de réplication OVH non confirmé → SKIP avec justification [CORR m-04] - Ne JAMAIS mocker la validation §5.1 dans les tests d'intégration - Ne JAMAIS utiliser Math.random() — crypto.randomUUID()
Mocks : - S3 : localstack ou mock @aws-sdk/client-s3 - Vault Transit : mock HTTP des endpoints encrypt/decrypt - pg_dump/pg_basebackup : mock child_process - BullMQ : @golevelup/ts-jest mock queue - AlertService : mock existant
4. Résumé d'exécution¶
| Métrique | Valeur |
|---|---|
| Total tâches | 13 |
| Niveaux de parallélisation | 4 |
| Temps séquentiel estimé | 25.5h |
| Temps parallèle estimé | 11h |
| Speedup factor | 2.32x |
| Stratégie Git | branch_per_level |
| Projets impactés | backend (T1-T2, T5-T13), infra (T3-T4) |
| Tâches backend | 11 |
| Tâches infra | 2 |
| Contracts couverts | CC-47-01 à CC-47-16 (16/16) |
Détail par niveau¶
| Niveau | Tâches | Durée parallèle | Branches |
|---|---|---|---|
| 0 | T1, T2, T3, T4 | 2h | 4 branches (2 backend + 2 infra) |
| 1 | T5, T6, T7, T8 | 2h | 4 branches backend |
| 2 | T9, T10, T11 | 3h | 3 branches backend |
| 3 | T12, T13 | 4h | 2 branches backend |
Risques d'intégration au merge¶
| Risque | Probabilité | Mitigation |
|---|---|---|
| Types incompatibles entre services | Faible | Types centralisés dans T1, contracts font référence |
| Conflit import paths après merge L0 | Faible | Chaque tâche produit des fichiers distincts |
Conflit app.module.ts (T12) | Moyen | T12 modifie un fichier partagé — merge manuel si conflit |
| Tests échouent après merge complet | Moyen | Exécution Jest post-merge avant acceptabilité |