Escrevendo um Reconciliador: ctrl.Request & Loop de Reconciliação
O reconciliador é o coração de um operador: uma função que o Kubernetes chama sempre que um objeto observado precisa de atenção.
Busque em todas as páginas da documentação
O reconciliador é o coração de um operador: uma função que o Kubernetes chama sempre que um objeto observado precisa de atenção.
ctrl.Request informa qual objeto buscar; seu código direciona o cluster em direção à especificação e retorna quando executar novamente.
Reconcile(ctx, req) deve ser idempotente e seguro para executar várias vezes para o mesmo objeto.
Busque o CR mais recente com r.Get, compare spec com recursos filhos, crie ou aplique patches em lacunas, e então atualize status.
Retorne ctrl.Result{} em caso de sucesso, ctrl.Result{RequeueAfter: duration} para esperar, ou um erro para acionar backoff exponencial.
Trate NotFound como sucesso quando o objeto foi excluído.
Cartão de receita de referência rápida - pronto para copiar e colar.
func (r *GuestbookReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var gb webappv1.Guestbook
if err := r.Get(ctx, req.NamespacedName, &gb); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
if err := r.ensureFrontend(ctx, &gb); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}Quando usar isso:
package controller
import (
"context"
"time"
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/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/log"
webappv1 "example.com/guestbook-operator/api/v1"
)
const guestbookFinalizer = "webapp.example.com/finalizer"
func (r *GuestbookReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
var gb webappv1.Guestbook
if err := r.Get(ctx, req.NamespacedName, &gb); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
if gb.DeletionTimestamp.IsZero() {
if !controllerutil.ContainsFinalizer(&gb, guestbookFinalizer) {
controllerutil.AddFinalizer(&gb, guestbookFinalizer)
if err := r.Update(ctx, &gb); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{Requeue: true}, nil
}
} else {
if err := r.teardown(ctx, &gb); err != nil {
return ctrl.Result{}, err
}
controllerutil.RemoveFinalizer(&gb, guestbookFinalizer)
if err := r.Update(ctx, &gb); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
dep := &appsv1.Deployment{}
depName := types.NamespacedName{Name: gb.Name + "-frontend", Namespace: gb.Namespace}
err := r.Get(ctx, depName, dep)
if apierrors.IsNotFound(err) {
dep = r.desiredDeployment(&gb)
if err := controllerutil.SetControllerReference(&gb, dep, r.Scheme); err != nil {
return ctrl.Result{}, err
}
if err := r.Create(ctx, dep); err != nil {
return ctrl.Result{}, err
}
logger.Info("created Deployment", "name", dep.Name)
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
}
if err != nil {
return ctrl.Result{}, err
}
gb.Status.Ready = dep.Status.ReadyReplicas >= gb.Spec.FrontendSize
if err := r.Status().Update(ctx, &gb); err != nil {
return ctrl.Result{}, err
}
if !gb.Status.Ready {
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
}
return ctrl.Result{}, nil
}
func (r *GuestbookReconciler) desiredDeployment(gb *webappv1.Guestbook) *appsv1.Deployment {
replicas := gb.Spec.FrontendSize
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: gb.Name + "-frontend", Namespace: gb.Namespace},
Spec: appsv1.DeploymentSpec{
Replicas: &replicas,
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": gb.Name}},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": gb.Name}},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "frontend",
Image: "nginx:1.27",
}},
},
},
},
}
}
func (r *GuestbookReconciler) teardown(ctx context.Context, gb *webappv1.Guestbook) error {
return nil // delete external DB rows, revoke IAM, etc.
}O que isso demonstra:
RequeueAfter enquanto espera pela prontidão do Deploymentctrl.Request{NamespacedName}; ela não passa o corpo do objeto.Get, pois caches e outros controladores podem ter alterado objetos durante a execução.error não nulo reenfileira com backoff exponencial, a menos que você use RequeueAfter com erro nulo.SetupWithManager conecta For(&Guestbook{}) para que alterações de especificação/status enfileirem o pai.| Retorno | Efeito |
|---|---|
ctrl.Result{}, nil | Sucesso; reenfileira apenas em futuros eventos de observação |
ctrl.Result{Requeue: true}, nil | Reenfileiramento imediato |
ctrl.Result{RequeueAfter: d}, nil | Reenfileiramento atrasado sem métrica de erro |
ctrl.Result{}, err | Erro registrado; reenfileiramento com backoff |
// Aplica patch nas condições de status sem sobrescrever outros campos
meta.SetStatusCondition(&gb.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionTrue,
Reason: "DeploymentReady",
Message: "frontend replicas ready",
})
// Agrega múltiplos erros de garantias filhas
return ctrl.Result{}, errors.Join(errDeploy, errSvc)spec do CR unicamente por webhooks de default ou edições do usuário.client.IgnoreNotFound(err) em Get.Update em vez de Status().Update - falhas de RBAC ou conflitos de especificação. Correção: use o cliente do subrecurso de status.RequeueAfter com limites razoáveis e exponha condições Degraded.SetControllerReference antes de Create.| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
| controller-runtime Reconciler | Operadores kubebuilder tipados | Você só precisa de um único callback de informer |
| Fila de trabalho bruta + manipuladores de informer | Controle de fila granular | Você deseja helpers de status e o padrão builder |
| Anotações de reconciliação Kopf / Java | Pilhas não-Go | Você precisa de controller-gen e envtest |
| Job por evento de CR | Transformações raras em lote | Convergência contínua é necessária |
NamespacedName com campos Namespace e Name.
Busque o objeto completo você mesmo com r.Get.
Use server-side apply ou strategic merge patches quando você possui apenas parte de um objeto compartilhado.
Para Deployments de propriedade que você controla totalmente, compare a especificação e use Update ou Patch idempotentemente.
Retorne error para falhas inesperadas da API que valem a pena alertar.
Use RequeueAfter para esperar o esperado (Deployment ainda não pronto).
Registre Owns(&appsv1.Deployment{}) em SetupWithManager para que as alterações do Deployment enfileirem o Guestbook proprietário.
Cada passagem lê a especificação atual e o estado completo do cluster, e então converge.
Não assume que você viu todos os eventos individuais que causaram desvio.
Mantenha a reconciliação rápida; delegue tarefas longas para Jobs ou goroutines com rastreamento de status cuidadoso.
Bloquear a reconciliação por muito tempo bloqueia threads de trabalho e atrasa outros objetos.
Use envtest ou um cliente fake, chame Reconcile com um ctrl.Request construído, e afirme sobre objetos filhos e status.
Passe ctx para todas as chamadas de cliente.
O cancelamento do Manager no desligamento deve interromper as chamadas de API de saída prontamente.
Use predicados para filtrar atualizações de status barulhentas, não para pular passagens orientadas por nível necessárias em alterações de especificação.
O paralelismo padrão geralmente é suficiente.
Aumente MaxConcurrentReconciles apenas quando a análise de desempenho mostrar backlog na fila e os limites da API permitirem.
Versões da Stack: Esta página foi escrita para Go 1.26.x (GC padrão Green Tea, go fix modernizers - verifique o patch na compilação), chi (última - verifique na compilação), gin (última - verifique na compilação), echo (última - verifique na compilação), google.golang.org/grpc (última - verifique na compilação), sigs.k8s.io/controller-runtime (última - verifique na compilação), kubebuilder (última - verifique na compilação), tinygo (última - verifique os alvos de placa na compilação), wazero (última - verifique na compilação) e golangci-lint (última - verifique o conjunto de linters na compilação).
Revisado por Chris St. John·Última atualização: 16 de jul. de 2026