<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Matheus Kocotem</title>
    <description>The latest articles on DEV Community by Matheus Kocotem (@m_kocotem_1b69865766c653).</description>
    <link>https://dev.to/m_kocotem_1b69865766c653</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4050237%2F535ee3e3-dd2a-4fc2-a42f-c1a6063c2939.png</url>
      <title>DEV Community: Matheus Kocotem</title>
      <link>https://dev.to/m_kocotem_1b69865766c653</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/m_kocotem_1b69865766c653"/>
    <language>en</language>
    <item>
      <title>Arquitetura Hexagonal em Python do zero: um domínio que não sabe onde mora nem quem o chama</title>
      <dc:creator>Matheus Kocotem</dc:creator>
      <pubDate>Wed, 16 Sep 2026 16:46:22 +0000</pubDate>
      <link>https://dev.to/m_kocotem_1b69865766c653/arquitetura-hexagonal-em-python-do-zero-um-dominio-que-nao-sabe-onde-mora-nem-quem-o-chama-2bfn</link>
      <guid>https://dev.to/m_kocotem_1b69865766c653/arquitetura-hexagonal-em-python-do-zero-um-dominio-que-nao-sabe-onde-mora-nem-quem-o-chama-2bfn</guid>
      <description>&lt;p&gt;Durante muito tempo, a minha relação com "arquitetura" foi do tipo &lt;em&gt;já ouvi falar&lt;/em&gt;. Sabia recitar que o domínio deve ser isolado, que regra de negócio não vai no controller, que acoplamento é ruim. Mas existe uma distância enorme entre repetir princípios e entender o que eles compram na prática e a única forma que conheço de atravessar essa distância é construir.&lt;/p&gt;

&lt;p&gt;Então construí, do zero, uma aplicação Python inteira em arquitetura hexagonal para responder, com um exemplo real na mão, a uma pergunta simples: &lt;em&gt;o que exatamente eu ganho isolando o domínio?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Este artigo é o registro dessa construção. Não é um tutorial de "cole esse comando"; é a explicação de por que cada peça existe. E ele gira em torno de duas provas concretas não de duas afirmações. Na primeira, um pedido gravado no PostgreSQL vai &lt;strong&gt;evaporar&lt;/strong&gt; quando eu troco uma variável de ambiente. Na segunda, vou adicionar uma interface de linha de comando inteira ao sistema e mostrar, pelo histórico do Git, que o coração da aplicação &lt;strong&gt;não mudou uma única linha&lt;/strong&gt;. Essas duas provas, juntas, são a arquitetura hexagonal inteira.&lt;/p&gt;

&lt;h2&gt;
  
  
  O que é arquitetura hexagonal, sem enrolação
&lt;/h2&gt;

&lt;p&gt;A ideia, criada por Alistair Cockburn por volta de 2005, também atende pelo nome &lt;em&gt;Ports &amp;amp; Adapters&lt;/em&gt; e esse nome descreve melhor o que ela é.&lt;/p&gt;

&lt;p&gt;O domínio (as suas regras de negócio) fica no centro, isolado de qualquer framework. Ele não sabe se está sendo chamado por uma API REST ou por um terminal. Não sabe se seus dados vão parar em PostgreSQL ou num dicionário em memória. Para viver nessa bolha de ignorância feliz, o domínio declara &lt;strong&gt;portas&lt;/strong&gt; interfaces que dizem &lt;em&gt;"eu preciso de algo capaz de salvar um pedido"&lt;/em&gt; ou &lt;em&gt;"me avise quando um pedido for pago"&lt;/em&gt;, sem dizer &lt;em&gt;onde&lt;/em&gt; nem &lt;em&gt;como&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Quem responde às portas são os &lt;strong&gt;adaptadores&lt;/strong&gt;. Um adaptador de entrada traduz uma requisição HTTP ou um comando de terminal numa chamada de caso de uso. Um adaptador de saída recebe o pedido &lt;em&gt;"persista isso"&lt;/em&gt; e decide se grava num banco ou numa lista.&lt;/p&gt;

