{"id":127,"date":"2021-05-14T20:20:49","date_gmt":"2021-05-14T18:20:49","guid":{"rendered":"https:\/\/www.iot-embedded.de\/iot-2021\/?p=127"},"modified":"2021-05-14T20:20:50","modified_gmt":"2021-05-14T18:20:50","slug":"eigenes-python-skript-in-docker-image-verpacken","status":"publish","type":"post","link":"https:\/\/www.iot-embedded.de\/iot-2021\/smart-drive\/eigenes-python-skript-in-docker-image-verpacken\/","title":{"rendered":"Eigenes Python-Skript in Docker-Image verpacken"},"content":{"rendered":"<p>Da im weiteren Verlauf des Projekts die Software f\u00fcr 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\u00f6glichkeit per MQTT-Topic erweitert und anschlie\u00dfend in ein Docker Image verpackt. Auf diese Art und Weise erfolgt sp\u00e4ter das Deployment f\u00fcr die IoT-Devices, wobei die Software noch zu entwickeln ist. Die wesentlichen Schnittstellen nach au\u00dfen, also das Ver\u00f6ffentlichen und Abonnieren von MQTT-Topics wird f\u00fcr den Prototyp jedoch schon implementiert, sodass die Infrastruktur ausprobiert werden kann.<\/p>\n<h2>Python Skript erweitern<\/h2>\n<p>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\u00e4\u00dfigen Abst\u00e4nden Nachrichten in einem Topic ver\u00f6ffentlicht werden, die Abst\u00e4nde jedoch flexibel ver\u00e4ndert werden k\u00f6nnen. Dies erfolgt \u00fcber die Ver\u00f6ffentlichung durch einen anderen Client in anderem Topic, das vom IoT-Device abonniert wird.<\/p>\n<p>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 <i>intervall<\/i> nach dem Aufbau der Verbindung abonniert werden und weiterhin im Topic <i>iot<\/i> die aktuelle Zeit ver\u00f6ffentlicht werden. Zus\u00e4tzlich soll das empfangene Intervall als Zahl in einer Variable gespeichert werden, sodass sich das Sendeverhalten dar\u00fcber einstellen l\u00e4sst. Dazu wird die Handler-Methode <i>on_message_handler<\/i> so erweitert, dass der Payload der empfangenen Nachricht in der globalen Variable <i>intervall<\/i> gespeichert wird.<\/p>\n<p>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 <i>loop_forever<\/i> die Methode <i>loop_start<\/i> des Client-Objekts verwendet.  Nach Aufruf dieser Methode lauscht der Client in einem neuen Thread auf Nachrichten und das Skript l\u00e4uft weiter, sodass hier die eigene Schleife zum Versand der Nachrichten implementiert werden kann. Bei Abbruch wird der zweite Thread mit <i>loop_stop<\/i> sauber beendet.<\/p>\n<pre class=\"block\">import paho.mqtt.client as mqtt\nfrom datetime import datetime\nfrom time import sleep\n\nintervall = 10\n\ndef on_connect_handler(client, userdata, flags, return_code):\n\tprint(\"Return Code: \" + str(return_code))\n\tclient.subscribe(\"intervall\")\n\ndef on_message_handler(client, userdata, message):\n\tprint(message.topic)\n\tprint(str(message.payload))\n\t\n\tif message.topic == \"intervall\":\n\t\tglobal intervall\n\t\tintervall = int(message.payload)\n\nclient = mqtt.Client()\nclient.on_connect = on_connect_handler\nclient.on_message = on_message_handler\nclient.connect(\"193.41.237.111\", 1883, 60)\n\ntry:\n\tclient.loop_start()\n\twhile True:\n\t\tpayload = datetime.now().strftime('%Y-%m-%d %H:%M:%S')\n\t\tclient.publish(\"iot\", payload=payload)\n\t\tprint(\"Ver\u00f6ffentliche in iot: \" + payload)\n\t\tsleep(intervall)\nfinally:\n\tclient.loop_stop()\n\tclient.disconnect()\n<\/pre>\n<h2>Docker Image erstellen<\/h2>\n<p>Ein Docker Image wird aus einem Docker File erzeugt. Aus einem Docker Image k\u00f6nnen 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\u00dft, die folgenden Informationen m\u00fcssen im Docker Image enthalten sein.<\/p>\n<ul>\n<li>Die entwickelte Software (in diesem Artikel erstmal das eine Python-Skript)<\/li>\n<li>Alle Abh\u00e4ngigkeiten der Software (Python und verwendete Bibliotheken)<\/li>\n<li>Der Befehl zum Start der Software<\/li>\n<\/ul>\n<p>Hat man die Software entwickelt, so sind die oben genannten Bestandteile vorhanden. F\u00fcr den hier vorgestellten Prototyp wird lediglich eine Skriptdatei ben\u00f6tigt, die ohne Parameter aufgerufen wird. Alle Abh\u00e4ngigkeiten 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&#8230;<\/p>\n<p>Zum Gl\u00fcck 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\u00fcr das hier zu erstellende Image ist also das offizielle Docker Image f\u00fcr Python, das bereits alles enth\u00e4lt, um Python ausf\u00fchren und pip verwenden zu k\u00f6nnen. Auf dieser Basis sind dann die folgenden Schritte notwendig.<\/p>\n<ol>\n<li>Projektordner in das Image kopieren (hier die Datei app.py und requirements.txt)<\/li>\n<li>Abh\u00e4ngigkeiten installieren<\/li>\n<li>Skript starten<\/li>\n<\/ol>\n<p>F\u00fcr die weiteren Schritte werden die folgenden Dateien und Ordner angenommen.<\/p>\n<pre class=\"block\">.\/dockerfile\n.\/requirements.txt\n.\/src\/app.py\n<\/pre>\n<p>Dabei ist .\/ das Arbeitsverzeichnis<\/p>\n<p>F\u00fcr das Docker File sind nur wenige Zeilen notwendig, um die oben genannten Schritte abzubilden. Zun\u00e4chst wird mit <i>FROM<\/i> ein bestehendes Docker Image angegeben auf dessen Basis ein neues Image erstellt wird. In diesem Fall wird das Image <i>pyton:3-alpine<\/i> als Basis verwendet. Der Teil vor dem Doppelpunkt ist der Name des Image, w\u00e4hrend der Teil nach dem Doppelpunkt als Tag bezeichnet wird. Der Tag muss nicht angegeben werden, ist jedoch n\u00fctzlich zur Auswahl einer bestimmten Version des Image. In diesem Fall wird Python 3.9 als aktuelle stabile Version gew\u00e4hlt. <i>Alpine<\/i> ist eine schlanke Linux-Distribution, die f\u00fcr sehr schlanke Docker Images sorgt. Daher wird hier dieser Tag verwendet.<\/p>\n<p>Als n\u00e4chstes wird mit <i>WORKDIR<\/i> in das Verzeichnis \/usr\/src\/app innerhalb des Image gewechselt. In dieses Verzeichnis soll die entwickelte Software kopiert werden. Zun\u00e4chst 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\u00e4rts. Da sich der Quellcode \u00f6fter \u00e4ndert als die Abh\u00e4ngigkeiten, wird der Quellcode erst in einer sp\u00e4teren Schicht aufgenommen, um unn\u00f6tige \u00c4nderungen an tieferen Schichten zu vermeiden. Das Kopieren erfolgt mit <i>COPY<\/i>. Als erstes Argument wird das Quellverzeichnis auf der Host Maschine angegeben und an zweiter Stelle das Zielverzeichnis im Image.<\/p>\n<p>Ist die Datei mit den Abh\u00e4ngigkeiten kopiert, k\u00f6nnen diese unter Verwendung von pip installiert werden. Der Aufruf erfolgt mit <i>RUN<\/i> analog zu einem Aufruf in der Konsole.<\/p>\n<p>Anschlie\u00dfend wird der Ordner <i>src<\/i> ebenfalls in das Image kopiert. Dadurch, dass der eigentliche Quellcode nur diese letzte Schicht beeinflusst, sind bei einem reinen Quellcode-Update keine tieferen \u00c4nderungen im Image notwendig. Aktuell beinhaltet der Ordner nur eine Datei. Bei Erweiterung des Projekts werden alle zus\u00e4tzlichen Dateien ebenfalls kopiert, sodass immer die gesamte Software vorhanden ist. Abschlie\u00dfend wird mit <i>CMD<\/i> der Befehl angegeben, der beim Start eines Containers, der aus dem Image erzeugt wird, ausgef\u00fchrt wird. Als Argument wird ein Array \u00fcbergeben, das an erster Stelle den Befehl <i>python<\/i> beinhaltet und an zweiter Stelle das auszuf\u00fchrende Skript. In diesem Fall <i>app.py<\/i><\/p>\n<p>Das gesamte Docker File sieht so aus:<\/p>\n<pre class=\"block\">FROM python:3-alpine\n\nWORKDIR \/usr\/src\/app\n\nCOPY requirements.txt .\n\nRUN pip install --no-cache-dir -r requirements.txt\n\nCOPY .\/src .\n\nCMD [\"python\", \"app.py\"]\n<\/pre>\n<p>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\u00e4ter zu finden wird ein Name mit angegeben.<\/p>\n<pre class=\"block\">docker build -t iot .\n<\/pre>\n<p>In der Ausgabe sieht man, dass nun die Abh\u00e4ngigkeiten 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.<\/p>\n<pre class=\"block\">docker run -it iot\n<\/pre>\n<p>Da beim Erzeugen des Image ein Name angegeben wurde, kann das Image dar\u00fcber referenziert werden. <i>-it<\/i> sorft f\u00fcr 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\u00f6nnen von anderen Clients empfangen werden. Wird im Topic <i>intervall<\/i> eine Zahl ver\u00f6ffentlicht, so ist diese der neue Abstand in Sekunden zwischen den Nachrichten.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Da im weiteren Verlauf des Projekts die Software f\u00fcr 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\u00f6glichkeit per MQTT-Topic erweitert<\/p>\n","protected":false},"author":3,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[8],"tags":[],"_links":{"self":[{"href":"https:\/\/www.iot-embedded.de\/iot-2021\/wp-json\/wp\/v2\/posts\/127"}],"collection":[{"href":"https:\/\/www.iot-embedded.de\/iot-2021\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.iot-embedded.de\/iot-2021\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.iot-embedded.de\/iot-2021\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/www.iot-embedded.de\/iot-2021\/wp-json\/wp\/v2\/comments?post=127"}],"version-history":[{"count":3,"href":"https:\/\/www.iot-embedded.de\/iot-2021\/wp-json\/wp\/v2\/posts\/127\/revisions"}],"predecessor-version":[{"id":130,"href":"https:\/\/www.iot-embedded.de\/iot-2021\/wp-json\/wp\/v2\/posts\/127\/revisions\/130"}],"wp:attachment":[{"href":"https:\/\/www.iot-embedded.de\/iot-2021\/wp-json\/wp\/v2\/media?parent=127"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.iot-embedded.de\/iot-2021\/wp-json\/wp\/v2\/categories?post=127"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.iot-embedded.de\/iot-2021\/wp-json\/wp\/v2\/tags?post=127"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}