← Voltar para o blog

Go e Kubernetes: Como Criar Operadores com controller-runtime

Aprenda a criar operadores Kubernetes em Go com controller-runtime. Tutorial com Reconcile loop, CRDs, envtest e exemplos de codigo prontos para usar.

Operadores Kubernetes sao uma das abstracoees mais poderosas do ecossistema cloud-native – e Go e a linguagem dominante para construi-los. Projetos como cert-manager, ArgoCD, Prometheus Operator e Crossplane sao todos escritos em Go, usando frameworks como controller-runtime e kubebuilder para gerenciar recursos customizados de forma declarativa.

Se voce ja trabalha com Docker e containers em Go ou construiu microsservicos com gRPC, criar operadores e o proximo passo natural na sua jornada cloud-native. Neste guia, vamos do conceito ao codigo funcional, mostrando como implementar um operador completo com controller-runtime.

O que sao Operadores Kubernetes

Um operador Kubernetes e um controller customizado que estende a API do Kubernetes para gerenciar aplicacoes complexas de forma automatizada. Em vez de scripts manuais ou runbooks, voce codifica o conhecimento operacional em um programa que o Kubernetes executa continuamente.

O padrao se baseia em dois conceitos:

  • Custom Resource Definitions (CRDs) – definem novos tipos de recursos no cluster (ex: PostgresCluster, RedisBackup)
  • Controllers – observam esses recursos e reconciliam o estado desejado com o estado atual

Pense em como o Kubernetes gerencia Deployments nativamente: voce declara que quer 3 replicas e o controller garante que existam exatamente 3. Um operador faz o mesmo para qualquer aplicacao – bancos de dados, filas de mensagens, certificados TLS.

Por que Go domina o ecossistema de operadores

Go e a linguagem padrao para operadores por razoes praticas:

  • Kubernetes e escrito em Go – client-go, a biblioteca oficial para acessar a API, e nativa
  • controller-runtime – o framework de referencia para operadores e em Go
  • Binarios estaticos – um unico binario sem dependencias facilita o deploy em containers
  • Concorrencia nativagoroutines e channels sao ideais para observar multiplos recursos simultaneamente
  • Tipagem forte – tipos Go para cada recurso Kubernetes eliminam erros em runtime

Se voce esta avaliando linguagens para backend, vale conferir por que aprender Go e como a linguagem se compara com alternativas como Rust ou Java nesse contexto.

Configurando o projeto com kubebuilder

Kubebuilder e a ferramenta oficial para scaffolding de operadores Go. Ele gera a estrutura do projeto, CRDs, controllers e manifests Kubernetes automaticamente.

# Instalar kubebuilder
curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)
chmod +x kubebuilder && sudo mv kubebuilder /usr/local/bin/

# Criar projeto
mkdir meu-operador && cd meu-operador
kubebuilder init --domain exemplo.com.br --repo github.com/seu-usuario/meu-operador

# Criar API (CRD + Controller)
kubebuilder create api --group app --version v1 --kind AppConfig

Esse comando gera a estrutura completa com Go modules configurados, incluindo os tipos CRD em api/v1/ e o controller em internal/controller/.

Definindo o Custom Resource

O CRD define a estrutura do recurso que seu operador vai gerenciar. Vamos criar um AppConfig que gerencia configuracoes de aplicacao:

package v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// AppConfigSpec define o estado desejado
type AppConfigSpec struct {
	// Replicas define o numero de instancias
	Replicas int32 `json:"replicas"`

	// Image define a imagem do container
	Image string `json:"image"`

	// ConfigData armazena configuracoes em key-value
	ConfigData map[string]string `json:"configData,omitempty"`

	// AutoScale habilita escalonamento automatico
	AutoScale bool `json:"autoScale,omitempty"`
}

// AppConfigStatus define o estado observado
type AppConfigStatus struct {
	// ReadyReplicas indica quantas replicas estao prontas
	ReadyReplicas int32 `json:"readyReplicas"`

	// Phase indica a fase atual (Pending, Running, Failed)
	Phase string `json:"phase"`

	// LastReconcileTime registra a ultima reconciliacao
	LastReconcileTime *metav1.Time `json:"lastReconcileTime,omitempty"`

	// Conditions lista as condicoes atuais do recurso
	Conditions []metav1.Condition `json:"conditions,omitempty"`
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:printcolumn:name="Replicas",type=integer,JSONPath=`.spec.replicas`
//+kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`

// AppConfig e o Schema para a API appconfigs
type AppConfig struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec   AppConfigSpec   `json:"spec,omitempty"`
	Status AppConfigStatus `json:"status,omitempty"`
}

//+kubebuilder:object:root=true

// AppConfigList contem uma lista de AppConfig
type AppConfigList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitempty"`
	Items           []AppConfig `json:"items"`
}

Os markers //+kubebuilder: geram automaticamente os manifests YAML do CRD, subresources e colunas extras para kubectl get.

Implementando o Reconcile Loop

