Kapitel 4. Der Kubernetes API Server

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Wie in der Übersicht über die Kubernetes-Komponenten erwähnt, ist der API-Server das Tor zum Kubernetes-Cluster. Er ist die zentrale Anlaufstelle, auf die alle Nutzer, Automatisierungen und Komponenten im Kubernetes-Cluster zugreifen. Der API-Server implementiert eine RESTful-API über HTTP, führt alle API-Vorgänge durch und ist für die Speicherung von API-Objekten in einem Backend mit persistenter Speicherung zuständig. In diesem Kapitel werden die Einzelheiten dieses Vorgangs erläutert.

Grundlegende Merkmale für die Verwaltbarkeit

Trotz seiner Komplexität ist der Kubernetes-API-Server vom Standpunkt der Verwaltung aus gesehen eigentlich relativ einfach zu verwalten. Da der gesamte dauerhafte Status des API-Servers in einer Datenbank gespeichert wird, die sich außerhalb des API-Servers befindet, ist der Server selbst zustandslos und kann repliziert werden, um die Last der Anfragen zu bewältigen und Fehlertoleranz zu gewährleisten. In einem hochverfügbaren Cluster wird der API-Server in der Regel dreimal repliziert.

Der API-Server kann ziemlich gesprächig sein, wenn es um die Protokolle geht, die er ausgibt. Er gibt für jede Anfrage, die er erhält, mindestens eine einzelne Zeile aus. Aus diesem Grund ist es wichtig, dass der API-Server eine Form der Protokollierung erhält, damit er nicht den gesamten verfügbaren Speicherplatz verbraucht. Da die Protokolle des API-Servers jedoch wichtig sind, um die Funktionsweise des API-Servers zu verstehen, empfehlen wir dringend, die Protokolle vom API-Server an einen Log-Aggregationsdienst zu senden, damit sie später eingesehen und abgefragt werden können, um Benutzer- oder Komponentenanfragen an die API zu debuggen.

Teile des API-Servers

Der Betrieb des Kubernetes-API-Servers umfasst drei Kernfunktionen:

API-Verwaltung

Der Prozess, durch den APIs offengelegt und vom Server verwaltet werden

Antragsbearbeitung

Die größte Gruppe von Funktionen, die einzelne API-Anfragen von einem Kunden verarbeiten

Interne Regelkreise

Interna, die für die Hintergrundoperationen verantwortlich sind, die für den erfolgreichen Betrieb des API-Servers notwendig sind

Die folgenden Abschnitte behandeln jede dieser großen Kategorien.

API-Verwaltung

Obwohl die API in erster Linie dazu dient, individuelle Kundenanfragen zu bedienen, muss der Kunde wissen, wie er eine API-Anfrage stellen kann, bevor sie bearbeitet werden kann. Letztendlich ist der API-Server ein HTTP-Server - jede API-Anfrage ist also eine HTTP-Anfrage. Aber die Eigenschaften dieser HTTP-Anfragen müssen beschrieben werden, damit der Client und der Server wissen, wie sie miteinander kommunizieren können. Für die Erkundung ist es gut, einen API-Server zu haben, der tatsächlich läuft und auf dem du herumstöbern kannst. Du kannst entweder einen bestehenden Kubernetes-Cluster verwenden, auf den du Zugriff hast, oder du kannst das minikube-Tool für einen lokalen Kubernetes-Cluster verwenden. Damit du den API-Server mit dem Tool curl leicht erkunden kannst, führe das Tool kubectl im Modus proxy aus, um einen unauthentifizierten API-Server auf localhost:8001 mit folgendem Befehl freizugeben:

kubectl proxy

API-Pfade

Jede Anfrage an den API-Server folgt einem RESTful-API-Muster, bei dem die Anfrage durch den HTTP-Pfad der Anfrage definiert wird. Alle Kubernetes-Anfragen beginnen mit dem Präfix /api/ (die Kern-APIs) oder /apis/ (APIs, die nach API-Gruppen zusammengefasst sind). Die beiden unterschiedlichen Pfade sind in erster Linie historisch bedingt. Ursprünglich gab es in der Kubernetes-API keine API-Gruppen, sodass die ursprünglichen oder "Kern"-Objekte wie Pods und Serviceunter dem Präfix "/api/" ohne API-Gruppe geführt werden. Spätere APIs wurden in der Regel unter API-Gruppen hinzugefügt, sodass sie dem Pfad "/apis/<api-group>/" folgen. Das Objekt Job ist zum Beispiel Teil der API-Gruppe batch und befindet sich daher unter /apis/batch/v1/....

Ein zusätzliches Problem bei den Ressourcenpfaden ist die Frage, ob die Ressource einem Namensraum angehört. Namespaces in Kubernetes fügt den Objekten eine Gruppierungsebene hinzu, Ressourcen mit Namensräumen können nur innerhalb eines Namensraums erstellt werden, und der Name dieses Namensraums ist im HTTP-Pfad für die Ressource mit Namensraum enthalten. Natürlich gibt es auch Ressourcen, die sich nicht in einem Namensraum befinden (das offensichtlichste Beispiel ist das Namespace API-Objekt selbst), und in diesem Fall haben sie keine Komponente Namensräume in ihrem HTTP-Pfad.

Hier sind die Komponenten der beiden unterschiedlichen Pfade für die Namensräume der Ressourcentypen:

  • /api/v1/namespaces/<namespace-name>/<resource-type-name>/<resource-name>

  • /apis/<api-group>/<api-version>/namespaces/<namespace-name>/<resource-type-name>/<resource-name>

Hier sind die Komponenten der beiden unterschiedlichen Pfade für Ressourcentypen ohne Namensräume:

  • /api/v1/<Ressourcen-Typ-Name>/<Ressourcen-Name>

  • /apis/<api-group>/<api-version>/<resource-type-name>/<resource-name>

API-Entdeckung

