1. Docker - Kommunikation und Cloud-Integration von Containern

1.1. Einführung

Docker ist eine Plattform, die es ermöglicht, Anwendungen in Containern zu erstellen, zu verteilen und auszuführen. Ein Container ist eine Einheit von Software, die Code und alle seine Abhängigkeiten enthält, so dass die Anwendung schnell und zuverlässig auf jedem Rechner ausgeführt werden kann. Docker hilft außerdem, die Entwicklungs- und Betriebsabläufe zu automatisieren, so dass Anwendungen schneller geliefert werden können.

1.2. Docker vs Virtualisierung

Mit einer virtuellen Maschine können beliebig viele Betriebssysteme auf einem Host-System ausgeführt werden. Im Gegensatz zu einer virtuellen Maschine, die ein vollständiges Betriebssystem enthält, enthält ein Docker-Container nur die Anwendung und alle ihre Abhängigkeiten.

1.3. Setup für den Rechner

Für das Installieren von Docker, verweisen wir auf die offizielle Seite von Docker: Installationsanleitung. Bitte beachten Sie die Systemvoraussetzungen und die Installationsanweisungen für Ihr Betriebssystem.

1.4. Erstellen eines Docker-Images

Beim Erstellen eines Docker-Images wird eine Dockerfile verwendet, um die Anwendung und ihre Abhängigkeiten zu definieren. Ein Dockerfile ist eine Textdatei, die eine Reihe von Schritten enthält, die Docker ausführen muss, um das Docker-Image zu erstellen. Die Kommandos sind fast identisch mit den Befehlen, die in Linux-Shell-Skripten verwendet werden.

Eine Dockerfile kann wie folgt aussehen:

# Spezifizieren der Basis
FROM python:3.8

# Kommandos werden relativ zu diesem Pfad ausgeführt
WORKDIR /app

# Kopieren von Dateien aus dem Host Rechner zum Working Directory
COPY main.py .
COPY requirements.txt .

# Ausführen von Kommandos, während dem Build Prozesses
RUN pip install --no-cache-dir -r requirements.txt

# Falls ein Container von dieser Image ausgeführt wird, dann wird CMD ausgeführt
CMD ["python", "main.py"]

Damit diese Dockerfile funktioniert, muss sie sich im gleichen Verzeichnis wie die Dateien main.py und requirements.txt befinden. Die main.py Datei ist das Hauptprogramm, das ausgeführt wird, wenn der Container gestartet wird. Die requirements.txt Datei enthält alle Abhängigkeiten, die für das Ausführen des Programms erforderlich sind.

Ein Beispiel für die main.py Datei könnte wie folgt aussehen:

import requests

def main():
    try:
        # Anfragen an Google senden
        response = requests.get('http://www.google.com')
        print("Response from Google:", response.status_code)
    except requests.exceptions.RequestException as e:
        print("Error:", e)

if __name__ == "__main__":
    main()

Dieses Programm sendet eine simple Anfrage an die Google-Homepage und gibt den Statuscode der Antwort aus.

Mithilfe vom Befehl

docker build -t python-docker .

wird das Docker-Image erstellt. Der Parameter -t wird verwendet, um dem Image einen Namen zu geben. Der Punkt am Ende des Befehls gibt an, dass das Dockerfile im aktuellen Verzeichnis liegt.

Durch den Befehl

docker images

kann eingesehen werden, ob das Image erfolgreich erstellt wurde.

Um den Container zu starten, wird der Befehl

docker run python-docker

verwendet. Nun sollte die Ausgabe des Programms auf der Konsole erscheinen.

Dieses Verfahren wird nun so oft angewendet, bis die gewünschte Anzahl an Containern erstellt wurden.

1.5. Erstellen eines Docker-Netzwerks

Um ein eigenes Docker Netzwerk zu erstellen, wird der Befehl

docker network create docker-network

verwendet. Wobei docker-network der Name des Netzwerks ist.

In diesem Beispiel wird nun der vorher erstellte Container angepasst, ein weiterer Container erstellt und beide in das Netzwerk eingebunden. Da die Dockerfiles immer das Prinzip verfolgen, werden diese in dieser Dokumentation nicht nochmals aufgeführt. Sie können jedoch im Repository eingesehen werden. Das gleiche gilt für die Logik der Source-Dateien, da diese als Grundlage angenommen werden.

Die angepasste main.py Datei sieht wie folgt aus:

import requests

def main():
    try:
        # Anfragen an NodeDocker senden
        headers = {"X-Container-Name": "PythonDocker"}

        response = requests.get('http://node-docker-container:3000', headers=headers)
        print("Response from NodeDocker (http://node-docker-container:3000):", response.text)
    except requests.exceptions.RequestException as e:
        print("Error:", e)

if __name__ == "__main__":
    main()

Der neu Dazugekommene NodeDocker beinhaltet folgende server.js Datei:

const http = require("http");

const server = http.createServer((req, res) => {
// Dymincally getting the hostname of the container
const hostname = process.env.HOSTNAME;
// Checking the hostname of the requester
const requesterHostname = req.headers['x-container-name'];
console.log(`Received request from Docker container: ${requesterHostname}`);


// Sending response
res.statusCode = 200;
res.setHeader("Content-Type", "text/plain");
res.end("Hello from NodeDocker\n");
});

