Da im weiteren Verlauf des Projekts die Software für die IoT-Devices in Docker Images verpackt werden muss, soll hier zu Testzwecken ein weiteres Minimalbeispiel dokumentiert werden. Dazu wird das Skript aus dem vorherigen Beitrag um eine Steuerungsmöglichkeit per MQTT-Topic erweitert und anschließend in ein Docker Image verpackt. Auf diese Art und Weise erfolgt später das Deployment für die IoT-Devices, wobei die Software noch zu entwickeln ist. Die wesentlichen Schnittstellen nach außen, also das Veröffentlichen und Abonnieren von MQTT-Topics wird für den Prototyp jedoch schon implementiert, sodass die Infrastruktur ausprobiert werden kann.

Python Skript erweitern

Da IoT-Devices nicht nur Nachrichten versenden sollen, sondern durch andere Topics steuerbar sein sollen, wird das Skript aus dem vorherigen Beitrag etwas erweitert, sodass in regelmäßigen Abständen Nachrichten in einem Topic veröffentlicht werden, die Abstände jedoch flexibel verändert werden können. Dies erfolgt über die Veröffentlichung durch einen anderen Client in anderem Topic, das vom IoT-Device abonniert wird.

Da der Client in diesem Skript sowohl ein Topic abonnieren soll als auch Nachrichten an ein anderes Topic senden soll, sind die Bestandteile aus dem vorherigen Beitrag zusammenzufassen. So soll das Topic intervall nach dem Aufbau der Verbindung abonniert werden und weiterhin im Topic iot die aktuelle Zeit veröffentlicht werden. Zusätzlich soll das empfangene Intervall als Zahl in einer Variable gespeichert werden, sodass sich das Sendeverhalten darüber einstellen lässt. Dazu wird die Handler-Methode on_message_handler so erweitert, dass der Payload der empfangenen Nachricht in der globalen Variable intervall gespeichert wird.

Damit der Client gleichzeitig lauschen und senden kann, ist die Auslagerung einer der beiden Aufgaben in einen eigenen Thread notwendig. Dazu wird statt der vormals verwendeten Methode loop_forever die Methode loop_start des Client-Objekts verwendet. Nach Aufruf dieser Methode lauscht der Client in einem neuen Thread auf Nachrichten und das Skript läuft weiter, sodass hier die eigene Schleife zum Versand der Nachrichten implementiert werden kann. Bei Abbruch wird der zweite Thread mit loop_stop sauber beendet.

import paho.mqtt.client as mqtt
from datetime import datetime
from time import sleep

intervall = 10

def on_connect_handler(client, userdata, flags, return_code):
	print("Return Code: " + str(return_code))
	client.subscribe("intervall")

def on_message_handler(client, userdata, message):
	print(message.topic)
	print(str(message.payload))
	
	if message.topic == "intervall":
		global intervall
		intervall = int(message.payload)

client = mqtt.Client()
client.on_connect = on_connect_handler
client.on_message = on_message_handler
client.connect("193.41.237.111", 1883, 60)

try:
	client.loop_start()
	while True:
		payload = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
		client.publish("iot", payload=payload)
		print("Veröffentliche in iot: " + payload)
		sleep(intervall)
finally:
	client.loop_stop()
	client.disconnect()

Docker Image erstellen

Ein Docker Image wird aus einem Docker File erzeugt. Aus einem Docker Image können wiederum Docker Container erzeugt werden. Das Docker File muss in diesem Fall geschrieben werden. Das daraus erzeugte Docker Image wird an alle IoT-Devices verteilt und auf jedem Device ein Container aus diesem Image gestartet. Das heißt, die folgenden Informationen müssen im Docker Image enthalten sein.

  • Die entwickelte Software (in diesem Artikel erstmal das eine Python-Skript)
  • Alle Abhängigkeiten der Software (Python und verwendete Bibliotheken)
  • Der Befehl zum Start der Software

Hat man die Software entwickelt, so sind die oben genannten Bestandteile vorhanden. Für den hier vorgestellten Prototyp wird lediglich eine Skriptdatei benötigt, die ohne Parameter aufgerufen wird. Alle Abhängigkeiten sind in der Datei requirements.txt festgehalten, sodass diese ebenfalls bekannt sind. Da bleibt nur die Frage, wie man Python selbst in das Image bekommt…