Um Anfragen an die API stellen zu können, muss man natürlich wissen, welche API-Objekte für den Client verfügbar sind. Dieser Prozess erfolgt durch die API-Erkennung auf Seiten des Clients. Um diesen Prozess in Aktion zu sehen und den API-Server auf praktische Weise zu erkunden, können wir die API-Ermittlung selbst durchführen.

Um die Dinge zu vereinfachen, verwenden wir zunächst die in das Kommandozeilen-Tool kubectl eingebaute proxy, um uns bei unserem Cluster zu authentifizieren. Ausführen:

kubectl proxy

Dadurch wird ein einfacher Server auf Port 8001 auf deinem lokalen Rechner eingerichtet.

Wir können diesen Server verwenden, um den Prozess der API-Erkennung zu starten. Wir beginnen mit der Untersuchung des Präfixes /api:

$ curl localhost:8001/api
{
  "kind": "APIVersions",
  "versions": [
    "v1"
  ],
  "serverAddressByClientCIDRs": [
    {
      "clientCIDR": "0.0.0.0/0",
      "serverAddress": "10.0.0.1:6443"
    }
  ]
}

Du kannst sehen, dass der Server ein API-Objekt vom Typ APIVersions zurückgegeben hat. Dieses Objekt liefert uns ein versions Feld, in dem die verfügbaren Versionen aufgelistet sind.

In diesem Fall gibt es nur eine einzige, aber für das Präfix /apis gibt es viele. Wir können diese Version verwenden, um unsere Untersuchung fortzusetzen:

