<h2>Introdução: O que são Operators em Kubernetes</h2>
<p>Um Kubernetes Operator é um padrão de design que encapsula conhecimento operacional complexo em código executável. Ele estende as capacidades nativas do Kubernetes através de Custom Resources (CRs) e controladores customizados, permitindo automatizar tarefas repetitivas e complexas de gerenciamento de aplicações stateful.</p>
<p>Enquanto recursos nativos como Deployments e StatefulSets lidam bem com aplicações stateless ou com estado simples, aplicações complexas como bancos de dados, sistemas de mensageria ou plataformas de análise exigem lógica sofisticada: provisioning, backup, failover, scaling inteligente e updates coordenados. Um Operator codifica essa expertise, tornando-a reutilizável e escalável.</p>
<h2>Fundamentos: Custom Resources e CRDs</h2>
<h3>O que é uma Custom Resource Definition</h3>
<p>Uma Custom Resource Definition (CRD) é um mecanismo que permite estender a API do Kubernetes com novos tipos de recursos. Sem uma CRD, você só pode trabalhar com resources nativos (Pods, Services, etc.). Com uma CRD, você define a estrutura e validação de seus próprios recursos, criando uma abstraçãohigher-level para suas aplicações.</p>
<p>Vamos criar uma CRD simples para gerenciar uma aplicação de banco de dados PostgreSQL customizado:</p>
<pre><code class="language-yaml">apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: postgresinstances.database.example.com
spec:
group: database.example.com
names:
kind: PostgresInstance
plural: postgresinstances
shortNames:
- pg
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
version:
type: string
description: "Versão do PostgreSQL"
example: "14.5"
replicas:
type: integer
minimum: 1
maximum: 10
description: "Número de replicas"
storage:
type: string
description: "Tamanho do volume de armazenamento"
example: "10Gi"
required:
- version
- replicas
- storage
status:
type: object
properties:
ready:
type: boolean
primaryPod:
type: string
replicaPods:
type: array
items:
type: string</code></pre>
<p>Essa CRD define um novo tipo de recurso chamado <code>PostgresInstance</code>. Agora você pode criar instâncias desse tipo:</p>
<pre><code class="language-yaml">apiVersion: database.example.com/v1
kind: PostgresInstance
metadata:
name: production-db
namespace: default
spec:
version: "14.5"
replicas: 3
storage: "100Gi"</code></pre>
<h3>A estrutura Spec e Status</h3>
<p>A separação entre <code>spec</code> (desejado) e <code>status</code> (observado) é fundamental em Kubernetes. O <code>spec</code> descreve o estado que você quer, enquanto o <code>status</code> reflete o estado atual do recurso. Seu Operator constantemente observa a diferença e age para reconciliar.</p>
<h2>Implementação: Construindo um Controlador com Go</h2>
<h3>Configuração do Projeto</h3>
<p>Para construir Operators em produção, usaremos o Operator SDK, que fornece scaffolding automático e bibliotecas essenciais. Aqui assumo que você já tem Go 1.20+ e kubebuilder/operator-sdk instalados.</p>
<pre><code class="language-bash">operator-sdk init --domain example.com --repo github.com/example/postgres-operator
operator-sdk create api --group database --version v1 --kind PostgresInstance --resource --controller</code></pre>
<p>Esses comandos criam a estrutura básica do projeto. Você encontrará:</p>
<ul>
<li><code>api/v1/postgresinstance_types.go</code> — definição da CRD em Go</li>
<li><code>controllers/postgresinstance_controller.go</code> — lógica do controlador</li>
<li><code>config/crd/</code> — manifestos YAML das CRDs</li>
</ul>
<h3>Definindo a CRD em Go</h3>
<p>O arquivo <code>api/v1/postgresinstance_types.go</code> define a estrutura Go que representa seu recurso:</p>
<pre><code class="language-go">package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// PostgresInstanceSpec define as propriedades desejadas
type PostgresInstanceSpec struct {
Version string json:"version,omitempty"
Replicas int32 json:"replicas,omitempty"
Storage string json:"storage,omitempty"
}
// PostgresInstanceStatus define o estado observado
type PostgresInstanceStatus struct {
Ready bool json:"ready,omitempty"
PrimaryPod string json:"primaryPod,omitempty"
ReplicaPods []string json:"replicaPods,omitempty"
ObservedGeneration int64 json:"observedGeneration,omitempty"
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Ready",type=boolean,JSONPath=.status.ready
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=.metadata.creationTimestamp
type PostgresInstance struct {
metav1.TypeMeta json:",inline"
metav1.ObjectMeta json:"metadata,omitempty"
Spec PostgresInstanceSpec json:"spec,omitempty"
Status PostgresInstanceStatus json:"status,omitempty"
}
// +kubebuilder:object:root=true
type PostgresInstanceList struct {
metav1.TypeMeta json:",inline"
metav1.ListMeta json:"metadata,omitempty"
Items []PostgresInstance json:"items"
}
func init() {
SchemeBuilder.Register(&PostgresInstance{}, &PostgresInstanceList{})
}</code></pre>
<p>Os comentários com <code>+kubebuilder</code> são marcadores que geram código e manifests automaticamente durante a build.</p>
<h3>Implementando a Lógica do Controlador</h3>
<p>O controlador é o coração do Operator. Ele observa recursos <code>PostgresInstance</code> e realiza ações (criar Pods, StatefulSets, Services, etc.). Aqui está uma implementação funcional simplificada:</p>
<pre><code class="language-go">package controllers
import (
"context"
"fmt"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/predicate"
databasev1 "github.com/example/postgres-operator/api/v1"
)
const finalizerName = "database.example.com/postgres-finalizer"
// PostgresInstanceReconciler reconcilia objetos PostgresInstance
type PostgresInstanceReconciler struct {
client.Client
Scheme *runtime.Scheme
}
//+kubebuilder:rbac:groups=database.example.com,resources=postgresinstances,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=database.example.com,resources=postgresinstances/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete
// Reconcile é chamado sempre que há mudanças no recurso observado
func (r *PostgresInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := ctrl.LoggerFrom(ctx)
// Busca a instância PostgreSQL
pg := &databasev1.PostgresInstance{}
if err := r.Get(ctx, req.NamespacedName, pg); err != nil {
if apierrors.IsNotFound(err) {
log.Info("PostgresInstance não encontrada, ignorando")
return ctrl.Result{}, nil
}
log.Error(err, "Erro ao buscar PostgresInstance")
return ctrl.Result{}, err
}
// Handle deletion with finalizer
if pg.ObjectMeta.DeletionTimestamp != nil {
if controllerutil.ContainsFinalizer(pg, finalizerName) {
// Aqui você faria lógica de limpeza (backup, etc)
log.Info("Limpando recursos do PostgresInstance", "name", pg.Name)
controllerutil.RemoveFinalizer(pg, finalizerName)
if err := r.Update(ctx, pg); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
// Add finalizer se não existe
if !controllerutil.ContainsFinalizer(pg, finalizerName) {
controllerutil.AddFinalizer(pg, finalizerName)
if err := r.Update(ctx, pg); err != nil {
return ctrl.Result{}, err
}
}
// Cria ou atualiza o StatefulSet para o PostgreSQL
sts := &appsv1.StatefulSet{}
stsName := types.NamespacedName{Name: pg.Name, Namespace: pg.Namespace}
if err := r.Get(ctx, stsName, sts); err != nil {
if apierrors.IsNotFound(err) {
log.Info("Criando novo StatefulSet")
sts = r.constructStatefulSet(pg)
if err := controllerutil.SetControllerReference(pg, sts, r.Scheme); err != nil {
return ctrl.Result{}, err
}
if err := r.Create(ctx, sts); err != nil {
log.Error(err, "Erro ao criar StatefulSet")
return ctrl.Result{}, err
}
} else {
log.Error(err, "Erro ao buscar StatefulSet")
return ctrl.Result{}, err
}
} else {
// StatefulSet existe, atualiza se necessário
sts.Spec.Replicas = &pg.Spec.Replicas
if err := r.Update(ctx, sts); err != nil {
log.Error(err, "Erro ao atualizar StatefulSet")
return ctrl.Result{}, err
}
}
// Cria ou atualiza o Service
svc := &corev1.Service{}
svcName := types.NamespacedName{Name: pg.Name, Namespace: pg.Namespace}
if err := r.Get(ctx, svcName, svc); err != nil {
if apierrors.IsNotFound(err) {
log.Info("Criando novo Service")
svc = r.constructService(pg)
if err := controllerutil.SetControllerReference(pg, svc, r.Scheme); err != nil {
return ctrl.Result{}, err
}
if err := r.Create(ctx, svc); err != nil {
log.Error(err, "Erro ao criar Service")
return ctrl.Result{}, err
}
} else {
log.Error(err, "Erro ao buscar Service")
return ctrl.Result{}, err
}
}
// Atualiza o status
pg.Status.Ready = true
pg.Status.PrimaryPod = fmt.Sprintf("%s-0", pg.Name)
pg.Status.ReplicaPods = make([]string, 0)
for i := int32(1); i < pg.Spec.Replicas; i++ {
pg.Status.ReplicaPods = append(pg.Status.ReplicaPods, fmt.Sprintf("%s-%d", pg.Name, i))
}
pg.Status.ObservedGeneration = pg.Generation
if err := r.Status().Update(ctx, pg); err != nil {
log.Error(err, "Erro ao atualizar status")
return ctrl.Result{}, err
}
log.Info("PostgresInstance reconciliado com sucesso", "name", pg.Name)
return ctrl.Result{}, nil
}
// constructStatefulSet cria o StatefulSet para o PostgreSQL
func (r PostgresInstanceReconciler) constructStatefulSet(pg databasev1.PostgresInstance) *appsv1.StatefulSet {
labels := map[string]string{
"app": "postgres",
"instance": pg.Name,
}
sts := &appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Name: pg.Name,
Namespace: pg.Namespace,
Labels: labels,
},
Spec: appsv1.StatefulSetSpec{
ServiceName: pg.Name,
Replicas: &pg.Spec.Replicas,
Selector: &metav1.LabelSelector{
MatchLabels: labels,
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: labels,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "postgres",
Image: fmt.Sprintf("postgres:%s", pg.Spec.Version),
Ports: []corev1.ContainerPort{
{
Name: "postgres",
ContainerPort: 5432,
Protocol: corev1.ProtocolTCP,
},
},
Env: []corev1.EnvVar{
{
Name: "POSTGRES_PASSWORD",
Value: "changeme",
},
},
VolumeMounts: []corev1.VolumeMount{
{
Name: "data",
MountPath: "/var/lib/postgresql/data",
},
},
},
},
},
},
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{
{
ObjectMeta: metav1.ObjectMeta{
Name: "data",
},
Spec: corev1.PersistentVolumeClaimSpec{
AccessModes: []corev1.PersistentVolumeAccessMode{
corev1.ReadWriteOnce,
},
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceStorage: *parseQuantity(pg.Spec.Storage),
},
},
},
},
},
},
}
return sts
}
// constructService cria o Service para o PostgreSQL
func (r PostgresInstanceReconciler) constructService(pg databasev1.PostgresInstance) *corev1.Service {
labels := map[string]string{
"app": "postgres",
"instance": pg.Name,
}
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: pg.Name,
Namespace: pg.Namespace,
Labels: labels,
},
Spec: corev1.ServiceSpec{
Selector: labels,
Ports: []corev1.ServicePort{
{
Name: "postgres",
Port: 5432,
TargetPort: intstr.FromString("postgres"),
Protocol: corev1.ProtocolTCP,
},
},
ClusterIP: corev1.ClusterIPNone, // Headless service para StatefulSets
},
}
return svc
}
// SetupWithManager registra o controlador com o manager
func (r *PostgresInstanceReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&databasev1.PostgresInstance{}).
Owns(&appsv1.StatefulSet{}).
Owns(&corev1.Service{}).
WithEventFilter(predicate.GenerationChangedPredicate{}). // Ignora updates de status
Complete(r)
}
// Helper para parsear quantity
func parseQuantity(s string) *resource.Quantity {
q, _ := resource.ParseQuantity(s)
return &q
}</code></pre>
<h2>Ciclo de Vida e Padrões Avançados</h2>
<h3>Reconciliação: O Loop Fundamental</h3>
<p>A reconciliação é o mecanismo central dos Operators. O controlador observa recursos e, quando há mudanças (criação, update, delete), o método <code>Reconcile</code> é acionado. Importante: o reconciliador deve ser idempotente — pode ser chamado múltiplas vezes e o resultado final deve ser sempre o mesmo.</p>
<p>No exemplo anterior, nossa lógica:</p>
<ol>
<li>Busca o recurso <code>PostgresInstance</code></li>
<li>Verifica se está sendo deletado (finalizers)</li>
<li>Cria ou atualiza StatefulSet e Service</li>
<li>Atualiza o status refletindo o estado real</li>
</ol>
<p>Essa lógica roda continuously. Se alguém deletar manualmente o StatefulSet, na próxima reconciliação ele será recriado automaticamente.</p>
<h3>Finalizers: Limpeza Segura</h3>
<p>Quando você deleta um recurso, Kubernetes normalmente remove-o imediatamente. Com finalizers, você pode executar lógica de limpeza antes da deletion definitiva. No exemplo acima, adicionamos um finalizer que permite fazer backup ou limpeza de dados antes de remover o PostgresInstance.</p>
<h3>Owner References: Rastreamento de Dependências</h3>
<p>Usamos <code>SetControllerReference</code> para estabelecer uma relação pai-filho entre o <code>PostgresInstance</code> (pai) e seus <code>StatefulSet</code> e <code>Service</code> (filhos). Quando o pai é deletado, os filhos são automaticamente removidos (garbage collection).</p>
<h3>Observabilidade: Logs e Métricas</h3>
<pre><code class="language-go">import (
"sigs.k8s.io/controller-runtime/pkg/log"
)
// No seu Reconcile:
log := log.FromContext(ctx)
log.Info("Iniciando reconciliação", "instance", pg.Name, "replicas", pg.Spec.Replicas)
log.Error(err, "Falha ao criar StatefulSet", "name", pg.Name)</code></pre>
<p>O controller-runtime integra-se com loggers estruturados (Zap, por padrão) e você consegue rastrear exatamente o que seu Operator está fazendo.</p>
<h2>Deployment e Testes</h2>
<h3>Gerando e Instalando a CRD</h3>
<p>Após implementar seu controlador, execute:</p>
<pre><code class="language-bash">make manifests</code></pre>
<p>Isso gera os YAMLs em <code>config/crd/bases/</code>. Para instalar localmente em um cluster:</p>
<pre><code class="language-bash">kubectl apply -f config/crd/bases/</code></pre>
<h3>Rodando o Operator Localmente</h3>
<p>Durante desenvolvimento, você pode rodar o controlador na sua máquina:</p>
<pre><code class="language-bash">make install run</code></pre>
<p>Isso instala a CRD e executa o Operator localmente, conectando-se ao seu cluster via kubeconfig.</p>
<h3>Buildando a Imagem Docker</h3>
<p>Para deployar em produção, você precisa criar uma imagem Docker:</p>
<pre><code class="language-dockerfile"># Dockerfile (já fornecido pelo Operator SDK)
FROM golang:1.20 as builder
WORKDIR /workspace
COPY go.mod go.mod
COPY go.sum go.sum
RUN go mod download
COPY main.go main.go
COPY api/ api/
COPY controllers/ controllers/
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags="-w -s" -o manager main.go
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532
ENTRYPOINT ["/manager"]</code></pre>
<pre><code class="language-bash">docker build -t myregistry/postgres-operator:v0.1.0 .
docker push myregistry/postgres-operator:v0.1.0</code></pre>
<h3>Testando com Envtest</h3>
<p>Para testes unitários, use o framework <code>envtest</code> que spinna um APIServer e etcd reais:</p>
<pre><code class="language-go">package controllers
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
databasev1 "github.com/example/postgres-operator/api/v1"
)
var testEnv *envtest.Environment
func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Controller Suite")
}
var _ = BeforeSuite(func() {
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
testEnv = &envtest.Environment{}
cfg, err := testEnv.Start()
Expect(err).NotTo(HaveOccurred())
err = databasev1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
})
var _ = AfterSuite(func() {
Expect(testEnv.Stop()).To(Succeed())
})</code></pre>
<h2>Conclusão</h2>
<p>Ao longo deste artigo, você aprendeu que <strong>Operators não são aplicações genéricas, mas codificações de conhecimento operacional específico</strong>. Você agora compreende como CRDs funcionam como contratos que definem a API do seu Operator, permitindo que usuários declarem intenção ao invés de descrever procedimentos.</p>
<p>O padrão de reconciliação contínua, embora possa parecer simples, é extraordinariamente poderoso: o controlador sempre busca o estado desejado (declarado na CRD) e o estado real (Pods, Services, etc) e age para reconciliá-los. Isso significa que seu Operator recupera-se automaticamente de falhas, não é necessário polling manual e a lógica é centralizada e versionável.</p>
<p>Por fim, lembre-se que <strong>a qualidade de um Operator está na robustez, observabilidade e documentação</strong>. Testes automatizados, logs estruturados, finalizers bem implementados e CRDs com validação clara transformam um código inicial em uma ferramenta confiável que outros engenheiros podem usar com segurança em seus clusters.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://kubernetes.io/docs/concepts/extend-kubernetes/operator/" target="_blank" rel="noopener noreferrer">Kubernetes Operator Pattern - Official Documentation</a></li>
<li><a href="https://sdk.operatorframework.io/" target="_blank" rel="noopener noreferrer">Operator SDK Documentation</a></li>
<li><a href="https://book.kubebuilder.io/" target="_blank" rel="noopener noreferrer">Kubebuilder Book - Practical Guide to Building Operators</a></li>
<li><a href="https://github.com/kubernetes-sigs/controller-runtime" target="_blank" rel="noopener noreferrer">Controller Runtime - Go Client Library</a></li>
<li><a href="https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/" target="_blank" rel="noopener noreferrer">Custom Resource Definitions in Kubernetes</a></li>
</ul>
<p><!-- FIM --></p>