---
title: "Kubernetes Operators em Zig: CRDs, Watch, Reconcile e Deploy"
url: "https://ziglang.com.br/artigos/kubernetes-operators-em-zig-crds-watch-reconcile-e-deploy/"
markdown_url: "https://ziglang.com.br/artigos/kubernetes-operators-em-zig-crds-watch-reconcile-e-deploy.MD"
description: "Como construir Kubernetes Operators em Zig: arquitetura, CRDs, client HTTP, watch, reconciliation loop, RBAC, deploy e limites práticos em produção."
date: "2026-02-21"
author: "Zig Brasil"
---

# Kubernetes Operators em Zig: CRDs, Watch, Reconcile e Deploy

Como construir Kubernetes Operators em Zig: arquitetura, CRDs, client HTTP, watch, reconciliation loop, RBAC, deploy e limites práticos em produção.


# Kubernetes Operators em Zig: CRDs, Watch, Reconcile e Deploy

Kubernetes Operators automatizam a gestão de aplicações complexas: recebem um estado desejado em um recurso customizado, observam o estado real do cluster e executam ações corretivas até os dois convergirem. O caminho mais comum é escrever operators em Go com controller-runtime, mas a ideia central não depende da linguagem. Um operator é, essencialmente, um processo Linux que fala HTTP com a API do Kubernetes.

Zig entra nessa conversa quando o operador precisa ser pequeno, previsível e barato de executar: binário estático, startup rápido, controle explícito de memória e uma imagem de container que pode ficar na casa de poucos megabytes. O custo é que você abre mão do ecossistema maduro de controller-runtime e precisa implementar mais peças: client HTTP, watch, cache local, retries, RBAC, serialização JSON e idempotência.

Este guia mostra uma arquitetura realista para um Kubernetes Operator em Zig, com foco em viabilidade de produção, não apenas em um snippet bonito.

## Quando faz sentido escrever um operator em Zig?

Use Zig quando pelo menos uma destas condições for verdadeira:

- o operator roda em muitos clusters pequenos e o footprint de memória importa;
- o reconciler é simples, com poucos recursos e regras bem controladas;
- você quer distribuir um único binário estático sem runtime;
- o operator faz trabalho de baixo nível, rede, arquivos, sidecars ou integração com binários nativos;
- a equipe já usa Zig em outros componentes e aceita manter infraestrutura própria.

Evite Zig quando o operator precisa de dezenas de integrações Kubernetes prontas, webhooks complexos, leader election sofisticado, cache compartilhado, finalizers avançados e evolução rápida do schema. Nesse caso, <a href="https://golang.com.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">Go</a> com controller-runtime continua sendo a escolha pragmática.

## Arquitetura básica de um operator

Um operator mínimo em Zig precisa de quatro blocos:

1. **Client Kubernetes** — lê token, CA e namespace do service account montado no pod.
2. **Watch loop** — abre streams contra a API do Kubernetes para receber eventos de CRDs, Pods, ConfigMaps ou Deployments.
3. **Fila de reconcile** — deduplica eventos por chave `namespace/name` e controla retry/backoff.
4. **Reconciler idempotente** — compara estado desejado e real, aplica mudanças e grava status.

```text
┌─────────────────────────────────────────────┐
│ Kubernetes API                              │
│ /apis/example.com/v1/widgets?watch=true     │
└───────────────────┬─────────────────────────┘
                    │ ADDED / MODIFIED / DELETED
                    ▼
┌─────────────────────────────────────────────┐
│ Watch loop em Zig                           │
│ - parse JSON incremental                    │
│ - reconecta em timeout                      │
│ - envia namespace/name para a fila          │
└───────────────────┬─────────────────────────┘
                    ▼
┌─────────────────────────────────────────────┐
│ Work queue                                  │
│ - dedupe por chave                          │
│ - retry com backoff                         │
│ - limite de concorrência                    │
└───────────────────┬─────────────────────────┘
                    ▼
┌─────────────────────────────────────────────┐
│ Reconcile loop                              │
│ - lê CRD                                    │
│ - lê estado real                            │
│ - cria/atualiza recursos                    │
│ - atualiza .status                          │
└─────────────────────────────────────────────┘
```

## CRD de exemplo

