Construindo uma plataforma GitOps do zero: Kubernetes, ArgoCD, Terraform e Observabilidade
DEV Community

Construindo uma plataforma GitOps do zero: Kubernetes, ArgoCD, Terraform e Observabilidade

O que vamos construir

Antes do código, o mapa mental. A plataforma tem quatro camadas que se encaixam. O Terraform provisiona o cluster e instala o motor de GitOps. É a fundação como código. O kind roda um cluster Kubernetes real dentro de containers Docker, localmente. O ArgoCD observa um repositório Git e garante que o cluster reflita exatamente o que está versionado. Por fim, Prometheus e Grafana coletam e visualizam métricas, incluindo métricas customizadas de uma API Spring Boot. O fio que costura tudo é uma ideia só: o Git é a fonte da verdade. Você não muda o cluster na mão; você muda arquivos no Git, e o cluster se ajusta.

Por que GitOps?

Vale parar um instante nessa ideia, porque ela é o coração de tudo. No modelo tradicional, você aplica mudanças no cluster diretamente - um kubectl apply aqui, um kubectl scale ali. O problema é que o cluster vira uma caixa-preta. Ninguém sabe ao certo por que ele está do jeito que está, quem mudou o quê ou como reproduzir aquele estado em outro ambiente.

GitOps inverte isso. O estado desejado do cluster vive num repositório Git. Uma ferramenta (aqui, o ArgoCD) fica continuamente comparando o que deveria estar rodando (o Git) com o que está rodando (o cluster) e corrige qualquer diferença. Na prática, isso significa que toda mudança passa a ter auditoria automática, porque ela é registrada como um commit com autor, data e motivo. Também significa que qualquer ambiente pode ser reproduzido a partir do mesmo repositório, que voltar atrás é tão simples quanto executar um git revert e que ninguém precisa de acesso direto ao cluster para fazer deploy: basta commitar. Guarde essa ideia, porque vamos vê-la acontecer na prática.

Camada 1: o cluster como código com Terraform

O primeiro instinto de quem começa é criar o cluster na mão. O comando existe e é simples. Mas isso já quebra a promessa da reprodutibilidade - amanhã você não lembra exatamente como criou. Por isso, desde o início, o cluster nasce de Terraform. O trecho central declara três providers e o cluster:

terraform {
  required_providers {
    kind = {
      source = "tehcyx/kind"
      version = "~> 0.9"
    }
    helm = {
      source = "hashicorp/helm"
      version = "~> 2.17"
    }
    kubernetes = {
      source = "hashicorp/kubernetes"
      version = "~> 2.35"
    }
  }
}

resource "kind_cluster" "this" {
  name = "gitops-lab"
  wait_for_ready = true

  kind_config {
    kind = "Cluster"
    api_version = "kind.x-k8s.io/v1alpha4"

    node {
      role = "control-plane"
    }
    node {
      role = "worker"
    }
    node {
      role = "worker"
    }
  }
}

Repare em duas decisões. Primeiro, o cluster tem três nós (um control-plane e dois workers) em vez de um só. Isso não é firula: com múltiplos nós, você vê o Kubernetes distribuir cargas entre eles, o que torna conceitos como alta disponibilidade concretos em vez de teóricos. Segundo, as versões dos providers estão fixadas. Isso é o que garante que um terraform apply daqui a seis meses produza o mesmo resultado de hoje. Reprodutibilidade não é acidente; é uma escolha.

A partir daí, o mesmo Terraform instala o ArgoCD via Helm, já apontando os providers para o cluster recém-criado. Um único terraform apply entrega o cluster e o motor de GitOps prontos.

Camada 2: ensinando o ArgoCD a observar o Git

Com o ArgoCD instalado, ele está de pé, mas ocioso - não sabe o que observar. É preciso apresentá-lo a um repositório. Isso se faz com um objeto chamado Application. A Application é a ponte. Ela diz três coisas: de onde puxar, para onde aplicar e como se comportar.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: nginx
  namespace: argocd
spec:
  source:
    repoURL: https://github.com/usuario/gitops-manifests.git
    targetRevision: main
    path: apps/nginx
  destination:
    server: https://kubernetes.default.svc
    namespace: nginx-demo
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

As duas linhas mais importantes deste arquivo são prune: true e selfHeal: true. Elas são o que torna o GitOps realmente vivo. Com selfHeal, se alguém alterar o cluster manualmente, o ArgoCD detecta a divergência em relação ao Git e desfaz a mudança. O Git vence. Já prune garante que, se um recurso for removido do repositório, ele também será removido do cluster. O cluster espelha exatamente o conteúdo do Git, nada mais.

O momento GitOps: escalar com um commit

Aqui a teoria vira prática, e é o momento que fixa o conceito. Com o nginx rodando com uma réplica, fiz uma mudança que normalmente exigiria um comando no cluster: aumentar para três réplicas. Só que, em GitOps, isso não é um comando. É uma edição de arquivo:

spec:
  replicas: 3 # antes era 1

Um git commit e um git push depois, o ArgoCD detectou a diferença e criou dois novos pods - sozinho. Eu nunca rodei kubectl scale. O cluster simplesmente convergiu para o que o Git passou a dizer.

