Kubernetes Operators: estendendo a API do K8s
1. Introdução aos Kubernetes Operators
Kubernetes Operators representam uma evolução significativa na forma como gerenciamos aplicações complexas no ecossistema Kubernetes. Eles surgiram da necessidade de superar as limitações dos controllers nativos, que embora eficientes para cargas de trabalho padrão, não conseguem lidar com a complexidade operacional de aplicações stateful como bancos de dados, sistemas de mensageria e ferramentas de monitoramento.
O conceito central por trás dos Operators é a "aplicação como código" — transformar o conhecimento operacional humano em software automatizado. Isso permite que tarefas repetitivas como instalação, upgrade, backup e recuperação sejam executadas de forma consistente e confiável.
Na perspectiva DevOps, os Operators reduzem drasticamente o toil — trabalho manual, repetitivo e automatizável. Em vez de um engenheiro executar procedimentos complexos de 15 passos para atualizar um cluster etcd, um Operator pode fazer isso com uma simples alteração no manifesto YAML.
2. Fundamentos: Controllers vs Operators
Controllers nativos como Deployment, StatefulSet e DaemonSet gerenciam o ciclo de vida básico de pods e serviços. Eles garantem que o estado atual corresponda ao estado desejado, mas não entendem o comportamento específico da aplicação.
Operators estendem esse conceito através de Custom Resource Definitions (CRDs). Enquanto um Deployment sabe apenas criar e destruir pods, um Operator para PostgreSQL sabe como:
- Inicializar um novo banco com configurações otimizadas
- Realizar backups incrementais
- Executar upgrades com zero downtime
- Recuperar de falhas de nó automaticamente
O padrão Operator implementa um loop de reconciliação contínuo:
func (r *MyOperator) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// 1. Observa o estado atual
currentState := r.observeCurrentState(req)
// 2. Compara com o estado desejado
desiredState := r.getDesiredState(req)
// 3. Calcula as ações necessárias
actions := r.calculateActions(currentState, desiredState)
// 4. Aplica as alterações
for _, action := range actions {
err := r.executeAction(ctx, action)
if err != nil {
return ctrl.Result{RequeueAfter: time.Second}, err
}
}
// 5. Retorna para continuar monitorando
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}
3. Custom Resource Definitions (CRDs)
CRDs são a espinha dorsal dos Operators. Eles permitem definir novos tipos de recursos no Kubernetes, com schemas validados usando OpenAPI v3.
Exemplo de um CRD básico para gerenciar bancos de dados:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: databases.mycompany.io
spec:
group: mycompany.io
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
engine:
type: string
enum: [postgres, mysql]
version:
type: string
storage:
type: string
pattern: '^[0-9]+Gi$'
replicas:
type: integer
minimum: 1
maximum: 5
required: [engine, version]
scope: Namespaced
names:
plural: databases
singular: database
kind: Database
shortNames:
- db
Para gerenciar versões e migrações, podemos implementar conversion webhooks:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: databases.mycompany.io
spec:
conversion:
strategy: Webhook
webhook:
conversionReviewVersions: ["v1", "v2"]
clientConfig:
service:
namespace: mycompany-system
name: mycompany-webhook-service
path: /convert
4. Construindo um Operator na prática
Frameworks como Operator SDK e Kubebuilder simplificam drasticamente a criação de Operators. Vamos criar um Operator simples para gerenciar um banco de dados usando Go.
Inicializando o projeto:
# Instalar o Operator SDK
curl -LO https://github.com/operator-framework/operator-sdk/releases/download/v1.32.0/operator-sdk_linux_amd64
chmod +x operator-sdk_linux_amd64
sudo mv operator-sdk_linux_amd64 /usr/local/bin/operator-sdk
# Criar novo projeto
operator-sdk init --domain=mycompany.io --repo=github.com/mycompany/database-operator
# Criar API
operator-sdk create api --group=database --version=v1 --kind=Database --resource=true --controller=true
Implementando a lógica de reconciliação:
func (r *DatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
// Buscar a instância do Database
database := &mycompanyv1.Database{}
if err := r.Get(ctx, req.NamespacedName, database); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// Verificar se o StatefulSet existe
sts := &appsv1.StatefulSet{}
err := r.Get(ctx, types.NamespacedName{
Name: database.Name,
Namespace: database.Namespace,
}, sts)
if err != nil && apierrors.IsNotFound(err) {
// Criar StatefulSet para o banco
sts = r.buildStatefulSet(database)
if err := r.Create(ctx, sts); err != nil {
return ctrl.Result{}, err
}
log.Info("StatefulSet criado", "name", sts.Name)
}
// Atualizar status do Database
database.Status.Ready = sts.Status.ReadyReplicas == *sts.Spec.Replicas
if err := r.Status().Update(ctx, database); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}
5. Ciclo de vida e reconciliação
O loop de reconciliação segue o padrão watch, diff, apply. O Operator monitora mudanças no CRD, calcula diferenças entre estado atual e desejado, e aplica as correções necessárias.
Para lidar com estados complexos como backup e rollback:
func (r *DatabaseReconciler) handleBackup(ctx context.Context, db *mycompanyv1.Database) error {
if !db.Spec.Backup.Enabled {
return nil
}
// Verificar último backup
lastBackup := &batchv1.Job{}
err := r.Get(ctx, types.NamespacedName{
Name: fmt.Sprintf("%s-backup-%s", db.Name, time.Now().Format("20060102")),
Namespace: db.Namespace,
}, lastBackup)
if err != nil && apierrors.IsNotFound(err) {
// Criar job de backup
backupJob := r.buildBackupJob(db)
return r.Create(ctx, backupJob)
}
return nil
}
Para tratamento de falhas com backoff:
func (r *DatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// Lógica de reconciliação...
if err != nil {
// Requeue com backoff exponencial
return ctrl.Result{
RequeueAfter: time.Duration(math.Pow(2, float64(retryCount))) * time.Second,
}, err
}
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}
6. Operações avançadas com Operators
Operators para sistemas complexos como etcd ou Prometheus implementam lógicas sofisticadas:
# Exemplo de recurso etcd gerenciado por Operator
apiVersion: etcd.database.coreos.com/v1beta2
kind: EtcdCluster
metadata:
name: my-etcd-cluster
spec:
size: 3
version: "3.5.0"
pod:
resources:
requests:
cpu: 200m
memory: 512Mi
backup:
backupIntervalInSecond: 3600
maxBackups: 5
TLS:
static:
member:
peerSecret: etcd-peer-tls
serverSecret: etcd-server-tls
Integração com Service Mesh para comunicação segura entre serviços gerenciados pelo Operator:
apiVersion: v1
kind: Service
metadata:
annotations:
sidecar.istio.io/inject: "true"
labels:
app: postgres
name: postgres
spec:
ports:
- port: 5432
name: postgres
selector:
app: postgres
7. Operators e GitOps
A integração com GitOps através de ArgoCD ou Flux permite gerenciar Operators de forma declarativa:
# Aplicação ArgoCD para o Operator
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: database-operator
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/mycompany/database-operator
targetRevision: HEAD
path: config/default
destination:
server: https://kubernetes.default.svc
namespace: database-operator-system
syncPolicy:
automated:
prune: true
selfHeal: true
Para gerenciar instâncias do banco de dados via GitOps:
# Instância do banco no repositório Git
apiVersion: mycompany.io/v1
kind: Database
metadata:
name: production-db
namespace: production
spec:
engine: postgres
version: "14"
storage: "100Gi"
replicas: 3
backup:
enabled: true
schedule: "0 2 * * *"
8. Boas práticas e considerações finais
Testes são cruciais para Operators. Utilize testes unitários para lógica de reconciliação:
func TestDatabaseReconciler(t *testing.T) {
// Configurar ambiente de teste
scheme := runtime.NewScheme()
_ = mycompanyv1.AddToScheme(scheme)
_ = appsv1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
// Criar reconciler mock
reconciler := &DatabaseReconciler{
Client: fake.NewClientBuilder().WithScheme(scheme).Build(),
Scheme: scheme,
}
// Testar criação de banco
db := &mycompanyv1.Database{
ObjectMeta: metav1.ObjectMeta{
Name: "test-db",
Namespace: "default",
},
Spec: mycompanyv1.DatabaseSpec{
Engine: "postgres",
Version: "14",
Storage: "10Gi",
Replicas: 3,
},
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Name: "test-db",
Namespace: "default",
},
}
result, err := reconciler.Reconcile(context.Background(), req)
assert.NoError(t, err)
assert.NotNil(t, result)
}
Monitoramento do próprio Operator é essencial:
# Métricas Prometheus expostas pelo Operator
# HELP database_operator_reconcile_total Total de reconciliações
# TYPE database_operator_reconcile_total counter
database_operator_reconcile_total{status="success"} 1245
database_operator_reconcile_total{status="error"} 23
# HELP database_operator_reconcile_duration_seconds Duração da reconciliação
# TYPE database_operator_reconcile_duration_seconds histogram
database_operator_reconcile_duration_seconds_bucket{le="0.1"} 890
database_operator_reconcile_duration_seconds_bucket{le="0.5"} 1150
database_operator_reconcile_duration_seconds_bucket{le="1"} 1230
Quando usar Operators vs soluções prontas:
- Use Operators quando precisar de automação complexa de ciclo de vida
- Prefira Helm charts para deploys simples e parametrizáveis
- Operators são ideais para aplicações stateful que exigem conhecimento operacional profundo
Os Kubernetes Operators transformam operações manuais em software confiável e escalável, permitindo que equipes DevOps automatizem tarefas complexas e reduzam significativamente o toil operacional.
Referências
- Documentação oficial do Operator SDK — Guia completo para criar, testar e publicar Operators usando o framework oficial.
- Kubebuilder: Build Kubernetes APIs using CRDs — Tutorial interativo para construir CRDs e controllers com Go.
- Kubernetes Operators: Automating the Container Orchestration Platform — Livro abrangente sobre conceitos e implementação de Operators.
- OperatorHub.io - Catálogo de Operators — Repositório oficial com Operators prontos para uso em produção.
- CoreOS etcd Operator — Exemplo prático de Operator para gerenciamento de clusters etcd.
- Red Hat: Building Operators with the Operator Framework — Artigo técnico detalhado sobre construção de Operators com o framework Red Hat.
- GitOps com ArgoCD e Operators — Documentação oficial sobre integração de Operators com ArgoCD.