AWS Lambda Custom Runtime in Bash schreiben: Runtime API und Handler-Loop
AI generated
$_
#!/
Bash · AWS Lambda · Serverless · Runtime API
AWS Lambda Custom Runtime in Bash
Runtime API, bootstrap-Datei und Handler-Loop für eine minimale Bash-Runtime

AWS Lambda erzwingt keine bestimmte Programmiersprache. Über die Runtime API lässt sich jede ausführbare Datei, auch ein reines Bash-Skript, als vollwertige Lambda-Runtime betreiben. Wer versteht, wie die bootstrap-Datei Events abholt, den eigentlichen Handler aufruft und das Ergebnis zurückmeldet, kann kleine Glue-Skripte ohne Interpreter-Overhead direkt in Lambda ausführen.

18 Min. Lesezeit Runtime API · bootstrap · curl AWS Lambda · Serverless

1. Was eine Custom Runtime ist und warum Bash dafür infrage kommt

AWS Lambda liefert offiziell unterstützte Runtimes für Node.js, Python, Java, Go, Ruby und .NET, öffnet die Ausführungsumgebung aber auch für Custom Runtimes. Eine Custom Runtime ist letztlich nur eine ausführbare Datei namens bootstrap, die Lambda beim Start eines Containers aufruft und die für den gesamten Lebenszyklus des Ausführungscontexts verantwortlich ist, inklusive Event-Abholung und Fehlerbehandlung.

Weil bootstrap lediglich ausführbar sein muss, qualifiziert sich auch ein Bash-Skript mit passender Shebang-Zeile als vollwertige Runtime, sofern das Ausführungsimage die nötigen Grundwerkzeuge wie curl und jq mitbringt. Für einfache Glue-Aufgaben, die ohnehin nur Shell-Befehle orchestrieren, entfällt damit der Umweg über einen Python- oder Node-Interpreter komplett, und der Cold Start bleibt minimal, weil kein Sprachlaufzeit-Overhead geladen werden muss.

2. Runtime API Grundlagen: /next, /response und /error

Die Lambda Runtime API ist ein einfacher HTTP-Server, der lokal im Ausführungscontext unter der Umgebungsvariable AWS_LAMBDA_RUNTIME_API erreichbar ist. Eine Runtime ruft GET /2018-06-01/runtime/invocation/next auf, um auf das nächste Event zu warten, wobei der Aufruf so lange blockiert, bis tatsächlich ein Event eintrifft, was sich in Bash direkt mit einem blockierenden curl-Aufruf abbilden lässt.

Nach der Verarbeitung meldet die Runtime das Ergebnis über POST /2018-06-01/runtime/invocation/{requestId}/response zurück, oder im Fehlerfall über den entsprechenden /error-Endpunkt. Die Request-ID steckt in einem Response-Header des /next-Aufrufs und muss von der Runtime selbst aus den Headern extrahiert werden, was in Bash typischerweise über curl -D und anschließendes Parsen mit grep oder sed passiert.

3. Minimaler Runtime-Loop in Bash schreiben

Der Kern der Custom Runtime ist eine Endlosschleife, die pro Durchlauf genau ein Event abholt, verarbeitet und das Ergebnis zurückmeldet. Diese Schleife läuft für die gesamte Lebensdauer des Lambda-Ausführungscontexts, sodass mehrere aufeinanderfolgende Invocations denselben Container und damit auch denselben Bash-Prozess wiederverwenden, was Cold Starts nach der ersten Ausführung vermeidet.

Wichtig ist, jeden Fehler innerhalb der Schleife abzufangen, statt die gesamte Runtime bei einem einzelnen fehlgeschlagenen Event abstürzen zu lassen. Ein set -e über die komplette Schleife wäre hier kontraproduktiv, weil ein einzelner Fehler dann den kompletten Container beendet, statt nur diese eine Invocation als fehlgeschlagen zu melden und mit der nächsten weiterzumachen.


#!/usr/bin/env bash
set -uo pipefail

readonly RUNTIME_API="$AWS_LAMBDA_RUNTIME_API"
readonly BASE_URL="http://${RUNTIME_API}/2018-06-01/runtime"

