Cloud Intelligence™Cloud Intelligence™

Cloud Intelligence™

Validating Admission Policies no Kubernetes: Casos de Uso Avançados

By Eyal ZekariaJun 5, 20238 min read

Esta página também está disponível em English, Deutsch, Español, Français, Italiano e 日本語.

No artigo anterior sobre o tema, falamos sobre os desafios de implementar o Dynamic Admission Control (DAC) no seu cluster Kubernetes usando Validating Admission Webhooks (VAW). Apresentamos o novo recurso Validation Admission Policy (VAP) e mostramos alguns casos de uso simples para demonstrar a sua simplicidade.

Hoje vamos focar em alguns recursos mais avançados da VAP:

  1. Correspondência e filtragem de recursos
  2. Parâmetros em políticas

Por fim, vamos discutir rapidamente quando faz sentido usar VAPs no lugar de VAWs e dar algumas dicas de migração.

Todos os exemplos do artigo anterior e deste estão disponíveis em um repositório no GitHub criado para esse fim.

Correspondência e filtragem de recursos

No caso de uso mostrado no artigo anterior, vimos como aplicar uma política para apenas um tipo de recurso (Deployment), o que faz sentido para validações específicas de recurso.

Mas e se quiséssemos aplicar uma política mais genérica no cluster, como impor uma convenção de nomenclatura?

Aqui temos algumas opções. A primeira é seguir o caminho explícito e criar uma regra de correspondência para cada tipo de recurso que queremos validar:

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'"

Você pode reutilizar a mesma regra para recursos do mesmo grupo/versão de API, claro, mas essa lista cresce muito rápido e dá trabalho manter (especialmente quando novos tipos de recurso são introduzidos no cluster).

A alternativa é fazer a correspondência com todos os recursos e excluir seletivamente aqueles que não queremos validar. A filtragem pode ser feita em dois níveis:

  1. dentro do recurso ValidatingAdmissionPolicy — usando .spec.matchConditions
  2. dentro do recurso ValidatingAdmissionPolicyBinding — usando .spec.matchResources.excludeResources

A primeira abordagem está documentada; a segunda, nem tanto. O .spec.matchConditions usa a Common Expression Language (CEL) para filtrar requisições que já passaram pelas regras (ou seja, que já passaram pelo binding e pelas restrições de correspondência da política).

Já as exclusões de recursos (.spec.matchResources.excludeResources) acontecem antes mesmo de o recurso chegar à política e usam a mesma sintaxe da correspondência para montar uma lista de exclusão.

Veja um exemplo de política que ignora recursos Lease do grupo de API coordination.k8s.io e qualquer recurso do grupo de API rbac.authorization.k8s.io usando match conditions em CEL:

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' # Cada match condition deve ter um nome único
      expression: '!(request.resource.group == "coordination.k8s.io" && request.resource.resource == "leases")' # Match em recursos que não sejam lease.
    - name: 'rbac' # Ignora requisições RBAC.
      expression: 'request.resource.group != "rbac.authorization.k8s.io"'
  validations:
    - expression: "!object.metadata.name.contains('demo') || object.metadata.namespace == 'demo'"

E aqui um binding que filtra requisições semelhantes via match exclusions:

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

Como os dois chegam praticamente ao mesmo resultado, eu recomendaria usar excludeResourceRules sempre que possível, já que a filtragem ocorre mais cedo e evita executar expressões CEL à toa (falamos sobre o orçamento de custo de execução de CEL no API server do Kubernetes no post anterior). Além disso, como ele usa a mesma sintaxe da correspondência de recursos, fica mais claro e fácil de entender.

Por outro lado, como o matchConditions usa expressões CEL para filtrar e tem acesso aos mesmos objetos das expressões de validação (object, oldObject, request e params), ele consegue filtrar coisas que o excludeResourceRules não alcança, como usuários/grupos ou qualquer outro atributo dos objetos disponíveis:

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)' # Permite requisições feitas por usuários que não sejam nodes.
  validations:
    - expression: 'object.metadata.name.startsWith("demo-")'

Resumindo, eu sugiro uma abordagem combinada: use excludeResourceRules quando quiser filtrar recursos específicos (por grupo de API, versão, operação, tipo de recurso) e use matchConditions quando precisar de uma filtragem mais avançada (requisições de usuários ou grupos específicos etc.).

Parametrizando políticas