$ curl localhost:8001/api/v1
{
  "kind": "APIResourceList",
  "groupVersion": "v1",
  "resources": [
    {
….
    {
      "name": "namespaces",
      "singularName": "",
      "namespaced": false,
      "kind": "Namespace",
      "verbs": [
        "create",
        "delete",
        "get",
        "list",
        "patch",
        "update",
        "watch"
      ],
      "shortNames": [
        "ns"
      ]
    },
    …
    {
      "name": "pods",
      "singularName": "",
      "namespaced": true,
      "kind": "Pod",
      "verbs": [
        "create",
        "delete",
        "deletecollection",
        "get",
        "list",
        "patch",
        "proxy",
        "update",
        "watch"
      ],
      "shortNames": [
        "po"
      ],
      "categories": [
        "all"
      ]
    },
    {
      "name": "pods/attach",
      "singularName": "",
      "namespaced": true,
      "kind": "Pod",
      "verbs": []
    },
    {
      "name": "pods/binding",
      "singularName": "",
      "namespaced": true,
      "kind": "Binding",
      "verbs": [
        "create"
      ]
    },
   ….
  ]
}

(Diese Ausgabe ist der Kürze halber stark gekürzt).

Jetzt kommen wir weiter. Wir sehen, dass die spezifischen Ressourcen, die unter einem bestimmten Pfad verfügbar sind, vom API-Server ausgedruckt werden. In diesem Fall enthält das zurückgegebene Objekt die Liste der Ressourcen, die unter dem Pfad /api/v1/ verfügbar sind.

Die OpenAPI/Swagger JSON-Spezifikation, die die API beschreibt (das Meta-API-Objekt), enthält neben den Ressourcentypen eine Vielzahl interessanter Informationen. Betrachte die OpenAPI-Spezifikation für das Objekt Pod:

{
      "name": "pods",
      "singularName": "",
      "namespaced": true,
      "kind": "Pod",
      "verbs": [
        "create",
        "delete",
        "deletecollection",
        "get",
        "list",
        "patch",
        "proxy",
        "update",
        "watch"
      ],
      "shortNames": [
        "po"
      ],
      "categories": [
        "all"
      ]
    },
    {
      "name": "pods/attach",
      "singularName": "",
      "namespaced": true,
      "kind": "Pod",
      "verbs": []
    }

Im Feld name findest du den Namen dieser Ressource. Es gibt auch den Unterpfad für diese Ressourcen an. Da es schwierig ist, die Pluralisierung eines englischen Wortes zu erkennen, enthält die API-Ressource auch ein Feld singularName, das den Namen angibt, der für eine Singularinstanz dieser Ressource verwendet werden sollte. Wir haben bereits über Namensräume gesprochen. Das Feld namespaced in der Objektbeschreibung gibt an, ob das Objekt über einen Namensraum verfügt. Das Feld kind enthält die Zeichenkette, die in der JSON-Darstellung des API-Objekts enthalten ist, um anzugeben, um welche Art von Objekt es sich handelt. Das Feld verbs ist eines der wichtigsten Felder des API-Objekts, da es angibt, welche Arten von Aktionen mit dem Objekt durchgeführt werden können. Das pods Objekt enthält alle möglichen Verben. Die meisten Auswirkungen der Verben sind aus ihren Namen ersichtlich. Die beiden, die ein wenig mehr Erklärung benötigen, sind watch und proxy. watch zeigt an, dass du eine Überwachung für die Ressource einrichten kannst. Ein Watch ist ein langwieriger Vorgang, der dich über Änderungen am Objekt informiert. Die Watch wird in späteren Abschnitten ausführlich behandelt. proxy ist eine spezielle Aktion, die eine Proxy-Netzwerkverbindung über den API-Server zu Netzwerkports herstellt. Derzeit gibt es nur zwei Ressourcen (Pods und Services), die proxy unterstützen.

Zusätzlich zu den Aktionen (die als Verben beschrieben werden), die du auf ein Objekt anwenden kannst, gibt es andere Aktionen, die als Subressourcen für einen Ressourcentyp modelliert werden. Der Befehl attach wird beispielsweise als Subressource modelliert:

    {
      "name": "pods/attach",
      "singularName": "",
      "namespaced": true,
      "kind": "Pod",
      "verbs": []
    }

attach bietet dir die Möglichkeit, ein Terminal an einen laufenden Container innerhalb eines Pods anzuhängen. Die Funktion exec, mit der du einen Befehl innerhalb eines Pods ausführen kannst, ist ähnlich aufgebaut.

OpenAPI Spec Serving

Natürlich ist die Kenntnis der Ressourcen und Pfade, mit denen du auf den API-Server zugreifen kannst, nur ein Teil der Informationen, die du für den Zugriff auf die Kubernetes-API benötigst. Neben dem HTTP-Pfad musst du auch wissen, welche JSON-Nutzdaten du senden und empfangen kannst. Der API-Server stellt auch Pfade bereit, die dich mit Informationen über die Schemas für Kubernetes-Ressourcen versorgen. Diese Schemata werden mit der OpenAPI-Syntax (früher Swagger) dargestellt. Du kannst die OpenAPI-Spezifikation unter dem folgenden Pfad abrufen:

/swaggerapi

Vor Kubernetes 1.10, dient Swagger 1.2

/openapi/v2

Kubernetes 1.10 und höher, dient OpenAPI (Swagger 2.0)

Die OpenAPI-Spezifikation ist ein komplettes Thema für sich und würde den Rahmen dieses Buches sprengen. In jedem Fall ist es unwahrscheinlich, dass du bei deinem täglichen Einsatz von Kubernetes auf sie zugreifen musst. Die verschiedenen Bibliotheken der Client-Programmiersprachen werden jedoch unter Verwendung dieser OpenAPI-Spezifikationen erstellt (die bemerkenswerte Ausnahme ist die Go-Client-Bibliothek, die derzeit noch von Hand programmiert wird). Wenn du oder ein/e Nutzer/in also Probleme mit dem Zugriff auf Teile der Kubernetes-API über eine Client-Bibliothek haben, solltest du zuerst die OpenAPI-Spezifikation lesen, um zu verstehen, wie die API-Objekte modelliert sind.

API-Übersetzung

In Kubernetes beginnt eine API als Alpha-API (z. B. v1alpha1). Die Alpha-Bezeichnung zeigt an, dass die API instabil und für Produktionsanwendungen ungeeignet ist. Nutzer/innen, die Alpha-APIs übernehmen, sollten damit rechnen, dass sich die API-Oberfläche zwischen verschiedenen Kubernetes-Versionen ändern kann und dass die Implementierung der API selbst instabil ist und sogar den gesamten Kubernetes-Cluster destabilisieren kann. Alpha-APIs sind daher in produktiven Kubernetes-Clustern deaktiviert.

Sobald eine API ausgereift ist, wird sie zu einer Beta-API (z. B. v1beta1). Die Beta-Bezeichnung zeigt an, dass die API im Allgemeinen stabil ist, aber noch Fehler oder letzte Verfeinerungen der API-Oberfläche aufweisen kann. Im Allgemeinen wird davon ausgegangen, dass Beta-APIs zwischen den Kubernetes-Releases stabil sind, und die Abwärtskompatibilität ist ein Ziel. In besonderen Fällen können Beta-APIs jedoch auch zwischen Kubernetes-Versionen inkompatibel sein. Ebenso sollen Beta-APIs stabil sein, aber es können immer noch Bugs vorhanden sein. Beta-APIs sind in der Regel in produktiven Kubernetes-Clustern aktiviert, sollten aber mit Vorsicht verwendet werden.

Schließlich wird eine API allgemein verfügbar (z. B. v1). Allgemeine Verfügbarkeit (GA) bedeutet, dass die API stabil ist. Diese APIs enthalten sowohl eine Garantie für die Abwärtskompatibilität als auch eine Auslaufgarantie. Nachdem eine API als zur Entfernung vorgesehen markiert wurde, behält Kubernetes die API für mindestens drei Versionen oder ein Jahr, je nachdem, was zuerst eintritt. Die Abschaffung ist auch ziemlich unwahrscheinlich. APIs werden erst dann veraltet, wenn eine bessere Alternative entwickelt wurde. Ebenso sind GA-APIs stabil und für den Produktionseinsatz geeignet.

Eine bestimmte Version von Kubernetes kann mehrere Versionen unterstützen (Alpha, Beta und GA). Um dies zu erreichen, verfügt der API-Server jederzeit über drei verschiedene Repräsentationen der API: die externe Repräsentation, d.h. die Repräsentation, die über eine API-Anfrage eingeht; die interne Repräsentation, d.h. die In-Memory-Repräsentation des Objekts, die innerhalb des API-Servers für die Verarbeitung verwendet wird; und die Speicherrepräsentation, die in der Speicherschicht aufgezeichnet wird, um die API-Objekte zu erhalten. Der API-Server verfügt über einen Code, der weiß, wie die verschiedenen Übersetzungen zwischen all diesen Darstellungen durchgeführt werden können. Ein API-Objekt kann als v1alpha1 Version eingereicht, als v1 Objekt gespeichert und anschließend als v1beta1 Objekt oder in einer beliebigen anderen unterstützten Version abgerufen werden. Diese Umwandlungen werden mit angemessener Leistung durch maschinell erstellte Deep-Copy-Bibliotheken erreicht, die die entsprechenden Übersetzungen vornehmen.

Anfrage Management

Die Hauptaufgabe des API-Servers in Kubernetes ist es, API-Aufrufe in Form von HTTP-Anfragen zu empfangen und zu verarbeiten. Diese Anfragen kommen entweder von anderen Komponenten im Kubernetes-System oder sie sind Anfragen von Endnutzern. In jedem Fall werden sie vom Kubernetes-API-Server auf dieselbe Weise verarbeitet.

Arten von Anträgen

Es gibt verschiedene Kategorien von Anfragen, die vom Kubernetes-API-Server ausgeführt werden.

GET

Die einfachsten Anfragen sind GET Anfragen für bestimmte Ressourcen. Diese Anfragen rufen die Daten ab, die mit einer bestimmten Ressource verbunden sind. Eine HTTP GET Anfrage an den Pfad /api/v1/namespaces/default/pods/foo ruft zum Beispiel die Daten für einen Pod namens foo ab.

LIST

Eine etwas kompliziertere, aber immer noch recht einfache Anfrage ist collection GET oder LIST. Dabei handelt es sich um Anfragen zur Auflistung einer Reihe von verschiedenen Anfragen. Eine HTTP GET -Anfrage an den Pfad /api/v1/namespaces/default/pods ruft beispielsweise eine Sammlung aller Pods im default -Namensraum ab. LIST -Anfragen können optional auch eine Label-Abfrage angeben. In diesem Fall werden nur Ressourcen zurückgegeben, die dieser Label-Abfrage entsprechen.

POST

Um eine Ressource zu erstellen, wird eine POST Anfrage verwendet. Der Text der Anfrage ist die neue Ressource, die erstellt werden soll. Bei einer POST -Anfrage ist der Pfad der Ressourcentyp (z. B. /api/v1/namespaces/default/pods). Um eine bestehende Ressource zu aktualisieren, wird eine PUT Anfrage an den spezifischen Ressourcenpfad gestellt (z. B. /api/v1/namespaces/default/pods/foo).

DELETE

Wenn es an der Zeit ist, eine Anfrage zu löschen, wird eine HTTP DELETE Anfrage an den Pfad der Ressource (z.B. /api/v1/namespaces/default/pods/foo) gestellt. Es ist wichtig zu wissen, dass diese Änderung dauerhaft ist - nachdem die HTTP-Anfrage gestellt wurde, ist die Ressource gelöscht.

Der Inhaltstyp für all diese Anfragen ist in der Regel textbasiertes JSON (application/json), aber neuere Versionen von Kubernetes unterstützen auch die binäre Codierung von Protocol Buffers. Generell ist JSON besser geeignet, um den Datenverkehr im Netzwerk zwischen Client und Server für den Menschen lesbar und debuggbar zu machen, aber es ist wesentlich ausführlicher und teurer zu parsen. Protocol Buffers sind mit gängigen Tools wie curl schwieriger zu analysieren, ermöglichen aber eine höhere Leistung und einen höheren Durchsatz bei API-Anfragen.

Zusätzlich zu diesen Standardanfragen verwenden viele Anfragen das WebSocket-Protokoll, um Streaming-Sitzungen zwischen Client und Server zu ermöglichen. Beispiele für solche Protokolle sind die Befehle exec und attach. Diese Anfragen werden in den folgenden Abschnitten beschrieben.

Das Leben einer Anfrage

Um besser zu verstehen, was der API-Server für jede dieser verschiedenen Anfragen tut, werden wir die Verarbeitung einer einzelnen Anfrage an den API-Server auseinandernehmen und beschreiben.

Authentifizierung

Die erste Stufe der Anfrageverarbeitung ist die Authentifizierung, bei der die mit der Anfrage verbundene Identität festgestellt wird. Der API-Server unterstützt verschiedene Arten der Identitätsfeststellung, darunter Client-Zertifikate, Inhaber-Tokens und HTTP Basic Authentication. Im Allgemeinen sollten Client-Zertifikate oder Inhaber-Token für die Authentifizierung verwendet werden; von der Verwendung der HTTP Basic Authentication wird abgeraten.

Zusätzlich zu diesen lokalen Methoden zur Identitätsfeststellung ist die Authentifizierung erweiterbar und es gibt mehrere Plug-in-Implementierungen, die entfernte Identitätsanbieter nutzen. Dazu gehören die Unterstützung des OpenID Connect (OIDC) Protokolls sowie Azure Active Directory. Diese Authentifizierungs-Plug-ins sind sowohl in den API-Server als auch in die Client-Bibliotheken integriert. Das bedeutet, dass du sicherstellen musst, dass sowohl die Befehlszeilentools als auch der API-Server ungefähr die gleiche Version haben oder die gleichen Authentifizierungsmethoden unterstützen.

Der API-Server unterstützt auch Remote-Webhook-basierte Authentifizierungskonfigurationen, bei denen die Authentifizierungsentscheidung über die Weiterleitung von Bearer Token an einen externen Server delegiert wird. Der externe Server validiert das Bearer-Token des Endnutzers und sendet die Authentifizierungsinformationen an den API-Server zurück.

Da dies für die Sicherung eines Servers sehr wichtig ist, wird es in einem späteren Kapitel ausführlich behandelt.

RBAC/Berechtigung

Nachdem der API-Server die Identität für eine Anfrage ermittelt hat, geht er zur Autorisierung über. Jede Anfrage an Kubernetes folgt einem traditionellen RBAC-Modell. Um auf eine Anfrage zuzugreifen, muss die Identität die entsprechende Rolle haben, die mit der Anfrage verbunden ist. RBAC in Kubernetes ist ein umfangreiches und kompliziertes Thema, weshalb wir den Details der Funktionsweise ein ganzes Kapitel gewidmet haben. Für die Zwecke dieser Zusammenfassung des API-Servers stellt der API-Server bei der Bearbeitung einer Anfrage fest, ob die mit der Anfrage verbundene Identität auf die Kombination aus dem Verb und dem HTTP-Pfad in der Anfrage zugreifen darf. Wenn die Identität der Anfrage die entsprechende Rolle hat, darf sie fortgesetzt werden. Andernfalls wird eine HTTP 403 Antwort zurückgegeben.

Dies wird in einem späteren Kapitel ausführlicher behandelt.

Einlasskontrolle

Nachdem eine Anfrage authentifiziert und autorisiert wurde, wird sie zur Zulassungskontrolle weitergeleitet. Authentifizierung und RBAC bestimmen, ob die Anfrage zulässig ist. Dies geschieht anhand der HTTP-Eigenschaften der Anfrage (Header, Methode und Pfad). Die Zulassungskontrolle stellt fest, ob die Anfrage korrekt formuliert ist und nimmt möglicherweise Änderungen an der Anfrage vor, bevor sie bearbeitet wird. Die Zulassungskontrolle definiert eine anpassbare Schnittstelle:

apply(request): (transformedRequest, error)

Wenn ein Zulassungsprüfer einen Fehler feststellt, wird die Anfrage abgelehnt. Wenn der Antrag angenommen wird, wird der umgewandelte Antrag anstelle des ursprünglichen Antrags verwendet. Die Zulassungskontrolleure werden nacheinander aufgerufen, wobei jeder die Ergebnisse des vorherigen Kontrolleurs erhält.

Da die Zulassungskontrolle ein so allgemeiner, anpassungsfähiger Mechanismus ist, wird er für eine Vielzahl unterschiedlicher Funktionen im API-Server verwendet. Sie wird zum Beispiel verwendet, um Objekten Standardwerte zuzuweisen. Sie kann auch verwendet werden, um Richtlinien durchzusetzen (z. B. um zu verlangen, dass alle Objekte ein bestimmtes Label haben). Außerdem kann er dazu verwendet werden, jedem Pod einen zusätzlichen Container hinzuzufügen. Das Service-Mesh Istio nutzt diesen Ansatz, um seinen Sidecar-Container transparent einzubinden.

Zulassungssteuerungen sind recht allgemein gehalten und können dem API-Server über eine Webhook-basierte Zulassungskontrolle dynamisch hinzugefügt werden.

Validierung

Die Validierung von Anfragen erfolgt nach der Zulassungskontrolle, obwohl sie auch als Teil der Zulassungskontrolle implementiert werden kann, insbesondere bei der externen Webhook-basierten Validierung. Außerdem wird die Validierung nur für ein einzelnes Objekt durchgeführt. Wenn sie ein umfassenderes Wissen über den Zustand des Clusters erfordert, muss sie als Zulassungskontrolle implementiert werden.

Die Validierung von Anfragen stellt sicher, dass eine bestimmte Ressource, die in einer Anfrage enthalten ist, gültig ist. Sie stellt zum Beispiel sicher, dass der Name eines Service Objekts mit den Regeln für DNS-Namen übereinstimmt, da der Name eines Service schließlich in den Kubernetes Service Discovery DNS-Server programmiert wird. Im Allgemeinen wird die Validierung als benutzerdefinierter Code implementiert, der pro Ressourcentyp definiert wird.

Spezialisierte Anfragen

Zusätzlich zu den standardmäßigen RESTful-Anfragen verfügt der API-Server über eine Reihe von spezialisierten Anfragemustern, die den Kunden erweiterte Funktionen bieten:

/proxy, /exec, /attach, /logs

Die erste wichtige Klasse von Vorgängen sind offene, lang andauernde Verbindungen zum API-Server. Diese Anfragen liefern Streaming-Daten und keine sofortigen Antworten.

Die Operation logs ist die erste Streaming-Anfrage, die wir beschreiben, weil sie am einfachsten zu verstehen ist. In der Tat ist logs standardmäßig gar keine Streaming-Anfrage. Ein Kunde stellt eine Anfrage, um die Logs für einen Pod abzurufen, indem er /logs an das Ende des Pfads für einen bestimmten Pod anhängt (z. B./api/v1/namespaces/default/pods/some-pod/logs) und dann den Containernamen als HTTP-Abfrageparameter und eine HTTP GET Anfrage angibt. Bei einer Standardabfrage gibt der API-Server alle Protokolle bis zum aktuellen Zeitpunkt als reinen Text zurück und schließt dann die HTTP-Anfrage. Wenn der Kunde jedoch anfordert, dass die Protokolle nachgereicht werden sollen (indem er den Abfrageparameter follow angibt), wird die HTTP-Antwort vom API-Server offen gehalten und neue Protokolle werden in die HTTP-Antwort geschrieben, sobald sie vom Kubelet über den API-Server empfangen werden. Diese Verbindung ist in Abbildung 4-1 dargestellt.

mgk8 0401
Abbildung 4-1. Der grundlegende Ablauf einer HTTP-Anfrage für Container-Logs

logs ist die am einfachsten zu verstehende Streaming-Anfrage, weil sie die Anfrage einfach offen lässt und weitere Daten einstreut. Die übrigen Vorgänge nutzen das WebSocket-Protokoll für bidirektionale Datenströme. Außerdem werden die Daten innerhalb dieser Streams gemultiplext, um eine beliebige Anzahl von bidirektionalen Streams über HTTP zu ermöglichen. Wenn sich das alles ein bisschen kompliziert anhört, ist es das auch, aber es ist auch ein wertvoller Teil der Oberfläche des API-Servers.

Hinweis

Der API-Server unterstützt eigentlich zwei verschiedene Streaming-Protokolle. Er unterstützt sowohl das SPDY-Protokoll als auch das HTTP2/WebSocket-Protokoll. SPDY wird durch HTTP2/WebSocket ersetzt, daher konzentrieren wir uns auf das WebSocket-Protokoll.

Das vollständige WebSocket-Protokoll würde den Rahmen dieses Buches sprengen, aber es ist an vielen anderen Stellen dokumentiert. Für das Verständnis des API-Servers kannst du dir WebSocket einfach als ein Protokoll vorstellen, das HTTP in ein bidirektionales Byte-Streaming-Protokoll umwandelt.

Zusätzlich zu diesen Streams führt der Kubernetes-API-Server jedoch ein zusätzliches Multiplex-Streaming-Protokoll ein. Der Grund dafür ist, dass es für viele Anwendungsfälle sehr nützlich ist, wenn der API-Server mehrere unabhängige Byte-Streams bedienen kann. Nehmen wir zum Beispiel die Ausführung eines Befehls innerhalb eines Containers. In diesem Fall müssen drei Streams verwaltet werden (stdin, stderr und stdout).

Das grundlegende Protokoll für dieses Streaming ist wie folgt: Jedem Stream wird eine Nummer von 0 bis 255 zugewiesen. Diese Streamnummer wird sowohl für die Eingabe als auch für die Ausgabe verwendet und bildet konzeptionell einen einzelnen bidirektionalen Bytestrom ab.

Bei jedem Frame, der über das WebSocket-Protokoll gesendet wird, ist das erste Byte die Stream-Nummer (z. B. 0) und der Rest des Frames sind die Daten, die auf diesem Stream übertragen werden(Abbildung 4-2).

mgk8 0402
Abbildung 4-2. Ein Beispiel für das Kubernetes WebSocket Multichannel Framing

Mit diesem Protokoll und WebSockets kann der API-Server gleichzeitig 256-Byte-Streams in einer einzigen WebSocket-Sitzung multiplexen.

Dieses Basisprotokoll wird für exec und attach Sitzungen mit den folgenden Kanälen verwendet:

0

Der stdin Stream zum Schreiben an den Prozess. Daten werden nicht aus diesem Stream gelesen.

1

Der stdout Ausgabestrom zum Lesen von stdout aus dem Prozess. In diesen Stream sollten keine Daten geschrieben werden.

2

Der stderr Ausgabestrom zum Lesen von stderr aus dem Prozess. In diesen Stream sollten keine Daten geschrieben werden.

Der Endpunkt /proxy wird verwendet, um den Netzwerkverkehr zwischen dem Client und den Containern und Diensten, die innerhalb des Clusters laufen, weiterzuleiten, ohne dass diese Endpunkte nach außen hin sichtbar sind. Um diese TCP-Sitzungen zu streamen, ist das Protokoll etwas komplizierter. Zusätzlich zum Multiplexing der verschiedenen Streams sind die ersten beiden Bytes des Streams (nach der Streamnummer, also eigentlich das zweite und dritte Byte im WebSockets-Frame) die Portnummer, die weitergeleitet wird, so dass ein einzelner WebSockets-Frame für /proxy wie in Abbildung 4-3 aussieht.

mgk8 0403
Abbildung 4-3. Ein Beispiel für den Datenrahmen für WebSockets-basierte Portweiterleitung

Operationen beobachten

Zusätzlich zu den Streaming-Daten unterstützt der API-Server eine Watch-API. Ein Watch überwacht einen Pfad auf Änderungen. Anstatt in bestimmten Intervallen nach möglichen Aktualisierungen zu fragen, was entweder zusätzliche Last (durch schnelles Polling) oder zusätzliche Latenz (durch langsames Polling) bedeutet, kann ein Nutzer mit einer einzigen Verbindung Aktualisierungen mit geringer Latenz erhalten. Wenn ein Nutzer eine Watch-Verbindung zum API-Server herstellt, indem er den Abfrageparameter ?watch=true zu einer API-Server-Anfrage hinzufügt, schaltet der API-Server in den Watch-Modus und lässt die Verbindung zwischen Client und Server offen. Ebenso sind die vom API-Server zurückgegebenen Daten nicht mehr nur das API-Objekt, sondern ein Watch Objekt, das sowohl die Art der Änderung (erstellt, aktualisiert, gelöscht) als auch das API-Objekt selbst enthält. Auf diese Weise kann ein Client alle Änderungen an dem Objekt oder der Objektgruppe beobachten und verfolgen.

Optimistischerweise gleichzeitige Aktualisierungen

Eine weitere erweiterte Operation, die vom API-Server unterstützt wird, ist die Möglichkeit, optimistische gleichzeitige Aktualisierungen der Kubernetes-API durchzuführen. Die Idee hinter der optimistischen Gleichzeitigkeit ist die Fähigkeit, die meisten Operationen ohne Sperren(pessimistische Gleichzeitigkeit) durchzuführen und stattdessen zu erkennen, wenn ein gleichzeitiger Schreibvorgang stattgefunden hat, und den späteren der beiden gleichzeitigen Schreibvorgänge abzulehnen. Ein abgelehnter Schreibvorgang wird nicht wiederholt (es ist Sache des Clients, den Konflikt zu erkennen und den Schreibvorgang selbst zu wiederholen).

Um zu verstehen, warum diese optimistische Gleichzeitigkeits- und Konflikterkennung erforderlich ist, ist es wichtig, die Struktur einer Lese-/Aktualisierungs-/Schreib-Race-Condition zu kennen. Der Betrieb vieler API-Server-Clients umfasst drei Vorgänge:

  1. Lies einige Daten vom API-Server.

  2. Aktualisiere diese Daten im Speicher.

  3. Schreibe sie zurück an den API-Server.

Nun stell dir vor, was passiert, wenn zwei dieser Lese-/Aktualisierungs-/Schreibmuster gleichzeitig auftreten.

  1. Server A liest Objekt O.

  2. Server B liest Objekt O.

  3. Server A aktualisiert Objekt O im Speicher auf dem Client.

  4. Server B aktualisiert Objekt O im Speicher auf dem Client.

  5. Server A schreibt Objekt O.

  6. Server B schreibt Objekt O.

Am Ende sind die Änderungen, die Server A vorgenommen hat, verloren, weil sie durch das Update von Server B überschrieben wurden.

Es gibt zwei Möglichkeiten, diesen Wettlauf zu lösen. Die erste ist eine pessimistische Sperre, die verhindert, dass andere Lesevorgänge durchgeführt werden, während Server A das Objekt bearbeitet. Das Problem dabei ist, dass dadurch alle Operationen serialisiert werden, was zu Leistungs- und Durchsatzproblemen führt.

Die andere Option, die der Kubernetes-API-Server implementiert, ist die optimistische Gleichzeitigkeit, bei der davon ausgegangen wird, dass alles klappt, und ein Problem erst dann erkannt wird, wenn ein konfliktreicher Schreibversuch unternommen wird. Um dies zu erreichen, gibt jede Instanz eines Objekts sowohl seine Daten als auch eine Ressourcenversion zurück. Diese Ressourcenversion zeigt die aktuelle Iteration des Objekts an. Wenn bei einem Schreibvorgang die Ressourcenversion des Objekts gesetzt ist, ist der Schreibvorgang nur dann erfolgreich, wenn die aktuelle Version mit der Version des Objekts übereinstimmt. Ist dies nicht der Fall, wird ein HTTP-Fehler 409 (Konflikt) zurückgegeben und der Client muss es erneut versuchen. Um zu sehen, wie das gerade beschriebene Wettrennen zwischen Lesen, Aktualisieren und Schreiben gelöst wird, sehen wir uns die Vorgänge noch einmal an:

  1. Server A liest Objekt O in der Version v1.

  2. Server B liest Objekt O in der Version v1.

  3. Server A aktualisiert Objekt O mit der Version v1 im Speicher des Clients.

  4. Server B aktualisiert Objekt O mit der Version v1 im Speicher des Clients.

  5. Server A schreibt Objekt O mit der Version v1; dies ist erfolgreich.

  6. Server B schreibt Objekt O mit der Version v1, aber das Objekt hat die Version v2; ein 409-Konflikt wird zurückgegeben.

Alternative Kodierungen

Der API-Server unterstützt nicht nur die JSON-Kodierung von Objekten für Anfragen, sondern auch zwei andere Formate für Anfragen. Die Kodierung der Anfragen wird durch den Content-Type HTTP-Header der Anfrage angegeben. Fehlt dieser Header, wird angenommen, dass der Inhalt application/json ist, was auf eine JSON-Kodierung hinweist. Die erste alternative Kodierung ist YAML, die durch den application/yaml Content-Type angegeben wird. YAML ist ein textbasiertes Format, das allgemein als besser lesbar gilt als JSON. Es gibt kaum einen Grund, YAML als Kodierung für die Kommunikation mit dem Server zu verwenden, aber in einigen Fällen kann es praktisch sein (z. B. beim manuellen Senden von Dateien an den Server über curl).

Die andere alternative Kodierung für Anfragen und Antworten ist das Protocol Buffers Kodierungsformat. Protocol Buffers ist ein ziemlich effizientes Protokoll für binäre Objekte. Die Verwendung von Protocol Buffers kann zu effizienteren und durchsatzstärkeren Anfragen an die API-Server führen. Viele der internen Kubernetes-Tools verwenden Protocol Buffers als Transportmedium. Das Hauptproblem bei Protocol Buffers ist, dass sie aufgrund ihres binären Charakters in ihrem Wire-Format deutlich schwieriger zu visualisieren/debuggen sind. Außerdem unterstützen derzeit nicht alle Client-Bibliotheken Protocol Buffers Anfragen oder Antworten. Das Protocol Buffers Format wird durch den application/vnd.kubernetes.protobuf Content-Type Header angezeigt.

Allgemeine Antwortcodes

Da der API-Server als RESTful-Server implementiert ist, sind alle Antworten des Servers auf HTTP-Antwortcodes ausgerichtet. Neben den typischen 200er-Antworten für OK und 500er-Antworten für interne Serverfehler sind hier einige der üblichen Antwortcodes und ihre Bedeutung aufgeführt:

202

Angenommen. Es wurde eine asynchrone Anfrage zum Erstellen oder Löschen eines Objekts empfangen. Das Ergebnis antwortet mit einem Statusobjekt, bis die asynchrone Anfrage abgeschlossen ist; dann wird das eigentliche Objekt zurückgegeben.

400

Schlechte Anfrage. Der Server konnte die Anfrage nicht parsen oder verstehen.

401

Nicht autorisiert. Es wurde eine Anfrage ohne ein bekanntes Authentifizierungsschema empfangen.

403

Verboten. Die Anfrage wurde empfangen und verstanden, aber der Zugriff ist untersagt.

409

Konflikt. Die Anfrage wurde empfangen, aber es war eine Anfrage zur Aktualisierung einer älteren Version des Objekts.

422

Unverarbeitbare Entität. Die Anfrage wurde korrekt geparst, schlug aber bei irgendeiner Art von Validierung fehl.

API Server Interna

Zusätzlich zu den Grundlagen für den Betrieb des HTTP-RESTful-Dienstes verfügt der API-Server über einige interne Dienste, die Teile der Kubernetes-API implementieren. Normalerweise werden solche Regelkreise in einer separaten Binärdatei, dem Controller Manager, ausgeführt. Es gibt aber auch ein paar Kontrollschleifen, die innerhalb des API-Servers ausgeführt werden müssen. In jedem Fall beschreiben wir die Funktionalität und den Grund für ihr Vorhandensein im API-Server.

CRD-Regelkreis

Benutzerdefinierte Ressourcendefinitionen (CRDs) sind dynamische API-Objekte, die einem laufenden API-Server hinzugefügt werden können. Da bei der Erstellung einer CRD neue HTTP-Pfade erstellt werden, die der API-Server kennen muss, ist der Controller, der für das Hinzufügen dieser Pfade verantwortlich ist, im API-Server untergebracht. Mit der Einführung von delegierten API-Servern (die in einem späteren Kapitel beschrieben werden) wurde dieser Controller größtenteils aus dem API-Server herausgelöst. Er wird derzeit noch standardmäßig im Prozess ausgeführt, kann aber auch außerhalb des Prozesses ausgeführt werden.

Der CRD-Regelkreis funktioniert folgendermaßen:

for crd in AllCustomResourceDefinitions:
    if !RegisteredPath(crd):
       registerPath

for path in AllRegisteredPaths:
    if !CustomResourceExists(path):
       markPathInvalid(path)
       delete custom resource data
       delete path

Die Erstellung des benutzerdefinierten Ressourcenpfads ist ziemlich einfach, aber das Löschen einer benutzerdefinierten Ressource ist ein wenig komplizierter. Das liegt daran, dass mit dem Löschen einer benutzerdefinierten Ressource auch alle Daten gelöscht werden, die mit Ressourcen dieses Typs verbunden sind. Wenn eine CRD gelöscht und zu einem späteren Zeitpunkt wieder hinzugefügt wird, werden die alten Daten nicht wiederhergestellt.

Bevor der HTTP-Serving-Pfad also entfernt werden kann, wird er zunächst als ungültig markiert, damit keine neuen Ressourcen erstellt werden können. Dann werden alle Daten, die mit dem CRD verbunden sind, gelöscht und schließlich wird der Pfad entfernt.

Den API-Server debuggen

Natürlich ist es gut, die Implementierung des API-Servers zu verstehen, aber in den meisten Fällen musst du in der Lage sein, zu debuggen, was mit dem API-Server (und den Clients, die den API-Server aufrufen) tatsächlich vor sich geht. Dies geschieht in erster Linie über die Logs, die der API-Server schreibt. Es gibt zwei Protokollströme, die der API-Server exportiert - die Standard- oder Basisprotokolle und die gezielteren Audit-Protokolle, die versuchen zu erfassen, warum und wie Anfragen gestellt wurden und wie sich der Zustand des API-Servers verändert hat. Darüber hinaus kann eine ausführlichere Protokollierung zur Fehlersuche bei bestimmten Problemen aktiviert werden.

Basic Logs

Der API-Server protokolliert standardmäßig jede Anfrage, die an den API-Server gesendet wird. Dieses Protokoll enthält die IP-Adresse des Kunden, den Pfad der Anfrage und den Code, den der Server zurückgegeben hat. Wenn ein unerwarteter Fehler zu einer Serverpanik führt, fängt der Server auch diese Panik ab, gibt eine 500 zurück und protokolliert den Fehler.

I0803 19:59:19.929302       1 trace.go:76] Trace[1449222206]:
 "Create /api/v1/namespaces/default/events" (started: 2018-08-03
 19:59:19.001777279 +0000 UTC m=+25.386403121) (total time: 927.484579ms):
