Inhaltsverzeichnis20 Abschnitte
- TL;DR
- Warum Container für Data Pipelines?
- Der Container-Native-Data-Platform-Stack
- Dockerfile-Best-Practices für Data Pipelines
- Multi-Stage-Build für PySpark
- Image-Tagging-Strategie
- Airflow auf Kubernetes: KubernetesExecutor
- Helm-Deployment
- Task-Level-Resource-Overrides
- Spark-Operator
- Install via Helm
- SparkApplication CRD
- Resource-Management und Autoscaling
- Namespace-Resource-Quotas
- Cluster-Autoscaler
- CI/CD-Pipeline für Data Pipelines
- Observability
- Key-Metrics für Data-Pipeline-Pods
- FAQ
- Fazit
Container-native Data Pipelines sind vom Experiment zur Standard- Praxis geworden. Docker für reproduzierbare Environments plus Kubernetes für elastisches Scheduling haben verändert, wie Plattform- Teams Data-Workloads bauen, deployen und betreiben. Dieser Guide geht tief in die Patterns, die in Production wirklich funktionieren.
TL;DR
- Multi-Stage-Builds halten Runtime-Images klein.
- KubernetesExecutor statt CeleryExecutor — pro Task ein frischer Pod, echte Isolation.
- Spark-Operator statt Standalone-Spark-Cluster — Lifecycle als K8s-Custom-Resource.
- Resource-Quotas pro Namespace verhindern, dass ein Team das Cluster monopolisiert.
- DACH-Hinweis: EU-Region wählen (eu-central-1, europe-west3), bei personenbezogenen Daten DSGVO-konforme Image-Registry (ECR EU, GCR Europe, ACR Westeurope).
Warum Container für Data Pipelines?
Der Case für Containerisierung war nie stärker:
| Problem (Pre-Container) | Lösung (Container-Native) |
|---|---|
| "Funktioniert bei mir auf der Maschine"-Dependency-Hell | Immutable Images mit gepinnten Dependencies |
| Shared-Cluster-Resource-Contention | Namespace-Isolation mit Resource-Quotas |
| Spark-Scaling braucht Cluster-Resize | Spark-Operator provisioniert Worker pro Job |
| Neue Python-Version bricht alles | Jede Pipeline trägt ihre eigene Runtime |
| Audit-Trail für Pipeline-Environments | Image-Digest = reproduzierbares Environment |
| Multi-Team-Monolith-Airflow | Isolierte DAG-Environments pro Team |
Der Container-Native-Data-Platform-Stack
graph TD
subgraph CI/CD
GH[GitHub Actions] --> REG[Container Registry
ECR / GCR / ACR]
end
subgraph Kubernetes Cluster
REG --> AIRFLOW[Apache Airflow
Kubernetes Executor]
REG --> SPARK[Spark Operator]
REG --> DBT[dbt Runner
CronJob]
subgraph Namespaces
AIRFLOW --> NS1[namespace: airflow]
SPARK --> NS2[namespace: spark-jobs]
DBT --> NS3[namespace: dbt]
end
subgraph Storage
NS2 --> PVC[PersistentVolumeClaim
EFS / Azure Files]
NS1 --> S3[S3 / GCS
remote logs]
end
end
subgraph Monitoring
NS1 & NS2 & NS3 --> PROM[Prometheus]
PROM --> GRAF[Grafana]
end
Dockerfile-Best-Practices für Data Pipelines
Multi-Stage-Build für PySpark
# Stage 1: Dependencies bauen
FROM python:3.11-slim AS builder
WORKDIR /build
# Build-Tools
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt
# Stage 2: Runtime-Image
FROM openjdk:17-slim AS runtime
# Python installieren
RUN apt-get update && apt-get install -y --no-install-recommends \
python3.11 \
python3-pip \
&& rm -rf /var/lib/apt/lists/*
# Installierte Packages aus Builder kopieren
COPY --from=builder /root/.local /root/.local
# Spark installieren
ENV SPARK_VERSION=3.5.1
ENV HADOOP_VERSION=3
RUN pip install pyspark==${SPARK_VERSION}
# Pipeline-Code
WORKDIR /app
COPY src/ ./src/
COPY entrypoint.sh .
RUN chmod +x entrypoint.sh
# Non-Root-User für Security
RUN useradd -m -u 1000 spark
USER spark
ENTRYPOINT ["./entrypoint.sh"]
Kernprinzipien:
- Multi-Stage-Builds: Runtime-Images klein halten
- Non-Root-User: reduziert Angriffsfläche
- Alle Versionen pinnen:
requirements.txtmit exakten Hashes - Keine Secrets im Image: K8s-Secrets oder externe Secret-Stores
Image-Tagging-Strategie
Niemals :latest in Production. Immer immutable Tags:
# Tag mit Git-SHA
IMAGE_TAG=$(git rev-parse --short HEAD)
docker build -t ecr.amazonaws.com/harbinger/pipeline:${IMAGE_TAG} .
docker push ecr.amazonaws.com/harbinger/pipeline:${IMAGE_TAG}
# Auch mit Semver-Tag für Release-Images
docker tag ecr.amazonaws.com/harbinger/pipeline:${IMAGE_TAG} \
ecr.amazonaws.com/harbinger/pipeline:v1.4.2
Airflow auf Kubernetes: KubernetesExecutor
Der KubernetesExecutor startet pro Task einen frischen Pod. Das ist der Production-grade-Ansatz für Teams, die wollen:
- Task-Isolation: ein fehlschlagender Task affectet keinen anderen
- Resource-Customization: CPU/Memory pro Task-Type
- Horizontale Skalierbarkeit: kein fixer Worker-Pool
Helm-Deployment
helm repo add apache-airflow https://airflow.apache.org
helm repo update
helm install airflow apache-airflow/airflow \
--namespace airflow \
--create-namespace \
--set executor=KubernetesExecutor \
--set images.airflow.repository=ecr.amazonaws.com/harbinger/airflow \
--set images.airflow.tag=2.9.1 \
--set config.core.dags_folder=/opt/airflow/dags \
--values airflow-values.yaml
airflow-values.yaml:
executor: KubernetesExecutor
workers:
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "4Gi"
cpu: "2000m"
scheduler:
resources:
requests:
memory: "512Mi"
cpu: "250m"
webserver:
replicas: 2
logs:
persistence:
enabled: false # Remote-Logging nutzen
remote:
remoteLogging: true
remoteBaseLogFolder: s3://harbinger-airflow-logs/
config:
kubernetes:
namespace: airflow
worker_container_repository: ecr.amazonaws.com/harbinger/airflow
worker_container_tag: 2.9.1
delete_worker_pods: true
delete_worker_pods_on_failure: false # Fürs Debugging behalten
extraEnvFrom:
- secretRef:
name: airflow-connections
Task-Level-Resource-Overrides
from airflow import DAG
from airflow.providers.cncf.kubernetes.operators.kubernetes_pod import KubernetesPodOperator
from kubernetes.client import models as k8s
from datetime import datetime
with DAG(
"harbinger_geopolitical_etl",
start_date=datetime(2024, 1, 1),
schedule="@hourly",
catchup=False,
) as dag:
# Schwerer Spark-Job — mehr Resources
spark_transform = KubernetesPodOperator(
task_id="spark_transform",
name="spark-transform",
namespace="airflow",
image="ecr.amazonaws.com/harbinger/spark-pipeline:v1.4.2",
image_pull_policy="Always",
env_vars={
"INPUT_PATH": "s3://harbinger-raw/events/",
"OUTPUT_PATH": "s3://harbinger-processed/events/",
},
container_resources=k8s.V1ResourceRequirements(
requests={"memory": "4Gi", "cpu": "2"},
limits={"memory": "8Gi", "cpu": "4"},
),
node_selector={"node-type": "compute-optimised"},
tolerations=[
k8s.V1Toleration(key="dedicated", value="spark", effect="NoSchedule")
],
is_delete_operator_pod=True,
get_logs=True,
)
# Leichter API-Call — minimale Resources
api_ingest = KubernetesPodOperator(
task_id="api_ingest",
name="api-ingest",
namespace="airflow",
image="ecr.amazonaws.com/harbinger/ingest:v1.4.2",
container_resources=k8s.V1ResourceRequirements(
requests={"memory": "256Mi", "cpu": "100m"},
limits={"memory": "512Mi", "cpu": "500m"},
),
is_delete_operator_pod=True,
)
api_ingest >> spark_transform
Spark-Operator
Der Kubernetes Operator for Apache Spark managt den vollen Lifecycle von Spark-Applikationen als K8s-native Custom-Resources.
Install via Helm
helm repo add spark-operator https://kubeflow.github.io/spark-operator
helm install spark-operator spark-operator/spark-operator \
--namespace spark-operator \
--create-namespace \
--set webhook.enable=true \
--set metrics.enable=true \
--set metrics.port=10254
SparkApplication CRD
apiVersion: sparkoperator.k8s.io/v1beta2
kind: SparkApplication
metadata:
name: harbinger-event-enrichment
namespace: spark-jobs
spec:
type: Python
pythonVersion: "3"
mode: cluster
image: ecr.amazonaws.com/harbinger/spark-pipeline:v1.4.2
imagePullPolicy: Always
mainApplicationFile: local:///app/src/enrich_events.py
sparkVersion: "3.5.1"
restartPolicy:
type: OnFailure
onFailureRetries: 3
onFailureRetryInterval: 30
driver:
cores: 2
memory: "2g"
serviceAccount: spark
labels:
app: harbinger-spark
annotations:
iam.amazonaws.com/role: arn:aws:iam::123456789:role/SparkPipelineRole
executor:
cores: 4
instances: 10
memory: "8g"
memoryOverhead: "2g"
labels:
app: harbinger-spark
tolerations:
- key: dedicated
value: spark
effect: NoSchedule
sparkConf:
"spark.sql.extensions": "io.delta.sql.DeltaSparkSessionExtension"
"spark.sql.catalog.spark_catalog": "org.apache.spark.sql.delta.catalog.DeltaCatalog"
"spark.hadoop.fs.s3a.aws.credentials.provider": "com.amazonaws.auth.WebIdentityTokenCredentialsProvider"
"spark.dynamicAllocation.enabled": "true"
"spark.dynamicAllocation.minExecutors": "2"
"spark.dynamicAllocation.maxExecutors": "50"
"spark.dynamicAllocation.shuffleTracking.enabled": "true"
volumes:
- name: spark-local-dir
emptyDir: {}
Resource-Management und Autoscaling
Namespace-Resource-Quotas
Verhindern, dass ein Team alle Cluster-Resources frisst:
apiVersion: v1
kind: ResourceQuota
metadata:
name: spark-jobs-quota
namespace: spark-jobs
spec:
hard:
requests.cpu: "100"
requests.memory: 400Gi
limits.cpu: "200"
limits.memory: 800Gi
count/pods: "500"
count/sparkoperator.k8s.io/v1beta2/sparkapplications: "20"
Cluster-Autoscaler
Für Cloud-managed Cluster (EKS, GKE, AKS), Node-Auto-Provisioning konfigurieren:
# EKS-managed Node-Group für Spark-Worker (EU-Region!)
resource "aws_eks_node_group" "spark" {
cluster_name = aws_eks_cluster.main.name
node_group_name = "spark-workers"
node_role_arn = aws_iam_role.node.arn
subnet_ids = var.private_subnet_ids
scaling_config {
desired_size = 2
min_size = 0
max_size = 50
}
instance_types = ["r5.4xlarge"] # Memory-optimized für Spark
taint {
key = "dedicated"
value = "spark"
effect = "NO_SCHEDULE"
}
labels = {
"node-type" = "compute-optimised"
}
}
CI/CD-Pipeline für Data Pipelines
# .github/workflows/pipeline-deploy.yml
name: Build and Deploy Pipeline
on:
push:
branches: [main]
paths:
- 'pipelines/**'
- 'Dockerfile'
jobs:
build:
runs-on: ubuntu-latest
outputs:
image-tag: ${{ steps.meta.outputs.tags }}
steps:
- uses: actions/checkout@v4
- name: AWS-Credentials konfigurieren
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
aws-region: eu-central-1
- name: ECR-Login
uses: aws-actions/amazon-ecr-login@v2
- name: Build und Push
uses: docker/build-push-action@v5
with:
push: true
tags: |
${{ env.ECR_REGISTRY }}/harbinger/pipeline:${{ github.sha }}
${{ env.ECR_REGISTRY }}/harbinger/pipeline:latest
- name: Security-Scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.ECR_REGISTRY }}/harbinger/pipeline:${{ github.sha }}
severity: CRITICAL,HIGH
exit-code: 1
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Spark-Application updaten
run: |
kubectl set image sparkapplication/harbinger-event-enrichment \
spark-kubernetes-driver=${{ needs.build.outputs.image-tag }} \
-n spark-jobs
Observability
Key-Metrics für Data-Pipeline-Pods
# ServiceMonitor für Prometheus
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: spark-metrics
namespace: monitoring
spec:
selector:
matchLabels:
app: harbinger-spark
endpoints:
- port: metrics
interval: 15s
path: /metrics
namespaceSelector:
matchNames:
- spark-jobs
Grafana-Dashboard-Panels, die du brauchst:
- Pipeline-Throughput: Records pro Minute
- Pod-Restart-Count: Crash-Loops erkennen
- CPU-Throttling-Ratio: signalisiert unter-provisionierte Limits
- Pending-Pod-Duration: K8s-Scheduling-Latency
- Spark-Executor-Utilization: Over-/Under-Scaling erkennen
FAQ
Reicht Docker Compose statt K8s für kleine Setups? Für Single-Node-Setups bis ca. 10 parallele Pipelines: ja. Sobald du Auto-Scaling, Multi-Tenant-Isolation oder GPU-Workloads brauchst, wird K8s die saubere Wahl.
Was kostet ein EKS/GKE/AKS-Cluster pro Monat in EU? Control-Plane ca. 70-80 €/Monat bei allen drei Hyperscalern (Mai 2026). Compute kommt extra. Hetzner-Alternative: K3s auf 3-5 CCX-Servern, ca. 100-150 €/Monat — aber ohne Cloud-managed-IAM.
Wie handle ich Secrets DSGVO-konform? External Secrets Operator + Vault oder AWS Secrets Manager mit EU-Region. Niemals Secrets in Images oder K8s-Secrets unverschlüsselt.
Fazit
Container-native Data Pipelines mit K8s eliminieren ganze Problemklassen — Dependency-Konflikte, unvorhersagbare Resource- Usage, Environment-Drift. Das Initial-Invest in Dockerfiles, Helm- Charts und Spark-Operator-Konfiguration zahlt sich schnell zurück durch schnellere Deployments, bessere Resource-Utilization und dramatisch einfacheres Debugging.
Plattformen wie Harbinger Explorer laufen ihre komplette Data-Ingestion und Enrichment auf K8s — das erlaubt Scaling von null bis hunderte parallele Spark-Executors als Reaktion auf Event-Spikes, und Scale-Down auf nahe null über Nacht.
Test Harbinger Explorer 7 Tage kostenlos — powered by einer K8s-nativen Data-Plattform, die mit den Events der Welt skaliert.
Stand: 14. Mai 2026. K8s, Spark und Cloud-Provider entwickeln sich weiter — verifiziere kritische Annahmen mit deiner aktuellen Toolchain.
Geschrieben von
Harbinger Team
Cloud-, Data- und AI-Engineer in DACH. Schreibt seit 2018 über infrastrukturkritische Tech-Entscheidungen — keine Marketing- Folien, sondern echte Trade-offs aus Production-Workloads.
Hat dir das geholfen?
Jede Woche ein neuer Artikel über DACH-Cloud, Data und AI — direkt in dein Postfach. Kein Spam, kein Marketing-Sprech.
Kein Spam. 1-Klick-Abmeldung. Datenschutz bei Loops.so.