Cloud Intelligence™Cloud Intelligence™

Cloud Intelligence™

Validating Admission Policies in Kubernetes: Fortgeschrittene Anwendungsfälle

By Eyal ZekariaJun 5, 20238 min read

Diese Seite ist auch in English, Español, Français, Italiano, 日本語 und Português verfügbar.

In unserem vorherigen Artikel zu diesem Thema haben wir die Herausforderungen bei der Umsetzung von Dynamic Admission Control (DAC) in Ihrem Kubernetes-Cluster mit Validating Admission Webhooks (VAW) beleuchtet. Außerdem haben wir die neue Ressource Validation Admission Policy (VAP) vorgestellt und an einigen einfachen Anwendungsfällen gezeigt, wie unkompliziert sie sich nutzen lässt.

Heute schauen wir uns einige fortgeschrittenere Funktionen von VAP genauer an:

  1. Resource Matching und Filterung
  2. Parameter in Policies

Zum Schluss gehen wir kurz darauf ein, wann sich VAPs gegenüber VAWs anbieten, und geben Tipps für die Migration.

Sämtliche Beispiele aus dem vorherigen und aus diesem Artikel finden Sie in einem eigens dafür erstellten GitHub-Repository.

Resource Matching und Filterung

Im Anwendungsfall aus dem vorherigen Artikel haben wir gesehen, wie sich eine Policy nur auf einen einzigen Ressourcentyp (Deployment) anwenden lässt — was bei ressourcenspezifischen Validierungen sinnvoll ist.

Doch was, wenn wir eine generischere Policy clusterweit durchsetzen wollen, etwa eine Namenskonvention?

Dafür gibt es mehrere Wege. Der explizite Weg: Wir legen für jeden Ressourcentyp, der validiert werden soll, eine eigene Resource-Matching-Regel an:

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicy
metadata:
  name: "demo-policy.example.com"
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups:   ["apps"]
        apiVersions: ["v1"]
        operations:  ["CREATE", "UPDATE"]
        resources:   ["deployments", "statefulsets"]
      - apiGroups:   ["autoscaling"]
        apiVersions: ["v2"]
        operations:  ["CREATE", "UPDATE"]
        resources:   ["horizontalpodautoscalers"]
  validations:
    - expression: "!object.metadata.name.contains('demo') || object.metadata.namespace == 'demo'"

Klar, für Ressourcen derselben API-Gruppe bzw. -Version lässt sich dieselbe Regel verwenden — aber diese Liste wird schnell sehr lang und muss laufend gepflegt werden, vor allem, wenn neue Ressourcentypen ins Cluster kommen.

Die Alternative: alle Ressourcen matchen und gezielt diejenigen ausschließen, die nicht validiert werden sollen. Resource Filtering kann auf zwei Ebenen passieren:

  1. innerhalb der Ressource ValidatingAdmissionPolicy — über .spec.matchConditions
  2. innerhalb der Ressource ValidatingAdmissionPolicyBinding — über .spec.matchResources.excludeResources

Der erste Ansatz ist dokumentiert, der zweite weniger gut. .spec.matchConditions nutzt die Common Expression Language (CEL), um Requests zu filtern, die bereits durch die Regeln gematcht wurden — also nach dem Binding und den Match Constraints der Policy.

Resource Exclusions (.spec.matchResources.excludeResources) hingegen greifen, bevor die Ressource die Policy überhaupt erreicht, und nutzen für die Ausschlussliste dieselbe Syntax wie das Resource Matching.

Hier ein Beispiel für eine Policy, die Lease-Ressourcen aus der API-Gruppe coordination.k8s.io sowie alle Ressourcen aus der API-Gruppe rbac.authorization.k8s.io per CEL Match Conditions überspringt:

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicy
metadata:
  name: "demo-policy.example.com"
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups:   ["*"]
        apiVersions: ["*"]
        operations:  ["CREATE", "UPDATE"]
        resources:   ["*"]
  matchConditions:
    - name: 'exclude-leases' # Each match condition must have a unique name
      expression: '!(request.resource.group == "coordination.k8s.io" && request.resource.resource == "leases")' # Match non-lease resources.
    - name: 'rbac' # Skip RBAC requests.
      expression: 'request.resource.group != "rbac.authorization.k8s.io"'
  validations:
    - expression: "!object.metadata.name.contains('demo') || object.metadata.namespace == 'demo'"