Para deixar as políticas ainda mais poderosas, você pode parametrizá-las. Isso permite reutilizar uma política com valores de parâmetros diferentes sem precisar redefini-la para cada combinação. Na prática, você separa a configuração da política em si.

Para usar parâmetros, o operador do cluster precisa criar um CustomResourceDefinition (CRD) que defina e armazene esses parâmetros. Ou seja, dá um pouco mais de trabalho — mas o uso é opcional, então a decisão é sua.

Veja um exemplo simples de CRD, fácil de modificar ou estender se necessário:

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

O CRD acima permite criar recursos do tipo Parameters com um único campo customizado no schema, maxReplicas, do tipo inteiro:

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

Com isso pronto, configuramos a política para receber parâmetros (.spec.paramKind) apontando para a versão e o kind corretos da API (conforme definidos no CRD) e podemos usá-los nas expressões de validação:

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)"

Repare que o messageExpression também foi atualizado para usar o parâmetro, evitando valores hard-coded em qualquer ponto da política.

Por fim, atualizamos o binding para referenciar pelo nome o recurso Parameters criado:

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

Depois de aplicar tudo no cluster, tentar criar um deployment com mais réplicas do que o configurado vai falhar, como esperado:

$ 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

Assim, podemos criar diferentes recursos Parameters e vinculá-los a recursos/namespaces distintos, reutilizando nossas políticas parametrizadas.

VAW ou VAP: qual escolher

Como as VAPs são bem mais simples de implementar do que as VAWs, é provável que elas acabem substituindo as VAWs — ou, no mínimo, se tornem o ponto de partida padrão para validação dentro do cluster no Kubernetes.

O suporte a CEL no Kubernetes abriu muitas portas para melhorias de qualidade de vida na perspectiva de quem opera o cluster. Uma Mutating Admission Policy é outro exemplo de funcionalidade que pode virar realidade graças ao CEL. Dá para apostar que as VAPs vão continuar recebendo investimento e desenvolvimento até atingirem maturidade e GA.

Dito isso, as VAPs ainda não são uma substituta completa das VAWs. Com VAWs, você consegue executar qualquer lógica de validação customizada que couber em código, seja uma simples checagem de atributo da requisição ou até consultar o estado de um sistema externo antes de admitir uma requisição. Além disso, como você mesmo escreve o servidor de validação, tem liberdade para escolher tecnologia e linguagem — não fica preso ao CEL.

Outro ponto: como as VAPs ainda estão em Alpha (a partir da v1.26), elas não são uma alternativa viável para quem usa Kubernetes gerenciado nas principais nuvens. EKS e AKS não suportam features alpha; o GKE suporta, mas só para fins de teste (clusters alpha expiram em 30 dias). Ou seja, mesmo que pudesse, provavelmente ainda não é hora de adotar as VAPs como o único mecanismo de validação dentro do cluster da sua organização.

Dicas para migrar de VAWs para VAPs

Você está lendo isto em um momento em que as VAPs já estão em beta ou GA e pensando em usá-las no lugar da sua solução atual? A boa notícia é que esses 2 recursos não são mutuamente exclusivos — dá para rodar os dois lado a lado durante a transição.

Como VAWs e VAPs fazem correspondência de recursos de forma parecida, criar VAPs deve ser um processo bem direto. Aqui vão algumas dicas para começar:

  • Crie uma validatingAdmissionPolicy com as mesmas .spec.matchConstraints.resourceRules que as .webhooks.rules do seu recurso ValidatingWebhookConfiguration.
  • Migre suas expressões de validação da linguagem atual (provavelmente Rego) para CEL. Essa é, de longe, a tarefa mais demorada, mas vale tentar usar o ChatGPT — regras de política costumam ser diretas o suficiente para ele dar conta com qualidade razoável.
  • Crie um ValidatingAdmissionPolicyBinding e defina .spec.validationActions como Warn para retornar erros ao cliente sem bloquear nenhuma requisição da API.
  • Decida como vincular a política aos recursos via .spec.matchResources. Pode ser um simples matcher de label de namespace ou uma match expression mais avançada. Rotule seus namespaces de acordo.
  • Tente criar recursos válidos e inválidos nos namespaces gerenciados, avalie os warnings retornados (ou ausentes) e refine as expressões ao longo do caminho.
  • Quando estiver confiante na política, mude o .spec.ValidationActions do binding para Deny e coloque a política em pleno efeito.
  • Remova quaisquer políticas duplicadas do sistema legado.

Recursos adicionais