Einfache ETL-Pipelines in Bash bauen
AI generated
$_
#!/
Bash · ETL · Datenintegration · Automatisierung
Einfache ETL-Pipelines in Bash bauen
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.

18 Min. Lesezeit Extract · Transform · Load · Checkpointing · Idempotenz Bash 4.x · 5.x · Linux

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.

11. FAQ: ETL-Pipelines in Bash

1Wann reicht Bash statt Airflow?
Für wenige Pipelines mit einfacher, linearer Abhängigkeit ohne Bedarf an einer Web-UI.
2Warum ETL in getrennte Skripte aufteilen?
Klare Fehlerzuordnung, jede Stufe unabhängig test- und wiederholbar.
3Was ist Checkpointing?
Fortschritt in einer Statusdatei festhalten, Neustart überspringt bereits erledigte Stufen.
4Was bedeutet Idempotenz?
Wiederholter Lauf mit gleichen Daten führt zum selben Endzustand, keine Duplikate.
5Doppelte Datensätze verhindern?
ON DUPLICATE KEY UPDATE oder Idempotency-Key aus stabilen Quellfeldern.
6Warum Transaktion in Load-Stufe?
Automatisches Rollback bei Fehlern, Zielsystem zeigt nie einen halbfertigen Zustand.
7Erfolgreiche Extraktion erkennen?
Explizite Prüfung von Dateigröße und Exit-Code, nie nur die Existenz der Ausgabedatei.
8Mehrere Pipelines mit Abhängigkeiten orchestrieren?
Zentrales Runner-Skript mit expliziter Reihenfolge, stoppt bei Fehlern die gesamte Kette.
9Welche Kennzahl zeigt Probleme früh?
Verarbeitungsdauer pro Stufe, wachsende Laufzeit ist ein Frühindikator.
10Wann Umstieg auf Airflow?
Bei zweistelliger Pipeline-Anzahl, komplexen Abhängigkeiten oder Bedarf an einer Web-UI.