Trace[1449222206]: [927.401927ms] [927.279642ms] Object stored in database
I0803 19:59:20.402215       1 controller.go:537] quota admission added
 evaluator for: { namespaces}

In diesem Protokoll kannst du sehen, dass es mit dem Zeitstempel I0803 19:59:… beginnt, wann die Protokollzeile ausgegeben wurde, gefolgt von der Zeilennummer, die sie ausgegeben hat, trace.go:76, und schließlich der eigentlichen Protokollmeldung.

Audit-Logs

Das Audit-Log soll es einem Server-Administrator ermöglichen, den Zustand des Servers und die Reihe von Client-Interaktionen, die zum aktuellen Zustand der Daten in der Kubernetes-API geführt haben, forensisch wiederherzustellen. So lassen sich zum Beispiel Fragen wie "Warum wurde ReplicaSet auf 100 skaliert?" oder "Wer hat diesen Pod gelöscht?" beantworten.

Für Audit-Protokolle gibt es ein anpassbares Backend, in das sie geschrieben werden. In der Regel werden Audit-Protokolle in eine Datei geschrieben, aber es ist auch möglich, dass sie in einen Webhook geschrieben werden. In jedem Fall handelt es sich bei den protokollierten Daten um ein strukturiertes JSON-Objekt des Typs event in der API-Gruppe audit.k8s.io.