O Reconcile loop e o coracao de qualquer operador. Ele e chamado sempre que o estado do recurso muda e deve garantir que o estado real corresponda ao desejado:

package controller

import (
	"context"
	"fmt"
	"time"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	"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/log"

	appv1 "github.com/seu-usuario/meu-operador/api/v1"
)

// AppConfigReconciler reconcilia objetos AppConfig
type AppConfigReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

//+kubebuilder:rbac:groups=app.exemplo.com.br,resources=appconfigs,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=app.exemplo.com.br,resources=appconfigs/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete

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

	// 1. Buscar o recurso AppConfig
	var appConfig appv1.AppConfig
	if err := r.Get(ctx, req.NamespacedName, &appConfig); err != nil {
		if errors.IsNotFound(err) {
			logger.Info("AppConfig deletado, ignorando")
			return ctrl.Result{}, nil
		}
		return ctrl.Result{}, err
	}

	// 2. Verificar se o Deployment ja existe
	var deployment appsv1.Deployment
	deploymentName := types.NamespacedName{
		Name:      appConfig.Name + "-deployment",
		Namespace: appConfig.Namespace,
	}

	err := r.Get(ctx, deploymentName, &deployment)
	if err != nil && errors.IsNotFound(err) {
		// 3. Criar o Deployment se nao existir
		dep := r.criarDeployment(&appConfig)
		logger.Info("Criando Deployment", "nome", dep.Name)
		if err := r.Create(ctx, dep); err != nil {
			return ctrl.Result{}, fmt.Errorf("falha ao criar deployment: %w", err)
		}
	} else if err != nil {
		return ctrl.Result{}, err
	} else {
		// 4. Atualizar se a spec mudou
		if *deployment.Spec.Replicas != appConfig.Spec.Replicas {
			deployment.Spec.Replicas = &appConfig.Spec.Replicas
			if err := r.Update(ctx, &deployment); err != nil {
				return ctrl.Result{}, fmt.Errorf("falha ao atualizar deployment: %w", err)
			}
			logger.Info("Replicas atualizadas", "replicas", appConfig.Spec.Replicas)
		}
	}

	// 5. Atualizar o status
	now := metav1.Now()
	appConfig.Status.Phase = "Running"
	appConfig.Status.ReadyReplicas = appConfig.Spec.Replicas
	appConfig.Status.LastReconcileTime = &now

	if err := r.Status().Update(ctx, &appConfig); err != nil {
		return ctrl.Result{}, fmt.Errorf("falha ao atualizar status: %w", err)
	}

	// Reconciliar novamente em 5 minutos para verificar saude
	return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil
}

func (r *AppConfigReconciler) criarDeployment(app *appv1.AppConfig) *appsv1.Deployment {
	labels := map[string]string{
		"app":        app.Name,
		"managed-by": "appconfig-operator",
	}

	dep := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      app.Name + "-deployment",
			Namespace: app.Namespace,
			Labels:    labels,
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: &app.Spec.Replicas,
			Selector: &metav1.LabelSelector{
				MatchLabels: labels,
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{Labels: labels},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{{
						Name:  app.Name,
						Image: app.Spec.Image,
						Ports: []corev1.ContainerPort{{
							ContainerPort: 8080,
						}},
					}},
				},
			},
		},
	}

	// Definir owner reference para garbage collection automatica
	ctrl.SetControllerReference(app, dep, r.Scheme)
	return dep
}

// SetupWithManager configura o controller no manager
func (r *AppConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&appv1.AppConfig{}).
		Owns(&appsv1.Deployment{}).
		Complete(r)
}

Observe como o context e passado em toda a cadeia – um padrao fundamental em Go para controlar cancelamento e timeouts. O tratamento de erros idiomatico com fmt.Errorf e wrapping garante rastreabilidade nos logs.

Observando recursos relacionados

O metodo SetupWithManager configura quais recursos o controller observa. Com Owns(&appsv1.Deployment{}), qualquer mudanca em Deployments criados pelo operador dispara uma reconciliacao automaticamente.

Para cenarios mais complexos, voce pode observar recursos arbitrarios:

func (r *AppConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&appv1.AppConfig{}).
		Owns(&appsv1.Deployment{}).
		Owns(&corev1.ConfigMap{}).
		Watches(
			&corev1.Secret{},
			handler.EnqueueRequestsFromMapFunc(r.findAppConfigsForSecret),
		).
		WithOptions(controller.Options{
			MaxConcurrentReconciles: 3,
		}).
		Complete(r)
}

A opcao MaxConcurrentReconciles aproveita diretamente o modelo de concorrencia do Go – cada reconciliacao roda em sua propria goroutine, e o controller-runtime gerencia a sincronizacao.

Testando operadores com envtest

O framework envtest permite testar operadores contra uma API Kubernetes real, sem precisar de um cluster completo. Ele sobe um etcd e um API server localmente:

package controller_test

import (
	"context"
	"testing"
	"time"

	. "github.com/onsi/gomega"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"

	appv1 "github.com/seu-usuario/meu-operador/api/v1"
)