O teste que mais ensina veio a seguir. Executei um kubectl scale manual, forçando o cluster de volta a uma réplica. Por um instante, os pods começaram a ser removidos. Então o selfHeal entrou em ação: o ArgoCD percebeu que o cluster havia divergido do Git, que ainda dizia três réplicas, e recriou automaticamente os pods. Minha alteração manual foi desfeita em poucos segundos. Essa é a garantia que dá segurança a ambientes de produção reais: não existe "conserta rápido no cluster e esquece". Toda mudança passa pelo Git ou acaba sendo revertida.

Camada 3: observabilidade e o diferencial da métrica de negócio

Uma plataforma que você não consegue enxergar é uma plataforma que você não controla. Por isso a última camada é observabilidade. Instalei o kube-prometheus-stack, um pacote que traz Prometheus, Grafana e Alertmanager já integrados, mantendo o padrão GitOps: ele entra no cluster como mais uma Application do ArgoCD.

Um detalhe técnico importante aqui é que charts Helm muito grandes, como esse, exigem a opção ServerSideApply=true no ArgoCD, porque seus CRDs ultrapassam o limite de tamanho da aplicação tradicional. É o tipo de detalhe que normalmente só aparece durante a prática.

Mas coletar métricas genéricas de CPU e memória é apenas o básico. O que realmente diferencia uma plataforma é medir lógica de negócio. E foi aqui que meu background em Java entrou como vantagem. Criei uma API Spring Boot simples que expõe uma métrica customizada: um contador que incrementa a cada chamada de um endpoint.

@RestController
public class HelloController {
    private final Counter helloCounter;

    public HelloController(MeterRegistry registry) {
        this.helloCounter = Counter.builder("demo_hello_requests_total")
            .description("Total de chamadas ao endpoint /hello")
            .register(registry);
    }

    @GetMapping("/hello")
    public String hello() {
        helloCounter.increment();
        return "Olá do GitOps Lab!";
    }
}

Com Spring Boot Actuator e Micrometer, expor essa métrica no formato que o Prometheus entende exige muito pouca configuração. A ponte final entre a aplicação e o Prometheus é um recurso chamado ServiceMonitor, responsável por informar ao Prometheus quais pods devem ser monitorados.

Existe, porém, um detalhe que costuma bloquear muita gente: o ServiceMonitor precisa possuir um label específico (release: monitoring) para ser descoberto pelo Prometheus. Sem esse label, a coleta simplesmente não acontece e, pior, não existe uma mensagem de erro evidente indicando o motivo.

Com tudo conectado, o fluxo finalmente se fecha. Cada chamada ao endpoint incrementa o contador, o Prometheus coleta esse valor periodicamente e o Grafana o exibe em tempo real. Pela primeira vez, vi uma métrica escrita por mim, em Java, aparecer em um dashboard. Houve ainda um detalhe interessante: o gráfico mostrava duas séries diferentes, uma para cada pod, com valores distintos. Não era um bug. Era o balanceamento de carga do Kubernetes se tornando visível através das métricas, mostrando que o tráfego estava sendo distribuído de forma desigual entre as instâncias da aplicação.

Os detalhes que só a prática ensina

Se eu tivesse que resumir o valor de construir tudo isso manualmente, em vez de apenas ler sobre o assunto, seria nos detalhes que dificilmente aparecem em diagramas.

  • Descobri que imagens carregadas localmente no kind exigem imagePullPolicy: IfNotPresent; caso contrário, o Kubernetes tentará buscá-las em um registry remoto e falhará.
  • Também percebi que separar o repositório de infraestrutura do repositório de manifestos evita acoplar mudanças na plataforma aos deploys das aplicações, permitindo que cada um siga seu próprio ciclo de vida.
  • E, talvez o hábito mais importante de todos, aprendi que ler cuidadosamente o resultado de um terraform plan antes do terraform apply é a diferença entre operar com confiança e simplesmente torcer para que tudo funcione.

Conclusão

No fim, a sensação é quase decepcionante de tão tranquila: você muda um número no Git, faz um git push e o cluster inteiro se ajusta sozinho. Mas essa simplicidade aparente é justamente o objetivo. Uma boa plataforma esconde a complexidade atrás de um simples git push. Toda a engenharia - o cluster multi-nó, os componentes do ArgoCD conversando entre si, a reconciliação contínua e toda a cadeia de observabilidade - existe para que operar seja simples.

Construir a transmissão automática é difícil; dirigir um carro automático é fácil. Eu quis construir a transmissão. Se você também está atravessando a jornada de backend para plataforma, meu conselho é simples: não leia apenas. Construa. Conceitos como reconciliação, estado desejado e fonte da verdade deixam de ser abstratos no momento em que você vê o ArgoCD desfazer uma alteração manual e restaurar exatamente o que o Git determina.

O código completo, com instruções para executar tudo do zero, está disponível no repositório. E este é apenas o começo: os próximos capítulos incluem alertas, um pipeline de validação de manifestos e a migração para uma cloud gerenciada. Até a próxima.

Comments

No comments yet. Start the discussion.