Imagine um CRD `Widget` que declara quantas réplicas de um pequeno serviço devem existir:

```yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: widgets.example.com
spec:
  group: example.com
  scope: Namespaced
  names:
    plural: widgets
    singular: widget
    kind: Widget
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                image:
                  type: string
                replicas:
                  type: integer
                  minimum: 1
                  maximum: 10
            status:
              type: object
              properties:
                readyReplicas:
                  type: integer
                phase:
                  type: string
      subresources:
        status: {}
```

O operator deve assistir `Widget`, criar ou atualizar um `Deployment`, observar quantos pods ficaram prontos e gravar o resumo em `status`.

## Client HTTP para a API do Kubernetes

Dentro de um pod, o Kubernetes monta credenciais em `/var/run/secrets/kubernetes.io/serviceaccount/`. Você precisa ler token, CA e namespace. O exemplo abaixo é propositalmente enxuto; em produção, encapsule headers, TLS, status codes e tratamento de erros.

```zig
const std = @import("std");

const ServiceAccount = struct {
    token: []const u8,
    namespace: []const u8,

    pub fn load(allocator: std.mem.Allocator) !ServiceAccount {
        const dir = "/var/run/secrets/kubernetes.io/serviceaccount";
        const token = try std.fs.cwd().readFileAlloc(
            allocator,
            dir ++ "/token",
            64 * 1024,
        );
        const namespace = try std.fs.cwd().readFileAlloc(
            allocator,
            dir ++ "/namespace",
            4096,
        );
        return .{
            .token = std.mem.trim(u8, token, "\n\r \t"),
            .namespace = std.mem.trim(u8, namespace, "\n\r \t"),
        };
    }
};

const K8sClient = struct {
    allocator: std.mem.Allocator,
    api_server: []const u8 = "https://kubernetes.default.svc",
    service_account: ServiceAccount,

    pub fn get(self: *K8sClient, path: []const u8) ![]u8 {
        const url = try std.fmt.allocPrint(self.allocator, "{s}{s}", .{
            self.api_server,
            path,
        });
        defer self.allocator.free(url);

        var client = std.http.Client{ .allocator = self.allocator };
        defer client.deinit();

        const auth = try std.fmt.allocPrint(
            self.allocator,
            "Bearer {s}",
            .{self.service_account.token},
        );
        defer self.allocator.free(auth);

        const uri = try std.Uri.parse(url);
        var header_buf: [16 * 1024]u8 = undefined;
        var req = try client.open(.GET, uri, .{
            .server_header_buffer = &header_buf,
            .extra_headers = &.{
                .{ .name = "Authorization", .value = auth },
                .{ .name = "Accept", .value = "application/json" },
            },
        });
        defer req.deinit();

        try req.send();
        try req.wait();

        if (req.response.status != .ok) {
            return error.KubernetesRequestFailed;
        }

        return try req.reader().readAllAlloc(self.allocator, 4 * 1024 * 1024);
    }
};
```

Para `POST`, `PATCH` e `PUT`, adicione `Content-Type: application/merge-patch+json` ou `application/apply-patch+yaml`, dependendo da estratégia. Para objetos Kubernetes, `PATCH` com server-side apply costuma ser mais robusto do que montar `PUT` completo.

## Watch de CRDs

O watch da API retorna uma sequência de eventos JSON, normalmente separados por quebras de linha. Cada evento tem um `type` e um `object`.

```json
{"type":"ADDED","object":{"metadata":{"namespace":"default","name":"demo"}}}
{"type":"MODIFIED","object":{"metadata":{"namespace":"default","name":"demo"}}}
```

Em Zig, a versão inicial pode ler chunks e processar linhas completas. A versão de produção deve respeitar `resourceVersion`, tratar `410 Gone`, reconectar com backoff e aplicar timeout para não ficar presa em conexões mortas.

