Das erste Mal, dass ich von Containern gehört habe, war 2018, als ein Kollege uns eine Präsentation darüber zeigte, was sie sind und wie sie funktionieren. Mein einziger Gedanke war: "Cool, das sind doch nur überkomplizierte virtuelle Maschinen (VMs). Anyways..." Es ist peinlich, wie naiv und falsch ich war.
Ich war noch neu in der Entwicklung von software und dieses Thema lag außerhalb dessen, was mich interessierte, nämlich Fehlerkorrekturen und einige kleinere Produktfunktionen, an denen ich zu dieser Zeit arbeitete. Wer konnte schon ahnen, dass wir 4 Jahre später von Containern hören würden, und dass ich die Federführung bei der Umsetzung übernehmen würde.
Aber Joe, warum all diese Zeit und Mühe aufwenden, um einen Monolithen zu containerisieren, der bereits stabil und zuverlässig ist?
Nun, liebe Leser, unsere Gründe sind vielleicht nicht die gleichen wie Ihre, aber wir haben uns aus Gründen der Modernisierung, der Skalierbarkeit und der Senkung der Infrastrukturkosten für diesen Schritt entschieden. Unsere Anwendung, die auf .NET Framework lief und auf Active Directory (AD) basierte, entsprach nicht mehr den Anforderungen der Kunden, die OAuth, plattformübergreifende Unterstützung und mehr Skalierbarkeit wünschten. Containerisierte Webanwendungen können je nach Bedarf skaliert werden, und die Infrastrukturkosten für das Hinzufügen weiterer Container betragen nur einen Bruchteil dessen, was die Einrichtung weiterer Windows-VMs und die Beibehaltung einer kompletten AD-Umgebung kosten würde.

