Introduction
L'automatisation pour Apache Spark est plus facile à adopter lorsqu'elle apporte une valeur claire et à faible risque sur des travaux réels. Ce guide montre comment mettre en place le CI/CD pour Spark avec des exemples pratiques que vous pouvez adapter à votre stack. Vous allez inventorier votre environnement, créer un pilote étroit, configurer les étapes de build et de test, valider avec des données à petite échelle, et promouvoir vers le staging et la production avec des contrôles sûrs et des options de rollback.
L'approche privilégie des étapes petites et inspectables qui produisent des artefacts cohérents, une configuration explicite, un retour rapide et une récupération rapide. Cette combinaison maintient les pipelines de données fiables, surtout lorsque les jobs Spark dépendent de Kafka, HDFS et d'ordonnanceurs comme Apache Airflow ou NiFi.
Inventaire des versions et de l’environnement
Le CI/CD Spark réussit ou échoue selon la dérive d'environnement. Avant de créer tout pipeline, documentez les versions exactes et la topologie qui importent pour builder et exécuter vos jobs.
Utilisez ce modèle d'inventaire simple et remplissez les valeurs réelles de votre configuration (valeurs d'exemple construites) :
| Élément | Valeur d'exemple | Où enregistrer |
|---|---|---|
| Runtime Spark | 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 | docs contrats de données |
| Dépôt d'artefacts | JARs et wheels versionnés | manifeste de release |
Pourquoi cela importe :
- Cohérence du build : le job doit être compilé et packagé contre les mêmes versions majeures de Spark et Scala utilisées dans le cluster.
- Résolution des dépendances : le shading ou le packaging PySpark doit correspondre aux classpaths d'exécution.
- Reproductibilité : chacun peut recréer une release ou effectuer un rollback avec les mêmes entrées.
Prérequis pour le pilote :
- Un job Spark qui produit une sortie déterministe et petite à partir d'un échantillon d'entrée.
- Des tests unitaires couvrant les transformations principales et les contrats de schéma.
- Un format d'artefact versionné (pour Scala/Java :
myjob_1.2.3.jar; pour PySpark :myjob-1.2.3-py3-none-any.whlet dépendances zippées via--py-files). - Un environnement de staging capable d'exécuter le job de bout en bout avec des données de test sûres.
Chemin de configuration sûr
Un pilote petit et mesurable construit la confiance et évite la complexité cachée.
Recommandations :
- Commencez par un unique job batch qui lit une source stable et écrit une petite sortie. Évitez les tables larges ou les shuffles coûteux dans la première itération.
- Utilisez des artefacts immuables et versionnés. Séparez la configuration d'exécution du code. Promouvez le même artefact à travers les étapes au lieu de le reconstruire.
- Introduisez des points de contrôle inspectables localement : tests unitaires, validation de schéma, et un
spark-submiten mode local sur quelques fichiers d'échantillon. - Ajoutez des portes de promotion qui imposent des signaux de qualité de base : tous les tests passent, artefact construit une fois, validation réussie, exécution à sec en staging avec métriques attendues.
Pourquoi ce chemin fonctionne :
- Il réduit la boucle de rétroaction à quelques contrôles observables, ce qui diminue le retravail en clarifiant où surviennent les problèmes.
- Il est facile à inspecter localement avant tout déploiement, ce qui baisse le risque.
Étapes pratiques d'implémentation CI/CD
Cette section présente un flux concret mais agnostique d'outils. Vous pouvez implémenter des étapes similaires avec votre service d'automatisation préféré.
Arborescence du dépôt (exemple construit) :
my-spark-job/
src/ # code Scala, Java ou PySpark
tests/ # tests unitaires
sample-data/ # petits fichiers CSV/JSON/Parquet pour validation
build.sbt ou pom.xml # ou setup.py/pyproject.toml pour PySpark
requirements.txt # pour PySpark
job.conf # config runtime (pas de secrets)
ENVIRONMENT.md # inventaire versions et topologie
RELEASE_MANIFEST.json # versions d'artefacts promues par étape
Build et test
Scala/Java (exemple sbt) :
sbt clean test
sbt assembly # si vous utilisez un fat JAR ; sinon package
Résultats attendus :
- Les tests rapportent succès et résumé de couverture.
- Un unique artefact est produit, par exemple
target/scala-2.12/myjob_1.0.0.jaroumyjob_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 :
- Les tests passent et produisent un résumé concis.
dist/myjob-1.0.0-py3-none-any.whlexiste et respecte un seuil de taille connu.
Validation locale avec spark-submit
Exécutez une petite validation en mode local pour confirmer le packaging et les dépendances. Utilisez des entrées déterministes et vérifiez une métrique comme le nombre d'enregistrements ou un checksum.
Exemple 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/
Exemple PySpark avec dépendances additionnelles :
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 :
- Les compteurs de lignes correspondent aux petits nombres attendus (exemple construit : 1 000 lignes d'entrée → 980 lignes de sortie après filtres).
- Le schéma de sortie équivaut au contrat vérifié par vos tests.
- Aucune erreur
ClassNotFound,NoSuchMethodouPy4Jne survient.
Publication de l'artefact
Publiez l'artefact validé dans un emplacement versionné. Gardez un manifeste simple qui mappe l'environnement à la version de l'artefact.
Exemple de manifeste construit :
{
"job": "my-spark-job",
"versions": {
"build": "1.0.0",
"staging": "1.0.0",
"production": "0.9.3"
}
}
Commandes d'upload (remplacez par votre stockage) :
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 données petites mais réalistes. Utilisez des limites de ressources explicites pour éviter les jobs incontrôlés.
Exemple 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 :
- Le job se termine dans un temps borné, par exemple sous 10 minutes pour le dataset de staging (exemple construit).
- Spark History Server ne montre aucune échec de tâche au-delà des re-tentatives transitoires.
- Le dossier de sortie contient des fichiers partitionnés avec les compteurs attendus.
Promotion avec portes
Ne promouvez que les artefacts qui passent des portes explicites. Gardez ce tableau de référence.
| Étape | Contrôles de porte | Résultat attendu |
|---|---|---|
| Build | Tests unitaires, lints, compilation | 0 échecs, artefact déterministe |
| Validation locale | spark-submit local[2] | Métriques de sortie conformes au baseline |
| Staging | Ressources limitées, dry-run | Exécution terminée, métriques dans les bornes |
| Promotion production | Approbation manuelle ou temporelle, version épinglée | Même ID d'artefact promu |
Patterns de déploiement en production
Choisissez un ou combinez :
- Exécutions planifiées : Configurez Airflow ou NiFi pour pointer vers l'artefact versionné et les paramètres runtime. La promotion se fait en mettant à jour seulement la chaîne de version dans la config de l'ordonnanceur.
- Backfills ad hoc : Fournissez un paramètre job comme
--backfill 2023-10-01..2023-10-07utilisable dans une fenêtre de maintenance contrôlée. - Jobs streaming : Pour Structured Streaming, ajoutez une instance canary en production lisant un sous-ensemble de partitions ou topics, puis basculez le trafic progressivement.
Garde-fous :
- Utilisez des écritures idempotentes avec chemins de staging et déplacements atomiques (là où supporté) pour éviter les sorties partielles.
- Incluez des IDs d'exécution dans les chemins de sortie pour la traçabilité, par exemple
output/run_id=20240212_153000Z/. - Maintenez les checkpoints séparés par environnement et par 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 : Validez noms de champs, types et nullabilité à la lecture et avant l'écriture.
- Invariants ligne par ligne : Par exemple, unicité de clé primaire ou plages de valeurs.
- Métriques agrégées : Comptages d'enregistrements, compteurs d'erreurs, tailles des partitions de sortie.
Contrôles de performance et stabilité :
- Surveillez l'utilisation CPU et mémoire des executors. Confirmez l'absence de Full GC répétés.
- Vérifiez les tailles de lecture/écriture shuffle et le skew. Un skew élevé suggère un repartitionnement ou du salting.
- Validez que les broadcast joins s'appliquent seulement pour tailles de table appropriées.
Observabilité runtime :
- Spark UI : Confirmez que les échecs de tâche par étape restent dans les niveaux de retry attendus.
- Event logs : Gardez le logging d'événements activé pour analyse post-mortem.
- Vues ordonnanceur : Dans Airflow ou NiFi, vérifiez les tendances de durée de tâche et taux de retry.
Exemples de scripts de vérification rapide (construits) :
Vérifier le compte 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 shell :
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 []etWRONG {}pour une sortie valide.
Modes de défaillance et récupération
Les casses courantes dans le CI/CD Spark sont prévisibles. Planifiez-les et documentez les corrections rapides.
| Mode de défaillance | Symptôme | Diagnostic rapide |
|---|---|---|
| Incompatibilité de version (Spark/Scala) | NoSuchMethodError ou incompatibilité binaire | Comparer ENVIRONMENT.md et logs de build d'artefact |
| Conflit de dépendance | ClassNotFound ou classe shaded fuite | Inspecter manifeste JAR et règles de shading assembly |
| Lacune packaging PySpark | Module introuvable à l'exécution | Vérifier --py-files et wheel inclus |
| Problème de credentials | AccessDenied sur HDFS/S3/Kafka | Retester principal de service ou portée de clé pour rôle runtime |
| Dérive de schéma | Échec d'écriture ou erreurs job aval | Comparer plan d'évolution de schéma vs source réelle |
| Jointures skew | Queues longues dans étapes | Spark UI timeline étape, vérifier distribution clés entrée |
| Incompatibilité checkpoint | Job streaming échoue au restart | Utiliser checkpoint séparé par version et environnement |
| Écritures non-idempotentes | Duplicatas ou sorties partielles | Assurer rename atomique ou write-to-temp-then-commit |
Patterns de récupération et rollback :
- Épinglage de version et revert
- Gardez les artefacts de release immuables et référencés par versions explicites.
- Pour rollback, mettez à jour seulement la référence de version dans
RELEASE_MANIFEST.jsonet la config de l'ordonnanceur pour le job. - Exemple de mise à jour manifeste (construit) : passer production de
1.0.0à0.9.3et redéployer la config.
- Gardes de sortie
- Écrivez dans un chemin temporaire scopé à l'exécution comme
output/tmp/run_id=...et validez en renommant vers le chemin final après validation. Si validation échoue, supprimez le chemin temporaire et ne commitez pas.
- Isolation des checkpoints streaming
- Maintenez un chemin checkpoint par version majeure du job. Si une nouvelle version échoue, stoppez-la, redéployez l'artefact précédent, et pointez vers le checkpoint précédent.
- Porte de contrat de données
- Définissez un schéma explicite et un seuil minimal de lignes ou de qualité de données pour le staging. Échouez la promotion si seuils non atteints.
- Échange rapide d'artefact
- Si vous maintenez un simple fichier
RELEASEdans HDFS ou objet storage contenant le numéro de version actif, le changement de version est une mise à jour de fichier unique.
Exemple HDFS :
echo "0.9.3" > RELEASE
hdfs dfs -put -f RELEASE /apps/spark/jobs/my-spark-job/RELEASE
La logique applicative lit la version au démarrage et résout le chemin d'artefact en conséquence. Au prochain run, l'ancien artefact est utilisé.
Vérifications post-rollback :
- Confirmez un run vert en staging et production avec l'artefact revenu.
- Vérifiez que les chemins de sortie du run échoué sont isolés ou supprimés.
- Marquez la version défaillante comme bloquée pour promotion jusqu'à correction mergée.
Check-list opérations
Utilisez ce runbook pour garder les opérations répétables.
Préparation de release
- Mettez à jour
ENVIRONMENT.mdquand les versions de cluster, Spark, Scala ou Python changent. - Mergez seulement des changements petits, bien scopés, avec tests et release notes claires.
- Assurez que
sample-datareflète le schéma source actuel et des valeurs typiques.
Build et validation
- Construisez un unique artefact immuable par commit destiné à la release.
- Exécutez tests unitaires et validation locale
spark-submitsur données d'échantillon. - Capturez métriques de sortie : compteur entrée, sortie, erreurs, durée.
Staging
- Déployez l'artefact construit avec ressources conservatrices.
- Comparez métriques aux baselines staging précédentes. Investiguez les écarts.
- Confirmez qu'aucun secret ou paramètre spécifique d'environnement n'est baked dans l'artefact.
Promotion
- Épinglez la version d'artefact spécifique dans la configuration de l'ordonnanceur.
- Annoncez le changement et la fenêtre de maintenance si nécessaire.
- Gardez une version de rollback prête dans
RELEASE_MANIFEST.json.
Production
- Surveillez Spark UI et tableaux de bord ordonnanceur pendant et après le premier run d'une nouvelle version.
- Validez sorties : schéma, compteurs, tailles partitions.
- Pour streaming, lancez topologie canary d'abord, puis scale.
Rollback
- Remettez les pointeurs de version vers la dernière version connue bonne.
- Arrêtez ou quarantainez les sorties du run fautif.
- Créez un ticket pour capturer cause racine et un test pour éviter récurrence.
Hygiène périodique
- Rebuild sur nouvelles versions Spark ou dépendances dans une branche séparée et validez avec les mêmes portes.
- Purgez anciens artefacts et sorties selon politiques de rétention.
- Revoyez seuils de qualité de données chaque trimestre et ajustez selon volume ou évolution de schéma.
Conclusion
Automatiser la livraison Apache Spark est plus efficace quand le scope est petit, l'artefact immuable, et chaque étape de promotion gardée par des contrôles explicites. Commencez par un pilote étroit que vous pouvez valider localement, capturez un inventaire complet des versions et de l'environnement, et câblez un pipeline qui build une fois, valide avec de petites données, déploie en staging avec limites de ressources, et promeut en production à travers des portes claires. Prévoyez les défaillances prévisibles comme les incompatibilités de version, conflits de dépendances, et dérive de schéma, et gardez le rollback comme un changement rapide et à faible risque sur un pointeur de version, pas un rebuild.
Avec ces patterns en place, votre équipe passera plus de temps à améliorer les transformations et moins à poursuivre les problèmes d'environnement. La prochaine étape pratique est de choisir un job, assembler son ENVIRONMENT.md et RELEASE_MANIFEST.json, implémenter la validation locale, et connecter votre ordonnanceur pour consommer les artefacts versionnés. Étendez aux jobs additionnels seulement après que le pilote devienne ennuyeux et prévisible.