Bei Thought Machine entwickeln wir eine Cloud-native Kernbankenplattform namens Vault Core. Zwei unserer Hauptanforderungen sind:
- die Fähigkeit, Millionen von Nutzern und Tausende von Transaktionen pro Sekunde zu verarbeiten, und
- die Bereitstellung auf einer Vielzahl verschiedener Cloud-Anbieter und in eigenen Rechenzentren
Darüber hinaus möchten wir unseren Code so schnell wie möglich bereitstellen können und gleichzeitig die Möglichkeit haben, bei Problemen ein Rollback durchzuführen.
In diesem Artikel beschreiben wir drei Hauptkomponenten unserer Infrastruktur: die CI/CD-Pipeline, die eine Entwicklung mit hoher Geschwindigkeit ermöglicht; das zugrunde liegende Betriebssystem und den Netzwerk-Stack, die Thought Machine Vault Core unterstützen; sowie die Tools, auf die Entwickler zugreifen können, um ihre Dienste zu überwachen.
Einleitung
Bis November 2016 haben wir Microservices mithilfe von CloudFormation und Auto-Scaling-Gruppen auf AWS EC2-Instanzen bereitgestellt. Wir entwickelten schlanke Wrapper um Troposphere und ermöglichten es Entwicklern, ihre eigenen Load Balancer, Sicherheitsgruppen, Startkonfigurationen und mehr einzurichten. Unsere Infrastruktur war als Code definiert, wir konnten automatisierte Tests vor der Bereitstellung durchführen, sie durchlief denselben Code-Review-Prozess wie alles andere und wir konnten unsere Infrastruktur in Modulen organisieren, um Wiederholungen zu vermeiden. Zur Laufzeit luden kleine Puppet-Module dann die erforderlichen Artefakte auf die Maschinen herunter und konfigurierten sie für den Betrieb.
Das funktionierte eine Zeit lang gut, hatte aber auch einige Nachteile.
- Da wir CloudFormation verwendeten, waren wir vollständig an das AWS-Ökosystem gebunden. Unsere Microservices konnten ohne eine umfassende Neuentwicklung nicht auf anderen Cloud-Anbietern bereitgestellt werden.
- VMs benötigten beim Ausrollen von Updates für unsere Microservices viel Zeit zum Starten (etwa 5 Minuten).
- Wir konnten bei einem Ausfall eines Microservices nicht eindeutig die genaue Ursache bestimmen. War der Fehler durch Änderungen an einer Sicherheitsgruppe verursacht worden oder gab es schlicht einen Fehler im Microservice?
- Unsere Puppet-Module befanden sich in einem separaten Repository, was eine zusätzliche Hürde für Entwickler darstellte, die die Laufzeitkonfiguration ihrer Microservices aktualisieren mussten.
Es war an der Zeit, sich nach etwas Neuem umzusehen. Einige der Eigenschaften, nach denen wir suchten, waren:
- Entwickler sollen in die Lage versetzt werden, ihre Deployments und Rollouts eigenständig zu verwalten. Das Infrastruktur-Team sollte kein Flaschenhals für alltägliche Aufgaben sein (z. B. das Ausrollen einer neuen Anwendungsversion oder das Hinzufügen eines komplett neuen Microservices). Produktteams sollten in der Lage sein, vom Prototyp bis zur Produktion ohne Beteiligung des Infrastruktur-Teams zu arbeiten.
- Klare Trennung zwischen Microservice-Deployments und Infrastrukturänderungen. Dies würde es uns ermöglichen, bei Fehlern leicht nachzuvollziehen, wer verantwortlich war.
- Feingranulare Authentifizierung und Autorisierung, Zugriffskontrolle auf Anwendungsebene und der Wegfall der Notwendigkeit für Entwickler, direkt auf Konsolen, APIs oder Benutzeroberflächen der Cloud-Anbieter zuzugreifen.
- Speicherung der Deployment-Konfiguration direkt beim Anwendungscode, um das Springen zwischen verschiedenen Repositories bei Änderungen zu vermeiden.
- Schnelle Startzeiten beim Ausrollen von Updates für unsere Microservices.
- Keine Notwendigkeit mehr, sich per SSH auf VMs einzuloggen, um Fehler zu beheben. Logs und weitere nützliche Informationen sollten für alle Entwickler sofort verfügbar sein, wenn etwas schiefgeht.
Hier kommt Kubernetes ins Spiel.
Kubernetes ermöglichte es uns, die Verantwortlichkeiten zwischen Produkt- und Ops-Teams klar zu definieren. Es lieferte die notwendigen Grundbausteine – wie Services, Deployments und ConfigMaps –, um unsere Software schnell und vorhersehbar auszurollen. Die Produktteams konnten ihre eigenen Deployments verwalten, während die Ops-Teams die notwendigen Monitoring-Dienste bereitstellten, um einen reibungslosen Ablauf der Deployments zu gewährleisten.
Der gesamte Code bei Thought Machine liegt in einem einzigen Monorepo, in dem wir Code in verschiedenen Sprachen (hauptsächlich Python, Go und Java) sowie unsere Kubernetes-Ressourcendateien und Infrastruktur-Code pflegen. Dies ermöglicht es dem Infrastruktur-Team, umfassende Änderungen systemweit auf atomare Weise durchzuführen. Früher haben wir ein separates Repository für Puppet-Module gepflegt, aber die Koordination von Änderungen und Deployments über mehrere Repositories hinweg war schwierig und fehleranfällig.
Wir haben eine strikte Code-Review-Richtlinie, bei der jede Änderung von mindestens einer Person überprüft werden muss (je nach Sensibilität des Codes auch von einem Senior Developer) – eine Richtlinie, die wir von Anfang an durchgesetzt haben. Es hat sich als hervorragende Methode erwiesen, um andere weiterzubilden, Entwürfe zu diskutieren oder andere Entwickler einfach über aktuelle Entwicklungen zu informieren. Sobald ein PR erstellt wird, werden Ihre Änderungen von einem Build-Agent erfasst und alle betroffenen Tests im Hintergrund ausgeführt (dafür nutzen wir spezialisierte On-Premise-Server).
Sobald Ihr Branch in den Master-Branch gemergt wurde, ermitteln wir die betroffenen Docker-Images, bauen sie neu und versehen sie mit einem eindeutigen Hash, der auf dem Inhalt des Images basiert. Anschließend fahren wir mit der Templatierung unserer Kubernetes-Objektdefinitionen fort (siehe unten).
Da wir Docker-Labels als unveränderlich betrachten, wäre es mühsam, unsere Deployment-YAML-Dateien bei jeder Image-Änderung manuell zu aktualisieren. Stattdessen fügen wir diese Werte am Ende des Builds per Template ein. Ein Beispiel für ein committetes Deployment könnte so aussehen:
apiVersion: apps/v1
kind: Deployment
metadata:
name: service-deployment
labels:
app: my-service
spec:
[...]
containers:
- name: service-container
image: //backend/docker:service
- name: sidecar
image: //common/util:sidecar
Die mit // gekennzeichneten Image-Tags beziehen sich auf spezifische Build-Targets (erstellt mit plz) innerhalb unseres Repositories. Jedes Target verfügt über zugehörige Metadaten (wie Image-Name und Quellen), die in der BUILD-Datei des jeweiligen Verzeichnisses definiert sind.
Sobald der Build abgeschlossen ist und alle Tests erfolgreich durchlaufen wurden, kennen wir die genauen Image-Tags und können sie per Templating einfügen. Die resultierende Spezifikation könnte dann etwa so aussehen:
containers:
- name: service-container
image: 331510202792.dkr.ecr.us-west-1.amazonaws.com/backend/service:b71f1
- name: sidecar
image: 331510202792.dkr.ecr.us-west-1.amazonaws.com/common/sidecar:43a95
Diese Kubernetes-Objekte werden anschließend automatisch in unseren initialen Entwicklerumgebungen bereitgestellt.
Kubernetes Deployments
Betriebssystem und Netzwerk-Stack
Wir betreiben unsere Kubernetes-Cluster derzeit auf AWS, experimentieren jedoch aktiv mit anderen Cloud-Anbietern wie GCP. CoreOS Container Linux (das Betriebssystem, auf dem alle unsere EC2-Instanzen laufen) gibt uns die Sicherheit, dass alle aktuellen Sicherheits-Patches sofort nach ihrem Erscheinen angewendet werden (Patches werden automatisch installiert und VMs ohne menschliches Eingreifen neu gestartet). Beim Start der VM wird eine grundlegende Konfiguration angewendet und das Kubelet als systemd-Unit ausgeführt. Alles Weitere läuft auf Kubernetes.
Wir verwenden Flannel zur Einrichtung unseres Layer-3-Pod-Netzwerks und Calico zur Durchsetzung von Netzwerkrichtlinien. Beide werden im selben DaemonSet bereitgestellt, wodurch sichergestellt ist, dass auf jeder VM ein Replikat läuft.
Flannel ist dafür verantwortlich, jedem Pod beim Start automatisch eine eindeutige IP-Adresse zuzuweisen. Wenn ein neuer Worker-Node zum Cluster hinzugefügt wird, reserviert Flannel einen Teil des clusterweiten Pod-Netzwerks und erneuert dessen Lease regelmäßig, um Konflikte mit anderen Nodes zu vermeiden. Diese Pod-IPs sind bei der Abfrage von Pods über kubectl sichtbar.
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
nginx-5vrpq 2/2 Running 0 11d 10.XXX.14.3 ip-10-XXX-72-162.eu-west-1.compute.internal
nginx-7j72k 2/2 Running 0 12d 10.XXX.12.2 ip-10-XXX-150-131.eu-west-1.compute.internal
nginx-q2f5j 2/2 Running 0 12d 10.XXX.11.19 ip-10-XXX-1-39.eu-west-1.compute.internal
Standardmäßig können alle Pods in einem Cluster miteinander kommunizieren. Dies wirft einige Sicherheitsbedenken auf; wir möchten beispielsweise den öffentlichen ausgehenden Netzwerkverkehr aller Pods einschränken (außer bei Edge-Pods oder Webservern) oder hochsensible Pods vom Rest des Clusters isolieren. Wir können dieses Problem durch den Einsatz von Network Policieslösen. Calico läuft parallel zu Flannel und ermöglicht es uns, Netzwerkrichtlinien festzulegen, um den Datenverkehr zwischen verschiedenen Pods zu begrenzen. Diese Netzwerkrichtlinien basieren auf abstrakten Konzepten wie Pod-Labels, was bedeutet, dass Produktteams ihre eigenen Richtlinien mit derselben Sprache schreiben können, die sie auch für ihre Deployment-Dateien verwenden. Calico läuft als privilegierter Container, überwacht Änderungen an den Netzwerkrichtlinien und schreibt die iptables-Regeln entsprechend um (in der Praxis haben wir festgestellt, dass Aktualisierungen sehr schnell, innerhalb von 1 bis 2 Sekunden, angewendet werden).
Clusterweite Dienste
Obwohl die Produktteams die volle Verantwortung für ihre Deployments und Rollouts tragen, stellt das Infrastruktur-Team eine Reihe von clusterweiten Diensten bereit, die jedes Deployment bei der Ausführung im Cluster automatisch erhält. Dazu gehören DNS-Auflösung, Monitoring und Alerting, Logging, Secret-Management sowie ein internes Tool für das Release-Management.
Monitoring und Alerting
Zur Erfassung von Metriken verwenden wir Prometheus, ein Open-Source-Tool, das ursprünglich bei SoundCloud entwickelt wurde. Es nutzt ein Pull-Modell, um Metriken von verschiedenen Endpunkten (normalerweise spezifische Pods im Cluster) abzurufen, wodurch Sie diese Zeitreihendaten speichern und abfragen können (meist unter Verwendung eines Frontends wie Grafana). Unser Monitoring-System auf Clusterebene stellt grundlegende Metriken für alle Pods bereit (wobei Entwickler dazu ermutigt werden, auch benutzerdefinierte Geschäftslogik-Metriken bereitzustellen, die ebenfalls automatisch erfasst werden). Hierfür setzen wir das node-exporter DaemonSet ein, das Metriken für jeden Pod auf dem jeweiligen Knoten bereitstellt: CPU- und Speicherauslastung, Festplatten- und Netzwerk-I/O usw. Alarme (unter Verwendung des Prometheus Alert Managers) werden ausgelöst, sobald bestimmte Bedingungen erfüllt sind (z. B. wenn die Anzahl der Replikate für einen Dienst unter einen bestimmten Schwellenwert fällt oder die Latenz für eine SQL-Abfrage zu hoch ist).
Wir sind schnell an die Grenzen einer Prometheus-Einzelinstanz gestoßen und haben die Metriken seitdem über mehrere Instanzen hinweg föderiert. Dies hat es uns ermöglicht, auf viele tausend Metriken zu skalieren und alles von der CPU-Auslastung bis hin zu individuellen SQL-Abfragelatenzen oder RPCs zwischen Diensten zu erfassen.
Logging
Ähnlich wie bei unserem Monitoring-Setup setzen wir ein Fluentd DaemonSet ein, das für das Sammeln der Logs aller auf einem Knoten laufenden Pods verantwortlich ist. Log-Nachrichten werden gebündelt und an einen ElasticSearch-Cluster weitergeleitet. Kibana wird anschließend verwendet, um die Log-Nachrichten zu durchsuchen und anzuzeigen.
Nach Tests mit einem einfachen ElasticSearch-Setup haben wir festgestellt, dass es selbst bei einer geringen Menge an Log-Nachrichten schnell überlastet sein kann. Unser aktueller Cluster umfasst drei verschiedene Arten von Pods: Daten-Pods, die für die Speicherung von ES-Indizes verantwortlich sind (bereitgestellt als StatefulSet), API-Pods zur Abwicklung von Abfragen an und vom ES-Cluster sowie Master-Pods zur Koordination des gesamten Prozesses (beide als Standard-Deployments bereitgestellt).
Secrets
Wie bei jeder Cloud-Anwendung müssen Microservices Zugriff auf sensible Informationen wie Datenbankpasswörter, API-Schlüssel usw. haben. Diese Werte werden niemals im Code-Repository gespeichert; für die lokale Entwicklung werden Platzhalter verwendet.
Um sicherzustellen, dass diese Secrets kryptografisch sicher gespeichert werden, nutzen wir HashiCorp Vault und dessen Kubernetes Auth-Backend. Entwickler können die Secrets, auf die sie Zugriff benötigen, als gemountete Volumes in ihrem Deployment angeben. Diese werden zur Laufzeit automatisch von einem Init-Container abgerufen, der das Service-Konto des Deployments zur Authentifizierung bei HashiCorp Vault verwendet. Dies ermöglicht uns eine fein abgestimmte Richtliniensteuerung darüber, auf welche Secrets jeder Microservice zugreifen kann, stellt sicher, dass Token nur kurzlebig sind, und erlaubt das Rotieren von Secrets bei Bedarf. Ungewöhnliche Aktivitäten können dank detaillierter Audit-Protokolle leicht identifiziert werden.
Wir verwenden HashiCorp Vault auch für interne oder Unternehmensdienste. Unser Hauptanwendungsfall hierbei ist die Ausstellung von kurzlebigen (~16h) Zertifikate für den Zugriff auf unsere Kubernetes-Cluster. Entwickler melden sich in der Regel einmal täglich an (unter Verwendung ihrer LDAP-Anmeldedaten), um Zugriff auf den Cluster zu erhalten. Diese Zertifikate sind mit RBAC-Rollenverknüpft, wodurch wir Berechtigungen für verschiedene Teams oder Organisationen einfach aktualisieren können. Bei Bedarf können Entwickler auch Zertifikate mit höheren Privilegien anfordern, die jedoch nur 30 bis 60 Minuten gültig sind.
Fazit
Wie bei allem gibt es auch beim Deployment-Management keine Patentlösungen, und Kubernetes bildet da keine Ausnahme. Das Endergebnis mit Kubernetes war positiv – und wir haben nicht vor, davon abzurücken –, aber es war mit viel Arbeit verbunden. Wir haben viele neue Technologien eingeführt, die eine ganz andere Terminologie verwendeten, als wir es gewohnt waren. Dies erforderte eine Umschulung nicht nur des Infrastrukturteams, sondern auch der Produktteams, da sich die Zuständigkeit für bestimmte Komponenten verschob. Die zusätzlichen Abstraktionsebenen (insbesondere auf Netzwerkebene) machen das Debugging in manchen Situationen schwieriger als gewohnt.
Wir sind mit unserem aktuellen Kubernetes-Setup zufrieden, haben aber noch viel Arbeit vor uns: die Ausweitung unserer Deployments auf andere Cloud-Anbieter wie GCP und Azure (sowie On-Premise, speziell für unsere Build-Agents und interne Infrastruktur), die Untersuchung von Service Meshes (wie Istio oder Conduit), um die Verbindungen zwischen Microservices zu verbessern, und die Vereinfachung unseres Deployment-Prozesses.
Thought Machine stellt für alle Bereiche ein: Backend, Frontend, Infrastruktur, Sicherheit, PMs, Vertrieb usw. Wenn das für dich interessant klingt und du dich bewerben möchtest, besuche bitte Workable.







