Deep Dive AWS Lambda – Teil II

Willkommen zum zweiten Teil dieser Miniserie, die sich darauf konzentriert, wie man Lambda-Funktionen entwirft und wie man seinen Code optimiert. Dieser Artikel baut auf Konzepten auf, die bereits in vorangegangenen Blog-Beiträgen erläutert wurden. Werfen Sie unbedingt einen Blick auf unsere früheren Blog-Posts „Einführung in Serverless Computing mit AWS Lambda“ und „Deep Dive AWS Lambda – Teil I„, um einen soliden Überblick über das Thema zu erhalten.

Die Handler-Methode

Beim Erstellen einer Lambda-Funktion geben Sie eine Handler-Methode an. Diese Methode wird standardmäßig generiert und ist der Einstiegspunkt für eine von der Funktion bediente Anfrage. Als Parameter erhält die Funktion zwei Objekte:

  • event
  • context

Das event-Objekt enthält Einzelheiten über das Ereignis, welches die Lambda-Funktion aufgerufen hat. Beispielsweise könnte die Funktion Daten einer http-POST-Anforderung empfangen. Normalerweise ist der Typ des event-Objekts ein Dictionary (in Python). Der Objekttyp kann jedoch variieren (list, str, int, float oder None). Inhalt und Objekttyp des Objekts werden durch die Art des Aufrufs bestimmt.

def handler_name(event, context): 
    ...
    return some_value

Das context-Objekt liefert Informationen über die Lambda-Funktion. Möglicherweise müssen Sie wissen, wie viel Zeit noch verbleibt, bis die Funktion durch den Timeout beendet wird, oder wie viel Speicher noch bis zum Erreichen des Limits zur Verfügung steht. Diese Art von Informationen und mehr können aus dem context-Objekt abgerufen werden. Eine vollständige Liste finden Sie in der offiziellen Dokumentation [1].

Einige Lambda-Funktionen enthalten einen Rückgabewert. Dies hängt stark vom Ausführungsmodell ab, welches im ersten Teil des Deep Dive diskutiert wurde. Wenn die Funktion synchron aufgerufen wird, erwartet der aufrufende Service eine Antwort auf seine Anfrage an AWS Lambda. Die Funktion muss also einen Wert an den Dienst zurückgeben, andernfalls wird der weitere Ablauf der gesamten Anwendung blockiert. Asynchrone Aufrufe hingegen warten nicht auf eine Antwort. Daher ist eine Rückgabeanweisung obsolet.

Best Practice: Design

Viele Themen in diesem Abschnitt stehen in engem Zusammenhang mit dem Design von Microservice-Architekturen. Dies ist keine Überraschung, da AWS Lambda selbst Teil einer Microserviceumgebung und gleichzeitig ein integraler Bestandteil vieler Microservicearchitekturen ist.

Die erste wichtige Regel ist die Trennung von Geschäfts- und Lambda-Logik. Die Handler-Methode gehört zur letzteren. Sie sollte nur verwendet werden, um Informationen aus dem event-Objekt zu extrahieren oder, falls erforderlich, Informationen aus dem context-Objekt zu erhalten. Weitere Methoden innerhalb der Lambda-Funktion sollten dann die extrahierten Informationen bearbeiten. Sobald die Verarbeitung abgeschlossen ist, wird möglicherweise eine Antwort an die Handler-Methode zurückgegeben. Diese wird anschließend an den aufrufenden Dienst zurückgegeben. Dieses Prinzip ermöglicht es Entwicklern, Unit- und Integrationstests nur auf die Geschäftslogik zu beziehen.

Eine weitere wichtige Regel bezieht sich auf die Architektur des Codes. Funktionen sollten nur einem Zweck dienen. Schreiben Sie keine Funktionen, die mehr als eine Sache tun. Dies hilft anderen Entwicklern, Ihren Code zu verstehen und zu pflegen.

Lambda-Funktionen müssen zudem zustandslos sein, und kein Zustand sollte im Kontext der Lambda-Funktion selbst gespeichert werden. Der Code existiert nur dann, wenn die Funktion von einem Event getriggert wird. Wenn etwas persistiert werden muss, sollten Sie S3 oder DynamoDB verwenden. Beide Dienste skalieren horizontal und sind einfach zu nutzen. Als Faustregel gilt: Verwenden Sie DynamoDB, wenn Sie eine Latenzzeit im Millisekundenbereich benötigen und sich Ihre Daten schnell ändern. Wenn der Durchsatz nicht wichtig ist und sich die Daten nicht stark ändern, verwenden Sie S3.

Schließlich sollten Sie nur die Dependencies einbeziehen, die Sie benötigen. Das ist leichter gesagt als getan, hat aber erhebliche Auswirkungen auf Deployment und die Perfomance in der Produktion. Das Einbinden ganzer SDKs in Ihren Code kommt einem größeren Deployment-Paket und damit längeren Deployment-Prozessen gleich. Außerdem benötigt die Runtime der Funktion mehr Zeit, um verfügbar zu sein (siehe auch Kalt- und Warmstart im Einführungsartikel). Um Abhängigkeiten in Ihrem Code zu kontrollieren, ziehen Sie einen weiteren unserer Blog-Artikel in Betracht: Dependency Management für AWS Lambda

Best Practice: Code