while true; do
  # Naechstes Event abholen, Request-ID aus dem Header extrahieren
  headers_file=$(mktemp)
  event=$(curl -sS -D "$headers_file" "${BASE_URL}/invocation/next")
  request_id=$(grep -Fi "Lambda-Runtime-Aws-Request-Id" "$headers_file" | tr -d '\r' | cut -d' ' -f2)
  rm -f "$headers_file"

  # Handler ausfuehren, Fehler abfangen statt Runtime abzubrechen
  if response=$(./handler.sh "$event" 2>&1); then
    curl -sS -X POST "${BASE_URL}/invocation/${request_id}/response" -d "$response" > /dev/null
  else
    curl -sS -X POST "${BASE_URL}/invocation/${request_id}/error" \
      -d "{\"errorMessage\": \"handler failed\", \"errorType\": \"HandlerError\"}" > /dev/null
  fi
done

4. Handler-Skript und bootstrap-Datei trennen

Es empfiehlt sich, die eigentliche Geschäftslogik in ein separates Handler-Skript auszulagern, während bootstrap ausschließlich für die Runtime-API-Kommunikation zuständig bleibt. Das hält beide Dateien überschaubar und erlaubt, den Handler unabhängig von der Runtime-Loop-Logik lokal zu testen, indem er einfach mit einem Beispiel-JSON als Argument aufgerufen wird.

Der Handler bekommt das rohe Event-JSON als Argument oder über Standard-Input und muss selbst mit jq die benötigten Felder extrahieren, da Bash kein natives JSON-Parsing besitzt. Die Rückgabe erfolgt über Standard-Output als gültiges JSON, das die bootstrap-Schleife unverändert an den /response-Endpunkt weiterreicht.


#!/usr/bin/env bash
# handler.sh -- eigentliche Geschaeftslogik, unabhaengig testbar
set -euo pipefail

event="$1"
name=$(echo "$event" | jq -r '.name // "World"')

echo "{\"message\": \"Hello, ${name}!\"}"

5. Paket bauen und als Lambda-Funktion deployen

Eine Custom Runtime wird als ganz normales Deployment-Paket hochgeladen, das mindestens die ausführbare bootstrap-Datei und das Handler-Skript enthält. Beide Dateien brauchen Ausführungsrechte, bevor sie ins ZIP-Archiv gepackt werden, weil Lambda die Berechtigungen aus dem Archiv übernimmt und ein fehlendes Ausführungsbit erst beim ersten Aufruf mit einer wenig aussagekräftigen Fehlermeldung auffällt.

Beim Erstellen der Funktion über die AWS CLI wird die Laufzeit als provided.al2 oder provided.al2023 angegeben, das minimale Amazon-Linux-Basisimage ohne vorinstallierte Sprachlaufzeit. Enthält dieses Basisimage nicht alle benötigten Werkzeuge wie jq, lassen sie sich über einen zusätzlichen Lambda Layer nachrüsten, statt sie in jedes einzelne Funktionspaket zu kopieren.


#!/usr/bin/env bash
set -euo pipefail

chmod +x bootstrap handler.sh
zip -j function.zip bootstrap handler.sh

aws lambda create-function \
  --function-name mironsoft-bash-glue \
  --runtime provided.al2023 \
  --handler bootstrap \
  --zip-file fileb://function.zip \
  --role arn:aws:iam::123456789012:role/lambda-bash-runtime-role \
  --timeout 10 \
  --memory-size 128

6. Fehlerbehandlung und Timeouts im Runtime-Loop

Weil die bootstrap-Schleife für die gesamte Ausführungsdauer eines Containers verantwortlich ist, muss sie zwischen einem Fehler in der eigentlichen Geschäftslogik und einem Fehler in der Runtime-Kommunikation selbst unterscheiden. Ein fehlgeschlagener Handler-Aufruf wird sauber über den /error-Endpunkt gemeldet und die Schleife läuft weiter, während ein Fehler beim curl-Aufruf gegen die Runtime API selbst meist auf ein tieferliegendes Problem hindeutet und den Container beenden sollte.

