Heimautomatisierung mit Alexa und Raspberry Pi — Teil 3

AWS Lambda und IoT MQTT

Einleitung

Nachdem wir in Teil 2 die Verbindung unseres lokalen Raspberry mit der MQTT-Instanz in AWS erfolgreich implementiert haben, beschäftigen wir uns in diesem Teil mit der Kommunikation einer Lambda-Funktion in AWS mit der MQTT-Instanz. Wir werden diese Funktionalität später brauchen, um aus unserem Alexa-Skill die Sprachbefehle an den Raspberry (über MQTT) weiterzuleiten.

Voraussetzungen

Die Voraussetzungen sind die gleichen wie in den ersten beiden Teilen. Wir benötigen einen AWS-Account für die Erstellung der Lambda-Funktion, wir benötigen ein IoT-Objekt (für den Zugriff auf die MQTT-Instanz) und optional die Verbindung zu unserem Raspberry, die wir in Teil 2 hergestellt haben (prinzipiell können wir die Tests auch mit dem IoT-Test-Client durchführen, aber die Ergebnisse auf der Raspberry-eigenen Konsole zu sehen ist natürlich netter).

Auch wenn dies eine Wiederholung ist: Für die spätere Interaktion mit Alexa ist (zumindest zum Zeitpunkt des Schreibens) wichtig, in der AWS-Region “EU-West-1 (Ireland)” zu arbeiten. Dies ist die AWS-Region, in der auch die Alexa-Skills für Deutschland laufen. Da jede Region eine eigene zugeordnete MQTT-Instanz für unseren Account hat, würde Alexa zwar die Befehle an die MQTT-Instanz in Irland weiterleiten, unser Raspberry würde aber bei einer anderen MQTT-Instanz (z.B. Frankfurt) lauschen.

Vorbereitungen

Aus der AWS-Management-Console (der Einstiegsseite bei AWS) wechseln wir zu AWS Lambda. Wenn dies unser erster Besuch ist, bietet AWS uns die Erstellung einer neuen Lambda-Funktion an. Andernfalls gehen wir über das Menü “Funktionen” und wählen “Funktion erstellen”. Für unsere Funktion benötigen wir keine Vorlage, wir wählen also “Ohne Vorgabe erstellen” aus, als Namen vergeben wir “publishToMQTT”. Wir schreiben die Funktion der Einfachheit halber in Node.js und wählen die neueste Version der Laufzeitumgebung aus (zum Zeitpunkt des Schreibens v8.10).

Im nächsten Schritt benötigen wir Berechtigungen, um von unserer Lambda-Funktion auf die IoT-Infrastruktur zugreifen zu können. Hierfür definieren wir eine benutzerdefinierte Rolle (zum Beispiel mit dem Namen “IOTAccessRole”), die die Berechtigungen für den Zugriff auf AWS IoT enthält. Diese kennen wir bereits aus dem zweiten Teil, dort hatten wir eine Zugriffsrichtlinie für unser Objekt definiert. Genau diese Definition verwenden wir jetzt für die Rolle. Dies erlaubt uns den benötigten Zugriff.

Wir wählen bei dem Auswahlfeld für die Rolle “Erstellen einer benutzerdefinierten Rolle”. Ein neues Fenster im Bereich IAM öffnet sich, das uns die Erstellung ermöglicht. Als Namen geben wir “IOTAccessRole” ein, klicken auf “Show Policy Document” und “Edit”. Natürlich lesen wir die Dokumentation bevor wir auf “Ok” klicken und fügen anstelle der dargestellten Richtlinie unsere eigene Richtlinie aus dem zweiten Teil an. Hier nochmal der genaue Inhalt:

Listing 1: Zugriffsrichtlinie für den freien Zugriff

Hierbei ist es wichtig, den Inhalt exakt so wie dargestellt (also auch ohne zusätzliche Leerzeilen an Anfang oder Ende) einzufügen. Wenn wir alles richtig gemacht haben, wird die Rolle erstellt. Diese Rolle steht uns auch später für andere Lambda-Funktionen zur Verfügung.

Jetzt können wir in unserem vorherigen Fenster “Funktion erstellen” anklicken und erhalten eine minimale Beispielfunktion.

Unsere Lambda-Funktion

Zugriff auf IoT

AWS bietet verschiedene Möglichkeiten der Kommunikation mit MQTT. Die offensichtliche, erste Wahl fällt auf das aws-iot-sdk, das uns eine direkte MQTT-Verbindung zur MQTT-Broker-Instanz ermöglicht. Hier existiert allerdings eine konzeptuelle Schwachstelle, da eine Lambda-Funktion von der Idee her kurzlebig ist und nur für die Zeit des Aufrufs existiert, eine durch das aws-iot-sdk zur Verfügung gestellte MQTT-Verbindung langlebig ist. MQTT geht prinzipiell davon aus, dass wir eine dauerhafte Verbindung der Kommunikationsteilnehmer zum Broker haben, und nimmt dafür einen höheren Aufwand für den Aufbau dieser Verbindung in Kauf.

Deshalb ist an dieser Stelle die kurzlebige Kommunikation über HTTPS die Variante, die nicht nur konzeptuell sauberer ist, sondern auch deutlich schneller und mit weniger Ressourcen-Verbrauch verbunden. Hierfür stellt uns das aws-sdk einen Typ IoTData, der diese Kommunikation für uns kapselt und einfach verwendbar macht.

Wir beginnen unsere Funktion also mit der Definition eines Objektes vom Typ IoTData, das uns den Zugriff auf unsere MQTT-Instanz erlaubt.

