Operator SDK: criando operadores em Go ou Ansible

1. Fundamentos de Operadores no Kubernetes

Operadores são extensões do Kubernetes que automatizam tarefas complexas de gerenciamento de aplicações. Eles implementam o padrão de reconciliação (Controller Pattern), onde um loop contínuo observa o estado atual do cluster e o compara com o estado desejado definido em um Recurso Customizado (CR).

A diferença fundamental entre operadores, controllers e CRDs pode ser resumida assim:
- CRD (Custom Resource Definition): define um novo tipo de recurso no Kubernetes (ex: MyApp, Database)
- Controller: implementa a lógica de reconciliação para um recurso específico
- Operador: é a combinação de um CRD + Controller + lógica de negócio completa

Casos de uso comuns incluem: gerenciamento de StatefulSets complexos (como bancos de dados), backups automatizados, upgrades com zero downtime e instalação/configuração de software empresarial.

2. Introdução ao Operator SDK

O Operator SDK é um framework que facilita a criação de operadores Kubernetes. Atualmente na versão v1 (substituindo a v0.x), ele oferece scaffolds para Go, Ansible e Helm.

A estrutura de um projeto criado com o SDK inclui:

meu-operator/
├── api/           # definições da CRD
├── controllers/   # lógica de reconciliação
├── config/        # manifests Kubernetes (RBAC, CRDs, deploy)
├── Dockerfile
├── Makefile
└── main.go

O fluxo de desenvolvimento segue: scaffold inicial → implementação da lógica → build da imagem → deploy no cluster.

3. Criando um Operador com Go (Golang)

Para criar um operador Go, execute:

operator-sdk init --domain=example.com --repo=github.com/user/meu-operator
operator-sdk create api --group=app --version=v1 --kind=MyApp --resource --controller

A lógica principal fica na função Reconcile:

func (r *MyAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    log := log.FromContext(ctx)

    // Buscar o CR
    var myApp appv1.MyApp
    if err := r.Get(ctx, req.NamespacedName, &myApp); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    // Definir estado desejado
    desiredReplicas := myApp.Spec.Replicas

    // Criar ou atualizar Deployment filho
    deploy := &appsv1.Deployment{
        ObjectMeta: metav1.ObjectMeta{
            Name:      myApp.Name + "-deploy",
            Namespace: myApp.Namespace,
        },
        Spec: appsv1.DeploymentSpec{
            Replicas: &desiredReplicas,
            Template: corev1.PodTemplateSpec{
                Spec: corev1.PodSpec{
                    Containers: []corev1.Container{
                        {
                            Name:  "app",
                            Image: myApp.Spec.Image,
                        },
                    },
                },
            },
        },
    }

    // Aplicar controle de ownership
    if err := controllerutil.SetControllerReference(&myApp, deploy, r.Scheme); err != nil {
        return ctrl.Result{}, err
    }

    // Criar ou atualizar
    _, err := controllerutil.CreateOrUpdate(ctx, r.Client, deploy, func() error {
        deploy.Spec.Replicas = &desiredReplicas
        return nil
    })

    // Atualizar status do CR
    myApp.Status.ReadyReplicas = deploy.Status.ReadyReplicas
    r.Status().Update(ctx, &myApp)

    return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}

Este exemplo cria um operador que gerencia uma aplicação web escalável, mantendo um Deployment sincronizado com o CR.

4. Criando um Operador com Ansible

Para operadores Ansible, o scaffold é:

operator-sdk init --domain=example.com --plugins=ansible
operator-sdk create api --group=db --version=v1 --kind=Database --generate-playbook

O arquivo watches.yaml mapeia CRDs para roles Ansible:

---
- version: v1
  group: db.example.com
  kind: Database
  role: /opt/ansible/roles/database

A estrutura de roles Ansible segue o padrão:

roles/database/
├── defaults/main.yml    # valores padrão
├── tasks/main.yml       # tarefas principais
├── templates/           # templates Jinja2
└── vars/main.yml        # variáveis

Exemplo de tarefa para configurar MySQL:

---
- name: Criar diretório de dados
  file:
    path: "/data/{{ meta.name }}"
    state: directory

- name: Deploy do StatefulSet MySQL
  k8s:
    state: present
    definition:
      apiVersion: apps/v1
      kind: StatefulSet
      metadata:
        name: "{{ meta.name }}-mysql"
        namespace: "{{ meta.namespace }}"
      spec:
        replicas: "{{ spec.replicas | default(1) }}"
        selector:
          matchLabels:
            app: mysql
        template:
          spec:
            containers:
            - name: mysql
              image: "{{ spec.image | default('mysql:8.0') }}"
              env:
              - name: MYSQL_ROOT_PASSWORD
                value: "{{ spec.rootPassword }}"

