O que são Custom Resource Definitions (CRDs)?
Custom Resource Definitions são uma forma poderosa de estender a API do Kubernetes, permitindo que você defina novos tipos de recursos personalizados além dos objetos nativos (Pods, Deployments, Services, etc.). Quando você cria uma CRD, está essencialmente ensinando ao Kubernetes a reconhecer e gerenciar um novo tipo de recurso como se fosse um objeto de primeira classe. Isso significa que você pode usar kubectl para criar, listar, atualizar e deletar instâncias do seu recurso personalizado, exatamente como faria com qualquer outro objeto Kubernetes.
A importância das CRDs vai além de uma simples extensão sintática. Elas formam a base para a criação de operadores Kubernetes, ferramentas que automatizam operações complexas em aplicações. Pense em um CRD como um esquema que define a estrutura de dados que sua aplicação precisa armazenar e gerenciar. O Kubernetes fornece a infraestrutura (armazenamento em etcd, API REST, validação, etc.), e você fornece a lógica de negócio através de um controlador que "observa" essas mudanças e as executa.
Arquitetura e Funcionamento das CRDs
Como as CRDs se Integram ao Kubernetes
Quando você registra uma CRD no cluster, o Kubernetes cria automaticamente novos endpoints de API para aquele recurso. Cada instância de sua CRD é armazenada no etcd, o banco de dados distribuído do Kubernetes. Um controlador (geralmente executado dentro de um Pod) observa essas mudanças através do mecanismo de watch da API Kubernetes e executa a lógica desejada.
O fluxo é simples mas poderoso: você escreve um manifesto YAML descrevendo a estrutura do seu CRD, aplica esse manifesto ao cluster com kubectl apply, e então pode criar instâncias daquele recurso. Um controlador custom que você escreve (ou obtém de um provedor) fica "escutando" essas mudanças e reage de acordo. Essa separação entre definição (CRD) e comportamento (controlador) é o que torna Kubernetes tão extensível.
Validação e Schema
As CRDs suportam validação de esquema OpenAPI 3.0, o que significa que você pode definir regras sobre quais campos são obrigatórios, seus tipos de dados, valores mínimos/máximos, e padrões de expressões regulares. Essa validação acontece no servidor, garantindo que dados inválidos nunca sejam armazenados no etcd. Isso protege a integridade dos seus dados e reduz bugs causados por estados inconsistentes.
Você também pode usar validação de regras customizadas através de CEL (Common Expression Language) para lógica mais complexa, permitindo validações que não cabem em regras simples de schema. Por exemplo, você poderia validar que um campo expiryDate seja sempre posterior a creationDate.
Criando uma CRD Prática: Exemplo Real
Definindo a CRD
Vamos criar um exemplo prático: uma CRD para gerenciar aplicações de banco de dados. Chamaremos de Database. Aqui está a definição completa:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: databases.data.example.com
spec:
group: data.example.com
names:
kind: Database
plural: databases
singular: database
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
description: "Define uma instância de banco de dados PostgreSQL"
properties:
spec:
type: object
required:
- engine
- version
- storage
properties:
engine:
type: string
enum: ["postgresql", "mysql", "mariadb"]
description: "Engine do banco de dados"
version:
type: string
pattern: '^\d+\.\d+\.\d+$'
description: "Versão do banco de dados (ex: 14.2.5)"
storage:
type: string
pattern: '^\d+Gi$'
description: "Tamanho do volume de armazenamento"
backupEnabled:
type: boolean
default: false
connectionPoolSize:
type: integer
minimum: 1
maximum: 100
default: 10
status:
type: object
properties:
phase:
type: string
enum: ["Pending", "Running", "Failed"]
message:
type: string
lastUpdateTime:
type: string
format: date-time
Salve este arquivo como database-crd.yaml e aplique ao cluster:
kubectl apply -f database-crd.yaml
Você pode verificar se a CRD foi registrada com sucesso:
kubectl get crd databases.data.example.com
Criando Instâncias da CRD
Agora que a CRD está definida, você pode criar instâncias dela. Aqui está um exemplo de recurso Database:
apiVersion: data.example.com/v1
kind: Database
metadata:
name: production-db
namespace: default
spec:
engine: postgresql
version: 14.2.5
storage: 100Gi
backupEnabled: true
connectionPoolSize: 50
Salve como database-instance.yaml e aplique:
kubectl apply -f database-instance.yaml
Agora você pode gerenciar sua database como qualquer outro recurso Kubernetes:
# Listar todas as databases
kubectl get databases
# Obter detalhes de uma database específica
kubectl describe database production-db
# Editar uma database
kubectl edit database production-db
# Deletar uma database
kubectl delete database production-db
Desenvolvendo um Controlador para sua CRD
Arquitetura de um Controlador
Um controlador é um programa que observa mudanças em recursos (eventos de criação, atualização, deleção) e executa lógica em resposta. O controlador usa a API Kubernetes para monitorar o estado desejado (spec) e atualizando o estado observado (status) conforme a lógica é executada. Essa é a essência da reconciliação: o controlador continua tentando fazer com que o estado atual corresponda ao estado desejado.
Existem bibliotecas especializadas para construir controladores, como Operator SDK, Kubebuilder e client-go. Aqui usaremos uma abordagem com Python e a biblioteca kopf (Kubernetes Operator Framework), que é simples e poderosa.
Implementação em Python com Kopf
Primeiro, instale as dependências:
pip install kopf pyyaml kubernetes
Aqui está um controlador funcional para nossa CRD Database:
import kopf
import kubernetes
from datetime import datetime
@kopf.on.event('data.example.com', 'v1', 'databases')
def monitor_database(event, **kwargs):
"""
Monitora eventos de mudanças em recursos Database
"""
db = event['object']
action = event['type']
name = db['metadata']['name']
namespace = db['metadata']['namespace']
print(f"Evento detectado: {action} na database {namespace}/{name}")
@kopf.on.create('data.example.com', 'v1', 'databases')
def on_database_create(spec, name, namespace, **kwargs):
"""
Executado quando uma nova Database é criada
"""
engine = spec.get('engine')
version = spec.get('version')
storage = spec.get('storage')
print(f"Criando database {name} com engine {engine} v{version}, storage {storage}")
# Aqui você colocaria a lógica real de criação:
# - Fazer chamadas a APIs externas
# - Provisionar recursos no cloud provider
# - Configurar credenciais
# - etc
return f"Database {name} criada com sucesso"
@kopf.on.update('data.example.com', 'v1', 'databases')
def on_database_update(spec, name, namespace, patch, **kwargs):
"""
Executado quando uma Database existente é atualizada
"""
engine = spec.get('engine')
storage = spec.get('storage')
print(f"Atualizando database {name}: engine={engine}, storage={storage}")
# Aplicar mudanças
with kubernetes.client.ApiClient() as api_client:
api_instance = kubernetes.client.CustomObjectsApi(api_client)
# Atualizar o status da database
body = {
'status': {
'phase': 'Running',
'message': f'Database atualizada em {datetime.now().isoformat()}',
'lastUpdateTime': datetime.now().isoformat()
}
}
try:
api_instance.patch_namespaced_custom_object(
group='data.example.com',
version='v1',
namespace=namespace,
plural='databases',
name=name,
body=body
)
print(f"Status de {name} atualizado para Running")
except Exception as e:
print(f"Erro ao atualizar status: {e}")
patch['status'] = {
'phase': 'Failed',
'message': str(e)
}
@kopf.on.delete('data.example.com', 'v1', 'databases')
def on_database_delete(name, namespace, **kwargs):
"""
Executado quando uma Database é deletada
"""
print(f"Deletando database {name} em namespace {namespace}")
# Aqui você faria limpeza:
# - Remover recursos do cloud provider
# - Fazer backup
# - Liberar credenciais
# - etc
return f"Database {name} deletada com sucesso"
@kopf.index('data.example.com', 'v1', 'databases', key=lambda db, **_: db['spec'].get('engine'))
def databases_by_engine(index, **kwargs):
"""
Índice para facilitar busca de databases por engine
"""
pass
Para executar este controlador localmente durante desenvolvimento:
kopf run -A database_controller.py
Para executar em produção dentro do cluster, crie um Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: database-controller
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: database-controller
template:
metadata:
labels:
app: database-controller
spec:
serviceAccountName: database-controller
containers:
- name: controller
image: my-registry/database-controller:1.0
imagePullPolicy: Always
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: database-controller
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: database-controller
rules:
- apiGroups: ["data.example.com"]
resources: ["databases"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: database-controller
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: database-controller
subjects:
- kind: ServiceAccount
name: database-controller
namespace: default
Padrões Avançados e Considerações de Produção
Versionamento de CRDs
À medida que sua CRD evolui, você precisa gerenciar versões para manter compatibilidade com clientes antigos. Kubernetes suporta múltiplas versões simultaneamente. Você pode marcar uma como served: true (aceita requisições) e outra como storage: true (usada internamente). Use conversion estratégies para transformar dados entre versões:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: databases.data.example.com
spec:
group: data.example.com
names:
kind: Database
plural: databases
scope: Namespaced
conversion:
strategy: Webhook
webhook:
clientConfig:
service:
name: crd-conversion-webhook
namespace: default
path: "/convert"
caBundle: base64-encoded-ca-cert
conversionReviewVersions: ["v1"]
versions:
- name: v1
served: true
storage: true
schema: {}
- name: v2
served: true
storage: false
schema: {}
Subcollections e Status
As CRDs suportam uma seção status separada, que é o padrão Kubernetes para representar o estado observado. Isso é fundamental para operadores, pois a spec representa o estado desejado e status o que realmente está acontecendo:
apiVersion: data.example.com/v1
kind: Database
metadata:
name: production-db
spec:
engine: postgresql
version: 14.2.5
storage: 100Gi
status:
phase: Running
message: "Database rodando normalmente"
lastUpdateTime: "2024-01-15T10:30:00Z"
readyReplicas: 3
observedGeneration: 2
RBAC para CRDs
Sempre defina permissões apropriadas. Usuários diferentes devem ter acesso diferente a diferentes tipos de recurso:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: database-user
namespace: default
rules:
- apiGroups: ["data.example.com"]
resources: ["databases"]
verbs: ["get", "list"]
- apiGroups: ["data.example.com"]
resources: ["databases/status"]
verbs: ["get"]
- apiGroups: ["data.example.com"]
resources: ["databases"]
resourceNames: ["production-db"]
verbs: ["get", "patch"]
Conclusão
Neste artigo, você aprendeu que Custom Resource Definitions são o mecanismo fundamental para estender o Kubernetes, permitindo criar abstrações de domínio específicas que se integram perfeitamente com a plataforma. Elas não são apenas sintaxe — elas abrem a porta para automação sofisticada através de operadores, que é como sistemas complexos são gerenciados em produção.
Em segundo lugar, compreendemos que um CRD sem um controlador é apenas armazenamento — o controlador é o que traz a inteligência. A reconciliação contínua entre estado desejado (spec) e estado observado (status) é o padrão que permite que sistemas auto-curem e se auto-escalem. Isso exige disciplina na separação de responsabilidades e na idempotência das operações.
Por fim, produção exige atenção a detalhes: validação de schema rigorosa, versionamento cuidadoso, RBAC apropriado e monitoramento do seu controlador. Comece simples, teste bem, e evolua incrementalmente.