Wir hatten eine Herausforderung vor uns: Unser Flaggschiff, die Lösung zur Automatisierung des Lebenszyklus von Zertifikaten, Keyfactor Command , war zu diesem Zeitpunkt ein .NET Framework-Monolith, der sich stark auf AD und Internet Information Services (IIS) stützte. Außerdem verwendete sie einen Microsoft Software Installer (MSI) und eine reine Windows-GUI für die Installation/Konfiguration. All das in einen Windows-Container zu packen und damit aufzuhören, war keine realistische Option.
Windows-Container sind zwar eine Sache, aber sie waren nie Industriestandard, und wir wollten uns nicht noch mehr in das Windows-Ökosystem einbinden. Windows Containers hätte als vorübergehender Schritt auf dem Weg funktioniert, aber wie es ein Kollege einmal so weise formulierte: "Es gibt nichts Dauerhafteres als eine vorübergehende Lösung." Es war an der Zeit, Cross-Plattform ernst zu nehmen.
Wir haben uns einige hochgesteckte Ziele gesetzt:
- Die Plattform Keyfactor Command kann in einer Kubernetes-Container-Umgebung gebootet und ausgeführt werden.
- Verwandte Komponenten (die "Arbeitsbienen"), einschließlich Orchestrator und CA Gateways, werden ebenfalls in Containern untergebracht, so dass die gesamte Produktsuite in derselben Umgebung ausgeführt werden kann.
- Aufrechterhaltung der Abwärtskompatibilität aller Produktfunktionen.
- Die AD-Authentifizierung funktioniert weiterhin für Kunden, die sie derzeit nutzen
- Windows-basierte Installationen funktionieren weiterhin und ohne Unterbrechungen
Großartig! Aber wie kommen wir dahin? Als wir uns auf den Weg zur Containerisierung machten, wurde unsere Liste der zu erledigenden Aufgaben immer länger....
Zunächst muss die Anwendung plattformübergreifend ausgeführt werden können, also mussten wir von .NET Framework auf .NET 8 umstellen. Ein Teil dieser Migration würde beinhalten, dass wir uns nicht mehr auf IIS und AD für die Authentifizierung verlassen, also müssen wir OAuth und OIDC-Unterstützung auf die Liste setzen. Außerdem mussten wir die .NET-Laufzeitkonvertierung und OAuth für alle Produktkomponenten implementieren, nicht nur für die Plattform selbst. Habe ich schon erwähnt, dass Sie alles unter der Haube, das auf Windows basiert, durch etwas völlig anderes ersetzen müssen? Unterm Strich kommt dies einer Neuprogrammierung so nahe, wie es nur möglich ist, ohne eine Neuprogrammierung vorzunehmen. Trotzdem gaben wir dem Projekt den Codenamen "Songbird" und machten uns an die Arbeit.
Die Voraussetzungen
Als wir mit Songbird begannen, waren wir bereits auf dem Weg zu einem anderen Projekt, das wir intern "AD-Unabhängigkeit" nannten. Dabei ging es darum, unsere enge Kopplung mit AD aufzubrechen und schließlich als Endziel OAuth-Unterstützung in das Produkt aufzunehmen. Großartig, das ist also in Arbeit, und zufälligerweise wurde die .NET-Konvertierung zu einer Voraussetzung für die AD-Unabhängigkeit. Also, das Wichtigste zuerst, Zeit, die .NET-Konvertierung in Angriff zu nehmen.
Für diejenigen, die mit .NET nicht so vertraut sind, ist die Konvertierung von Framework zu .NET eine Achterbahnfahrt aus Trivialitäten und kompletten Neuschriften. Während 95 % Ihrer Codebasis die Umstellung 1:1 überstehen, muss insbesondere der Code für den Anwendungsstart komplett umgeschrieben werden. Dann müssen Sie das für jede Anwendung tun. Die Plattform Command ist eine Kombination aus 7 Anwendungen, die alle zusammenarbeiten: Fünf davon sind ASP.NET-Websites, eine ein Windows-Dienst und eine weitere die Konsolen-Konfigurationsanwendung.
Die Konvertierung der ASP.NET-Websites war sicherlich die größte Herausforderung, da die gesamte ASP.NET-Pipeline vom Empfang einer Anfrage bis zu dem Punkt, an dem unsere Controller-Schicht empfängt, völlig anders ist. Nicht nur der Code für den Start der Anwendung musste überarbeitet werden, sondern auch ganze Teile der grundlegenden Webanwendungen, wie Autorisierung, Anforderungsprotokollierung und Fehlerbehandlung, mussten überarbeitet werden, um besser in die neue ASP.NET-Form zu passen.
Ein weiterer Stolperstein bei der Konvertierung in .NET ist unser Konfigurationsassistent. Heute ist dies eine WPF-basierte Anwendung, die selbst mit einer .NET-Laufzeitumgebung nicht plattformübergreifend ist. Wir müssten auf ein anderes UI-Framework wie MAUI oder Avalonia umsteigen, aber was hätten wir davon? Die Möglichkeit, Command auf einer Linux-Box zu installieren und etwas wie NGINX als IIS-Ersatz zu verwenden, stand nicht auf der Liste der Endziele, so dass wir diese Idee schließlich verwarfen.
Stattdessen haben wir einen Großteil unserer Kernkonfiguration in eine gemeinsame Bibliothek für mehrere Zielgruppen verschoben und eine neue Konsolenanwendung erstellt, die die Konfigurationsfunktionen eines Assistenten nachahmt. Diese neue Anwendung, das Datenbank-Upgrade-Tool, wäre der Bootstrapper für die Datenbank im Container-Einsatzfall. Von einem Container aus kann man sowieso keine GUI verwenden :).
Nachdem nun alles auf .NET umgestellt ist, kommen wir zum Problem der Authentifizierung (AuthN) und Autorisierung (AuthZ). Bisher haben wir uns auf IIS verlassen, um die Authentifizierung über Basic oder Kerberos auth durchzuführen, und ASP.NET hat dann eine Benutzeridentität zur Verfügung gestellt. Dieses Thema verdient einen eigenen Beitrag, um es zu vertiefen, aber im Wesentlichen mussten wir die ASP.NET-Middleware verwenden, damit sowohl die AD-basierte IIS-Authentifizierung als auch der OAuth-Authentifizierungscode und die Client-Zugangsdatenströme weiterhin funktionieren. Außerdem musste das Sicherheitsmodell überarbeitet werden, damit es auf Ansprüchen basiert, anstatt Anwendungsrollen und Berechtigungen auf AD-Benutzer und -Gruppen abzubilden.
Zeit für die Herstellung von Containern
Es hat lange gedauert, aber jetzt sind wir endlich da und können damit beginnen, die Teile zusammenzusetzen. Um unsere Builds nicht zu kompliziert zu machen, haben wir uns entschieden, unsere Dockerdateien so schlank wie möglich zu gestalten und bereits erstellte Binärdateien in die Images zu kopieren, anstatt den gesamten Quellcode in den Container-Images neu zu erstellen.
Sobald wir die Images hatten, mussten sie irgendwo ausgeführt werden. Zu Beginn der Entwicklung waren dies lokale Instanzen von Docker Compose mit einer Menge benutzerdefinierter Konfigurationen und einem Reverse-Proxy. Wir erkannten schnell, dass dies nicht skalierbar und in der Produktion nicht zu unterstützen war, da es einfach zu viele Konfigurationseinstellungen und Container gab, die aufeinander abgestimmt werden mussten, damit alles wie erwartet funktionierte. Außerdem hatten wir uns zuvor das Ziel gesetzt, die Anwendung in Kubernetes (K8s) auszuführen.
Wir haben uns für einen anderen Ansatz entschieden und sind im Container-Ökosystem eine Ebene höher gegangen, um mit Kubernetes (K8s) und Helm zu orchestrieren. Mit Helm konnten wir ein Diagramm mit einem gemeinsamen Satz von Konfigurationswerten definieren, das die gesamte Arbeit des korrekten Einbindens für Sie übernimmt. Wir haben kurz darüber nachgedacht, einen K8s-Operator zu schreiben, der praktisch dasselbe tut, uns aber dagegen entschieden, da dies mehr Code erfordert, der gewartet werden muss, während Helm flache Konfigurationsvorlagen sind, die genau das widerspiegeln, was in K8s bereitgestellt wird.
Wir haben viel Zeit darauf verwendet, dieses Chart so benutzerfreundlich wie möglich zu gestalten. Da eine unserer Anforderungen die Aufrechterhaltung der Abwärtskompatibilität war, musste Keyfactor Command weiterhin über unser MSI auf Windows installiert werden können, und wir wollten die Lernkurve für die Verwendung dieser alternativen Installationsmethode niedrig halten. Auch wenn das Helm-Diagramm in der Produktion wahrscheinlich von automatisierten Pipelines genutzt wird, war es uns wichtig, dass interne Nutzer, die sich mit Containern nicht so gut auskennen, auf einen Cluster verwiesen werden können, eine Wertedatei erhalten, in der sie die wichtigen Bits ausfüllen müssen, und dann eine "Helm-Installation" ausführen können, und zwei Minuten später können sie sich bei ihrer Command -Instanz anmelden und mit der Nutzung beginnen. Gleichzeitig haben wir, obwohl der Standardfall in der Tabelle so einfach wie möglich ist, genügend Konfigurationsmöglichkeiten vorgesehen, damit die Anwendung an jeden Bedarf angepasst werden kann.

