Extract, Transform, Load ohne Airflow-Overhead
Nicht jede Datenintegration braucht Airflow, Dagster oder ein verteiltes Orchestrierungssystem. Eine ETL Pipeline in Bash mit klar getrennten Extract-, Transform- und Load-Stufen, Checkpointing und idempotenten Läufen bewältigt einen Großteil der täglichen Datenflüsse zwischen Systemen zuverlässig und ohne zusätzliche Infrastruktur.
Inhaltsverzeichnis
- 1. Wann eine Bash-ETL-Pipeline die richtige Wahl ist
- 2. Extract: Daten aus Quellen zuverlässig abholen
- 3. Transform: Daten normalisieren und anreichern
- 4. Load: Zielsystem transaktionssicher befüllen
- 5. Checkpointing: nach einem Absturz genau dort weitermachen
- 6. Idempotenz: Pipelines, die man gefahrlos erneut starten kann
- 7. Orchestrierung mehrerer Stufen mit einem zentralen Runner
- 8. Beobachtbarkeit: wissen, wo die Pipeline gerade steht
- 9. Bash-ETL im Vergleich zu Airflow und Managed-Services
- 10. Zusammenfassung
- 11. FAQ
1. Wann eine Bash-ETL-Pipeline die richtige Wahl ist
Der Begriff ETL Pipeline weckt sofort Assoziationen zu Airflow, Dagster oder teuren Managed-Services. Für viele alltägliche Datenflüsse, ein nächtlicher Export aus einem Legacy-System, eine Synchronisation zwischen zwei Datenbanken, ein Abgleich zwischen einem ERP und einem Shop-System, ist dieser Werkzeugaufwand unverhältnismäßig. Eine ETL Pipeline in Bash mit drei klar getrennten Stufen, Extract, Transform und Load, deckt genau diesen Bereich ab, ohne einen Scheduler-Server, eine Web-UI oder eine eigene Python-Umgebung zu benötigen.
Der entscheidende Denkfehler bei kleinen Datenintegrationen ist, alles in ein einziges monolithisches Skript zu packen, das Daten abholt, transformiert und gleichzeitig lädt. Fällt die Zielverbindung mitten in der Verarbeitung aus, ist unklar, welche Daten bereits geladen wurden und welche nicht. Eine ETL Pipeline, die bewusst in drei separate, nacheinander ausführbare Skripte aufgeteilt ist, macht jede Stufe einzeln testbar, wiederholbar und beobachtbar.
Dieser Artikel zeigt, wie eine solche ETL Pipeline in Bash aufgebaut wird: von der sauberen Trennung der drei Stufen über Checkpointing und Idempotenz bis zur Orchestrierung mehrerer Pipelines mit einem zentralen Runner-Skript.
2. Extract: Daten aus Quellen zuverlässig abholen
Die Extract-Stufe einer ETL Pipeline holt Rohdaten aus der Quelle und schreibt sie unverändert in einen Zwischenspeicher, typischerweise ein Verzeichnis mit Zeitstempel im Dateinamen. Wichtig ist, diese Stufe von der Transformation zu trennen: Schlägt die Extraktion fehl, weil die Quelle nicht erreichbar ist, soll das nicht mit einem Transformationsfehler verwechselt werden. Beide Fehlerarten brauchen unterschiedliche Reaktionen, ein Netzwerkproblem rechtfertigt einen Retry, ein Datenformatfehler nicht.
Für Datenbankquellen exportiert mysqldump oder ein direktes SELECT ... INTO OUTFILE die Rohdaten, für APIs übernimmt curl mit Retry-Logik diese Aufgabe. Entscheidend für eine robuste ETL Pipeline ist, dass die Extract-Stufe niemals stillschweigend eine leere oder unvollständige Datei erzeugt, sondern bei jedem Fehler explizit abbricht, bevor nachgelagerte Stufen mit kaputten Daten weiterarbeiten.
#!/usr/bin/env bash
# 01-extract.sh — pull raw data from source, fail loudly on any problem
set -euo pipefail
readonly RUN_ID="$(date +%Y%m%d-%H%M%S)"
readonly STAGING_DIR="/var/etl/staging/${RUN_ID}"
readonly SOURCE_API="https://legacy-erp.internal/api/orders/export"
mkdir -p "$STAGING_DIR"
# Retry logic: 3 attempts, exponential backoff
attempt=1
max_attempts=3
until curl -sf --max-time 60 "$SOURCE_API" -o "${STAGING_DIR}/orders.raw.json"; do
if (( attempt >= max_attempts )); then
echo "[ERROR] Extract failed after $max_attempts attempts" >&2
rm -rf "$STAGING_DIR"
exit 1
fi
echo "[WARN] Attempt $attempt failed, retrying in $((attempt * 5))s..." >&2
sleep $(( attempt * 5 ))
attempt=$(( attempt + 1 ))
done
# Fail fast on an empty or truncated result — never pass bad data downstream
file_size="$(stat -c%s "${STAGING_DIR}/orders.raw.json")"
if (( file_size < 10 )); then
echo "[ERROR] Extracted file suspiciously small ($file_size bytes)" >&2
exit 1
fi
echo "$RUN_ID" > /var/etl/current_run_id
echo "[OK] Extracted $(jq 'length' "${STAGING_DIR}/orders.raw.json") records to $STAGING_DIR"
Der geschriebene current_run_id ist der Schlüssel, der alle drei Stufen der ETL Pipeline miteinander verbindet, ohne dass Extract, Transform und Load direkt voneinander wissen müssen. Jede Stufe liest diese ID und weiß, mit welchem Staging-Verzeichnis sie arbeitet.
3. Transform: Daten normalisieren und anreichern
Die Transform-Stufe einer ETL Pipeline ist der Ort, an dem die eigentliche fachliche Logik passiert: Feldnamen werden vereinheitlicht, Werte normalisiert, fehlende Daten mit Standardwerten aufgefüllt, und Datensätze werden gegebenenfalls mit zusätzlichen Informationen aus einer zweiten Quelle angereichert. Diese Stufe liest ausschließlich aus dem Staging-Verzeichnis der Extract-Stufe und schreibt in ein separates Transform-Verzeichnis, niemals direkt in das Zielsystem.
Diese strikte Trennung erlaubt, die Transform-Stufe beliebig oft erneut auszuführen, ohne die Quelle erneut abzufragen, was besonders bei API-Quellen mit Rate-Limits wertvoll ist. Für eine ETL Pipeline, die täglich läuft, bedeutet das: Findet man einen Fehler in der Transformationslogik, wird nur diese eine Stufe korrigiert und erneut mit den bereits vorhandenen Rohdaten ausgeführt, ohne die Extract-Stufe zu wiederholen.
#!/usr/bin/env bash
# 02-transform.sh — normalize and enrich extracted data
set -euo pipefail
readonly RUN_ID="$(cat /var/etl/current_run_id)"
readonly STAGING_DIR="/var/etl/staging/${RUN_ID}"
readonly TRANSFORM_DIR="/var/etl/transformed/${RUN_ID}"
mkdir -p "$TRANSFORM_DIR"
jq '
map({
order_id: .id,
customer_email: (.email | ascii_downcase),
total_amount: (.total | tonumber),
currency: (.currency // "EUR"),
status: (if .status == "" then "pending" else .status end),
processed_at: (now | todate)
})
' "${STAGING_DIR}/orders.raw.json" > "${TRANSFORM_DIR}/orders.normalized.json"
record_count="$(jq 'length' "${TRANSFORM_DIR}/orders.normalized.json")"
echo "[OK] Transformed $record_count records into $TRANSFORM_DIR"
Die Reihenfolge der Transformationen ist bewusst gewählt: Erst die E-Mail-Adresse auf Kleinschreibung normalisieren, dann Zahlen typisieren, dann fehlende Felder mit Fallback-Werten auffüllen. Diese Reihenfolge verhindert, dass eine spätere Regel auf einem noch nicht normalisierten Wert operiert, ein häufiger Fehler in wachsenden ETL Pipeline-Skripten, wenn Regeln unabhängig voneinander hinzugefügt werden.
4. Load: Zielsystem transaktionssicher befüllen
Die Load-Stufe einer ETL Pipeline überträgt die transformierten Daten ins Zielsystem, eine Datenbank, ein Data Warehouse oder eine API. Der kritischste Aspekt dieser Stufe ist, was bei einem Teilausfall passiert: Werden zweihundert von tausend Datensätzen geladen und dann bricht die Verbindung ab, muss klar definiert sein, ob die zweihundert bereits geladenen Datensätze im Zielsystem verbleiben oder zurückgerollt werden.
Für Datenbankziele ist eine explizite Transaktion die sicherste Lösung: Alle Inserts einer Charge laufen innerhalb eines BEGIN/COMMIT-Blocks, sodass bei einem Fehler ein automatisches ROLLBACK greift und das Zielsystem nie einen halbfertigen Zustand zeigt. Für API-Ziele, die keine Transaktionen unterstützen, übernimmt die im nächsten Abschnitt beschriebene Idempotenz diese Rolle, indem ein wiederholter Lauf bereits geladene Datensätze erkennt und überspringt.
#!/usr/bin/env bash
# 03-load.sh — load transformed data into the target database transactionally
set -euo pipefail
readonly RUN_ID="$(cat /var/etl/current_run_id)"
readonly TRANSFORM_DIR="/var/etl/transformed/${RUN_ID}"
readonly DB_NAME="warehouse"
# Build a single transactional SQL script from the JSON records
sql_file="$(mktemp)"
{
echo "START TRANSACTION;"
jq -r '.[] | "INSERT INTO orders (order_id, customer_email, total_amount, currency, status) VALUES (\(.order_id), \"\(.customer_email)\", \(.total_amount), \"\(.currency)\", \"\(.status)\") ON DUPLICATE KEY UPDATE status = VALUES(status);"' \
"${TRANSFORM_DIR}/orders.normalized.json"
echo "COMMIT;"
} > "$sql_file"
if mysql "$DB_NAME" < "$sql_file"; then
echo "[OK] Loaded records from run $RUN_ID"
rm -f "$sql_file"
else
echo "[ERROR] Load failed, transaction rolled back automatically" >&2
rm -f "$sql_file"
exit 1
fi
ON DUPLICATE KEY UPDATE ist hier bewusst gesetzt, damit ein erneuter Lauf derselben ETL Pipeline mit denselben Datensätzen keinen Fehler wegen doppelter Primärschlüssel erzeugt, sondern den bestehenden Datensatz aktualisiert. Das ist der Kern von Idempotenz, die im nächsten Abschnitt vertieft wird.
5. Checkpointing: nach einem Absturz genau dort weitermachen
Eine ETL Pipeline, die bei jedem Fehler komplett von vorne beginnt, verschwendet Zeit und Ressourcen, besonders wenn die Extract-Stufe langsam oder mit Rate-Limits belegt ist. Checkpointing bedeutet, den Fortschritt jeder Stufe in einer Statusdatei festzuhalten, sodass ein erneuter Lauf nach einem Absturz genau bei der zuletzt erfolgreich abgeschlossenen Stufe fortsetzt, statt von Extract erneut zu beginnen.
Die Implementierung ist einfach: Nach jeder erfolgreich abgeschlossenen Stufe schreibt das Skript einen Status-Marker in eine Datei, etwa extract:done, transform:done. Ein zentrales Runner-Skript (siehe Abschnitt sieben) prüft vor jeder Stufe, ob der entsprechende Marker bereits gesetzt ist, und überspringt die Stufe, wenn ja. Für eine ETL Pipeline, die aus Kostengründen oder wegen API-Limits nicht beliebig oft komplett neu laufen darf, ist dieses Muster unverzichtbar.
#!/usr/bin/env bash
set -euo pipefail
readonly RUN_ID="$1"
readonly CHECKPOINT_FILE="/var/etl/checkpoints/${RUN_ID}.state"
mkdir -p "$(dirname "$CHECKPOINT_FILE")"
touch "$CHECKPOINT_FILE"
is_stage_done() {
local stage="$1"
grep -qx "${stage}:done" "$CHECKPOINT_FILE" 2>/dev/null
}
mark_stage_done() {
local stage="$1"
echo "${stage}:done" >> "$CHECKPOINT_FILE"
}
run_stage() {
local stage="$1" script="$2"
if is_stage_done "$stage"; then
echo "[SKIP] Stage '$stage' already completed for run $RUN_ID"
return 0
fi
echo "[RUN] Stage '$stage' starting..."
if "$script" "$RUN_ID"; then
mark_stage_done "$stage"
echo "[OK] Stage '$stage' completed"
else
echo "[ERROR] Stage '$stage' failed, checkpoint not advanced" >&2
exit 1
fi
}
run_stage "extract" ./01-extract.sh
run_stage "transform" ./02-transform.sh
run_stage "load" ./03-load.sh
6. Idempotenz: Pipelines, die man gefahrlos erneut starten kann
Checkpointing verhindert unnötige Wiederholungen bereits abgeschlossener Stufen, schützt aber nicht vor Doppelverarbeitung, wenn eine Stufe teilweise durchgelaufen ist, bevor sie fehlschlug. Eine idempotente ETL Pipeline stellt sicher, dass ein erneuter Lauf mit denselben Eingabedaten zum selben Endzustand führt, unabhängig davon, wie oft sie ausgeführt wird. Das ist der entscheidende Unterschied zwischen einer Pipeline, die man beruhigt erneut anstoßen kann, und einer, bei der jeder Wiederholungslauf Duplikate erzeugt.
Für die Load-Stufe wird Idempotenz über ON DUPLICATE KEY UPDATE oder ein äquivalentes Upsert-Pattern erreicht, wie im vorigen Beispiel gezeigt. Für API-Ziele ohne native Upsert-Unterstützung übernimmt ein Idempotency-Key, meist eine deterministische ID aus den Quelldaten, dieselbe Rolle: Die Ziel-API erkennt anhand dieses Keys, ob eine Anfrage bereits verarbeitet wurde, und verarbeitet sie nicht ein zweites Mal.
#!/usr/bin/env bash
set -euo pipefail
# Generate a deterministic idempotency key from stable source fields
generate_idempotency_key() {
local order_id="$1" customer_email="$2"
echo -n "${order_id}:${customer_email}" | sha256sum | cut -d' ' -f1
}
while IFS=$'\t' read -r order_id email amount; do
idempotency_key="$(generate_idempotency_key "$order_id" "$email")"
curl -sf -X POST "https://api.target-system.com/v1/orders" \
-H "Idempotency-Key: ${idempotency_key}" \
-H "Content-Type: application/json" \
-d "{\"order_id\": \"${order_id}\", \"email\": \"${email}\", \"amount\": ${amount}}" \
|| echo "[WARN] Failed to submit order $order_id" >&2
done < orders.tsv
Der Idempotency-Key basiert bewusst nur auf stabilen Quellfeldern, nicht auf einem Zeitstempel oder einer zufälligen UUID, weil ein erneuter Lauf mit denselben Quelldaten sonst einen anderen Key erzeugen und die API zur erneuten Verarbeitung verleiten würde. Diese Detailtreue ist der Unterschied zwischen einer ETL Pipeline, die wirklich idempotent ist, und einer, die es nur oberflächlich zu sein scheint.
7. Orchestrierung mehrerer Stufen mit einem zentralen Runner
Sobald mehrere ETL Pipelines parallel oder in Abhängigkeit voneinander laufen sollen, etwa Kundenstammdaten vor Bestelldaten, lohnt sich ein zentrales Runner-Skript, das die Ausführungsreihenfolge und Abhängigkeiten explizit definiert. Dieses Runner-Skript ist selbst kein Framework, sondern eine einfache Bash-Funktion, die Pipeline-Namen mit ihren Abhängigkeiten verknüpft und bei einem Fehler die gesamte Kette stoppt, statt nachgelagerte Pipelines mit unvollständigen Daten laufen zu lassen.
Für die meisten Anwendungsfälle reicht eine simple Liste von Pipeline-Aufrufen in der richtigen Reihenfolge innerhalb eines einzigen Cron-Skripts. Erst wenn echte Parallelität mit unterschiedlichen Startzeitpunkten pro Pipeline benötigt wird, lohnt sich der Umstieg auf ein dediziertes Orchestrierungswerkzeug, das über den Rahmen einer Bash-ETL Pipeline hinausgeht.
8. Beobachtbarkeit: wissen, wo die Pipeline gerade steht
Eine ETL Pipeline, die nachts läuft, muss am Morgen ohne manuelles Nachschauen in Log-Dateien erkennbar machen, ob sie erfolgreich war. Jede Stufe sollte am Ende einen strukturierten Statuseintrag schreiben, mindestens Zeitstempel, Stufenname, Anzahl verarbeiteter Datensätze und Erfolg oder Fehlschlag. Diese Statuseinträge lassen sich, wie im ersten Artikel dieser Reihe beschrieben, direkt zu einem HTML-Report zusammenfassen, der nach jedem Lauf automatisch verschickt wird.
Für eine ETL Pipeline im Dauerbetrieb ist besonders die Verarbeitungsdauer pro Stufe eine wertvolle Kennzahl, weil eine schleichend wachsende Laufzeit oft ein Frühindikator für ein wachsendes Datenvolumen oder ein Performance-Problem in der Zielverbindung ist, lange bevor die Pipeline tatsächlich einen Timeout erreicht.
9. Bash-ETL im Vergleich zu Airflow und Managed-Services
Die Entscheidung zwischen einer Bash-ETL Pipeline und einem vollwertigen Orchestrierungswerkzeug hängt von Komplexität, Anzahl der Pipelines und Team-Größe ab. Für ein bis eine Handvoll Pipelines mit einfacher, linearer Abhängigkeit ist Bash meist die pragmatischere Wahl.
| Kriterium | Bash-ETL-Pipeline | Airflow | Managed-ETL-Service |
|---|---|---|---|
| Setup-Aufwand | Minuten | Tage | Stunden, aber Vendor-Lock-in |
| Anzahl Pipelines | 1 bis 10 | 10 bis hunderte | Beliebig |
| Web-UI, Visualisierung | Nicht vorgesehen | Ja | Ja |
| Laufende Kosten | Keine zusätzlichen | Server/Infrastruktur | Nutzungsbasiert, oft teuer |
| Team-Einarbeitung | Minimal, jeder kennt Bash | DAG-Konzepte lernen | Vendor-spezifisch |
Für kleine bis mittlere Datenintegrationen mit klarer, linearer Abhängigkeit liefert eine Bash-ETL Pipeline denselben fachlichen Nutzen wie ein schwergewichtiges Framework, ohne dessen Betriebsaufwand. Erst bei zweistelliger Pipeline-Anzahl mit komplexen Abhängigkeiten und dem Bedarf nach einer Web-UI für nicht-technische Nutzer überwiegt der Vorteil eines dedizierten Orchestrierungswerkzeugs.
Mironsoft
Shell-Automatisierung, Datenintegration und Deployment-Infrastruktur
Datenflüsse zwischen euren Systemen, robust und nachvollziehbar?
Wir bauen ETL-Pipelines in Bash mit Checkpointing, Idempotenz und klarer Fehlerbehandlung für eure Datenintegrationen, ohne Airflow-Overhead und ohne zusätzliche Server-Infrastruktur.
Pipeline-Architektur
Saubere Trennung von Extract, Transform und Load mit Checkpointing
Idempotenz
Gefahrlos wiederholbare Läufe ohne Duplikate im Zielsystem
Beobachtbarkeit
Statusreports und Kennzahlen pro Stufe für schnelle Fehlerdiagnose
10. Zusammenfassung
Eine solide ETL Pipeline in Bash trennt Extract, Transform und Load bewusst in drei eigenständige, nacheinander ausführbare Skripte, die jeweils nur aus dem Zwischenspeicher der vorigen Stufe lesen. Diese Trennung macht jede Stufe einzeln testbar und wiederholbar, ohne bei einem Fehler in der Transformationslogik die gesamte Extraktion erneut ausführen zu müssen. Checkpointing über Statusdateien verhindert unnötige Wiederholungen bereits erfolgreich abgeschlossener Stufen nach einem Absturz.
Idempotenz, umgesetzt über Upsert-Patterns in der Datenbank oder Idempotency-Keys bei API-Zielen, macht eine ETL Pipeline gefahrlos wiederholbar, ohne Duplikate zu erzeugen. Ein zentrales Runner-Skript orchestriert mehrere Pipelines mit klaren Abhängigkeiten, und strukturierte Statuseinträge pro Stufe machen den Erfolg oder Fehlschlag jedes Laufs auf einen Blick erkennbar. Für kleine bis mittlere Datenintegrationen liefert dieser Ansatz denselben fachlichen Nutzen wie Airflow, ohne dessen Betriebsaufwand.
ETL-Pipelines in Bash: Das Wichtigste auf einen Blick
Stufentrennung
Extract, Transform und Load als eigenständige Skripte, jede Stufe liest nur aus dem Zwischenspeicher der vorigen.
Checkpointing
Statusdateien markieren abgeschlossene Stufen, ein Neustart überspringt bereits erledigte Arbeit.
Idempotenz
ON DUPLICATE KEY UPDATE oder Idempotency-Keys verhindern Duplikate bei wiederholten Läufen.
Beobachtbarkeit
Strukturierte Statuseinträge pro Stufe machen Erfolg oder Fehlschlag ohne manuelles Log-Wälzen sichtbar.