&lt;p&gt;A regra que sustenta tudo tem uma direção: &lt;strong&gt;de fora para dentro&lt;/strong&gt;. Os adaptadores conhecem o domínio; o domínio nunca conhece um adaptador. Quando essa seta aponta para o lado certo, tanto a borda de entrada quanto a de saída viram peças plugáveis.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        ADAPTADORES DE ENTRADA  (dois, mesma aplicação)
   ┌──────────────────────┐        ┌──────────────────────┐
   │  FastAPI (HTTP)      │        │   CLI (terminal)     │
   │  POST /orders ...    │        │   orders pay &amp;lt;id&amp;gt; ...│
   └──────────┬───────────┘        └───────────┬──────────┘
              └──────────────┬─────────────────┘
                             ▼ ambos chamam o mesmo
              ┌────────────────────────────────────┐
              │              PORTAS                │
              │        (Protocol do domínio)       │
              │   ┌────────────────────────────┐   │
              │   │         DOMÍNIO            │   │
              │   │  (zero import de framework)│   │
              │   │  Order, regras, desconto,  │   │
              │   │  estoque, expiração,       │   │
              │   │  eventos (OrderPaid)       │   │
              │   │  OrderService(casos de uso)│   │
              │   └────────────────────────────┘   │
              │  OrderRepository · NotificationPort│
              │  StockPort · Clock                 │
              └───────────────┬────────────────────┘
                              │ implementado por
        ┌─────────────┬───────┴───────┬──────────────┐
        ▼             ▼               ▼              ▼
   InMemoryRepo  SqlAlchemyRepo  Logging/Memory   MemoryStock
                                 Notification
              ADAPTADORES DE SAÍDA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Guarde esse desenho. O resto do artigo é ele ganhando vida e depois sendo estressado.&lt;/p&gt;

&lt;h2&gt;
  
  
  O domínio: o único lugar que importa
&lt;/h2&gt;

&lt;p&gt;Escolhi um caso pequeno mas com regra de verdade: gestão de pedidos. E a primeira regra que impus a mim mesmo foi radical: &lt;strong&gt;nada em &lt;code&gt;domain/&lt;/code&gt; pode importar um framework.&lt;/strong&gt; Nenhum &lt;code&gt;import fastapi&lt;/code&gt;, nenhum &lt;code&gt;import sqlalchemy&lt;/code&gt;, nenhum &lt;code&gt;import pydantic&lt;/code&gt;. Se você abrir um arquivo do domínio e vir o nome de uma tecnologia, algo já deu errado.&lt;/p&gt;

&lt;p&gt;A entidade &lt;code&gt;Order&lt;/code&gt; é uma &lt;code&gt;dataclass&lt;/code&gt; de Python puro. Repare no total:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@property&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;total&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subtotal&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;subtotal&lt;/span&gt; &lt;span class="nf"&gt;discount_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subtotal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O total não é um campo que alguém preenche é uma propriedade &lt;em&gt;derivada&lt;/em&gt;, recalculada sempre a partir dos itens e da política de desconto. É &lt;strong&gt;impossível&lt;/strong&gt; um pedido existir com um total que não bate com seu conteúdo. O cliente da API pode mandar o total que quiser no corpo da requisição: vai ser solenemente ignorado, porque quem manda no total é o domínio, não a rede.&lt;/p&gt;

&lt;p&gt;As regras ficam junto da entidade, protegendo o estado:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;OrderError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Só é possível pagar um pedido PENDING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PAID&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderPaid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Um pedido só é pago se estiver pendente; um pago não se cancela; um pendente velho demais vira &lt;code&gt;EXPIRED&lt;/code&gt;. E, ao ser pago, o pedido &lt;strong&gt;registra um evento&lt;/strong&gt; numa lista interna guarde isso, porque é assim que a notificação vai acontecer sem o domínio jamais conhecer quem notifica.&lt;/p&gt;