Vantagens do Ansible: menor curva de aprendizado, reuso de playbooks existentes. Limitações: performance inferior ao Go, menor controle sobre reconciliação.

5. Testes, Validação e Debug de Operadores

Testes unitários com envtest:

func TestMyAppController(t *testing.T) {
    // Configurar ambiente de teste
    cfg, ctx := setupTestEnv(t)
    defer ctx.Cancel()

    k8sClient, err := client.New(cfg, client.Options{})
    require.NoError(t, err)

    // Criar CR de teste
    myApp := &appv1.MyApp{
        ObjectMeta: metav1.ObjectMeta{
            Name:      "test-app",
            Namespace: "default",
        },
        Spec: appv1.MyAppSpec{
            Replicas: 3,
            Image:   "nginx:latest",
        },
    }
    err = k8sClient.Create(ctx, myApp)
    require.NoError(t, err)

    // Verificar se o Deployment foi criado
    deploy := &appsv1.Deployment{}
    err = k8sClient.Get(ctx, types.NamespacedName{
        Name: "test-app-deploy", Namespace: "default",
    }, deploy)
    require.NoError(t, err)
    assert.Equal(t, int32(3), *deploy.Spec.Replicas)
}

Para debug local, use make run e monitore os logs:

kubectl logs deployment/meu-operator-controller-manager -n meu-operator-system
operator-sdk scorecard --namespace meu-operator-system

6. Build, Deploy e Gerenciamento do Ciclo de Vida

Dockerfile multi-estágio para operador Go:

# Build stage
FROM golang:1.21 AS builder
WORKDIR /workspace
COPY go.mod go.mod
COPY go.sum go.sum
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o manager main.go

# Runtime stage
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532
ENTRYPOINT ["/manager"]

Deploy via OLM (Operator Lifecycle Manager):

operator-sdk generate kustomize manifests
make bundle
operator-sdk bundle validate ./bundle
operator-sdk run bundle quay.io/user/meu-operator:v0.1.0

Para monitoramento, adicione métricas Prometheus:

import (
    "sigs.k8s.io/controller-runtime/pkg/metrics"
    "github.com/prometheus/client_golang/prometheus"
)

var (
    appReplicas = prometheus.NewGaugeVec(
        prometheus.GaugeOpts{
            Name: "myapp_replicas_total",
            Help: "Total replicas managed by operator",
        },
        []string{"name", "namespace"},
    )
)

func init() {
    metrics.Registry.MustRegister(appReplicas)
}

7. Boas Práticas e Padrões Avançados

Tratamento de erros com retry e backoff:

func (r *MyAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    if err := r.syncDeployment(ctx, req); err != nil {
        // Retry com backoff exponencial
        return ctrl.Result{RequeueAfter: time.Duration(rand.Intn(10)) * time.Second}, nil
    }
    return ctrl.Result{}, nil
}

Finalizers para limpeza:

const myAppFinalizer = "finalizer.example.com"

func (r *MyAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    myApp := &appv1.MyApp{}
    r.Get(ctx, req.NamespacedName, myApp)

    if !myApp.DeletionTimestamp.IsZero() {
        if controllerutil.ContainsFinalizer(myApp, myAppFinalizer) {
            // Limpar recursos externos
            controllerutil.RemoveFinalizer(myApp, myAppFinalizer)
            r.Update(ctx, myApp)
        }
        return ctrl.Result{}, nil
    }

    controllerutil.AddFinalizer(myApp, myAppFinalizer)
    r.Update(ctx, myApp)
    // ... lógica principal
}

RBAC mínimo:

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: meu-operator-role
rules:
- apiGroups: ["app.example.com"]
  resources: ["myapps", "myapps/status", "myapps/finalizers"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

8. Conclusão e Próximos Passos

O fluxo completo de criação de um operador envolve: definir um CRD → implementar o controller com lógica de reconciliação → construir e deployar o operador no cluster. O Operator SDK abstrai grande parte da complexidade, permitindo focar na lógica de negócio.

Para aprofundamento, explore integrações com Knative (serverless), Kubeflow (ML pipelines) e sistemas de batch processing. A comunidade Kubernetes oferece vasto material para evoluir seus operadores.

Referências