Custom Resource Definitions (CRDs): estendendo o Kubernetes
1. Introdução às CRDs: Por que estender a API do Kubernetes?
O Kubernetes nativo oferece recursos poderosos como Pods, Services e Deployments, mas esses recursos genéricos nem sempre capturam as necessidades específicas de uma aplicação. Por exemplo, gerenciar um backup de banco de dados com Deployments e CronJobs exige múltiplos manifestos e lógica externa complexa. As Custom Resource Definitions (CRDs) resolvem esse problema permitindo que você crie seus próprios recursos na API do Kubernetes, como se fossem recursos nativos.
As CRDs funcionam como extensões da API REST do Kubernetes. Você define um novo tipo de recurso (ex: Backup), e o Kubernetes automaticamente cria endpoints REST para ele. A principal diferença entre CRDs e Aggregated APIs é que as CRDs não exigem um servidor de API externo — elas são implementadas diretamente pelo kube-apiserver. Aggregated APIs são recomendadas quando você precisa de lógica de validação ou autorização complexa no servidor de API.
2. Anatomia de uma CRD: Estrutura e Especificação
Uma CRD é definida usando um schema OpenAPI v3 que descreve a estrutura do spec e do status. O spec representa o estado desejado pelo usuário, enquanto o status é preenchido pelo controller com o estado atual.
As CRDs podem ser namespaced (isoladas por namespace) ou cluster-scoped (globais). Para recursos como backups, o escopo namespaced é mais comum, pois cada equipe gerencia seus próprios backups.
O versionamento é crítico: você pode definir múltiplas versões (ex: v1alpha1, v1beta1, v1) e especificar qual versão é usada para armazenamento (storage) e qual é servida para leitura/escrita.
3. Criando e Instalando sua Primeira CRD
Vamos criar uma CRD para um recurso Backup que gerencia backups de um banco de dados PostgreSQL.
# backup-crd.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: backups.mycompany.com
spec:
group: mycompany.com
names:
kind: Backup
plural: backups
singular: backup
shortNames:
- bk
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
databaseName:
type: string
minLength: 1
retentionDays:
type: integer
minimum: 1
maximum: 30
schedule:
type: string
pattern: "^[0-9*/, -]+$"
required:
- databaseName
- retentionDays
status:
type: object
properties:
phase:
type: string
enum:
- Pending
- Running
- Completed
- Failed
lastBackupTime:
type: string
format: date-time
message:
type: string
Instale a CRD e crie uma instância:
kubectl apply -f backup-crd.yaml
kubectl get crd backups.mycompany.com
# Criando uma instância (Custom Resource)
kubectl apply -f - <<EOF
apiVersion: mycompany.com/v1
kind: Backup
metadata:
name: meu-backup-postgres
namespace: production
spec:
databaseName: mydb
retentionDays: 7
schedule: "0 2 * * *"
EOF
kubectl get backups -n production
kubectl describe backup meu-backup-postgres -n production
Teste a validação: tente omitir databaseName ou definir retentionDays: 100 — o Kubernetes rejeitará a criação.
4. Controllers e Reconciliação: Dando Vida à CRD
Uma CRD sem controller é apenas uma estrutura de dados. O padrão de reconciliação é um loop contínuo no qual o controller lê o spec, compara com o estado atual (via status) e executa ações para convergir os dois.
Abaixo, um controller mínimo em Python usando a biblioteca kubernetes:
# controller.py
import time
import kubernetes
from kubernetes import client, config, watch
config.load_incluster_config()
api = client.CustomObjectsApi()
batch_api = client.BatchV1Api()
group = "mycompany.com"
version = "v1"
plural = "backups"
namespace = "production"
def reconcile(backup):
name = backup["metadata"]["name"]
spec = backup.get("spec", {})
status = backup.get("status", {"phase": "Pending"})
if status.get("phase") == "Completed":
return
# Criar um Job de backup
job_name = f"backup-{name}-{int(time.time())}"
job = {
"apiVersion": "batch/v1",
"kind": "Job",
"metadata": {"name": job_name, "namespace": namespace},
"spec": {
"template": {
"spec": {
"containers": [{
"name": "backup",
"image": "postgres:15",
"command": ["pg_dump", f"--dbname={spec['databaseName']}"]
}],
"restartPolicy": "Never"
}
},
"backoffLimit": 2
}
}
batch_api.create_namespaced_job(namespace, job)
# Atualizar status
patch = {
"status": {
"phase": "Running",
"message": f"Job {job_name} criado",
"lastBackupTime": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
}
}
api.patch_namespaced_custom_object_status(
group, version, namespace, plural, name, patch
)
# Loop de reconciliação
w = watch.Watch()
for event in w.stream(api.list_namespaced_custom_object, group, version, namespace, plural):
if event["type"] in ["ADDED", "MODIFIED"]:
reconcile(event["object"])
O controller atualiza o status.phase para refletir o progresso — essencial para observabilidade.
5. Admission Controllers e Validação Customizada
O schema OpenAPI v3 já valida tipos e formatos, mas regras de negócio complexas exigem webhooks. Um ValidatingAdmissionWebhook pode, por exemplo, verificar se o campo retentionDays não excede 30 dias (já feito no schema, mas podemos adicionar lógica mais complexa, como verificar permissões).
Exemplo de webhook de validação (simplificado):
# webhook-server.py
from flask import Flask, request, jsonify
import json
app = Flask(__name__)
@app.route("/validate", methods=["POST"])
def validate():
body = request.json
obj = body["request"]["object"]
# Validar regra de negócio: retentionDays <= 30
if obj["spec"]["retentionDays"] > 30:
return jsonify({
"response": {
"allowed": False,
"status": {
"message": "retentionDays cannot exceed 30"
}
}
})
return jsonify({"response": {"allowed": True}})
if __name__ == "__main__":
app.run(port=443, ssl_context=("cert.pem", "key.pem"))
Webhooks de mutação podem preencher defaults automaticamente, como definir retentionDays: 7 se o campo estiver vazio.
6. Operadores e o Operator SDK: Automatizando Ciclos de Vida
Um operador combina CRDs, controllers e lógica de ciclo de vida para automatizar tarefas complexas. O Operator SDK (Go) simplifica a criação de operadores.
Exemplo de operador para backups usando Operator SDK (Go):
// backup_controller.go (trecho)
func (r *BackupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var backup mycompanyv1.Backup
if err := r.Get(ctx, req.NamespacedName, &backup); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// Verificar se o backup já foi executado hoje
if backup.Status.Phase == "Completed" && isToday(backup.Status.LastBackupTime) {
return ctrl.Result{RequeueAfter: 1 * time.Hour}, nil
}
// Criar Job de backup
job := createBackupJob(&backup)
if err := r.Create(ctx, job); err != nil {
return ctrl.Result{}, err
}
// Atualizar status
backup.Status.Phase = "Running"
backup.Status.LastBackupTime = metav1.Now()
if err := r.Status().Update(ctx, &backup); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{RequeueAfter: 24 * time.Hour}, nil
}
Boas práticas incluem:
- Finalizers: impedem a deleção da CR até que o backup real seja limpo.
- Garbage collection: deletar Jobs antigos automaticamente.
- Reconciliação idempotente: o mesmo evento pode ser processado múltiplas vezes sem efeitos colaterais.
7. Boas Práticas, Versionamento e Segurança
Versionamento semântico: mantenha versões antigas servidas (served: true) enquanto a nova versão amadurece. Use storage: true apenas na versão estável. Para conversão entre versões (ex: v1alpha1 → v1), implemente um Conversion Webhook.
Segurança:
- RBAC mínimo: conceda apenas os recursos que o controller realmente precisa.
- Escopo de namespaces: evite CRDs cluster-scoped a menos que necessário.
- Proteção contra deleção acidental: use kubectl annotate crd backups.mycompany.com "helm.sh/resource-policy=keep".
8. Monitoramento e Observabilidade de Recursos Customizados
Exponha métricas do Prometheus a partir do status das CRs:
# metrics.py
from prometheus_client import Gauge, start_http_server
backup_phase = Gauge('backup_phase', 'Phase of backup resources', ['name', 'phase'])
def update_metrics():
backups = api.list_namespaced_custom_object(group, version, namespace, plural)
for bk in backups["items"]:
phase = bk["status"].get("phase", "Unknown")
backup_phase.labels(name=bk["metadata"]["name"], phase=phase).set(1)
start_http_server(8000)
while True:
update_metrics()
time.sleep(30)
Crie dashboards no Grafana para visualizar backups pendentes vs concluídos, tempo desde o último backup e taxas de falha. Logs estruturados no controller (JSON) facilitam a correlação com métricas.
Referências
- Documentação oficial de CRDs do Kubernetes — Guia completo sobre como criar e gerenciar Custom Resource Definitions.
- Operator SDK: Criando operadores Kubernetes — Ferramenta oficial para construir operadores Kubernetes com Go, Ansible ou Helm.
- Kubernetes Admission Controllers — Documentação sobre webhooks de validação e mutação.
- Prometheus Operator: Monitorando recursos customizados — Como expor métricas de CRDs e integrar com Prometheus.
- Kubebuilder: Framework para CRDs e controllers — Framework popular para construir CRDs e controllers em Go com boas práticas.
- Client-go: Biblioteca Go para Kubernetes — Biblioteca oficial para interagir com a API do Kubernetes a partir de controllers.
- CRD Conversion Webhooks — Como implementar conversão entre versões de CRDs.