```zig
fn watchWidgets(client: *K8sClient, queue: *WorkQueue) !void {
    var resource_version: []const u8 = "";

    while (true) {
        const path = try std.fmt.allocPrint(
            client.allocator,
            "/apis/example.com/v1/widgets?watch=true&resourceVersion={s}",
            .{resource_version},
        );
        defer client.allocator.free(path);

        const body = client.get(path) catch |err| {
            std.log.warn("watch failed: {}, reconnecting", .{err});
            std.time.sleep(2 * std.time.ns_per_s);
            continue;
        };
        defer client.allocator.free(body);

        var lines = std.mem.splitScalar(u8, body, '\n');
        while (lines.next()) |line| {
            if (line.len == 0) continue;
            const key = parseWatchEventForKey(line) catch continue;
            try queue.pushDedup(key);
            resource_version = key.resource_version;
        }
    }
}
```

O exemplo acima simplifica a vida útil de `resource_version`; não copie a alocação literalmente. Em produção, salve uma cópia própria da string e libere a anterior.

## Reconciliation loop idempotente

Um reconciler bom pode rodar cem vezes e terminar no mesmo estado. Ele não presume que um evento significa mudança real. Ele lê o mundo, calcula o delta e aplica somente o necessário.

```zig
const ReconcileResult = union(enum) {
    done,
    requeue,
    requeue_after_ns: u64,
};

fn reconcile(client: *K8sClient, namespace: []const u8, name: []const u8) ReconcileResult {
    const widget = loadWidget(client, namespace, name) catch |err| switch (err) {
        error.NotFound => return .done,
        else => return .requeue,
    };
    defer widget.deinit();

    ensureDeployment(client, widget) catch return .requeue;
    ensureService(client, widget) catch return .requeue;

    const ready = countReadyPods(client, widget) catch return .{ .requeue_after_ns = 30 * std.time.ns_per_s };
    updateStatus(client, widget, ready) catch return .requeue;

    return .done;
}
```

Regras práticas:

- nunca dependa apenas do evento recebido; sempre releia o recurso atual;
- trate `NotFound` como sucesso quando o recurso já foi removido;
- use nomes determinísticos para objetos filhos;
- grave `ownerReferences` para garbage collection;
- atualize `status` separadamente de `spec`;
- use backoff para erros transitórios e limite tentativas ruins.

## RBAC mínimo

Não rode o operator com permissões de cluster-admin. Comece com um `Role` por namespace e expanda apenas quando necessário:

```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: widget-operator
rules:
  - apiGroups: ["example.com"]
    resources: ["widgets"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["example.com"]
    resources: ["widgets/status"]
    verbs: ["patch", "update"]
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch", "create", "patch", "update"]
  - apiGroups: [""]
    resources: ["services", "pods"]
    verbs: ["get", "list", "watch", "create", "patch", "update"]
```

Se o operator precisa gerenciar recursos em vários namespaces, use `ClusterRole` e `ClusterRoleBinding`, mas documente o motivo. Operators são automação privilegiada; permissões amplas viram incidente quando há bug no reconcile.

## Deploy com imagem pequena

O fluxo natural é compilar um binário Linux estático e copiar para uma imagem `scratch` ou distroless:

```dockerfile
FROM alpine:3.20 AS build
RUN apk add --no-cache zig ca-certificates
WORKDIR /src
COPY build.zig build.zig.zon ./
COPY src ./src
RUN zig build -Doptimize=ReleaseSafe -Dtarget=x86_64-linux-musl

FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /src/zig-out/bin/widget-operator /widget-operator
USER 65532:65532
ENTRYPOINT ["/widget-operator"]
```

Mesmo em `scratch`, mantenha certificados CA se o operator fala com endpoints HTTPS externos. Para falar só com `https://kubernetes.default.svc`, você pode usar o CA do service account montado pelo cluster.

## Observabilidade do operator

Operators silenciosos são difíceis de operar. Inclua desde o começo:

- logs estruturados com `namespace`, `name`, `resourceVersion`, resultado e duração;
- métricas de reconcile por status: sucesso, retry, erro permanente;
- tamanho e latência da fila;
- contador de reconexões do watch;
- readiness probe que falha se o watch principal morreu;
- health check simples para saber se o processo continua vivo.

Veja também o guia de [observabilidade em Zig](/artigos/zig-observabilidade/) e o guia de [HTTP server em produção](/artigos/zig-http-server-producao/) para padrões de logs, health checks e métricas Prometheus.

## Segurança e finalizers