Das Auditing selbst kann über ein Richtlinienobjekt in derselben API-Gruppe konfiguriert werden. Mit dieser Richtlinie kannst du die Regeln festlegen, nach denen Audit-Ereignisse in das Audit-Protokoll aufgenommen werden.

Aktivieren zusätzlicher Logs

Kubernetes verwendet für die Protokollierung das Paket github.com/golang/glog leveled logging. Mit dem --v Flag auf dem API-Server kannst du die Ausführlichkeit der Protokollierung einstellen. Im Allgemeinen hat das Kubernetes-Projekt die Protokollierungsstufe 2 (--v=2) als vernünftigen Standard für die Protokollierung relevanter, aber nicht zu spammiger Meldungen festgelegt. Wenn du bestimmte Probleme untersuchst, kannst du die Protokollierungsstufe erhöhen, um mehr (möglicherweise spammige) Meldungen zu sehen. Wegen der Leistungseinbußen, die eine übermäßige Protokollierung mit sich bringt, empfehlen wir, in der Produktion nicht mit einer ausführlichen Protokollstufe zu arbeiten. Wenn du eine gezieltere Protokollierung wünschst, kannst du mit dem Flag --vmodule die Protokollierungsstufe für einzelne Quelldateien erhöhen. Dies kann für eine sehr gezielte ausführliche Protokollierung nützlich sein, die auf eine kleine Anzahl von Dateien beschränkt ist.

