Em 2016, Brandon Philips (CTO da CoreOS) publicou um post que introduziu o padrão Operator: a ideia de que o conhecimento operacional de sistemas complexos — como operar um cluster etcd, fazer upgrades sem downtime, recuperar de falhas — pode ser codificado em um controlador Kubernetes em vez de em runbooks que dependem de intervenção humana. O princípio é simples: se um engenheiro sênior sabe o que fazer quando o etcd primário cai, esse conhecimento pode ser codificado em um programa que reage automaticamente ao mesmo evento.
Operators elevam o Kubernetes de um orquestrador de containers para uma plataforma de automação de operações. A pergunta muda de "como faço deploy desse banco de dados?" para "como codifico o conhecimento de operar esse banco de dados de forma que a plataforma o faça automaticamente?".
Custom Resource Definitions (CRDs)
Antes de falar em Operators, é necessário entender CRDs. O Kubernetes é extensível: você pode adicionar novos tipos de recursos além dos built-in (Deployment, Service, ConfigMap). Um CRD define um novo tipo de recurso com seu próprio schema:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: bancosdedados.minha.org
spec:
group: minha.org
names:
kind: BancoDeDados
listKind: BancoDeDadosList
plural: bancosdedados
singular: bancodedados
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required: [engine, version, storage]
properties:
engine:
type: string
enum: [postgres, mysql]
version:
type: string
pattern: '^[0-9]+\.[0-9]+$'
storage:
type: string # "10Gi", "100Gi"
pattern: '^[0-9]+(Mi|Gi)$'
highAvailability:
type: boolean
default: false
status:
type: object
properties:
phase:
type: string
enum: [Pending, Provisioning, Ready, Failed]
endpoint:
type: string
lastError:
type: string
Depois de aplicar esse CRD, você pode criar recursos do tipo BancoDeDados:
apiVersion: minha.org/v1
kind: BancoDeDados
metadata:
name: banco-producao
namespace: meu-servico
spec:
engine: postgres
version: "16.2"
storage: "100Gi"
highAvailability: true
O recurso é armazenado no etcd, mas sem um controller, nada acontece. É o Operator que age sobre o recurso.
O padrão Operator — reconciliation loop
Um Operator é um controller que observa recursos (CRDs ou recursos built-in) e reconcilia o estado real com o estado desejado. O loop de reconciliação é o coração do padrão:
// Go — controller com kubebuilder (simplificado)
func (r *BancoDeDadosReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// 1. Buscar o recurso atual
banco := &minhav1.BancoDeDados{}
if err := r.Get(ctx, req.NamespacedName, banco); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 2. Determinar o estado atual vs desejado
switch banco.Status.Phase {
case "":
// Primeira vez — iniciar provisionamento
banco.Status.Phase = "Pending"
r.Status().Update(ctx, banco)
return ctrl.Result{Requeue: true}, nil
case "Pending":
// Criar os recursos Kubernetes necessários
if err := r.createStatefulSet(ctx, banco); err != nil {
banco.Status.LastError = err.Error()
banco.Status.Phase = "Failed"
r.Status().Update(ctx, banco)
return ctrl.Result{}, err
}
banco.Status.Phase = "Provisioning"
r.Status().Update(ctx, banco)
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
case "Provisioning":
// Verificar se o StatefulSet está pronto
if r.isReady(ctx, banco) {
banco.Status.Phase = "Ready"
banco.Status.Endpoint = r.getEndpoint(banco)
r.Status().Update(ctx, banco)
}
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
case "Ready":
// Reconciliar continuamente — detectar drift
if r.specChanged(banco) {
return r.handleSpecChange(ctx, banco)
}
return ctrl.Result{RequeueAfter: 60 * time.Second}, nil
}
return ctrl.Result{}, nil
}
O controller é notificado pelo API server quando qualquer objeto BancoDeDados é criado, modificado, ou deletado — e também periodicamente (via RequeueAfter). A reconciliação é idempotente: se chamada múltiplas vezes com o mesmo estado, o resultado é o mesmo. Isso é o que torna o loop de reconciliação robusto a falhas parciais.
Kubebuilder e Operator SDK
Escrever um Operator do zero envolve muita boilerplate de interação com o API server. Kubebuilder e Operator SDK são frameworks que geram o scaffolding:
# Criar um novo projeto Operator com kubebuilder
kubebuilder init --domain minha.org --repo github.com/minha-org/meu-operator
# Criar CRD e controller
kubebuilder create api --group banco --version v1 --kind BancoDeDados
# Estrutura gerada
meu-operator/
├── api/v1/
│ ├── bancodedados_types.go # estrutura do CRD (você preenche)
│ └── zz_generated.deepcopy.go
├── controllers/
│ └── bancodedados_controller.go # lógica de reconciliação (você implementa)
├── config/
│ ├── crd/ # CRD gerado automaticamente
│ └── rbac/ # RBAC para o controller
└── main.go
O Operator SDK (Red Hat) é similar ao kubebuilder mas também suporta Operators em Ansible e Helm — útil para encapsular charts Helm existentes em um Operator sem escrever Go.
Operators canônicos — exemplos de produção
cert-manager: gerencia certificados TLS dentro do cluster. Você cria um recurso Certificate ou anota um Ingress, e o cert-manager emite automaticamente o certificado via Let's Encrypt (ACME), Vault, ou CA interno — e o renova antes da expiração. Elimina o toil de gestão manual de certificados.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: meu-servico-tls
spec:
secretName: meu-servico-tls-secret
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- api.minha-empresa.com
External Secrets Operator (ESO): sincroniza secrets de sistemas externos (Vault, AWS Secrets Manager, GCP Secret Manager) para Kubernetes Secrets. O secret não fica armazenado no git — apenas a referência a onde ele está:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: db-credentials # Kubernetes Secret criado
data:
- secretKey: password
remoteRef:
key: producao/banco-de-dados
property: password
Crossplane: provision de infraestrutura cloud via recursos Kubernetes. Em vez de usar Terraform ou Pulumi, você cria um recurso RDSInstance no Kubernetes e o Crossplane cria o banco de dados na AWS. O cluster Kubernetes se torna o plano de controle de toda a infraestrutura:
apiVersion: rds.aws.crossplane.io/v1alpha1
kind: DBInstance
metadata:
name: producao-postgres
spec:
forProvider:
region: us-east-1
dbInstanceClass: db.t3.medium
engine: postgres
engineVersion: "16.2"
multiAZ: true
providerConfigRef:
name: aws-provider
Prometheus Operator: gerencia configuração do Prometheus via CRDs — ServiceMonitor, PodMonitor, PrometheusRule. Times de produto adicionam um ServiceMonitor ao seu serviço para expor métricas sem editar o arquivo de configuração central do Prometheus.
Admission Webhooks — validação e mutação
Admission webhooks são plugins que interceptam requisições ao API server antes que recursos sejam persistidos no etcd. Existem dois tipos:
Validating Admission Webhook: valida um recurso e pode rejeitar a criação/modificação. Exemplos de uso: impedir Deployments sem resource limits, bloquear imagens de registries não aprovados, validar que todos os recursos têm as tags obrigatórias.
Mutating Admission Webhook: modifica um recurso antes de persistir. Exemplos: injetar sidecar de logging automaticamente, adicionar labels padrão, ou setar valores padrão que o usuário não especificou.
// Go — Validating webhook que rejeita Pods sem resource limits
func (v *PodValidator) Handle(ctx context.Context, req admission.Request) admission.Response {
pod := &corev1.Pod{}
if err := v.decoder.Decode(req, pod); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
for _, container := range pod.Spec.Containers {
if container.Resources.Limits == nil {
return admission.Denied(fmt.Sprintf(
"container %q não tem resource limits definidos — obrigatório em produção",
container.Name,
))
}
if container.Resources.Limits.Memory().IsZero() {
return admission.Denied(fmt.Sprintf(
"container %q não tem memory limit — pode causar OOM no nó",
container.Name,
))
}
}
return admission.Allowed("resource limits OK")
}
Webhooks são registrados como ValidatingWebhookConfiguration e MutatingWebhookConfiguration. O API server envia a requisição ao webhook via HTTPS antes de persistir — o webhook precisa ter alta disponibilidade, pois sua indisponibilidade bloqueia operações no cluster.
Um webhook com failurePolicy: Fail (o default) que fica indisponível bloqueia toda criação de Pod no cluster — incluindo novos Pods do próprio webhook. Configure failurePolicy: Ignore para webhooks não-críticos, garanta no mínimo 2 réplicas com PodAntiAffinity por zona, e use PodDisruptionBudget. Webhooks de segurança críticos (como Open Policy Agent) com failurePolicy: Fail devem ser os workloads mais resilientes do cluster.
Policy enforcement com OPA/Gatekeeper
Open Policy Agent (OPA) com Gatekeeper é o padrão para policy enforcement em Kubernetes. Gatekeeper é implementado como admission webhook, e as políticas são escritas em Rego:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: require-team-label
spec:
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment"]
parameters:
labels: ["team", "service", "environment"]
---
# ConstraintTemplate que define a lógica de validação
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
validation:
openAPIV3Schema:
properties:
labels:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
violation[{"msg": msg}] {
provided := {label | input.review.object.metadata.labels[label]}
required := {label | label := input.parameters.labels[_]}
missing := required - provided
count(missing) > 0
msg := sprintf("labels obrigatórias ausentes: %v", [missing])
}
Operators fazem sentido quando você tem operações manuais repetitivas que seguem um padrão previsível: "quando X acontece, faça Y e Z". Se o seu time tem um runbook de 5 etapas para um evento específico — e esse evento acontece com frequência — é candidato para um Operator. Não construa Operators para automatizar operações que acontecem uma vez por ano: o custo de manter o código supera o benefício. Os melhores Operators são aqueles que empacodam décadas de conhecimento operacional de um sistema complexo (banco de dados, Kafka, Elasticsearch) — e por isso os Operators de projetos com ecossistemas maduros (PostgreSQL com CloudNativePG, Kafka com Strimzi) geralmente são melhores que Operators internos escritos em um sprint.
Decisões de engenharia
Perguntas de entrevista
O que é o padrão Operator e por que idempotência do loop de reconciliação é fundamental?
O padrão Operator é a combinação de CRDs (que estendem a API do Kubernetes com tipos de recurso novos) com controllers (que observam esses recursos e reconciliam o estado real com o estado desejado). É a codificação do conhecimento operacional humano — runbooks, procedimentos de failover, rotinas de manutenção — em software que reage automaticamente a eventos.
Idempotência é fundamental porque o loop de reconciliação é invocado múltiplas vezes para o mesmo estado: quando o recurso é criado, quando é modificado, quando o controller reinicia, e periodicamente (via RequeueAfter). Uma função idempotente chamada N vezes com o mesmo input produz o mesmo resultado que chamada uma única vez. Se a reconciliação não for idempotente, múltiplas invocações podem criar duplicatas, fazer mudanças duplas, ou gerar estado inconsistente.
A forma prática de garantir idempotência é: (1) verificar se o recurso já existe antes de criar (use CreateOrUpdate); (2) usar server-side apply em vez de replace; (3) derivar o estado desejado apenas do spec do CRD, nunca de estado externo; (4) tratar cada invocação como se fosse a primeira — não assumir que invocações anteriores completaram com sucesso.
Qual a diferença entre Validating e Mutating Admission Webhook, e por que webhooks exigem alta disponibilidade?
Admission webhooks são plugins que o API server invoca antes de persistir um recurso no etcd. Mutating webhooks rodam primeiro: podem modificar o objeto (adicionar labels, injetar containers, setar defaults). Validating webhooks rodam depois: podem aprovar ou rejeitar o objeto mas não modificá-lo. A ordem importa: um Mutating webhook pode adicionar um campo que o Validating webhook depois valida.
A diferença prática: use Mutating para injeção automática de sidecars (Istio proxy, Vault agent), adição de labels padrão, e valores default. Use Validating para policy enforcement — rejeitar Pods sem resource limits, bloquear imagens de registries não aprovados, impedir Deployments com réplicas = 0 em namespace de produção. Mensagens de erro de Validating webhooks chegam diretamente ao usuário via kubectl apply.
Alta disponibilidade é obrigatória porque o API server invoca o webhook de forma síncrona na pipeline de admissão. Com failurePolicy: Fail (padrão para webhooks de segurança), se o webhook estiver indisponível, toda operação de criação/modificação de recursos cobertos pelo webhook falha — incluindo criação de novos Pods do próprio webhook, criando um deadlock. A configuração mínima é 2 réplicas com PodAntiAffinity entre zonas e PodDisruptionBudget.
Como o External Secrets Operator resolve o problema de secrets em GitOps e quais são suas alternativas?
O problema central de GitOps com secrets é que o princípio "tudo no git" conflita com "nunca commite credentials no git". Se o state desejado do cluster vive no git, e o estado inclui um Secret com uma senha de banco, você não pode armazenar o secret diretamente — ele ficaria exposto no histórico do repositório.
O ESO resolve isso com uma camada de indireção: você armazena no git apenas a referência a onde o secret está (ExternalSecret apontando para AWS Secrets Manager, Vault, ou GCP Secret Manager), e o ESO cria o Kubernetes Secret no cluster sincronizando do sistema externo. O secret nunca passa pelo git. O ESO também garante rotação: com refreshInterval: 1h, o Kubernetes Secret é atualizado toda hora com o valor mais recente do Secrets Manager.
Alternativas: (1) Sealed Secrets (Bitnami) — encripta o secret com a chave pública do cluster, o secret encriptado pode ser commitado no git com segurança; o controller decripta no cluster. Mais simples que ESO mas não integra com sistemas externos de secrets management. (2) Vault Agent Injector — injeta secrets do Vault diretamente em containers via sidecar; mais integrado com Vault mas mais complexo de operar. (3) Secrets Store CSI Driver — monta secrets de providers externos como volumes; mais flexível mas requer configuração por pod.
O que é OPA/Gatekeeper e como ele implementa policy-as-code para Kubernetes?
Open Policy Agent (OPA) é um engine de policy generalista que avalia rules escritas em Rego — uma linguagem declarativa para consultas de dados estruturados. Gatekeeper adapta o OPA para Kubernetes, implementado como Validating Admission Webhook que chama o OPA para cada recurso criado ou modificado.
O modelo do Gatekeeper tem duas camadas: ConstraintTemplate (define a lógica da regra em Rego) e Constraint (instância da regra com parâmetros específicos). Isso permite reutilização: você escreve um ConstraintTemplate para "recursos devem ter labels obrigatórias" e cria múltiplas Constraints com listas diferentes de labels. Times de plataforma controlam os ConstraintTemplates; times de produto configuram Constraints para seus namespaces.
A vantagem sobre webhooks Go customizados é que as regras em Rego são declarativas, testáveis unitariamente (opa test), e auditáveis — você pode ver exatamente por que um recurso foi rejeitado. O Gatekeeper também suporta modo de auditoria (scan de recursos já existentes que violam policies novas) sem bloquear deploys — útil para migrar policies gradualmente.
Qual é a estrutura de status de um CRD e por que separar spec e status é importante para o padrão Operator?
No design de CRDs, spec representa o estado desejado declarado pelo usuário (o que o usuário quer), e status representa o estado observado atual (o que o Operator relata sobre o que existe). Essa separação espelha o modelo de toda a API do Kubernetes: o usuário escreve spec.replicas: 3 (o que quer), e o controller atualiza status.readyReplicas: 2 (o que existe agora).
A separação é fundamental por duas razões: (1) Controle de acesso — usuários escrevem spec, controllers escrevem status. O RBAC pode permitir que um usuário atualize spec mas não status (via /status subresource), prevenindo que usuários manipulem o estado observado; (2) Idempotência e reconciliação — o Operator lê spec para saber o estado desejado e lê status para saber o estado atual, calculando o delta. Se spec e status fossem o mesmo campo, o Operator não saberia o que o usuário queria vs o que ele mesmo reportou.
Boas práticas de design de status: inclua um campo phase com enum de estados (Pending, Provisioning, Ready, Failed), um campo conditions com o formato padrão de Kubernetes Conditions (Type, Status, Reason, Message, LastTransitionTime), e campos de output relevantes (endpoint, version deployed, etc.). Conditions são preferíveis a phase para sistemas com múltiplos aspectos de saúde.
Exercícios práticos
Crie um Operator simples com kubebuilder para o CRD WebApp com spec contendo image (string), replicas (int), e port (int). O controller deve reconciliar criando/atualizando um Deployment e um Service baseados no spec. Implemente status com campos phase (Pending/Ready) e availableReplicas. Teste a reconciliação: crie um WebApp, verifique que o Deployment é criado; modifique replicas no WebApp, verifique que o Deployment é atualizado; delete o WebApp e verifique que os recursos filho são removidos (owner references).
WebApp resulta em um Deployment e Service funcionais; modificar o spec atualiza os recursos filho dentro de 30 segundos; deletar o WebApp cascata para os recursos filho via owner references; o status.phase reflete corretamente o estado do Deployment.
Instale o cert-manager via Helm em um cluster com Ingress controller (nginx-ingress ou traefik). Configure um ClusterIssuer para Let's Encrypt staging (use staging para testes — evita rate limits). Crie um Ingress com a anotação cert-manager.io/cluster-issuer e um campo tls apontando para um secretName. Verifique que o cert-manager emite o certificado automaticamente e popula o Secret. Observe o status do recurso Certificate e os eventos do CertificateRequest durante o processo de emissão.
tls.crt e tls.key; o recurso Certificate tem status Ready: True; sem intervenção manual em nenhuma etapa.
Instale o External Secrets Operator via Helm. Configure um SecretStore (ou ClusterSecretStore) apontando para AWS Secrets Manager usando IRSA (IAM Roles for Service Accounts) — sem AWS credentials no cluster. Crie um secret no AWS Secrets Manager com um JSON de credenciais de banco. Crie um ExternalSecret no cluster que sincroniza o secret do AWS para um Kubernetes Secret. Verifique que o Secret é criado com os campos corretos. Atualize o valor no AWS Secrets Manager e aguarde o refreshInterval para verificar que o Kubernetes Secret é atualizado automaticamente.
Critério: Nenhuma AWS credential é armazenada como Kubernetes Secret — a autenticação usa IRSA; o ExternalSecret cria o Kubernetes Secret com os campos corretos; após atualizar o valor no AWS Secrets Manager, o Kubernetes Secret é atualizado dentro do refreshInterval configurado.Implemente um Validating Admission Webhook em Go usando o kubebuilder webhook scaffolding. O webhook deve rejeitar qualquer Pod onde algum container não tenha resources.limits.cpu e resources.limits.memory definidos. Configure o ValidatingWebhookConfiguration para interceptar apenas Pods em namespaces com a label enforce-limits: "true". Implemente testes unitários para o handler usando admission.Request simulado. Deploy o webhook com 2 réplicas e PodAntiAffinity por zona, com failurePolicy: Fail.
Instale o Gatekeeper via Helm. Crie dois ConstraintTemplates com Rego: (1) K8sRequiredLabels que exige labels team e app em todos os Deployments de um namespace; (2) K8sAllowedRegistries que rejeita Pods com imagens de registries não aprovados (lista de prefixos aprovados como parâmetro). Configure as Constraints correspondentes. Teste: tente criar um Deployment sem as labels obrigatórias (deve falhar com mensagem clara); tente criar um Pod com imagem do Docker Hub quando apenas ghcr.io é aprovado (deve falhar). Use o modo de auditoria para verificar recursos já existentes que violam as policies.
kubectl describe constraint na seção violations; policies são testáveis com opa test sem um cluster real.
Referências
- article Brandon Philips — Introducing Operators: Putting Operational Knowledge into Software
- docs Kubebuilder Book
- docs Open Policy Agent — Gatekeeper
- book Jason Dobies & Joshua Wood — Kubernetes Operators
- docs OperatorHub.io
- docs cert-manager — Documentation
- docs External Secrets Operator — Documentation
- docs Crossplane — Documentation
- docs Operator SDK — Documentation
- article CNCF — Operator White Paper
- article CloudNativePG — PostgreSQL Operator for Kubernetes
- article Kubernetes — Admission Controllers Reference