Unsere letzte Herausforderung war die Einschränkung, dass unsere Datenbank erstellt und konfiguriert werden muss, bevor die Anwendungen selbst erfolgreich gestartet werden können. In den alten Windows-Umgebungen wird die Datenbankkonfiguration als Teil des Installationsprozesses durchgeführt, zusammen mit den zugehörigen Webdiensten und der IIS-Konfiguration. K8s ist sehr deklarativ, Sie sagen ihm, dass es einige Anwendungen starten soll, und es tut dies. Verstehen Sie das Problem? Wenn wir die Webservices und einen Job zur Konfiguration der Datenbank gleichzeitig starten, ist ein Fehler vorprogrammiert. Da wir versuchen, die Anwendung so einfach wie möglich zu gestalten, blieb uns nur die Möglichkeit, dies in das Diagramm einzubauen, was schwieriger ist, als es vielleicht aussieht.
Wie K8s versucht auch Helm, so deklarativ wie möglich zu funktionieren. Wir haben jedoch einige Optionen, um diesen Startablauf zu erreichen:
- Stellen Sie alles zusammen bereit, verwenden Sie Init-Container, um die Datenbank abzufragen, warten Sie auf den Abschluss eines Konfigurationsauftrags und starten Sie dann die Webservices.
- Verwenden Sie Helm-Hooks, um die Einsätze so zu gewichten, dass der Konfigurationsauftrag ausgeführt wird, bevor die Webservices vorhanden sind, und stellen Sie die Webservices bereit, sobald sie fertig sind.
Wir haben viel Zeit damit verbracht, diese Optionen abzuwägen, da jede ihre Vor- und Nachteile hatte und es keine eindeutig bessere Option gab. Option zwei hat sich letztendlich durchgesetzt, da wir Bedenken wegen der Rennbedingungen hatten, die mit Option eins möglich wären. Die Haken selbst haben einige Nachteile, nämlich dass Helm im Falle eines Fehlers bei der Installation die Hakenressourcen nicht bereinigt und Sie dies von Hand tun müssen. In der Produktion ist dies nicht wirklich ein Problem, aber während der Entwicklungstests war dies ein ständiger Schmerzpunkt, der uns viele Male fast zur Option der Init-Container zurückgebracht hat.
Das Endprodukt
Und das war's. Jetzt, da wir das Playbook im Kopf hatten, mussten wir nur noch ein ähnliches Konzept auf die anderen Komponenten anwenden. Begrüßen Sie die moderne Cloud und verabschieden Sie sich von Ihren alten Windows-Servern. Damit haben wir nun Keyfactor Command Version 24.4 mit Containerisierung für Kunden verfügbar und unsere hochgesteckten Ziele endlich erreicht.
Am Ende konnten wir unser Ziel erreichen, eine robuste containerisierte Bereitstellung von Command mit Kubernetes und Helm zu erstellen und gleichzeitig eine MSI-basierte Windows-Installation beizubehalten. Obwohl es eine Menge Arbeit mit zahlreichen komplexen Schritten war, erfüllt das Ergebnis erfolgreich alle unsere Anforderungen. Wir haben auf diesem Weg viel gelernt und sehen nun ein Meer von Möglichkeiten, unsere Anwendung noch Cloud-nativer und noch besser für unsere Kunden zu machen.
Dies war unser erster Keyfactor Entwicklungsblog, und wenn er Ihnen gefallen hat, würden wir uns über Ihr Feedback freuen, damit wir Sie in Zukunft auch hinter die Kulissen anderer Projekte führen können. Verbinden Sie sich mit mir auf LinkedIn und schicken Sie mir eine Nachricht, wenn Ihnen das Gelesene gefallen hat, und bleiben Sie dran für ähnliche Entwicklungsblogs in der Zukunft.