Listing 2: Erzeugung eines Objekts vom Typ IoTData für den Zugriff auf die MQTT-Instanz

Wir beginnen mit dem Wechsel zum “Strict Mode”, der uns deutlich mehr Warnungen bei der Programmierung gibt, binden dann die “aws-sdk”-Bibliothek ein und erzeugen ein neues Objekt vom Typ IoTData (die Dokumentation findet sich unter https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/IotData.html). Hier legen wir sowohl die Region als auch den Endpoint für die Kommunikation fest. Dies ist exakt der Kommunikationsendpunkt, den wir in Teil 2 für die Kommunikation verwendet haben.

Unser Funktion zum Publizieren

Nachdem wir jetzt das Objekt zur Kommunikation mit der MQTT-Instanz erzeugt haben, definieren wir jetzt eine Funktion sendToIoT(), die übergebene Daten publiziert.

Listing 3: Funktion sendToIoT() zum Senden von Daten an die MQTT-Instanz

Unsere Funktion sendToIoT(), akzeptiert zwei Parameter, die zu sendenden Daten und den Kontext, der uns beim Aufruf durch die AWS-Umgebung übergeben wird. Diesen verwenden wir, um das Ergebnis unseres Versuchs, die Daten zu publizieren zurückzumelden.

Wir beginnen mit der Definition der Parameter für die Methode publish() unseres IoTData-Objekts, die das Topic und die Daten enthalten (die Daten werden hierbei in eine JSON-Repräsentation überführt). Für unsere Testfunktionalität wählen wir als Topic “topic_1”, so dass wir mit unseren bisherigen Implementierungen unter Node-Red, mit dem Beispielskript start.sh und mit dem Test-Client die Daten sehen, die wir senden (siehe Teil 2 für die Details).

Dann rufen wir die Methode publish() auf und übergeben eine anonyme Funktion, die im Fehlerfall die durch den Kontext zur Verfügung gestellte Fehlerfunktion und im Erfolgsfall die Erfolgsfunktion (context.fail() bzw. context.succeed()) aufruft.

Listing 4: Erste Testfunktion für den Aufruf der MQTT-Instanz

Zum Schluss müssen wir die Funktionalität noch der Umgebung zur Verfügung stellen, und dies passiert über den Export einer anonymen Funktion von unserem Modul aus. AWS erwartet eine Lamba-Funktion unter exports.handler, und wir definieren diese sehr einfach als einen Aufruf der Funktion sendToIoT() mit der Nachricht “Hello from Lambda”.

Hier noch einmal der vollständige Quelltext zum Kopieren und Einfügen:

Listing 5: Die vollständige Lambda-Funktion für den Test der Interaktion mit der MQTT-Instanz

Aufruf unserer Lambda-Funktion

Um unsere Lambda-Funktion aufzurufen, verwenden wir die von AWS zur Verfügung gestellte Test-Funktionalität.

Der Klick auf “Test” erlaubt uns die Ausführung verschiedener Tests, und wir können im Rahmen der Konfiguration beliebige Kontextobjekte mitgeben. Für unseren aktuellen Quelltext ist es egal, wie unser Kontext definiert ist, da wir keine Parameter übergeben. Wir können aber für unsere Tests dieses Testereignis definieren:

{
“message”: “hallo from Test”
}

Wenn wir jetzt in unserer anonymen exportierten Funktion anstatt des Aufrufs sendToIoT(“Hello from Lambda”, context); den Aufruf sendToIoT(event.message, context); verwenden, dann schicken wir die im Testereignis definierte Nachricht in die MQTT-Instanz.

Wenn wir jetzt den “Test” klicken, sollten wir in unserer Debug-Ausgabe bei Node-Red, in unserem Test-Client bei AWS IoT und in der Konsolenausgabe des Beispielskripts start.sh die publizierte Nachricht sehen.

message topic_1 “Hello from Lambda”

Verwendung eines IAM-Benutzers

Um die Sicherheit weiter zu erhöhen, können wir einen eigenen IAM-Benutzer mit beschränkten Rechten einführen, den wir in der Erzeugung des IoTData-Objekts erzeugen. Damit können wir prinzipiell die möglichen Rechte des Skriptes über die Beschränkung durch die Rolle hinaus einschränken. Dies machen wir sehr einfach dadurch, dass wir zwei zusätzliche Felder accessKeyId und secretAccessKey angeben. Die hierfür notwendigen Werte bekommen wir im IAM dadurch, dass wir einen neuen Benutzer mit beschränkten Rechten anlegen. Dessen Zugriffsinformationen können wir wie folgt angeben:

Listing 6: Verwendung eines IAM-Benutzers zur Autorisierung des Zugriffs

Zusammenfassung

Das Publizieren von Daten in unserer MQTT-Instanz ist nicht schwer, wenn man weiß welche Teile der AWS-Infrastruktur zu verwenden sind. Und da die Kommunikation lokal funktioniert, ist sie sehr schnell und nahezu ohne Zeitverzögerung möglich. Wir können jetzt also von einer AWS-Lambda-Funktion aus direkt mit unserem Raspberry kommunizieren, und im nächsten Artikel werden wir betrachten, wie wir dies nutzen um mit einem Alexa-Skill die Sprachbefehle, die wir Alexa geben, an unseren Raspberry weiterzuleiten.

Die vollständige Lambda-Funktion findet sich auf Github.


Vielen Dank fürs Lesen und ich freue mich auf Feedback. Die weiteren Artikel dieser Serie, genau wie andere interessante Artikel, erscheinen im Blog der Digital Frontiers und werden auch auf unserem Twitter-Account angekündigt.