Wir haben kürzlich Integrationen mit der OpenSSF Scorecard auf der OpenSauced-Plattform veröffentlicht. Die OpenSSF Scorecard ist eine leistungsstarke Go-Befehlszeilenschnittstelle, die jeder verwenden kann, um die Sicherheitslage seiner Projekte und Abhängigkeiten zu verstehen. Es führt mehrere Prüfungen auf gefährliche Arbeitsabläufe, CICD-Best Practices, ob das Projekt noch gepflegt wird und vieles mehr durch. Dies ermöglicht es Softwareentwicklern und Verbrauchern, ihr allgemeines Sicherheitsbild zu verstehen, abzuleiten, ob ein Projekt sicher zu verwenden ist und wo Verbesserungen an den Sicherheitspraktiken vorgenommen werden müssen.
Aber eines unserer Ziele bei der Integration der OpenSSF Scorecard in die OpenSauced-Plattform bestand darin, diese dem breiteren Open-Source-Ökosystem insgesamt zur Verfügung zu stellen. Wenn es sich um ein Repository auf GitHub handelt, wollten wir eine Bewertung dafür anzeigen können. Dies bedeutete, die Scorecard-CLI so zu skalieren, dass sie auf nahezu jedes Repository auf GitHub abzielt. Viel leichter gesagt als getan!
Lassen Sie uns in diesem Blogbeitrag näher darauf eingehen, wie wir das mit Kubernetes gemacht haben und welche technischen Entscheidungen wir bei der Implementierung dieser Integration getroffen haben.
Wir wussten, dass wir einen Microservice vom Typ Cron erstellen mussten, der die Ergebnisse in einer Vielzahl von Repositorys regelmäßig aktualisiert: Die eigentliche Frage war, wie wir das machen würden. Es würde keinen Sinn machen, die Scorecard-CLI ad hoc auszuführen: Die Plattform könnte zu leicht überlastet werden und wir wollten in der Lage sein, eine tiefergehende Analyse der Scores im gesamten Open-Source-Ökosystem durchzuführen, auch wenn die OpenSauced-Repo-Seite dies nicht getan hat vor Kurzem besucht. Zunächst haben wir uns mit der Verwendung der Scorecard Go-Bibliothek als direkt abhängigem Code und der Ausführung von Scorecard-Prüfungen innerhalb eines einzigen, monolithischen Mikroservices beschäftigt. Wir haben auch darüber nachgedacht, serverlose Jobs zu verwenden, um einmalige Scorecard-Container auszuführen, die die Ergebnisse für einzelne Repositorys zurückgeben würden.
Der Ansatz, den wir letztendlich gewählt haben und der Einfachheit, Flexibilität und Leistung vereint, besteht darin, Kubernetes-Jobs in großem Maßstab zu nutzen, alles verwaltet von einem „Scheduler“-Kubernetes-Controller-Microservice. Anstatt eine tiefere Code-Integration mit der Scorecard aufzubauen, bietet uns die Ausführung eines einzelnen Kubernetes-Jobs die gleichen Vorteile wie ein serverloser Ansatz, allerdings mit geringeren Kosten, da wir alles direkt auf unserem Kubernetes-Cluster verwalten. Jobs bieten auch viel Flexibilität bei der Ausführung: Sie können lange, längere Zeitüberschreitungen haben, sie können Festplatten verwenden und wie bei jedem anderen Kubernetes-Paradigma können mehrere Pods verschiedene Aufgaben ausführen.
Lassen Sie uns die einzelnen Komponenten dieses Systems aufschlüsseln und sehen, wie sie im Detail funktionieren:
Der erste und größte Teil dieses Systems ist der „Scorecard-K8s-Scheduler“; ein Kubernetes-Controller-ähnlicher Mikrodienst, der neue Jobs im Cluster startet. Obwohl dieser Microservice vielen Prinzipien, Mustern und Methoden folgt, die beim Aufbau eines herkömmlichen Kubernetes-Controllers oder -Operators verwendet werden, überwacht er nicht nach benutzerdefinierten Ressourcen im Cluster und verändert diese auch nicht. Seine Funktion besteht darin, einfach Kubernetes-Jobs zu starten, die die Scorecard-CLI ausführen und fertige Jobergebnisse sammeln.
Sehen wir uns zunächst den Hauptregelkreis im Go-Code an. Dieser Microservice verwendet die Kubernetes Client-Go-Bibliothek, um direkt mit dem Cluster zu kommunizieren, auf dem der Microservice ausgeführt wird: Dies wird oft als On-Cluster-Konfiguration und Client bezeichnet. Innerhalb des Codes fragen wir nach dem Bootstrapping des On-Cluster-Clients nach Repositorys in unserer Datenbank ab, die aktualisiert werden müssen. Sobald einige Repos gefunden sind, starten wir Kubernetes-Jobs in einzelnen Worker-„Threads“, die auf den Abschluss jedes Jobs warten.
// buffered channel, sort of like semaphores, for threaded working sem := make(chan bool, numConcurrentJobs) // continuous control loop for { // blocks on getting semaphore off buffered channel sem <- true go func() { // release the hold on the channel for this Go routine when done defer func() { <-sem }() // grab repo needing update, start scorecard Kubernetes Job on-cluster, // wait for results, etc. etc. // sleep the configured amount of time to relieve backpressure time.Sleep(backoff) }() }
Diese Methode der „unendlichen Regelschleife“ mit einem gepufferten Kanal ist eine gängige Methode in Go, um kontinuierlich etwas zu tun, aber nur eine konfigurierte Anzahl von Threads zu verwenden. Die Anzahl gleichzeitiger Go-Funktionen, die zu einem bestimmten Zeitpunkt ausgeführt werden, hängt davon ab, welchen konfigurierten Wert die Variable „numConcurrentJobs“ hat. Dadurch wird der gepufferte Kanal so eingerichtet, dass er als Worker-Pool oder Semaphor fungiert, was die Anzahl gleichzeitiger Go-Funktionen angibt, die zu einem bestimmten Zeitpunkt ausgeführt werden. Da es sich bei dem gepufferten Kanal um eine gemeinsam genutzte Ressource handelt, die alle Threads nutzen und prüfen können, stelle ich mir das oft als Semaphor vor: eine Ressource, ähnlich einem Mutex, auf die mehrere Threads zugreifen und auf die sie zugreifen können. In unserer Produktionsumgebung haben wir die Anzahl der Threads in diesem Scheduler skaliert, die alle gleichzeitig ausgeführt werden. Da der eigentliche Planer nicht sehr rechenintensiv ist und nur Jobs anstößt und darauf wartet, dass schließlich Ergebnisse angezeigt werden, können wir die Grenzen dessen, was dieser Planer bewältigen kann, ausreizen. Wir verfügen außerdem über ein eingebautes Backoff-System, das bei Bedarf versucht, den Druck zu entlasten: Dieses System erhöht den konfigurierten „Backoff“-Wert, wenn Fehler auftreten oder keine Repos gefunden werden, für die die Punktzahl berechnet werden kann. Dadurch wird sichergestellt, dass wir unsere Datenbank nicht ständig mit Abfragen überlasten und der Scorecard-Scheduler selbst im „Wartezustand“ bleiben kann, ohne wertvolle Rechenressourcen im Cluster zu beanspruchen.
Innerhalb der Regelschleife führen wir einige Dinge aus: Zuerst fragen wir unsere Datenbank nach Repositories ab, deren Scorecard aktualisiert werden muss. Dabei handelt es sich um eine einfache Datenbankabfrage, die auf einigen Zeitstempel-Metadaten basiert, nach denen wir Ausschau halten und für die wir Indizes haben. Sobald eine konfigurierte Zeitspanne seit der Berechnung des letzten Punktestands für ein Repo verstrichen ist, wird dieser angezeigt und von einem Kubernetes-Job verarbeitet, der die Scorecard-CLI ausführt.
Als nächstes starten wir, sobald wir ein Repo haben, für das wir die Punktzahl erhalten können, einen Kubernetes-Job mit dem Bild „gcr.io/openssf/scorecard“. Das Bootstrapping dieses Jobs im Go-Code mithilfe von Client-Go sieht sehr ähnlich aus wie mit Yaml, wobei lediglich die verschiedenen Bibliotheken und APIs verwendet werden, die über „k8s.io“-Importe verfügbar sind, und dies programmgesteuert durchgeführt wird:
// defines the Kubernetes Job and its spec job := &batchv1.Job{ // structs and details for the actual Job // including metav1.ObjectMeta and batchv1.JobSpec } // create the actual Job on cluster // using the in-cluster config and client return s.clientset.BatchV1().Jobs(ScorecardNamespace).Create(ctx, job, metav1.CreateOptions{})
Nachdem der Job erstellt wurde, warten wir darauf, dass er signalisiert, dass er abgeschlossen wurde oder ein Fehler aufgetreten ist. Ähnlich wie bei kubectl bietet Client-Go eine hilfreiche Möglichkeit, Ressourcen zu „beobachten“ und ihren Zustand zu beobachten, wenn sie sich ändern:
// watch selector for the job name on cluster watch, err := s.clientset.BatchV1().Jobs(ScorecardNamespace).Watch(ctx, metav1.ListOptions{ FieldSelector: "metadata.name=" + jobName, }) // continuously pop off the watch results channel for job status for event := range watch.ResultChan() { // wait for job success, error, or other states }
Sobald wir den Job erfolgreich abgeschlossen haben, können wir schließlich die Ergebnisse aus den Pod-Protokollen des Jobs abrufen, die die tatsächlichen JSON-Ergebnisse aus der Scorecard-CLI enthalten! Sobald wir diese Ergebnisse haben, können wir die Ergebnisse wieder in die Datenbank einfügen und alle erforderlichen Metadaten ändern, um unseren anderen Microservices oder der OpenSauced-API zu signalisieren, dass es ein neues Ergebnis gibt!
Wie bereits erwähnt, kann der Scorecard-k8s-Scheduler eine beliebige Anzahl gleichzeitiger Jobs gleichzeitig ausführen: In unserer Produktionsumgebung laufen eine große Anzahl gleichzeitiger Jobs, die alle von diesem Microservice verwaltet werden. Ziel ist es, die Ergebnisse alle zwei Wochen in allen Repositories auf GitHub aktualisieren zu können. Mit dieser Größenordnung hoffen wir, jedem Open-Source-Betreuer oder Verbraucher leistungsstarke Tools und Einblicke bieten zu können!
Der „Scheduler“-Microservice ist letztendlich ein kleiner Teil dieses gesamten Systems: Jeder, der mit Kubernetes-Controllern vertraut ist, weiß, dass es zusätzliche Teile der Kubernetes-Infrastruktur gibt, die erforderlich sind, damit das System funktioniert. In unserem Fall benötigten wir eine rollenbasierte Zugriffskontrolle (RBAC), damit unser Microservice Jobs im Cluster erstellen kann.
Zuerst benötigen wir ein Dienstkonto: Dies ist das Konto, das vom Planer verwendet wird und an das Zugriffskontrollen gebunden sind:
apiVersion: v1 kind: ServiceAccount metadata: name: scorecard-sa namespace: scorecard-ns
Wir platzieren dieses Dienstkonto in unserem „scorecard-ns“-Namespace, in dem alles läuft.
Als nächstes benötigen wir eine Rolle und Rollenbindung für das Dienstkonto. Dazu gehören die eigentlichen Zugriffskontrollen (einschließlich der Möglichkeit, Jobs zu erstellen, Pod-Protokolle anzuzeigen usw.)
apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: scorecard-scheduler-role namespace: scorecard-ns rules: - apiGroups: ["batch"] resources: ["jobs"] verbs: ["create", "delete", "get", "list", "watch", "patch", "update"] - apiGroups: [""] resources: ["pods", "pods/log"] verbs: ["get", "list", "watch"] — apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: scorecard-scheduler-role-binding namespace: scorecard-ns subjects: - kind: ServiceAccount name: scorecard-sa namespace: scorecard-ns roleRef: kind: Role name: scorecard-scheduler-role apiGroup: rbac.authorization.k8s.io
Sie fragen sich vielleicht: „Warum muss ich diesem Dienstkonto Zugriff gewähren, um Pods und Pod-Protokolle zu erhalten?“ Ist das nicht eine Überausweitung der Zugangskontrollen?“ Erinnern! Jobs haben Pods und um die Pod-Protokolle zu erhalten, die die tatsächlichen Ergebnisse der Scorecard-CLI enthalten, müssen wir in der Lage sein, die Pods eines Jobs aufzulisten und dann ihre Protokolle zu lesen!
Im zweiten Teil davon, dem „RoleBinding“, fügen wir die Rolle tatsächlich dem Dienstkonto hinzu. Dieses Dienstkonto kann dann verwendet werden, wenn neue Jobs im Cluster gestartet werden.
—
Ein großes Lob an Alex Ellis und seinen hervorragenden Run-Job-Controller: Das war eine große Inspiration und Referenz für die korrekte Verwendung von Client-Go mit Jobs!
Bleiben Sie alle frech!
Das obige ist der detaillierte Inhalt vonWie wir Kubernetes-Jobs verwenden, um OpenSSF Scorecard zu skalieren. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!