const PORT = 3000;
server.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}/`);
});

Da somit das Netzwerk und beide Container strukturen stehen, können nun diese Container erstellt und gestartet werden.

Zum erstellen der Container wird der bereits bekannte Befehl

docker build -t node-docker .
docker build -t python-docker .

verwendet. Bitte beachten Sie, dass sie sich im richtigen Verzeichnis befinden.

Beim starten der Container, in der richtigen Reihenfolge, wird ebenfalls ein bereits bekannte Befehl verwendet. Jedoch mit 2 weiteren Parametern:

docker run --name node-docker-container --network docker-network -d node-docker
docker run --name python-docker-container --network docker-network -d python-docker

Der Parameter –name wird verwendet, um dem Container einen Namen zu geben. Der Parameter –network wird verwendet, um den Container in das zuvor erstellte Netzwerk einzubinden. Der Parameter -d wird verwendet, um den Container im Hintergrund laufen zu lassen.

Da wir mit dem Parameter -d den Container im Hintergrund laufen lassen, können wir die Ausgabe des Programms nicht sehen. Um die Ausgabe zu sehen, können wir den Befehl

docker logs python-docker-container
docker logs node-docker-container

verwenden. Nun sollte in den Logs des Pyhton-Dockers der Text „Response from NodeDocker (http://node-docker-container:3000): Hello from NodeDocker“ erscheinen. In den Logs von Node-Docker sollte der Text „Received request from Docker container: PythonDocker“ erscheinen.

Somit haben wir erfolgreich ein Netzwerk und zwei Container erstellt, welche miteinander in 2 verschiedene Sprachen kommunizieren können.

1.6. Überlick des Netzwerks

Das Netzwerk besteht aus 4 Dockern:

  • HTTP Server (Node Server)

  • Python Application

  • C# Console Application

  • Cloud Application

HTTP Server

Node Server der HTTP Requests von den anderen Docker entgegennimmt und eine Response zurückschickt. Er ist über http://node-docker-container:3000 erreichbar.

Python Application

Einfache Application die jeweils ein HTTP Request an den Node Server + Cloud Server stellt und auf die responses wartet.

C# Console Application

Console Application im .NET 7.0 Framework. Diese stellt ebenfalls jeweils ein HTTP Request an den Node Server + Cloud Server und wartet auf die responses.

Cloud Application

Fast identisch zum HTTP-Node Server der HTTP Requests von den anderen Docker Containern entgegennimmt und eine Response zurückschickt. Der Unterschied ist das dieser Container auf einer Google Cloud Instanz läuft, heißt er ist nicht in dem selben lokalen Netzwerk von den anderen Containern. Er ist über https://nodecloudiiiii-nss7c5v2aa-uc.a.run.app erreichbar.

Informationen zu den Dockerfiles + Source Code befinden sich im Repository. In den jeweiligen Files sind die Vorgangsschritte kommentiert.

1.7. Den Cloud Docker Container einrichten

  1. Als erstes muss das Image gebuildet werden

docker build -t cloud-docker .

Zu beachten ist hier, dass man sich im Verzeichnis /src/CloudDocker befinden muss.

  1. Als nächstes muss ein Tag für das erstellte Image erstellt werden um es auf DockerHub pushen zu können.

docker tag cloud-docker <username>/cloud-docker:latest
  1. Nun kann das Image gepusht werden.

docker push <username>/cloud-docker:latest
  1. Als nächstes wird auf Google Cloud Run ein Projekt angelegt und innerhalb dieses Projekts ein Service konfiguriert.

Wichtig: Der CloudDocker muss auf Port 8080 hören, das ist der Standart Port auf Google Cloud. Wenn nicht, dann muss der Standart Port auf Google Cloud noch geändert werden.

In der Service Konfiguration muss das hochgepushte Image verlinkt werden: <username>/cloud-docker:latest

Nun sollte der Container in der Google Cloud laufen und erreichbar sein.

1.8. Das Netzwerk aufbauen

  1. Als erstes müssen alle Images gebuildet werden

docker build -t node-docker .
docker build -t python-docker .
docker build -t csharp-docker .

Zu beachten ist hier, dass Sie sich immer im Verzeichnis des jeweiligen Dockerfiles befinden müssen.

  1. Als nächstes muss das Netzwerk erstellt werden in dem sich zukünftig die Container befinden.

docker network create docker-network
  1. Nun müssen die Container erstellt werden die sich dann im zuvor erstellten Netzwerk befinden.

docker run --name node-docker-container --network docker-network -d node-docker
docker run --name python-docker-container --network docker-network -d python-docker
docker run --name csharp-docker-container --network docker-network -d csharp-docker
  1. Um zu überprüfen ob alles geklappt hat, einfach die jeweiligen Logs auslesen

docker logs python-docker-container
docker logs node-docker-container
docker logs csharp-docker-container

1.9. Quellen

https://medium.com/@jaydeepvpatil225/containerize-the-net-core-7-console-application-using-docker-86e537321592

https://docs.docker.com/desktop/