Lambda erzwingt sein eigenes Timeout unabhängig von der Runtime, killt also den Container, wenn der konfigurierte Timeout-Wert überschritten wird, egal was die bootstrap-Schleife gerade tut. Innerhalb der Runtime lohnt sich trotzdem ein eigenes, etwas kürzeres Timeout für den Handler-Aufruf, damit eine sauber formulierte Fehlermeldung an /error geht, statt dass Lambda die Ausführung mit einer generischen Timeout-Meldung abwürgt.

7. Performance: Cold Start und Interpreter-Overhead im Vergleich

Ein Bash-Lambda startet ohne den Overhead, eine Python- oder Node-Laufzeit zu initialisieren, weil die Shell selbst als Prozess bereits Teil des minimalen Basisimages ist. Bei sehr einfachen Aufgaben, die überwiegend aus dem Aufruf externer Programme bestehen, etwa aws s3 cp oder curl, macht sich das in messbar kürzeren Cold-Start-Zeiten bemerkbar, weil kein zusätzlicher Interpreter geladen werden muss.

Sobald jedoch komplexere Logik ins Spiel kommt, etwa verschachteltes JSON-Parsing, Fehlerobjekte mit Stacktraces oder Zustandsverwaltung über mehrere Invocations hinweg, kehrt sich der Vorteil um: Bash-Skripte mit vielen externen Prozessaufrufen wie jq, awk und curl erzeugen pro Aufruf einen neuen Subprozess, was bei rechenintensiver Logik langsamer ist als dieselbe Logik in einer nativen Python-Runtime mit In-Process-Bibliotheken.

8. Wann ein Bash-Lambda sinnvoll ist: Glue-Skripte und einfache Automationen

Ein Bash-Lambda eignet sich hervorragend für kleine Glue-Skripte, die als Reaktion auf ein S3-Event oder einen EventBridge-Trigger ein paar CLI-Befehle ausführen, etwa eine Datei zwischen zwei S3-Buckets kopieren, ein Backup-Kommando anstoßen oder ein einfaches Health-Check-Skript regelmäßig ausführen. Solche Aufgaben bestehen ohnehin fast nur aus Shell-Befehlen, sodass eine native Sprach-Runtime kaum echten Mehrwert bringt, aber zusätzlichen Cold-Start-Overhead verursacht.

Auch als Wegwerf-Werkzeug für einmalige Migrationen oder für Teams, die bereits umfangreiche Bash-Tooling-Bibliotheken für ihre Deployment-Pipelines pflegen, ist die Custom Runtime attraktiv, weil sich vorhandene Skripte fast unverändert in Lambda weiterverwenden lassen, statt sie in eine andere Sprache zu portieren, nur um sie serverless auszuführen.

9. Wann ein Bash-Lambda nicht sinnvoll ist

Sobald eine Funktion komplexe Datenstrukturen verarbeitet, umfangreiche Fehlerbehandlung mit typisierten Exceptions braucht oder auf ein reichhaltiges SDK für andere AWS-Dienste angewiesen ist, wird Bash schnell zur Last. Fehlendes natives JSON-Handling, keine echten Datenstrukturen jenseits von Arrays und die Notwendigkeit, praktisch jede Logik über externe Prozesse wie jq zu delegieren, machen den Code fragil und schwer testbar, sobald er über wenige Dutzend Zeilen hinauswächst.

Für produktionskritische Geschäftslogik, Teams ohne tiefe Bash-Erfahrung oder Funktionen, die von vielen Entwicklern gemeinsam gepflegt werden, ist eine offizielle Runtime wie Python oder Node fast immer die bessere Wahl, weil Tooling, Testframeworks und die Fehlermeldungen der AWS-SDKs dort erheblich ausgereifter sind als das, was sich in Bash mit curl und jq nachbauen lässt.

Kriterium Bash Custom Runtime Native Runtime (Python/Node) Empfehlung
Cold Start Minimal, kein Interpreter-Start Interpreter- und SDK-Initialisierung Bash für simple Glue-Skripte
JSON-Verarbeitung Nur über externes jq Nativ eingebaut Native Runtime bei viel JSON-Logik
Fehlerbehandlung Manuell über /error-Endpunkt Try/Except mit Stacktraces Native Runtime bei kritischer Logik
AWS-SDK-Zugriff Nur über AWS CLI-Aufrufe Voll ausgestattetes SDK (boto3 etc.) Native Runtime bei vielen AWS-Diensten
Wartbarkeit im Team Gering ohne Bash-Erfahrung Hoch, breit bekannte Sprache Native Runtime im Team-Kontext

