Guia para Estender a API Kubernetes: Controle de Recursos do Cluster Usando Webhooks

Introdução

Kubernetes se consolidou como a principal plataforma de orquestração de containers, permitindo que aplicações sejam executadas com total abstração das máquinas que fornecem os recursos computacionais. Essa flexibilidade, porém, pode trazer desafios significativos, especialmente em termos de custos e conformidade no ambiente tecnológico. Definir políticas de uso de recursos diretamente com os desenvolvedores nem sempre é suficiente para garantir que essas diretrizes sejam seguidas.

Felizmente, a extensibilidade do Kubernetes nos permite ir além. Através de webhooks, podemos interceptar alterações em recursos criados, editados ou removidos dentro do cluster, permitindo a implementação automática de políticas personalizadas.

Neste post, vamos construir, de forma prática, um interceptador para controlar alterações em deployments, garantindo que políticas específicas sejam obedecidas, como:

  • Limite de réplicas: nenhum deployment pode ter mais de duas réplicas.

  • Restrição de recursos: containers não podem requisitar mais de 100mCPU ou 250Mi de memória.

  • Aplicação em escopo específico: as regras devem se aplicar somente ao namespace production.

Acompanhe o passo a passo para implementar essas políticas de maneira eficiente e automatizada no seu cluster Kubernetes.

Requisitos

  1. Cluster Kubernetes com acesso via kubectl

  2. Tunnel NGROK para expormos nosso localhost ao cluster kubernetes.

Roadmap

  • Configurar um webhook no namespace production: O webhook será responsável por interceptar todas as solicitações relacionadas a recursos do tipo deployment na API version v1 do grupo apps, garantindo que cada alteração passe por validação antes de ser aplicada.

  • Desenvolver um microserviço em Go: Criar um microserviço dedicado com um endpoint específico para receber as requisições de admissão do webhook. Este endpoint será responsável por processar as validações necessárias e retornar a aprovação ou rejeição conforme as políticas estabelecidas.

Controle de admissão

O Kubernetes oferece um recurso chamado ValidatingWebhookConfiguration, que permite configurar webhooks para serem acionados sempre que o cluster receber uma solicitação de admissão, ou seja, alterações em recursos dentro do cluster. Nesse recurso, é possível especificar o webhook a ser invocado e os parâmetros que determinam quando e como ele será executado.

Para implementar essa configuração, crie um arquivo chamado ValidatingWebhook.yaml com o seguinte conteúdo:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: deployment-validation
  namespace: production
webhooks:
  - name: "deployment-validation.production.svc"
    namespaceSelector:
      matchExpressions:
        - key: kubernetes.io/metadata.name
          operator: In
          values: ["production"]
    rules:
      - operations: ["CREATE", "UPDATE", "DELETE"]
        apiGroups: ["apps"]
        apiVersions: ["v1"]
        resources: ["deployments"]
        scope: "Namespaced"
    clientConfig:
      url: [URL-NGROK]
    admissionReviewVersions: ["v1"]
    sideEffects: None
    timeoutSeconds: 30

Neste manifesto, estamos detalhando os requisitos descritos no item 1 do nosso roadmap. Para aplicar essa configuração no cluster, execute o comando abaixo (lembre-se de substituir pela URL gerada pelo seu ngrok):

kubectl apply -f ValidatingWebhook.yaml

Confirme que o webhook foi criado com o seguinte comando

kubectl get ValidatingWebhookConfiguration

O retorno deverá ser algo parecido com:

Com isso, nosso webhook está devidamente configurado, e todas as solicitações que atendam aos critérios definidos serão interceptadas. Para validar sua funcionalidade, vamos testar criando um deployment do Nginx. Crie um arquivo chamado deployment-nginx.yaml com o seguinte conteúdo:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  namespace: production
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:latest
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: 50m
              memory: 200Mi

Execute o seguinte comando para criar o recurso:

kubectl apply -f deployment-nginx.yaml

É esperado que ocorra um erro nesse momento, pois ainda não configuramos a rota que será responsável por processar a solicitação de admissão.

Serviço de admissão da requisição.

Vamos iniciar a criação de uma API em Go utilizando o framework Gin e as dependências necessárias para manipular objetos da API do Kubernetes. Para configurar o ambiente, execute os seguintes comandos:

go mod init github.com/meurepositorio/admission_webhook
go get github.com/gin-gonic/gin
go get k8s.io/api
go get k8s.io/apimachinery

A lógica do nosso endpoint, responsável por processar as requisições de admissão, pode ser expressa da seguinte forma:

package handlers

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"

    "github.com/gin-gonic/gin"
    v1 "k8s.io/api/admission/v1"
    appsv1 "k8s.io/api/apps/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const MAX_CPU_VALUE int64 = 100