Zum Glück ist das sehr einfach, da Docker Images in Schichten aufgebaut sind. Das bedeutet, dass ein bereits bestehendes Docker Image um die notwendigen Bestandteile erweitert werden kann. Die Grundlage für das hier zu erstellende Image ist also das offizielle Docker Image für Python, das bereits alles enthält, um Python ausführen und pip verwenden zu können. Auf dieser Basis sind dann die folgenden Schritte notwendig.

  1. Projektordner in das Image kopieren (hier die Datei app.py und requirements.txt)
  2. Abhängigkeiten installieren
  3. Skript starten

Für die weiteren Schritte werden die folgenden Dateien und Ordner angenommen.

./dockerfile
./requirements.txt
./src/app.py

Dabei ist ./ das Arbeitsverzeichnis

Für das Docker File sind nur wenige Zeilen notwendig, um die oben genannten Schritte abzubilden. Zunächst wird mit FROM ein bestehendes Docker Image angegeben auf dessen Basis ein neues Image erstellt wird. In diesem Fall wird das Image pyton:3-alpine als Basis verwendet. Der Teil vor dem Doppelpunkt ist der Name des Image, während der Teil nach dem Doppelpunkt als Tag bezeichnet wird. Der Tag muss nicht angegeben werden, ist jedoch nützlich zur Auswahl einer bestimmten Version des Image. In diesem Fall wird Python 3.9 als aktuelle stabile Version gewählt. Alpine ist eine schlanke Linux-Distribution, die für sehr schlanke Docker Images sorgt. Daher wird hier dieser Tag verwendet.

Als nächstes wird mit WORKDIR in das Verzeichnis /usr/src/app innerhalb des Image gewechselt. In dieses Verzeichnis soll die entwickelte Software kopiert werden. Zunächst wird jedoch nur die Datei requirements.txt kopiert. Dies hat den Grund, dass Docker Images in Schichten aufgebaut sind. Jede Version eines Image aktualisiert nur ab der notwendigen Schicht aufwärts. Da sich der Quellcode öfter ändert als die Abhängigkeiten, wird der Quellcode erst in einer späteren Schicht aufgenommen, um unnötige Änderungen an tieferen Schichten zu vermeiden. Das Kopieren erfolgt mit COPY. Als erstes Argument wird das Quellverzeichnis auf der Host Maschine angegeben und an zweiter Stelle das Zielverzeichnis im Image.

Ist die Datei mit den Abhängigkeiten kopiert, können diese unter Verwendung von pip installiert werden. Der Aufruf erfolgt mit RUN analog zu einem Aufruf in der Konsole.

Anschließend wird der Ordner src ebenfalls in das Image kopiert. Dadurch, dass der eigentliche Quellcode nur diese letzte Schicht beeinflusst, sind bei einem reinen Quellcode-Update keine tieferen Änderungen im Image notwendig. Aktuell beinhaltet der Ordner nur eine Datei. Bei Erweiterung des Projekts werden alle zusätzlichen Dateien ebenfalls kopiert, sodass immer die gesamte Software vorhanden ist. Abschließend wird mit CMD der Befehl angegeben, der beim Start eines Containers, der aus dem Image erzeugt wird, ausgeführt wird. Als Argument wird ein Array übergeben, das an erster Stelle den Befehl python beinhaltet und an zweiter Stelle das auszuführende Skript. In diesem Fall app.py

Das gesamte Docker File sieht so aus:

FROM python:3-alpine

WORKDIR /usr/src/app

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

COPY ./src .

CMD ["python", "app.py"]

Nun ist es an der Zeit, aus dem Docker File ein Docker Image zu erstellen. Das ist mit einem Befehl erledigt. Um das Image später zu finden wird ein Name mit angegeben.

docker build -t iot .

In der Ausgabe sieht man, dass nun die Abhängigkeiten installiert werden. Das Skript wird jedoch nicht gestartet, da noch keine Instanz, also kein Container aus dem Image erstellt wird. Das Image liegt jetzt aber lokal bereit und kann verwendet werden. Ein Container wird mit dem folgenden Befehl gestartet.

docker run -it iot

Da beim Erzeugen des Image ein Name angegeben wurde, kann das Image darüber referenziert werden. -it sorft für eine interaktive Sitzung, sodass das Skript mit Strg+C beendet werden kann. Bis dahin erscheinen die Ausgaben in der Konsole und die versendeten MQTT-Nachrichten können von anderen Clients empfangen werden. Wird im Topic intervall eine Zahl veröffentlicht, so ist diese der neue Abstand in Sekunden zwischen den Nachrichten.

Eigenes Python-Skript in Docker-Image verpacken

Ein Kommentar zu „Eigenes Python-Skript in Docker-Image verpacken

Schreibe einen Kommentar