&lt;p&gt;Um detalhe de disciplina que vale destacar: a expiração é um caso de uso &lt;code&gt;expire_stale_orders(now, max_age)&lt;/code&gt; que &lt;strong&gt;recebe o "agora" como parâmetro&lt;/strong&gt;. Nada de &lt;code&gt;datetime.now()&lt;/code&gt; escondido dentro do domínio. Isso mantém o núcleo determinístico e testável você testa "expirou depois de 24h" passando um relógio de mentira, sem esperar 24 horas nem depender do relógio da máquina. O tempo, aqui, também entra por uma porta (&lt;code&gt;Clock&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  As portas: contratos que o domínio dita
&lt;/h2&gt;

&lt;p&gt;Aqui está o detalhe que torna isso especialmente elegante em Python. As portas são &lt;code&gt;typing.Protocol&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_by_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Em Java, uma porta é uma interface e o adaptador declara &lt;code&gt;implements&lt;/code&gt;. Em Python, com &lt;code&gt;Protocol&lt;/code&gt;, o encaixe é &lt;strong&gt;estrutural&lt;/strong&gt;: qualquer classe com esses métodos e assinaturas &lt;em&gt;já é&lt;/em&gt; um &lt;code&gt;OrderRepository&lt;/code&gt;, sem herdar de nada. O domínio define a forma da tomada; o adaptador só precisa ter o formato do plugue.&lt;/p&gt;

&lt;p&gt;Parece frouxo "e se eu errar uma assinatura?". Não é: rodo &lt;code&gt;mypy --strict&lt;/code&gt;, e é o verificador de tipos que confirma, &lt;strong&gt;antes de o código rodar&lt;/strong&gt;, que cada adaptador satisfaz sua porta. No projeto final, o mypy strict passa limpo em 25 arquivos. Uma assinatura incompatível num adaptador seria pega ali, estaticamente sem herança e sem precisar subir um banco para descobrir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Os casos de uso: orquestração sem tecnologia
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;OrderService&lt;/code&gt; é onde os casos de uso vivem, e ele depende &lt;strong&gt;só das portas&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OrderRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NotificationPort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;stock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;StockPort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Clock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pay_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pay&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pull_events&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;      &lt;span class="c1"&gt;# drena OrderPaid
&lt;/span&gt;            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify_order_paid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Olhe o &lt;code&gt;pay_order&lt;/code&gt;: ele paga, persiste, drena os eventos que a entidade registrou e despacha a notificação tudo através de portas. O serviço &lt;strong&gt;nunca importa um adaptador concreto&lt;/strong&gt;. Ele não sabe se a notificação vira um log, um e-mail ou uma mensagem numa fila. Sabe só que existe uma &lt;code&gt;NotificationPort&lt;/code&gt; e que ela precisa ser avisada.&lt;/p&gt;

&lt;p&gt;Isso foi verificado de verdade: rodando em Docker, o pagamento de um pedido produziu um log real do adaptador concreto&lt;code&gt;notification.order_paid&lt;/code&gt;  disparado sem que &lt;code&gt;OrderService&lt;/code&gt; conhecesse esse adaptador. E, no teste de caso de uso, injeto uma &lt;code&gt;NotificationPort&lt;/code&gt; de mentira que guarda as notificações numa lista, e afirmo o comportamento com precisão: &lt;strong&gt;pagar notifica exatamente uma vez; cancelar notifica zero&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prova nº 1: o pedido que evaporou
&lt;/h2&gt;

&lt;p&gt;Para o domínio, "onde os dados moram" é indiferente. Escrevi dois adaptadores de persistência para a mesma porta &lt;code&gt;OrderRepository&lt;/code&gt;: um dicionário em memória (dezenas de linhas, zero dependências) e uma implementação real com SQLAlchemy sobre PostgreSQL, com o mapeamento de tabelas vivendo em &lt;code&gt;orm.py&lt;/code&gt;, &lt;strong&gt;fora&lt;/strong&gt; do domínio.&lt;/p&gt;

&lt;p&gt;Quem escolhe qual usar? Um único arquivo a &lt;em&gt;composition root&lt;/em&gt;, &lt;code&gt;composition.py&lt;/code&gt; lendo a config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_repository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OrderRepository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;SqlAlchemyOrderRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;database_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;InMemoryOrderRepository&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Subi tudo com Docker (API + Postgres real, com Alembic aplicando o schema no start). Criei um pedido via &lt;code&gt;curl&lt;/code&gt;; foi para o Postgres. Para confirmar persistência de verdade, &lt;strong&gt;reiniciei o container da aplicação&lt;/strong&gt; o pedido continuava lá. Paguei; tentei cancelar em seguida e recebi &lt;code&gt;409 Conflict&lt;/code&gt;, exatamente como a regra manda.&lt;/p&gt;

&lt;p&gt;Então subi a mesma aplicação trocando &lt;strong&gt;uma variável&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;REPOSITORY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;memory docker compose up app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Busquei o pedido pelo id. &lt;strong&gt;404 Not Found.&lt;/strong&gt; Evaporou.&lt;/p&gt;

&lt;p&gt;E isso é uma &lt;em&gt;boa&lt;/em&gt; notícia. Não é bug: com &lt;code&gt;REPOSITORY=memory&lt;/code&gt;, a aplicação fala com um armazenamento genuinamente diferente, um dicionário vazio na memória do processo. O pedido segue vivo no Postgres; a aplicação é que não olha mais para lá. Troquei a tecnologia de persistência inteira de banco relacional para memória volátil sem tocar &lt;strong&gt;uma linha&lt;/strong&gt; de &lt;code&gt;domain/&lt;/code&gt; ou &lt;code&gt;services.py&lt;/code&gt;. O núcleo nunca soube que foi trocado. É esse não-saber que é o objetivo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prova nº 2: a interface que não mexeu no coração
&lt;/h2&gt;

&lt;p&gt;A troca do banco prova metade do teorema que a borda de &lt;em&gt;saída&lt;/em&gt; é plugável. Faltava a outra metade: a borda de &lt;em&gt;entrada&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Então adicionei um segundo adaptador de entrada. Além da API FastAPI, o mesmo sistema agora tem uma &lt;strong&gt;CLI&lt;/strong&gt; dá para criar, pagar, cancelar e listar pedidos pelo terminal. E o ponto não é "que legal, tem CLI". O ponto é &lt;em&gt;como&lt;/em&gt; ela foi adicionada.&lt;/p&gt;

&lt;p&gt;A CLI reaproveita &lt;code&gt;build_order_service()&lt;/code&gt; a mesmíssima composição que a API usa. Ela é só mais um adaptador traduzindo entrada (argumentos de terminal, em vez de JSON HTTP) em chamadas do mesmo &lt;code&gt;OrderService&lt;/code&gt;. Nenhuma regra foi reescrita. Nenhuma regra foi copiada.&lt;/p&gt;

&lt;p&gt;E aqui está a prova que eu mais gosto neste projeto, porque não depende da minha palavra. Isolei a adição da CLI num único commit. O &lt;code&gt;git diff --stat&lt;/code&gt; desse commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; app/adapters/inbound/cli.py | 118 +++++++++++++++++++++++
 tests/test_cli.py           |  64 +++++++++++++
 2 files changed, 182 insertions(+)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dois arquivos. O adaptador novo e seu teste. &lt;strong&gt;Zero linhas alteradas em &lt;code&gt;app/domain/&lt;/code&gt;. Zero em &lt;code&gt;services.py&lt;/code&gt;.&lt;/strong&gt; O sistema ganhou uma forma inteiramente nova de ser operado, e o coração dele não sentiu. Isso é a arquitetura hexagonal deixando de ser um slogan e virando uma linha de &lt;code&gt;git diff&lt;/code&gt; que qualquer pessoa pode conferir.&lt;/p&gt;

&lt;p&gt;Duas provas, uma simetria: a saída troca sem tocar no núcleo (o pedido evaporou), e a entrada se multiplica sem tocar no núcleo (o commit da CLI). O domínio não sabe onde mora, nem quem o chama.&lt;/p&gt;

&lt;h2&gt;
  
  
  O bônus que só a separação permite: testes que voam
&lt;/h2&gt;

&lt;p&gt;Manter o domínio puro tem um efeito colateral delicioso nos testes. O projeto tem 53 testes, divididos assim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;domain      -&amp;gt; 21 testes, Python puro
use_cases   -&amp;gt; 13 testes, OrderService + adaptadores em memória
api         -&amp;gt; 10 testes, FastAPI TestClient
cli         -&amp;gt;  5 testes, o segundo adaptador de entrada
sqlalchemy  -&amp;gt;  4 testes de integração (só estes pedem Postgres)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Os 34 testes de domínio e casos de uso rodam sem subir servidor nem banco, porque dependem só do domínio puro e de adaptadores em memória que são adaptadores legítimos das mesmas portas. Você não precisa de um Postgres de pé para testar "pedido pago não se cancela", porque essa regra não tem nada a ver com Postgres. No modo padrão de desenvolvimento, os 4 testes de integração são pulados automaticamente quando não há banco configurado; o CI, esse sim, sobe um Postgres real e roda os 53. A mesma arquitetura que isola o domínio para trocar o banco é a que deixa você testar o domínio sem banco nenhum.&lt;/p&gt;

&lt;h2&gt;
  
  
  Os detalhes que só a prática ensina
&lt;/h2&gt;

&lt;p&gt;Além da teoria, construir isso me ensinou coisas que não aparecem nos diagramas:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pydantic não é o seu domínio.&lt;/strong&gt; O erro mais comum ao "fingir" hexagonal com FastAPI é usar o modelo Pydantic como entidade de negócio. Mantive os schemas Pydantic (validação/serialização HTTP) separados da &lt;code&gt;dataclass&lt;/code&gt; &lt;code&gt;Order&lt;/code&gt; (regra de negócio). Fundir os dois é abrir um furo por onde a web vaza para dentro do núcleo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traduza erros de domínio num só lugar.&lt;/strong&gt; As exceções do domínio não sabem o que é status HTTP nem deveriam. Um único ponto em &lt;code&gt;api.py&lt;/code&gt; traduz &lt;code&gt;OrderError&lt;/code&gt;/&lt;code&gt;OrderNotFoundError&lt;/code&gt; em &lt;code&gt;409&lt;/code&gt;/&lt;code&gt;404&lt;/code&gt;. Uma ponte de vocabulário, num lugar só.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alembic tem uma armadilha clássica.&lt;/strong&gt; Ao gerar a migration por autogenerate contra o Postgres, o &lt;code&gt;downgrade&lt;/code&gt; não removia o tipo enum criado — o famoso bug de enum órfão. Corrigi e testei o roundtrip completo (upgrade → downgrade → upgrade). É o tipo de detalhe que só aparece quando você roda de verdade, não quando lê sobre.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;O tempo é uma dependência.&lt;/strong&gt; Tratar &lt;code&gt;now&lt;/code&gt; como algo que entra por uma porta (&lt;code&gt;Clock&lt;/code&gt;), em vez de chamar &lt;code&gt;datetime.now()&lt;/code&gt; no meio da regra, foi o que tornou a expiração testável sem hacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusão
&lt;/h2&gt;

&lt;p&gt;No fim, a sensação é quase decepcionante de tão tranquila: você muda &lt;code&gt;REPOSITORY&lt;/code&gt;, sobe a aplicação, e o sistema fala com outro banco sem reclamar. Você adiciona uma CLI inteira, e o &lt;code&gt;git diff&lt;/code&gt; do domínio vem vazio.&lt;/p&gt;

&lt;p&gt;Mas essa tranquilidade é o produto. Toda a disciplina — o domínio sem imports de framework, as portas como &lt;code&gt;Protocol&lt;/code&gt;, os adaptadores plugáveis dos dois lados, o composition root isolado existe para que as decisões de tecnologia deixem de ser casamentos e virem o que deveriam ser: peças plugáveis na borda, trocáveis sem medo.&lt;/p&gt;

&lt;p&gt;O sistema completo tem bastante coisa de projeto sério que este artigo nem detalhou — migrations versionadas com Alembic, configuração via &lt;code&gt;pydantic-settings&lt;/code&gt;, logging estruturado em JSON, health check que testa o banco, paginação, métricas opcionais e um CI que roda lint, type-check estrito e os 53 testes contra um Postgres real. Mas nada disso é o ponto. O ponto são as duas linhas de prova: um pedido que some de propósito, e um commit que não toca no coração.&lt;/p&gt;

&lt;p&gt;Construir o encaixe é o trabalho difícil. Trocar a peça encaixada de qualquer lado é o que fica fácil. E eu quis construir o encaixe.&lt;/p&gt;

&lt;p&gt;Se você também está tentando sair do &lt;em&gt;"já ouvi falar"&lt;/em&gt; para o &lt;em&gt;"entendi de verdade"&lt;/em&gt;, o conselho é o de sempre: não leia só, construa. Porta, adaptador e inversão de dependência param de ser abstrações no exato momento em que você vê um pedido evaporar do banco de propósito, sem medo e confere, no histórico do Git, que dar ao sistema uma boca nova não exigiu mexer no seu coração.&lt;/p&gt;

&lt;p&gt;O código completo, pronto para rodar com &lt;code&gt;docker compose up&lt;/code&gt;, está no repositório.&lt;/p&gt;

</description>
      <category>python</category>
      <category>architecture</category>
      <category>fastapi</category>
      <category>cleanarchitecture</category>
    </item>
    <item>
      <title>Construindo uma plataforma GitOps do zero: Kubernetes, ArgoCD, Terraform e Observabilidade</title>
      <dc:creator>Matheus Kocotem</dc:creator>
      <pubDate>Mon, 27 Jul 2026 22:53:33 +0000</pubDate>
      <link>https://dev.to/m_kocotem_1b69865766c653/construindo-uma-plataforma-gitops-do-zero-kubernetes-argocd-terraform-e-observabilidade-34jh</link>
      <guid>https://dev.to/m_kocotem_1b69865766c653/construindo-uma-plataforma-gitops-do-zero-kubernetes-argocd-terraform-e-observabilidade-34jh</guid>
      <description>&lt;p&gt;Durante muito tempo, meu contato com Kubernetes foi do tipo "já mexi": subi um pod aqui, apliquei um manifesto ali, vi um deploy acontecer. Mas existe uma distância enorme entre &lt;em&gt;usar&lt;/em&gt; uma ferramenta e &lt;em&gt;entender&lt;/em&gt; o que ela faz por baixo. Eu venho de backend Java e automação de testes, e decidi atravessar essa distância de propósito construindo, do zero, uma plataforma GitOps completa e local.&lt;/p&gt;

&lt;p&gt;Este artigo é o registro dessa construção. Não é um tutorial de "cole esse comando"; é uma explicação de &lt;em&gt;por que&lt;/em&gt; cada peça existe e como elas se encaixam. Ao final, você terá visto uma infraestrutura nascer de um único comando, deploys acontecerem a partir de um &lt;code&gt;git push&lt;/code&gt;, e métricas de uma aplicação Java fluírem até um dashboard tudo versionado, tudo reproduzível.&lt;/p&gt;

&lt;h2&gt;
  
  
  O que vamos construir
&lt;/h2&gt;

&lt;p&gt;Antes do código, o mapa mental. A plataforma tem quatro camadas que se encaixam.&lt;/p&gt;

&lt;p&gt;O &lt;strong&gt;Terraform&lt;/strong&gt; provisiona o cluster e instala o motor de GitOps. É a fundação como código.&lt;/p&gt;

&lt;p&gt;O &lt;strong&gt;kind&lt;/strong&gt; roda um cluster Kubernetes real dentro de containers Docker, localmente.&lt;/p&gt;

&lt;p&gt;O &lt;strong&gt;ArgoCD&lt;/strong&gt; observa um repositório Git e garante que o cluster reflita exatamente o que está versionado.&lt;/p&gt;

&lt;p&gt;Por fim, &lt;strong&gt;Prometheus e Grafana&lt;/strong&gt; coletam e visualizam métricas, incluindo métricas customizadas de uma API Spring Boot.&lt;/p&gt;

&lt;p&gt;O fio que costura tudo é uma ideia só: &lt;strong&gt;o Git é a fonte da verdade.&lt;/strong&gt; Você não muda o cluster na mão; você muda arquivos no Git, e o cluster se ajusta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por que GitOps?
&lt;/h2&gt;

&lt;p&gt;Vale parar um instante nessa ideia, porque ela é o coração de tudo.&lt;/p&gt;

&lt;p&gt;No modelo tradicional, você aplica mudanças no cluster diretamente — um &lt;code&gt;kubectl apply&lt;/code&gt; aqui, um &lt;code&gt;kubectl scale&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;GitOps inverte isso. O estado desejado do cluster vive num repositório Git. Uma ferramenta (aqui, o ArgoCD) fica continuamente comparando o que &lt;em&gt;deveria&lt;/em&gt; estar rodando (o Git) com o que &lt;em&gt;está&lt;/em&gt; rodando (o cluster) e corrige qualquer diferença.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;git revert&lt;/code&gt; e que ninguém precisa de acesso direto ao cluster para fazer deploy: basta commitar.&lt;/p&gt;

&lt;p&gt;Guarde essa ideia, porque vamos vê-la acontecer na prática.&lt;/p&gt;

&lt;h2&gt;
  
  
  Camada 1: o cluster como código com Terraform
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Por isso, desde o início, o cluster nasce de Terraform. O trecho central declara três providers e o cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;kind&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tehcyx/kind"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 0.9"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;helm&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/helm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 2.17"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;kubernetes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/kubernetes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 2.35"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"kind_cluster"&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"gitops-lab"&lt;/span&gt;
  &lt;span class="nx"&gt;wait_for_ready&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="nx"&gt;kind_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;kind&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Cluster"&lt;/span&gt;
    &lt;span class="nx"&gt;api_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"kind.x-k8s.io/v1alpha4"&lt;/span&gt;

    &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"control-plane"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"worker"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"worker"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repare em duas decisões. Primeiro, o cluster tem &lt;strong&gt;três nós&lt;/strong&gt; (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.&lt;/p&gt;

&lt;p&gt;Segundo, as &lt;strong&gt;versões dos providers estão fixadas&lt;/strong&gt;. Isso é o que garante que um &lt;code&gt;terraform apply&lt;/code&gt; daqui a seis meses produza o mesmo resultado de hoje. Reprodutibilidade não é acidente; é uma escolha.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Camada 2: ensinando o ArgoCD a observar o Git
&lt;/h2&gt;

&lt;p&gt;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 &lt;strong&gt;Application&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A Application é a ponte. Ela diz três coisas: de onde puxar, para onde aplicar e como se comportar.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Application&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;repoURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/usuario/gitops-manifests.git&lt;/span&gt;
    &lt;span class="na"&gt;targetRevision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/nginx&lt;/span&gt;
  &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://kubernetes.default.svc&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx-demo&lt;/span&gt;
  &lt;span class="na"&gt;syncPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;automated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;prune&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;selfHeal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;syncOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CreateNamespace=true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As duas linhas mais importantes deste arquivo são &lt;code&gt;prune: true&lt;/code&gt; e &lt;code&gt;selfHeal: true&lt;/code&gt;. Elas são o que torna o GitOps realmente vivo.&lt;/p&gt;

&lt;p&gt;Com &lt;code&gt;selfHeal&lt;/code&gt;, 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.&lt;/p&gt;

&lt;p&gt;Já &lt;code&gt;prune&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  O momento GitOps: escalar com um commit
&lt;/h2&gt;

&lt;p&gt;Aqui a teoria vira prática, e é o momento que fixa o conceito.&lt;/p&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;   &lt;span class="c1"&gt;# antes era 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Um &lt;code&gt;git commit&lt;/code&gt; e um &lt;code&gt;git push&lt;/code&gt; depois, o ArgoCD detectou a diferença e criou dois novos pods — sozinho. Eu nunca rodei &lt;code&gt;kubectl scale&lt;/code&gt;. O cluster simplesmente convergiu para o que o Git passou a dizer.&lt;/p&gt;

&lt;p&gt;O teste que mais ensina veio a seguir. Executei um &lt;code&gt;kubectl scale&lt;/code&gt; manual, forçando o cluster de volta a uma réplica. Por um instante, os pods começaram a ser removidos. Então o &lt;code&gt;selfHeal&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Camada 3: observabilidade e o diferencial da métrica de negócio
&lt;/h2&gt;

&lt;p&gt;Uma plataforma que você não consegue enxergar é uma plataforma que você não controla. Por isso a última camada é observabilidade.&lt;/p&gt;

&lt;p&gt;Instalei o &lt;code&gt;kube-prometheus-stack&lt;/code&gt;, 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.&lt;/p&gt;

&lt;p&gt;Um detalhe técnico importante aqui é que charts Helm muito grandes, como esse, exigem a opção &lt;code&gt;ServerSideApply=true&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;Mas coletar métricas genéricas de CPU e memória é apenas o básico. O que realmente diferencia uma plataforma é medir &lt;strong&gt;lógica de negócio&lt;/strong&gt;. E foi aqui que meu background em Java entrou como vantagem.&lt;/p&gt;

&lt;p&gt;Criei uma API Spring Boot simples que expõe uma métrica customizada: um contador que incrementa a cada chamada de um endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HelloController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt; &lt;span class="n"&gt;helloCounter&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;HelloController&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MeterRegistry&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;helloCounter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo_hello_requests_total"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Total de chamadas ao endpoint /hello"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/hello"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;hello&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;helloCounter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;increment&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"Olá do GitOps Lab!"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Com Spring Boot Actuator e Micrometer, expor essa métrica no formato que o Prometheus entende exige muito pouca configuração.&lt;/p&gt;

&lt;p&gt;A ponte final entre a aplicação e o Prometheus é um recurso chamado &lt;code&gt;ServiceMonitor&lt;/code&gt;, responsável por informar ao Prometheus quais pods devem ser monitorados. Existe, porém, um detalhe que costuma bloquear muita gente: o &lt;code&gt;ServiceMonitor&lt;/code&gt; precisa possuir um label específico (&lt;code&gt;release: monitoring&lt;/code&gt;) 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Os detalhes que só a prática ensina
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Descobri que imagens carregadas localmente no kind exigem &lt;code&gt;imagePullPolicy: IfNotPresent&lt;/code&gt;; caso contrário, o Kubernetes tentará buscá-las em um registry remoto e falhará.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;E, talvez o hábito mais importante de todos, aprendi que ler cuidadosamente o resultado de um &lt;code&gt;terraform plan&lt;/code&gt; antes do &lt;code&gt;terraform apply&lt;/code&gt; é a diferença entre operar com confiança e simplesmente torcer para que tudo funcione.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusão
&lt;/h2&gt;

&lt;p&gt;No fim, a sensação é quase decepcionante de tão tranquila: você muda um número no Git, faz um &lt;code&gt;git push&lt;/code&gt; e o cluster inteiro se ajusta sozinho.&lt;/p&gt;

&lt;p&gt;Mas essa simplicidade aparente é justamente o objetivo. Uma boa plataforma esconde a complexidade atrás de um simples &lt;code&gt;git push&lt;/code&gt;. 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Até a próxima.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>gitops</category>
    </item>
  </channel>
</rss>