Und so sieht ein Binding aus, das vergleichbare Requests über Match Exclusions herausfiltert:

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "demo-binding-test.example.com"
spec:
  policyName: "demo-policy.example.com"
  validationActions: [Deny]
  matchResources:
    excludeResourceRules:
      - apiGroups:   ["rbac.authorization.k8s.io"]
        apiVersions: ["*"]
        operations:  ["*"]
        resources:   ["*"]
      - apiGroups:   ["coordination.k8s.io"]
        apiVersions: ["*"]
        operations:  ["*"]
        resources:   ["leases"]
    namespaceSelector:
      matchLabels:
        environment: demo

Da beide Varianten praktisch dasselbe leisten, würde ich nach Möglichkeit zu excludeResourceRules raten: Die Filterung greift früher, und es werden keine CEL-Ausdrücke unnötig ausgewertet (das Cost Budget für CEL im Kubernetes-API-Server haben wir im vorherigen Beitrag behandelt). Hinzu kommt: Da hier dieselbe Syntax wie beim Matching verwendet wird, ist das Ganze meiner Meinung nach übersichtlicher und leichter nachzuvollziehen.

Weil matchConditions jedoch CEL-Ausdrücke für die Filterung einsetzt und auf dieselben Objekte zugreifen kann wie die Validierungsausdrücke (object, oldObject, request und params), lassen sich damit auch Dinge filtern, die mit excludeResourceRules nicht möglich sind — etwa Benutzer bzw. Gruppen oder beliebige andere Attribute der verfügbaren Objekte:

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicy
metadata:
 name: "demo-policy.example.com"
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups:   ["*"]
        apiVersions: ["*"]
        operations:  ["CREATE", "UPDATE"]
        resources:   ["*"]
  matchConditions:
    - name: 'exclude-kubelet-requests'
      expression: '!("system:nodes" in request.userInfo.groups)' # Allow requests made by non-node users.
  validations:
    - expression: 'object.metadata.name.startsWith("demo-")'

Kurz gesagt: Ich würde einen kombinierten Ansatz empfehlen — excludeResourceRules für das Herausfiltern bestimmter Ressourcen (nach API-Gruppe, Version, Operation, Ressourcentyp) und matchConditions, sobald eine feinere Filterung nötig wird (Requests bestimmter Benutzer, Gruppen usw.).

Policies parametrisieren

Noch leistungsfähiger werden Policies, wenn Sie sie parametrisieren. So lässt sich eine Policy mit unterschiedlichen Parameterwerten wiederverwenden, ohne sie für jede Variante neu definieren zu müssen — die Konfiguration wird also von der Policy selbst entkoppelt.

Für den Einsatz von Parametern muss der Cluster-Operator eine CustomResourceDefinition (CRD) anlegen, in der die Parameter definiert und gehalten werden. Das ist mit etwas Mehraufwand verbunden — aber Sie müssen Parameter nicht zwingend nutzen, das bleibt Ihnen überlassen.

Hier ein einfaches Beispiel für eine CRD, die sich bei Bedarf leicht anpassen oder erweitern lässt:

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: parameters.example.com
spec:
  group: example.com
  names:
    kind: Parameters
    plural: parameters
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        type: object
        properties:
          maxReplicas:
            type: integer
    served: true
    storage: true
  scope: Cluster

Mit der obigen CRD können wir Ressourcen vom Typ Parameters mit einem einzigen Custom Field im Schema erstellen: maxReplicas vom Typ Integer:

apiVersion: example.com/v1
kind: Parameters
metadata:
  name: demo-parameters
maxReplicas: 3

Anschließend konfigurieren wir die Policy so, dass sie Parameter entgegennimmt (.spec.paramKind), indem wir die passende API-Version und das Kind aus unserer CRD referenzieren — und nutzen sie dann in den Validierungsausdrücken:

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicy
metadata:
  name: "demo-policy.example.com"
spec:
  failurePolicy: Fail
  paramKind:
    apiVersion: example.com/v1
    kind: Parameters
  matchConstraints:
    resourceRules:
      - apiGroups:   ["apps"]
        apiVersions: ["v1"]
        operations:  ["CREATE", "UPDATE"]
        resources:   ["deployments"]
  validations:
    - expression: "object.spec.replicas <= params.maxReplicas"
      messageExpression: "'Deployments cannot have more than ' + string(params.maxReplicas) + ' replicas, this one has ' + string(object.spec.replicas)"

Beachten Sie: Auch die messageExpression wurde so angepasst, dass sie den Parameter verwendet — fest verdrahtete Werte in der Policy entfallen damit komplett.

Zum Abschluss aktualisieren wir das Binding so, dass es die zuvor erstellte Parameters-Ressource per Name referenziert:

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "demo-binding-test.example.com"
spec:
  policyName: "demo-policy.example.com"
  validationActions: [Deny]
  paramRef:
    name: "demo-parameters"
  matchResources:
    namespaceSelector:
      matchLabels:
        environment: demo

