Introduction
L'adoption de l'automatisation pour Apache Spark est plus simple quand elle apporte une valeur claire et peu risquée sur des jobs réels. Ce guide fournit une mise en œuvre concrète de l'Apache Spark CI/CD, avec des exemples adaptables à votre stack. Vous allez inventorier l'environnement, créer un pilote réduit, préparer les étapes de build et de test, valider avec de petites données, puis promouvoir vers staging et production à l'aide de contrôles explicites et d'options de rollback. L'approche privilégie des étapes petites et inspectables, des artefacts cohérents, une configuration explicite, un feedback rapide et une récupération simple. Ce mélange stabilise les pipelines de données, notamment quand les jobs Spark dépendent de Kafka, HDFS et d'ordonnanceurs comme Apache Airflow ou NiFi. Les expressions clés à intégrer dans vos pratiques et documents incluent Apache Spark CI/CD, Apache Spark automation, Apache Spark deployment, Apache Spark pipeline et Apache Spark rollback.
Inventaire des versions et de l'environnement
La réussite d'une CI/CD Spark se joue sur la dérive d'environnement. Avant toute pipeline, documentez avec précision les versions et la topologie nécessaires pour construire et exécuter vos jobs.
Modèle d'inventaire (valeurs d'exemple construites) :
| Élément | Valeur d'exemple | Où consigner |
|---|---|---|
| Spark runtime | 3.4.1 | repo/ENVIRONMENT.md |
| Hadoop/YARN | 3.3.4 | repo/ENVIRONMENT.md |
| Scala / JVM | Scala 2.12, Temurin JDK 11 | build.sbt, ENVIRONMENT.md |
| Python | 3.10.6 | requirements.txt, ENVIRONMENT.md |
| Gestionnaire de cluster | YARN, 10 nœuds workers | ENVIRONMENT.md |
| Stockage | HDFS /apps/spark, S3 s3://my-bucket | ENVIRONMENT.md |
| Ordonnanceur | Airflow 2.7, NiFi 1.21 | configs ordonnanceur |
| Sources de données | Kafka 3.5, topic events_v1 | contrats de données |
| Dépôt d'artefacts | JARs et wheels versionnés | manifeste de release |
Pourquoi c'est important :
- Cohérence de build : compiler/packager avec les mêmes versions majeures de Spark et Scala que le cluster.
- Résolution des dépendances : shading et packaging PySpark alignés avec le classpath runtime.
- Reproductibilité : permettre de reconstruire une release ou opérer un rollback avec les mêmes entrées.
Prérequis pour le pilote :
- Un job Spark qui produit une sortie déterministe à partir d'un échantillon.
- Des tests unitaires couvrant les transformations clés et le contrat de schéma.
- Un format d'artefact versionné (Scala/Java : myjob_1.2.3.jar ; PySpark : myjob-1.2.3-py3-none-any.whl et dépendances via --py-files).
- Un environnement de staging capable d'exécuter le job de bout en bout avec des données sûres.
Chemin de configuration sûr
Un pilote petit et mesurable réduit les surprises.
Recommandations :
- Démarrez avec un job batch unique lisant une source stable et écrivant une petite sortie. Évitez d'emblée les larges tables et shuffles coûteux.
- Utilisez des artefacts immuables et versionnés. Séparez configuration runtime et code. Promouvez le même artefact entre les étapes.
- Ajoutez des points d'arrêt inspectables : tests unitaires, validation de schéma, spark-submit en mode local sur quelques fichiers.
- Introduisez des contrôles de promotion : tous les tests passent, artefact construit une seule fois, validation réussie, dry run en staging avec métriques attendues.
Pourquoi ça marche :
- La boucle de feedback est courte et observable, ce qui réduit le retravail en clarifiant l'origine des problèmes.
- C'est facilement vérifiable en local avant tout déploiement, donc moins risqué.
Implémentation CI/CD pratique
Voici un flux concret et agnostique en matière d'outils. Reproduisez des étapes similaires avec votre service d'automatisation préféré.
Arborescence de dépôt (exemple construit) :
my-spark-job/
src/ # Code Scala, Java ou PySpark
tests/ # Tests unitaires
sample-data/ # Petits fichiers CSV/JSON/Parquet
build.sbt ou pom.xml # Ou setup.py/pyproject.toml pour PySpark
requirements.txt # Pour PySpark
job.conf # Config runtime (sans secrets)
ENVIRONMENT.md # Inventaire versions et topologie
RELEASE_MANIFEST.json # Versions d'artefacts par environnement
Build et tests
Scala/Java (exemple sbt) :
sbt clean test
sbt assembly # si JAR "fat" ; sinon package
Résultats attendus :
- Tests OK et résumé de couverture.
- Un artefact unique : target/scala-2.12/myjob_1.0.0.jar ou myjob_1.0.0-all.jar.
PySpark (pytest et wheel) :
pip install -r requirements.txt
pytest -q
python -m build # ou python setup.py bdist_wheel
Résultats attendus :
- Tests réussis avec un résumé concis.
- dist/myjob-1.0.0-py3-none-any.whl existe et reste sous un seuil de taille connu.
Validation locale avec spark-submit
Exécutez une validation locale pour confirmer l'empaquetage et les dépendances avec des entrées déterministes et une métrique vérifiable (compte de lignes ou checksum).
Scala/Java :
spark-submit \
--master local[2] \
--class com.example.jobs.MyJob \
target/scala-2.12/myjob_1.0.0.jar \
--config file:./job.conf \
--input ./sample-data/input/ \
--output ./target/validation-output/
PySpark avec dépendances :
spark-submit \
--master local[2] \
--py-files dist/myjob-1.0.0-py3-none-any.whl, extra_deps.zip \
src/main.py \
--config file:./job.conf \
--input ./sample-data/input/ \
--output ./target/validation-output/
Cibles de vérification :
- Comptes de lignes conformes (ex. 1 000 entrées → 980 sorties après filtres).
- Schéma de sortie conforme au contrat testé.
- Pas d'erreurs ClassNotFound, NoSuchMethod ni Py4J.
Publication des artefacts
Publiez l'artefact validé à un emplacement versionné. Tenez un manifeste simple mappant l'environnement à la version.
Exemple de manifeste :
{
"job": "my-spark-job",
"versions": {
"build": "1.0.0",
"staging": "1.0.0",
"production": "0.9.3"
}
}
Commandes d'upload (à adapter) :
- HDFS :
hdfs dfs -mkdir -p /apps/spark/jobs/my-spark-job/1.0.0/
hdfs dfs -put -f target/scala-2.12/myjob_1.0.0.jar /apps/spark/jobs/my-spark-job/1.0.0/
- Stockage objet :
aws s3 cp target/scala-2.12/myjob_1.0.0.jar s3://my-bucket/jobs/my-spark-job/1.0.0/
Déploiement en staging
Déployez le même artefact sur un cluster de staging avec des ressources explicites pour éviter les débordements.
YARN (Scala/Java) :
spark-submit \
--master yarn \
--deploy-mode cluster \
--conf spark.executor.instances=2 \
--conf spark.executor.memory=2g \
--class com.example.jobs.MyJob \
hdfs:///apps/spark/jobs/my-spark-job/1.0.0/myjob_1.0.0.jar \
--config hdfs:///apps/spark/configs/job.conf \
--input hdfs:///data/staging/input/ \
--output hdfs:///data/staging/output/my-spark-job/1.0.0/
Résultats attendus :
- Exécution bornée (ex. < 10 minutes pour l'échantillon en staging).
- Spark History Server sans échecs de tâches persistants.
- Dossier de sortie partitionné avec les comptes attendus.
Promotion avec contrôles explicites
Ne promeut que les artefacts qui passent des contrôles clairs.
| Étape | Contrôles | Résultat attendu |
|---|---|---|
| Build | Tests unitaires, lints, compilation | 0 échec, artefact déterministe |
| Validation locale | spark-submit local[2] | Métriques conformes à la référence |
| Staging | Ressources limitées, dry run | Exécution complète, métriques dans les bornes |
| Production | Approbation temporelle/manuelle, version fixée | Même ID d'artefact promu |
Schémas de déploiement en production
- Exécutions planifiées : configurez Airflow ou NiFi pour pointer vers l'artefact versionné et les paramètres runtime. La promotion consiste à mettre à jour la version dans la config de l'ordonnanceur.
- Backfills ad hoc : exposez un paramètre comme --backfill 2023-10-01..2023-10-07 dans un créneau contrôlé.
- Jobs streaming : avec Structured Streaming, démarrez un canari lisant un sous-ensemble de partitions/topics, puis basculez progressivement.
Garde-fous :
- Écritures idempotentes avec chemins de staging et renommage atomique quand c'est supporté.
- Émettre des IDs d'exécution dans les chemins de sortie : output/run_id=20240212_153000Z/.
- Checkpoints séparés par environnement et version majeure pour éviter la corruption d'état.
Vérification et diagnostics
La vérification doit être explicite, rapide et proche du job.
Contrôles fonctionnels :
- Assertions de schéma : noms, types, nullabilité en entrée/avant écriture.
- Invariants au niveau ligne : unicité de clé primaire, plages de valeurs.
- Métriques agrégées : comptes d'entrées/sorties, erreurs et tailles de partitions.
Performance et stabilité :
- Surveiller CPU/mémoire des executors. Pas de GC complets répétés.
- Tailles de shuffle et skew : fort skew → repartition/salting.
- Joins broadcast seulement pour des tables de taille adaptée.
Observabilité runtime :
- UI Spark : vérifier que les échecs de tâches restent dans les niveaux de retry attendus.
- Logs d'événements : garder activés pour analyses a posteriori.
- Vues ordonnanceur (Airflow/NiFi) : durées et taux de retry.
Exemples rapides (construits) :
Vérifier le nombre d'enregistrements avec Spark SQL :
spark-sql -e "SELECT COUNT(1) FROM parquet.'hdfs:///data/staging/output/my-spark-job/1.0.0/'"
Valider un schéma avec pyspark :
pyspark -q <<'PY'
from pyspark.sql import SparkSession
spark = SparkSession.builder.getOrCreate()
df = spark.read.parquet('hdfs:///data/staging/output/my-spark-job/1.0.0/')
expected = {
'user_id': 'string',
'event_time': 'timestamp',
'amount': 'double'
}
actual = {f.name: f.dataType.simpleString() for f in df.schema.fields}
missing = set(expected) - set(actual)
wrong = {k: (expected[k], actual.get(k)) for k in expected if actual.get(k) != expected[k]}
print('MISSING', sorted(list(missing)))
print('WRONG', wrong)
PY
Résultats attendus :
- MISSING [] et WRONG {} pour une sortie valide.
Modes de panne et reprise
Les pannes Spark en CI/CD sont prévisibles. Anticipez-les et documentez des corrections rapides.
| Mode de panne | Symptôme | Diagnostic rapide |
|---|---|---|
| Mismatch de versions (Spark/Scala) | NoSuchMethodError, incompatibilité binaire | Comparer ENVIRONMENT.md et logs de build |
| Conflit de dépendances | ClassNotFound, shading qui fuit | Inspecter le manifest JAR et les règles d'assembly |
| Packaging PySpark incomplet | Module introuvable au runtime | Vérifier --py-files et la wheel |
| Problème d'identifiants | AccessDenied sur HDFS/S3/Kafka | Re-tester le principal/clé du runtime |
| Dérive de schéma | Échec d'écriture ou erreur downstream | Comparer plan d'évolution vs source réelle |
| Joins déséquilibrés | Longues traînes de stages | UI Spark, distribution des clés |
| Checkpoint incompatible | Échec streaming au redémarrage | Checkpoints séparés par version/env |
| Écritures non idempotentes | Doublons ou sorties partielles | Renommage atomique ou write-then-commit |
Stratégies de récupération et rollback :
- Version figée et retour arrière
- Artefacts immuables référencés par versions explicites.
- Pour revenir en arrière, modifiez uniquement la version dans RELEASE_MANIFEST.json et la config de l'ordonnanceur.
- Exemple : passer production de 1.0.0 à 0.9.3, redéployer la config.
- Garde de sortie
- Écrire d'abord vers output/tmp/run_id=..., valider, puis renommer vers le chemin final. En cas d'échec, supprimer le chemin temporaire et ne pas valider.
- Isolement des checkpoints streaming
- Un checkpoint par version majeure et par environnement. En cas d'échec, rétablir l'artefact précédent et repointer le checkpoint antérieur.
- Contrat de données bloquant
- Définir un schéma explicite et un seuil minimal de qualité/volume pour le staging. Bloquer la promotion si non atteints.
- Bascule rapide d'artefact
- Maintenir un fichier RELEASE dans HDFS/objet indiquant la version active. Le changement de version devient une simple mise à jour.
Exemple HDFS :
echo "0.9.3" > RELEASE
hdfs dfs -put -f RELEASE /apps/spark/jobs/my-spark-job/RELEASE
Après rollback :
- Confirmer une exécution verte en staging et production.
- Vérifier que les sorties de la version fautive sont isolées ou supprimées.
- Marquer la version échouée comme bloquée jusqu'au correctif.
Checklist d'exploitation
Préparer la release
- Mettre à jour ENVIRONMENT.md lors de tout changement de versions (cluster, Spark, Scala, Python).
- Fusionner des changements petits et testés, avec notes de version claires.
- Tenir sample-data en phase avec le schéma et les valeurs typiques.
Build et validation
- Construire un artefact immuable par commit destiné à la release.
- Lancer tests unitaires et validation locale spark-submit.
- Capturer les métriques : entrées, sorties, erreurs, durée.
Staging
- Déployer l'artefact construit avec des ressources conservatrices.
- Comparer aux bases de référence staging et investiguer les écarts.
- S'assurer qu'aucun secret ni paramètre spécifique n'est figé dans l'artefact.
Promotion
- Figer la version d'artefact dans la configuration de l'ordonnanceur.
- Annoncer le changement et, si nécessaire, la fenêtre de maintenance.
- Garder une version de rollback prête dans RELEASE_MANIFEST.json.
Production
- Surveiller l'UI Spark et les tableaux de bord d'ordonnancement pendant/après la première exécution.
- Valider la sortie : schéma, comptes et tailles de partitions.
- Pour le streaming : canari d'abord, puis montée en charge.
Rollback
- Revenir au dernier bon en mettant à jour les pointeurs de version.
- Stopper ou mettre en quarantaine les sorties fautives.
- Ouvrir un ticket pour la cause racine et ajouter un test de non-régression.
Hygiène périodique
- Rebuilder sur nouvelles versions Spark/dépendances dans une branche dédiée et valider avec les mêmes contrôles.
- Purger artefacts et sorties anciens selon la rétention.
- Revoir trimestriellement les seuils de qualité de données.
Conclusion
L'automatisation de la livraison Spark fonctionne mieux quand le périmètre est restreint, l'artefact immuable et chaque promotion encadrée par des contrôles explicites. Démarrez par un pilote que vous pouvez valider en local, constituez un inventaire complet des versions et de l'environnement, puis mettez en place une chaîne qui construit une seule fois, valide sur petites données, déploie en staging avec des limites de ressources et promeut en production de manière contrôlée. Anticipez les pannes prévisibles (mismatch de versions, conflits de dépendances, dérive de schéma) et faites du rollback un simple changement de pointeur de version plutôt qu'une reconstruction. Prochaine étape : choisissez un job, préparez ENVIRONMENT.md et RELEASE_MANIFEST.json, implémentez la validation locale et connectez votre ordonnanceur pour consommer des artefacts versionnés. Étendez ensuite à d'autres jobs une fois le pilote devenu fiable et prévisible.