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:
- Correspondência e filtragem de recursos
- 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:
- dentro do recurso
ValidatingAdmissionPolicy— usando.spec.matchConditions - 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
validatingAdmissionPolicycom as mesmas.spec.matchConstraints.resourceRulesque as.webhooks.rulesdo seu recursoValidatingWebhookConfiguration. - 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
ValidatingAdmissionPolicyBindinge defina.spec.validationActionscomoWarnpara 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.ValidationActionsdo binding paraDenye coloque a política em pleno efeito. - Remova quaisquer políticas duplicadas do sistema legado.
Recursos adicionais
- Effortless In-Cluster Validation with Kubernetes: Introducing Validating Admission Policies
- validating-admission-policy-playground — repositório no GitHub com exemplos completos e funcionais de Validating Admission Policies
- Validating Admission Policy na documentação oficial do Kubernetes
- Common Expression Language no Kubernetes
- Kubernetes Enhancement Proposal (KEP)-3488 — CEL para Admission Control
- The Path to Self Contained CRDs — palestra de Cici Huang