Sobald alles im Cluster aktiv ist, schlägt der Versuch, ein Deployment mit mehr als der konfigurierten Anzahl an Replicas zu erstellen, erwartungsgemäß fehl:

$ k create deployment nginx — image=nginx — replicas=6
error: failed to create deployment: deployments.apps "nginx" is forbidden: ValidatingAdmissionPolicy 'demo-policy.example.com' with binding 'demo-binding-test.example.com' denied request: Deployments cannot have more than 3 replicas, this one has 6

So lassen sich verschiedene Parameters-Ressourcen anlegen und an unterschiedliche Ressourcen bzw. Namespaces binden — und parametrisierte Policies bequem wiederverwenden.

VAW oder VAP — was nehmen?

Da VAPs deutlich einfacher umzusetzen sind als VAWs, ist es wahrscheinlich, dass sie VAWs früher oder später ablösen — oder zumindest zum Standard für den Einstieg in die In-Cluster-Validierung in Kubernetes werden.

Der CEL-Support in Kubernetes hat aus Sicht von Cluster-Operatoren viele Türen für Quality-of-Life-Verbesserungen geöffnet. Eine Mutating Admission Policy ist ein weiteres Beispiel für ein Feature, das dank CEL Realität werden könnte. Es ist davon auszugehen, dass VAPs weiter im Fokus bleiben und Schritt für Schritt zur Reife und allgemeinen Verfügbarkeit geführt werden.

Trotzdem sind VAPs noch kein vollwertiger Ersatz für VAWs. Mit VAWs lässt sich nach wie vor jede individuelle Validierungslogik abbilden, die sich in Code gießen lässt — von der einfachen Validierung von Request-Attributen bis hin zur Prüfung des Zustands eines externen Systems, bevor ein Request zugelassen wird. Hinzu kommt: Weil Sie den Validierungsserver selbst schreiben, sind Sie bei Technologie und Sprache frei und nicht auf CEL festgelegt.

Außerdem stecken VAPs immer noch im Alpha-Stadium (ab v1.26) und sind damit für alle, die Managed-Kubernetes-Angebote der großen Cloud-Anbieter nutzen, noch keine echte Option. EKS und AKS unterstützen keine Alpha-Features; GKE tut es zwar, aber nur zu Testzwecken (Alpha-Cluster laufen nach 30 Tagen ab). Auch wenn es technisch ginge: VAPs sollten Sie aktuell noch nicht als alleinigen Mechanismus für die In-Cluster-Validierung in Ihrer Organisation einsetzen.

Tipps für die Migration von VAWs zu VAPs

Lesen Sie diesen Artikel zu einem Zeitpunkt, an dem VAPs bereits Beta- oder GA-Status erreicht haben, und überlegen, sie anstelle Ihrer aktuellen Lösung einzusetzen? Zum Glück schließen sich die beiden Features nicht gegenseitig aus — Sie können sie in der Übergangsphase also parallel betreiben.

Da VAWs und VAPs Ressourcen ähnlich matchen, ist das Anlegen von VAPs in der Regel überschaubar. Ein paar Tipps für den Einstieg:

  • Legen Sie eine validatingAdmissionPolicy mit denselben .spec.matchConstraints.resourceRules an wie die .webhooks.rules Ihrer ValidatingWebhookConfiguration-Ressource.
  • Migrieren Sie Ihre Validierungsausdrücke aus der bisherigen Sprache (vermutlich Rego) nach CEL. Das ist mit Abstand die zeitaufwendigste Aufgabe — probieren Sie ruhig ChatGPT dafür aus. Policy-Regeln sind meist klar genug strukturiert, sodass das Ergebnis durchaus brauchbar ausfällt.
  • Erstellen Sie ein ValidatingAdmissionPolicyBinding und setzen Sie .spec.validationActions auf Warn: Fehler werden an den Client zurückgegeben, ohne API-Requests zu blockieren.
  • Legen Sie fest, wie eine Policy über .spec.matchResources an Ressourcen gebunden wird. Das kann ein einfacher Namespace-Label-Matcher sein oder eine fortgeschrittenere Match Expression. Versehen Sie Ihre Namespaces mit den passenden Labels.
  • Versuchen Sie, in den verwalteten Namespaces gültige und ungültige Ressourcen zu erstellen, werten Sie die zurückgegebenen (oder ausbleibenden) Warnungen aus und schärfen Sie Ihre Ausdrücke nach.
  • Wenn Sie mit Ihrer Policy zufrieden sind, setzen Sie .spec.ValidationActions im Binding auf Deny, um die Policy vollständig scharf zu schalten.
  • Entfernen Sie redundante Policies aus dem Altsystem.

Weiterführende Ressourcen