Se o CRD cria recursos externos — buckets, filas, bancos, DNS — implemente finalizers. O finalizer impede que o objeto desapareça antes de o operator limpar o recurso externo.

Checklist de segurança:

- validar campos do CRD no schema OpenAPI;
- limitar tamanho de strings e listas;
- não interpolar `spec` em comandos shell;
- usar RBAC mínimo;
- separar reconcilers por escopo quando permissões forem muito diferentes;
- não gravar tokens, headers ou segredos em logs;
- usar [configuração segura e segredos](/artigos/zig-configuracao-segura-segredos-env/) para credenciais externas.

## Limites práticos em 2026

Zig ainda não tem um equivalente amplamente adotado ao controller-runtime. Isso significa que você provavelmente vai escrever ou manter:

- tipos para objetos Kubernetes que usa;
- serialização e validação de JSON;
- watch com `resourceVersion`;
- retry/backoff;
- queue interna;
- patch server-side apply;
- leader election, se rodar mais de uma réplica;
- testes de integração contra `kind` ou `k3d`.

Para um operator simples, esse custo é aceitável. Para uma plataforma inteira, talvez não. Uma estratégia comum é escrever o plano de controle principal em Go e usar Zig para componentes auxiliares de alta performance, agentes de nó, sidecars ou ferramentas CLI.

## Checklist antes de ir para produção

- [ ] CRD tem schema OpenAPI e limites de campo.
- [ ] Reconciler é idempotente e trata `NotFound` corretamente.
- [ ] Objetos filhos têm `ownerReferences`.
- [ ] Status é atualizado separadamente de spec.
- [ ] Watch reconecta com backoff e `resourceVersion`.
- [ ] RBAC usa menor privilégio possível.
- [ ] Logs não expõem tokens ou segredos.
- [ ] Imagem roda como usuário não-root.
- [ ] Health/readiness probes existem.
- [ ] Testes cobrem create, update, delete e erro transitório.

## FAQ rápido

### Dá para escrever um Kubernetes Operator em Zig?

Sim. Um operator só precisa falar com a API do Kubernetes, assistir eventos e reconciliar estado. Zig consegue fazer isso via HTTP, JSON e binário estático. O desafio é a falta de um framework maduro equivalente ao controller-runtime.

### Zig substitui Go para operators?

Na maioria dos times, não. Go continua melhor para operators complexos por causa do ecossistema Kubernetes. Zig faz sentido para operators pequenos, agentes leves, sidecars e casos em que footprint e distribuição estática importam muito.

### Preciso de cluster-admin?

Quase nunca. Comece com `Role` por namespace e permissões específicas para o CRD e os recursos filhos. Use `ClusterRole` apenas se o operator realmente precisa observar ou criar recursos em múltiplos namespaces.

### Como testar um operator em Zig?

Use testes unitários para lógica de diff e reconciliação, testes de contrato para JSON/patches e testes de integração com `kind` ou `k3d`. O teste mais importante é garantir que rodar o reconciler repetidas vezes não cria mudanças infinitas.

## Conclusão

Kubernetes Operators em Zig são viáveis quando o domínio é pequeno, o footprint importa e você aceita construir parte da infraestrutura que Go entrega pronta. A arquitetura recomendada é simples: client HTTP seguro, watch resiliente, fila com dedupe, reconcile idempotente, RBAC mínimo e observabilidade desde o primeiro deploy.

Para a maioria dos operators complexos, Go segue como padrão. Para operators leves, agentes e automações especializadas, Zig pode entregar binários muito pequenos, consumo previsível e uma superfície operacional enxuta.

## Conteúdo Relacionado

- [Zig Cloud Native](/artigos/zig-cloud-native/) — Zig na nuvem
- [Zig e Docker](/artigos/zig-docker-containers/) — Containers pequenos e reproduzíveis
- [Microserviços com Zig](/artigos/zig-microservicos/) — Quando vale a pena usar Zig em serviços
- [HTTP Server em Produção](/artigos/zig-http-server-producao/) — Proxy, logs, limites e health checks
- [Observabilidade em Zig](/artigos/zig-observabilidade/) — Monitoramento de serviços e operators
- [Configuração Segura em Zig](/artigos/zig-configuracao-segura-segredos-env/) — Env vars, segredos e runtime