func TestAppConfigReconciler(t *testing.T) {
	g := NewWithT(t)

	// Criar um AppConfig de teste
	appConfig := &appv1.AppConfig{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "teste-app",
			Namespace: "default",
		},
		Spec: appv1.AppConfigSpec{
			Replicas: 3,
			Image:    "nginx:latest",
		},
	}

	// Criar no cluster de teste
	err := k8sClient.Create(context.Background(), appConfig)
	g.Expect(err).NotTo(HaveOccurred())

	// Verificar que o Deployment foi criado
	deploymentKey := types.NamespacedName{
		Name:      "teste-app-deployment",
		Namespace: "default",
	}

	g.Eventually(func() bool {
		var dep appsv1.Deployment
		err := k8sClient.Get(context.Background(), deploymentKey, &dep)
		return err == nil
	}, 10*time.Second, 250*time.Millisecond).Should(BeTrue())
}

Combinar envtest com testes nativos do Go garante que seu operador funcione corretamente antes de chegar ao cluster de producao. Para testes de integracao mais amplos, Testcontainers em Go pode complementar a estrategia.

Deploy e distribuicao

Operadores sao distribuidos como containers normais. O kubebuilder gera um Dockerfile multi-stage otimizado:

FROM golang:1.22 AS builder
WORKDIR /workspace
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -o manager cmd/main.go

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

Para deploy no cluster, voce pode usar:

  • make deploy – aplica os CRDs e o Deployment do operador
  • Helm charts – para distribuicao em multiplos clusters
  • OLM (Operator Lifecycle Manager) – para publicacao no OperatorHub

Se voce utiliza Terraform com Go para gerenciar infraestrutura, operadores complementam essa abordagem gerenciando o ciclo de vida das aplicacoes dentro do cluster.

Casos de uso reais

Alguns dos operadores open-source mais populares em Go:

  • cert-manager – gerencia certificados TLS automaticamente
  • ArgoCD – GitOps e deploy continuo
  • Prometheus Operator – configura monitoramento declarativamente
  • Crossplane – provisiona infraestrutura cloud via CRDs
  • Strimzi – gerencia clusters Kafka no Kubernetes

Todos usam controller-runtime e seguem os mesmos padroes descritos aqui. A observabilidade com OpenTelemetry e essencial para monitorar operadores em producao, fornecendo metricas sobre latencia de reconciliacao e erros.

Para otimizar ainda mais a performance do seu operador em producao, vale explorar Profile-Guided Optimization (PGO) – uma tecnica que usa perfis reais de CPU para gerar binarios mais eficientes.

Boas praticas para operadores

  1. Idempotencia – o Reconcile deve produzir o mesmo resultado independente de quantas vezes e chamado
  2. Owner References – sempre defina para que recursos filhos sejam coletados automaticamente
  3. Finalizers – use para cleanup de recursos externos ao deletar
  4. Status conditions – reporte o estado via conditions padronizadas
  5. Rate limiting – configure backoff para evitar loops de reconciliacao
  6. Logging estruturado – use o slog ou o logger do controller-runtime para logs contextuais
  7. Metricas – exporte metricas Prometheus sobre reconciliacoes, erros e latencia

Perguntas frequentes

Qual a diferenca entre um operador e um controller?

Todo operador e um controller, mas nem todo controller e um operador. Um controller observa recursos e reconcilia estado. Um operador e um controller que encapsula conhecimento operacional sobre uma aplicacao especifica – ele sabe como fazer backup, restore, upgrade e scaling de forma inteligente.

Preciso usar kubebuilder para criar operadores?

Nao. Kubebuilder e uma ferramenta de scaffolding que acelera o desenvolvimento, mas voce pode usar controller-runtime diretamente. Outros frameworks como Operator SDK (baseado em kubebuilder) e metacontroller tambem sao opcoes. O importante e usar controller-runtime como base.

Operadores em Go sao mais performaticos que em outras linguagens?

Sim, em geral. Go gera binarios compilados com footprint de memoria baixo, inicializacao rapida e excelente suporte a concorrencia. Operadores em Python (kopf) ou Java (java-operator-sdk) tendem a consumir mais recursos e ter startup mais lento – algo critico em clusters com muitos operadores.

Como depurar problemas no Reconcile loop?

Use logging estruturado com niveis (info, debug, error), metricas de latencia por reconciliacao e o flight recorder do Go 1.25+ para capturar traces retroativos. Em desenvolvimento, rode o operador localmente com make run apontando para um cluster de teste.

Posso ter multiplos controllers no mesmo operador?

Sim, e e comum. O manager do controller-runtime suporta registrar multiplos controllers que compartilham o mesmo cache e cliente da API. Cada controller observa seus proprios recursos e tem seu proprio Reconcile loop independente.

Operadores Kubernetes também podem ser escritos em outras linguagens. Compare com o ecossistema de operadores em Rust com kube-rs ou operadores em Python com kopf.