Mironsoft

Shell-Automatisierung, DevOps-Tooling und Deployment-Infrastruktur

Shell-Skripte, die in der Produktion zuverlässig laufen?

Wir analysieren bestehende Bash-Skripte, erkennen fragile Muster und ersetzen sie durch robuste Bash-Patterns: mit vollständiger Fehlerbehandlung, Logging und sicherer Parallelisierung für euren Deployment-Stack.

Code-Review

ShellCheck-Analyse und manuelle Prüfung auf kritische Bash-Pattern-Verstöße.

Refactoring

Fehlerbehandlung, Logging und sichere Dateioperationen nachrüsten.

CI-Integration

ShellCheck und BATS in Pipelines integrieren und Regressionstests aufbauen.

10. Zusammenfassung

AWS Lambda Custom Runtime in Bash: Das Wichtigste auf einen Blick

Grundprinzip

Eine Custom Runtime ist nur eine ausführbare bootstrap-Datei, die per Runtime API Events abholt und Ergebnisse zurückmeldet.

Kernschleife

while true mit curl gegen /invocation/next, Handler-Aufruf, Ergebnis an /response oder Fehler an /error.

Deployment

bootstrap und Handler ausführbar machen, als ZIP hochladen, Laufzeit provided.al2023 wählen.

Einsatzgrenze

Sinnvoll für Glue-Skripte ohne komplexe Logik, ungeeignet für datenintensive, teamgetragene Geschäftslogik.

11. FAQ: AWS Lambda Custom Runtime in Bash: Das Wichtigste auf einen Blick

1Was ist eine AWS Lambda Custom Runtime?
Eine ausführbare Datei namens bootstrap, die Lambda beim Containerstart aufruft und die selbst für Event-Abholung, Handler-Aufruf und Ergebnisrückmeldung über die Runtime API verantwortlich ist.
2Warum eignet sich Bash überhaupt für eine Lambda-Runtime?
Weil bootstrap nur ausführbar sein muss. Ein Bash-Skript mit Shebang-Zeile erfüllt das, solange curl und jq im Ausführungsimage verfügbar sind.
3Wie hole ich das nächste Event ab?
Mit einem blockierenden GET-Aufruf gegen /2018-06-01/runtime/invocation/next über die in AWS_LAMBDA_RUNTIME_API hinterlegte Adresse. Der Aufruf wartet, bis ein Event eintrifft.
4Wie melde ich das Ergebnis zurück?
Per POST an /2018-06-01/runtime/invocation/{requestId}/response mit dem JSON-Ergebnis als Body, oder bei einem Fehler an den entsprechenden /error-Endpunkt.
5Wie parse ich JSON in Bash für den Handler?
Mit jq, da Bash kein natives JSON-Parsing besitzt. jq -r '.feldname' extrahiert einzelne Felder als Text aus dem Event-JSON.
6Welche Laufzeit gebe ich beim Deployment an?
provided.al2 oder provided.al2023, das minimale Amazon-Linux-Basisimage ohne vorinstallierte Sprachlaufzeit, passend für Custom Runtimes.
7Was passiert, wenn der Handler fehlschlägt?
Die bootstrap-Schleife fängt den Fehler ab, meldet ihn über den /error-Endpunkt und läuft für die nächste Invocation weiter, statt den ganzen Container zu beenden.
8Ist eine Bash-Lambda schneller beim Cold Start?
Für einfache Aufgaben ja, weil kein Interpreter geladen werden muss. Bei komplexer Logik mit vielen externen Prozessaufrufen kann der Vorteil aber wieder verschwinden.
9Fehlen Werkzeuge wie jq im Basisimage?
Dann lassen sie sich über einen zusätzlichen Lambda Layer nachrüsten, statt sie in jedes einzelne Funktionspaket zu kopieren.
10Wann sollte ich lieber Python oder Node statt Bash nehmen?
Bei komplexen Datenstrukturen, umfangreicher Fehlerbehandlung, intensiver Nutzung von AWS-SDKs oder wenn die Funktion von einem größeren Team gemeinsam gepflegt wird.