Debuggen von kubectl-Anfragen

Zusätzlich zum Debuggen des API-Servers über die Protokolle ist es auch möglich, die Interaktionen mit dem API-Server über das Befehlszeilentool kubectl zu debuggen. Wie der API-Server protokolliert auch das Befehlszeilentool kubectl über das Paket github.com/golang/glog und unterstützt das Verbosity-Flag --v. Wenn du die Ausführlichkeit auf Stufe 10 (--v=10) setzt, wird die Protokollierung maximal ausführlich. In diesem Modus protokolliert kubectl alle Anfragen, die es an den Server stellt, und versucht, curl Befehle auszugeben, mit denen du diese Anfragen wiederholen kannst. Beachte, dass diese curl Befehle manchmal unvollständig sind.

Wenn du direkt auf den API-Server zugreifen möchtest, funktioniert der Ansatz, den wir bereits für die API-Ermittlung verwendet haben, ebenfalls gut. Wenn du kubectl proxy ausführst, wird ein Proxy-Server auf localhost erstellt, der automatisch deine Authentifizierungs- und Autorisierungsdaten auf der Grundlage einer lokalen $HOME/.kube/config-Datei bereitstellt. Nachdem du den Proxy gestartet hast, kannst du ganz einfach verschiedene API-Anfragen mit dem Befehl curl abfragen.

Zusammenfassung

Als Betreiber ist die Kubernetes-API der wichtigste Dienst, den du deinen Nutzern zur Verfügung stellst. Um diesen Dienst effektiv anbieten zu können, ist es wichtig, die Kernkomponenten von Kubernetes zu verstehen und zu wissen, wie deine Nutzer/innen diese APIs zusammensetzen, um Anwendungen zu erstellen, um einen nützlichen und zuverlässigen Kubernetes-Cluster zu implementieren. Nach der Lektüre dieses Kapitels solltest du ein Grundwissen über die Kubernetes-API und ihre Verwendung haben.

Get Kubernetes verwalten now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.