const MAX_MEMORY_VALUE int64 = 250
const MAX_REPLICAS int32 = 2

// validateResourceContainers validates the CPU and memory resource requests of containers in a deployment.
// It checks if the CPU and memory values provided in the deployment's containers are within the allowed limits.
// If any of the values exceed the maximum limits, an error is returned.
// The function takes a deployment object as input and iterates over the containers in the deployment's template.
// It retrieves the CPU and memory values from the container's resource requests and compares them with the maximum limits.
// If any value exceeds the limit, an error message is returned with the corresponding resource type and value.
// If all values are within the limits, nil is returned.
func validateResourceContainers(deployment appsv1.Deployment) error {
    for _, container := range deployment.Spec.Template.Spec.Containers {
        cpuValue := container.Resources.Requests.Cpu().Value()
        memoryValue := container.Resources.Requests.Memory().Value()
        if cpuValue > MAX_CPU_VALUE {
            return fmt.Errorf("cpu request is too high, the maximum value is %f and you provided %f", MAX_CPU_VALUE, cpuValue)
        }

        if memoryValue > MAX_MEMORY_VALUE {
            return fmt.Errorf("memory request is too high, the maximum value is %f and you provided %f", MAX_MEMORY_VALUE, memoryValue)
        }
    }
    return nil
}

// validateReplicasContainer validates the number of replicas in a deployment.
// It checks if the number of replicas provided in the deployment is within the allowed limit.
// If the number of replicas exceeds the maximum limit, an error is returned.
// The function takes a deployment object as input and compares the number of replicas with the maximum limit.
// If the number of replicas exceeds the limit, an error message is returned with the provided number of replicas and the maximum limit.
// If the number of replicas is within the limit, nil is returned.
func validateReplicasContainer(deployment appsv1.Deployment) error {
    if *deployment.Spec.Replicas > MAX_REPLICAS {
        return fmt.Errorf("the maximum number of replicas is %d and you provided %d", MAX_REPLICAS, *deployment.Spec.Replicas)
    }
    return nil
}

// admitRequest sets the admission response to allow the request.
// It takes an admission review object as input and sets the response to allowed with the same UID as the request.
// The modified admission review object is returned.
func admitRequest(admissionReview *v1.AdmissionReview) *v1.AdmissionReview {
    admissionReview.Response = &v1.AdmissionResponse{
        Allowed: true,
        UID:     admissionReview.Request.UID,
        Result: &metav1.Status{ 
            Status: "Success",
             Message: "The deployment is valid",
        },
    }
    return admissionReview
}

func rejectRequest(admissionReview *v1.AdmissionReview, message string) *v1.AdmissionReview {
    admissionReview.Response = &v1.AdmissionResponse{
        Allowed: false,
        Result: &metav1.Status{ 
            Status: "Failure",
            Message: message,
        },
        UID: admissionReview.Request.UID,
    }

    return admissionReview;
}

