MQTT Deployments in containerbasierten Anwendungsplattformen wie OpenShift

Das MQTT-Protokoll wurde 1999 von Andy Stanford-Clark (IBM) und Arlen Nipper (Arcom, jetzt Cirrus Link) erfunden. Sie benötigten ein Protokoll für minimalen Batterieverlust und minimale Bandbreite, um über Satelliten eine Verbindung mit Ölpipelines herzustellen. Ungefähr 3 Jahre nach der Erstveröffentlichung wurde bekanntgegeben, dass MQTT unter der Obhut von OASIS, einer offenen Organisation zur Weiterentwicklung von Standards, standardisiert werden soll. AMQP, SAML und DocBook sind nur einige der zuvor veröffentlichten OASIS-Standards. Der Standardisierungsprozess dauerte ca. 1 Jahr. Im Oktober 2014 wurde MQTT ein offiziell anerkannter OASIS-Standard. Im Januar 2018 wurde die neueste Version MQTT 5, die als Nachfolger von MQTT 3.1.1 gilt, offiziell released. Neben der noch recht neuen Spezifikation MQTT 5 ist die MQTT Version 3.1.1 die Version mit der größten Verbreitung.
Heute gilt MQTT als de facto Standard für die M2M-Kommunikation im Internet der Dinge. Das zeigen Trends wie das Eclipse Foundation IOT Survey [1] oder auch aktuelle Google-Trends-Analysen.

Eigenschaften von MQTT
Auf dem TCP-Protokoll aufbauend nutzt MQTT die TCP eigenen Eigenschaften, dass Informationen geordnet, verlustfrei und bi-direktional transportiert werden. Basierend darauf bietet MQTT einige weitere wichtige Eigenschaften, deren wesentliche Elemente sind:
- Das Protokoll spezifiziert die Verwendung des Publish-/Subscribe-Nachrichtenmusters, das eine 1-zu-N-Nachrichtenverteilung und damit eine Entkopplung von Nachrichtensendern und Empfängern ermöglicht.
- Der Transport der MQTT-Nachricht ist agnostisch bezüglich des eigentlichen Inhalts, des Payloads einer Nachricht.
- Das Protokoll ist binär und besitzt nur einen geringen Transport-Overhead, was dazu beiträgt, den Netzwerkverkehr zu reduzieren.
- Bei abnormen Verbindungsabbrüchen bietet es einen Mechanismus, interessierte Nachrichtenempfänger darüber zu benachrichtigen.
Mit dem Release der neuen MQTT-Spezifikation MQTT 5 zum Beginn des Jahres 2019 wurden neue Features, wie
- User Properties für spezifische MQTT-Pakete,
- die Möglichkeit ein Request-/Response-Verhalten zu definieren,
- die Definition einer Gültigkeitsdauer für Sessions und Nachrichten und
- die Möglichkeit der Definition von Shared Subscription Topics um Client Loadbalancing zu ermöglichen
und viele weitere Neuerungen und Verbesserungen eingeführt. Es gibt eine ausführliche Definition der Protokollspezifikation [2] und einführende Blog-Posts zu MQTT 3 [3] und den neuen Features von MQTT 5 [4].
MQTT-Infrastruktur
So verschieden die Anwendungsszenarien im MQTT-Umfeld auch sind, fast immer lassen sich bestimmte Kernkomponenten identifizieren. Eine MQTT-Infrastruktur besteht aus einer beliebigen Anzahl von MQTT-Clients und einem MQTT-Broker, als Herzstück der Kommunikation auf Basis von MQTT.
- MQTT-Clients sind in der Regel bezüglich Ressourcen limitiert, insbesondere wenn sie in großer Stückzahl kostengünstig verbaut werden müssen. Dies wird vom Protokoll auch durch seine Spezifikation und die Leichtigkeit unterstützt, da es auch für einfache Clients, die in einfachen IoT-Geräten verbaut sind und auch über langsame Netzwerkverbindungen funktionieren muss.
- Der Broker wird in der Regel eine zentrale Komponente der Infrastruktur der IoT-Plattform sein und sollte auch auf allen gängigen Systemumgebungen direkt, in VMs oder auch containerbasiert zu betreiben sein.
Nicht zuletzt will man in einer IoT-Infrastruktur auch immer die Daten sammeln, analysieren und aufbereiten und sie anderen Services verfügbar machen, um daraus weite Informationen und Mehrwert zu ziehen. Dafür ist die Möglichkeit der Anbindung an weitere Systeme notwendig. Im Artikel wird beispielhaft der MQTT Message Broker HiveMQ in der frei verfügbaren Community-Edition verwendet [5].