Die AWS Entwickler-Dokumentation bietet Abschnitte mit dem Titel „Arbeiten mit …“. Dieser Abschnitt gibt spezifische Tipps für die Arbeit mit einer unterstützten Programmiersprache, enthält aber die gleichen sechs Themen für jede Programmiersprache:

  • Handler
  • Deployment package
  • Context
  • Logging
  • Errors
  • Tracing

Der folgende Teil befasst sich mit „Logging, Errors und Tracing“, „Umgebungsvariablen“ und „Rekursiver Code“. Wenn Sie mehr über die anderen Themen erfahren möchten, sollten Sie unbedingt unsere vorangegangenen Artikel lesen.

Logging, Errors und Tracing

Logging ist für Entwickler und Mitglieder von Betriebsteams gleichmaßen wichtig. Es hilft bei der Entwicklung von Code und auch bei der Fehlersuche. Wenn ein Entwickler eine Lambda-Funktion ausführt, kann er Ausgabeanweisungen in den Code einfügen, die automatisch vom Dienst protokolliert werden.

Für Python können Sie print()Statements einfügen. Je jedoch größer das Projekt, desto mehr Module werden entwickelt, weshalb ein strukturierter Loggingansatz nötig ist. Außerdem geht das Projekt schließlich von der Entwicklung, dem Debuggen und Testen in den Produktionsstatus über. Für diese Zustände sollten möglicherweise verschiedene Logging-Ebenen verwendet werden.

Für die strukturiertes Logging in Python kann natürlich das logging-Modul (Teil der Standard-Library) verwendet werden. Diese bietet folgende Features:

  • Kontrolle des Logging-Levels
  • Festlegen von Speicherorten für Log-Dateien
  • Vordefinierte Vorlage für Log-Level
  • Quellenangabe in Logeinträge integriert

Ein noch besseres Werkzeug (nur Python) sind die „AWS Lambda Powertools „, welche auf GitHub [2] gehostet werden. Diese Erweiterung ermöglicht die Protokollierung von Informationen (ähnliche wie das logging-Modul), enthält aber auch die Bibliotheken für AWS X-Ray. AWS X-Ray ist ein Tracing-Dienst, mit dem API-Aufrufe zwischen verschiedenen Diensten verfolgt werden und Engpässe im Workflow gefunden werden können.

Umgebungsvariablen

Umgebungsvariablen ermöglichen es, das Verhalten der Funktion zu ändern, ohne Code zu aktualisieren. Standardmäßig sind die Variablen im Ruhezustand verschlüsselt, daher sensible Parameter in den Umgebungsvariablen statt im Code gespeichert werden. Es ist möglich, einen anderen als den Standardschlüssel zu verwenden, und es ist ebenfalls möglich, die Variablen auf einem Client zu verschlüsseln, bevor sie in AWS Lambda eingegeben werden. 

Es gelten jedoch Einschränkungen bei den Umgebungsvariablen (neben den Anforderungen an die Benennung):

  • Schlüssel, die nicht von Lambda reserviert sind
  • Die Gesamtgröße aller Umgebungsvariablen darf 4 KB nicht überschreiten

Ein Anwendungsfall für Umgebungsvariablen sind Softwaretests. Angenommen, Sie möchten eine Funktion testen, die eine Verbindung zu einer Produktionsdatenbank herstellt. Für den Test möchten Sie eine Verbindung mit der Testdatenbank herstellen. Die Aktualisierung des Funktionscodes bei jedem Test ist fehleranfällig. Vergisst man die erneute Änderung des Parameters, bevor die Funktion wieder Produktionsevents verarbeitet, verlieren Sie möglicherweise Daten. Sicherer ist es, zwei Lambda-Funktionen mit identischem Code, aber unterschiedlichen Werten für die Datenbankverbindung einzusetzen.

Die oben beschriebenen Einschränkungen haben gezeigt, dass es bestimmte Umgebungsvariablen gibt, die von Lambda reserviert werden. Diese enthalten Informationen über die Laufzeit und die Funktion selbst. Für weitere Informationen besuchen Sie bitte die Entwicklerdokumentation.

Rekursiver Code

Der kürzeste und wahrscheinlich wichtigste Ratschlag: Vermeiden Sie die Verwendung von rekursivem Code. Dies kann zu Verzögerungen im Workflow führen, da sich die Ausführungszeit einer Funktion aufgrund von rekursivem Code in der Funktion erhöht. Dies ist besonders gefährlich, wenn die Funktion synchron aufgerufen wird. Ein weiteres Beispiel für rekursiven Code ist, wenn Ihre Funktion sich selbst mittels Software Development Kit (SDK) aufruft. Hierdurch erhöht sich die Anzahl der gleichzeitigen Ausführungen sehr schnell.

Wenn rekursiver Code nicht vermieden werden kann, sollten in jedem Fall Stoppkriterien implementiert werden, um versehentliche Blockierungen oder zu viele gleichzeitige Aufrufe zu vermeiden. Vielen Dank, dass Sie sich die Zeit genommen haben, sich über Best Practices beim Funktionsdesign und die Organisation Ihres Codes zu informieren. Der kommende Artikel wird die Themen „Concurrency“ in AWS Lambda und „Monitoring“ von Lambda-Funktionen behandeln.

[1] https://docs.aws.amazon.com/lambda/latest/dg/python-context.html
[2] https://github.com/awslabs/aws-lambda-powertools-python – Connect to preview

Weitere Informationen zum Thema (Eng.):