// validateDeploymentHandler is the handler function for validating a deployment.
// It takes a Gin context as input and validates the deployment in the request body.
// If the deployment is valid, an admission review with allowed status is returned.
// If the deployment is invalid, an error response is returned.
func ValidateDeploymentHandler(context *gin.Context) {

    jsonAdmission, err := io.ReadAll(context.Request.Body)
    if err != nil {
        context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    incomeAdmissionReview := &v1.AdmissionReview{}

    err = json.Unmarshal(jsonAdmission, incomeAdmissionReview)
    if err != nil {
        rejectedAdmissionReview := rejectRequest(incomeAdmissionReview, err.Error())
        context.JSON(http.StatusBadRequest, rejectedAdmissionReview)
        return
    }

    deployment := appsv1.Deployment{}
    err = json.Unmarshal(incomeAdmissionReview.Request.Object.Raw, &deployment)
    if err != nil {
        rejectedAdmissionReview := rejectRequest(incomeAdmissionReview, err.Error())
        context.JSON(http.StatusBadRequest, rejectedAdmissionReview)
        return
    }

    err = validateResourceContainers(deployment)
    if err != nil {
        rejectedAdmissionReview := rejectRequest(incomeAdmissionReview, err.Error())
        context.JSON(http.StatusBadRequest, rejectedAdmissionReview)
        return
    }

    err = validateReplicasContainer(deployment)
    if err != nil {
        rejectedAdmissionReview := rejectRequest(incomeAdmissionReview, err.Error())
        context.JSON(http.StatusBadRequest, rejectedAdmissionReview)
        return
    }

    admimittedAdmissionReview := admitRequest(incomeAdmissionReview)
    context.JSON(http.StatusOK, admimittedAdmissionReview)
    return 
}

O código completo da aplicação pode ser conferido no Github.

Com o serviço devidamente configurado e em execução localmente, utilizando o ngrok para expor sua URL pública, podemos executar o seguinte comando:

kubectl apply -f deployment-nginx.yaml

Como o deployment do Nginx atende às regras estabelecidas, ele será aplicado com sucesso. No entanto, se ajustarmos a solicitação de CPU para 200m, nosso webhook de admissão irá rejeitar o deployment, garantindo o cumprimento das políticas definidas.

Outras ideias de extensões:

Além das políticas de validação de réplicas e limites de CPU e memória que abordamos, o Kubernetes oferece diversas outras possibilidades para garantir o controle e a conformidade no seu cluster. Abaixo, apresentamos algumas ideias de regras adicionais que podem ser implementadas através de webhooks de validação:

  1. Definição de Horizontal Pod Autoscaler (HPA):

    • Regra: Garantir que todos os deployments tenham o HPA configurado, para que o escalonamento automático dos pods seja habilitado quando necessário.

    • Exemplo de Validação: O webhook pode verificar se o HPA está presente no deployment e se as métricas de CPU/memória estão configuradas corretamente para garantir o escalonamento eficiente.

  2. Limitação de Recursos de CPU e Memória:

    • Regra: Definir limites máximos e mínimos para o uso de CPU e memória em containers.

    • Exemplo de Validação: O webhook pode ser configurado para garantir que os containers não ultrapassem limites pré-estabelecidos de recursos, evitando que um container monopolize a capacidade do nó.

  3. Política de Uso de Imagens:

    • Regra: Garantir que os containers utilizem imagens de repositórios aprovados e verificados.

    • Exemplo de Validação: O webhook pode rejeitar o deployment caso a imagem do container não esteja em uma lista de imagens aprovadas ou não possua uma tag específica (como stable ou latest).

  4. Uso de ServiceAccount Padrão:

    • Regra: Garantir que os deployments utilizem ServiceAccounts específicas para minimizar os privilégios concedidos aos pods.

    • Exemplo de Validação: O webhook pode verificar se o deployment está usando a ServiceAccount correta e rejeitar aqueles que utilizam a ServiceAccount padrão ou não possuem nenhuma configurada.

  5. Controle de Acessos (RBAC):

    • Regra: Assegurar que os pods estejam configurados com permissões adequadas de acesso aos recursos do cluster.

    • Exemplo de Validação: O webhook pode validar se os pods estão utilizando políticas de RBAC que concedem apenas as permissões mínimas necessárias, reforçando o princípio do menor privilégio.

  6. Política de Nomes de Recursos:

    • Regra: Validar que os nomes de deployments, pods, e outros recursos estejam em conformidade com uma convenção de nomenclatura específica.

    • Exemplo de Validação: O webhook pode garantir que os nomes de recursos sigam um padrão, como a inclusão de prefixos para identificar o ambiente (e.g., dev, staging, prod) ou o time responsável.

  7. Regras de Segurança (PodSecurityPolicy ou PSP):

    • Regra: Garantir que os pods sigam políticas de segurança, como a proibição de containers rodando como root ou com permissões excessivas.

    • Exemplo de Validação: O webhook pode ser configurado para verificar configurações de segurança do pod, garantindo que apenas containers com permissões restritas possam ser executados.

  8. Estrutura de NetworkPolicies:

    • Regra: Assegurar que todos os deployments tenham as NetworkPolicies adequadas configuradas para controlar o tráfego de rede entre os pods.

    • Exemplo de Validação: O webhook pode garantir que a comunicação entre os pods seja restrita apenas ao necessário, bloqueando acessos indesejados ou vulneráveis.

Conclusão

Neste post, exploramos como o Kubernetes pode ser estendido para garantir conformidade e controle sobre os recursos criados dentro do cluster. Implementamos um webhook de validação que aplica políticas específicas em tempo de admissão, mostrando como é possível automatizar e reforçar boas práticas sem depender exclusivamente de acordos manuais.

Essa abordagem destaca a flexibilidade e o poder do Kubernetes em cenários complexos, permitindo que clusters sejam gerenciados de forma mais eficiente, segura e escalável. Você pode adaptar esse exemplo para aplicar outras políticas conforme as necessidades do seu ambiente, ampliando ainda mais o controle e a governança sobre os seus recursos.

Se você achou útil este guia ou tem sugestões para melhorias, sinta-se à vontade para compartilhar nos comentários! 🚀

0
Subscribe to my newsletter

Read articles from Victor Nascimento directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Victor Nascimento
Victor Nascimento

Electrical Engineering by Universidade Federal Fluminense 🌱 Learning Large Language Models for processing automation and how to utilize blockchain (Ethereum) on enterprise environment Working as Software Engineering @ BTG Pactual Several certifications on AWS platform and Brazilian Financial Markets