{"id":199,"date":"2021-05-20T22:13:55","date_gmt":"2021-05-20T20:13:55","guid":{"rendered":"https:\/\/www.iot-embedded.de\/iot-2021\/?p=199"},"modified":"2021-05-20T22:13:57","modified_gmt":"2021-05-20T20:13:57","slug":"automatisiertes-deployment-von-backend-komponenten","status":"publish","type":"post","link":"https:\/\/www.iot-embedded.de\/iot-2021\/smart-drive\/automatisiertes-deployment-von-backend-komponenten\/","title":{"rendered":"Automatisiertes Deployment von Backend-Komponenten"},"content":{"rendered":"Das in der Vorlesung gezeigte voll automatisierte Deployment f\u00fcr beliebig viele IoT-Devices hat uns sehr begeistert. Daher entstand die Idee, etwas \u00e4quivalentes auch f\u00fcr die Backend-Komponenten des Projekts zu schaffen. Wie die Balena Cloud verwenden wir daf\u00fcr Docker. Es wird jedoch kein kommerzielles Angebot verwendet, sondern ein Open-Source Tool, das selbst wiederum unter Verwendung von Docker deployed wurde. Die Rede ist von <i>Gitlab<\/i>. Hier wird allerdings nicht das angebot von gitlab.com verwendet, sondern eine selbst verwaltete Instanz der Community Edition. Im Gegensatz zum bekannten GitHub ist die Software Gitlab frei verf\u00fcgbar und kann so auf eigenen Servern installiert werden. Gitlab integriert eine m\u00e4chtige CI\/CD Infrastruktur, die wir zum Automatisieren der Backend-Komponenten verwenden.\n\nGitlab erm\u00f6glicht die Definition von Pipelines, die zu einem bestimmten Zeitpunkt ausgef\u00fchrt werden. Bei uns wird nach jedem Push in den master-Branch eine Pipeline ausgef\u00fchrt, die das Deployment \u00fcbernimmt. Dabei werden momentan noch keine Tests ausgef\u00fchrt, sondern lediglich die folgenden Schritte:\n<ul>\n \t<li>Docker Image erstellen<\/li>\n \t<li>Erstelltes Image in die eigene Docker Registry pushen<\/li>\n \t<li>Das Docker Image aus der Registry pullen und einen Container starten<\/li>\n<\/ul>\nDamit das Docker Image erstellt werden kann, beinhaltet das Repository ein entsprechendes Dockerfile. Daneben sind die Quellcodes enthalten und au\u00dferdem eine YAML-Datei zur Definition der CI\/CD Pipeline f\u00fcr Gitlab.\n<h2>Die Beispiel-Anwendung<\/h2>\nZiel ist es eine Pipeline zum automatisierten Deployment zu entwickeln. Aus diesem Grund beschr\u00e4nkt sich die hier verwendete Beispiel-Anwendung auf ein Minimalbeispiel eines MQTT-Empf\u00e4ngers. Die empfangenen Nachrichten werden zun\u00e4chst nicht in einer Datenbank gespeichert, sondern \u00fcber ein Websocket gesendet. Dies dient zur Veranschaulichung und zum einfacheren Testen, ob das Deployment funktioniert. Es wird das Python-Framework <i>FastAPI<\/i> verwendet. Als Grundlage dient ein Beispiel aus der entsprechenden <a href=\"https:\/\/fastapi.tiangolo.com\/advanced\/websockets\/\">Doku-Seite<\/a>. Zus\u00e4tzlich dazu wird die MQTT-Erweiterung f\u00fcr FastAPI zum Empfangen der MQTT-Nachrichten verwendet.\n<pre class=\"block\">from fastapi import FastAPI, WebSocket\nfrom fastapi.responses import HTMLResponse\nfrom asyncio import Queue\nfrom fastapi_mqtt import FastMQTT, MQTTConfig\n\nqueues = []\n\napp = FastAPI()\n\nmqtt_config = MQTTConfig(\n\thost = \"193.41.237.111\",\n\tport= 1883,\n\tkeepalive = 60\n)\n\n\nmqtt = FastMQTT(\n\tconfig=mqtt_config\n)\n\nmqtt.init_app(app)\n\n\n\n@mqtt.on_connect()\ndef connect(client, flags, rc, properties):\n\tmqtt.client.subscribe(\"iot-projekt\/#\")\n\tprint(\"MQTT: Connected: \", client, flags, rc, properties)\n\n@mqtt.on_message()\nasync def message(client, topic, payload, qos, properties):\n\tmsg = \"Received message: \" + topic + payload.decode() #+ qos + properties\n\tfor queue in queues:\n\t\tawait queue.put(msg)\n\tprint(msg)\n\tprint(\"Writing to {} queues\".format(len(queues)))\n\n@mqtt.on_disconnect()\ndef disconnect(client, packet, exc=None):\n\tprint(\"MQTT: Disconnected\")\n\n@mqtt.on_subscribe()\ndef subscribe(client, mid, qos, properties):\n\tprint(\"MQTT: subscribed\", client, mid, qos, properties)\n\n\n\nhtml = \"\"\"\n&lt;!DOCTYPE html&gt;\n&lt;html&gt;\n\t&lt;head&gt;\n\t\t&lt;title&gt;MQTT-Empf\u00e4nger&lt;\/title&gt;\n\t&lt;\/head&gt;\n\t&lt;body&gt;\n\t\t&lt;h1&gt;MQTT Nachrichten&lt;\/h1&gt;\n\t\t&lt;ul id='messages'&gt;\n\t\t&lt;\/ul&gt;\n\t\t&lt;script&gt;\n\t\t\tconsole.log(\"Starting...\");\n\t\t\tlet ws = new WebSocket(\"ws:\/\/193.41.237.111:8000\/ws\");\n\t\t\tws.onmessage = (event) =&gt; {\n\t\t\t\tlet messages = document.getElementById('messages')\n\t\t\t\tlet message = document.createElement('li')\n\t\t\t\tlet content = document.createTextNode(event.data)\n\t\t\t\tmessage.appendChild(content)\n\t\t\t\tmessages.appendChild(message)\n\t\t\t};\n\t\t&lt;\/script&gt;\n\t&lt;\/body&gt;\n&lt;\/html&gt;\n\"\"\"\n\n\n@app.get(\"\/\")\nasync def get():\n\treturn HTMLResponse(html)\n\n\n@app.websocket(\"\/ws\")\nasync def websocket_endpoint(websocket: WebSocket):\n\tawait websocket.accept()\n\ttry:\n\t    queue = Queue()\n\t    queues.append(queue)\n\t    while True:\n\t        msg = await queue.get()\n\t        await websocket.send_text(msg)\n\texcept:\n\t\tprint(\"Verbindung durch Client getrennt\")\n\t\tqueues.remove(queue)\n<\/pre>\nZun\u00e4chst wird der MQTT-Client instanziiert. Es werden die schon bekannten Parameter verwendet, die in fr\u00fcheren Beitr\u00e4gen mit paho-mqtt zum Einsatz kamen. Ebenso wird bei erfolgreicher Verbindung das Abo f\u00fcr alle Topics, die mit <i>iot-projekt\/#<\/i> beginnen abgeschlossen. Beim Empfang einer Nachricht, wird diese in alle registrierten Queues geschrieben. Dieses Vorgehen ist notwendig, damit alle Clients die Nachricht angezeigt bekommen, also jeder, der die Seite in seinem Browser ge\u00f6ffnet hat. Es wird die Queue aus <i>asyncio<\/i> verwendet, damit das Skript nicht beim Warten auf eine Nachricht festh\u00e4ngt, sondern alle Operationen asynchron ausgef\u00fchrt werden.\n\nDie auszuliefernde HTML-Seite wird f\u00fcr dieses Minimalbeispiel als String definiert. Per JavaScript wird die Verbindung zum Websocket hergestellt und neue Nachrichten an eine Liste angeh\u00e4ngt.\n\nDie im unteren Teil definierten Funktionen bilden die Endpunkte der API ab. Bei GET-Anfragen auf das obere Verzeichnis wird eine HTML-Antwort ausgeliefert, die die oben beschriebene Funktionalit\u00e4t enth\u00e4lt. Im Endpunkt <i>\/ws<\/i> wird das Websocket definiert. Hier ist zu sehen, dass zun\u00e4chst eine neue Queue angelegt und der Liste aller Queues hinzugef\u00fcgt wird. Die Liste enth\u00e4lt f\u00fcr jedes ge\u00f6ffnete Browserfenster eine Queue. Sobald eine Nachricht in die Queue geschrieben wurde, wird diese als Textnachricht \u00fcber das Websocket versendet. Die wenigen Zeilen JavaScript-Code innerhalb des HTML sorgen daf\u00fcr, dass die Nachricht im Browserfenster angezeigt wird.\n\nWeiter soll die Beispiel-Anwendung nicht erkl\u00e4rt werden, da f\u00fcr den tats\u00e4chlichen Einsatz im Projekt die Daten in eine Datenbank geschrieben und nicht direkt an Clients ausgeliefert werden.\n<h2>Das Dockerfile<\/h2>\nUm das oben gezeigte Skript in ein Docker Image zu verpacken, wird das <a href=\"https:\/\/fastapi.tiangolo.com\/deployment\/docker\/\">offizielle Image von FastAPI<\/a> verwendet. In dem Image l\u00e4uft ein f\u00fcr Python optimierter Webserver, der die programmierte API bereitstellt und \u00fcber Port 80 erreichbar ist. Wie aus fr\u00fcheren Dockerfiles bekannt, werden zuvor noch die in <i>requirements.txt<\/i> gelisteten Abh\u00e4ngigkeiten installiert. Hier ist unbedingt das Paket <i>websockets<\/i> einzutragen, da das Websocket von FastAPI sonst nicht funktioniert. Die App muss in der Datei <i>main.py<\/i> in der Variablen <i>app<\/i> initiiert sein, damit der Webserver das Skript richtig ausf\u00fchrt. Daher ist hier kein Startbefehl mit CMD anzugeben.\n<pre class=\"block\">FROM tiangolo\/uvicorn-gunicorn:python3.8-alpine3.10\n\nCOPY requirements.txt .\n\nRUN pip install --no-cache-dir -r requirements.txt\n\nCOPY .\/src \/app\n<\/pre>\n<h2>Die Pipeline<\/h2>\nDa in der Entwicklung von Pipelines f\u00fcr Gitlab keine Vorkenntnisse vorhanden waren, wurde <a href=\"https:\/\/blog.itelekt.com\/build-deploy-run-docker-image-with-gitlab\/\">dieser Blogbeitrag<\/a> als Grundlage verwendet und an notwendigen Stellen Ver\u00e4nderungen vorgenommen. Die dort gezeigte und \u00fcbernommene Pipeline teilt sich in drei Schritte auf, die die oben beschriebenen Aufgaben abbilden. Zu jedem Schritt wird in diesem Fall nur eine Aufgabe definiert, mehrere w\u00e4ren m\u00f6glich. Die Schritte werden als <i>stages<\/i> definiert und sind in diesem Fall image, transfer und deploy.\n\nBei der Definition von Aufgaben wird einer der Schritte angegeben, zu dem die Aufgabe geh\u00f6rt und diesem somit zugeordnet. Au\u00dferdem wird mit <i>only<\/i> angegeben, dass die Aufgabe nur f\u00fcr den master-Branch ausgef\u00fchrt werden soll. Die Eintr\u00e4ge unter <i>script<\/i> sind die Befehle, die ausgef\u00fchrt werden. Zum Erstellen des Docker Image ist dies der bekannte Befehl <i>docker build<\/i>. Die verwendeten Variablen sind teilweise durch Gitlab automatisch festgelegt oder wurden manuell \u00fcber die Weboberfl\u00e4che f\u00fcr dieses Projekt deklariert.\n\nIm zweiten Schritt wird das erstellte Docker Image in die Registry von Gitlab hochgeladen. Die Docker Registry ist nicht die offizielle Registry Docker Hub, sondern ebenfalls auf dem Server unter der Verwendung von Docker selbst gehostet. Der Login funktioniert, da die Registry Ihre Authentifizierungsinformationen dynamisch von Gitlab \u00fcber einen anderen Kanal erh\u00e4lt. Um dies abzusichern wurde bei der Installation der beiden Komponenten ein Zertifikat ausgetauscht.\n\nIm dritten Schritt muss nun auf einem Server ein Container aus dem erstellten Image erstellt und gestartet werden. Dazu wird eine SSH-Verbindung hergestellt und mit <i>docker run<\/i> ein Container gestartet. Da die Ausf\u00fchrung der Aufgabe selbst ebenfalls in einem Docker Container geschieht und die Voraussetzungen f\u00fcr SSH-Verbindungen nicht gegeben sind, wird unter dem Eintrag <i>before script<\/i> der SSH-Agent initialisiert und diesem der f\u00fcr den Server passende Private-Key \u00fcbergeben. Der Private-Key kann \u00fcber die Weboberfl\u00e4che von Gitlab in einer Variablen gespeichert werden. Nachdem die SSH-Verbindung hergestellt wurde, wird zun\u00e4chst der Container <i>iot-receiver<\/i> gestoppt und gel\u00f6scht, damit anschlie\u00dfend ein neuer Container mit selbem Namen erstellt werden kann. Das Skript l\u00e4uft also nur, wenn vorher ein Container mit dem Namen existiert. Hier ist deutlicher Verbesserungsbedarf. Bei Erstellung des Containers wird der Port 80 des im Container laufenden Webservers an den externen Port 8000 ggebunden, da der Port 80 auf dem Zielserver bereits verwendet wird.\n\nIm Folgenden der Inhalt der Datei <i>.gitlab-ci.yml<\/i>. Gro\u00dfe Teile davon sind aus dem verlinkten Blogbeitrag \u00fcbernommen worden.\n<pre class=\"block\">image: docker:latest\n\nvariables:\n DOCKER_DRIVER: overlay2\n DOCKER_TLS_CERTDIR: \"\"\n APP_VERSION: \"1.3.0\"\n\nservices:\n - docker:19.03.5-dind\n \nstages:\n  - image\n  - transfer\n  - deploy\n  \nbuild_image:\n  stage: image\n  only:\n    - master\n  script:\n    - docker build --no-cache=true -f .\/dockerfile -t $CI_REGISTRY_IMAGE:$APP_VERSION .\n    \ntransfer_image:\n  stage: transfer\n  script:\n    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY\n    - docker push $CI_REGISTRY_IMAGE:$APP_VERSION\n  only:\n    - master\n    \n    \ndeploy image:\n  stage: deploy\n  variables:\n    DOCKER_HOST: ssh:\/\/user@$DESTINATION_HOST\n  before_script:\n    - 'which ssh-agent || ( apt-get update -y &amp;&amp; apt-get install openssh-client -y )'\n    - eval $(ssh-agent -s)\n    - echo \"$SSH_PRIVATE_KEY\" | base64 -d | tr -d '\\r' | ssh-add - &gt; \/dev\/null\n    - mkdir -p ~\/.ssh\n    - chmod 700 ~\/.ssh\n    - ssh-keyscan -t rsa $DESTINATION_HOST &gt;&gt; ~\/.ssh\/known_hosts\n  script:\n    - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY\n    - docker pull $CI_REGISTRY_IMAGE:$APP_VERSION\n    - docker container stop iot-receiver\n    - docker container rm iot-receiver\n    - docker container run --name iot-receiver -d -p 8000:80 $CI_REGISTRY_IMAGE:$APP_VERSION\n  only:\n    - master\n<\/pre>\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"582\" class=\"wp-image-203\" src=\"https:\/\/www.iot-embedded.de\/iot-2021\/wp-content\/uploads\/sites\/5\/2021\/05\/Bildschirmfoto-2021-05-20-um-22.03.00-1024x582.png\" alt=\"\" srcset=\"https:\/\/www.iot-embedded.de\/iot-2021\/wp-content\/uploads\/sites\/5\/2021\/05\/Bildschirmfoto-2021-05-20-um-22.03.00-1024x582.png 1024w, https:\/\/www.iot-embedded.de\/iot-2021\/wp-content\/uploads\/sites\/5\/2021\/05\/Bildschirmfoto-2021-05-20-um-22.03.00-300x170.png 300w, https:\/\/www.iot-embedded.de\/iot-2021\/wp-content\/uploads\/sites\/5\/2021\/05\/Bildschirmfoto-2021-05-20-um-22.03.00-768x436.png 768w, https:\/\/www.iot-embedded.de\/iot-2021\/wp-content\/uploads\/sites\/5\/2021\/05\/Bildschirmfoto-2021-05-20-um-22.03.00.png 1227w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/>\n<figcaption>Screenshot von der Gitlab-Weboberfl\u00e4che zeigt die erfolgreich durchlaufene Pipeline.<\/figcaption><\/figure>\n\n\nF\u00fcr den hier durchgef\u00fchrten Versuch laufen alle erw\u00e4hnten Komponenten (Gitlab, Docker Registry, das deployte Image und der MQTT-Broker) auf einer Maschine. Dies ist f\u00fcr den produktiven Einsatz sicherlich nicht sinnvoll, reicht aber zum Testen. Die in einer Gitlab-Pipeline ausgef\u00fchrten Aufgaben, werden durch den sog. <i>Gitlab-Runner<\/i> ausgef\u00fchrt. Dieser l\u00e4sst sich ebenfalls mit Docker verwenden. Dieser l\u00e4uft also ebenfalls auf dem Server.\n<h2>Fazit<\/h2>\nDie Implementierung unseres Projekts ist durch das hier gezeigte Vorgehen keinen Schritt voran gegangen. Es wurde aber eine sehr komfortable Umgebung geschaffen, um alle verwendeten Backend-Komponenten deployen zu k\u00f6nnen. Nach aktuellem Stand w\u00fcrde f\u00fcr jede Komponente ein eigenes Repository erstellt und jeweils eine eigene Pipeline konfiguriert werden. Ob es sinnvoll ist, alle Komponenten wie bei der vorgeschlagenen Architektur f\u00fcr die IoT-Devices, in einem Repository zusammenzufassen und mit docker-compose zu orchestrieren bleibt abzuw\u00e4gen, da dann bei jedem push alle Komponenten aktualisiert w\u00fcrden. Im jetzigen Stand dauert die Ausf\u00fchrung der Pipeline bereits etwa eine Minute, sodass es hier zu langen Wartezeiten kommen k\u00f6nnte.","protected":false},"excerpt":{"rendered":"<p>Das in der Vorlesung gezeigte voll automatisierte Deployment f\u00fcr beliebig viele IoT-Devices hat uns sehr begeistert. Daher entstand die Idee, etwas \u00e4quivalentes auch f\u00fcr die Backend-Komponenten des Projekts zu schaffen. Wie die Balena Cloud verwenden wir daf\u00fcr Docker. Es wird<\/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\/199"}],"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=199"}],"version-history":[{"count":5,"href":"https:\/\/www.iot-embedded.de\/iot-2021\/wp-json\/wp\/v2\/posts\/199\/revisions"}],"predecessor-version":[{"id":205,"href":"https:\/\/www.iot-embedded.de\/iot-2021\/wp-json\/wp\/v2\/posts\/199\/revisions\/205"}],"wp:attachment":[{"href":"https:\/\/www.iot-embedded.de\/iot-2021\/wp-json\/wp\/v2\/media?parent=199"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.iot-embedded.de\/iot-2021\/wp-json\/wp\/v2\/categories?post=199"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.iot-embedded.de\/iot-2021\/wp-json\/wp\/v2\/tags?post=199"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}