Containerbasierte Anwendungsplattformen
Containerbasierte Anwendungsplattformen sind auf einem Kern von Anwendungscontainern aufgebaut und bieten zusätzlich Tools für das Erstellen, Bereitstellen und Administrieren der containerbasierten Anwendungen im Cluster. Im Artikel wird beispielhaft die Anwendungsplattform OpenShift Origin (OKD) genutzt [6]. OpenShift Origin, seit August 2018 auch als OKD (Origin Community Distribution) bekannt, ist das Upstream-Community-Projekt. Der gesamt Source-Code für das Origin-Projekt ist auf GitHub verfügbar [6]. OpenShift Origin basiert auf einem Kern der Docker-Container-Packages und des Kubernetes-Container-Cluster-Managements und wird durch die Funktionen des Application-Lifecycle-Managements und der DevOps-Tools erweitert.
- Docker kann de facto als Standard sowohl für die Container-Virtualisierung als auch als für das Paketieren der Anwendungen, Tools und Infrastruktur gesehen werden. Es gilt der Ansatz "Run Anywhere", d. h. Applikationen in Docker-Containern sollten auf allen gängigen Systemen wie OpenShift, Linux, Amazon, Azure u. a. lauffähig sein und das gleiche Verhalten zeigen.
- Kubernetes basiert auf dem Cluster-Management-Tool von Google und bietet Cluster-Management für Docker-Container. Wichtige Funktionen sind hierbei Namensraum-Trennung, automatische Skalierung und Rolling Updates. Wichtige Komponenten sind Pods, Services, Deployments und persistente Volumes. Container werden in Kubernetes in Pods gruppiert und von einem Replication Controller skaliert. Kubernetes bietet die Flexibilität, eine clusterfähige Applikation im Cluster über Knoten in gemischten Infrastrukturen (lokale Server, öffentliche Clouds, mehrere Standorte) aufzubauen.
OKD bietet eine Nutzerverwaltung, Mandantenfähigkeit, Monitoring und Zugriff auf Log-Daten, sowie weitere operative Tools. Es verfügt über ein eigenes SDN, Routing, Loadbalancing, Build- und Deployment-Konfigurationen und eine eigene interne Docker Registry. Projekt-Isolierung (vLANs) und die Autorisierung für die Docker Registry und Logzugriff, sowie die Entfernung der Docker Root Execution gehören zu den Security-Features von OKD.
OpenShift Origin kann als PaaS betrieben werden. Es ermöglicht Continuous Integration, also die Durchführung von automatischen Builds und Deployments getriggert durch Änderungen im Quellcode oder in Konfigurationen. Durch ein Commit in ein Git Repository kann ein vollständiger Delivery-Prozess initiiert werden, um ultimativ Software dem Nutzer zur Verfügung zu stellen. OpenShift bietet – analog zu Kubernetes – ein Command Line Interface (oc) und ein Web Interface zum Management der OpenShift-Objekte.
OKD bündelt die Best Practices der Container-Technologie und bietet Bausteine, um den Deployment-Prozess vom Source Code Repository zum Betreiben der Applikation zu komplettieren.
Der Master Node kümmert sich um die Verwaltung einer oder mehrerer Nodes und stellt das Interface zur Kommunikation via Restfull-API sowohl für die Entwickler-Tools (SCM, CI/CD), als auch die Administration-Tools zur Verfügung. Aufbauend darauf existiert ein Command Line Interface für einfachen Zugriff auf alle OpenShift-Komponenten und deren Administration sowie eine Java-API für den programmatischen Zugriff auf OpenShift-Objekte. Service Layer ermöglichen die Kommunikation zwischen den einzelnen Nodes. Routing Layer ermöglichen die externe Kommunikation via Bereitstellung von Http-Interfaces.
Um mit dem Entwickeln schnell und ohne großen Aufwand loslegen zu können, ohne ein ganzes OpenShift Cluster aufbauen zu müssen, ist es möglich, mit einer lokalen Variante zu starten.
OpenShift stellt eine Version – genannt MiniShift – bereit, die auf der lokalen Umgebung installiert werden kann [7].
Aufbau einer IoT-Anwendung mit einem HiveMQ Cluster in OpenShift
Um den Message Broker im Cluster aufzusetzen, sind folgende Schritte notwendig:
- Bereitstellung eines HiveMQ Image Stream über
- ein Dockerfile, das auf DockerHub gehostet wird,
- ein Dockerfile, das selbst zusammengebaut werden kann und in einem Git Repository konfiguriert wird. - Bauen des Images Stream auf Basis des Dockerfiles und Pushen in das OpenShift Repository.
- Erstellen eines BuildConfig Templates, dass die Strategie festlegt, wie das Image zu bauen ist und wiederholt angewendet werden kann.
- Erstellen eines Deployment Configuration Templates, das folgende Objekte definiert:
- Container mit Replica count
- Liveness-Probe, um die Verfügbarkeit der Anwendung von Intern zu prüfen
- Readiness-Probe, um die Verfügbarkeit der Anwendung von Extern zu prüfen
- Services und deren Ports, für die Clusterkommunikation und den internen und externen Zugriff
- Routen für den Zugriff auf das HiveMQ Control Center (http Access). - Anwendung der Deployment-Konfiguration.
Erzeugung eines eigenen Projektes/ Namespaces
Jede OKD Application wird innerhalb eines Projektes bzw. Namensraumes gehostet, für das entsprechend User- und Administrationsrechte vergeben werden können. Der Message Broker sollte in einem dafür vorgesehenen (eigenen) Namensraum aufgesetzt werden. Dazu legt man via OC oder über das Webinterface von OKD ein eigenes Projekt an.
Listing 1:
oc login <YourOpenShiftDomain> --token='<hidden>'
oc new-project playground 'Playground' 'A Description of the Project'
oc project playground
Templates
Templates sind eine wesentliche Komponenten im OpenShift. Es kann für jedes beliebige Objekt (Build Konfigurationen, Deployments, Services, …) ein Template zur Erzeugung angelegt werden. Templates können entweder in YAML oder JSON geschrieben werden und direkt über die CLI oder auch über den Import im Web Interface erstellt werden. Dabei findet eine Syntaxüberprüfung des Templates statt. Eine Versionierung von Templates ist momentan nicht Bestandteil von OpenShift.
OpenShift selbst stellt eine Reihe von vorgefertigten Templates zum Bauen einer Applikation, wie z. B. für .NET, MySql, NodeJS, WildFly, Jenkins u. a. zur Verfügung.
Build Konfiguration
Um eine eigene Applikation, die nicht auf Basis der im Servicekatalog verfügbaren Templates gebaut werden kann, in OpenShift verfügbar zu machen, muss zunächst ein Build konfiguriert werden, dessen Ergebnis ein Image Stream ist, der im OpenShift abgelegt wird und in der OpenShift eigenen Docker Registry verwaltet wird. Zur Wiederverwendung wird die BuildConfig in einem Template abgelegt.
BuildConfig Templates definieren, neben Namen, Metadaten und Annotation, den Typ sowie den Build-Input, die Build-Strategie, Build-Output und den Build-Trigger.
- Als Build-Inputs sind u.a. Git Repository, Docker File oder existierende Images möglich.
- Build-Strategien können SourceToImage oder DockerStrategy mit den jeweiligen Settings definiert werden.
- Der Build-Output kann ein ImageStreamTag, der in die integrierte OpenShift-Container-Plattform-Registry gepusht wird, oder ein DockerImage sein.
Was passiert beim Aufruf eines Build?
- Ein temporäres Arbeitsverzeichnis wird erstellt und der gesamte Input Content wird in das Arbeitsverzeichnis geladen. Z. B. wird bei einem Git Repository als Input das Git-Repository gecloned.
- Der Build-Prozess wechselt in das Context Directory, soweit definiert.
- Vom Context Directory aus werden je nach Strategie bestimmte Files geparsed und ausgeführt wie z. B. ein Docker File oder eine Maven Pom-File.
- Ein Inline Docker File wird z. B. als temporäres File in das aktuelle Verzeichnis geschrieben. Das Setzen des Context Directory impliziert auch, dass jeglicher Inhalt der außerhalb dieses Verzeichnisses liegt, während des Build-Vorgangs ignoriert wird, bzw. darauf Maven Pomfile nicht zugegriffen werden kann.
- Das Ergebnis des Build-Prozesses wird dann den Output-Settings entsprechend geschrieben.
Im Beispielcode ist der Ausschnitt eines BuildConfig Templates dargestellt. Als Quelle ist direkt der Inhalt eines Dockerfiles gewählt. Wird im Dockerfile keine Registry angegeben, wird direkt auf Dockerhub zugegriffen. Im Beispiel wird direkt das vorgefertigte HiveMQ Image mit Cluster Support geladen.
Als Build Strategy ist Docker gesetzt. Über noCache/forcePull wird sichergestellt, dass keine gecachten Layer genutzt werden und alles vollständig neu gebaut wird.
Ist das Image erstellt, wird es als ImageStreamTag via OpenShift Registry im OpenShift zentral zur Verfügung stellt.
Listing 2:
objects:
- apiVersion: v1
kind: ImageStream
metadata:
name: ${IMAGE_STREAM_NAME}
spec:
tags:
- name: latest
from:
kind: ImageStreamTag
name: ${IMAGE_STREAM_VERSION}
- apiVersion: v1
kind: BuildConfig
metadata:
name: hivemq4-cluster-builder
spec:
runPolicy: Serial
triggers:
- type: ConfigChange
source:
dockerfile: "FROM hivemq/hivemq4:dns-latest"
strategy:
dockerStrategy:
noCache: true
forcePull: true
type: Docker
output:
to:
kind: ImageStreamTag
name: "${IMAGE_STREAM_NAME}:${IMAGE_STREAM_VERSION}"
Die im Template verwendeten Variablen sind als Parameter ebenfalls im Template definiert und können via CLI oder auch im Web-UI von OpenShift beim Aufruf des Templates gesetzt werden
Erzeugen des Templates
Listing 3:
oc create -f ./hivemq4-cluster-docker-buildconfig.yaml -n playground
Nach dem Erzeugen kann das Template über die Web-UI oder CLI ausgeführt werden (s. Abb. 9).
Der Build startet im Anschluss (s. Abb. 10). Das Ergebnis ist ein Image-Stream im OpenShift Repository.
Bauen des MQTT Message Brokers
Um vom Image Stream zu einer lauffähigen Applikation zu kommen, wird eine Deployment-Konfiguration benötigt, die alle notwendigen Objekte für das Deployment eines Message Brokers beinhaltet.
Dazu gehören das Setup für die Container selbst, eine Servicedefinition für die Erreichbarkeit des Brokers via MQTT, eine Servicedefinition für Cluster-Discovery und optional weitere Elemente wie Volumes und Routes.
Deployment-Konfiguration
Die Deployment-Konfiguration beschreibt im Wesentlichen den Container mit den folgenden Informationen:
- Angabe des Images, das im Container genutzt werden soll,
- alle Ports, die nach außen bekannt sein sollen,
- die Ressourcen für CPU und RAM, die dem Container beim Start zugewiesen werden sollten und
- Readiness- und Liveness-Probe, um die Erreichbarkeit von außen und innen von OKD verifizieren zu können.
Sowie
- die Strategy für ein Rolling-Upgrade.
- die Policy, wie auf das Cluster zugegriffen werden soll und
- die Anzahl an Replicas, mit denen das Deployment gestartet werden soll.
Listing 4:
- kind: DeploymentConfig
apiVersion: v1
metadata:
name: "${HIVEMQ_DEPLOYMENT_NAME}"
annotations:
template.alpha.openshift.io/wait-for-ready: 'true'
spec:
strategy:
type: Rolling
rollingParams:
updatePeriodSeconds: 30
intervalSeconds: 10
timeoutSeconds: 1800
maxSurge: 1
maxUnavailable: 0
triggers:
- type: ConfigChange
replicas: 1
selector:
name: "${HIVEMQ_DEPLOYMENT_NAME}"
template:
spec:
containers:
- name: hivemq-plain
image: "${IMAGE_NAME}"
ports:
- containerPort: 1883
protocol: TCP
name: mqtt
resources:
requests:
cpu: "${RES_REQ_CPU}"
memory: "${RES_REQ_MEM}"
readinessProbe:
httpGet:
path: "/api/status"
port: 8080
initialDelaySeconds: 30
timeoutSeconds: 30
livenessProbe:
timeoutSeconds: 40
initialDelaySeconds: 30
periodSeconds: 300
successThreshold: 1
tcpSocket:
port: 1883
env:
…
volumeMounts:
…
terminationMessagePath: "/dev/termination-log"
imagePullPolicy: Always
capabilities: {}
securityContext:
capabilities: {}
privileged: false
volumes:
…
restartPolicy: Always
dnsPolicy: ClusterFirst
Da ein clusterfähiger Message Broker mit Persistenz-Schicht, ähnlich einer Stateful-Applikation bezüglich Skalierung und Migrations-Szenarien behandelt werden sollte, sind die Zeiten (timeoutSeconds, initialDelaySeconds) für das Erreichen eines neu deployten Containers von außen (Readiness-Probe) und der Erreichbarkeit innerhalb des Clusters (Lifeness-Probe) relativ großzügig zu setzen.
Der Grund dafür liegt darin, dass der Message Broker im Falle eines Hoch- oder Runter-Skalierens das gesamte Cluster neu aufbauen und Daten der MQTT Clients wie
- Retained Messages,
- Subskription der persistenten Session Clients,
- alle Queued Messages von persistenten Session Clients und
- den aktuellen Status jeder individuellen Nachrichtenübertragung, die noch nicht vollständig abgeschlossen wurde
neu im Cluster verteilen muss.
Die Parameter für die Deployment-Konfiguration werden auch hier wieder über Umgebungsvariablen gesetzt, sodass diese beim Ausführen des Templates via OC oder in der Web-UI angegeben werden können.
Services
Um den Message Broker für MQTT Clients erreichbar zu machen, müssen die TCP-Ports die für die MQTT Connection notwendig sind, nach außen bekannt gegeben und verfügbar gemacht werden. Die MQTT Ports können nicht wie bei typischen http-basierten Web-Applikation automatisch über eine Route konfiguriert werden, da MQTT nicht auf http basiert sondert auf TCP.
Um von extern auf den Broker in der PaaS zugreifen zu können, wird im gleichen OpenShift Projectspace ein Service-Objekt konfiguriert, welches die internen TCP Ports des Message Brokers für die Nutzung von MQTT auf NodePorts mapped, die von außen erreichbar sind. Mit NodePort: 0 wird beim Anwenden der Service-Objekt-Definition automatisch ein NodePort zugewiesen.
Neben den MQTT Ports wird zusätzlich ein Port für eine Websockets-Verbindung und ein Port für die Kommunikation zum Web-Interface, welches zum Monitoren des Brokers dient, konfiguriert. Für den HTTP-Zugriff wird der Service als Typ Loadbalancer mit sessionAffinity ClientIP (Sticky Session) definiert. Dies ist notwendig, um im Cluster auf das http-basierte Webinterface des Message Brokers zugreifen zu können, ohne die Session zu verlieren.
Listing 5:
- kind: Service
apiVersion: v1
metadata:
name: "HIVEMQ-service"
annotations:
service.spec.externalTrafficPolicy: Local
spec:
ports:
- name: client
protocol: TCP
port: 1883
targetPort: 1883
nodePort: 0
- name: web-sockets
port: 8883
targetPort: 8883
- name: control-center
port: 8080
targetPort: 8080
type: LoadBalancer
sessionAffinity: ClientIP
Clusterfähigkeit
Um den Broker im Cluster betreiben zu können, wird der built-in DNS von OpenShift genutzt. Dazu muss die Discovery-Adresse des Services definiert werden, die alle Pods an den Service bindet. Der Servicename muss dem Muster
<service>.<pod_namespace>.svc.cluster.local
entsprechen.
Dazu wird ein zweites Service-Objekt gebaut. Die DNS-Struktur im Service wird als headless services definiert, indem die ClusterIP auf None gesetzt wird. Um dieses Feature von OpenShift zu nutzen, wurde der Message Broker mit der DNS-Extension deployed, die Cluster discovery via DNS unterstützt. (Docker-Image von dockerhub: FROM hivemq/hivemq4:dns-latest)
Listing 6:
- kind: Service
apiVersion: v1
metadata:
name: "${HIVEMQ_DEPLOYMENT_NAME}-discovery"
annotations:
template.openshift.io/tolerate-unready-endpoints: "true"
spec:
clusterIP: None
selector:
name: "${HIVEMQ_DEPLOYMENT_NAME}"
ports:
- name: client
port: 1883
protocol: TCP
targetPort: 1883
Loadbalancing
Um von außen via MQTT auf beliebige Nodes des Brokers zugreifen zu können, wäre TCP Loadbalancing notwendig. Dies wird aktuell nicht ohne weiteres im OKD zur Verfügung gestellt, kann aber mit Administrationsaufwand konfiguriert werden. OpenShift bringt nur HTTP Loadbalancing mit. HTTP Loadbalancing wird im Beispiel für den Zugriff auf das Webinterface des Brokers genutzt.
Weitere OpenShift-Objekte, die für den vollständigen Aufbau des Message Brokers notwendig sind, sind eine Route, für die Bereitstellung einer URL zum Zugriff auf das Web Interface und Volumes zur Definition und Zuweisung von Directories und Zugriffspfaden.
Nach dem Ausführen der Deployment-Konfiguration haben wir eine vollständige HiveMQ-Applikation im OpenShift zur Verfügung. Im Beispiel (Abb. 12) ist der Message Broker im Projektspace deployed und auf 3 Instanzen skaliert. In der Detailansicht eines Pods kann auf die Logs zugegriffen werden. Diese zeigen, wie sich nach dem Hochskalieren das 3-Node-Cluster geformt hat.
MQTT Clients verbinden
Um einen MQTT Client mit dem Broker zu verbinden, muss die Broker-Adresse inklusive eines NodePorts für den Aufbau der MQTT Connection genutzt werden (s. Abb. 14). Im Beispiel wird ein externer MQTT Client verwendet, der nicht im OpenShift als Applikation gehostet ist. Dafür wird das Tool MQTTfx genutzt, um den MQTT Client zu connecten.
Der Port 32635 entspricht hier im Beispiel dem NodePort, der über den Service für den internen MQTT Port 1883 von OpenShift zur Verfügung gestellt wird. Die Brokeradresse kann ein beliebiger Node (Pod) aus dem Cluster sein.
Ist der MQTT Client als Anwendung innerhalb von OpenShift gehostet, ist die Anbindung entsprechend einfacher, da auf die internen Ports und IP-Adressen zugegriffen werden kann.
CI/CD und Test mit OpenShift-Pipelines
Mit der OKD Container-Plattform und Jenkins können wir CI/CD-Build-Pipelines erstellen, bei denen die Tasks der Pipeline in temporären Containern ausgeführt werden, die on Demand gestartet werden. Mit diesem Stack können auch ganze Umgebungen für Integrationstests dynamisch erstellt werden. So kann eine Containeranwendungsplattform genutzt werden, um die Ressourcennutzung zu optimieren und den Aufwand für die Aufrechterhaltung permanenter Testumgebungen zu vermeiden.
Um Build-Pipelines nutzen zu können, muss zunächst Jenkins, das im OpenShift-Standard-Katalog verfügbar ist, aufgesetzt werden. OpenShift-Pipelines sind eine Kombination aus der Jenkins-Pipeline-Build-Strategie, den Jenkins-Files und einer OpenShift DSL, die vom OpenShift-Jenkins-Client-Plugin bereitgestellt wird, das im Jenkins-Image enthalten ist. Für Testzwecke reicht es, das Jenkins Ephemeral-Template zur Installation zu verwenden.
Nach der Installation steht Jenkins zur Verfügung. Im Build ist auch die Erzeugung einer Route enthalten, die eine URL auf Jenkins erzeugt. Über den Link kann direkt in die Jenkins-Umgebung gewechselt werden. Um Java-basierte Tests auszuführen wird im Beispiel Maven3 verwendet. Je nach Jenkins-Version ist noch die Installation dieses und weiterer Jenkins-Plugins notwendig.
Dazu gehören
- die Maven-Integration-Pipeline und
- das Cleanup-Workspace-Plugin.
Beide Plugins werden im Jenkins direkt über das Management-Interface nachinstalliert.
Aufbau einer Pipeline
OpenShift stellt einen Strategietyp Jenkins-Pipeline für das Objekt BuildConfig zur Verfügung und integriert den Status der Pipeline-Builds in die OpenShift-Webkonsole, sodass der gesamte Applikation-Lebenszyklus in einer Ansicht sichtbar wird.
Im Beispiel wird eine Pipeline aufgebaut, die nach einer Änderung eines Images die Applikation neu ausrollt, startet, verifiziert und im Anschluss einige Tests ausführt.
Zunächst wird die BuildConfig für die HiveMQ-Build-Pipeline aufgebaut. Dazu wird das Git-Repository referenziert, in dem das dafür notwendige Jenkins-File abgelegt ist. Der Trigger für die Anwendung der Pipeline sowie die Build-Strategie-Jenkins-Pipeline wird festgelegt.
Listing 7:
objects:
- apiVersion: v1
kind: BuildConfig
metadata:
name: hivemq-test-pipeline
spec:
runPolicy: Serial
triggers:
- type: ConfigChange
# trigger the pipeline on image change
- type: ImageChange
imageChange:
from:
kind: ImageStreamTag
name: "hivemq4-custom:1.0"
source:
git:
uri: ${GIT_REPOSITORY}
ref: ${GIT_REF}
sourceSecret:
name: ${GIT_SECRET}
strategy:
jenkinsPipelineStrategy:
jenkinsfilePath: ${GIT_JENKINSFILE}
type: JenkinsPipeline
Die Parameter werden auch hier wieder über Umgebungsvariablen gesetzt. Ist das Git-Repository nicht öffentlich, kann der Zugriff über ein Public Key autorisiert werden. Dazu muss ein Schlüsselpaar erzeugt werden, bei dem der Public Key als Deploykey im Repository abgelegt und der Private Key im OpenShift als Secret-Objekt hinterlegt wird. In der BuildConfig für die Pipeline wird dann nur auf das Secret referenziert. Nach Anwendung der BuildConfig steht ein neues Objekt im Project-Space zur Verfügung.
Jenkins-File
Das Jenkins-File kann direkt inline in der BuildConfig definiert oder über eine Pfad-/Dateiangabe (jenkinsfilePath) referenziert werden. Als Skriptsprache wird groovy benutzt. Es können beliebig viele Stages beschrieben werden (z. B. cleanup, create, build, deploy, verify), die nacheinander abgearbeitet werden. Innerhalb der Stages kann auf die OpenShift-DSL zurückgegriffen und OpenShift-Operationen durchgeführt werden, wie z. B. das Auslösen und Verifizieren eines Deployments. Weiterhin wird eine Fehlerbehandlung zur Verfügung gestellt, um den Buildvorgang über die Pipeline entsprechend zu verifizieren und zu steuern.
Listing 8: Beispiel für ein Inline Jenkins-File (Auszug)
spec:
strategy:
jenkinsPipelineStrategy:
jenkinsfile: |-
try {
timeout(time: 20, unit: 'MINUTES') {
openshift.withCluster() {
openshift.withProject() {
def bld = openshift.startBuild('${NAME}')
bld.untilEach {
return it.object().status.phase == "Running"
}
bld.logs('-f')
}}
openshift.withCluster() {
openshift.withProject() {
def dc = openshift.selector('dc', '${NAME}')
dc.rollout().latest()
}}}
}
} catch (err) {
echo "Caught: ${err}"
currentBuild.result = 'FAILURE'
throw err
}
HiveMQ Jenkins-File – Beispiel
Eine andere Möglichkeit, eine Pipeline einzubinden, besteht darin, auf ein in einem entfernten Git-Repository gespeichertes File über den JenkinsfilePath in der Jenkins-Strategie zu referenzieren. Als Auslöser eines neuen Pipeline-Builds ist eine Änderung des HiveMQ-Images, auf dem der Build basiert definiert. Als Stages für die Pipeline sind die Schritte Build, Deploy, Verify, Prepare Tests und Tests definiert.
Build
Im Build Stage wird via OpenShift-DSL ein OpenShift-Build unter Angabe des Namens der Konfiguration durchgeführt. Es wird maximal 30 Minuten gewartet, die Logs werden in der Jenkins-Konsole dargestellt.
Listing 9:
stage('Build') {
steps {
openshiftBuild bldCfg: 'hivemq4-test-builder',
namespace: 'playground',
showBuildLogs: 'true',
verbose: 'true',
waitTime: '30',
waitUnit: 'min'
}
}
Deploy
OpenShift-DSL verfügt über die Möglichkeit, Operationen über das gesamte Cluster auszuführen. Im Beispiel wird für jeden Knoten im Cluster der entsprechende Deployment-Selector gesucht, der das HiveMQ-Deployment definiert (Variable dcName) und das Deployment ausgeführt. Wird der Selector nicht gefunden, wird ein Fehler ausgegeben.
Listing 10:
stage('Deploy') {
steps {
script {
openshift.logLevel(6)
openshift.withCluster() {
openshift.withProject(namespace) {
def dcSelector = openshift.selector("dc", [app: app]);
def dcMetadata = dcSelector.object().metadata;
if (dcMetadata.name == dcName) {
echo "Using Deployment ${dcMetadata.name} "
openshiftDeploy(depCfg: dcName);
} else {
error "No Deployment Config found for ${dcMetadata.name}";
}
}
}
}
sh 'echo Deployment successful'
}
}
Verify
In diesem Stage wird über alle Clusternodes geprüft, ob das Deployment vollständig war und auch der Service erzeugt wurde. Dazu werden die folgenden OpenShift-DSL-Methoden genutzt:
- openshiftVerifyDeployment(depCfg: dcName);
- openshiftVerifyService(svcName: service);
Prepare Tests
Um das Deployment zu testen, müssen zunächst alle TCP-Ports des neuen Deployments ermittelt werden. In einem weiteren Schritt müssen die aktuellen IP-Addressen der zum Deployent gehörenden Pods ermittelt werden. Neben den neu deployten Pods existieren unter Umständen auch noch die terminierten Pods des vorherigen Deployments. Diese müssen vom Test ausgeschlossen werden, da sie nicht mehr erreichbar sind.
Listing 11:
stage('PrepareTest') {
//...
def svcSelector = openshift.selector("svc", [app: app])
def svcPorts = svcSelector.object().spec.ports;
for (int i = 0; i < svcPorts.size(); i++) {
if (svcPorts[i].name == "client") {
HIVEMQ_TCP_PORT = svcPorts[i].port
} //...
}
if (HIVEMQ_TCP_PORT == 0 ) {
error "Found neither the TCP port! Please check if the Service is created and the right label is set."
}
def dcSelector = openshift.selector("dc", [app: app]);
def latestVersion = dcSelector.object().status.latestVersion;
podName = podName + "-${latestVersion}";
def podSelector = openshift.selector("pod", [deployment: podName]);
def podList = podSelector.objects()
int hasAddress =0;
for (int i = 0; i < podList.size(); i++) {
def containerStatuses = podList[i].status.containerStatuses;
Boolean ready = containerStatuses[0].ready;
def lastState = containerStatuses[0].lastState;
if(ready && !( "${lastState}".contains("terminated")) ) {
if (hasAddress != 0 ) {
HIVEMQ_ADDRESS = HIVEMQ_ADDRESS + "," + podList[i].status.podIP;
echo "Address(es): ${HIVEMQ_ADDRESS}";
} //...
}
}
//...
}
Tests
Mit den Werten für TCP-Port und IP-Adresse aus dem vorherigen Prepare-Stage können die Tests gestartet werden. Diese sind ebenfalls aus dem Git-Repository in den aktuellen Arbeitsbereich geladen. Zum Test gehört einen Maven-File. Dieses wird unter Verwendung des Maven-Plugins für Jenkins aufgerufen.
Es können beliebige Tests gestartet werden, z. B. ein funktionaler Test und ein Skalierungstest.
Wird der Stage erfolgreich durchlaufen, ist die Pipeline insgesamt erfolgreich.
Listing 12:
stage('Test') {
steps {
withMaven (
maven: 'Maven3'
) {
script {
env.PATH = "${tool 'Maven3'}/bin:${env.PATH}"
def additionalParameters = "-Dgroups='com.hivemq.listener.TcpTest"
if ("$HIVEMQ_WS_PORT" != "") {
additionalParameters += ",com.hivemq.listener.WsTest"
}
sh "mvn -f smoketests/pom.xml -e clean test " +
"-DHIVEMQ_ADDRESS=${HIVEMQ_ADDRESS} " +
"-DHIVEMQ_TCP_PORT=${HIVEMQ_TCP_PORT} " +
"-DHIVEMQ_WS_PORT=${HIVEMQ_WS_PORT} " +
"${additionalParameters}"
sh "mvn -f smoketests/pom.xml -e clean test " +
"-DHIVEMQ_ADDRESS=${HIVEMQ_ADDRESS} " +
"-DHIVEMQ_TCP_PORT=${HIVEMQ_TCP_PORT} " +
"-Dgroups='com.hivemq.scale.ScaleTest'"
}
}
}
}
Die Ansicht der Pipeline, der Verlauf, sowie eine Historie aller Pipeline-Builds ist in der OpenShift-Webkonsole verfügbar.
Über die Historie der Builds kann der Verlauf inklusive Details aller Builds angeschaut werden.
Fazit
PaaS wie OpenShift sind nicht nur für das Management von Infrastruktur geeignet, sondern auch für den Einsatz von Continious Integration. Dies trifft nicht nur für die Entwicklung klassischer Web-Anwendungen zu, sondern auch für den Aufbau und die Entwicklung von IoT-Anwendungen. Bis auf wenige Komponenten (TCP Loadbalancer) sind die Elemente, die für eine clusterfähige IoT-Infrastruktur notwendig sind, mit den Boardmitteln von OKD ohne Administrationsaufwand verfügbar und anwendbar. Damit lässt sich auch ein clusterfähiger MQTT-Broker aufbauen und betreiben.
Gerade im Projektbereich ist es sinnvoll, die Ressourcennutzung zu optimieren und den Aufwand für die Aufrechterhaltung permanenter Testumgebungen zu vermeiden, wie es mit den OKD-Jenkins-Pipelines möglich ist.