<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="pt-BR"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://vndmtrx.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://vndmtrx.github.io/" rel="alternate" type="text/html" hreflang="pt-BR" /><updated>2026-10-11T02:55:45+00:00</updated><id>https://vndmtrx.github.io/feed.xml</id><title type="html">Vndmtrx</title><subtitle>Um blog com a proposta de falar sobre vários assuntos, desde desenvolvimento, passando por infra, devops, hacktivismo e até sobre conspirações alienígenas e portais circulares que permitem viagem a outros planetas. &lt;br/&gt;&lt;br/&gt;Inclusive, se a criptografia fosse proibida, &lt;em&gt;fbzragr bf sbenf qn yrv grevnz cevinpvqnqr&lt;/em&gt;...
</subtitle><author><name>Eduardo N. S. R.</name><email>vndmtrx@duck.com</email></author><entry><title type="html">Kubernetes in a Box, Parte 4 - Preparação do Sistema Base</title><link href="https://vndmtrx.github.io/posts/k8sbox-preparacao-sistema-base/" rel="alternate" type="text/html" title="Kubernetes in a Box, Parte 4 - Preparação do Sistema Base" /><published>2026-10-10T23:51:00+00:00</published><updated>2026-10-10T23:51:00+00:00</updated><id>https://vndmtrx.github.io/posts/k8sbox-preparacao-sistema-base</id><content type="html" xml:base="https://vndmtrx.github.io/posts/k8sbox-preparacao-sistema-base/"><![CDATA[<p>Uma VM AlmaLinux recém-saída do <code class="language-plaintext highlighter-rouge">vagrant up</code> é um ótimo servidor genérico: quieta, previsível e configurada para aplicações comuns. Ela não repassa pacotes entre interfaces, não deixa o firewall inspecionar o tráfego das pontes de rede, convive com swap sem remorso e nem sequer conhece os nomes das outras máquinas do laboratório. Para quem vai virar nó de Kubernetes, cada uma dessas características é um problema que precisa ser resolvido.</p>

<p>Neste post, vamos cobrir exatamente essa lacuna: tudo o que precisa mudar no sistema operacional de fábrica antes que o primeiro binário do Kubernetes encoste no disco. No <code class="language-plaintext highlighter-rouge">kubeadm</code>, essa etapa é mascarada como uma simples checagem de <em>preflight</em>. Aqui, sem instaladores automáticos, nós assumimos essa responsabilidade e deixamos o Ansible preparar cada nó de forma repetível, previsível e sem surpresas.</p>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Nota da Série e Contexto de Laboratório</p><p>Este post faz parte da série <strong>"Kubernetes in a Box"</strong>, onde dissecamos e construímos, do zero e de forma totalmente reproduzível via Ansible, um cluster Kubernetes completo, com alta disponibilidade, armazenamento persistente, rede moderna e observabilidade. Todo o código do projeto está disponível no repositório parceiro <a href="https://github.com/vndmtrx/k8s-in-a-box">vndmtrx/k8s-in-a-box</a>.</p>

<p><strong>Aviso de escopo:</strong> as decisões de arquitetura e parâmetros deste projeto foram pensadas sob medida para a nossa realidade de laboratório local em estações Linux com Vagrant e KVM/Libvirt. Elas priorizam aprendizado profundo e reprodutibilidade rápida sobre convenções corporativas de produção. O cluster foi originalmente projetado em versões anteriores e recentemente atualizado para o <strong>Kubernetes v1.37.0</strong>.</p>
</div>

<details class="toc-box">
  <summary><strong>Sumário &amp; Índice de Seções (TOC)</strong></summary>
<ul id="markdown-toc">
  <li><a href="#duas-roles-três-grupos-de-máquinas" id="markdown-toc-duas-roles-três-grupos-de-máquinas">Duas roles, três grupos de máquinas</a></li>
  <li><a href="#a-role-infra-sistema-o-que-toda-máquina-precisa" id="markdown-toc-a-role-infra-sistema-o-que-toda-máquina-precisa">A role infra-sistema: o que toda máquina precisa</a>    <ul>
      <li><a href="#selinux-permissivo-de-novo" id="markdown-toc-selinux-permissivo-de-novo">SELinux permissivo, de novo</a></li>
      <li><a href="#pacotes-fuso-horário-e-locale" id="markdown-toc-pacotes-fuso-horário-e-locale">Pacotes, fuso horário e locale</a></li>
      <li><a href="#o-arquivo-etchosts-gerado-pelo-inventário" id="markdown-toc-o-arquivo-etchosts-gerado-pelo-inventário">O arquivo /etc/hosts gerado pelo inventário</a></li>
      <li><a href="#tailspin-colorização-de-logs" id="markdown-toc-tailspin-colorização-de-logs">Tailspin: colorização de logs</a></li>
    </ul>
  </li>
  <li><a href="#a-role-k8s-cluster-base-transformando-uma-vm-em-nó" id="markdown-toc-a-role-k8s-cluster-base-transformando-uma-vm-em-nó">A role k8s-cluster-base: transformando uma VM em nó</a>    <ul>
      <li><a href="#os-pacotes-do-nó" id="markdown-toc-os-pacotes-do-nó">Os pacotes do nó</a></li>
      <li><a href="#o-reboot-condicional" id="markdown-toc-o-reboot-condicional">O reboot condicional</a></li>
      <li><a href="#módulos-de-kernel" id="markdown-toc-módulos-de-kernel">Módulos de kernel</a></li>
      <li><a href="#por-que-swap-e-kubernetes-não-se-dão" id="markdown-toc-por-que-swap-e-kubernetes-não-se-dão">Por que swap e Kubernetes não se dão</a></li>
      <li><a href="#o-serviço-nftables" id="markdown-toc-o-serviço-nftables">O serviço nftables</a></li>
      <li><a href="#parâmetros-de-sysctl-e-a-ponte-que-enxerga-o-firewall" id="markdown-toc-parâmetros-de-sysctl-e-a-ponte-que-enxerga-o-firewall">Parâmetros de sysctl e a ponte que enxerga o firewall</a></li>
      <li><a href="#usuário-diretórios-e-os-cas" id="markdown-toc-usuário-diretórios-e-os-cas">Usuário, diretórios e os CAs</a></li>
    </ul>
  </li>
  <li><a href="#executando-só-o-que-importa" id="markdown-toc-executando-só-o-que-importa">Executando só o que importa</a></li>
  <li><a href="#diagnóstico-e-troubleshooting" id="markdown-toc-diagnóstico-e-troubleshooting">Diagnóstico e Troubleshooting</a>    <ul>
      <li><a href="#swap-desligado" id="markdown-toc-swap-desligado">Swap desligado</a></li>
      <li><a href="#módulos-de-kernel-carregados" id="markdown-toc-módulos-de-kernel-carregados">Módulos de kernel carregados</a></li>
      <li><a href="#parâmetros-de-sysctl" id="markdown-toc-parâmetros-de-sysctl">Parâmetros de sysctl</a></li>
      <li><a href="#selinux" id="markdown-toc-selinux">SELinux</a></li>
      <li><a href="#resolução-de-nomes" id="markdown-toc-resolução-de-nomes">Resolução de nomes</a></li>
      <li><a href="#usuário-diretórios-e-certificados-dos-cas" id="markdown-toc-usuário-diretórios-e-certificados-dos-cas">Usuário, diretórios e certificados dos CAs</a></li>
      <li><a href="#tailspin-instalado" id="markdown-toc-tailspin-instalado">Tailspin instalado</a></li>
      <li><a href="#problemas-comuns" id="markdown-toc-problemas-comuns">Problemas comuns</a></li>
    </ul>
  </li>
  <li><a href="#conclusão" id="markdown-toc-conclusão">Conclusão</a></li>
  <li><a href="#referências" id="markdown-toc-referências">Referências</a></li>
</ul>

</details>

<h2 id="duas-roles-três-grupos-de-máquinas">Duas roles, três grupos de máquinas</h2>

<p>Todo o código desta etapa está dividido em duas roles no repositório parceiro <sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>, porque as máquinas do laboratório têm papéis e requisitos diferentes:</p>

<p>A <a href="https://github.com/vndmtrx/k8s-in-a-box/tree/main/ansible/infra-sistema">infra-sistema</a> cuida do que qualquer máquina do laboratório precisa, inclusive as que nunca vão rodar <code class="language-plaintext highlighter-rouge">kubelet</code>. A <a href="https://github.com/vndmtrx/k8s-in-a-box/tree/main/ansible/k8s-cluster-base">k8s-cluster-base</a> cuida apenas de quem vai ser nó.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>todos (cluster.yml, tag cluster-sistema)
├── suporte
│   ├── loadbalancers  -&gt; infra-sistema
│   └── nfs            -&gt; infra-sistema
└── cluster            (cluster.yml, tag cluster-kubernetes-base)
    ├── managers       -&gt; infra-sistema + k8s-cluster-base
    └── workers        -&gt; infra-sistema + k8s-cluster-base

clientes (ops.yml, tag ops-sistema)
└── kubox              -&gt; infra-sistema
</code></pre></div></div>

<p>O grupo <code class="language-plaintext highlighter-rouge">todos</code> recebe a <code class="language-plaintext highlighter-rouge">infra-sistema</code> no <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/ansible/cluster.yml">cluster.yml</a>. O grupo <code class="language-plaintext highlighter-rouge">cluster</code> recebe, além dela, a <code class="language-plaintext highlighter-rouge">k8s-cluster-base</code>. E o <code class="language-plaintext highlighter-rouge">kubox</code>, que está fora de <code class="language-plaintext highlighter-rouge">todos</code> e mora no grupo <code class="language-plaintext highlighter-rouge">clientes</code>, recebe a <code class="language-plaintext highlighter-rouge">infra-sistema</code> pelo <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/ansible/ops.yml">ops.yml</a>. É por isso que não existe <code class="language-plaintext highlighter-rouge">swapoff</code> nem <code class="language-plaintext highlighter-rouge">br_netfilter</code> no balanceador: ele não é nó, e carregar módulo de ponte numa máquina que só roda HAProxy seria enfeite.</p>

<p>Do lado do <code class="language-plaintext highlighter-rouge">Makefile</code>, as duas roles ficam em alvos diferentes:</p>

<div class="language-makefile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Trecho do Makefile
</span><span class="nl">infra</span><span class="o">:</span> <span class="nf">garante-config</span>
	<span class="nv">ANSIBLE_CONFIG</span><span class="o">=</span><span class="s2">"</span><span class="p">$(</span><span class="s2">CFG</span><span class="p">)</span><span class="s2">"</span> ansible-playbook <span class="s2">"</span><span class="p">$(</span><span class="s2">PLAYBOOK</span><span class="p">)</span><span class="s2">"</span> <span class="p">$(</span>ANSIBLE_VERBOSE<span class="p">)</span> <span class="nt">--tags</span> cluster-artefatos,cluster-pki,cluster-sistema,cluster-balanceador,cluster-nfs

<span class="nl">control-plane</span><span class="o">:</span> <span class="nf">garante-config</span>
	<span class="nv">ANSIBLE_CONFIG</span><span class="o">=</span><span class="s2">"</span><span class="p">$(</span><span class="s2">CFG</span><span class="p">)</span><span class="s2">"</span> ansible-playbook <span class="s2">"</span><span class="p">$(</span><span class="s2">PLAYBOOK</span><span class="p">)</span><span class="s2">"</span> <span class="p">$(</span>ANSIBLE_VERBOSE<span class="p">)</span> <span class="nt">--tags</span> cluster-kubernetes-base,cluster-kubelet,cluster-etcd,cluster-kube-apiserver,cluster-kube-controller-manager,cluster-kube-scheduler
</code></pre></div></div>

<p>O <code class="language-plaintext highlighter-rouge">make infra</code> leva a <code class="language-plaintext highlighter-rouge">infra-sistema</code> (junto com artefatos, PKI, balanceador e NFS). O <code class="language-plaintext highlighter-rouge">make control-plane</code> abre com a <code class="language-plaintext highlighter-rouge">k8s-cluster-base</code> e segue para o kubelet e o resto do <em>control plane</em>. Dentro do <code class="language-plaintext highlighter-rouge">cluster.yml</code>, a ordem dos <em>plays</em> também importa: artefatos, PKI, sistema, balanceador, NFS e só então a base dos nós.</p>

<div class="markdown-alert markdown-alert-warning"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-alert" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg> Rodar fora de ordem</p><p>As duas roles dependem de coisas produzidas por <em>plays</em> anteriores. O Tailspin que a <code class="language-plaintext highlighter-rouge">infra-sistema</code> copia para as VMs é baixado antes, pela role <code class="language-plaintext highlighter-rouge">infra-artefatos</code> (tag <code class="language-plaintext highlighter-rouge">cluster-artefatos</code>), e os certificados dos CAs que a <code class="language-plaintext highlighter-rouge">k8s-cluster-base</code> distribui são gerados antes, pela <code class="language-plaintext highlighter-rouge">infra-pki</code> (tag <code class="language-plaintext highlighter-rouge">cluster-pki</code>). Se você rodar apenas <code class="language-plaintext highlighter-rouge">--tags cluster-sistema</code> ou <code class="language-plaintext highlighter-rouge">--tags cluster-kubernetes-base</code> num clone recém-feito, o Ansible vai reclamar de arquivo de origem inexistente.</p>
</div>

<h2 id="a-role-infra-sistema-o-que-toda-máquina-precisa">A role infra-sistema: o que toda máquina precisa</h2>

<p>O arquivo que importa aqui é o <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/ansible/infra-sistema/tasks/00-base.yml">00-base.yml</a>. São poucas tasks, mas cada uma resolve um problema que costuma aparecer no pior momento possível.</p>

<h3 id="selinux-permissivo-de-novo">SELinux permissivo, de novo</h3>

<p>Lembra do <code class="language-plaintext highlighter-rouge">setenforce 0</code> escondido no <code class="language-plaintext highlighter-rouge">Vagrantfile</code>, lá na <a href="/posts/k8sbox-ferramentas-ambiente/">Parte 2</a>? A <code class="language-plaintext highlighter-rouge">infra-sistema</code> repete a instrução, desta vez declarativa:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Trecho de ansible/infra-sistema/tasks/00-base.yml (nome da task encurtado)</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Colocar SELinux em modo permissivo</span>
  <span class="na">become</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="na">ansible.posix.selinux</span><span class="pi">:</span>
    <span class="na">policy</span><span class="pi">:</span> <span class="s">targeted</span>
    <span class="na">state</span><span class="pi">:</span> <span class="s">permissive</span>
</code></pre></div></div>

<p>Redundante? Um pouco, e de propósito. O módulo <sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> faz duas coisas: grava o modo no <code class="language-plaintext highlighter-rouge">/etc/selinux/config</code>, para sobreviver ao reboot, e aplica na hora o equivalente a um <code class="language-plaintext highlighter-rouge">setenforce 0</code>. A role não deve depender de quem criou a VM. Se você trocar de <em>box</em>, provisionar de outro jeito, ou se alguém rodar <code class="language-plaintext highlighter-rouge">setenforce 1</code> no meio do caminho, a próxima execução do playbook devolve a máquina ao estado combinado.</p>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Permissivo continua sendo diferente de desligado</p><p>No modo permissivo o kernel não bloqueia nada, mas continua auditando cada acesso que a política teria negado, e registra tudo no <code class="language-plaintext highlighter-rouge">audit.log</code>. É esse registro que alimenta a política customizada do projeto, compilada e instalada para o kubelet. Se quiser entender os detalhes dessa política, o repositório traz a documentação completa em <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/docs/selinux.md">docs/selinux.md</a>.</p>
</div>

<h3 id="pacotes-fuso-horário-e-locale">Pacotes, fuso horário e locale</h3>

<p>A lista de pacotes em <code class="language-plaintext highlighter-rouge">defaults/main.yml</code> da role (<code class="language-plaintext highlighter-rouge">pacotes_extras</code>) não traz nada de Kubernetes, mas entrega a base operacional: utilitários de rede e shell (<code class="language-plaintext highlighter-rouge">mtr</code>, <code class="language-plaintext highlighter-rouge">nano</code>, <code class="language-plaintext highlighter-rouge">wget</code>, <code class="language-plaintext highlighter-rouge">bash-completion</code>), suporte a idiomas para o <code class="language-plaintext highlighter-rouge">pt_BR.UTF-8</code> e o <code class="language-plaintext highlighter-rouge">dnf-utils</code> (que traz o <code class="language-plaintext highlighter-rouge">needs-restarting</code>, usado mais adiante para decidir se o nó precisa reiniciar após atualizações de kernel).</p>

<p>Em seguida, o Ansible garante que todas as VMs estejam sob o mesmo fuso horário (<code class="language-plaintext highlighter-rouge">America/Sao_Paulo</code>) e locale. Fuso alinhado evita dor de cabeça na hora de correlacionar logs de componentes distribuídos como <code class="language-plaintext highlighter-rouge">etcd</code> e <code class="language-plaintext highlighter-rouge">kube-apiserver</code>.</p>

<div class="markdown-alert markdown-alert-tip"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-light-bulb" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"></path></svg> Dica</p><p>Com <code class="language-plaintext highlighter-rouge">pt_BR.UTF-8</code>, mensagens de erro no terminal passam a vir traduzidas, o que atrapalha pesquisas de bugs. Para ver saídas técnicas no padrão em inglês, rode o comando precedido de <code class="language-plaintext highlighter-rouge">LC_ALL=C</code> (exemplo: <code class="language-plaintext highlighter-rouge">LC_ALL=C systemctl status chronyd</code>).</p>
</div>

<h3 id="o-arquivo-etchosts-gerado-pelo-inventário">O arquivo /etc/hosts gerado pelo inventário</h3>

<p>Não existe servidor de DNS no laboratório. As VMs enxergam a internet pela <code class="language-plaintext highlighter-rouge">eth0</code> e conversam entre si pela <code class="language-plaintext highlighter-rouge">eth1</code>, e nada no meio traduz <code class="language-plaintext highlighter-rouge">worker1.k8sbox.local</code> para <code class="language-plaintext highlighter-rouge">172.24.0.41</code>. Quem faz esse papel é um <code class="language-plaintext highlighter-rouge">/etc/hosts</code> gerado a partir do próprio inventário, pelo template <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/ansible/infra-sistema/templates/hosts.j2">hosts.j2</a>:</p>

<div class="language-jinja highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Trecho de ansible/infra-sistema/templates/hosts.j2
# Hostname da máquina
127.0.1.1 <span class="cp">{{</span> <span class="nv">inventory_hostname</span> <span class="cp">}}</span>

# Entradas para os FQDNs dos endpoints
<span class="cp">{{</span> <span class="nv">keepalived_vip_ip</span> <span class="cp">}}</span> <span class="cp">{{</span> <span class="nv">vip_api_fqdn</span> <span class="cp">}}</span> <span class="cp">{{</span> <span class="nv">vip_etcd_fqdn</span> <span class="cp">}}</span>

# Entradas das máquinas do cluster
<span class="cp">{%</span> <span class="k">for</span> <span class="nv">host</span> <span class="ow">in</span> <span class="nv">groups</span><span class="p">[</span><span class="s1">'todos'</span><span class="p">]</span> <span class="cp">%}</span>
<span class="cp">{{</span> <span class="nv">hostvars</span><span class="p">[</span><span class="nv">host</span><span class="p">]</span><span class="err">.</span><span class="nv">ansible_host</span> <span class="cp">}}</span> <span class="cp">{{</span> <span class="nv">hostvars</span><span class="p">[</span><span class="nv">host</span><span class="p">]</span><span class="err">.</span><span class="nv">fqdn</span> <span class="cp">}}</span>
<span class="cp">{%</span> <span class="k">endfor</span> <span class="cp">%}</span>
</code></pre></div></div>

<p>O IP e o FQDN de cada máquina vêm de <code class="language-plaintext highlighter-rouge">ansible_host</code> e <code class="language-plaintext highlighter-rouge">fqdn</code>, exatamente os campos que você viu no <code class="language-plaintext highlighter-rouge">hosts-*.yml</code> da Parte 3. No topologia <code class="language-plaintext highlighter-rouge">mini</code>, o arquivo renderizado fica assim (a ordem das últimas linhas pode variar):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Hostname da máquina
127.0.1.1 manager1

# Entradas para os FQDNs dos endpoints
172.24.0.10 api.k8sbox.local etcd.k8sbox.local

# Entradas das máquinas do cluster
172.24.0.21 loadbalancer1.k8sbox.local
172.24.0.25 nfs.k8sbox.local
172.24.0.31 manager1.k8sbox.local
172.24.0.41 worker1.k8sbox.local
172.24.0.42 worker2.k8sbox.local
</code></pre></div></div>

<p>Repare na linha do meio. O IP <code class="language-plaintext highlighter-rouge">172.24.0.10</code> é o VIP do Keepalived, configurado para o balanceador de carga. Mesmo antes do serviço subir, o nome <code class="language-plaintext highlighter-rouge">api.k8sbox.local</code> já aponta para ele em todas as máquinas desde agora. Quando o VIP responder, todo mundo já sabe onde ele mora. E é esse nome, não o IP de um manager individual, que o kube-proxy usa para falar com a API (<code class="language-plaintext highlighter-rouge">https://api.k8sbox.local:6443</code>) e que consta nos certificados emitidos para o cluster.</p>

<div class="markdown-alert markdown-alert-tip"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-light-bulb" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"></path></svg> Dica</p><p>Para testar resolução de nomes, use <code class="language-plaintext highlighter-rouge">getent hosts api.k8sbox.local</code> e não <code class="language-plaintext highlighter-rouge">nslookup</code> ou <code class="language-plaintext highlighter-rouge">dig</code>. Essas duas ferramentas perguntam direto a um servidor DNS e ignoram o <code class="language-plaintext highlighter-rouge">/etc/hosts</code>, então elas dirão que o nome não existe mesmo quando qualquer aplicação do sistema o resolve normalmente. O <code class="language-plaintext highlighter-rouge">getent</code> passa pelo mesmo caminho que as aplicações (a ordem definida no <code class="language-plaintext highlighter-rouge">nsswitch.conf</code>, em que arquivos locais vêm antes do DNS).</p>
</div>

<h3 id="tailspin-colorização-de-logs">Tailspin: colorização de logs</h3>

<p>Para fechar a <code class="language-plaintext highlighter-rouge">infra-sistema</code>, instalamos o <a href="https://github.com/bensadeh/tailspin">Tailspin</a> <sup id="fnref:4"><a href="#fn:4" class="footnote" rel="footnote" role="doc-noteref">3</a></sup> (<code class="language-plaintext highlighter-rouge">tspin</code>). Ele destaca automaticamente IPs, datas, status HTTP e níveis de severidade na saída padrão, transformando a leitura do <code class="language-plaintext highlighter-rouge">journalctl</code> em algo muito mais legível quando a rede ou o kubelet começarem a chiar:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>journalctl <span class="nt">-u</span> sshd <span class="nt">--no-pager</span> | tspin
</code></pre></div></div>

<p>O binário é baixado no host pela role de artefatos e descompactado em <code class="language-plaintext highlighter-rouge">/usr/local/bin</code> com verificação de idempotência para não baixar novamente se já existir. Diferente dos binários do Kubernetes, aqui usamos a URL <code class="language-plaintext highlighter-rouge">latest</code> sem checksum rígido — concessão aceitável para uma ferramenta de conveniência em lab local descartável.</p>

<h2 id="a-role-k8s-cluster-base-transformando-uma-vm-em-nó">A role k8s-cluster-base: transformando uma VM em nó</h2>

<p>Daqui em diante estamos apenas nas máquinas do grupo <code class="language-plaintext highlighter-rouge">cluster</code>, os managers e os workers. O <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/ansible/k8s-cluster-base/tasks/main.yml">main.yml</a> da role importa dois arquivos de tasks: <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/ansible/k8s-cluster-base/tasks/01-instalacao.yml">01-instalacao.yml</a> e <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/ansible/k8s-cluster-base/tasks/02-pki.yml">02-pki.yml</a>. O primeiro é uma sequência em que a ordem não é estética:</p>

<pre><code class="language-mermaid">flowchart TD
    PKG["&lt;b&gt;1. Pacotes&lt;/b&gt;&lt;br&gt;dnf instala as ferramentas e o kernel-modules-extra"]
    RB["&lt;b&gt;2. Reboot condicional&lt;/b&gt;&lt;br&gt;needs-restarting -r decide se a VM reinicia agora"]
    MOD["&lt;b&gt;3. Módulos de kernel&lt;/b&gt;&lt;br&gt;overlay, br_netfilter, vxlan e a família nf_*"]
    NFT["&lt;b&gt;4. Serviço nftables&lt;/b&gt;&lt;br&gt;habilitado e iniciado"]
    SWP["&lt;b&gt;5. Swap&lt;/b&gt;&lt;br&gt;swapoff agora e entrada do fstab comentada"]
    SYS["&lt;b&gt;6. Parâmetros sysctl&lt;/b&gt;&lt;br&gt;ip_forward e bridge-nf-call-*"]
    USR["&lt;b&gt;7. Usuário e diretórios&lt;/b&gt;&lt;br&gt;conta kubernetes, /etc/kubernetes e /var/lib/kubernetes"]
    PKI["&lt;b&gt;8. Certificados dos CAs&lt;/b&gt;&lt;br&gt;cópia para /etc/kubernetes/pki"]

    PKG --&gt; RB
    RB --&gt; MOD
    MOD --&gt; NFT
    NFT --&gt; SWP
    SWP --&gt; SYS
    SYS --&gt; USR
    USR --&gt; PKI

    class PKG key;
    class RB,MOD,NFT,SWP,SYS,USR neutral;
    class PKI success;
</code></pre>

<p>Duas dependências de ordem merecem atenção. O reboot precisa vir antes dos módulos, e os módulos precisam vir antes do <code class="language-plaintext highlighter-rouge">sysctl</code>. Já vamos ver por quê.</p>

<h3 id="os-pacotes-do-nó">Os pacotes do nó</h3>

<p>Em vez de uma lista de compras, vou agrupar pelo problema que cada pacote resolve. A variável é <code class="language-plaintext highlighter-rouge">pacotes_kubernetes</code>, nos <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/ansible/k8s-cluster-base/defaults/main.yml">defaults da role</a>:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Pacote</th>
      <th style="text-align: left">Por que está aqui</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">kernel-modules-extra</code></td>
      <td style="text-align: left">Módulos de kernel de uso menos comum, que nem sempre vêm instalados. É um seguro contra descobrir que falta um módulo de rede no meio de um pod. Para ver o que ele traz, <code class="language-plaintext highlighter-rouge">rpm -ql kernel-modules-extra</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">iproute</code></td>
      <td style="text-align: left">O <code class="language-plaintext highlighter-rouge">ip</code> e o <code class="language-plaintext highlighter-rouge">ss</code>, base de qualquer diagnóstico de rede</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">nftables</code></td>
      <td style="text-align: left">O cliente <code class="language-plaintext highlighter-rouge">nft</code>, para ler as regras que o kube-proxy vai escrever</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">iptables</code> e <code class="language-plaintext highlighter-rouge">iptables-nft</code></td>
      <td style="text-align: left">O comando <code class="language-plaintext highlighter-rouge">iptables</code> com backend nf_tables. Muita ferramenta e muito plugin de CNI do ecossistema ainda chamam <code class="language-plaintext highlighter-rouge">iptables</code>, e esse pacote faz tudo cair no mesmo subsistema do kernel em vez de manter duas tabelas de regras em paralelo</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">conntrack-tools</code></td>
      <td style="text-align: left">O <code class="language-plaintext highlighter-rouge">conntrack -L</code> lista a tabela de conexões e o <code class="language-plaintext highlighter-rouge">conntrack -E</code> mostra eventos em tempo real. Essa tabela é o coração do NAT dos Services</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">ethtool</code></td>
      <td style="text-align: left">Inspeciona e ajusta a placa de rede. <em>Checksum offload</em> é suspeito clássico quando o tráfego VXLAN simplesmente some</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">socat</code></td>
      <td style="text-align: left">O canivete de redirecionar bytes entre dois pontos. Historicamente, foi o que fazia o <code class="language-plaintext highlighter-rouge">kubectl port-forward</code> funcionar quando o runtime precisava</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">tar</code> e <code class="language-plaintext highlighter-rouge">xz</code></td>
      <td style="text-align: left">O módulo <code class="language-plaintext highlighter-rouge">unarchive</code> do Ansible depende do <code class="language-plaintext highlighter-rouge">tar</code> no alvo, e o <code class="language-plaintext highlighter-rouge">xz</code> cobre tarballs comprimidos nesse formato</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">skopeo</code></td>
      <td style="text-align: left">Copia imagens de contêiner entre registries e arquivos sem precisar de daemon. A role do kubelet usa para o cache local de imagens que vimos na Parte 3</td>
    </tr>
  </tbody>
</table>

<p>Nada disso inicia serviço ou altera comportamento do sistema. São ferramentas esperando para serem úteis.</p>

<h3 id="o-reboot-condicional">O reboot condicional</h3>

<p>Logo depois da instalação de pacotes, a role pergunta se a máquina precisa reiniciar:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Trecho de ansible/k8s-cluster-base/tasks/01-instalacao.yml</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Verificar se precisa reiniciar pós-instalação de pacotes</span>
  <span class="na">ansible.builtin.command</span><span class="pi">:</span> <span class="s">needs-restarting -r</span>
  <span class="na">register</span><span class="pi">:</span> <span class="s">reboot_check_k8s</span>
  <span class="na">ignore_errors</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="na">changed_when</span><span class="pi">:</span> <span class="s">reboot_check_k8s.rc == </span><span class="m">1</span>
  <span class="na">failed_when</span><span class="pi">:</span> <span class="s">reboot_check_k8s.rc not in [0, 1]</span>
  <span class="na">notify</span><span class="pi">:</span> <span class="s">Reiniciar VM</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Executar reboot imediato se necessário</span>
  <span class="na">ansible.builtin.meta</span><span class="pi">:</span> <span class="s">flush_handlers</span>
</code></pre></div></div>

<p>O <code class="language-plaintext highlighter-rouge">needs-restarting -r</code> devolve código de saída 0 quando está tudo em ordem e 1 quando algum componente central (kernel, glibc, systemd, entre outros) foi atualizado e o reboot é recomendado. O Ansible não tem como saber isso sozinho, então a task traduz: código 1 vira "changed", e "changed" dispara o <code class="language-plaintext highlighter-rouge">notify</code>.</p>

<p>Por padrão, handlers só executam no fim do <em>play</em>. O <code class="language-plaintext highlighter-rouge">meta: flush_handlers</code> força a execução agora, e o handler faz o reboot de verdade, esperando a máquina voltar:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Trecho de ansible/k8s-cluster-base/handlers/main.yml</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Reiniciar VM</span>
  <span class="na">ansible.builtin.reboot</span><span class="pi">:</span>
    <span class="na">post_reboot_delay</span><span class="pi">:</span> <span class="m">10</span>
    <span class="na">reboot_timeout</span><span class="pi">:</span> <span class="m">300</span>
</code></pre></div></div>

<p>Reiniciar antes de qualquer outra coisa não é capricho. Se o <code class="language-plaintext highlighter-rouge">dnf</code> trouxe um kernel mais novo, os módulos instalados pertencem a ele, e o <code class="language-plaintext highlighter-rouge">modprobe</code> procura sempre em <code class="language-plaintext highlighter-rouge">/lib/modules/$(uname -r)</code>, ou seja, no kernel que está rodando. Carregar módulos sem reiniciar poderia resultar em "módulo não encontrado" por um motivo confuso.</p>

<h3 id="módulos-de-kernel">Módulos de kernel</h3>

<p>A lista de módulos está em <code class="language-plaintext highlighter-rouge">modulos_kernel_kubernetes</code>:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Módulo</th>
      <th style="text-align: left">Para que serve</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">overlay</code></td>
      <td style="text-align: left">O OverlayFS, sistema de arquivos em camadas que o runtime de contêineres usa para empilhar imagens e criar camadas graváveis efêmeras</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">br_netfilter</code></td>
      <td style="text-align: left">Faz o tráfego das pontes de rede passar pelo firewall do kernel. Detalhes na seção de <code class="language-plaintext highlighter-rouge">sysctl</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">vxlan</code></td>
      <td style="text-align: left">Encapsulamento de rede usado por CNIs em modo <em>overlay</em>, como o Flannel e o Cilium em modo túnel</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">ip_tables</code> e <code class="language-plaintext highlighter-rouge">ip6_tables</code></td>
      <td style="text-align: left">As famílias clássicas do iptables, para quem ainda usa o backend legado (algumas imagens de contêiner trazem o próprio <code class="language-plaintext highlighter-rouge">iptables</code>)</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">nf_tables</code></td>
      <td style="text-align: left">O núcleo do nftables</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">nf_nat</code></td>
      <td style="text-align: left">Tradução de endereços, a base do DNAT dos Services</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">nf_conntrack</code> e <code class="language-plaintext highlighter-rouge">xt_conntrack</code></td>
      <td style="text-align: left">O rastreamento de conexões e a extensão que permite às regras de firewall consultar o estado de cada conexão</td>
    </tr>
  </tbody>
</table>

<p>Boa parte desses módulos seria carregada sob demanda pelo próprio kernel, mas listar explicitamente compra duas coisas: ambiente determinístico em todas as máquinas e falha antecipada. Um módulo que não existe estoura no Ansible, na hora, e não às três da manhã dentro de um pod.</p>

<p>A role registra a lista em dois lugares ao mesmo tempo:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Trecho de ansible/k8s-cluster-base/tasks/01-instalacao.yml</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Garantir configuração de módulos necessários</span>
  <span class="na">ansible.builtin.copy</span><span class="pi">:</span>
    <span class="na">dest</span><span class="pi">:</span> <span class="s">/etc/modules-load.d/kubernetes.conf</span>
    <span class="na">content</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">modulos_kernel_kubernetes</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">join('</span><span class="se">\n</span><span class="s">')</span><span class="nv"> </span><span class="s">}}</span><span class="se">\n</span><span class="s">"</span>
    <span class="na">mode</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0644"</span>
  <span class="na">notify</span><span class="pi">:</span> <span class="s">Recarregar módulos do kernel</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Carregar módulos explicitamente</span>
  <span class="na">community.general.modprobe</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">item</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">state</span><span class="pi">:</span> <span class="s">present</span>
    <span class="na">persistent</span><span class="pi">:</span> <span class="s">present</span>
  <span class="na">loop</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">modulos_kernel_kubernetes</span><span class="nv"> </span><span class="s">}}"</span>
</code></pre></div></div>

<p>O arquivo <code class="language-plaintext highlighter-rouge">/etc/modules-load.d/kubernetes.conf</code> é lido pelo <code class="language-plaintext highlighter-rouge">systemd-modules-load</code> a cada boot, e o conteúdo é só um módulo por linha:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>overlay
br_netfilter
vxlan
ip_tables
ip6_tables
nf_tables
nf_nat
nf_conntrack
xt_conntrack
</code></pre></div></div>

<p>Já a task do <code class="language-plaintext highlighter-rouge">modprobe</code> <sup id="fnref:6"><a href="#fn:6" class="footnote" rel="footnote" role="doc-noteref">4</a></sup> carrega os módulos na hora, sem esperar o próximo boot, e o parâmetro <code class="language-plaintext highlighter-rouge">persistent: present</code> pede ao módulo do Ansible para também registrar cada um em <code class="language-plaintext highlighter-rouge">/etc/modules-load.d/</code>. Na prática, as duas coisas se sobrepõem. Se um dia a role for enxugada, uma delas pode sair sem que ninguém perceba.</p>

<h3 id="por-que-swap-e-kubernetes-não-se-dão">Por que swap e Kubernetes não se dão</h3>

<p>Essa é a regra mais famosa da lista, e quase sempre repetida sem explicação. No Linux, os limites de memória de um contêiner são barreiras impostas pelo cgroup (<em>control group</em>): se o processo ultrapassa o teto estipulado, o kernel aciona o OOM Killer (<em>Out of Memory Killer</em>), finaliza o processo excedente e o orquestrador sobe outro em seu lugar. É um corte seco, mas rápido e previsível.</p>

<p>Agora some swap à equação:</p>

<ul>
  <li><strong>O limite deixa de ser parede.</strong> Um pod acima do teto pode ter páginas empurradas para o disco, em vez de morrer. Em vez de uma queda rápida, você ganha uma lentidão arrastada, difícil de enxergar.</li>
  <li><strong>O agendador planeja com RAM.</strong> O <em>scheduler</em> decide onde encaixar um pod somando <code class="language-plaintext highlighter-rouge">requests</code> de memória. Se o nó empurra páginas para o disco por baixo dos panos, o plano de encaixe vira ficção.</li>
  <li><strong>O nó não se defende.</strong> Os limiares de despejo (<em>eviction</em>) do kubelet olham para a memória disponível. Com swap, o nó pode ficar patinando no disco em vez de despejar pods a tempo.</li>
</ul>

<p>Por padrão, o kubelet nem tenta conviver com isso: ele recusa iniciar se detectar swap ativo. A configuração <code class="language-plaintext highlighter-rouge">failSwapOn</code> <sup id="fnref:7"><a href="#fn:7" class="footnote" rel="footnote" role="doc-noteref">5</a></sup> tem como valor padrão <code class="language-plaintext highlighter-rouge">true</code>, e o template <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/ansible/k8s-kubelet/templates/kubelet-config.yml.j2">kubelet-config.yml.j2</a> do projeto mantém essa diretriz intacta. Se o swap estiver ligado quando o kubelet subir, ele simplesmente não inicializa.</p>

<p>A role cuida do swap em três tasks:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Trecho de ansible/k8s-cluster-base/tasks/01-instalacao.yml</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Verificar se swap está ativo</span>
  <span class="na">ansible.builtin.command</span><span class="pi">:</span> <span class="s">swapon --show</span>
  <span class="na">register</span><span class="pi">:</span> <span class="s">swap_status</span>
  <span class="na">changed_when</span><span class="pi">:</span> <span class="kc">false</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Desabilitar swap</span>
  <span class="na">ansible.builtin.command</span><span class="pi">:</span> <span class="s">swapoff -a</span>
  <span class="na">when</span><span class="pi">:</span> <span class="s">swap_status.stdout != ""</span>
  <span class="na">changed_when</span><span class="pi">:</span> <span class="kc">true</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Remover entrada de swap do /etc/fstab</span>
  <span class="na">ansible.builtin.replace</span><span class="pi">:</span>
    <span class="na">path</span><span class="pi">:</span> <span class="s">/etc/fstab</span>
    <span class="na">regexp</span><span class="pi">:</span> <span class="s1">'</span><span class="s">^([^#].*?\sswap\s.*)'</span>
    <span class="na">replace</span><span class="pi">:</span> <span class="s1">'</span><span class="s">#</span><span class="nv"> </span><span class="s">\1'</span>
</code></pre></div></div>

<p>A primeira task só observa: se <code class="language-plaintext highlighter-rouge">swapon --show</code> não imprimir nada, não há swap. A segunda só roda quando há, e liga o swap para fora na hora. A terceira é a que garante que o problema não volta depois de um reboot.</p>

<p>A expressão regular merece uma leitura calma. O padrão <code class="language-plaintext highlighter-rouge">^([^#].*?\sswap\s.*)</code> pega qualquer linha que <strong>não</strong> comece com <code class="language-plaintext highlighter-rouge">#</code> e que tenha a palavra <code class="language-plaintext highlighter-rouge">swap</code> cercada por espaços, e a substitui por ela mesma com <code class="language-plaintext highlighter-rouge"># </code> na frente. O <code class="language-plaintext highlighter-rouge">[^#]</code> no início é o que torna a task idempotente: uma linha já comentada não casa mais, então a segunda execução não encontra nada para mudar.</p>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Mas Dudu, o Kubernetes não suporta swap hoje?</p><p>Suporta, e a <a href="https://kubernetes.io/docs/concepts/cluster-administration/swap-memory-management/">documentação oficial</a> trata do assunto com detalhe. Mas o suporte é opcional, exige cgroup v2 e uma configuração explícita no kubelet (<code class="language-plaintext highlighter-rouge">failSwapOn: false</code> junto com <code class="language-plaintext highlighter-rouge">memorySwap</code>), além de decisões sobre quanto swap cada tipo de pod pode usar. Nada disso é o padrão, e nada disso é o que o projeto faz: o nosso kubelet mantém <code class="language-plaintext highlighter-rouge">memorySwap: {}</code> e o comportamento previsível de sempre. Quem quiser experimentar tem um bom ponto de partida na documentação.</p>
</div>

<div class="markdown-alert markdown-alert-warning"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-alert" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg> Swap que não vem do fstab</p><p>A task só comenta o que está no <code class="language-plaintext highlighter-rouge">/etc/fstab</code>. Swap criado por outras vias, como um <code class="language-plaintext highlighter-rouge">zram</code> gerado por <em>generator</em> do systemd ou uma unidade <code class="language-plaintext highlighter-rouge">.swap</code> escrita à mão, não é tocado e pode voltar no próximo boot. Se <code class="language-plaintext highlighter-rouge">swapon --show</code> voltar a mostrar algo depois de um reboot, rode <code class="language-plaintext highlighter-rouge">systemctl list-units --type=swap</code> para descobrir de onde ele está vindo.</p>
</div>

<h3 id="o-serviço-nftables">O serviço nftables</h3>

<p>Uma distinção ajuda aqui: quem aplica as regras de filtragem é o subsistema <code class="language-plaintext highlighter-rouge">nf_tables</code> do kernel, sempre presente. O serviço <code class="language-plaintext highlighter-rouge">nftables.service</code> apenas garante o carregamento das tabelas base no boot. Habilitamos esse serviço porque o kube-proxy do projeto é configurado em <code class="language-plaintext highlighter-rouge">mode: nftables</code> (na role <a href="https://github.com/vndmtrx/k8s-in-a-box/tree/main/ansible/addon-kube-proxy">addon-kube-proxy</a>). Esse modo dispensa o legado do iptables e exige kernel recente <sup id="fnref:8"><a href="#fn:8" class="footnote" rel="footnote" role="doc-noteref">6</a></sup>, requisito que o AlmaLinux 10 atende com folga.</p>

<p>Repare no que <strong>não</strong> está nessa role: regras de filtragem do host. O projeto não configura firewall de máquina, e no laboratório isso funciona porque a rede <code class="language-plaintext highlighter-rouge">172.24.0.0/24</code> é privada e isolada. Em produção, isso precisaria virar regras explícitas para as portas do cluster, como as que listamos no <code class="language-plaintext highlighter-rouge">all.yml</code>: <code class="language-plaintext highlighter-rouge">6443</code> para a API, <code class="language-plaintext highlighter-rouge">2379</code> e <code class="language-plaintext highlighter-rouge">2380</code> para o etcd, e <code class="language-plaintext highlighter-rouge">10250</code> para o kubelet.</p>

<div class="markdown-alert markdown-alert-warning"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-alert" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg> Cuidado ao reiniciar o nftables</p><p>No AlmaLinux, o <em>unit</em> do <code class="language-plaintext highlighter-rouge">nftables.service</code> executa <code class="language-plaintext highlighter-rouge">nft flush ruleset</code> ao parar e ao recarregar (confira com <code class="language-plaintext highlighter-rouge">systemctl cat nftables</code>). Num nó em operação, um <code class="language-plaintext highlighter-rouge">systemctl restart nftables</code> apaga todas as regras da máquina, inclusive as do kube-proxy, até ele reescrever as suas. Se precisar mexer, prefira alterar o arquivo de regras com cuidado e validar com <code class="language-plaintext highlighter-rouge">nft -c -f</code> antes.</p>
</div>

<h3 id="parâmetros-de-sysctl-e-a-ponte-que-enxerga-o-firewall">Parâmetros de sysctl e a ponte que enxerga o firewall</h3>

<p>Chegamos à task que dá mais "ah, então era isso". Três parâmetros, aplicados por loop:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Trecho de ansible/k8s-cluster-base/tasks/01-instalacao.yml</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Configurar parâmetros de rede para Kubernetes</span>
  <span class="na">ansible.posix.sysctl</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">item.key</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">item.value</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">state</span><span class="pi">:</span> <span class="s">present</span>
    <span class="na">reload</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="na">loop</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="pi">{</span> <span class="nv">key</span><span class="pi">:</span> <span class="s2">"</span><span class="s">net.ipv4.ip_forward"</span><span class="pi">,</span> <span class="nv">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1"</span> <span class="pi">}</span>
    <span class="pi">-</span> <span class="pi">{</span> <span class="nv">key</span><span class="pi">:</span> <span class="s2">"</span><span class="s">net.bridge.bridge-nf-call-iptables"</span><span class="pi">,</span> <span class="nv">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1"</span> <span class="pi">}</span>
    <span class="pi">-</span> <span class="pi">{</span> <span class="nv">key</span><span class="pi">:</span> <span class="s2">"</span><span class="s">net.bridge.bridge-nf-call-ip6tables"</span><span class="pi">,</span> <span class="nv">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1"</span> <span class="pi">}</span>
</code></pre></div></div>

<p>O módulo <code class="language-plaintext highlighter-rouge">sysctl</code> <sup id="fnref:9"><a href="#fn:9" class="footnote" rel="footnote" role="doc-noteref">7</a></sup> do Ansible grava cada valor no <code class="language-plaintext highlighter-rouge">/etc/sysctl.conf</code>, para persistir entre boots, e recarrega as configurações logo em seguida (<code class="language-plaintext highlighter-rouge">reload: true</code>). Vamos por parâmetro:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Parâmetro</th>
      <th style="text-align: left">O que faz</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">net.ipv4.ip_forward = 1</code></td>
      <td style="text-align: left">Por padrão, o Linux descarta pacotes que chegam numa interface com destino a outra. O nó precisa encaminhar tráfego entre pods, nós e Services, então o roteamento precisa estar ligado. É um requisito documentado pelo Kubernetes <sup id="fnref:10"><a href="#fn:10" class="footnote" rel="footnote" role="doc-noteref">8</a></sup></td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">net.bridge.bridge-nf-call-iptables = 1</code></td>
      <td style="text-align: left">Faz quadros IPv4 que cruzam uma <em>bridge</em> passarem pelo firewall do kernel. Os plugins de rede <sup id="fnref:11"><a href="#fn:11" class="footnote" rel="footnote" role="doc-noteref">9</a></sup> esperam esse valor</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">net.bridge.bridge-nf-call-ip6tables = 1</code></td>
      <td style="text-align: left">O mesmo para IPv6. O laboratório é IPv4, mas a linha custa nada e evita surpresa se alguém ligar dual-stack</td>
    </tr>
  </tbody>
</table>

<p>Agora o "porquê" da segunda linha, que é a que menos gente entende. Uma <em>bridge</em> Linux é um <em>switch</em> virtual que opera na camada 2. Por padrão, o firewall do kernel só enxerga tráfego de camada 3, então quadros que passam de uma porta da <em>bridge</em> para outra seguem direto, sem nenhuma regra de NAT ou de conexão olhar para eles. O módulo <code class="language-plaintext highlighter-rouge">br_netfilter</code> muda isso, e o parâmetro <code class="language-plaintext highlighter-rouge">bridge-nf-call-iptables</code> liga o comportamento <sup id="fnref:12"><a href="#fn:12" class="footnote" rel="footnote" role="doc-noteref">10</a></sup>.</p>

<p>O cenário que quebra sem isso é um clássico de <em>hairpin</em>:</p>

<pre><code class="language-mermaid">flowchart TD
    CALL["&lt;b&gt;1. Pod A chama um Service&lt;/b&gt;&lt;br&gt;O destino é o IP virtual do Service"]
    DNAT["&lt;b&gt;2. Regra de NAT do kube-proxy&lt;/b&gt;&lt;ul&gt;&lt;li&gt;O destino vira o IP do Pod B&lt;/li&gt;&lt;li&gt;Pod B está no mesmo nó, na mesma bridge&lt;/li&gt;&lt;/ul&gt;"]
    OFF["&lt;b&gt;3a. Sem br_netfilter&lt;/b&gt;&lt;ul&gt;&lt;li&gt;A bridge entrega a resposta direto, sem passar pelo firewall&lt;/li&gt;&lt;li&gt;O conntrack não desfaz o NAT&lt;/li&gt;&lt;li&gt;Pod A recebe resposta de um IP que nunca contatou e descarta&lt;/li&gt;&lt;/ul&gt;"]
    ON["&lt;b&gt;3b. Com br_netfilter e sysctl em 1&lt;/b&gt;&lt;ul&gt;&lt;li&gt;A resposta passa pelo firewall do kernel&lt;/li&gt;&lt;li&gt;O conntrack reverte o NAT&lt;/li&gt;&lt;li&gt;Pod A enxerga a resposta vindo do IP do Service&lt;/li&gt;&lt;/ul&gt;"]

    CALL --&gt; DNAT
    DNAT --&gt; OFF
    DNAT --&gt; ON

    class CALL key;
    class DNAT neutral;
    class OFF failure;
    class ON success;
</code></pre>

<p>Sem o módulo, o Pod B responde diretamente ao Pod A pela <em>bridge</em>, sem que ninguém desfaça a tradução de endereço feita na ida. O Pod A espera uma resposta do IP do Service e recebe uma vinda do IP do Pod B. Descarta. A chamada simplesmente trava, e só quando o Service tem um <em>endpoint</em> no mesmo nó que quem chama. É o tipo de bug que "funciona na minha máquina" e quebra em produção quando o escalonador muda um pod de lugar.</p>

<p>Também por isso a ordem das tasks importa: as chaves <code class="language-plaintext highlighter-rouge">net.bridge.*</code> só existem no <code class="language-plaintext highlighter-rouge">/proc/sys</code> depois que o módulo <code class="language-plaintext highlighter-rouge">br_netfilter</code> é carregado. Rode o <code class="language-plaintext highlighter-rouge">sysctl</code> antes e o kernel responde <code class="language-plaintext highlighter-rouge">cannot stat /proc/sys/net/bridge/bridge-nf-call-iptables: No such file or directory</code>.</p>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> E com o Cilium, ainda precisa?</p><p>O Cilium, nosso CNI padrão, não usa uma <em>bridge</em> Linux para conectar os pods, e sim <em>veth pairs</em> com programas eBPF. Nessa configuração, os parâmetros de ponte têm pouco efeito. Mantemos mesmo assim por três motivos: a pilha alternativa <code class="language-plaintext highlighter-rouge">canal</code> (Flannel mais Calico) é uma opção real do projeto, via <code class="language-plaintext highlighter-rouge">plugin_cni</code>, e o Flannel usa <em>bridge</em>; a documentação do Kubernetes pede; e custa duas linhas.</p>
</div>

<h3 id="usuário-diretórios-e-os-cas">Usuário, diretórios e os CAs</h3>

<p>Os últimos passos da role preparam o terreno para os arquivos do cluster:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Trecho de ansible/k8s-cluster-base/tasks/01-instalacao.yml</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Criar usuário kubernetes</span>
  <span class="na">ansible.builtin.user</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">kubernetes</span>
    <span class="na">system</span><span class="pi">:</span> <span class="kc">true</span>
    <span class="na">shell</span><span class="pi">:</span> <span class="s">/usr/sbin/nologin</span>
    <span class="na">home</span><span class="pi">:</span> <span class="s">/var/lib/kubernetes</span>
    <span class="na">create_home</span><span class="pi">:</span> <span class="kc">false</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Criar diretório de configuração</span>
  <span class="na">ansible.builtin.file</span><span class="pi">:</span>
    <span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">item</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">state</span><span class="pi">:</span> <span class="s">directory</span>
    <span class="na">owner</span><span class="pi">:</span> <span class="s">root</span>
    <span class="na">group</span><span class="pi">:</span> <span class="s">kubernetes</span>
    <span class="na">mode</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0750"</span>
  <span class="na">loop</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">/etc/kubernetes</span>
    <span class="pi">-</span> <span class="s">/etc/kubernetes/pki</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Criar diretório de trabalho do Kubernetes</span>
  <span class="na">ansible.builtin.file</span><span class="pi">:</span>
    <span class="na">path</span><span class="pi">:</span> <span class="s">/var/lib/kubernetes</span>
    <span class="na">state</span><span class="pi">:</span> <span class="s">directory</span>
    <span class="na">owner</span><span class="pi">:</span> <span class="s">kubernetes</span>
    <span class="na">group</span><span class="pi">:</span> <span class="s">kubernetes</span>
    <span class="na">mode</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0700"</span>
</code></pre></div></div>

<p>A conta <code class="language-plaintext highlighter-rouge">kubernetes</code> é de sistema e não tem shell de login: ninguém entra por ela, ela só existe para ser dona de coisas. As permissões seguem o princípio do menor privilégio:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Diretório</th>
      <th style="text-align: left">Dono</th>
      <th style="text-align: left">Modo</th>
      <th style="text-align: left">Quem consegue entrar</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">/etc/kubernetes</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">root:kubernetes</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">0750</code></td>
      <td style="text-align: left">root e membros do grupo <code class="language-plaintext highlighter-rouge">kubernetes</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">/etc/kubernetes/pki</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">root:kubernetes</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">0750</code></td>
      <td style="text-align: left">root e membros do grupo <code class="language-plaintext highlighter-rouge">kubernetes</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">/var/lib/kubernetes</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">kubernetes:kubernetes</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">0700</code></td>
      <td style="text-align: left">apenas a conta <code class="language-plaintext highlighter-rouge">kubernetes</code> (e root)</td>
    </tr>
  </tbody>
</table>

<p>Nas próximas partes, você vai ver certificados e chaves nascendo com dono <code class="language-plaintext highlighter-rouge">root</code>, grupo <code class="language-plaintext highlighter-rouge">kubernetes</code> e modo <code class="language-plaintext highlighter-rouge">0640</code> ou <code class="language-plaintext highlighter-rouge">0644</code>. A ideia é dar um dono e um grupo reconhecíveis aos arquivos do cluster, em vez de largar tudo como <code class="language-plaintext highlighter-rouge">root:root</code>, e deixar o acesso de leitura sob controle de quem pertence ao grupo.</p>

<p>O arquivo <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/ansible/k8s-cluster-base/tasks/02-pki.yml">02-pki.yml</a> fecha a role. É o primeiro contato da PKI com os nós: ele copia, a partir da pasta de artefatos do host, os certificados dos três CAs intermediários (etcd, kubernetes e front-proxy) para <code class="language-plaintext highlighter-rouge">/etc/kubernetes/pki</code>, com modo <code class="language-plaintext highlighter-rouge">0644</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Trecho de ansible/k8s-cluster-base/tasks/02-pki.yml</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Copiar certificados dos CAs</span>
  <span class="na">ansible.builtin.copy</span><span class="pi">:</span>
    <span class="na">src</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">item.src</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">dest</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">item.dest</span><span class="nv"> </span><span class="s">}}"</span>
    <span class="na">owner</span><span class="pi">:</span> <span class="s">root</span>
    <span class="na">group</span><span class="pi">:</span> <span class="s">kubernetes</span>
    <span class="na">mode</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">item.mode</span><span class="nv"> </span><span class="s">}}"</span>
  <span class="na">loop</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">src</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">pki_cas.etcd.path_cert</span><span class="nv"> </span><span class="s">}}"</span>
      <span class="na">dest</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/etc/kubernetes/pki/{{</span><span class="nv"> </span><span class="s">pki_cas.etcd.path_cert</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">basename</span><span class="nv"> </span><span class="s">}}"</span>
      <span class="na">mode</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0644"</span>
    <span class="c1"># (...) o mesmo para o certificado do CA kubernetes e do CA front-proxy</span>
</code></pre></div></div>

<p>Os certificados são públicos, então <code class="language-plaintext highlighter-rouge">0644</code> é razoável. A geração das chaves, as autoridades certificadoras e as emissões de cada par são tratadas na role de PKI.</p>

<div class="markdown-alert markdown-alert-warning"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-alert" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg> Chaves de CA espalhadas pelos nós</p><p>A Root CA nunca sai do host, como vimos anteriormente. Mas a lista completa dessa task também copia as <strong>chaves privadas</strong> dos CAs intermediários <code class="language-plaintext highlighter-rouge">kubernetes</code> e <code class="language-plaintext highlighter-rouge">front-proxy</code> para todos os nós do grupo <code class="language-plaintext highlighter-rouge">cluster</code>, workers incluídos, com modo <code class="language-plaintext highlighter-rouge">0640</code> e grupo <code class="language-plaintext highlighter-rouge">kubernetes</code>. No laboratório descartável, o impacto é pequeno. Em produção, chave de CA fora do control plane é convite para problema: restrinja essas cópias aos managers ou, melhor, mantenha as chaves de assinatura longe das máquinas do cluster.</p>
</div>

<h2 id="executando-só-o-que-importa">Executando só o que importa</h2>

<p>Se você quiser rodar exatamente o que este post cobre, sem seguir para o kubelet e o resto do <em>control plane</em>, o comando é um conjunto de tags:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ ANSIBLE_CONFIG</span><span class="o">=</span>./ansible/.ansible.cfg ansible-playbook ./ansible/cluster.yml <span class="se">\</span>
    <span class="nt">--tags</span> cluster-artefatos,cluster-pki,cluster-sistema,cluster-kubernetes-base
</code></pre></div></div>

<p>As quatro tags correspondem às dependências que vimos: artefatos (baixa o Tailspin no host), PKI (gera os CAs), sistema (a <code class="language-plaintext highlighter-rouge">infra-sistema</code>) e <code class="language-plaintext highlighter-rouge">cluster-kubernetes-base</code> (a <code class="language-plaintext highlighter-rouge">k8s-cluster-base</code>). Pode haver um reboot no meio do caminho, quando o <code class="language-plaintext highlighter-rouge">needs-restarting</code> mandar. Se preferir o caminho completo de uma vez, <code class="language-plaintext highlighter-rouge">make k8s-in-a-box</code> faz tudo e você volta aqui para entender o que aconteceu.</p>

<h2 id="diagnóstico-e-troubleshooting">Diagnóstico e Troubleshooting</h2>

<p>Todos os comandos abaixo rodam do host, a partir da raiz do repositório, com as VMs ligadas (<code class="language-plaintext highlighter-rouge">make up</code>). A saída mostra o laboratório <code class="language-plaintext highlighter-rouge">mini</code> (um manager e dois workers), e a ordem dos hosts pode variar.</p>

<h3 id="swap-desligado">Swap desligado</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ ANSIBLE_CONFIG</span><span class="o">=</span>./ansible/.ansible.cfg ansible cluster <span class="nt">-m</span> <span class="nb">command</span> <span class="nt">-a</span> <span class="s2">"swapon --show"</span>
manager1 | CHANGED | <span class="nv">rc</span><span class="o">=</span>0 <span class="o">&gt;&gt;</span>

worker1 | CHANGED | <span class="nv">rc</span><span class="o">=</span>0 <span class="o">&gt;&gt;</span>

worker2 | CHANGED | <span class="nv">rc</span><span class="o">=</span>0 <span class="o">&gt;&gt;</span>
</code></pre></div></div>

<p><em>Saída vazia depois do <code class="language-plaintext highlighter-rouge">&gt;&gt;</code> é o resultado esperado: nenhum dispositivo de swap ativo. Para dupla checagem, <code class="language-plaintext highlighter-rouge">free -h</code> deve mostrar a linha de swap zerada.</em></p>

<h3 id="módulos-de-kernel-carregados">Módulos de kernel carregados</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ ANSIBLE_CONFIG</span><span class="o">=</span>./ansible/.ansible.cfg ansible cluster <span class="nt">-m</span> shell <span class="se">\</span>
    <span class="nt">-a</span> <span class="s2">"lsmod | awk '{print </span><span class="se">\$</span><span class="s2">1}' | grep -xE 'overlay|br_netfilter|vxlan'"</span>
</code></pre></div></div>

<p><em>Cada nó deve listar as três linhas, uma por módulo (a ordem varia). Se algum faltar, confira se o arquivo <code class="language-plaintext highlighter-rouge">/etc/modules-load.d/kubernetes.conf</code> existe e, em seguida, se a máquina reiniciou depois da instalação do kernel.</em></p>

<h3 id="parâmetros-de-sysctl">Parâmetros de sysctl</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ ANSIBLE_CONFIG</span><span class="o">=</span>./ansible/.ansible.cfg ansible cluster <span class="nt">-m</span> <span class="nb">command</span> <span class="se">\</span>
    <span class="nt">-a</span> <span class="s2">"sysctl net.ipv4.ip_forward net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-ip6tables"</span>
manager1 | CHANGED | <span class="nv">rc</span><span class="o">=</span>0 <span class="o">&gt;&gt;</span>
net.ipv4.ip_forward <span class="o">=</span> 1
net.bridge.bridge-nf-call-iptables <span class="o">=</span> 1
net.bridge.bridge-nf-call-ip6tables <span class="o">=</span> 1
</code></pre></div></div>

<p><em>Os três valores precisam ser <code class="language-plaintext highlighter-rouge">1</code> em todos os nós. Se o comando reclamar de <code class="language-plaintext highlighter-rouge">cannot stat</code>, o módulo <code class="language-plaintext highlighter-rouge">br_netfilter</code> não está carregado.</em></p>

<p>Para confirmar que os valores sobrevivem a um reboot, veja o que foi gravado no arquivo:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ ANSIBLE_CONFIG</span><span class="o">=</span>./ansible/.ansible.cfg ansible cluster <span class="nt">-m</span> shell <span class="se">\</span>
    <span class="nt">-a</span> <span class="s2">"grep -E 'ip_forward|bridge-nf' /etc/sysctl.conf"</span>
</code></pre></div></div>

<h3 id="selinux">SELinux</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ ANSIBLE_CONFIG</span><span class="o">=</span>./ansible/.ansible.cfg ansible todos <span class="nt">-m</span> <span class="nb">command</span> <span class="nt">-a</span> <span class="s2">"getenforce"</span>
manager1 | CHANGED | <span class="nv">rc</span><span class="o">=</span>0 <span class="o">&gt;&gt;</span>
Permissive
</code></pre></div></div>

<p><em>Todas as máquinas de <code class="language-plaintext highlighter-rouge">todos</code> devem responder <code class="language-plaintext highlighter-rouge">Permissive</code>. O <code class="language-plaintext highlighter-rouge">kubox</code> é checado separadamente, porque recebe a role por outro playbook.</em></p>

<h3 id="resolução-de-nomes">Resolução de nomes</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ ANSIBLE_CONFIG</span><span class="o">=</span>./ansible/.ansible.cfg ansible todos <span class="nt">-m</span> <span class="nb">command</span> <span class="nt">-a</span> <span class="s2">"getent hosts api.k8sbox.local"</span>
manager1 | CHANGED | <span class="nv">rc</span><span class="o">=</span>0 <span class="o">&gt;&gt;</span>
172.24.0.10     api.k8sbox.local etcd.k8sbox.local
</code></pre></div></div>

<p><em>Todas as máquinas precisam responder o IP do VIP. Lembre: use <code class="language-plaintext highlighter-rouge">getent</code>, não <code class="language-plaintext highlighter-rouge">nslookup</code>.</em></p>

<h3 id="usuário-diretórios-e-certificados-dos-cas">Usuário, diretórios e certificados dos CAs</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ ANSIBLE_CONFIG</span><span class="o">=</span>./ansible/.ansible.cfg ansible cluster <span class="nt">-b</span> <span class="nt">-m</span> <span class="nb">command</span> <span class="se">\</span>
    <span class="nt">-a</span> <span class="s2">"stat -c '%U:%G %a %n' /etc/kubernetes /etc/kubernetes/pki /var/lib/kubernetes"</span>
manager1 | CHANGED | <span class="nv">rc</span><span class="o">=</span>0 <span class="o">&gt;&gt;</span>
root:kubernetes 750 /etc/kubernetes
root:kubernetes 750 /etc/kubernetes/pki
kubernetes:kubernetes 700 /var/lib/kubernetes
</code></pre></div></div>

<p><em>O <code class="language-plaintext highlighter-rouge">-b</code> (become) é necessário porque os diretórios têm modo <code class="language-plaintext highlighter-rouge">0750</code> e o usuário <code class="language-plaintext highlighter-rouge">vagrant</code> não pertence ao grupo <code class="language-plaintext highlighter-rouge">kubernetes</code>.</em></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ ANSIBLE_CONFIG</span><span class="o">=</span>./ansible/.ansible.cfg ansible cluster <span class="nt">-b</span> <span class="nt">-m</span> shell <span class="se">\</span>
    <span class="nt">-a</span> <span class="s2">"stat -c '%a %U:%G %n' /etc/kubernetes/pki/*"</span>
manager1 | CHANGED | <span class="nv">rc</span><span class="o">=</span>0 <span class="o">&gt;&gt;</span>
644 root:kubernetes /etc/kubernetes/pki/ca-etcd-cert.pem
644 root:kubernetes /etc/kubernetes/pki/ca-front-proxy-cert.pem
640 root:kubernetes /etc/kubernetes/pki/ca-front-proxy-priv.pem
644 root:kubernetes /etc/kubernetes/pki/ca-k8s-cert.pem
640 root:kubernetes /etc/kubernetes/pki/ca-k8s-priv.pem
</code></pre></div></div>

<p><em>Se você pediu o mesmo comando a um worker, vai ver a saída idêntica, e as linhas <code class="language-plaintext highlighter-rouge">priv</code> são exatamente o alerta que fizemos há pouco.</em></p>

<p>Para a conta de sistema:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ ANSIBLE_CONFIG</span><span class="o">=</span>./ansible/.ansible.cfg ansible cluster <span class="nt">-m</span> shell <span class="se">\</span>
    <span class="nt">-a</span> <span class="s2">"getent passwd kubernetes | cut -d: -f1,6,7"</span>
manager1 | CHANGED | <span class="nv">rc</span><span class="o">=</span>0 <span class="o">&gt;&gt;</span>
kubernetes:/var/lib/kubernetes:/usr/sbin/nologin
</code></pre></div></div>

<h3 id="tailspin-instalado">Tailspin instalado</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ ANSIBLE_CONFIG</span><span class="o">=</span>./ansible/.ansible.cfg ansible todos <span class="nt">-m</span> shell <span class="nt">-a</span> <span class="s2">"command -v tspin"</span>
manager1 | CHANGED | <span class="nv">rc</span><span class="o">=</span>0 <span class="o">&gt;&gt;</span>
/usr/local/bin/tspin
</code></pre></div></div>

<h3 id="problemas-comuns">Problemas comuns</h3>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Sintoma</th>
      <th style="text-align: left">Causa provável</th>
      <th style="text-align: left">Solução</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">sysctl: cannot stat /proc/sys/net/bridge/bridge-nf-call-iptables: No such file or directory</code></td>
      <td style="text-align: left">O módulo <code class="language-plaintext highlighter-rouge">br_netfilter</code> não está carregado</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">sudo modprobe br_netfilter</code> e rodar de novo a tag <code class="language-plaintext highlighter-rouge">cluster-kubernetes-base</code></td>
    </tr>
    <tr>
      <td style="text-align: left">Play falha em "Copiar certificados dos CAs" com arquivo de origem inexistente</td>
      <td style="text-align: left">A PKI ainda não foi gerada no host</td>
      <td style="text-align: left">Rodar antes a tag <code class="language-plaintext highlighter-rouge">cluster-pki</code> (ou o <code class="language-plaintext highlighter-rouge">make infra</code>)</td>
    </tr>
    <tr>
      <td style="text-align: left">Play falha ao copiar o Tailspin com arquivo de origem inexistente</td>
      <td style="text-align: left">O download no host ainda não aconteceu</td>
      <td style="text-align: left">Rodar antes a tag <code class="language-plaintext highlighter-rouge">cluster-artefatos</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">modprobe: FATAL: Module xt_conntrack not found in directory /lib/modules/...</code></td>
      <td style="text-align: left">O kernel em execução não é o mesmo dos módulos instalados</td>
      <td style="text-align: left">Comparar <code class="language-plaintext highlighter-rouge">uname -r</code> com <code class="language-plaintext highlighter-rouge">rpm -q kernel-core kernel-modules-extra</code> e reiniciar a VM</td>
    </tr>
    <tr>
      <td style="text-align: left">O Ansible reclama de <em>timeout</em> depois do reboot</td>
      <td style="text-align: left">A VM demorou mais que os 300 segundos do handler, ou não voltou</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">vagrant status</code> e <code class="language-plaintext highlighter-rouge">virsh list --all</code>; se travou, <code class="language-plaintext highlighter-rouge">vagrant reload &lt;vm&gt;</code></td>
    </tr>
    <tr>
      <td style="text-align: left">O swap reaparece depois de reiniciar</td>
      <td style="text-align: left">Swap vindo de outra fonte que não o <code class="language-plaintext highlighter-rouge">/etc/fstab</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">systemctl list-units --type=swap</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">nslookup api.k8sbox.local</code> diz que o nome não existe, mas as aplicações resolvem</td>
      <td style="text-align: left">O <code class="language-plaintext highlighter-rouge">nslookup</code> ignora o <code class="language-plaintext highlighter-rouge">/etc/hosts</code></td>
      <td style="text-align: left">Testar com <code class="language-plaintext highlighter-rouge">getent hosts api.k8sbox.local</code></td>
    </tr>
    <tr>
      <td style="text-align: left">As regras de rede somem depois de mexer no serviço</td>
      <td style="text-align: left">O <code class="language-plaintext highlighter-rouge">nftables.service</code> roda <code class="language-plaintext highlighter-rouge">flush ruleset</code> ao parar e recarregar</td>
      <td style="text-align: left">Evitar <code class="language-plaintext highlighter-rouge">restart</code> em nós ativos; conferir com <code class="language-plaintext highlighter-rouge">systemctl cat nftables</code></td>
    </tr>
  </tbody>
</table>

<h2 id="conclusão">Conclusão</h2>

<p>Neste ponto, as VMs deixaram de ser servidores genéricos e viraram candidatas a nó. Todas se enxergam pelo nome, os nós têm memória de verdade, o kernel sabe rotear e deixa o firewall espiar as pontes, e existem uma conta, três diretórios e três certificados de CA esperando o resto da família chegar.</p>

<p>O que <strong>não</strong> existe ainda é o mais importante: nenhum binário do Kubernetes, nenhum serviço do cluster rodando, nenhum certificado de componente. Só a fundação, que ninguém elogia e todo mundo sente quando falta.</p>

<p>Dá para perceber que esses três CAs apareceram sem muita explicação. Chaves, CSRs, cadeias de confiança, mTLS e SANs parecem vocabulário puramente acadêmico até virarem a causa de um nó que se recusa a entrar no cluster. No próximo post (<strong>Parte 5: Certificados e a Infraestrutura de PKI</strong> <em>(em breve)</em>), vamos parar de rodar automações e dissecar, com calma e sem código de role, por que componentes distribuídos precisam provar quem são e como a PKI sustenta toda a segurança do Kubernetes.</p>

<h2 id="referências">Referências</h2>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p><strong>Repositório k8s-in-a-box</strong> {<em>GitHub</em>} (<a href="https://github.com/vndmtrx/k8s-in-a-box">Link</a>) <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3">
      <p><strong>ansible.posix.selinux module</strong> {<em>Ansible Documentation</em>} (<a href="https://docs.ansible.com/ansible/latest/collections/ansible/posix/selinux_module.html">Link</a>) <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4">
      <p><strong>Tailspin: a log file highlighter</strong> {<em>GitHub</em>} (<a href="https://github.com/bensadeh/tailspin">Link</a>) <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:6">
      <p><strong>community.general.modprobe module</strong> {<em>Ansible Documentation</em>} (<a href="https://docs.ansible.com/ansible/latest/collections/community/general/modprobe_module.html">Link</a>) <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:7">
      <p><strong>Kubelet Configuration (v1beta1)</strong> {<em>Kubernetes Documentation</em>} (<a href="https://kubernetes.io/docs/reference/config-api/kubelet-config.v1beta1/">Link</a>) <a href="#fnref:7" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:8">
      <p><strong>Virtual IPs and Service Proxies</strong> {<em>Kubernetes Documentation</em>} (<a href="https://kubernetes.io/docs/reference/networking/virtual-ips/">Link</a>) <a href="#fnref:8" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:9">
      <p><strong>ansible.posix.sysctl module</strong> {<em>Ansible Documentation</em>} (<a href="https://docs.ansible.com/ansible/latest/collections/ansible/posix/sysctl_module.html">Link</a>) <a href="#fnref:9" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:10">
      <p><strong>Container Runtimes</strong> {<em>Kubernetes Documentation</em>} (<a href="https://kubernetes.io/docs/setup/production-environment/container-runtimes/">Link</a>) <a href="#fnref:10" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:11">
      <p><strong>Network Plugins</strong> {<em>Kubernetes Documentation</em>} (<a href="https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/">Link</a>) <a href="#fnref:11" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:12">
      <p><strong>Ebtables/Iptables interaction on a Linux-based bridge</strong> {<em>netfilter.org</em>} (<a href="https://ebtables.netfilter.org/documentation/bridge-nf.html">Link</a>) <a href="#fnref:12" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>[&quot;Eduardo N. S. R.&quot;]</name></author><category term="Tutoriais" /><category term="Kubernetes" /><category term="Ansible" /><category term="DevOps" /><category term="Infraestrutura" /><category term="Linux" /><summary type="html"><![CDATA[Uma VM AlmaLinux recém-saída do vagrant up é um ótimo servidor genérico: quieta, previsível e configurada para aplicações comuns. Ela não repassa pacotes entre interfaces, não deixa o firewall inspecionar o tráfego das pontes de rede, convive com swap sem remorso e nem sequer conhece os nomes das outras máquinas do laboratório. Para quem vai virar nó de Kubernetes, cada uma dessas características é um problema que precisa ser resolvido.]]></summary></entry><entry><title type="html">Fundamentos e Anatomia de Contêineres</title><link href="https://vndmtrx.github.io/posts/conteineres-fundamentos/" rel="alternate" type="text/html" title="Fundamentos e Anatomia de Contêineres" /><published>2026-10-05T11:26:00+00:00</published><updated>2026-10-05T11:26:00+00:00</updated><id>https://vndmtrx.github.io/posts/conteineres-fundamentos</id><content type="html" xml:base="https://vndmtrx.github.io/posts/conteineres-fundamentos/"><![CDATA[<p>Você já rodou <code class="language-plaintext highlighter-rouge">docker run</code> centenas de vezes. Digita o comando, aperta enter, um punhado de letrinhas corre pelo terminal e, em questão de meio segundo, você tem uma aplicação inteira rodando em um ambiente isolado com rede, disco e variáveis próprias. Mas se eu te perguntar exatamente o que o kernel Linux fez naquele exato instante de meio segundo, você sabe responder?</p>

<p>A maioria dos desenvolvedores e operadores de infraestrutura aprendeu a usar contêineres como se fossem "máquinas virtuais mais leves e rápidas". Essa metáfora quebra galho para começar, mas ela é conceitualmente falsa. Contêineres não são máquinas virtuais. Não existe um hipervisor emulando placa-mãe, processador ou controladora SCSI. Não existe um segundo kernel dando <em>boot</em> silencioso no fundo da sua máquina. O que existe é um processo comum, rodando no mesmo kernel que o seu editor de texto e o seu navegador, só que usando uma coleira extremamente sofisticada fornecida pelo próprio sistema operacional.</p>

<p>Entender essa coleira é o divisor de águas entre o profissional que só sabe colar comandos do Stack Overflow e aquele que realmente resolve problemas quando o bicho pega em produção. Quando uma aplicação estoura o limite de memória e é abatida sem piedade pelo sistema operacional, quando duas instâncias brigam pela mesma porta de rede, ou quando você precisa projetar um cluster distribuído com Kubernetes, a abstração bonitinha do Docker desaparece. O que sobra é o kernel puro.</p>

<p>Este conhecimento também é o alicerce fundamental para a nossa série sobre criação de clusters locais com o projeto <a href="/posts/k8sbox-visao-geral/">Kubernetes in a Box</a> e para a etapa de empacotamento que veremos na Parte 14 da série <a href="/posts/spring-boot-tutorial-parte-1-ambiente/">Spring Boot Tutorial</a>. Sem dominar o que é um contêiner por baixo do capô, tentar entender a arquitetura de <em>Pods</em>, <em>Container Network Interface</em> (CNI) ou <em>Container Runtime Interface</em> (CRI) vira mero exercício de decoreba.</p>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Nota da Série</p><p>Este post é a Parte 1 da série <strong>"Contêineres de Cabo a Rabo"</strong>. Se você quer entender como chegamos até aqui, leia primeiro o <a href="/posts/conteineres-historia/">Post Zero: A História dos Contêineres</a>. Aqui vamos dissecar a anatomia interna dos contêineres e explorar as três tecnologias fundamentais do kernel Linux: <em>namespaces</em>, <em>cgroups</em> e <em>union filesystems</em>, fechando com a construção de um mini-contêiner do zero em apenas 50 linhas de shell script. Na Parte 2, vamos subir o nível para as ferramentas de produção: Dockerfiles avançados, Docker Compose, arquitetura sem daemon do Podman, segurança e o caminho para a orquestração.</p>
</div>

<details class="toc-box">
  <summary><strong>Sumário &amp; Índice de Seções (TOC)</strong></summary>
<ul id="markdown-toc">
  <li><a href="#contêiner-não-é-vm-a-distinção-que-muda-tudo" id="markdown-toc-contêiner-não-é-vm-a-distinção-que-muda-tudo">Contêiner Não é VM: A Distinção que Muda Tudo</a></li>
  <li><a href="#namespaces-os-muros-de-isolamento-do-kernel" id="markdown-toc-namespaces-os-muros-de-isolamento-do-kernel">Namespaces: Os Muros de Isolamento do Kernel</a>    <ul>
      <li><a href="#isolamento-de-processos-o-namespace-pid" id="markdown-toc-isolamento-de-processos-o-namespace-pid">Isolamento de Processos: O Namespace PID</a></li>
      <li><a href="#isolamento-de-rede-o-namespace-net" id="markdown-toc-isolamento-de-rede-o-namespace-net">Isolamento de Rede: O Namespace NET</a></li>
      <li><a href="#isolamento-de-hostname-o-namespace-uts" id="markdown-toc-isolamento-de-hostname-o-namespace-uts">Isolamento de Hostname: O Namespace UTS</a></li>
      <li><a href="#inspecionando-os-namespaces-no-próprio-linux" id="markdown-toc-inspecionando-os-namespaces-no-próprio-linux">Inspecionando os Namespaces no Próprio Linux</a></li>
      <li><a href="#o-segredo-do-comando-docker-exec-nsenter" id="markdown-toc-o-segredo-do-comando-docker-exec-nsenter">O Segredo do Comando docker exec: nsenter</a></li>
    </ul>
  </li>
  <li><a href="#cgroups-o-guarda-de-recursos" id="markdown-toc-cgroups-o-guarda-de-recursos">Cgroups: O Guarda de Recursos</a>    <ul>
      <li><a href="#a-grande-mudança-cgroups-v1-versus-cgroups-v2" id="markdown-toc-a-grande-mudança-cgroups-v1-versus-cgroups-v2">A Grande Mudança: Cgroups v1 versus Cgroups v2</a></li>
      <li><a href="#criando-e-testando-um-cgroup-manualmente" id="markdown-toc-criando-e-testando-um-cgroup-manualmente">Criando e Testando um Cgroup Manualmente</a></li>
      <li><a href="#o-guarda-em-ação-o-assassino-oom-out-of-memory-killer" id="markdown-toc-o-guarda-em-ação-o-assassino-oom-out-of-memory-killer">O Guarda em Ação: O Assassino OOM (Out of Memory Killer)</a></li>
    </ul>
  </li>
  <li><a href="#union-filesystems-imagens-em-camadas" id="markdown-toc-union-filesystems-imagens-em-camadas">Union Filesystems: Imagens em Camadas</a>    <ul>
      <li><a href="#a-mecânica-do-copy-on-write-cow" id="markdown-toc-a-mecânica-do-copy-on-write-cow">A Mecânica do Copy-on-Write (CoW)</a></li>
      <li><a href="#montando-um-overlayfs-na-mão" id="markdown-toc-montando-um-overlayfs-na-mão">Montando um OverlayFS na Mão</a></li>
    </ul>
  </li>
  <li><a href="#a-especificação-oci-e-o-ecossistema-de-runtimes" id="markdown-toc-a-especificação-oci-e-o-ecossistema-de-runtimes">A Especificação OCI e o Ecossistema de Runtimes</a>    <ul>
      <li><a href="#a-cadeia-de-execução-moderna" id="markdown-toc-a-cadeia-de-execução-moderna">A Cadeia de Execução Moderna</a></li>
      <li><a href="#o-raio-x-do-configjson-da-oci" id="markdown-toc-o-raio-x-do-configjson-da-oci">O Raio-X do config.json da OCI</a></li>
    </ul>
  </li>
  <li><a href="#grand-finale-construindo-um-mini-contêiner-do-zero" id="markdown-toc-grand-finale-construindo-um-mini-contêiner-do-zero">Grand Finale: Construindo um Mini-Contêiner do Zero</a>    <ul>
      <li><a href="#entendendo-a-mecânica-passo-a-passo" id="markdown-toc-entendendo-a-mecânica-passo-a-passo">Entendendo a Mecânica Passo a Passo</a></li>
    </ul>
  </li>
  <li><a href="#exercícios" id="markdown-toc-exercícios">Exercícios</a></li>
  <li><a href="#o-que-vem-na-parte-2" id="markdown-toc-o-que-vem-na-parte-2">O Que Vem na Parte 2</a></li>
  <li><a href="#referências" id="markdown-toc-referências">Referências</a></li>
</ul>

</details>

<h2 id="contêiner-não-é-vm-a-distinção-que-muda-tudo">Contêiner Não é VM: A Distinção que Muda Tudo</h2>

<p>Para consolidar o funcionamento prático, precisamos sepultar de vez a ideia de que um contêiner é uma máquina virtual em miniatura.</p>

<p>Imagine a estrutura física de moradia. Uma máquina virtual é como uma <strong>casa isolada em seu próprio lote de terreno</strong>. Essa casa tem fundações de concreto próprias, caixa d'água exclusiva, gerador elétrico individual e seu próprio quadro de disjuntores. Se o encanamento dessa casa arrebentar, o problema fica restrito a ela. Em contrapartida, construir essa estrutura exige muito material, ocupa um espaço físico enorme e custa caro para manter aquecida.</p>

<p>Um contêiner é como um <strong>apartamento dentro de um edifício moderno</strong>. O apartamento tem paredes sólidas, fechadura na porta e total privacidade interior: os vizinhos não enxergam o que você faz dentro da sua sala. Porém, o apartamento compartilha toda a infraestrutura estrutural do edifício: a fundação de sustentação, os prumadas hidráulicas e as colunas de cabos elétricos. Essa infraestrutura compartilhada é o <strong>kernel Linux</strong>. Se a fundação de concreto do prédio sofrer um abalo estrutural gravíssimo (uma falha catastrófica de pânico no kernel), todos os apartamentos sentem o impacto imediatamente.</p>

<p>Em termos de arquitetura de computadores, a virtualização tradicional funciona por meio de um <strong>hipervisor</strong> (<em>hypervisor</em>). Em hipervisores do Tipo 1 (como VMware ESXi, KVM e Xen) ou do Tipo 2 (como VirtualBox e VMware Workstation), o software cria uma camada de emulação de hardware completa. Cada máquina virtual convidada (<em>guest</em>) acredita piamente que tem processadores x86 exclusivos, barramentos PCI, placas de rede Realtek ou Intel e controladoras de disco dedicadas. Por conta disso, cada máquina virtual precisa carregar um sistema operacional convidado completo: rodar o processo de inicialização (<em>bootloader</em>), subir um kernel próprio, inicializar serviços de sistema (como o <em>systemd</em>) e carregar drivers. Isso consome centenas de megabytes de memória RAM apenas para manter a máquina em espera, além de exigir minutos preciosos para reiniciar.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        VIRTUALIZAÇÃO (VM)                           CONTÊINERES

┌─────────────────────────────────┐       ┌─────────────────────────────────┐
│  App A   │   App B   │   App C  │       │  App A   │   App B   │   App C  │
├──────────┼───────────┼──────────┤       ├──────────┼───────────┼──────────┤
│ Libs /   │  Libs /   │  Libs /  │       │ Libs /   │  Libs /   │  Libs /  │
│ Bins     │  Bins     │  Bins    │       │ Bins     │  Bins     │  Bins    │
├──────────┼───────────┼──────────┤       ├──────────┴───────────┴──────────┤
│ Guest OS │ Guest OS  │ Guest OS │       │        Container Runtime        │
│ (Kernel) │ (Kernel)  │ (Kernel) │       │     (containerd, runc, crun)    │
├──────────┴───────────┴──────────┤       ├─────────────────────────────────┤
│           Hypervisor            │       │       Sistema Operacional       │
│       (KVM, Xen, VMware)        │       │       Host (Kernel Linux)       │
├─────────────────────────────────┤       ├─────────────────────────────────┤
│         Hardware Físico         │       │         Hardware Físico         │
└─────────────────────────────────┘       └─────────────────────────────────┘
</code></pre></div></div>

<p>Nos contêineres, essa camada de emulação de hardware simplesmente não existe. Quando você inicia um contêiner, o runtime não pede para uma CPU virtual dar boot. Ele simplesmente faz chamadas de sistema normais do Linux (como <code class="language-plaintext highlighter-rouge">clone</code> e <code class="language-plaintext highlighter-rouge">execve</code>) para criar um novo processo comum no sistema hospedeiro.</p>

<p>A diferença reside no fato de que esse novo processo recebe restrições estritas do kernel sobre o que ele pode visualizar e quantos recursos ele pode consumir. O tempo de inicialização cai de quarenta segundos para dez milissegundos porque criar um contêiner tem exatamente o mesmo custo computacional que iniciar qualquer programa na linha de comando.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Característica</th>
      <th style="text-align: left">Máquina Virtual Tradicional</th>
      <th style="text-align: left">Contêiner Linux</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>Tempo de Inicialização</strong></td>
      <td style="text-align: left">Dezenas de segundos a minutos</td>
      <td style="text-align: left">Poucos milissegundos</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Overhead de Memória</strong></td>
      <td style="text-align: left">Alto (kernel próprio + daemons do SO convidado)</td>
      <td style="text-align: left">Mínimo (apenas o consumo real da aplicação)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Arquitetura de Isolamento</strong></td>
      <td style="text-align: left">Hardware emulado por hipervisor</td>
      <td style="text-align: left">Recursos isolados e limitados pelo mesmo kernel</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Tamanho dos Artefatos</strong></td>
      <td style="text-align: left">Gigabytes (imagens de disco completas)</td>
      <td style="text-align: left">Megabytes (apenas binários e bibliotecas extras)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Densidade Operacional</strong></td>
      <td style="text-align: left">Dezenas de VMs por servidor físico</td>
      <td style="text-align: left">Centenas a milhares de contêineres por servidor</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Heterogeneidade de Kernel</strong></td>
      <td style="text-align: left">Total (Linux roda sobre Windows, BSD sobre Linux)</td>
      <td style="text-align: left">Inexistente (compartilha estritamente o kernel host)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Superfície de Ataque</strong></td>
      <td style="text-align: left">Menor (barreira física de hardware emulado)</td>
      <td style="text-align: left">Maior (compartilhamento direto de chamadas ao kernel)</td>
    </tr>
  </tbody>
</table>

<p>Essa tabela deixa claro o que um contêiner realmente é: o resultado de três tecnologias nativas do kernel Linux trabalhando em uníssono. Os <strong>namespaces</strong> criam as paredes visuais de isolamento, os <strong>cgroups</strong> agem como o medidor de consumo de recursos, e os <strong>union filesystems</strong> estruturam o sistema de arquivos em camadas. Vamos analisar detalhadamente cada uma dessas peças.</p>

<h2 id="namespaces-os-muros-de-isolamento-do-kernel">Namespaces: Os Muros de Isolamento do Kernel</h2>

<p>Se um contêiner é apenas um processo comum rodando no mesmo kernel do hospedeiro, surge a pergunta inevitável: como uma aplicação dentro do contêiner não enxerga os outros processos do servidor, não escuta conexões nas interfaces de rede de outras aplicações e não acessa os arquivos das outras pastas?</p>

<p>A resposta atende pelo nome de <strong>namespaces</strong> <sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>.</p>

<p>Um <em>namespace</em> é uma camada de abstração do kernel Linux que envelopa recursos globais do sistema em partições isoladas. Quando um processo é colocado dentro de um namespace, ele passa a enxergar uma visão filtrada e privativa daquele recurso específico. Para todos os efeitos práticos, aquele processo tem a ilusão completa de que é a única entidade existente em toda a máquina.</p>

<p>O kernel Linux disponibiliza atualmente oito tipos diferentes de namespaces, cada um responsável por fatiar um subsistema específico:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Namespace</th>
      <th style="text-align: left">Flag no Kernel</th>
      <th style="text-align: left">Recurso Isolado</th>
      <th style="text-align: left">Kernel Inicial</th>
      <th style="text-align: left">Ano de Introdução</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>MNT</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">CLONE_NEWNS</code></td>
      <td style="text-align: left">Pontos de montagem do sistema de arquivos</td>
      <td style="text-align: left">2.4.19</td>
      <td style="text-align: left">2002</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>UTS</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">CLONE_NEWUTS</code></td>
      <td style="text-align: left">Nome da máquina (<em>hostname</em>) e domínio NIS</td>
      <td style="text-align: left">2.6.19</td>
      <td style="text-align: left">2006</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>IPC</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">CLONE_NEWIPC</code></td>
      <td style="text-align: left">Filas de mensagens POSIX/SysV e memória compartilhada</td>
      <td style="text-align: left">2.6.19</td>
      <td style="text-align: left">2006</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>PID</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">CLONE_NEWPID</code></td>
      <td style="text-align: left">Árvore de identificadores de processos</td>
      <td style="text-align: left">2.6.24</td>
      <td style="text-align: left">2008</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>NET</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">CLONE_NEWNET</code></td>
      <td style="text-align: left">Interfaces de rede, rotas, regras de firewall e portas</td>
      <td style="text-align: left">2.6.29</td>
      <td style="text-align: left">2009</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>USER</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">CLONE_NEWUSER</code></td>
      <td style="text-align: left">Mapeamento de identificadores de usuário e grupo (UID/GID)</td>
      <td style="text-align: left">3.8</td>
      <td style="text-align: left">2013</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>CGROUP</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">CLONE_NEWCGROUP</code></td>
      <td style="text-align: left">Visão da própria hierarquia do pseudo-sistema cgroup</td>
      <td style="text-align: left">4.6</td>
      <td style="text-align: left">2016</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>TIME</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">CLONE_NEWTIME</code></td>
      <td style="text-align: left">Relógios de inicialização e contadores de tempo monotônico</td>
      <td style="text-align: left">5.6</td>
      <td style="text-align: left">2020</td>
    </tr>
  </tbody>
</table>

<p>A criação e manipulação desses muros é realizada por três chamadas de sistema fundamentais: <code class="language-plaintext highlighter-rouge">clone()</code> (que aceita as flags <code class="language-plaintext highlighter-rouge">CLONE_NEW*</code> para criar um processo filho já nascido em novos namespaces), <code class="language-plaintext highlighter-rouge">unshare()</code> (que desconecta o processo atual de seus namespaces herdados) e <code class="language-plaintext highlighter-rouge">setns()</code> (que anexa um processo em execução a um namespace preexistente).</p>

<p>Vamos deixar a teoria de lado e exercitar essas chamadas diretamente no terminal.</p>

<h3 id="isolamento-de-processos-o-namespace-pid">Isolamento de Processos: O Namespace PID</h3>

<p>No Linux, todo processo possui um número identificador único chamado PID. O primeiro processo inicializado pelo kernel ao ligar a máquina recebe o PID 1 (tradicionalmente o <code class="language-plaintext highlighter-rouge">/sbin/init</code> ou o <code class="language-plaintext highlighter-rouge">systemd</code>). Todos os outros processos do sistema são descendentes diretos ou indiretos dele.</p>

<p>Quando você isola o namespace PID, o primeiro processo disparado dentro do novo ambiente recebe o PID 1 privativo. Ele se torna o processo pai de todas as tarefas daquele espaço. Ao mesmo tempo, ele perde totalmente a capacidade de enxergar ou enviar sinais de encerramento (<code class="language-plaintext highlighter-rouge">kill</code>) para qualquer processo que esteja no hospedeiro.</p>

<p>Vamos abrir uma sessão isolada usando o comando utilitário <code class="language-plaintext highlighter-rouge">unshare</code> <sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># No host: conferir a quantidade massiva de processos normais</span>
<span class="nv">$ </span>ps aux | <span class="nb">head</span> <span class="nt">-n</span> 5
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0 169592 13092 ?        Ss   09:00   0:01 /sbin/init
root         2  0.0  0.0      0     0 ?        S    09:00   0:00 <span class="o">[</span>kthreadd]
root         3  0.0  0.0      0     0 ?        I&lt;   09:00   0:00 <span class="o">[</span>rcu_gp]
root         4  0.0  0.0      0     0 ?        I&lt;   09:00   0:00 <span class="o">[</span>rcu_par_gp]

<span class="c"># Criar um namespace PID isolado disparando um novo shell bash</span>
<span class="nv">$ </span><span class="nb">sudo </span>unshare <span class="nt">--pid</span> <span class="nt">--fork</span> <span class="nt">--mount-proc</span> bash

<span class="c"># Dentro do novo ambiente: listar os processos ativos</span>
<span class="nv">$ </span>ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0   8536  5312 pts/0    S    14:00   0:00 bash
root         2  0.0  0.0  10072  3456 pts/0    R+   14:00   0:00 ps aux
</code></pre></div></div>
<p><em>A flag <code class="language-plaintext highlighter-rouge">--mount-proc</code> remonta o sistema de arquivos <code class="language-plaintext highlighter-rouge">/proc</code> no novo contexto; sem ela, o utilitário ps continuaria lendo a tabela de processos do host.</em></p>

<p>Repare no detalhe da chamada: a flag <code class="language-plaintext highlighter-rouge">--fork</code> é estritamente necessária porque a chamada de sistema <code class="language-plaintext highlighter-rouge">unshare(CLONE_NEWPID)</code> afeta apenas os processos filhos criados posteriormente. Para que o seu shell assuma o papel de PID 1 dentro do novo espaço, o utilitário precisa criar um processo filho bifurcado (<em>forked</em>).</p>

<h3 id="isolamento-de-rede-o-namespace-net">Isolamento de Rede: O Namespace NET</h3>

<p>O namespace de rede isola todos os artefatos relacionados à comunicação: tabelas de roteamento, regras de filtragem (<em>iptables</em> e <em>nftables</em>), sockets abertos e interfaces de rede físicas ou lógicas.</p>

<p>Quando um novo namespace NET é instanciado, ele nasce como um deserto digital:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># No hospedeiro: listar interfaces existentes</span>
<span class="nv">$ </span>ip <span class="nb">link
</span>1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    <span class="nb">link</span>/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp3s0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    <span class="nb">link</span>/ether bc:24:11:43:82:aa brd ff:ff:ff:ff:ff:ff
3: docker0: &lt;NO-CARRIER,BROADCAST,MULTICAST,UP&gt; mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default
    <span class="nb">link</span>/ether 02:42:1a:de:99:11 brd ff:ff:ff:ff:ff:ff

<span class="c"># Disparar um namespace de rede isolado</span>
<span class="nv">$ </span><span class="nb">sudo </span>unshare <span class="nt">--net</span> bash

<span class="c"># Verificar as interfaces disponíveis no novo contexto</span>
<span class="nv">$ </span>ip <span class="nb">link
</span>1: lo: &lt;LOOPBACK&gt; mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    <span class="nb">link</span>/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

<span class="c"># Tentar consultar tabelas de roteamento</span>
<span class="nv">$ </span>ip route
<span class="c"># (absolutamente nada retornado)</span>
</code></pre></div></div>
<p><em>O namespace de rede nasce isolado do mundo exterior, contendo exclusivamente a interface de loopback em estado desligado.</em></p>

<p>É exatamente por essa razão que um contêiner Docker recém-criado não interfere nas portas do seu computador hospedeiro. Se uma aplicação subir escutando na porta 80 dentro do contêiner, essa porta fica aberta apenas dentro do seu namespace de rede privativo. Para expô-la ao mundo exterior, ferramentas como o Docker criam pares de interfaces virtuais conectadas por um cabo de rede virtual imaginário (<em>veth pair</em>), plugando uma ponta dentro do contêiner e a outra em uma ponte de rede (<em>bridge</em>) no hospedeiro.</p>

<h3 id="isolamento-de-hostname-o-namespace-uts">Isolamento de Hostname: O Namespace UTS</h3>

<p>O namespace UTS (cujo nome histórico vem de <em>UNIX Timesharing System</em>) permite que um grupo de processos tenha seu próprio nome de máquina (<em>hostname</em>) e domínio de rede sem impactar o resto do sistema.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Consultar nome original do host</span>
<span class="nv">$ </span><span class="nb">hostname
</span>meu-servidor-producao

<span class="c"># Criar namespace UTS e trocar o nome localmente</span>
<span class="nv">$ </span><span class="nb">sudo </span>unshare <span class="nt">--uts</span> bash
<span class="nv">$ </span><span class="nb">hostname </span>conteiner-temporario
<span class="nv">$ </span><span class="nb">hostname
</span>conteiner-temporario

<span class="c"># Sair da sessão isolada e conferir o host original</span>
<span class="nv">$ </span><span class="nb">exit</span>
<span class="nv">$ </span><span class="nb">hostname
</span>meu-servidor-producao
</code></pre></div></div>
<p><em>O hostname configurado dentro da sessão isolada desaparece ao sair, mantendo a configuração do hospedeiro intacta.</em></p>

<h3 id="inspecionando-os-namespaces-no-próprio-linux">Inspecionando os Namespaces no Próprio Linux</h3>

<p>Toda essa mágica não fica escondida em estruturas de dados misteriosas. O Linux expõe os namespaces de qualquer tarefa em execução através do pseudo-sistema de arquivos <code class="language-plaintext highlighter-rouge">/proc</code>.</p>

<p>Se você listar a pasta <code class="language-plaintext highlighter-rouge">/proc/&lt;PID&gt;/ns/</code>, verá apontadores simbólicos especiais:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Inspecionar os namespaces do seu próprio terminal</span>
<span class="nv">$ </span><span class="nb">ls</span> <span class="nt">-la</span> /proc/self/ns/
total 0
dr-x--x--x 2 usuario usuario 0 <span class="nb">set  </span>8 14:00 <span class="nb">.</span>
dr-xr-xr-x 9 usuario usuario 0 <span class="nb">set  </span>8 14:00 ..
lrwxrwxrwx 1 usuario usuario 0 <span class="nb">set  </span>8 14:00 cgroup -&gt; <span class="s1">'cgroup:[4026531835]'</span>
lrwxrwxrwx 1 usuario usuario 0 <span class="nb">set  </span>8 14:00 ipc -&gt; <span class="s1">'ipc:[4026531839]'</span>
lrwxrwxrwx 1 usuario usuario 0 <span class="nb">set  </span>8 14:00 mnt -&gt; <span class="s1">'mnt:[4026531841]'</span>
lrwxrwxrwx 1 usuario usuario 0 <span class="nb">set  </span>8 14:00 net -&gt; <span class="s1">'net:[4026531840]'</span>
lrwxrwxrwx 1 usuario usuario 0 <span class="nb">set  </span>8 14:00 pid -&gt; <span class="s1">'pid:[4026531836]'</span>
lrwxrwxrwx 1 usuario usuario 0 <span class="nb">set  </span>8 14:00 pid_for_children -&gt; <span class="s1">'pid:[4026531836]'</span>
lrwxrwxrwx 1 usuario usuario 0 <span class="nb">set  </span>8 14:00 <span class="nb">time</span> -&gt; <span class="s1">'time:[4026531834]'</span>
lrwxrwxrwx 1 usuario usuario 0 <span class="nb">set  </span>8 14:00 time_for_children -&gt; <span class="s1">'time:[4026531834]'</span>
lrwxrwxrwx 1 usuario usuario 0 <span class="nb">set  </span>8 14:00 user -&gt; <span class="s1">'user:[4026531837]'</span>
lrwxrwxrwx 1 usuario usuario 0 <span class="nb">set  </span>8 14:00 uts -&gt; <span class="s1">'uts:[4026531838]'</span>
</code></pre></div></div>
<p><em>Os números entre colchetes representam o identificador de inode do namespace no kernel; dois processos compartilham o mesmo namespace se os números de inode forem idênticos.</em></p>

<p>Agora veja a prova cabal. Vamos subir um contêiner com o Docker e inspecionar os mesmos apontadores a partir do sistema operacional hospedeiro:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Subir um contêiner alpine dormindo em segundo plano</span>
<span class="nv">$ </span>docker run <span class="nt">-d</span> <span class="nt">--name</span> teste-namespaces alpine <span class="nb">sleep </span>3600

<span class="c"># Descobrir o PID do processo sleep no hospedeiro</span>
<span class="nv">$ CONTAINER_PID</span><span class="o">=</span><span class="si">$(</span>docker inspect <span class="nt">-f</span> <span class="s1">'{{.State.Pid}}'</span> teste-namespaces<span class="si">)</span>
<span class="nv">$ </span><span class="nb">echo</span> <span class="s2">"O PID real no hospedeiro é: </span><span class="nv">$CONTAINER_PID</span><span class="s2">"</span>
O PID real no hospedeiro é: 28419

<span class="c"># Comparar os namespaces do contêiner com os do hospedeiro</span>
<span class="nv">$ </span><span class="nb">ls</span> <span class="nt">-l</span> /proc/<span class="nv">$CONTAINER_PID</span>/ns/net /proc/1/ns/net
lrwxrwxrwx 1 root root 0 <span class="nb">set  </span>8 14:05 /proc/1/ns/net -&gt; <span class="s1">'net:[4026531840]'</span>
lrwxrwxrwx 1 root root 0 <span class="nb">set  </span>8 14:05 /proc/28419/ns/net -&gt; <span class="s1">'net:[4026532455]'</span>
</code></pre></div></div>

<p><em>Os números de inode são completamente diferentes, comprovando que o processo do contêiner vive em um namespace de rede próprio.</em></p>

<h3 id="o-segredo-do-comando-docker-exec-nsenter">O Segredo do Comando docker exec: nsenter</h3>

<p>Quando você roda <code class="language-plaintext highlighter-rouge">docker exec -it teste-namespaces sh</code>, o Docker não cria um contêiner novo nem faz conexões remotas via SSH. Ele simplesmente descobre o PID do processo principal do contêiner no hospedeiro e executa a chamada de sistema <code class="language-plaintext highlighter-rouge">setns()</code> para colocar um novo terminal dentro dos mesmos namespaces já utilizados por aquele contêiner.</p>

<p>O utilitário de linha de comando que faz isso diretamente no Linux chama-se <code class="language-plaintext highlighter-rouge">nsenter</code> <sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Entrar nos namespaces do contêiner usando apenas ferramentas padrão do Linux</span>
<span class="nv">$ </span><span class="nb">sudo </span>nsenter <span class="nt">--target</span> <span class="nv">$CONTAINER_PID</span> <span class="nt">--pid</span> <span class="nt">--net</span> <span class="nt">--mount</span> <span class="nt">--uts</span> <span class="nt">--ipc</span> sh

<span class="c"># Agora estamos dentro da visão do contêiner</span>
/ <span class="c"># hostname</span>
teste-namespaces
/ <span class="c"># ps aux</span>
PID   USER     TIME  COMMAND
    1 root      0:00 <span class="nb">sleep </span>3600
    7 root      0:00 sh
    8 root      0:00 ps aux
/ <span class="c"># exit</span>
</code></pre></div></div>
<p><em>O comando nsenter anexa o terminal aos namespaces do processo alvo sem depender do daemon do Docker.</em></p>

<div class="markdown-alert markdown-alert-tip"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-light-bulb" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"></path></svg> Dica de Bastidor</p><p>Da próxima vez que o daemon do Docker travar e não responder aos comandos convencionais, você não precisa ficar desesperado. Se o processo do contêiner ainda estiver vivo na tabela de processos do Linux, você pode usar <code class="language-plaintext highlighter-rouge">nsenter</code> para entrar diretamente no ambiente, diagnosticar arquivos de log e salvar seus dados sem depender de reiniciar serviço algum.</p>
</div>

<h2 id="cgroups-o-guarda-de-recursos">Cgroups: O Guarda de Recursos</h2>

<p>Os <em>namespaces</em> resolvem com brilhantismo a questão da visibilidade: eles garantem que os processos não enxerguem o que não devem. Mas imagine a seguinte situação: você cria um contêiner em um namespace isolado, mas uma linha de código maliciosa ou mal implementada dispara um laço infinito que aloca memória sem parar ou cria milhares de threads simultâneas.</p>

<p>Se dependêssemos apenas de namespaces, esse contêiner solitário consumiria 100% da CPU do computador, esgotaria toda a memória física do hospedeiro e causaria o travamento total do sistema operacional, derrubando todos os outros contêineres que dividem o mesmo servidor.</p>

<p>Para resolver o problema do consumo desenfreado de hardware, entram em cena os <strong>Control Groups</strong> ou simplesmente <strong>cgroups</strong> <sup id="fnref:4"><a href="#fn:4" class="footnote" rel="footnote" role="doc-noteref">4</a></sup>.</p>

<p>Se o namespace representa a <strong>parede de alvenaria do seu apartamento</strong>, o cgroup é a <strong>caixa de disjuntores e o hidrômetro</strong>: ele determina rigidamente quanta corrente elétrica e quantos litros de água aquele cômodo tem permissão para puxar da rede principal.</p>

<p>No Linux, os cgroups não são configurados por meio de interfaces obscuras ou comandos mágicos. Eles são implementados como um pseudo-sistema de arquivos montado tradicionalmente no caminho <code class="language-plaintext highlighter-rouge">/sys/fs/cgroup/</code>. Isso significa que, para criar um grupo de controle, limitar recursos e associar processos a ele, basta usar operações básicas de sistema de arquivos: criar pastas com <code class="language-plaintext highlighter-rouge">mkdir</code> e escrever valores numéricos em arquivos de texto com comandos simples como <code class="language-plaintext highlighter-rouge">echo</code>.</p>

<h3 id="a-grande-mudança-cgroups-v1-versus-cgroups-v2">A Grande Mudança: Cgroups v1 versus Cgroups v2</h3>

<p>Durante muitos anos, o Linux utilizou a primeira versão dos grupos de controle (cgroups v1). No modelo v1, cada recurso (CPU, memória, entrada e saída de disco, identificadores de processo) vivia em uma hierarquia de diretórios totalmente separada e independente dentro de <code class="language-plaintext highlighter-rouge">/sys/fs/cgroup/</code>. Um processo podia pertencer ao grupo <code class="language-plaintext highlighter-rouge">limite-baixo</code> na hierarquia de memória, mas pertencer ao grupo <code class="language-plaintext highlighter-rouge">prioridade-alta</code> na hierarquia de CPU.</p>

<p>Na prática, gerenciar essa matriz multidimensional virou um pesadelo de complexidade para os desenvolvedores de sistemas operacionais. Além disso, surgiam inconsistências graves: o controlador de memória não conversava direito com o controlador de gravação em disco (<em>writeback cache</em>), gerando bloqueios misteriosos de I/O.</p>

<p>Para solucionar essas dores, o engenheiro Tejun Heo liderou a reestruturação que deu origem aos <strong>cgroups v2</strong> (também conhecida como <em>Unified Hierarchy</em> ou hierarquia unificada) <sup id="fnref:5"><a href="#fn:5" class="footnote" rel="footnote" role="doc-noteref">5</a></sup>. No cgroups v2, existe apenas uma árvore de diretórios unificada. Um processo pertence a exatamente um cgroup, e todos os controladores de recursos habilitados para aquele grupo são aplicados de forma consistente sobre os mesmos processos.</p>

<p>Você pode verificar qual versão está em operação no seu sistema inspecionando as montagens:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Verificar o tipo de montagem ativa em /sys/fs/cgroup</span>
<span class="nv">$ </span>mount | <span class="nb">grep </span>cgroup
cgroup2 on /sys/fs/cgroup <span class="nb">type </span>cgroup2 <span class="o">(</span>rw,nosuid,nodev,noexec,relatime,nsdelegate<span class="o">)</span>
</code></pre></div></div>
<p><em>Se a saída indicar o tipo cgroup2, seu sistema já opera com a moderna hierarquia unificada adotada por padrão em distribuições recentes como Debian 12+, Ubuntu 22.04+ e Fedora.</em></p>

<p>Os principais controladores de recursos disponíveis no cgroups v2 e seus arquivos correspondentes são apresentados na tabela a seguir:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Controlador</th>
      <th style="text-align: left">O que gerencia</th>
      <th style="text-align: left">Arquivos Chave no cgroup v2</th>
      <th style="text-align: left">Exemplo de Valor Aplicado</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>memory</strong></td>
      <td style="text-align: left">Consumo de RAM física e área de swap</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">memory.max</code>, <code class="language-plaintext highlighter-rouge">memory.current</code>, <code class="language-plaintext highlighter-rouge">memory.events</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">536870912</code> (limite estrito de 512 MB)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>cpu</strong></td>
      <td style="text-align: left">Tempo de processamento e cotas em milissegundos</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">cpu.max</code>, <code class="language-plaintext highlighter-rouge">cpu.weight</code>, <code class="language-plaintext highlighter-rouge">cpu.stat</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">150000 100000</code> (limita a 1.5 núcleos de CPU)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>pids</strong></td>
      <td style="text-align: left">Quantidade máxima de processos simultâneos</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">pids.max</code>, <code class="language-plaintext highlighter-rouge">pids.current</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">50</code> (evita ataques de fork bomb)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>io</strong></td>
      <td style="text-align: left">Largura de banda e operações por segundo em disco</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">io.max</code>, <code class="language-plaintext highlighter-rouge">io.stat</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">8:0 rbps=10485760</code> (limite de leitura a 10 MB/s)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>cpuset</strong></td>
      <td style="text-align: left">Fixação de processos em núcleos específicos</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">cpuset.cpus</code>, <code class="language-plaintext highlighter-rouge">cpuset.mems</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">0,2</code> (amarra execução apenas nos núcleos 0 e 2)</td>
    </tr>
  </tbody>
</table>

<h3 id="criando-e-testando-um-cgroup-manualmente">Criando e Testando um Cgroup Manualmente</h3>

<p>Vamos construir um grupo de controle na mão, sem recorrer ao Docker, para ver a mecânica pura do kernel em ação.</p>

<p>Primeiro, garantimos que os controladores desejados estejam habilitados para delegação na raiz dos cgroups:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Habilitar controladores de memória e processos para os subdiretórios</span>
<span class="nv">$ </span><span class="nb">echo</span> <span class="s2">"+memory +pids"</span> | <span class="nb">sudo tee</span> /sys/fs/cgroup/cgroup.subtree_control
+memory +pids

<span class="c"># Criar um novo grupo de controle chamado laboratorio-cgroup</span>
<span class="nv">$ </span><span class="nb">sudo mkdir</span> /sys/fs/cgroup/laboratorio-cgroup

<span class="c"># Inspecionar os arquivos gerados automaticamente pelo kernel dentro da nova pasta</span>
<span class="nv">$ </span><span class="nb">ls</span> /sys/fs/cgroup/laboratorio-cgroup/
cgroup.controllers      cgroup.procs            memory.high             memory.stat
cgroup.events           cgroup.stat             memory.max              pids.current
cgroup.freeze           cgroup.subtree_control  memory.min              pids.events
cgroup.kill             memory.current          memory.oom.group        pids.max
</code></pre></div></div>
<p><em>O simples fato de criar um diretório faz o kernel popular instantaneamente a pasta com as interfaces de controle do cgroup v2.</em></p>

<p>Agora vamos estabelecer limites rígidos para este grupo: um teto de 50 megabytes de memória RAM e um limite de no máximo 20 processos simultâneos:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Definir o limite de memória em bytes (50 MB = 52428800 bytes)</span>
<span class="nv">$ </span><span class="nb">echo</span> <span class="s2">"52428800"</span> | <span class="nb">sudo tee</span> /sys/fs/cgroup/laboratorio-cgroup/memory.max
52428800

<span class="c"># Definir o limite máximo de processos (pids)</span>
<span class="nv">$ </span><span class="nb">echo</span> <span class="s2">"20"</span> | <span class="nb">sudo tee</span> /sys/fs/cgroup/laboratorio-cgroup/pids.max
20

<span class="c"># Conferir se as configurações foram gravadas</span>
<span class="nv">$ </span><span class="nb">cat</span> /sys/fs/cgroup/laboratorio-cgroup/memory.max
52428800
</code></pre></div></div>
<p><em>Valores gravados nesses arquivos são imediatamente assumidos pelo subsistema de alocação de memória do kernel.</em></p>

<p>Para colocar um processo sob a tutela desse cgroup, basta escrever o número do seu PID dentro do arquivo <code class="language-plaintext highlighter-rouge">cgroup.procs</code>. Vamos anexar a sessão atual do nosso terminal (representada pela variável especial <code class="language-plaintext highlighter-rouge">$$</code>):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Mover o shell atual para dentro do cgroup criado</span>
<span class="nv">$ </span><span class="nb">echo</span> <span class="nv">$$</span> | <span class="nb">sudo tee</span> /sys/fs/cgroup/laboratorio-cgroup/cgroup.procs

<span class="c"># Consultar o consumo atual de memória da nossa sessão em bytes</span>
<span class="nv">$ </span><span class="nb">cat</span> /sys/fs/cgroup/laboratorio-cgroup/memory.current
1925120
</code></pre></div></div>
<p><em>O shell consome aproximadamente 1.8 MB de memória RAM; o kernel rastreia a contabilidade em tempo real com precisão de bytes.</em></p>

<h3 id="o-guarda-em-ação-o-assassino-oom-out-of-memory-killer">O Guarda em Ação: O Assassino OOM (Out of Memory Killer)</h3>

<p>O que acontece se uma aplicação que vive dentro de um cgroup tentar ultrapassar o teto estipulado no arquivo <code class="language-plaintext highlighter-rouge">memory.max</code>?</p>

<p>O kernel Linux entra em ação através do mecanismo <strong>OOM Killer</strong> (<em>Out of Memory Killer</em>). Ele avalia os processos pertencentes àquele grupo específico e envia um sinal fatal <code class="language-plaintext highlighter-rouge">SIGKILL</code> para eliminar a aplicação gulosa, protegendo a estabilidade de todo o restante do servidor hospedeiro.</p>

<p>Vamos forçar essa situação usando o Docker com limites declarados:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Subir um contêiner limitado a meros 30 MB de RAM e tentar forçar o consumo de 60 MB</span>
<span class="nv">$ </span>docker run <span class="nt">--name</span> teste-estouro <span class="nt">--memory</span><span class="o">=</span>30m alpine sh <span class="nt">-c</span> <span class="se">\</span>
    <span class="s2">"head -c 60M /dev/zero | tail -c +1 &gt; /dev/null"</span>

<span class="c"># Inspecionar o motivo do encerramento do contêiner</span>
<span class="nv">$ </span>docker inspect teste-estouro <span class="nt">--format</span><span class="o">=</span><span class="s1">'Status: {{.State.Status}} | OOMKilled: {{.State.OOMKilled}} | ExitCode: {{.State.ExitCode}}'</span>
Status: exited | OOMKilled: <span class="nb">true</span> | ExitCode: 137
</code></pre></div></div>

<p><em>O código de saída 137 indica terminação por sinal fatal (128 + 9 para SIGKILL), acompanhado pelo marcador OOMKilled configurado como verdadeiro.</em></p>

<p>Se você consultar as mensagens de diagnóstico do kernel no mesmo instante com o comando <code class="language-plaintext highlighter-rouge">dmesg</code>, verá o registro oficial do evento:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">sudo </span>dmesg <span class="nt">-T</span> | <span class="nb">tail</span> <span class="nt">-n</span> 6
<span class="o">[</span>...] oom-kill:constraint<span class="o">=</span>CONSTRAINT_MEMCG,nodemask<span class="o">=(</span>null<span class="o">)</span>,cpuset<span class="o">=</span>docker-...
<span class="o">[</span>...] memory: usage 30720kB, limit 30720kB, failcnt 1
<span class="o">[</span>...] Memory cgroup out of memory: Killed process 31405 <span class="o">(</span>sh<span class="o">)</span> total-vm:63280kB, anon-rss:29840kB
</code></pre></div></div>
<p><em>O kernel deixa registrado explicitamente que o encerramento ocorreu por restrição do grupo de controle de memória (CONSTRAINT_MEMCG).</em></p>

<div class="markdown-alert markdown-alert-warning"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-alert" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg> Alerta de Produção</p><p>Em ambientes de produção, contêineres morrendo misteriosamente com código 137 são quase sempre vítimas do OOM Killer. Monitore a métrica <code class="language-plaintext highlighter-rouge">memory.current</code> ou configure alertas em ferramentas de observabilidade antes que ela alcance a marca do <code class="language-plaintext highlighter-rouge">memory.max</code>.</p>
</div>

<div class="markdown-alert markdown-alert-tip"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-light-bulb" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"></path></svg> Ferramentas de Inspeção de Cgroups</p><p>Você pode conferir os limites de qualquer contêiner Docker em tempo real diretamente no hospedeiro. O comando <code class="language-plaintext highlighter-rouge">docker stats --no-stream</code> exibe uma tabela resumida de consumo (CPU, memória, rede, PIDs) que é uma leitura formatada dos arquivos do cgroup de cada contêiner. Para uma investigação mais granular, descubra o PID real com <code class="language-plaintext highlighter-rouge">docker inspect -f '{{.State.Pid}}' &lt;nome&gt;</code>, consulte <code class="language-plaintext highlighter-rouge">/proc/&lt;PID&gt;/cgroup</code> para localizar o caminho do grupo de controle e leia diretamente os arquivos <code class="language-plaintext highlighter-rouge">memory.max</code>, <code class="language-plaintext highlighter-rouge">cpu.max</code> e <code class="language-plaintext highlighter-rouge">pids.current</code> sob <code class="language-plaintext highlighter-rouge">/sys/fs/cgroup/</code>.</p>
</div>

<h2 id="union-filesystems-imagens-em-camadas">Union Filesystems: Imagens em Camadas</h2>

<p>Chegamos à terceira perna do nosso tripé fundamental. Até aqui, vimos como os <em>namespaces</em> isolam a visão dos processos e como os <em>cgroups</em> limitam o consumo de memória e processamento. Mas resta um problema crucial: de onde vem o sistema de arquivos que o contêiner enxerga?</p>

<p>Se você baixar uma imagem oficial do Ubuntu, ela tem cerca de 80 megabytes. Se você precisar rodar cem contêineres idênticos desse mesmo Ubuntu no mesmo servidor, copiar 80 MB cem vezes gastaria 8 gigabytes de espaço em disco e demoraria vários segundos durante a clonagem.</p>

<p>A solução elegante encontrada pelo ecossistema de contêineres é o uso de <strong>Sistemas de Arquivos de União</strong> (<em>Union Filesystems</em>), materializados no Linux moderno pelo driver de kernel <strong>OverlayFS</strong> <sup id="fnref:6"><a href="#fn:6" class="footnote" rel="footnote" role="doc-noteref">6</a></sup>.</p>

<h3 id="a-mecânica-do-copy-on-write-cow">A Mecânica do Copy-on-Write (CoW)</h3>

<p>A ideia central de um sistema de arquivos de união é sobrepor múltiplos diretórios de forma transparente, apresentando para quem consome o ponto de montagem a ilusão de um único diretório unificado.</p>

<p>As imagens de contêiner são compostas por uma pilha de camadas estáticas somente leitura (<em>read-only</em>). Cada comando executado no processo de construção de uma imagem gera uma nova fatia fina gravada em disco. Quando você inicia um contêiner baseado nessa imagem, o runtime não duplica esses dados: ele simplesmente adiciona uma camada finíssima e efêmera de leitura e escrita (<em>read-write</em>) no topo de toda a pilha.</p>

<p>A regra de ouro desse modelo chama-se <strong>Copy-on-Write (CoW)</strong>:</p>
<ol>
  <li><strong>Operação de Leitura</strong>: Se a sua aplicação ler um arquivo que pertence à imagem base, o sistema acessa diretamente o arquivo na camada inferior somente leitura.</li>
  <li><strong>Operação de Modificação</strong>: No milissegundo em que sua aplicação tenta alterar um arquivo existente da imagem base, o OverlayFS intercepta a chamada, faz uma cópia idêntica daquele arquivo para a camada superior de escrita e aplica a alteração exclusivamente na cópia. O arquivo original na camada inferior permanece 100% intocado.</li>
  <li><strong>Operação de Criação</strong>: Qualquer novo arquivo criado vai direto para a camada superior de escrita.</li>
  <li><strong>Operação de Remoção</strong>: Se um arquivo da camada base for apagado, o OverlayFS cria um arquivo especial de bloqueio (<em>whiteout file</em>) na camada de escrita, ocultando o arquivo original da visão unificada.</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─────────────────────────────────────────────────────────┐
│              VISÃO UNIFICADA DO CONTÊINER               │  &lt;- Diretório "merged"
│            /app  /etc  /usr  /bin  /tmp                 │     (o processo enxerga isto)
├─────────────────────────────────────────────────────────┤
│          CAMADA DE ESCRITA EFÊMERA (R/W)                │  &lt;- Diretório "upperdir"
│     Arquivos criados ou modificados pelo contêiner      │     (morre com o contêiner)
├─────────────────────────────────────────────────────────┤
│          CAMADA DA IMAGEM: COPY app.jar (R/O)           │  &lt;- Diretório "lowerdir[0]"
├─────────────────────────────────────────────────────────┤
│          CAMADA DA IMAGEM: RUN apt-get install (R/O)    │  &lt;- Diretório "lowerdir[1]"
├─────────────────────────────────────────────────────────┤
│          CAMADA BASE: FROM debian:bookworm (R/O)        │  &lt;- Diretório "lowerdir[2]"
└─────────────────────────────────────────────────────────┘
</code></pre></div></div>

<p>O OverlayFS estrutura essa mágica dividindo a operação em quatro diretórios fundamentais:</p>
<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">lowerdir</code></strong>: Os diretórios base somente leitura (as camadas da imagem). Múltiplos caminhos podem ser encadeados separando-os por dois-pontos.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">upperdir</code></strong>: O diretório de leitura e escrita onde ficam as modificações efetuadas pelo contêiner.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">workdir</code></strong>: Uma pasta de serviço interna necessária para o kernel preparar operações atômicas antes de exibi-las.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">merged</code></strong>: O diretório final onde o sistema de arquivos unificado é montado e disponibilizado para consumo.</li>
</ul>

<h3 id="montando-um-overlayfs-na-mão">Montando um OverlayFS na Mão</h3>

<p>Para entender isso em definitivo, nada melhor do que criar as camadas e montá-las manualmente sem usar nenhum comando do Docker:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Criar a estrutura física dos diretórios no /tmp</span>
<span class="nv">$ </span><span class="nb">mkdir</span> <span class="nt">-p</span> /tmp/experimento-overlay/<span class="o">{</span>base,escrita,trabalho,unificado<span class="o">}</span>

<span class="c"># Criar arquivos simulando uma camada base de sistema operacional</span>
<span class="nv">$ </span><span class="nb">echo</span> <span class="s2">"Configuracao padrao v1.0"</span> <span class="o">&gt;</span> /tmp/experimento-overlay/base/sistema.conf
<span class="nv">$ </span><span class="nb">echo</span> <span class="s2">"Binario utilitario"</span> <span class="o">&gt;</span> /tmp/experimento-overlay/base/ferramenta.sh
<span class="nv">$ </span><span class="nb">chmod</span> +x /tmp/experimento-overlay/base/ferramenta.sh

<span class="c"># Montar o OverlayFS usando o utilitário padrão de montagem do Linux</span>
<span class="nv">$ </span><span class="nb">sudo </span>mount <span class="nt">-t</span> overlay overlay <span class="se">\</span>
    <span class="nt">-o</span> <span class="nv">lowerdir</span><span class="o">=</span>/tmp/experimento-overlay/base,upperdir<span class="o">=</span>/tmp/experimento-overlay/escrita,workdir<span class="o">=</span>/tmp/experimento-overlay/trabalho <span class="se">\</span>
    /tmp/experimento-overlay/unificado

<span class="c"># Listar o conteúdo da visão unificada</span>
<span class="nv">$ </span><span class="nb">ls</span> /tmp/experimento-overlay/unificado/
ferramenta.sh  sistema.conf

<span class="c"># Conferir o conteúdo do arquivo lido da camada base</span>
<span class="nv">$ </span><span class="nb">cat</span> /tmp/experimento-overlay/unificado/sistema.conf
Configuracao padrao v1.0
</code></pre></div></div>
<p><em>A pasta unificada expõe perfeitamente os arquivos que fisicamente residem dentro da pasta base.</em></p>

<p>Agora, vamos observar o comportamento do <em>Copy-on-Write</em> alterando um arquivo e criando um item inédito:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Modificar o arquivo existente através do diretório unificado</span>
<span class="nv">$ </span><span class="nb">echo</span> <span class="s2">"Configuracao customizada pelo usuario"</span> <span class="o">&gt;</span> /tmp/experimento-overlay/unificado/sistema.conf

<span class="c"># Criar um arquivo novo dentro da visão unificada</span>
<span class="nv">$ </span><span class="nb">echo</span> <span class="s2">"Arquivo gerado em execucao"</span> <span class="o">&gt;</span> /tmp/experimento-overlay/unificado/novo.log

<span class="c"># O arquivo original na camada base PERMANECE INTACTO</span>
<span class="nv">$ </span><span class="nb">cat</span> /tmp/experimento-overlay/base/sistema.conf
Configuracao padrao v1.0

<span class="c"># O arquivo modificado e o arquivo novo foram parar no diretório de escrita (upperdir)</span>
<span class="nv">$ </span><span class="nb">ls</span> /tmp/experimento-overlay/escrita/
novo.log  sistema.conf

<span class="nv">$ </span><span class="nb">cat</span> /tmp/experimento-overlay/escrita/sistema.conf
Configuracao customizada pelo usuario

<span class="c"># Desmontar o sistema de arquivos de teste</span>
<span class="nv">$ </span><span class="nb">sudo </span>umount /tmp/experimento-overlay/unificado
</code></pre></div></div>
<p><em>A camada base permaneceu imutável; toda e qualquer alteração foi gravada exclusivamente dentro do diretório de escrita.</em></p>

<p>Quando você roda <code class="language-plaintext highlighter-rouge">docker rm meu-conteiner</code>, tudo o que o Docker precisa fazer em disco é deletar o diretório <code class="language-plaintext highlighter-rouge">upperdir</code> correspondente. As camadas da imagem base nunca foram alteradas, e por isso podem ser compartilhadas concorrentemente por centenas de contêineres sem risco de corrupção cruzada.</p>

<div class="markdown-alert markdown-alert-tip"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-light-bulb" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"></path></svg> Inspecionando Camadas pelo Docker</p><p>Se quiser visualizar a pilha de camadas de qualquer imagem sem montá-las manualmente, use <code class="language-plaintext highlighter-rouge">docker image inspect &lt;imagem&gt; --format='{{json .RootFS.Layers}}'</code>. A saída é uma lista de hashes SHA256 representando cada camada em ordem de empilhamento. Para um contêiner em execução, <code class="language-plaintext highlighter-rouge">docker inspect &lt;contêiner&gt; --format='{{json .GraphDriver.Data}}'</code> revela os caminhos físicos exatos do <code class="language-plaintext highlighter-rouge">LowerDir</code>, <code class="language-plaintext highlighter-rouge">UpperDir</code>, <code class="language-plaintext highlighter-rouge">WorkDir</code> e <code class="language-plaintext highlighter-rouge">MergedDir</code> no sistema de arquivos do hospedeiro.</p>
</div>

<h2 id="a-especificação-oci-e-o-ecossistema-de-runtimes">A Especificação OCI e o Ecossistema de Runtimes</h2>

<p>Nos primeiros anos de vida do Docker, toda essa orquestração de chamadas de sistema era centralizada em um daemon monolítico e proprietário. À medida que grandes empresas começaram a adotar contêineres para cargas de trabalho críticas, surgiu o receio de que o formato ficasse refém dos rumos comerciais de uma única empresa.</p>

<p>Para sanar essa preocupação, a indústria se uniu sob a tutela da Linux Foundation e instituiu a <strong>Open Container Initiative (OCI)</strong>. O trabalho da OCI gerou três padrões fundamentais abertos que regem o universo de contêineres até hoje:</p>

<ol>
  <li><strong>OCI Image Specification</strong>: Define como uma imagem deve ser empacotada em disco. Uma imagem OCI nada mais é do que um conjunto de arquivos compactados (<em>tarballs</em>) contendo as camadas de arquivos do sistema, acompanhados de um manifesto em formato JSON descrevendo arquitetura de hardware, variáveis de ambiente, portas e comandos de entrada.</li>
  <li><strong>OCI Runtime Specification</strong>: Define como pegar aquele sistema de arquivos descompactado e executá-lo no sistema operacional. Ela descreve a estrutura de um arquivo padronizado chamado <code class="language-plaintext highlighter-rouge">config.json</code>, que lista os namespaces que devem ser criados, os limites de cgroups e os privilégios do processo.</li>
  <li><strong>OCI Distribution Specification</strong>: Padroniza a API HTTP para publicação, busca e transferência de imagens entre registros de contêineres (como Docker Hub, Quay.io, GitHub Container Registry e Amazon ECR).</li>
</ol>

<h3 id="a-cadeia-de-execução-moderna">A Cadeia de Execução Moderna</h3>

<p>Graças à padronização OCI, o ecossistema de execução foi modularizado em camadas independentes de responsabilidade:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌──────────────┐
│  docker CLI  │  Interface com o usuário (interpreta comandos e flags)
└──────┬───────┘
       │  API REST (via socket unix /var/run/docker.sock)
       ▼
┌──────────────┐
│   dockerd    │  Daemon de gerenciamento (imagens, volumes, redes e API)
└──────┬───────┘
       │  gRPC
       ▼
┌──────────────┐
│  containerd  │  Runtime de Alto Nível (baixa imagens, gerencia ciclo de vida)
└──────┬───────┘
       │  Execução OCI
       ▼
┌──────────────┐
│     runc     │  Runtime de Baixo Nível (chama clone, unshare, cgroups e morre)
└──────┬───────┘
       │
       ▼
┌──────────────┐
│   Processo   │  Sua aplicação rodando isolada dentro do kernel
└──────────────┘
</code></pre></div></div>

<p>Repare na separação nítida:</p>
<ul>
  <li>O <strong>Runtime de Alto Nível</strong> (como <code class="language-plaintext highlighter-rouge">containerd</code> ou <code class="language-plaintext highlighter-rouge">CRI-O</code>) é responsável por baixar imagens pela rede, validar assinaturas criptográficas, descompactar camadas em disco e preparar o sistema de arquivos montado.</li>
  <li>O <strong>Runtime de Baixo Nível</strong> (como <code class="language-plaintext highlighter-rouge">runc</code> ou <code class="language-plaintext highlighter-rouge">crun</code>) tem um papel estritamente pontual: ele recebe uma pasta já montada com um arquivo <code class="language-plaintext highlighter-rouge">config.json</code>, invoca as chamadas de sistema do kernel (<code class="language-plaintext highlighter-rouge">unshare</code>, <code class="language-plaintext highlighter-rouge">mount</code>, configuração de cgroup), executa o binário da sua aplicação e em seguida se encerra, deixando o processo isolado rodando supervisionado pelo kernel.</li>
</ul>

<h3 id="o-raio-x-do-configjson-da-oci">O Raio-X do config.json da OCI</h3>

<p>Você pode comprovar que um contêiner não passa de uma pasta com um arquivo JSON utilizando o próprio <code class="language-plaintext highlighter-rouge">runc</code> <sup id="fnref:7"><a href="#fn:7" class="footnote" rel="footnote" role="doc-noteref">7</a></sup> instalado no sistema.</p>

<p>Vamos gerar uma especificação padrão de contêiner OCI:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Criar uma pasta temporária para o nosso pacote OCI</span>
<span class="nv">$ </span><span class="nb">mkdir</span> <span class="nt">-p</span> /tmp/bundle-oci <span class="o">&amp;&amp;</span> <span class="nb">cd</span> /tmp/bundle-oci

<span class="c"># Gerar o arquivo de configuração de referência da OCI</span>
<span class="nv">$ </span>runc spec

<span class="c"># Inspecionar as primeiras linhas do arquivo gerado</span>
<span class="nv">$ </span><span class="nb">head</span> <span class="nt">-n</span> 35 config.json
<span class="o">{</span>
	<span class="s2">"ociVersion"</span>: <span class="s2">"1.0.2-dev"</span>,
	<span class="s2">"process"</span>: <span class="o">{</span>
		<span class="s2">"terminal"</span>: <span class="nb">true</span>,
		<span class="s2">"user"</span>: <span class="o">{</span>
			<span class="s2">"uid"</span>: 0,
			<span class="s2">"gid"</span>: 0
		<span class="o">}</span>,
		<span class="s2">"args"</span>: <span class="o">[</span>
			<span class="s2">"sh"</span>
		<span class="o">]</span>,
		<span class="s2">"env"</span>: <span class="o">[</span>
			<span class="s2">"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"</span>,
			<span class="s2">"TERM=xterm"</span>
		<span class="o">]</span>,
		<span class="s2">"cwd"</span>: <span class="s2">"/"</span>
	<span class="o">}</span>,
	<span class="s2">"root"</span>: <span class="o">{</span>
		<span class="s2">"path"</span>: <span class="s2">"rootfs"</span>,
		<span class="s2">"readonly"</span>: <span class="nb">true</span>
	<span class="o">}</span>,
	<span class="s2">"hostname"</span>: <span class="s2">"runc"</span>,
	<span class="s2">"mounts"</span>: <span class="o">[</span>
		<span class="o">{</span>
			<span class="s2">"destination"</span>: <span class="s2">"/proc"</span>,
			<span class="s2">"type"</span>: <span class="s2">"proc"</span>,
			<span class="s2">"source"</span>: <span class="s2">"proc"</span>
		<span class="o">}</span>
</code></pre></div></div>
<p><em>O arquivo config.json padroniza universalmente cada parâmetro de execução do processo.</em></p>

<p>Se você rolar o arquivo até a seção <code class="language-plaintext highlighter-rouge">linux</code>, encontrará exatamente as estruturas de dados que estudamos nas seções anteriores deste post:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"namespaces"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
	</span><span class="p">{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pid"</span><span class="w"> </span><span class="p">},</span><span class="w">
	</span><span class="p">{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"network"</span><span class="w"> </span><span class="p">},</span><span class="w">
	</span><span class="p">{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ipc"</span><span class="w"> </span><span class="p">},</span><span class="w">
	</span><span class="p">{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"uts"</span><span class="w"> </span><span class="p">},</span><span class="w">
	</span><span class="p">{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"mount"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<p>Qualquer ferramenta que gere essa pasta com essa estrutura consegue rodar um contêiner em qualquer distribuição Linux do planeta. É a interoperabilidade levada ao estado da arte.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Runtime de Baixo Nível</th>
      <th style="text-align: left">Linguagem</th>
      <th style="text-align: left">Mantenedor</th>
      <th style="text-align: left">Características Principais</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>runc</strong></td>
      <td style="text-align: left">Go</td>
      <td style="text-align: left">OCI / Comunidade</td>
      <td style="text-align: left">A implementação de referência padrão utilizada pelo Docker e containerd</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>crun</strong></td>
      <td style="text-align: left">C puro</td>
      <td style="text-align: left">Red Hat</td>
      <td style="text-align: left">Extremamente veloz, menor pegada de memória, padrão no Podman</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>youki</strong></td>
      <td style="text-align: left">Rust</td>
      <td style="text-align: left">Comunidade</td>
      <td style="text-align: left">Implementação segura em memória aproveitando o ecossistema Rust</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>gVisor (runsc)</strong></td>
      <td style="text-align: left">Go</td>
      <td style="text-align: left">Google</td>
      <td style="text-align: left">Intercepta chamadas de sistema em espaço de usuário para isolamento extremo</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Kata Containers</strong></td>
      <td style="text-align: left">Go / C</td>
      <td style="text-align: left">OpenInfra</td>
      <td style="text-align: left">Executa o contêiner dentro de uma micro-máquina virtual QEMU/Cloud-Hypervisor</td>
    </tr>
  </tbody>
</table>

<h2 id="grand-finale-construindo-um-mini-contêiner-do-zero">Grand Finale: Construindo um Mini-Contêiner do Zero</h2>

<p>Chegou a hora de juntar todas as peças. Nós vimos a história, entendemos por que contêineres não são máquinas virtuais, exploramos os <em>namespaces</em>, configuramos <em>cgroups</em> na mão e montamos camadas com <em>OverlayFS</em>.</p>

<p>Agora, vamos provar a tese inicial: <strong>um contêiner é apenas um processo com a coleira do kernel</strong>.</p>

<p>Abaixo está o código de um script em Bash funcional com cerca de 50 linhas úteis. Ele faz absolutamente tudo o que o Docker faz por baixo do capô quando você roda <code class="language-plaintext highlighter-rouge">docker run</code>: baixa uma distribuição Linux mínima oficial (Alpine Linux), monta as camadas com OverlayFS preservando a imagem intacta, cria um cgroup v2 com limites estritos de memória e quantidade de processos, isola todos os namespaces necessários e entrega um terminal interativo com o processo assumindo o PID 1.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="c"># mini-conteiner.sh: Um motor de contêineres funcional em ~50 linhas de shell script</span>
<span class="c"># Uso: sudo ./mini-conteiner.sh</span>
<span class="nb">set</span> <span class="nt">-euo</span> pipefail

<span class="nv">CONTAINER_DIR</span><span class="o">=</span><span class="s2">"/tmp/mini-conteiner-lab"</span>
<span class="nv">ALPINE_VERSION</span><span class="o">=</span><span class="s2">"3.20"</span>
<span class="nv">ALPINE_ARCH</span><span class="o">=</span><span class="s2">"x86_64"</span>
<span class="nv">ALPINE_URL</span><span class="o">=</span><span class="s2">"https://dl-cdn.alpinelinux.org/alpine/v</span><span class="k">${</span><span class="nv">ALPINE_VERSION</span><span class="k">}</span><span class="s2">/releases/</span><span class="k">${</span><span class="nv">ALPINE_ARCH</span><span class="k">}</span><span class="s2">/alpine-minirootfs-</span><span class="k">${</span><span class="nv">ALPINE_VERSION</span><span class="k">}</span><span class="s2">.0-</span><span class="k">${</span><span class="nv">ALPINE_ARCH</span><span class="k">}</span><span class="s2">.tar.gz"</span>
<span class="nv">CGROUP_NAME</span><span class="o">=</span><span class="s2">"mini-conteiner-</span><span class="nv">$$</span><span class="s2">"</span>
<span class="nv">MEMORY_LIMIT</span><span class="o">=</span><span class="s2">"104857600"</span>  <span class="c"># Limite estrito de 100 MB em bytes</span>
<span class="nv">PIDS_LIMIT</span><span class="o">=</span><span class="s2">"50"</span>           <span class="c"># Limite máximo de 50 processos</span>

<span class="nb">echo</span> <span class="s2">"==&gt; [1/6] Preparando estrutura de diretórios do OverlayFS..."</span>
<span class="nb">mkdir</span> <span class="nt">-p</span> <span class="s2">"</span><span class="k">${</span><span class="nv">CONTAINER_DIR</span><span class="k">}</span><span class="s2">"</span>/<span class="o">{</span>rootfs,upper,work,merged<span class="o">}</span>

<span class="nb">echo</span> <span class="s2">"==&gt; [2/6] Obtendo imagem base oficial do Alpine Linux </span><span class="k">${</span><span class="nv">ALPINE_VERSION</span><span class="k">}</span><span class="s2">..."</span>
<span class="k">if</span> <span class="o">[</span> <span class="o">!</span> <span class="nt">-f</span> <span class="s2">"</span><span class="k">${</span><span class="nv">CONTAINER_DIR</span><span class="k">}</span><span class="s2">/alpine.tar.gz"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span>curl <span class="nt">-fsSL</span> <span class="s2">"</span><span class="k">${</span><span class="nv">ALPINE_URL</span><span class="k">}</span><span class="s2">"</span> <span class="nt">-o</span> <span class="s2">"</span><span class="k">${</span><span class="nv">CONTAINER_DIR</span><span class="k">}</span><span class="s2">/alpine.tar.gz"</span>
    <span class="nb">tar </span>xzf <span class="s2">"</span><span class="k">${</span><span class="nv">CONTAINER_DIR</span><span class="k">}</span><span class="s2">/alpine.tar.gz"</span> <span class="nt">-C</span> <span class="s2">"</span><span class="k">${</span><span class="nv">CONTAINER_DIR</span><span class="k">}</span><span class="s2">/rootfs"</span>
<span class="k">fi

</span><span class="nb">echo</span> <span class="s2">"==&gt; [3/6] Montando sistema de arquivos OverlayFS (camadas CoW)..."</span>
mount <span class="nt">-t</span> overlay overlay <span class="se">\</span>
    <span class="nt">-o</span> <span class="s2">"lowerdir=</span><span class="k">${</span><span class="nv">CONTAINER_DIR</span><span class="k">}</span><span class="s2">/rootfs,upperdir=</span><span class="k">${</span><span class="nv">CONTAINER_DIR</span><span class="k">}</span><span class="s2">/upper,workdir=</span><span class="k">${</span><span class="nv">CONTAINER_DIR</span><span class="k">}</span><span class="s2">/work"</span> <span class="se">\</span>
    <span class="s2">"</span><span class="k">${</span><span class="nv">CONTAINER_DIR</span><span class="k">}</span><span class="s2">/merged"</span>

<span class="nb">echo</span> <span class="s2">"==&gt; [4/6] Configurando limites de recursos via Cgroup v2 (</span><span class="k">${</span><span class="nv">CGROUP_NAME</span><span class="k">}</span><span class="s2">)..."</span>
<span class="c"># Garante que os controladores de memória e processos estejam delegáveis aos subdiretórios</span>
<span class="nb">echo</span> <span class="s2">"+memory +pids"</span> <span class="o">&gt;</span> /sys/fs/cgroup/cgroup.subtree_control 2&gt;/dev/null <span class="o">||</span> <span class="nb">true
</span><span class="nv">CGROUP_PATH</span><span class="o">=</span><span class="s2">"/sys/fs/cgroup/</span><span class="k">${</span><span class="nv">CGROUP_NAME</span><span class="k">}</span><span class="s2">"</span>
<span class="nb">mkdir</span> <span class="nt">-p</span> <span class="s2">"</span><span class="k">${</span><span class="nv">CGROUP_PATH</span><span class="k">}</span><span class="s2">"</span>
<span class="nb">echo</span> <span class="s2">"</span><span class="k">${</span><span class="nv">MEMORY_LIMIT</span><span class="k">}</span><span class="s2">"</span> <span class="o">&gt;</span> <span class="s2">"</span><span class="k">${</span><span class="nv">CGROUP_PATH</span><span class="k">}</span><span class="s2">/memory.max"</span>
<span class="nb">echo</span> <span class="s2">"</span><span class="k">${</span><span class="nv">PIDS_LIMIT</span><span class="k">}</span><span class="s2">"</span> <span class="o">&gt;</span> <span class="s2">"</span><span class="k">${</span><span class="nv">CGROUP_PATH</span><span class="k">}</span><span class="s2">/pids.max"</span>

<span class="nb">echo</span> <span class="s2">"==&gt; [5/6] Preparando arquivos de configuração (DNS)..."</span>
<span class="nb">mkdir</span> <span class="nt">-p</span> <span class="s2">"</span><span class="k">${</span><span class="nv">CONTAINER_DIR</span><span class="k">}</span><span class="s2">/merged/etc"</span>
<span class="nb">cat</span> <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">EOF</span><span class="sh">' &gt; "</span><span class="k">${</span><span class="nv">CONTAINER_DIR</span><span class="k">}</span><span class="sh">/merged/etc/resolv.conf"
nameserver 1.1.1.1
nameserver 8.8.8.8
</span><span class="no">EOF

</span><span class="nb">echo</span> <span class="s2">"==&gt; [6/6] Inicializando processo isolado em novos Namespaces..."</span>
<span class="nb">echo</span> <span class="s2">"     Ambiente pronto! Digite 'exit' para sair e liberar os recursos."</span>
<span class="nb">echo</span> <span class="s2">""</span>

<span class="c"># unshare isola PID, rede, hostname, IPC, pontos de montagem e cgroups</span>
<span class="c"># Pseudo-sistemas (/proc, /sys, /dev) são montados no namespace privado do contêiner</span>
unshare <span class="nt">--pid</span> <span class="nt">--fork</span> <span class="nt">--net</span> <span class="nt">--uts</span> <span class="nt">--ipc</span> <span class="nt">--mount</span> <span class="nt">--cgroup</span> <span class="se">\</span>
    sh <span class="nt">-c</span> <span class="s2">"
        echo </span><span class="se">\$\$</span><span class="s2"> &gt; '</span><span class="k">${</span><span class="nv">CGROUP_PATH</span><span class="k">}</span><span class="s2">/cgroup.procs' 2&gt;/dev/null || true
        mount -t proc proc '</span><span class="k">${</span><span class="nv">CONTAINER_DIR</span><span class="k">}</span><span class="s2">/merged/proc'
        mount -t sysfs sysfs '</span><span class="k">${</span><span class="nv">CONTAINER_DIR</span><span class="k">}</span><span class="s2">/merged/sys'
        mount --bind /dev '</span><span class="k">${</span><span class="nv">CONTAINER_DIR</span><span class="k">}</span><span class="s2">/merged/dev'
        exec chroot '</span><span class="k">${</span><span class="nv">CONTAINER_DIR</span><span class="k">}</span><span class="s2">/merged' /bin/sh -c '
            hostname mini-conteiner
            export PATH=/bin:/sbin:/usr/bin:/usr/sbin
            echo </span><span class="se">\"</span><span class="s2">==================================================</span><span class="se">\"</span><span class="s2">
            echo </span><span class="se">\"</span><span class="s2">  BEM-VINDO AO SEU CONTÊINER ARTESANAL!</span><span class="se">\"</span><span class="s2">
            echo </span><span class="se">\"</span><span class="s2">  Hostname: </span><span class="se">\$</span><span class="s2">(hostname)</span><span class="se">\"</span><span class="s2">
            echo </span><span class="se">\"</span><span class="s2">  PID local: </span><span class="se">\$\$\"</span><span class="s2">
            echo </span><span class="se">\"</span><span class="s2">  Memória limitada a: 100 MB</span><span class="se">\"</span><span class="s2">
            echo </span><span class="se">\"</span><span class="s2">==================================================</span><span class="se">\"</span><span class="s2">
            exec /bin/sh
        '
    "</span>

<span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"==&gt; Finalizando e desmontando camadas com segurança..."</span>
<span class="nb">sleep </span>1
umount <span class="nt">-l</span> <span class="s2">"</span><span class="k">${</span><span class="nv">CONTAINER_DIR</span><span class="k">}</span><span class="s2">/merged"</span> 2&gt;/dev/null <span class="o">||</span> <span class="nb">true
rmdir</span> <span class="s2">"</span><span class="k">${</span><span class="nv">CGROUP_PATH</span><span class="k">}</span><span class="s2">"</span> 2&gt;/dev/null <span class="o">||</span> <span class="nb">true
echo</span> <span class="s2">"==&gt; Limpeza concluída com sucesso."</span>
</code></pre></div></div>
<p><em>Script autossuficiente demonstrando a convergência prática de namespaces, cgroups v2 e OverlayFS.</em></p>

<h3 id="entendendo-a-mecânica-passo-a-passo">Entendendo a Mecânica Passo a Passo</h3>

<p>A elegância desse script reside no fato de que cada bloco de código espelha diretamente os conceitos que dissecamos ao longo de todo o texto:</p>

<p>O primeiro bloco prepara as quatro pastas exigidas pelo driver OverlayFS. O segundo passo faz o papel do comando <code class="language-plaintext highlighter-rouge">docker pull</code>: ele baixa um arquivo comprimido de apenas três megabytes contendo os binários do Alpine Linux e descompacta o conteúdo na pasta <code class="language-plaintext highlighter-rouge">rootfs</code>. Essa pasta atua como a nossa camada inferior imutável (<code class="language-plaintext highlighter-rouge">lowerdir</code>).</p>

<p>No terceiro passo, o script realiza a montagem do OverlayFS. O diretório <code class="language-plaintext highlighter-rouge">merged</code> passa a ser a nossa visão consolidada, enquanto a pasta <code class="language-plaintext highlighter-rouge">upper</code> acolherá qualquer arquivo modificado pelo usuário durante o teste.</p>

<p>No quarto passo, o script fala diretamente com o subsistema cgroups v2 do kernel: habilita os controladores para delegação aos subgrupos em <code class="language-plaintext highlighter-rouge">cgroup.subtree_control</code>, cria a pasta correspondente ao grupo de controle do contêiner e grava os limites de cem megabytes de RAM e cinquenta identificadores de processo.</p>

<p>O quinto passo cuida da configuração básica de rede, gravando diretamente servidores de DNS públicos em <code class="language-plaintext highlighter-rouge">/etc/resolv.conf</code> para evitar conflitos com o resolvedor local em stub (<code class="language-plaintext highlighter-rouge">127.0.0.53</code>) do <em>systemd-resolved</em>.</p>

<p>No sexto passo, a grande mágica se concretiza: o utilitário <code class="language-plaintext highlighter-rouge">unshare</code> dispara novas instâncias de todos os seis namespaces essenciais. No namespace privado de montagem recém-criado, o script monta os pseudo-sistemas de arquivos <code class="language-plaintext highlighter-rouge">/proc</code>, <code class="language-plaintext highlighter-rouge">/sys</code> e <code class="language-plaintext highlighter-rouge">/dev</code> (garantindo que o <code class="language-plaintext highlighter-rouge">/proc</code> reflita exclusivamente a tabela de processos do novo contêiner e que essas montagens desapareçam do hospedeiro assim que o namespace for encerrado). Antes de entregar o terminal ao usuário, o shell grava o seu próprio identificador de processo dentro do arquivo <code class="language-plaintext highlighter-rouge">cgroup.procs</code> daquele cgroup, garantindo que os limites de hardware passem a valer imediatamente. Em seguida, a chamada <code class="language-plaintext highlighter-rouge">exec chroot</code> substitui o processo atual pelo <code class="language-plaintext highlighter-rouge">/bin/sh</code> assumindo rigorosamente o PID 1.</p>

<p>Ao digitar <code class="language-plaintext highlighter-rouge">exit</code>, a rotina de encerramento entra em ação: desmonta o ponto OverlayFS e remove o diretório do cgroup, devolvendo o sistema operacional hospedeiro exatamente ao estado em que se encontrava.</p>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Nota Operacional: chroot versus pivot_root</p><p>Em nosso laboratório didático, utilizamos o comando <code class="language-plaintext highlighter-rouge">chroot</code> pela simplicidade e legibilidade do script em 50 linhas. Runtimes de produção da especificação OCI (como <code class="language-plaintext highlighter-rouge">runc</code> e <code class="language-plaintext highlighter-rouge">crun</code>) utilizam a chamada de sistema <code class="language-plaintext highlighter-rouge">pivot_root</code>, que move atomicamente o novo sistema de arquivos para a raiz do namespace <code class="language-plaintext highlighter-rouge">MNT</code> e descarta a raiz antiga, impedindo qualquer tentativa de fuga (<em>container breakout</em>). Da mesma forma, motores de contêiner evitam bind mounts diretos de <code class="language-plaintext highlighter-rouge">/dev</code>, criando em vez disso instâncias isoladas de <code class="language-plaintext highlighter-rouge">devpts</code> e nós mínimos de dispositivos (<code class="language-plaintext highlighter-rouge">null</code>, <code class="language-plaintext highlighter-rouge">zero</code>, <code class="language-plaintext highlighter-rouge">random</code>, <code class="language-plaintext highlighter-rouge">urandom</code>).</p>
</div>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Nota sobre Propagação de Montagens</p><p>Em distribuições modernas com <code class="language-plaintext highlighter-rouge">systemd</code>, o sistema de arquivos raiz herda a flag de propagação compartilhada (<code class="language-plaintext highlighter-rouge">shared</code>). Ao isolar namespaces de montagem com <code class="language-plaintext highlighter-rouge">unshare --mount</code>, o kernel cria uma cópia privada dos pontos existentes, mas se você criar montagens novas que não devam ser propagadas para o hospedeiro, motores de produção executam internamente <code class="language-plaintext highlighter-rouge">mount --make-rprivate /</code> antes de montar <code class="language-plaintext highlighter-rouge">/proc</code> e <code class="language-plaintext highlighter-rouge">/sys</code>. No nosso laboratório, a chamada <code class="language-plaintext highlighter-rouge">unshare</code> garante que o <code class="language-plaintext highlighter-rouge">/proc</code> remountado viva estritamente dentro da visão do processo bifurcado.</p>
</div>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Nota Operacional: Permissões de Execução</p><p>Para executar o script em sua plenitude, rode-o com <code class="language-plaintext highlighter-rouge">sudo</code> para permitir a execução de <code class="language-plaintext highlighter-rouge">mount</code> e a configuração de cgroups no sistema. Se você preferir uma abordagem sem privilégios administrativos no seu usuário diário, utilize ferramentas como o Podman, que orquestram esses mesmos passos combinando <em>User Namespaces</em> com ferramentas auxiliares como o <code class="language-plaintext highlighter-rouge">crun</code>.</p>
</div>

<h2 id="exercícios">Exercícios</h2>

<p>Para fixar a dinâmica de isolamento do kernel, namespaces, cgroups e OverlayFS, execute os desafios práticos abaixo no seu terminal Linux.</p>

<p><strong>1. Criando Rootless com User Namespaces</strong></p>

<p>Crie um namespace de usuário (<em>User Namespace</em>) sem utilizar privilégios de <code class="language-plaintext highlighter-rouge">sudo</code>, mapeando o seu usuário atual para o usuário <code class="language-plaintext highlighter-rouge">root</code> dentro da sessão isolada através do comando:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>unshare <span class="nt">--user</span> <span class="nt">--map-root-user</span> bash
</code></pre></div></div>

<p>Dentro dessa nova sessão, execute os comandos <code class="language-plaintext highlighter-rouge">whoami</code> e <code class="language-plaintext highlighter-rouge">id</code>. Em seguida, abra outro terminal no seu hospedeiro, descubra o PID desse novo shell bash e verifique com qual usuário ele está registrado no sistema executando <code class="language-plaintext highlighter-rouge">ps -o user,pid,comm -p &lt;PID&gt;</code>. O que você observa e por que esse mecanismo é crucial para a segurança de contêineres modernos?</p>

<details>
  <summary>Ver resposta</summary>

  <p>Ao rodar <code class="language-plaintext highlighter-rouge">whoami</code> e <code class="language-plaintext highlighter-rouge">id</code> dentro da sessão isolada, a saída exibirá <code class="language-plaintext highlighter-rouge">root</code> e <code class="language-plaintext highlighter-rouge">uid=0(root) gid=0(root)</code>. Para o ambiente interno, você tem poderes administrativos totais. Porém, ao consultar o mesmo processo no segundo terminal do hospedeiro através do <code class="language-plaintext highlighter-rouge">ps</code>, você verá que o processo continua pertencendo estritamente ao seu usuário normal sem privilégios.</p>

  <p>Esse mecanismo é a base fundamental dos <strong>contêineres sem root</strong> (<em>rootless containers</em>). Se um invasor conseguir explorar uma falha de segurança na sua aplicação web e quebrar a execução dentro do contêiner, para o kernel do hospedeiro ele continua sendo apenas um usuário comum sem privilégio algum. Ele não consegue alterar arquivos de sistema, instalar módulos no kernel ou comprometer o servidor físico.</p>

</details>

<p><strong>2. O Bloqueio de Fork Bombs com o Controller de PIDs</strong></p>

<p>Suba um contêiner em segundo plano restringindo o número total de processos simultâneos com um teto de segurança (por exemplo, <code class="language-plaintext highlighter-rouge">--pids-limit=20</code>):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker run <span class="nt">-d</span> <span class="nt">--name</span> teste-pids <span class="nt">--pids-limit</span><span class="o">=</span>20 alpine <span class="nb">sleep </span>3600
</code></pre></div></div>

<p>Em seguida, conecte-se a ele via terminal interativo (<code class="language-plaintext highlighter-rouge">docker exec -it teste-pids sh</code>) e dispare a clássica <em>fork bomb</em> de shell do ecossistema Unix:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/ <span class="c"># :(){ :|:&amp; };:</span>
</code></pre></div></div>

<p>O que acontece a partir do momento em que a função recursiva atinge o limite? Como o hospedeiro se comporta e qual métrica no cgroup confirma a contenção?</p>

<details>
  <summary>Ver resposta</summary>

  <p>A função recursiva <code class="language-plaintext highlighter-rouge">:</code> define uma rotina que se auto-invoca enviando instâncias para segundo plano através de um pipe (<code class="language-plaintext highlighter-rouge">:|:&amp;</code>), gerando uma duplicação exponencial de tarefas ($2^n$).</p>

  <p>Assim que o número total de processos atinge a cota de 20 (somando o shell <code class="language-plaintext highlighter-rouge">sh</code>, o processo inicial <code class="language-plaintext highlighter-rouge">sleep 3600</code> e as ramificações filhas), qualquer nova chamada de sistema <code class="language-plaintext highlighter-rouge">fork()</code> é sumariamente rejeitada pelo kernel Linux:</p>

  <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sh: can't fork: Resource temporarily unavailable
</code></pre></div>  </div>

  <p>Ao inspecionar o cgroup correspondente no hospedeiro (em <code class="language-plaintext highlighter-rouge">/sys/fs/cgroup/system.slice/docker-&lt;ID&gt;.scope/pids.current</code>), o contador crava no valor máximo configurado (<code class="language-plaintext highlighter-rouge">20</code>). Enquanto um <em>fork bomb</em> tradicional fora de controle congelaria a máquina hospedeira ao esgotar a tabela global de PIDs do sistema operacional, o controlador de PIDs do cgroup v2 enjaula a tempestade estritamente dentro daquele contêiner, mantendo o servidor 100% responsivo e estável.</p>

</details>

<p><strong>3. Múltiplas Camadas e Resolução de Conflitos no OverlayFS</strong></p>

<p>Crie duas pastas de base diferentes: <code class="language-plaintext highlighter-rouge">/tmp/overlay-multi/camada1</code> contendo um arquivo <code class="language-plaintext highlighter-rouge">app.env</code> com o texto <code class="language-plaintext highlighter-rouge">AMB=producao</code> e <code class="language-plaintext highlighter-rouge">/tmp/overlay-multi/camada2</code> contendo um arquivo <code class="language-plaintext highlighter-rouge">app.env</code> com o texto <code class="language-plaintext highlighter-rouge">AMB=desenvolvimento</code>. Monte um OverlayFS passando ambas no parâmetro <code class="language-plaintext highlighter-rouge">lowerdir</code> com a sintaxe <code class="language-plaintext highlighter-rouge">lowerdir=/tmp/overlay-multi/camada2:/tmp/overlay-multi/camada1</code>. Qual conteúdo é exibido ao ler <code class="language-plaintext highlighter-rouge">app.env</code> no diretório montado e qual é a regra de precedência do kernel?</p>

<details>
  <summary>Ver resposta</summary>

  <p>Ao inspecionar o arquivo montado, o valor retornado será <code class="language-plaintext highlighter-rouge">AMB=desenvolvimento</code>.</p>

  <p>A regra do OverlayFS estabelece que as camadas passadas no parâmetro <code class="language-plaintext highlighter-rouge">lowerdir</code> são avaliadas rigorosamente da esquerda para a direita. O primeiro caminho listado tem precedência máxima sobre os subsequentes. É exatamente através dessa mecânica que uma instrução <code class="language-plaintext highlighter-rouge">COPY</code> ou <code class="language-plaintext highlighter-rouge">RUN</code> em um Dockerfile consegue substituir arquivos introduzidos por camadas anteriores da imagem.</p>

</details>

<p><strong>4. Investigando o Mini-Contêiner a Partir do Hospedeiro</strong></p>

<p>Execute o script <code class="language-plaintext highlighter-rouge">mini-conteiner.sh</code>. Enquanto o terminal interno estiver ativo aguardando comandos, abra outro terminal comum no seu hospedeiro e execute os seguintes passos de investigação:</p>

<ol>
  <li>Localize o processo do contêiner com <code class="language-plaintext highlighter-rouge">ps aux | grep mini-conteiner</code>.</li>
  <li>Compare a lista de namespaces de <code class="language-plaintext highlighter-rouge">/proc/&lt;PID&gt;/ns/</code> do processo localizado com os namespaces do processo 1 do host.</li>
  <li>Consulte o arquivo <code class="language-plaintext highlighter-rouge">/sys/fs/cgroup/mini-conteiner-&lt;PID&gt;/memory.current</code> para averiguar o consumo de RAM em bytes.</li>
  <li>Volte ao terminal do contêiner e execute <code class="language-plaintext highlighter-rouge">ps aux</code>. Quantos processos aparecem? Como você explica essa diferença gritante entre a visão do hospedeiro e a visão interna?</li>
</ol>

<details>
  <summary>Ver resposta</summary>

  <p>No hospedeiro, o processo do contêiner aparece listado como um processo comum qualquer, possuindo um identificador de PID regular e elevado (por exemplo, PID 41208). Seus inodes de namespace diferem totalmente dos inodes do hospedeiro, e o cgroup registra o consumo exato daquele processo.</p>

  <p>Já dentro do contêiner, o comando <code class="language-plaintext highlighter-rouge">ps aux</code> exibe apenas dois processos: o <code class="language-plaintext highlighter-rouge">/bin/sh</code> assumindo com orgulho o posto de PID 1 e o próprio executável <code class="language-plaintext highlighter-rouge">ps</code> como PID 2. Essa divergência demonstra na prática o poder do isolamento do kernel: para quem está dentro, o universo se resume àquele ambiente; para quem está fora no sistema hospedeiro, é apenas mais um processo comum consumindo fatias de tempo da CPU.</p>

</details>

<h2 id="o-que-vem-na-parte-2">O Que Vem na Parte 2</h2>

<p>Chegamos ao fim da primeira metade da nossa jornada.</p>

<p>Neste post, despimos o contêiner de todos os seus mitos de marketing. Você viu que não há mágica nem virtualização pesada: existem <em>namespaces</em> estabelecendo o que os processos conseguem ver, <em>cgroups</em> limitando o que eles conseguem gastar de hardware, e <em>sistemas de arquivos de união</em> entregando imagens em camadas sem desperdício de disco. Vimos também como a especificação da OCI padronizou essa fundação em componentes modulares como o <code class="language-plaintext highlighter-rouge">runc</code> e o <code class="language-plaintext highlighter-rouge">containerd</code>.</p>

<p>Mas vamos ser francos: ninguém em sã consciência vai ficar escrevendo scripts manuais em shell script e calculando offsets de montagem de OverlayFS toda vez que precisar colocar uma API em produção às três horas da tarde de uma sexta-feira.</p>

<p>É exatamente aqui que entram as ferramentas modernas de engenharia.</p>

<p>Na <strong>Parte 2 da nossa série</strong> <em>(em breve)</em>, vamos subir para a camada de ferramentas e práticas do mundo real:</p>
<ul>
  <li><strong>Anatomia avançada de Dockerfiles</strong>: como estruturar construções em múltiplos estágios (<em>multi-stage builds</em>) eficientes para compilar aplicações pesadas gerando imagens de produção com menos de cem megabytes;</li>
  <li><strong>O ciclo de vida do contêiner</strong>: as diferenças operacionais críticas entre estados de pausa, parada graciosa com tratamento de sinais <code class="language-plaintext highlighter-rouge">SIGTERM</code> e extermínio abrupto com <code class="language-plaintext highlighter-rouge">SIGKILL</code>;</li>
  <li><strong>Redes no Docker</strong>: a arquitetura de pontes virtuais (<em>bridge</em>), tabelas de roteamento e resolução interna de nomes via DNS integrado;</li>
  <li><strong>Docker Compose</strong>: como orquestrar múltiplos serviços interligados com bancos de dados, filas e caches em uma stack completa;</li>
  <li><strong>A revolução do Podman</strong>: a arquitetura sem daemons centralizados (<em>daemonless</em>), contêineres sem privilégios (<em>rootless</em>) por padrão e a criação nativa de <em>Pods</em> no modelo do Kubernetes;</li>
  <li><strong>Segurança de contêineres</strong>: análise de vulnerabilidades em imagens, restrição de capacidades administrativas do Linux (<em>capabilities</em>) e os perigos fatais de montar o socket do Docker em ambientes compartilhados.</li>
</ul>

<p>Nos vemos na Parte 2 para colocar toda essa fundação em prática no mundo real.</p>

<h2 id="referências">Referências</h2>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p><strong>namespaces(7) - Overview of Linux Namespaces</strong> {<em>Linux Programmer's Manual, man7.org</em>} (<a href="https://man7.org/linux/man-pages/man7/namespaces.7.html">Link</a>) <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p><strong>unshare(1) - Run Program with Some Namespaces Unshared from Parent</strong> {<em>util-linux, man7.org</em>} (<a href="https://man7.org/linux/man-pages/man1/unshare.1.html">Link</a>) <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3">
      <p><strong>nsenter(1) - Run Program in the Namespaces of Another Process</strong> {<em>util-linux, man7.org</em>} (<a href="https://man7.org/linux/man-pages/man1/nsenter.1.html">Link</a>) <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4">
      <p><strong>cgroups(7) - Linux Control Groups Overview</strong> {<em>Linux Programmer's Manual, man7.org</em>} (<a href="https://man7.org/linux/man-pages/man7/cgroups.7.html">Link</a>) <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5">
      <p><strong>Control Group v2 Official Kernel Documentation</strong> {<em>Tejun Heo et al., The Linux Kernel Archives</em>} (<a href="https://docs.kernel.org/admin-guide/cgroup-v2.html">Link</a>) <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:6">
      <p><strong>Overlay Filesystem Documentation</strong> {<em>Miklos Szeredi, The Linux Kernel Archives</em>} (<a href="https://docs.kernel.org/filesystems/overlayfs.html">Link</a>) <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:7">
      <p><strong>Open Container Initiative Runtime Specification (runc)</strong> {<em>Open Container Initiative, GitHub</em>} (<a href="https://github.com/opencontainers/runtime-spec">Link</a>) <a href="#fnref:7" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>[&quot;Eduardo N. S. R.&quot;]</name></author><category term="Tutoriais" /><category term="Docker" /><category term="Contêineres" /><category term="Linux" /><category term="DevOps" /><summary type="html"><![CDATA[Você já rodou docker run centenas de vezes. Digita o comando, aperta enter, um punhado de letrinhas corre pelo terminal e, em questão de meio segundo, você tem uma aplicação inteira rodando em um ambiente isolado com rede, disco e variáveis próprias. Mas se eu te perguntar exatamente o que o kernel Linux fez naquele exato instante de meio segundo, você sabe responder?]]></summary></entry><entry><title type="html">Hotspot Wi-Fi 6 no Debian: Domando o Firmware Intel</title><link href="https://vndmtrx.github.io/posts/wifi6-debian-trixie/" rel="alternate" type="text/html" title="Hotspot Wi-Fi 6 no Debian: Domando o Firmware Intel" /><published>2026-09-29T10:38:00+00:00</published><updated>2026-09-29T10:38:00+00:00</updated><id>https://vndmtrx.github.io/posts/wifi6-debian-trixie</id><content type="html" xml:base="https://vndmtrx.github.io/posts/wifi6-debian-trixie/"><![CDATA[<p>Existe uma ilusão vendida pelas interfaces gráficas modernas de que transformar o computador em ponto de acesso sem fio é um clique e pronto. Você abre o painel da distribuição, clica em "Criar Hotspot", inventa uma senha qualquer e espera que funcione. Na melhor das hipóteses, sobe uma rede WPA2 capenga; na pior, a interface congela, o NetworkManager se perde sozinho e você não faz ideia do que deu errado.</p>

<p>Para quem só quer conectar o celular ou o notebook com um mínimo de controle e desempenho, essa automação de desktop mais atrapalha do que ajuda.</p>

<p>A minha dúvida era bem prática: tenho um computador rodando Debian Trixie, conectado no cabo de rede e com uma placa Intel Wi-Fi 6E AX211 sobrando ali. <em>Será que consigo transformar essa placa num Access Point moderno pra atender meus dispositivos móveis?</em> A ideia era montar algo enxuto: Wi-Fi 6 (802.11ax), WPA3-Personal puro (SAE) e regras simples de <code class="language-plaintext highlighter-rouge">nftables</code>, sem mágica de interface gráfica.</p>

<p>A resposta curta é sim, dá pra fazer. A resposta longa é que o processo virou uma caça aos motivos de o rádio simplesmente se recusar a emitir sinal, esbarrando em microcódigo da Intel, travas regulatórias e mensagens crípticas no <code class="language-plaintext highlighter-rouge">dmesg</code>.</p>

<details class="toc-box">
  <summary><strong>Sumário &amp; Índice de Seções (TOC)</strong></summary>
<ul id="markdown-toc">
  <li><a href="#o-ponto-de-partida-o-hardware-da-máquina" id="markdown-toc-o-ponto-de-partida-o-hardware-da-máquina">O ponto de partida: o hardware da máquina</a></li>
  <li><a href="#a-ideia-separando-as-peças-sem-depender-do-desktop" id="markdown-toc-a-ideia-separando-as-peças-sem-depender-do-desktop">A ideia: separando as peças sem depender do desktop</a></li>
  <li><a href="#o-primeiro-tropeço-por-que-o-rádio-se-recusa-a-emitir" id="markdown-toc-o-primeiro-tropeço-por-que-o-rádio-se-recusa-a-emitir">O primeiro tropeço: por que o rádio se recusa a emitir</a>    <ul>
      <li><a href="#o-paradoxo-do-ap-enabled-com-interface-em-state-down" id="markdown-toc-o-paradoxo-do-ap-enabled-com-interface-em-state-down">O paradoxo do AP-ENABLED com interface em state DOWN</a></li>
      <li><a href="#a-armadilha-do-location-aware-regulatory-e-o-perfil-country-00" id="markdown-toc-a-armadilha-do-location-aware-regulatory-e-o-perfil-country-00">A armadilha do Location Aware Regulatory e o perfil country 00</a></li>
      <li><a href="#a-evidência-no-dmesg-os-erros-de-microcódigo-do-firmware-intel" id="markdown-toc-a-evidência-no-dmesg-os-erros-de-microcódigo-do-firmware-intel">A evidência no dmesg: os erros de microcódigo do firmware Intel</a></li>
      <li><a href="#a-tentação-do-5-ghz-na-faixa-unii-3-e-o-veto-definitivo-de-silício" id="markdown-toc-a-tentação-do-5-ghz-na-faixa-unii-3-e-o-veto-definitivo-de-silício">A tentação do 5 GHz na faixa UNII-3 e o veto definitivo de silício</a></li>
      <li><a href="#a-falácia-do-atualizar-o-kernel-resolve-tudo" id="markdown-toc-a-falácia-do-atualizar-o-kernel-resolve-tudo">A falácia do atualizar o kernel resolve tudo</a></li>
    </ul>
  </li>
  <li><a href="#a-pilha-de-produção-configuração-limpa-e-determinística" id="markdown-toc-a-pilha-de-produção-configuração-limpa-e-determinística">A pilha de produção: configuração limpa e determinística</a>    <ul>
      <li><a href="#isolamento-e-endereçamento-estático-no-ifupdown" id="markdown-toc-isolamento-e-endereçamento-estático-no-ifupdown">Isolamento e endereçamento estático no ifupdown</a></li>
      <li><a href="#serviços-de-suporte-dhcp-e-dns-com-dnsmasq" id="markdown-toc-serviços-de-suporte-dhcp-e-dns-com-dnsmasq">Serviços de suporte: DHCP e DNS com dnsmasq</a></li>
      <li><a href="#a-camada-de-rádio-hostapd-com-wi-fi-6-e-wpa3-puro" id="markdown-toc-a-camada-de-rádio-hostapd-com-wi-fi-6-e-wpa3-puro">A camada de rádio: hostapd com Wi-Fi 6 e WPA3 puro</a></li>
      <li><a href="#firewall-moderno-e-roteamento-com-nftables" id="markdown-toc-firewall-moderno-e-roteamento-com-nftables">Firewall moderno e roteamento com nftables</a></li>
    </ul>
  </li>
  <li><a href="#telemetria-real-286-mbps-1024-qam-e-wpa3-em-24-ghz" id="markdown-toc-telemetria-real-286-mbps-1024-qam-e-wpa3-em-24-ghz">Telemetria real: 286 Mbps, 1024-QAM e WPA3 em 2.4 GHz</a>    <ul>
      <li><a href="#conferindo-o-espectro-com-o-wifiman-no-celular" id="markdown-toc-conferindo-o-espectro-com-o-wifiman-no-celular">Conferindo o espectro com o WiFiman no celular</a></li>
    </ul>
  </li>
  <li><a href="#conclusão-no-fim-das-contas-valeu-a-pena" id="markdown-toc-conclusão-no-fim-das-contas-valeu-a-pena">Conclusão: no fim das contas, valeu a pena?</a></li>
  <li><a href="#referências" id="markdown-toc-referências">Referências</a></li>
</ul>

</details>

<h2 id="o-ponto-de-partida-o-hardware-da-máquina">O ponto de partida: o hardware da máquina</h2>

<p>Antes de sair chutando parâmetros de rádio no escuro, o jeito mais prático é olhar o que realmente está espetado na máquina. Tentar adivinhar configuração sem saber qual driver ou firmware está no comando é o caminho mais rápido pra perder horas sem sair do lugar.</p>

<p>A máquina onde fiz os testes é um desktop rodando Debian Trixie. As peças do quebra-cabeça:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Componente</th>
      <th style="text-align: left">Especificação</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>Sistema Operacional</strong></td>
      <td style="text-align: left">Debian GNU/Linux 13 (Trixie)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Kernel</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Linux 6.12</code> (Debian amd64)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Placa Wi-Fi</strong></td>
      <td style="text-align: left">Intel Wi-Fi 6E AX211 160MHz (arquitetura CNVi)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Driver / Módulos</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">iwlwifi</code> + <code class="language-plaintext highlighter-rouge">iwlmvm</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Firmware Carregado</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">so-a0-gf-a0-89.ucode</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Interface Sem Fio</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">wlp0s20f3</code> (Hostapd / AP)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Interface Uplink</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">enp0s31f6</code> (Cabo Ethernet Gigabit)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Softwares</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">hostapd 2.10</code>, <code class="language-plaintext highlighter-rouge">nftables 1.1.3</code>, <code class="language-plaintext highlighter-rouge">dnsmasq</code></td>
    </tr>
  </tbody>
</table>

<p>O detalhe fundamental dessa tabela é o controlador Intel AX211 com arquitetura CNVi (<em>Connectivity Interface</em>). Diferente de adaptadores PCIe tradicionais onde o processador e o rádio moram na mesma plaquinha dedicada, no modelo CNVi a Intel resolveu dividir o casamento: o cérebro lógico do Wi-Fi fica soldado dentro do chipset da placa-mãe (PCH), enquanto o slot M.2 abriga só os transceptores analógicos de radiofrequência (CRF).</p>

<p>Essa intimidade profunda entre a placa-mãe e o rádio significa uma coisa bem simples: você não manda em quase nada. O microcódigo da Intel tem rédea curta sobre o que o hardware pode ou não fazer, e ele não aceita desaforo do sistema operacional.</p>

<h2 id="a-ideia-separando-as-peças-sem-depender-do-desktop">A ideia: separando as peças sem depender do desktop</h2>

<p>A tentação mais comum ao criar um hotspot no Linux é deixar o NetworkManager cuidar de tudo: ele cria uma ponte virtual, sobe um <code class="language-plaintext highlighter-rouge">dnsmasq</code> temporário onde bem entender e joga um punhado de regras legadas de <code class="language-plaintext highlighter-rouge">iptables</code>.</p>

<p>O problema é que o NetworkManager se comporta igual àquele estagiário excessivamente proativo: você não pediu nada, mas ele vê uma interface dando sopa, assume o controle sem avisar, tenta adivinhar o que você quer e, no primeiro sinal de instabilidade, reseta o link ou decide unilateralmente que a interface virou "não gerenciável". Pra um AP que você só quer deixar quieto funcionando no canto, essa boa vontade é a receita perfeita pra dor de cabeça.</p>

<p>Em vez de brigar com o desktop, a saída foi chutar o NetworkManager pra longe e separar as responsabilidades em camadas simples:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌────────────────────────────────────────────────────────┐
│               PILHA DE SOFTWARE PURA (AP)              │
│                                                        │
│  [NetworkManager]  ──&gt; Isolar interface (unmanaged)    │
│         │                                              │
│  [ifupdown / L3]   ──&gt; Fixar IP estático no boot       │
│         │              (/etc/network/interfaces)       │
│  [dnsmasq / L7]    ──&gt; Subir DHCP e DNS exclusivo      │
│         │              (Sub-rede 192.168.100.0/24)     │
│  [hostapd / L1-L2] ──&gt; Subir rádio 802.11ax + WPA3     │
│         │              (Canal 13, SAE puro, CCMP)      │
│  [nftables / L3-L4]──&gt; Forwarding e NAT Masquerade     │
│                        (Regras atômicas sem iptables)  │
└────────────────────────────────────────────────────────┘
</code></pre></div></div>

<p>Cada peça tem uma função direta:</p>

<ol>
  <li><strong>Camada L3 Estática (<code class="language-plaintext highlighter-rouge">ifupdown</code>):</strong> Assume a interface <code class="language-plaintext highlighter-rouge">wlp0s20f3</code>, garantindo um IP fixo logo no boot sem interferência de applets de desktop.</li>
  <li><strong>Camada de Suporte L7 (<code class="language-plaintext highlighter-rouge">dnsmasq</code>):</strong> Entrega DHCP rápido e DNS na sub-rede sem fio, escutando estritamente nessa interface.</li>
  <li><strong>Camada de Rádio L1/L2 (<code class="language-plaintext highlighter-rouge">hostapd</code>):</strong> Controla a sinalização do rádio 802.11ax, os beacons e o <em>handshake</em> criptográfico em WPA3-Personal (SAE) via Netlink (<code class="language-plaintext highlighter-rouge">nl80211</code>).</li>
  <li><strong>Camada de Encaminhamento L3/L4 (<code class="language-plaintext highlighter-rouge">nftables</code>):</strong> Faz a filtragem de estados via <code class="language-plaintext highlighter-rouge">conntrack</code> e aplica o NAT Masquerade em direção à placa cabeada <code class="language-plaintext highlighter-rouge">enp0s31f6</code>.</li>
</ol>

<p>Na teoria, era só subir os serviços e conectar. Na prática, o rádio tinha outros planos.</p>

<h2 id="o-primeiro-tropeço-por-que-o-rádio-se-recusa-a-emitir">O primeiro tropeço: por que o rádio se recusa a emitir</h2>

<p>Com os arquivos rascunhados, iniciei os serviços pro primeiro teste. O comando <code class="language-plaintext highlighter-rouge">sudo systemctl start hostapd</code> rodou de primeira e o status parecia animador:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>● hostapd.service - Access point and authentication server for Wi-Fi and Ethernet
     Loaded: loaded (/usr/lib/systemd/system/hostapd.service; enabled; preset: enabled)
     Active: active (running) since Sun 2026-09-28 10:15:22 -03; 4s ago
...
Sep 28 10:15:22 debian hostapd[14205]: wlp0s20f3: interface state UNINITIALIZED-&gt;COUNTRY_UPDATE
Sep 28 10:15:22 debian hostapd[14205]: wlp0s20f3: interface state COUNTRY_UPDATE-&gt;ENABLED
Sep 28 10:15:22 debian hostapd[14205]: wlp0s20f3: AP-ENABLED
</code></pre></div></div>

<p>O <code class="language-plaintext highlighter-rouge">hostapd</code> reportava com todas as letras: <code class="language-plaintext highlighter-rouge">AP-ENABLED</code>. Na teoria, o ponto de acesso estava no ar e transmitindo.</p>

<p>Peguei o celular, abri o painel de redes sem fio e nada. O SSID não aparecia na lista de varredura. Esperei alguns minutos, forcei novas buscas e nada mudou. Não havia sinal sendo emitido.</p>

<p>Fui ao terminal investigar o estado da interface com o utilitário <code class="language-plaintext highlighter-rouge">ip</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ip addr show wlp0s20f3
</code></pre></div></div>

<p>O resultado expôs a contradição:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>3: wlp0s20f3: &lt;NO-CARRIER,BROADCAST,MULTICAST,UP&gt; mtu 1500 qdisc noqueue state DOWN group default qlen 1000
    link/ether 02:00:00:00:00:01 brd ff:ff:ff:ff:ff:ff
    inet 192.168.100.1/24 brd 192.168.100.255 scope global wlp0s20f3
       valid_lft forever preferred_lft forever
</code></pre></div></div>

<p>Como é possível o <code class="language-plaintext highlighter-rouge">hostapd</code> indicar que o ponto de acesso está habilitado (<code class="language-plaintext highlighter-rouge">AP-ENABLED</code>) enquanto o kernel Linux classifica o link como <code class="language-plaintext highlighter-rouge">state DOWN</code> e com a flag <code class="language-plaintext highlighter-rouge">NO-CARRIER</code> ativa?</p>

<h3 id="o-paradoxo-do-ap-enabled-com-interface-em-state-down">O paradoxo do AP-ENABLED com interface em state DOWN</h3>

<p>Essa discrepância entre o que o software jura que está fazendo e o que o hardware realmente entrega é a clássica divisão de mundos do Linux: o espaço de usuário (<em>userspace</em>) vive de otimismo; o kernel vive da realidade física do silício.</p>

<p>O <code class="language-plaintext highlighter-rouge">hostapd</code> opera em <em>userspace</em>. Para conversar com os adaptadores modernos, ele conversa via Netlink usando a família <code class="language-plaintext highlighter-rouge">nl80211</code> <sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>. Quando o serviço sobe, ele despacha mensagens pedindo a criação do BSSID lógico, aloca as estruturas em memória e avisa ao subsistema <code class="language-plaintext highlighter-rouge">cfg80211</code> que o AP está autorizado a rodar. Na cabeça do <code class="language-plaintext highlighter-rouge">hostapd</code>, o dever foi cumprido: tá autorizado, logo <code class="language-plaintext highlighter-rouge">AP-ENABLED</code>.</p>

<p>Só que o kernel Linux não cai em papo furado. Uma interface de rede só abandona a flag <code class="language-plaintext highlighter-rouge">NO-CARRIER</code> e atinge o estado <code class="language-plaintext highlighter-rouge">state UP</code> quando o rádio físico reporta que a portadora está ligada de verdade (<code class="language-plaintext highlighter-rouge">LOWER_UP</code>). No cabo de rede, é quando o conector detecta tensão elétrica no cobre. No Wi-Fi, é quando o chip efetivamente liga o transmissor, sintoniza a frequência e joga os primeiros <em>beacons</em> 802.11 no ar.</p>

<p>Se o <code class="language-plaintext highlighter-rouge">hostapd</code> comemora que o AP subiu, mas a interface continua em <code class="language-plaintext highlighter-rouge">NO-CARRIER</code> no kernel, a constatação é límpida (depois de perder um belo par de horas quebrando a cabeça): o microcódigo da Intel simplesmente puxou o freio de mão e se recusou a transmitir um mísero watt de sinal.</p>

<h3 id="a-armadilha-do-location-aware-regulatory-e-o-perfil-country-00">A armadilha do Location Aware Regulatory e o perfil country 00</h3>

<p>Ao investigar por que a interface teimava em não emitir sinal no início dos testes, executei o comando de auditoria regulatória do subsistema sem fio:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>iw reg get
</code></pre></div></div>

<p>A saída foi esclarecedora:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>global
country BR: DFS-FCC
    (2400 - 2483 @ 40), (N/A, 30), (N/A)
    (5150 - 5250 @ 80), (N/A, 27), (N/A), NO-OUTDOOR, AUTO-BW
    (5250 - 5350 @ 80), (N/A, 27), (0 ms), NO-OUTDOOR, DFS, AUTO-BW
    (5470 - 5725 @ 160), (N/A, 27), (0 ms), DFS, AUTO-BW
    (5725 - 5850 @ 80), (N/A, 30), (N/A), AUTO-BW
    (5925 - 7125 @ 320), (N/A, 12), (N/A), NO-OUTDOOR

phy#0 (self-managed)
country 00: DFS-UNSET
    (2402 - 2437 @ 40), (6, 22), (N/A), AUTO-BW, NO-HT40MINUS, NO-80MHZ, NO-160MHZ
    (2422 - 2462 @ 40), (6, 22), (N/A), AUTO-BW, NO-80MHZ, NO-160MHZ
    (2447 - 2482 @ 40), (6, 22), (N/A), AUTO-BW, NO-HT40PLUS, NO-80MHZ, NO-160MHZ
    (5170 - 5190 @ 160), (6, 22), (N/A), NO-OUTDOOR, AUTO-BW, IR-CONCURRENT, NO-HT40MINUS, NO-320MHZ, PASSIVE-SCAN
    (5735 - 5755 @ 160), (6, 22), (N/A), AUTO-BW, IR-CONCURRENT, NO-HT40MINUS, NO-320MHZ, PASSIVE-SCAN
</code></pre></div></div>

<p>Observe a divergência entre as duas seções da saída:</p>

<ol>
  <li>A seção <code class="language-plaintext highlighter-rouge">global</code> informa <code class="language-plaintext highlighter-rouge">country BR</code>, refletindo o banco de dados regulatório que o sistema operacional tentou carregar (<code class="language-plaintext highlighter-rouge">wireless-regdb</code>).</li>
  <li>A seção física <code class="language-plaintext highlighter-rouge">phy#0</code> exibe a indicação <code class="language-plaintext highlighter-rouge">self-managed</code>, vinculada ao código universal <code class="language-plaintext highlighter-rouge">country 00: DFS-UNSET</code>.</li>
</ol>

<p>Essa distinção explica uma dúvida muito comum: <em>por que executar <code class="language-plaintext highlighter-rouge">sudo iw reg set BR</code> altera o <code class="language-plaintext highlighter-rouge">global</code>, mas não surte efeito nenhum na placa <code class="language-plaintext highlighter-rouge">phy#0</code>?</em></p>

<p>No subsistema sem fio do Linux (<code class="language-plaintext highlighter-rouge">cfg80211</code>), os adaptadores dividem-se em duas categorias:</p>
<ul>
  <li><strong>Placas gerenciadas pelo kernel:</strong> Em chipsets abertos como MediaTek (<code class="language-plaintext highlighter-rouge">mt7921e</code>) ou Atheros (<code class="language-plaintext highlighter-rouge">ath9k</code>), o driver repassa a autoridade regulatória diretamente para o kernel. Quando você roda <code class="language-plaintext highlighter-rouge">iw reg set BR</code>, o sistema atualiza as frequências e potências da interface física quase de imediato.</li>
  <li><strong>Placas com domínio auto-gerenciado (<code class="language-plaintext highlighter-rouge">self-managed</code>):</strong> Os adaptadores da Intel possuem a flag interna <code class="language-plaintext highlighter-rouge">WIPHY_FLAG_CUSTOM_REGULATORY</code>. Ao carregar o módulo <code class="language-plaintext highlighter-rouge">iwlwifi</code> <sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>, a placa avisa ao kernel que ela mesma manda nas próprias regras a partir de tabelas gravadas na memória OTP (<em>One-Time Programmable</em>) durante a fabricação. O microcódigo simplesmente ignora os seus palpites.</li>
</ul>

<p>E quando há conflito entre o que o sistema operacional quer e o que a placa física declara, a regra do kernel é implacável: a restrição mais severa sempre vence. Se o sistema operacional autoriza canais abertos e potência alta no Brasil, mas a tabela gravada no silício da placa (<code class="language-plaintext highlighter-rouge">phy#0</code>) bate o pé dizendo que o canal é restrito ou passivo, não tem conversa: o hardware vence e o kernel senta e chora.</p>

<p>Para coroar a burocracia, os adaptadores Intel vêm equipados com o famigerado LAR (<em>Location Aware Regulatory</em>) <sup id="fnref:4"><a href="#fn:4" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>.</p>

<p>O LAR foi desenhado sob a premissa de mobilidade em notebooks: a placa escuta os roteadores ao redor para descobrir se você pousou em Guarulhos ou em Frankfurt antes de emitir rádio. Só tem um pequeno detalhe: o meu computador não sai da minha mesa e passa a maior parte da sua vida espetado na tomada da parede e ligado no cabo de rede. Não tem a menor chance de esse gabinete acordar subitamente na Baviera. Mas a Intel insiste em tratá-lo como se fosse um ultrabook passeando no saguão de um aeroporto internacional.</p>

<p>A lógica do LAR funciona em três passos:</p>
<ol>
  <li>Para descobrir em que canto do planeta está operando, a placa precisa conectar-se como cliente (STA) a algum ponto de acesso ou escutar passivamente anúncios periódicos de quadros 802.11d com o código do país (<em>Country IE</em>).</li>
  <li>Se múltiplos roteadores vizinhos confirmarem que estão no Brasil, o próprio microcódigo da Intel valida o consenso e atualiza a tabela da placa para <code class="language-plaintext highlighter-rouge">BR</code>.</li>
  <li>Mas quando tentamos usar a placa puramente como ponto de acesso autônomo (Master/AP), ela não se conecta a ninguém para herdar essa validação geográfica.</li>
</ol>

<p>Resultado: sem o consenso de outros rádios vizinhos, a placa assume a paranoia máxima e recua para o perfil mundial de contingência: o <code class="language-plaintext highlighter-rouge">country 00</code>.</p>

<p>E o que dita o perfil <code class="language-plaintext highlighter-rouge">country 00</code>?</p>
<ul>
  <li><strong>Faixa de 5 GHz e 6 GHz:</strong> recebe as diretivas mandatórias <code class="language-plaintext highlighter-rouge">PASSIVE-SCAN</code> e <code class="language-plaintext highlighter-rouge">NO-IR</code> (<em>No Initiate Radiation</em>), proibindo categoricamente qualquer emissão de beacons como AP.</li>
  <li><strong>Faixa de 2.4 GHz:</strong> a transmissão é autorizada de 2402 a 2482 MHz, mas com uma trava explícita: <code class="language-plaintext highlighter-rouge">NO-HT40PLUS</code> na faixa superior (canais agregados de 40 MHz para cima são sumariamente vetados).</li>
</ul>

<p>Somado a isso, o NetworkManager rodando em paralelo vivia tentando assumir o controle da interface, forçando-a para baixo toda vez que o <code class="language-plaintext highlighter-rouge">hostapd</code> inicializava. Para isolar as variáveis e estabelecer um ponto de partida limpo e estável, o primeiro passo foi blindar a interface contra o NetworkManager e configurar o rádio no canal canônico mais seguro da literatura: o canal 6 (<code class="language-plaintext highlighter-rouge">channel=6</code>), com largura fixa de 20 MHz.</p>

<h3 id="a-evidência-no-dmesg-os-erros-de-microcódigo-do-firmware-intel">A evidência no dmesg: os erros de microcódigo do firmware Intel</h3>

<p>Durante a bateria de testes com o módulo AX211, experimentei combinações mais agressivas de parâmetros High Efficiency (HE) e canais de 40 MHz em frequências limítrofes.</p>

<p>A resposta do hardware foi imediata. Ao consultar o buffer de mensagens do kernel com <code class="language-plaintext highlighter-rouge">dmesg -T</code>, encontrei o registro de reinício do microcódigo:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Sun Sep 28 10:22:09 2026] iwlwifi 0000:00:14.3: Microcode SW error detected. Restarting 0x0.
[Sun Sep 28 10:22:09 2026] iwlwifi 0000:00:14.3: Loaded firmware version: 89.4d42c933.0 so-a0-gf-a0-89.ucode
[Sun Sep 28 10:22:09 2026] iwlwifi 0000:00:14.3: 0x00000084 | ADVANCED_SYSASSERT
</code></pre></div></div>

<p>O microcódigo <code class="language-plaintext highlighter-rouge">so-a0-gf-a0-89.ucode</code>, rodando no microcontrolador dedicado do AX211, encontrou uma combinação de parâmetros pedida pelo driver <code class="language-plaintext highlighter-rouge">iwlmvm</code> que violava as tabelas rígidas da sua memória.</p>

<p>Em vez de devolver um erro civilizado para o kernel, o firmware entrou em pânico (<code class="language-plaintext highlighter-rouge">ADVANCED_SYSASSERT</code>) e puxou a tomada do próprio chip (<code class="language-plaintext highlighter-rouge">Restarting 0x0</code>). O driver do Linux teve que recarregar o arquivo <code class="language-plaintext highlighter-rouge">.ucode</code> do zero, derrubando a interface e desconectando tudo no susto.</p>

<p>O recado da Intel foi direto e sem meias palavras: ou você joga pelas regras estritas do microcódigo, ou a placa vai reiniciar na sua cara toda vez que você tentar inventar moda.</p>

<h3 id="a-tentação-do-5-ghz-na-faixa-unii-3-e-o-veto-definitivo-de-silício">A tentação do 5 GHz na faixa UNII-3 e o veto definitivo de silício</h3>

<p>Durante a auditoria aprofundada das tabelas de radiofrequência com <code class="language-plaintext highlighter-rouge">iw phy phy0 info</code>, uma descoberta parecia abrir uma brecha tentadora no bloqueio da Intel.</p>

<p>Embora toda a faixa baixa de 5 GHz (canais 36 a 144) estivesse carimbada com <code class="language-plaintext highlighter-rouge">no IR</code>, os canais altos da faixa UNII-3 (canais 149 a 165, de 5745 a 5825 MHz) exibiam uma condição diferente:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Frequencies:
    * 5745.0 MHz [149] (22.0 dBm)
    * 5765.0 MHz [153] (22.0 dBm)
    * 5785.0 MHz [157] (22.0 dBm)
    * 5805.0 MHz [161] (22.0 dBm)
    * 5825.0 MHz [165] (22.0 dBm)
</code></pre></div></div>

<p>No Brasil, sob regulamentação da Anatel e em conformidade com as regras da FCC, a faixa UNII-3 não divide frequências com radares meteorológicos (dispensando o complexo mecanismo DFS) e autoriza emissão de maior potência. Como a placa havia escutado passivamente o ambiente e herdado temporariamente o código geográfico <code class="language-plaintext highlighter-rouge">country BR: DFS-UNSET</code>, a flag restritiva de <code class="language-plaintext highlighter-rouge">no IR</code> parecia ter desaparecido desses cinco canais.</p>

<p>A ideia era tentadora: por que me contentar com os 286 Mbps da faixa de 2.4 GHz se dava pra tentar o canal 149 com canais agregados de 80 MHz (<code class="language-plaintext highlighter-rouge">vht_oper_chwidth=1</code>, <code class="language-plaintext highlighter-rouge">he_oper_chwidth=1</code>) e buscar taxas bem mais altas no Wi-Fi 6?</p>

<p>Ajustei o <code class="language-plaintext highlighter-rouge">hostapd.conf</code> para operar em 5 GHz no canal 149 e reiniciei o daemon. O log de depuração (<code class="language-plaintext highlighter-rouge">hostapd -d</code>) mostrou exatamente onde o microcódigo barra a operação:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wlp0s20f3: interface state UNINITIALIZED-&gt;COUNTRY_UPDATE
Previous country code BR, new country code BR 
nl80211: Regulatory information - country=00
...
nl80211: 5735-5755 @ 160 MHz 22 mBm (no IR)
Frequency 5745 (primary) not allowed for AP mode, flags: 0x20053 NO-IR
Primary frequency not allowed
wlp0s20f3: IEEE 802.11 Hardware does not support configured channel
Could not select hw_mode and channel. (-3)
wlp0s20f3: AP-DISABLED 
</code></pre></div></div>

<p>O log expõe com exatidão onde o firmware da Intel puxa o freio de mão:</p>

<ol>
  <li><strong>A ilusão da escuta passiva:</strong> Enquanto a placa atua apenas escutando o ar em silêncio (<em>scan passivo</em>), o firmware tolera a indicação de <code class="language-plaintext highlighter-rouge">country BR</code>. Dá a falsa impressão de que a frequência alta está pronta pra uso.</li>
  <li><strong>O bloqueio ao ligar a transmissão:</strong> No milissegundo em que o <code class="language-plaintext highlighter-rouge">hostapd</code> tenta colocar a interface em modo Master e ligar o transmissor em 5 GHz, a memória de fábrica do chip (OTP) assume o controle. O microcódigo ignora solenemente a instrução do sistema operacional e força o perfil mundial de contingência (<code class="language-plaintext highlighter-rouge">country=00</code>).</li>
  <li><strong>O recuo forçado:</strong> Sob <code class="language-plaintext highlighter-rouge">country=00</code>, o canal 149 recebe a trava <code class="language-plaintext highlighter-rouge">0x20053 NO-IR</code> (proibido iniciar transmissão). O subsistema <code class="language-plaintext highlighter-rouge">cfg80211</code> do kernel acata a ordem do silício, recusa a operação e derruba a interface para <code class="language-plaintext highlighter-rouge">AP-DISABLED</code>.</li>
</ol>

<p>O motivo dessa sabotagem é uma mistura de regulação e contenção de custos <sup id="fnref:7"><a href="#fn:7" class="footnote" rel="footnote" role="doc-noteref">4</a></sup>. Os adaptadores AX201 e AX211 foram homologados internacionalmente apenas como dispositivos clientes (estações móveis). Testar e certificar um transmissor para operar como ponto de acesso autônomo (Master AP) em 5 GHz e 6 GHz exige processos de homologação caríssimos e baterias de testes de conformidade de radar (DFS) que a Intel não quis pagar para placas de desktop/laptop. Para blindar os seus próprios advogados contra multas de órgãos reguladores mundo afora, a fabricante trancou o microcódigo no silício: você comprou o hardware, mas quem decide onde ele pode transmitir são os advogados da Intel.</p>

<p>Se o seu objetivo de vida for montar um roteador Linux moderno operando em 5 GHz ou 6 GHz a 1.200 Mbps ou 2.400 Mbps, esqueça a Intel. A saída definitiva no mundo open source é trocar o módulo M.2 por chipsets MediaTek <sup id="fnref:8"><a href="#fn:8" class="footnote" rel="footnote" role="doc-noteref">5</a></sup> (como o MT7921 ou MT7922). A MediaTek adotou a postura elegante de manter drivers 100% livres no kernel (<code class="language-plaintext highlighter-rouge">mt7921e</code>), sem essa palhaçada de LAR e autorizando operação Master plena em 5 GHz.</p>

<p>No ecossistema Intel AX, contudo, a realidade é essa: o canal de 2.4 GHz em 20 MHz é o único refúgio seguro onde o microcódigo nos deixa brincar em paz.</p>

<h3 id="a-falácia-do-atualizar-o-kernel-resolve-tudo">A falácia do atualizar o kernel resolve tudo</h3>

<p>Em fóruns e discussões na internet, a resposta automática para qualquer problema envolvendo drivers Wi-Fi no Linux é o clichê habitual: "compila o kernel 6.13, 6.14 ou espera o Linux 7.x nos backports que resolve".</p>

<p>Essa recomendação ignora a fronteira física do problema:</p>

<ol>
  <li><strong>O que não muda com kernels novos:</strong> A trava regulatória do LAR e as restrições <code class="language-plaintext highlighter-rouge">NO-IR</code> em 5 GHz/6 GHz para modo AP residem no firmware e na memória OTP gravada no silício pela Intel. Nenhum kernel, por mais recente que seja, tem o poder de sobrepor essa verificação sem que você adulterasse o código do driver e recompilasse o microcódigo fechado da Intel.</li>
  <li><strong>O que muda de verdade com kernels novos:</strong> A evolução da árvore principal do kernel traz melhorias reais na sincronização de filas de transmissão concorrentes (<code class="language-plaintext highlighter-rouge">txqs</code>), redução de <em>race conditions</em> no driver <code class="language-plaintext highlighter-rouge">iwlwifi</code> quando múltiplos dispositivos comunicam-se via Wi-Fi 6 e suporte a revisões mais recentes da API de firmware (como as transições da API 80 para a 89 e 90).</li>
</ol>

<p>No nosso caso, o kernel Debian padrão (Linux 6.12) com o firmware da série 89 provou-se totalmente estável, contanto que as restrições do rádio fossem respeitadas na camada de configuração.</p>

<h2 id="a-pilha-de-produção-configuração-limpa-e-determinística">A pilha de produção: configuração limpa e determinística</h2>

<p>Com a causa raiz compreendida e os limites do hardware devidamente mapeados, passamos à implementação da pilha de produção. Cada componente foi configurado de forma declarativa e com inicialização gerenciada pelo <code class="language-plaintext highlighter-rouge">systemd</code>.</p>

<h3 id="isolamento-e-endereçamento-estático-no-ifupdown">Isolamento e endereçamento estático no ifupdown</h3>

<p>Para que o <code class="language-plaintext highlighter-rouge">hostapd</code> assuma a interface sem que o NetworkManager tente desconectá-la ou atribuir endereços via DHCP de terceiros, declaramos o dispositivo diretamente no <code class="language-plaintext highlighter-rouge">/etc/network/interfaces</code>.</p>

<p>No Debian, o NetworkManager respeita por padrão os dispositivos configurados no <code class="language-plaintext highlighter-rouge">ifupdown</code> e os coloca em modo não gerenciado (<em>unmanaged</em>):</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># /etc/network/interfaces
</span><span class="na">auto</span><span class="w"> </span><span class="na">wlp0s20f3</span><span class="w">
</span><span class="na">iface</span><span class="w"> </span><span class="na">wlp0s20f3</span><span class="w"> </span><span class="na">inet</span><span class="w"> </span><span class="na">static</span><span class="w">
    </span><span class="na">address</span><span class="w"> </span><span class="na">192.168.100.1</span><span class="w">
    </span><span class="na">netmask</span><span class="w"> </span><span class="na">255.255.255.0</span><span class="w">
</span></code></pre></div></div>
<p><em>Configuração estática da camada L3 para o adaptador sem fio.</em></p>

<p>Para fechar qualquer brecha onde o NetworkManager pudesse recapturar a interface após um reinício de sessão gráfica ou atualização de pacote, adicionamos uma regra de blindagem explícita no <code class="language-plaintext highlighter-rouge">/etc/NetworkManager/conf.d/99-unmanaged.conf</code>:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># /etc/NetworkManager/conf.d/99-unmanaged.conf
</span><span class="nn">[keyfile]</span><span class="w">
</span><span class="py">unmanaged-devices</span><span class="p">=</span><span class="s">interface-name:wlp0s20f3</span>
</code></pre></div></div>
<p><em>Blindagem definitiva para impedir qualquer interferência de daemons de desktop.</em></p>

<p>Essa configuração combinada garante que, durante a fase de inicialização da rede (<code class="language-plaintext highlighter-rouge">networking.service</code>), a interface <code class="language-plaintext highlighter-rouge">wlp0s20f3</code> receba imediatamente o endereço de gateway <code class="language-plaintext highlighter-rouge">192.168.100.1/24</code>, ficando totalmente invisível para os applets de bandeja do desktop.</p>

<h3 id="serviços-de-suporte-dhcp-e-dns-com-dnsmasq">Serviços de suporte: DHCP e DNS com dnsmasq</h3>

<p>Para distribuir endereços e resolver nomes com agilidade, o <code class="language-plaintext highlighter-rouge">dnsmasq</code> <sup id="fnref:6"><a href="#fn:6" class="footnote" rel="footnote" role="doc-noteref">6</a></sup> continua imbatível: consome meia dúzia de megabytes de RAM e responde quase instantaneamente.</p>

<p>O arquivo <code class="language-plaintext highlighter-rouge">/etc/dnsmasq.conf</code> foi moldado para isolar o serviço estritamente na interface sem fio, sem vazar requisições para a rede cabeada (<code class="language-plaintext highlighter-rouge">enp0s31f6</code>):</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># /etc/dnsmasq.conf
</span><span class="py">interface</span><span class="p">=</span><span class="s">wlp0s20f3</span>
<span class="py">listen-address</span><span class="p">=</span><span class="s">192.168.100.1</span>
<span class="na">bind-dynamic</span><span class="w">
</span><span class="py">dhcp-range</span><span class="p">=</span><span class="s">192.168.100.10,192.168.100.100,255.255.255.0,12h</span>
<span class="py">dhcp-option</span><span class="p">=</span><span class="s">option:dns-server,1.1.1.1,8.8.8.8</span>
</code></pre></div></div>
<p><em>Configuração do dnsmasq com amarração dinâmica na interface sem fio.</em></p>

<p>O parâmetro <code class="language-plaintext highlighter-rouge">bind-dynamic</code> aqui é o segredo: diferente do rígido <code class="language-plaintext highlighter-rouge">bind-interfaces</code>, ele permite que o <code class="language-plaintext highlighter-rouge">dnsmasq</code> aguarde e se vincule à interface assim que ela receber o IP estático, sem capotar o serviço com erro de socket se o daemon subir alguns milissegundos antes da rede durante o boot.</p>

<h3 id="a-camada-de-rádio-hostapd-com-wi-fi-6-e-wpa3-puro">A camada de rádio: hostapd com Wi-Fi 6 e WPA3 puro</h3>

<p>Esta é a peça central do quebra-cabeça. O <code class="language-plaintext highlighter-rouge">/etc/hostapd/hostapd.conf</code> reúne as diretivas de rádio para operar em 2.4 GHz contornando o LAR da Intel, mas ativando os recursos do padrão 802.11ax (Wi-Fi 6) <sup id="fnref:5"><a href="#fn:5" class="footnote" rel="footnote" role="doc-noteref">7</a></sup> e a segurança do WPA3-Personal puro (SAE):</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># /etc/hostapd/hostapd.conf
</span><span class="py">interface</span><span class="p">=</span><span class="s">wlp0s20f3</span>
<span class="py">driver</span><span class="p">=</span><span class="s">nl80211</span>
<span class="py">ssid</span><span class="p">=</span><span class="s">Debian-Hotspot-AX</span>
<span class="w">
</span><span class="c"># Domínio regulatório e governança de RF
</span><span class="py">country_code</span><span class="p">=</span><span class="s">BR</span>
<span class="py">ieee80211d</span><span class="p">=</span><span class="s">1</span>
<span class="w">
</span><span class="c"># Socket UNIX para gerência e telemetria com hostapd_cli
</span><span class="py">ctrl_interface</span><span class="p">=</span><span class="s">/run/hostapd</span>
<span class="py">ctrl_interface_group</span><span class="p">=</span><span class="s">0</span>
<span class="w">
</span><span class="c"># Banda e Canal (2.4 GHz - Canal 13 fora do engarrafamento)
</span><span class="py">hw_mode</span><span class="p">=</span><span class="s">g</span>
<span class="py">channel</span><span class="p">=</span><span class="s">13</span>
<span class="w">
</span><span class="c"># Otimização física: Preâmbulo Curto (96 µs vs 192 µs)
</span><span class="py">preamble</span><span class="p">=</span><span class="s">1</span>
<span class="w">
</span><span class="c"># Criptografia WPA3-Personal Puro (SAE) e blindagem contra timing attacks
</span><span class="py">wpa</span><span class="p">=</span><span class="s">2</span>
<span class="py">wpa_key_mgmt</span><span class="p">=</span><span class="s">SAE</span>
<span class="py">rsn_pairwise</span><span class="p">=</span><span class="s">CCMP</span>
<span class="py">ieee80211w</span><span class="p">=</span><span class="s">2</span>
<span class="py">sae_pwe</span><span class="p">=</span><span class="s">2</span>
<span class="py">wpa_passphrase</span><span class="p">=</span><span class="s">SuaSenhaForteAqui</span>
<span class="w">
</span><span class="c"># QoS e alicerce do Wi-Fi 4 (WMM obrigatório para HT/HE)
</span><span class="py">wmm_enabled</span><span class="p">=</span><span class="s">1</span>
<span class="py">ieee80211n</span><span class="p">=</span><span class="s">1</span>
<span class="py">ht_capab</span><span class="p">=</span><span class="s">[SHORT-GI-20][TX-STBC][RX-STBC1]</span>
<span class="w">
</span><span class="c"># Wi-Fi 6 (802.11ax - High Efficiency)
</span><span class="py">ieee80211ax</span><span class="p">=</span><span class="s">1</span>
<span class="py">he_su_beamformer</span><span class="p">=</span><span class="s">1</span>
<span class="py">he_su_beamformee</span><span class="p">=</span><span class="s">1</span>
<span class="py">he_oper_chwidth</span><span class="p">=</span><span class="s">0</span>
<span class="w">
</span><span class="py">dtim_period</span><span class="p">=</span><span class="s">2</span>
</code></pre></div></div>
<p><em>Arquivo hostapd.conf afinado no canal 13 com WPA3-SAE, extensões 802.11ax e preâmbulo curto.</em></p>

<p>Para entender o que cada uma dessas diretivas está orquestrando no rádio:</p>

<ul>
  <li><strong>Identificação da interface e sinalização (<code class="language-plaintext highlighter-rouge">interface</code>, <code class="language-plaintext highlighter-rouge">driver</code>, <code class="language-plaintext highlighter-rouge">ssid</code>, <code class="language-plaintext highlighter-rouge">country_code</code>, <code class="language-plaintext highlighter-rouge">ieee80211d</code>):</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">interface=wlp0s20f3</code>: O nome do adaptador físico gerado pelo systemd com base no barramento PCIe/CNVi.</li>
      <li><code class="language-plaintext highlighter-rouge">driver=nl80211</code>: A interface Netlink moderna do Linux. Esqueça drivers legados como <code class="language-plaintext highlighter-rouge">wext</code>: o <code class="language-plaintext highlighter-rouge">nl80211</code> é a única pilha que conversa nativamente com as extensões 802.11n, ac e ax.</li>
      <li><code class="language-plaintext highlighter-rouge">country_code=BR</code> e <code class="language-plaintext highlighter-rouge">ieee80211d=1</code>: Injeta nos beacons periódicos o código do Brasil. Isso avisa aos clientes conectados em que país estão operando, alinhando as potências de transmissão às normas locais.</li>
      <li><code class="language-plaintext highlighter-rouge">ssid=Debian-Hotspot-AX</code>: O nome da rede que vai aparecer nos dispositivos.</li>
    </ul>
  </li>
  <li><strong>Socket de controle local (<code class="language-plaintext highlighter-rouge">ctrl_interface</code>, <code class="language-plaintext highlighter-rouge">ctrl_interface_group</code>):</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">ctrl_interface=/run/hostapd</code>: Cria o socket UNIX para que utilitários como o <code class="language-plaintext highlighter-rouge">hostapd_cli</code> consultem métricas de sinal, modulação e estados de associação em tempo real sem precisar reiniciar o serviço.</li>
      <li><code class="language-plaintext highlighter-rouge">ctrl_interface_group=0</code>: Restringe o socket ao <code class="language-plaintext highlighter-rouge">root</code> para que nenhum processo não autorizado invente de mexer no rádio.</li>
    </ul>
  </li>
  <li><strong>Banda, canal deserto e corte de overhead (<code class="language-plaintext highlighter-rouge">hw_mode</code>, <code class="language-plaintext highlighter-rouge">channel</code>, <code class="language-plaintext highlighter-rouge">preamble</code>):</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">hw_mode=g</code>: Define a operação na faixa de 2.4 GHz. Mesmo operando em Wi-Fi 6, o <code class="language-plaintext highlighter-rouge">hostapd</code> exige o modo base <code class="language-plaintext highlighter-rouge">g</code> para montar o mapa inicial de frequências antes de injetar as extensões modernas.</li>
      <li><code class="language-plaintext highlighter-rouge">channel=13</code>: Frequência central de 2472 MHz em 20 MHz de largura. No perfil <code class="language-plaintext highlighter-rouge">country 00</code>, a faixa alta de 2.4 GHz não possui a trava <code class="language-plaintext highlighter-rouge">NO-IR</code>. Enquanto os canais 1, 6 e 11 viviam engarrafados pelos vizinhos, o canal 13 revelou-se um refúgio totalmente limpo.</li>
      <li><code class="language-plaintext highlighter-rouge">preamble=1</code>: Força o preâmbulo curto na camada física (PLCP). O preâmbulo longo herdado do 802.11b consome 192 µs de sinalização a cada pacote; ao cortar para 96 µs, sobra mais tempo útil de ar (<em>airtime</em>) para transferir dados de verdade.</li>
    </ul>
  </li>
</ul>

<div class="markdown-alert markdown-alert-warning"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-alert" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg> Warning</p><p><strong>O dogma do canal 13 (ou: Anatel, por favor não me processe):</strong>
Qualquer manual clássico de redes sem fio vai repetir como um mantra que você <em>só deve usar os canais 1, 6 e 11</em> em 2.4 GHz. Os motivos para esse pânico são bem conhecidos: primeiro, nos Estados Unidos a FCC proíbe ou limita severamente o canal 13, o que faz aparelhos importados de lá (ou configurados com código de país americano) ficarem completamente cegos sem enxergar o seu SSID; segundo, o canal 13 sobrepõe-se parcialmente ao canal 11, gerando interferência de canal adjacente em vez da contenção ordenada de mesmo canal. Mas no Brasil (sob regulação da Anatel) e na Europa, o canal 13 é perfeitamente legal e homologado. Diante de um ambiente com dezenas de roteadores se esmurrando nos canais 1, 6 e 11, e sabendo que meus aparelhos pessoais suportam o canal 13 com folga, explorar esse refúgio deserto foi a melhor decisão prática possível: saímos de um congestionamento infernal para uma pista totalmente livre.</p>
</div>

<ul>
  <li><strong>WPA3-Personal puro e blindagem de enlace (<code class="language-plaintext highlighter-rouge">wpa</code>, <code class="language-plaintext highlighter-rouge">wpa_key_mgmt</code>, <code class="language-plaintext highlighter-rouge">rsn_pairwise</code>, <code class="language-plaintext highlighter-rouge">ieee80211w</code>, <code class="language-plaintext highlighter-rouge">sae_pwe</code>):</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">wpa_key_mgmt=SAE</code>: Ativa o <em>Simultaneous Authentication of Equals</em> (RFC 7664), base do WPA3. Adeus PSK tradicional: o handshake Dragonfly usa prova de conhecimento zero, tornando capturas de ar totalmente imunes a ataques de dicionário offline com Hashcat ou Aircrack-ng.</li>
      <li><code class="language-plaintext highlighter-rouge">sae_pwe=2</code>: Força o método Hash-to-Element (H2E). Isso blinda a negociação contra os ataques de canal lateral conhecidos como <em>Dragonblood</em>, que exploravam variações de tempo no método antigo de caça-e-bicagem.</li>
      <li><code class="language-plaintext highlighter-rouge">ieee80211w=2</code>: Torna os <em>Protected Management Frames</em> (PMF, 802.11w) estritamente obrigatórios. Com ele, até os quadros de gerência da rede são assinados, neutralizando aqueles ataques clássicos de derrubar clientes injetando pacotes de desautenticação (<em>deauth flood</em>).</li>
      <li><em>E o isolamento de clientes?</em> Se o seu objetivo for um hotspot para visitantes, vale adicionar a diretiva <code class="language-plaintext highlighter-rouge">ap_isolate=1</code> para impedir que os clientes se enxerguem na camada L2. Como no meu caso a rede atende exclusivamente os meus próprios aparelhos (e eu quero transferir arquivos locais entre celular e notebook sem intermediários), deixei o parâmetro de fora.</li>
    </ul>
  </li>
  <li><strong>Wi-Fi 6 High Efficiency e QoS (<code class="language-plaintext highlighter-rouge">wmm_enabled</code>, <code class="language-plaintext highlighter-rouge">ieee80211n</code>, <code class="language-plaintext highlighter-rouge">ht_capab</code>, <code class="language-plaintext highlighter-rouge">ieee80211ax</code>, <code class="language-plaintext highlighter-rouge">he_*</code>):</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">wmm_enabled=1</code>: Habilita as filas de QoS (IEEE 802.11e). Parâmetro obrigatório: sem WMM, as especificações 802.11n e 802.11ax rebaixam compulsoriamente a conexão para o teto de 54 Mbps do jurássico 802.11g.</li>
      <li><code class="language-plaintext highlighter-rouge">ht_capab=[SHORT-GI-20][TX-STBC][RX-STBC1]</code>: Ativa o intervalo de guarda curto (400 ns) e o <em>Space-Time Block Coding</em>, que ajuda o sinal a contornar reflexões nas paredes do cômodo transmitindo cópias redundantes e ortogonais pelas antenas.</li>
      <li><code class="language-plaintext highlighter-rouge">ieee80211ax=1</code>: Aciona o motor do Wi-Fi 6 (High Efficiency), liberando o uso de subportadoras densas e a modulação 1024-QAM.</li>
      <li><code class="language-plaintext highlighter-rouge">he_su_beamformer=1</code>: Ativa a formação de feixe (<em>beamforming</em>). O AP analisa o estado do canal reportado pelo smartphone e ajusta a fase do sinal em cada antena para que as ondas se somem de forma construtiva exatamente onde o aparelho está.</li>
    </ul>
  </li>
</ul>

<h3 id="firewall-moderno-e-roteamento-com-nftables">Firewall moderno e roteamento com nftables</h3>

<p>Com o rádio transmitindo e os endereços sendo distribuídos, o passo final é garantir que os pacotes da rede sem fio cheguem à internet pela placa cabeada <code class="language-plaintext highlighter-rouge">enp0s31f6</code>.</p>

<p>Primeiro, habilitamos o encaminhamento de pacotes no kernel criando o arquivo de persistência no <code class="language-plaintext highlighter-rouge">sysctl</code>:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># /etc/sysctl.d/99-ip-forward.conf
</span><span class="py">net.ipv4.ip_forward</span><span class="p">=</span><span class="s">1</span>
</code></pre></div></div>
<p><em>Habilitação do roteamento IPv4 no kernel Linux.</em></p>

<p>Para aplicar imediatamente sem reiniciar:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>sysctl <span class="nt">-p</span> /etc/sysctl.d/99-ip-forward.conf
</code></pre></div></div>

<p>Antes, eu resolvia esse roteamento com três regras clássicas e imperativas de <code class="language-plaintext highlighter-rouge">iptables</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>iptables <span class="nt">-t</span> nat <span class="nt">-A</span> POSTROUTING <span class="nt">-s</span> 192.168.100.0/24 <span class="nt">-o</span> enp0s31f6 <span class="nt">-j</span> MASQUERADE
<span class="nb">sudo </span>iptables <span class="nt">-A</span> FORWARD <span class="nt">-i</span> wlp0s20f3 <span class="nt">-o</span> enp0s31f6 <span class="nt">-j</span> ACCEPT
<span class="nb">sudo </span>iptables <span class="nt">-A</span> FORWARD <span class="nt">-m</span> conntrack <span class="nt">--ctstate</span> RELATED,ESTABLISHED <span class="nt">-j</span> ACCEPT
</code></pre></div></div>
<p><em>As três regras clássicas de iptables para NAT e encaminhamento de pacotes.</em></p>

<p>Essas três linhas resolvem o básico perfeitamente. No entanto, no Debian moderno o framework padrão do Netfilter é o <code class="language-plaintext highlighter-rouge">nftables</code> <sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">8</a></sup>. Em vez de manter comandos soltos em scripts de inicialização ou depender de pacotes legados de persistência (<code class="language-plaintext highlighter-rouge">iptables-persistent</code>), o caminho mais limpo é consolidar essa tradução direta de forma declarativa e atômica no <code class="language-plaintext highlighter-rouge">/etc/nftables.conf</code>:</p>

<pre><code class="language-nft">#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    chain input {
        type filter hook input priority filter; policy accept;
    }
    chain forward {
        type filter hook forward priority filter; policy drop;
        ct state { established, related } accept
        iifname "wlp0s20f3" oifname "enp0s31f6" accept
    }
    chain output {
        type filter hook output priority filter; policy accept;
    }
}

table ip nat {
    chain postrouting {
        type nat hook postrouting priority srcnat; policy accept;
        ip saddr 192.168.100.0/24 oifname "enp0s31f6" masquerade
    }
}
</code></pre>
<p><em>Regras nativas do nftables com correspondência direta às três regras de iptables.</em></p>

<p>Note como a tradução entre os dois modelos é direta e sem rodeios:</p>
<ol>
  <li><code class="language-plaintext highlighter-rouge">ct state { established, related } accept</code>: Substitui exatamente o <code class="language-plaintext highlighter-rouge">-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT</code>, permitindo que respostas de pacotes que saíram voltem sem impedimentos.</li>
  <li><code class="language-plaintext highlighter-rouge">iifname "wlp0s20f3" oifname "enp0s31f6" accept</code>: É a tradução de <code class="language-plaintext highlighter-rouge">-i wlp0s20f3 -o enp0s31f6 -j ACCEPT</code>, autorizando os pacotes que chegam pelo Wi-Fi a cruzarem em direção à interface cabeada. No <code class="language-plaintext highlighter-rouge">nftables</code>, a política padrão do chain <code class="language-plaintext highlighter-rouge">forward</code> foi definida como <code class="language-plaintext highlighter-rouge">drop</code>, fechando o encaminhamento para qualquer outro sentido.</li>
  <li><code class="language-plaintext highlighter-rouge">ip saddr 192.168.100.0/24 oifname "enp0s31f6" masquerade</code>: Faz exatamente o mesmo papel do <code class="language-plaintext highlighter-rouge">-t nat -A POSTROUTING -s 192.168.100.0/24 -o enp0s31f6 -j MASQUERADE</code>, reescrevendo o IP de origem dos pacotes para o IP da interface cabeada.</li>
</ol>

<div class="markdown-alert markdown-alert-tip"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-light-bulb" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"></path></svg> Tip</p><p><strong>Isolamento de portas locais para redes de visitantes:</strong>
Como o objetivo principal aqui é um ambiente de desenvolvimento (onde costumo subir serviços locais no computador e testá-los no notebook ou no celular pela rede sem fio), mantemos o chain <code class="language-plaintext highlighter-rouge">input</code> com <code class="language-plaintext highlighter-rouge">policy accept;</code>. Se o seu caso for um hotspot estritamente para visitantes ou dispositivos não confiáveis, vale a pena restringir o tráfego que entra pela interface sem fio apenas a portas essenciais como DNS (<code class="language-plaintext highlighter-rouge">udp/tcp 53</code>) e DHCP (<code class="language-plaintext highlighter-rouge">udp 67</code>), descartando as demais conexões ao host com regras específicas de <code class="language-plaintext highlighter-rouge">drop</code>.</p>
</div>

<p>Para carregar o conjunto de regras e garantir persistência nos próximos boots:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl <span class="nb">enable</span> <span class="nt">--now</span> nftables
</code></pre></div></div>

<h2 id="telemetria-real-286-mbps-1024-qam-e-wpa3-em-24-ghz">Telemetria real: 286 Mbps, 1024-QAM e WPA3 em 2.4 GHz</h2>

<p>Com a pilha completa rodando, conectei um smartphone com suporte a Wi-Fi 6 à rede recém-criada. O aparelho associou-se no mesmo instante, exibindo o ícone de rede segura com o numeral 6 ao lado das barrinhas de sinal.</p>

<p>A ironia do momento é deliciosa: compramos uma placa batizada com pompa de Wi-Fi 6E, com suporte teórico a 6 GHz, canais nababescos de 160 MHz e promessa de gigabits por segundo, para no final do dia comemorar 286 Mbps cravados em 2.4 GHz no canal 13. É o equivalente exato a comprar uma Ferrari para rodar na primeira marcha em estrada de terra de condomínio rural. Mas hey: é uma Ferrari perfeitamente regulada, com WPA3 puro e sem bater na lataria dos vizinhos.</p>

<p>Para auditar o que estava acontecendo na camada física do rádio, consultei o subsistema de rede com a ferramenta <code class="language-plaintext highlighter-rouge">iw</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>iw dev wlp0s20f3 station dump
</code></pre></div></div>

<p>A telemetria coletada no host confirmou a negociação das taxas:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Station 22:39:6c:xx:xx:xx (on wlp0s20f3)
    inactive time:  3448 ms
    rx bytes:       1442331
    rx packets:     8110
    tx bytes:       733731
    tx packets:     2580
    signal:         -29 [-29, -42] dBm
    tx bitrate:     286.7 MBit/s HE-MCS 11 HE-NSS 2 HE-GI 0 HE-DCM 0
    authorized:     yes
    authenticated:  yes
    associated:     yes
    preamble:       short
    WMM/WME:        yes
    MFP:            yes
    DTIM period:    2
    short preamble: yes
    connected time: 573 seconds
</code></pre></div></div>

<p>Os dados dessa captura merecem uma leitura honesta:</p>

<ul>
  <li><strong>Taxa de transferência teórica vs. prática:</strong> O link cravou em 286.7 MBit/s de TX. Na norma IEEE 802.11ax, um canal de 20 MHz com modulação 1024-QAM (MCS 11) e Guard Interval curto (0.8 µs) entrega 143.38 Mbps por stream. Multiplicado por dois fluxos espaciais (<code class="language-plaintext highlighter-rouge">HE-NSS 2</code>), batemos no teto físico absoluto de 286.8 Mbps. Só não confunda taxa física (PHY) com milagre: descontando o overhead de pacotes, colisões e confirmações do CSMA/CA, a vazão real utilizável (<em>goodput</em> em TCP medido via <code class="language-plaintext highlighter-rouge">iperf3</code>) fica na casa dos 180 a 200 Mbps. Para navegar no celular e rodar vídeos em 4K, é mais do que sobra.</li>
  <li><strong>Preâmbulo curto ativo (<code class="language-plaintext highlighter-rouge">preamble: short</code>):</strong> A diretiva <code class="language-plaintext highlighter-rouge">preamble=1</code> negociada com a estação poupou 96 µs de sinalização a cada pacote transmitido.</li>
  <li><strong>Modulação no talo (HE-MCS 11):</strong> O rádio sustentou a modulação mais densa da norma: 1024-QAM, empacotando 10 bits por símbolo sob um sinal cristalino de -29 dBm.</li>
  <li><strong>Proteção de quadros confirmada (<code class="language-plaintext highlighter-rouge">MFP: yes</code>):</strong> O hardware atestou o <em>Management Frame Protection</em>, garantindo que ninguém derruba o link com ataques bobos de desautenticação.</li>
</ul>

<p>Para fechar o diagnóstico de segurança, consultei o estado da associação através do socket do <code class="language-plaintext highlighter-rouge">hostapd_cli</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>hostapd_cli all_sta
</code></pre></div></div>

<p>O relatório retornou os detalhes da negociação criptográfica:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>22:39:6c:xx:xx:xx
flags=[AUTH][ASSOC][AUTHORIZED][SHORT_PREAMBLE][WMM][MFP][HT][HE]
dot11RSNAStatsSelectedPairwiseCipher=00-0f-ac-4 (CCMP-128)
AKMSuiteSelector=00-0f-ac-8 (SAE / WPA3-Personal)
sae_group=19 (NIST P-256 Curve)
</code></pre></div></div>

<p>A negociação WPA3-Personal foi concluída com sucesso: o seletor AKM foi definido em <code class="language-plaintext highlighter-rouge">00-0f-ac-8</code> (SAE puro), com a curva elíptica NIST P-256 (<code class="language-plaintext highlighter-rouge">sae_group=19</code>) e cifras CCMP-128 para os dados em trânsito.</p>

<h3 id="conferindo-o-espectro-com-o-wifiman-no-celular">Conferindo o espectro com o WiFiman no celular</h3>

<p>Para verificar o comportamento da rede do ponto de vista de quem está na ponta, abri o aplicativo WiFiman no próprio smartphone para auditar o espectro de radiofrequência ao redor.</p>

<p>A inspeção em campo trouxe resultados ainda mais interessantes:</p>

<ul>
  <li><strong>O oásis no espectro:</strong> No gráfico de densidade de radiofrequência, os canais 1, 6 e 11 aparecem tomados por uma sobreposição caótica de redes institucionais e corporativas competindo pelo meio (dezenas de SSIDs disputando espaço entre -50 dBm e -80 dBm). O canal 13 aparece como um platô isolado na borda direita (operando entre 2462 e 2482 MHz, com pico em 2472 MHz), registrando sinal estável de -30 dBm a -36 dBm sem qualquer outra rede sobreposta.</li>
  <li><strong>Latência e estabilidade:</strong> No monitor de desempenho em tempo real, a latência do cliente até o gateway local (<code class="language-plaintext highlighter-rouge">192.168.100.1</code>) ficou em 8 ms (e 67 ms para a internet em <code class="language-plaintext highlighter-rouge">8.8.8.8</code>), com 0% de perda de pacotes. Fugir da disputa de meio dos canais saturados praticamente zerou as filas de contenção.</li>
  <li><strong>Simetria física em 286 Mbps:</strong> O monitor de enlace do aparelho registrou velocidade física simétrica: 286 Mbps de download e 286 Mbps de upload no enlace PHY do Wi-Fi 6 (MIMO 2x2 em 20 MHz).</li>
  <li><strong>Proteção mandatória (<code class="language-plaintext highlighter-rouge">MFPR</code>):</strong> O smartphone identificou a rede como Wi-Fi 6 (802.11ax) com fabricante Intel Corporate e as flags <code class="language-plaintext highlighter-rouge">[RSN-SAE-CCMP-128][ESS][MFPR][MFPC][SAE]</code>. A presença da flag <code class="language-plaintext highlighter-rouge">MFPR</code> (<em>Management Frame Protection Required</em>) atesta que o aparelho móvel assumiu o modo estrito de segurança, descartando qualquer pacote não autenticado.</li>
</ul>

<h2 id="conclusão-no-fim-das-contas-valeu-a-pena">Conclusão: no fim das contas, valeu a pena?</h2>

<p>O que começou como uma simples curiosidade prática, querendo saber se dava pra subir um ponto de acesso Wi-Fi 6 no Debian pro celular sem depender de mágicas quebradas de interface gráfica, acabou virando uma expedição arqueológica pelo subsistema de rede do Linux e pelas idiossincrasias do microcódigo da Intel.</p>

<p>Por trás da promessa sedutora do "criar hotspot em um clique", existe um abismo de detalhes. Os adaptadores da Intel foram paridos com a premissa de serem apenas clientes; tentar forçar a placa a virar um roteador 5 GHz autônomo sem entender o que a fabricante fez no silício só resulta em mensagens crípticas de assertiva no <code class="language-plaintext highlighter-rouge">dmesg</code> e muita frustração.</p>

<p>Mas jogando pelas regras do jogo e aproveitando a brecha limpa do canal 13 em 2.4 GHz, o resultado compensou: conexão estável como rocha, WPA3 moderno com SAE puro, preâmbulo curto e velocidade real de sobra pros meus aparelhos, tudo orquestrado de forma declarativa e atômica.</p>

<p>Agora, se formos honestos na ponta do lápis (e a nossa fatura de energia elétrica exige essa honestidade): vale a pena manter um computador desktop de 80W ligado direto na tomada só pra servir Wi-Fi pro celular? <em>Por favor, não faça isso.</em> Se você quer apenas internet estável sem inventar moda, compre um roteador Wi-Fi 6 dedicado de duzentos reais que consome 5W e vá viver a sua vida em paz. Mas se o seu objetivo era o prazer sádico de domar o hardware, entender como o rádio do Linux opera por baixo do capô e não deixar a Intel decidir o que você pode ou não rodar na sua própria máquina: aí sim, cada linha de log valeu a pena.</p>

<h2 id="referências">Referências</h2>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p><strong>hostapd: IEEE 802.11 AP, IEEE 802.1X/WPA/WPA2/EAP/RADIUS Authenticator</strong> {<em>w1.fi</em>} (<a href="https://w1.fi/hostapd/">Link</a>) <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3">
      <p><strong>Intel Wireless WiFi Drivers (iwlwifi)</strong> {<em>Linux Wireless Documentation</em>} (<a href="https://wireless.wiki.kernel.org/en/users/drivers/iwlwifi">Link</a>) <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4">
      <p><strong>Location Aware Regulatory (LAR) in Linux Wireless</strong> {<em>Kernel.org cfg80211</em>} (<a href="https://wireless.docs.kernel.org/en/latest/en/developers/regulatory/processing_rules.html">Link</a>) <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:7">
      <p><strong>making hostapd LAR-friendly (on Intel 5GHz wireless cards)</strong> {<em>Tildearrow</em>} (<a href="https://tildearrow.org/?p=post&amp;month=7&amp;year=2022&amp;item=lar">Link</a>) <a href="#fnref:7" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:8">
      <p><strong>MediaTek (mt76): Linux wireless driver support for MT7921/MT7922</strong> {<em>Linux Wireless Documentation</em>} (<a href="https://wireless.docs.kernel.org/en/latest/en/users/drivers/mediatek.html">Link</a>) <a href="#fnref:8" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:6">
      <p><strong>dnsmasq: A lightweight DHCP and caching DNS server</strong> {<em>Simon Kelley / The Kape</em>} (<a href="https://thekelleys.org.uk/dnsmasq/doc.html">Link</a>) <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5">
      <p><strong>802.11ax (Wi-Fi 6) High Efficiency Physical Layer Overview</strong> {<em>IEEE Standards Association</em>} (<a href="https://standards.ieee.org/ieee/802.11ax/6618/">Link</a>) <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p><strong>nftables: High performance traffic classification</strong> {<em>Netfilter Project</em>} (<a href="https://netfilter.org/projects/nftables/">Link</a>) <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>[&quot;Eduardo N. S. R.&quot;]</name></author><category term="Artigos" /><category term="Debian" /><category term="Linux" /><category term="Wi-Fi 6" /><category term="hostapd" /><category term="nftables" /><summary type="html"><![CDATA[Existe uma ilusão vendida pelas interfaces gráficas modernas de que transformar o computador em ponto de acesso sem fio é um clique e pronto. Você abre o painel da distribuição, clica em "Criar Hotspot", inventa uma senha qualquer e espera que funcione. Na melhor das hipóteses, sobe uma rede WPA2 capenga; na pior, a interface congela, o NetworkManager se perde sozinho e você não faz ideia do que deu errado.]]></summary></entry><entry><title type="html">Kubernetes in a Box, Parte 3 - O Inventário Ansible e as Variáveis do Cluster</title><link href="https://vndmtrx.github.io/posts/k8sbox-inventario-ansible/" rel="alternate" type="text/html" title="Kubernetes in a Box, Parte 3 - O Inventário Ansible e as Variáveis do Cluster" /><published>2026-09-23T21:48:00+00:00</published><updated>2026-09-23T21:48:00+00:00</updated><id>https://vndmtrx.github.io/posts/k8sbox-inventario-ansible</id><content type="html" xml:base="https://vndmtrx.github.io/posts/k8sbox-inventario-ansible/"><![CDATA[<p>No post anterior, deixamos nossas máquinas virtuais ligadas e conversando em uma rede privada isolada no KVM. Temos load balancers, managers, workers e um servidor NFS com endereços estáticos e resolução de nomes básica. Se tentarmos rodar qualquer comando do Kubernetes nelas agora, nada vai acontecer: não há binários instalados, não há certificados emitidos e nenhum processo do <em>control plane</em> existe. Antes de enviar a primeira instrução para as máquinas, precisamos responder a uma pergunta de arquitetura: onde vive a verdade sobre as versões, as faixas de rede e os papéis de cada nó?</p>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Nota da Série e Contexto de Laboratório</p><p>Este post faz parte da série <strong>"Kubernetes in a Box"</strong>, onde dissecamos e construímos, do zero e de forma totalmente reproduzível via Ansible, um cluster Kubernetes completo, com alta disponibilidade, armazenamento persistente, rede moderna e observabilidade. Todo o código do projeto está disponível no repositório parceiro <a href="https://github.com/vndmtrx/k8s-in-a-box">vndmtrx/k8s-in-a-box</a>.</p>

<p><strong>Aviso de escopo:</strong> as decisões de arquitetura e parâmetros deste projeto foram pensadas sob medida para a nossa realidade de laboratório local em estações Linux com Vagrant e KVM/Libvirt. Elas priorizam aprendizado profundo e reprodutibilidade rápida sobre convenções corporativas de produção. O cluster foi originalmente projetado em versões anteriores e recentemente atualizado para o <strong>Kubernetes v1.37.0</strong>.</p>
</div>

<p>A tentação em tutoriais de automação é pular direto para a execução dos playbooks. Alguém escreve meia dúzia de tarefas com comandos shell soltos, define caminhos hardcoded nos scripts e torce para que nada mude no futuro. Quem já precisou atualizar a versão do Kubernetes em um ambiente desses sabe o pesadelo que isso representa: você precisa vasculhar cinquenta arquivos procurando por <code class="language-plaintext highlighter-rouge">1.36.2</code>, torcendo para não esquecer um manifesto estático ou uma URL de download escondida em uma role secundária.</p>

<p>No <strong>k8s-in-a-box</strong>, tomamos uma direção oposta. Toda a inteligência da infraestrutura está desacoplada da execução. O arquivo <code class="language-plaintext highlighter-rouge">inventario/group_vars/all.yml</code> funciona como o mapa genético do laboratório, enquanto a estrutura de inventário dita quem é quem na topologia. Para provar o valor dessa separação, fizemos hoje uma atualização real no repositório do projeto: saltamos a versão base do Kubernetes de <code class="language-plaintext highlighter-rouge">v1.36.2</code> para o <strong>Kubernetes v1.37.0</strong> <sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>, acompanhado do etcd 3.7.1, crictl v1.37.0, helm v3.22.0 e cilium v1.20.2.</p>

<p>Sabe quantos arquivos de automação ou manifestos de pods estáticos precisaram ser reescritos para suportar essa nova versão em todo o cluster? Apenas um.</p>

<details class="toc-box">
  <summary><strong>Sumário &amp; Índice de Seções (TOC)</strong></summary>
<ul id="markdown-toc">
  <li><a href="#o-arquivo-de-configuração-do-ansible-e-suas-decisões" id="markdown-toc-o-arquivo-de-configuração-do-ansible-e-suas-decisões">O arquivo de configuração do Ansible e suas decisões</a></li>
  <li><a href="#anatomia-do-inventário-e-topologias-com-symlinks" id="markdown-toc-anatomia-do-inventário-e-topologias-com-symlinks">Anatomia do inventário e topologias com symlinks</a>    <ul>
      <li><a href="#a-simbiose-entre-vagrant-libvirt-e-o-inventário" id="markdown-toc-a-simbiose-entre-vagrant-libvirt-e-o-inventário">A simbiose entre Vagrant, Libvirt e o inventário</a></li>
    </ul>
  </li>
  <li><a href="#o-arquivo-allyml-como-mapa-genético-da-stack" id="markdown-toc-o-arquivo-allyml-como-mapa-genético-da-stack">O arquivo all.yml como mapa genético da stack</a>    <ul>
      <li><a href="#definição-de-redes-e-cálculo-de-ips-derivados" id="markdown-toc-definição-de-redes-e-cálculo-de-ips-derivados">Definição de redes e cálculo de IPs derivados</a></li>
      <li><a href="#alternância-de-runtimes-e-plugins-de-rede" id="markdown-toc-alternância-de-runtimes-e-plugins-de-rede">Alternância de runtimes e plugins de rede</a></li>
      <li><a href="#o-caso-prático-do-upgrade-para-kubernetes-v1370" id="markdown-toc-o-caso-prático-do-upgrade-para-kubernetes-v1370">O caso prático do upgrade para Kubernetes v1.37.0</a></li>
    </ul>
  </li>
  <li><a href="#a-tríade-de-playbooks-e-o-sistema-semântico-de-tags" id="markdown-toc-a-tríade-de-playbooks-e-o-sistema-semântico-de-tags">A tríade de playbooks e o sistema semântico de tags</a>    <ul>
      <li><a href="#o-playbook-principal-clusteryml" id="markdown-toc-o-playbook-principal-clusteryml">O playbook principal: cluster.yml</a></li>
      <li><a href="#o-playbook-operacional-opsyml" id="markdown-toc-o-playbook-operacional-opsyml">O playbook operacional: ops.yml</a></li>
      <li><a href="#o-playbook-de-addons-addonsyml" id="markdown-toc-o-playbook-de-addons-addonsyml">O playbook de addons: addons.yml</a></li>
      <li><a href="#como-o-makefile-encapsula-as-tags" id="markdown-toc-como-o-makefile-encapsula-as-tags">Como o Makefile encapsula as tags</a></li>
    </ul>
  </li>
  <li><a href="#decisões-de-design-no-ansible-do-projeto" id="markdown-toc-decisões-de-design-no-ansible-do-projeto">Decisões de design no Ansible do projeto</a>    <ul>
      <li><a href="#construção-centralizada-de-certificados-no-host" id="markdown-toc-construção-centralizada-de-certificados-no-host">Construção centralizada de certificados no host</a></li>
      <li><a href="#downloads-atômicos-e-reaproveitamento-de-binários" id="markdown-toc-downloads-atômicos-e-reaproveitamento-de-binários">Downloads atômicos e reaproveitamento de binários</a></li>
      <li><a href="#cache-local-de-imagens-oci-com-skopeo-e-importação-direta" id="markdown-toc-cache-local-de-imagens-oci-com-skopeo-e-importação-direta">Cache local de imagens OCI com Skopeo e importação direta</a></li>
      <li><a href="#extração-dinâmica-de-ferramentas-via-overlayfs" id="markdown-toc-extração-dinâmica-de-ferramentas-via-overlayfs">Extração dinâmica de ferramentas via OverlayFS</a></li>
      <li><a href="#padronização-semântica-de-roles-e-tasklists" id="markdown-toc-padronização-semântica-de-roles-e-tasklists">Padronização semântica de roles e tasklists</a></li>
    </ul>
  </li>
  <li><a href="#diagnóstico-e-validação-rápida" id="markdown-toc-diagnóstico-e-validação-rápida">Diagnóstico e validação rápida</a>    <ul>
      <li><a href="#inspecionando-a-árvore-de-grupos" id="markdown-toc-inspecionando-a-árvore-de-grupos">Inspecionando a árvore de grupos</a></li>
      <li><a href="#verificando-variáveis-resolvidas-por-nó" id="markdown-toc-verificando-variáveis-resolvidas-por-nó">Verificando variáveis resolvidas por nó</a></li>
      <li><a href="#testando-variáveis-ativas-com-o-módulo-debug" id="markdown-toc-testando-variáveis-ativas-com-o-módulo-debug">Testando variáveis ativas com o módulo debug</a></li>
    </ul>
  </li>
  <li><a href="#conclusão" id="markdown-toc-conclusão">Conclusão</a></li>
  <li><a href="#referências" id="markdown-toc-referências">Referências</a></li>
</ul>

</details>

<h2 id="o-arquivo-de-configuração-do-ansible-e-suas-decisões">O arquivo de configuração do Ansible e suas decisões</h2>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Mas Dudu, a gente não ia falar de Kubernetes?!</p><p>Eu sei o que você está pensando: "vim aqui pra aprender Kubernetes Hardcore e o cara tá me explicando flag de SSH e arquivo de configuração do Ansible". Segura a ansiedade! O Vagrant e o Ansible são os alicerces que sustentam todo esse laboratório em pé. Precisamos dissecar e deixar essa fundação de automação totalmente resolvida agora exatamente para tirá-la do caminho. Nas próximas partes da série, quase não vamos mais falar de Vagrant ou Ansible: o foco será total e exclusivo nas entranhas do Kubernetes (PKI, etcd, Static Pods, CNI e observabilidade). Gastar esse tempo no assunto mais burocrático hoje é o que vai nos dar liberdade para mergulhar direto nos tópicos mais empolgantes depois, sem interrupções.</p>

<p>Mas olha: se você não aguenta de curiosidade e prefere ver o bicho funcionando na prática antes de ler todo o manual, sem problemas! Vai lá no terminal, clona o repositório, roda a esteira com <code class="language-plaintext highlighter-rouge">make k8s-in-a-box</code> e vê o cluster se materializar. Depois volta aqui pra continuarmos a dissecar o inventário com calma. Eu te espero!</p>
</div>

<p>O ponto de entrada de qualquer automação com a ferramenta é o arquivo de configuração <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/ansible/.ansible.cfg">.ansible.cfg</a>. No repositório, ele fica dentro do diretório <code class="language-plaintext highlighter-rouge">ansible/</code> e define exatamente como eu escolhi configurar o Ansible para atender às necessidades desse laboratório:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[defaults]</span><span class="w">
</span><span class="py">interpreter_python</span><span class="p">=</span><span class="s">auto_silent</span>
<span class="py">deprecation_warnings</span><span class="p">=</span><span class="s">False</span>
<span class="py">nocows</span><span class="p">=</span><span class="s">True</span>
<span class="py">forks</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">5</span>
<span class="py">inventory</span><span class="p">=</span><span class="s">../inventario/</span>
<span class="py">host_key_checking</span><span class="p">=</span><span class="s">False</span>
<span class="py">become</span><span class="p">=</span><span class="s">false</span>
<span class="py">gather_facts</span><span class="p">=</span><span class="s">false</span>
<span class="py">log_path</span><span class="p">=</span><span class="s">../artefatos/ansible_logs/ansible.log</span>
<span class="py">fact_caching</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">jsonfile</span>
<span class="py">fact_caching_connection</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">../artefatos/ansible_cache/ansible_facts</span>
<span class="py">fact_caching_timeout</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">3600</span>
<span class="py">callbacks_enabled</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">profile_tasks, timer</span>
<span class="w">
</span><span class="nn">[inventory]</span><span class="w">
</span><span class="py">enable_plugins</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">host_list, script, auto, yaml, ini, toml</span>
<span class="py">cache</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">yes</span>
<span class="py">cache_connection</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">../artefatos/ansible_cache/ansible_inventory</span>
<span class="w">
</span><span class="nn">[ssh_connection]</span><span class="w">
</span><span class="py">ssh_args</span><span class="p">=</span><span class="s">-F ./ssh_config -o ControlMaster=auto -o ControlPersist=180s</span>
<span class="py">control_path_dir</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">../artefatos/ansible_cache/cp</span>
</code></pre></div></div>

<p>Cada uma dessas linhas resolve um problema prático recorrente de automação local.</p>

<p>A diretiva <code class="language-plaintext highlighter-rouge">inventory=../inventario/</code> aponta para um diretório inteiro em vez de um arquivo estático. Isso permite que o Ansible combine o arquivo de máquinas (<code class="language-plaintext highlighter-rouge">hosts.yml</code>) com o subdiretório de variáveis de grupo (<code class="language-plaintext highlighter-rouge">group_vars/</code>) de maneira orgânica.</p>

<p>A opção <code class="language-plaintext highlighter-rouge">host_key_checking=False</code> é uma concessão consciente para o fluxo de desenvolvimento. Como estamos destruindo e recriando máquinas virtuais frequentemente com Vagrant, o hash de chave pública de cada VM mudaria a cada recriação, causando erros de <em>host key mismatch</em> no SSH.</p>

<div class="markdown-alert markdown-alert-caution"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-stop" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Alerta de Segurança</p><p>A desativação de <code class="language-plaintext highlighter-rouge">host_key_checking</code> é aceitável exclusivamente em laboratórios locais descartáveis com redes privadas isoladas. Em ambientes de homologação ou produção, desativar essa checagem abre as portas para ataques de Man-in-the-Middle (MitM) em conexões SSH corporativas.</p>
</div>

<p>Outro ganho perceptível de fôlego vem da trinca <code class="language-plaintext highlighter-rouge">gather_facts=false</code>, <code class="language-plaintext highlighter-rouge">forks = 5</code> e as opções de <code class="language-plaintext highlighter-rouge">[ssh_connection]</code>. Por padrão, toda vez que você pisca o olho, o Ansible quer passar um pente fino na VM para descobrir o endereço MAC de cada interface virtual, o tamanho do disco e até a fase da lua no sistema operacional. Para quem está rodando tudo localmente no notebook ou desktop, esperar esse interrogatório a cada <em>play</em> só serve para usar CPU à toa e testar a nossa paciência. Desativei essa coleta global por padrão: quando uma role realmente precisar de algum dado específico da máquina, ela que vá lá e colete pontualmente.</p>

<p>Com o <code class="language-plaintext highlighter-rouge">ssh_args</code> configurado com <code class="language-plaintext highlighter-rouge">ControlMaster=auto</code> e <code class="language-plaintext highlighter-rouge">ControlPersist=180s</code>, o cliente SSH reaproveita os sockets de conexão abertos com as VMs durante três minutos. Tarefas subsequentes na mesma máquina acontecem de forma quase instantânea, sem a latência de um novo <em>handshake</em> criptográfico a cada task.</p>

<h2 id="anatomia-do-inventário-e-topologias-com-symlinks">Anatomia do inventário e topologias com symlinks</h2>

<p>O diretório <code class="language-plaintext highlighter-rouge">configs/</code> na raiz do projeto abriga três modelos de cluster distintos: <code class="language-plaintext highlighter-rouge">hosts-nano.yml</code>, <code class="language-plaintext highlighter-rouge">hosts-mini.yml</code> e <code class="language-plaintext highlighter-rouge">hosts-completo.yml</code>. Como discutimos na Parte 1, nem todo desenvolvedor tem uma máquina com recursos de sobra para rodar várias máquinas virtuais e manter um cluster etcd com quórum completo.</p>

<p>Quando executamos <code class="language-plaintext highlighter-rouge">make init</code> (ou definimos <code class="language-plaintext highlighter-rouge">CLUSTER=mini</code> no arquivo <code class="language-plaintext highlighter-rouge">config.mk</code>), o Makefile cria um link simbólico transparente:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>inventario/hosts.yml -&gt; ../configs/hosts-mini.yml
</code></pre></div></div>

<p>Essa abordagem garante que todo o restante do ferramental (Ansible, scripts de verificação e Vagrant) consuma sempre o mesmo caminho (<code class="language-plaintext highlighter-rouge">inventario/hosts.yml</code>), sem precisar reescrever parâmetros de linha de comando.</p>

<p>A estrutura de cada nó e o agrupamento inicial seguem este padrão (o arquivo completo com todas as VMs está no <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/configs/hosts-mini.yml">configs/hosts-mini.yml</a>):</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">all</span><span class="pi">:</span>
  <span class="na">children</span><span class="pi">:</span>
    <span class="na">managers</span><span class="pi">:</span>
      <span class="na">hosts</span><span class="pi">:</span>
        <span class="na">manager1</span><span class="pi">:</span>
          <span class="na">ansible_host</span><span class="pi">:</span> <span class="s">172.24.0.31</span>
          <span class="na">fqdn</span><span class="pi">:</span> <span class="s">manager1.k8sbox.local</span>
          <span class="na">memory</span><span class="pi">:</span> <span class="m">3072</span>
          <span class="na">cpus</span><span class="pi">:</span> <span class="m">2</span>

    <span class="c1"># Agrupamento composto para orquestração</span>
    <span class="na">cluster</span><span class="pi">:</span>
      <span class="na">children</span><span class="pi">:</span>
        <span class="na">managers</span><span class="pi">:</span> <span class="pi">{}</span>
        <span class="na">workers</span><span class="pi">:</span> <span class="pi">{}</span>
</code></pre></div></div>

<p>A composição de grupos simplifica a execução dos playbooks:</p>

<ol>
  <li><strong>Grupos específicos</strong>: <code class="language-plaintext highlighter-rouge">loadbalancers</code>, <code class="language-plaintext highlighter-rouge">nfs</code>, <code class="language-plaintext highlighter-rouge">managers</code>, <code class="language-plaintext highlighter-rouge">workers</code> e <code class="language-plaintext highlighter-rouge">clientes</code> (<code class="language-plaintext highlighter-rouge">kubox</code>, nosso <em>bastion host</em>).</li>
  <li><strong>Grupo cluster</strong>: une <code class="language-plaintext highlighter-rouge">managers</code> e <code class="language-plaintext highlighter-rouge">workers</code> para tudo o que é comum aos nós Kubernetes (CRI-O, kubelet, CNI).</li>
  <li><strong>Grupo suporte</strong>: une <code class="language-plaintext highlighter-rouge">loadbalancers</code> (HAProxy e Keepalived) e <code class="language-plaintext highlighter-rouge">nfs</code> (armazenamento persistente).</li>
  <li><strong>Grupo todos</strong>: encapsula todo o ambiente para a preparação base do sistema operacional.</li>
</ol>

<h3 id="a-simbiose-entre-vagrant-libvirt-e-o-inventário">A simbiose entre Vagrant, Libvirt e o inventário</h3>

<p>O inventário não serve apenas para o Ansible rodar tarefas. Ele funciona como a <strong>fonte única da verdade</strong> de toda a infraestrutura virtual:</p>

<ul>
  <li><strong>Vagrant alimentado pelo inventário</strong>: o <code class="language-plaintext highlighter-rouge">Vagrantfile</code> não tem máquinas hardcoded. Ele lê diretamente o <code class="language-plaintext highlighter-rouge">inventario/hosts.yml</code> e provisiona cada VM no hipervisor com as variáveis <code class="language-plaintext highlighter-rouge">memory</code> e <code class="language-plaintext highlighter-rouge">cpus</code> que declaramos lá. Ajustou no YAML, o hardware da VM acompanha.</li>
  <li><strong>Libvirt (KVM) em vez de VirtualBox</strong>: KVM roda direto no kernel Linux, sem emulação pesada. Isso economiza RAM e CPU para aguentar várias VMs ligadas no mesmo computador sem fazer o sistema hospedeiro pedir socorro.</li>
  <li><strong>Isolamento de redes com duas placas (Dual-NIC)</strong>:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">eth0</code> (<code class="language-plaintext highlighter-rouge">net_vagrant</code>): interface exclusiva para a gerência do Vagrant (<code class="language-plaintext highlighter-rouge">vagrant ssh</code>, triggers e provisioners). Configuramos com <code class="language-plaintext highlighter-rouge">ipv4.never-default yes</code> e métrica alta (<code class="language-plaintext highlighter-rouge">500</code>) justamente para ela nunca disputar rota padrão ou rota de saída.</li>
      <li><code class="language-plaintext highlighter-rouge">eth1</code> (<code class="language-plaintext highlighter-rouge">net_mgmt</code>, 172.24.0.0/24): a placa de rede principal da VM. É ela que recebe os IPs estáticos do inventário, assume a rota padrão (<code class="language-plaintext highlighter-rouge">metric 50</code>) com o gateway <code class="language-plaintext highlighter-rouge">172.24.0.1</code> (NAT do Libvirt) e trafega tudo: comunicação interna do cluster (<code class="language-plaintext highlighter-rouge">etcd</code>, <code class="language-plaintext highlighter-rouge">kube-apiserver</code>, CNI, NFS) e saída externa controlada.</li>
    </ul>
  </li>
</ul>

<p>Com isso, o lab fica imune à sua rede física: você pode trocar de Wi-Fi, ligar VPN ou tirar o cabo de rede que as VMs continuam se falando sem quebrar o cluster.</p>

<h2 id="o-arquivo-allyml-como-mapa-genético-da-stack">O arquivo all.yml como mapa genético da stack</h2>

<p>Se a topologia define onde as coisas rodam, o arquivo <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/inventario/group_vars/all.yml">inventario/group_vars/all.yml</a> define o que roda e em qual versão. No Ansible, variáveis colocadas dentro de <code class="language-plaintext highlighter-rouge">group_vars/all.yml</code> tornam-se visíveis automaticamente para todos os hosts do inventário.</p>

<p>Vamos dissecar os principais blocos do <code class="language-plaintext highlighter-rouge">all.yml</code> e examinar as decisões por trás de cada um.</p>

<h3 id="definição-de-redes-e-cálculo-de-ips-derivados">Definição de redes e cálculo de IPs derivados</h3>

<p>Um cluster Kubernetes possui pelo menos três domínios de rede distintos que nunca devem se sobrepor: a rede física ou dos hosts, a rede interna dos Pods e a rede virtual de Services (ClusterIPs).</p>

<p>Em vez de preencher manualmente endereços IP em dezenas de lugares, definimos as faixas CIDR principais e calculamos os endereços derivados utilizando o plugin <code class="language-plaintext highlighter-rouge">ansible.utils.ipaddr</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Trecho de inventario/group_vars/all.yml</span>
<span class="na">rede_cidr_services</span><span class="pi">:</span> <span class="s2">"</span><span class="s">172.25.128.0/17"</span>
<span class="na">primeiro_ip_services</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">rede_cidr_services</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">ansible.utils.ipaddr('1')</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">ansible.utils.ipaddr('address')</span><span class="nv"> </span><span class="s">}}"</span>
<span class="na">coredns_ip</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">rede_cidr_services</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">ansible.utils.ipaddr('10')</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">ansible.utils.ipaddr('address')</span><span class="nv"> </span><span class="s">}}"</span>
</code></pre></div></div>

<p>O filtro <code class="language-plaintext highlighter-rouge">ansible.utils.ipaddr('10')</code> pega o décimo endereço utilizável dentro da sub-rede <code class="language-plaintext highlighter-rouge">172.25.128.0/17</code>, resultando em <code class="language-plaintext highlighter-rouge">172.25.128.10</code>. Esse endereço é repassado tanto para a configuração do <em>service</em> do CoreDNS quanto para o parâmetro <code class="language-plaintext highlighter-rouge">--cluster-dns</code> na inicialização de cada <code class="language-plaintext highlighter-rouge">kubelet</code>. Se você precisar mudar toda a faixa de Services do cluster para <code class="language-plaintext highlighter-rouge">10.96.0.0/12</code>, altera apenas <code class="language-plaintext highlighter-rouge">rede_cidr_services</code> e todos os componentes convergem sem inconsistências.</p>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Por que o décimo IP para o CoreDNS?</p><p>Para as outras redes derivadas no projeto (hosts, pods e o primeiro IP de services), pegamos sistematicamente o primeiro IP disponível usando o filtro com argumento <code class="language-plaintext highlighter-rouge">'1'</code>. Mas o CoreDNS é um caso à parte: o Kubernetes precisa de um endereço de ClusterIP fixo e previsível para que os nós e os pods saibam exatamente para onde mandar consultas de DNS. Em vez de disputar o primeiro IP da rede (que tradicionalmente fica reservado para o próprio serviço interno da API do Kubernetes), escolhemos um endereço aleatório dentro da faixa que calhou de ser o décimo IP do range (<code class="language-plaintext highlighter-rouge">.10</code>). O segredo não é nenhuma mística no número dez, e sim ter essa decisão declarada de ponta a ponta no inventário. O mapeamento completo das redes pode ser conferido em <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/inventario/group_vars/all.yml">inventario/group_vars/all.yml</a>.</p>
</div>

<h3 id="alternância-de-runtimes-e-plugins-de-rede">Alternância de runtimes e plugins de rede</h3>

<p>O projeto foi desenhado para permitir experimentação controlada. Queremos poder alternar o runtime de contêiner ou o plugin de rede CNI com uma flag no inventário:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Trecho de inventario/group_vars/all.yml</span>
<span class="na">container_runtime</span><span class="pi">:</span> <span class="s2">"</span><span class="s">crio"</span>  <span class="c1"># crio ou containerd</span>
<span class="na">plugin_cni</span><span class="pi">:</span> <span class="s2">"</span><span class="s">cilium"</span>       <span class="c1"># cilium ou canal</span>
</code></pre></div></div>

<p>A variável <code class="language-plaintext highlighter-rouge">container_runtime</code> é consumida pelas roles para definir quais repositórios habilitar, quais pacotes instalar e qual socket registrar na inicialização do kubelet (<code class="language-plaintext highlighter-rouge">--container-runtime-endpoint</code>).</p>

<p>Na rede, a variável <code class="language-plaintext highlighter-rouge">plugin_cni: "cilium"</code> seleciona a stack padrão moderna com eBPF nativo e suporte a Gateway API. Caso o leitor queira testar a stack tradicional com <code class="language-plaintext highlighter-rouge">canal</code> (Flannel + Calico) e kube-vip, basta alterar essa variável para <code class="language-plaintext highlighter-rouge">canal</code>.</p>

<h3 id="o-caso-prático-do-upgrade-para-kubernetes-v1370">O caso prático do upgrade para Kubernetes v1.37.0</h3>

<p>Aqui chegamos ao coração da atualização que realizamos hoje no repositório. Em vez de espalhar versões em múltiplos manifestos, concentramos a versão base em uma única linha e deixamos o Ansible propagar o valor:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Trecho de inventario/group_vars/all.yml</span>
<span class="na">versao_kubernetes</span><span class="pi">:</span> <span class="s2">"</span><span class="s">v1.37.0"</span>

<span class="c1"># O valor propaga automaticamente para imagens dos Static Pods:</span>
<span class="na">control_plane_images</span><span class="pi">:</span>
  <span class="na">apiserver</span><span class="pi">:</span> <span class="s2">"</span><span class="s">registry.k8s.io/kube-apiserver:{{</span><span class="nv"> </span><span class="s">versao_kubernetes</span><span class="nv"> </span><span class="s">}}"</span>

<span class="c1"># E para os downloads oficiais com checagem de integridade:</span>
<span class="na">download_artefatos</span><span class="pi">:</span>
  <span class="na">kubelet-bin</span><span class="pi">:</span>
    <span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://dl.k8s.io/release/{{</span><span class="nv"> </span><span class="s">versao_kubernetes</span><span class="nv"> </span><span class="s">}}/bin/linux/amd64/kubelet"</span>
    <span class="na">arquivo</span><span class="pi">:</span> <span class="s2">"</span><span class="s">kubelet"</span>
    <span class="na">checksum</span><span class="pi">:</span> <span class="s2">"</span><span class="s">sha256:https://dl.k8s.io/release/{{</span><span class="nv"> </span><span class="s">versao_kubernetes</span><span class="nv"> </span><span class="s">}}/bin/linux/amd64/kubelet.sha256"</span>
</code></pre></div></div>

<p>Ao alterar a linha <code class="language-plaintext highlighter-rouge">versao_kubernetes: "v1.37.0"</code>, o Ansible automaticamente:</p>

<ol>
  <li>Calcula a nova URL oficial de download do binário do <code class="language-plaintext highlighter-rouge">kubelet</code> e seu hash SHA256 correspondente.</li>
  <li>Faz o download do binário compilado do <code class="language-plaintext highlighter-rouge">kubectl v1.37.0</code>.</li>
  <li>Atualiza os manifestos dos <em>Static Pods</em> em cada manager para apontar para as tags <code class="language-plaintext highlighter-rouge">registry.k8s.io/kube-apiserver:v1.37.0</code>, <code class="language-plaintext highlighter-rouge">kube-controller-manager:v1.37.0</code> e <code class="language-plaintext highlighter-rouge">kube-scheduler:v1.37.0</code>.</li>
  <li>Garante que os nós baixem a imagem correspondente do <code class="language-plaintext highlighter-rouge">kube-proxy:v1.37.0</code>.</li>
</ol>

<p>A lista com todas as versões vinculadas da stack (como etcd 3.7.1, crictl v1.37.0, helm v3.22.0 e cilium v1.20.2) está documentada na íntegra no <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/inventario/group_vars/all.yml">inventario/group_vars/all.yml</a>. A versão do cluster torna-se um parâmetro puramente declarativo.</p>

<h2 id="a-tríade-de-playbooks-e-o-sistema-semântico-de-tags">A tríade de playbooks e o sistema semântico de tags</h2>

<p>A execução do Ansible no projeto está dividida em três playbooks complementares, com separação estrita de contexto:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ansible/
├── cluster.yml   # Orquestra infraestrutura base, PKI, nós e control plane
├── ops.yml       # Provisiona o bastion host (kubox), rede CNI e exemplos
└── addons.yml    # Instala aplicações de monitoramento e dashboards
</code></pre></div></div>

<h3 id="o-playbook-principal-clusteryml">O playbook principal: cluster.yml</h3>

<p>O arquivo <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/ansible/cluster.yml">ansible/cluster.yml</a> orquestra a subida do cluster desde a máquina física até os pods do <em>control plane</em>. Em vez de um script monolítico, ele organiza as etapas por grupos de nós:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Trecho de ansible/cluster.yml</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Tarefas a serem executadas no host</span>
  <span class="na">hosts</span><span class="pi">:</span> <span class="s">localhost</span>
  <span class="na">connection</span><span class="pi">:</span> <span class="s">local</span>
  <span class="na">become</span><span class="pi">:</span> <span class="kc">false</span>
  <span class="na">roles</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">role</span><span class="pi">:</span> <span class="s">infra-artefatos</span>
      <span class="na">tags</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">cluster-artefatos</span><span class="pi">,</span> <span class="nv">cluster</span><span class="pi">]</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Configuração do kubelet</span>
  <span class="na">hosts</span><span class="pi">:</span> <span class="s">cluster</span>
  <span class="na">become</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="na">serial</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">roles</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">role</span><span class="pi">:</span> <span class="s">k8s-kubelet</span>
      <span class="na">tags</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">cluster-kubelet</span><span class="pi">,</span> <span class="nv">cluster</span><span class="pi">]</span>
</code></pre></div></div>

<p>Note o uso da diretiva <code class="language-plaintext highlighter-rouge">serial: 1</code> na role <code class="language-plaintext highlighter-rouge">k8s-kubelet</code>. Quando atualizamos ou configuramos os agentes dos nós, queremos que o Ansible processe uma máquina por vez, minimizando o impacto no cluster em execuções de manutenção.</p>

<p>A sequência completa, incluindo as plays para balanceadores de carga, servidor NFS e os <em>Static Pods</em> do <em>control plane</em> (etcd, apiserver, controller-manager e scheduler), pode ser conferida diretamente em <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/ansible/cluster.yml">ansible/cluster.yml</a>.</p>

<h3 id="o-playbook-operacional-opsyml">O playbook operacional: ops.yml</h3>

<p>O arquivo <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/ansible/ops.yml">ansible/ops.yml</a> opera em um plano diferente. Ele não roda contra os nós do cluster, mas sim contra a máquina <code class="language-plaintext highlighter-rouge">kubox</code> (grupo <code class="language-plaintext highlighter-rouge">clientes</code>), gerenciando a instalação da rede CNI por meio de condições dinâmicas:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Trecho de ansible/ops.yml</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Instalação da Rede CNI e dependências</span>
  <span class="na">hosts</span><span class="pi">:</span> <span class="s">clientes</span>
  <span class="na">become</span><span class="pi">:</span> <span class="kc">false</span>
  <span class="na">roles</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">role</span><span class="pi">:</span> <span class="s">cni-cilium</span>
      <span class="na">when</span><span class="pi">:</span> <span class="s">plugin_cni == 'cilium'</span>
      <span class="na">tags</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">ops-cni-cilium</span><span class="pi">,</span> <span class="nv">ops-cni</span><span class="pi">,</span> <span class="nv">ops</span><span class="pi">]</span>
    <span class="pi">-</span> <span class="na">role</span><span class="pi">:</span> <span class="s">cni-canal</span>
      <span class="na">when</span><span class="pi">:</span> <span class="s">plugin_cni == 'canal'</span>
      <span class="na">tags</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">ops-cni-canal</span><span class="pi">,</span> <span class="nv">ops-cni</span><span class="pi">,</span> <span class="nv">ops</span><span class="pi">]</span>
</code></pre></div></div>

<p>É o <code class="language-plaintext highlighter-rouge">ops.yml</code> que aplica os manifestos de CNI no cluster. Em vez de rodar o comando <code class="language-plaintext highlighter-rouge">helm install</code> ou <code class="language-plaintext highlighter-rouge">kubectl apply</code> na sua máquina de desenvolvimento local (o que exigiria instalar o Helm, o CLI do Cilium e o kubectl no seu próprio Linux), essas ferramentas são instaladas e executadas de dentro da VM <code class="language-plaintext highlighter-rouge">kubox</code>. Seu hospedeiro permanece limpo.</p>

<p>O playbook completo com a instalação das ferramentas operacionais (<code class="language-plaintext highlighter-rouge">k9s</code>, <code class="language-plaintext highlighter-rouge">popeye</code>, <code class="language-plaintext highlighter-rouge">tailspin</code>) e deploys de exemplo está em <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/ansible/ops.yml">ansible/ops.yml</a>.</p>

<h3 id="o-playbook-de-addons-addonsyml">O playbook de addons: addons.yml</h3>

<p>Por fim, o arquivo <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/ansible/addons.yml">ansible/addons.yml</a> isola a implantação de serviços opcionais que não afetam o ciclo de vida do Kubernetes básico:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Trecho de ansible/addons.yml</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Aplicação de addons independentes de CNI no cluster</span>
  <span class="na">hosts</span><span class="pi">:</span> <span class="s">clientes</span>
  <span class="na">become</span><span class="pi">:</span> <span class="kc">false</span>
  <span class="na">roles</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">role</span><span class="pi">:</span> <span class="s">addon-apps-cluster</span>
      <span class="na">tags</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">addons</span><span class="pi">]</span>
</code></pre></div></div>

<p>Ele gerencia painéis e telemetria (Headlamp, Prometheus, Grafana) através da role <code class="language-plaintext highlighter-rouge">addon-apps-cluster</code>, permitindo ligar ou desligar essa camada a qualquer momento sem interferir no core do cluster.</p>

<h3 id="como-o-makefile-encapsula-as-tags">Como o Makefile encapsula as tags</h3>

<p>Ninguém quer memorizar comandos longos do Ansible com argumentos de configuração e dezenas de tags separadas por vírgula. O <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/Makefile">Makefile</a> do repositório funciona como uma camada de abstração ergonômica:</p>

<div class="language-makefile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Trecho do Makefile
</span><span class="nl">infra</span><span class="o">:</span> <span class="nf">garante-config</span>
	<span class="nv">ANSIBLE_CONFIG</span><span class="o">=</span><span class="s2">"</span><span class="p">$(</span><span class="s2">CFG</span><span class="p">)</span><span class="s2">"</span> ansible-playbook <span class="s2">"</span><span class="p">$(</span><span class="s2">PLAYBOOK</span><span class="p">)</span><span class="s2">"</span> <span class="nt">--tags</span> cluster-artefatos,cluster-pki,cluster-sistema,cluster-balanceador,cluster-nfs

<span class="nl">control-plane</span><span class="o">:</span> <span class="nf">garante-config</span>
	<span class="nv">ANSIBLE_CONFIG</span><span class="o">=</span><span class="s2">"</span><span class="p">$(</span><span class="s2">CFG</span><span class="p">)</span><span class="s2">"</span> ansible-playbook <span class="s2">"</span><span class="p">$(</span><span class="s2">PLAYBOOK</span><span class="p">)</span><span class="s2">"</span> <span class="nt">--tags</span> cluster-kubernetes-base,cluster-kubelet,cluster-etcd,cluster-kube-apiserver,cluster-kube-controller-manager,cluster-kube-scheduler
</code></pre></div></div>

<p>Quando você digita <code class="language-plaintext highlighter-rouge">make infra</code>, o Makefile valida se a configuração ativa no <code class="language-plaintext highlighter-rouge">config.mk</code> corresponde ao symlink atual, define a variável de ambiente <code class="language-plaintext highlighter-rouge">ANSIBLE_CONFIG</code> e dispara exatamente as tags necessárias para preparar o terreno antes do Kubernetes.</p>

<p>Além disso, o Makefile traz utilitários essenciais de confiabilidade:</p>

<ul>
  <li><strong>Validação prévia de dependências (<code class="language-plaintext highlighter-rouge">make check-deps</code>)</strong>: antes de subir qualquer máquina ou playbook, o script verifica se o processador tem suporte a KVM habilitado, se o seu usuário faz parte do grupo <code class="language-plaintext highlighter-rouge">libvirt</code>, e se o Ansible, o Vagrant e o plugin <code class="language-plaintext highlighter-rouge">vagrant-libvirt</code> estão presentes. Isso impede falhas frustrantes no meio do provisionamento por falta de permissão ou pacote esquecido.</li>
  <li><strong>Ciclos rápidos de estudo (<code class="language-plaintext highlighter-rouge">make cluster-etcd</code>, <code class="language-plaintext highlighter-rouge">make cluster-kubelet</code>)</strong>: durante o desenvolvimento ou depuração de um subsistema específico, essas tags executam apenas as tarefas da role designada. Você testa alterações em segundos sem ter que esperar a execução completa de todo o cluster.</li>
  <li><strong>Limpeza e purga de estado (<code class="language-plaintext highlighter-rouge">make clean</code>)</strong>: além de destruir as VMs no Vagrant, esse comando apaga completamente a pasta <code class="language-plaintext highlighter-rouge">artefatos/</code>, garantindo que certificados antigos e caches locais do Ansible (<code class="language-plaintext highlighter-rouge">fact_caching</code> e inventário) sejam purgados sem deixar resíduos.</li>
</ul>

<p>A lista completa de alvos está disponível no <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/Makefile">Makefile</a>.</p>

<h2 id="decisões-de-design-no-ansible-do-projeto">Decisões de design no Ansible do projeto</h2>

<p>Antes de avançarmos para os comandos práticos, vale a pena abrir o capô e entender por que a estrutura do Ansible neste projeto é organizada dessa maneira. Não se trata de convenção estética: tomamos decisões muito conscientes para garantir que o laboratório seja rápido, reproduzível e seguro.</p>

<h3 id="construção-centralizada-de-certificados-no-host">Construção centralizada de certificados no host</h3>

<p>Em muitas instalações manuais, a geração de certificados TLS é uma bagunça espalhada: comandos <code class="language-plaintext highlighter-rouge">openssl</code> ou <code class="language-plaintext highlighter-rouge">cfssl</code> rodam dentro de cada VM, autoridades certificadoras são copiadas de um lado para o outro e qualquer erro exige caçar chaves perdidas no sistema de arquivos.</p>

<p>No nosso projeto, a role <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/ansible/infra-pki">ansible/infra-pki</a> roda inteiramente em <code class="language-plaintext highlighter-rouge">localhost</code>. Todos os certificados (Root CA, intermediárias de etcd, kubernetes e front-proxy, além de certificados de nós, clientes e service accounts) são forjados uma única vez na máquina hospedeira e armazenados em <code class="language-plaintext highlighter-rouge">artefatos/pki/</code>.</p>

<p>As máquinas virtuais nunca chegam a ver a chave privada da Root CA: elas recebem por cópia SSH estritamente o par exato de chave e certificado que precisam para funcionar. Além disso, controlamos a regeração com a variável <code class="language-plaintext highlighter-rouge">pki_regerar_certs: false</code>. Se você destruir e recriar as VMs, não precisa regerar toda a infraestrutura criptográfica do zero.</p>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> E as listas de revogação de certificados (CRLs)?</p><p>Em ambientes de produção corporativos, é indispensável manter listas de revogação de certificados (CRLs) ou validação OCSP para revogar imediatamente o acesso caso uma máquina seja comprometida. Em um laboratório local e descartável como o nosso, onde os nós são destruídos e recriados em minutos e o acesso é estritamente isolado no host, implementar CRLs adicionaria burocracia sem benefício prático imediato. Contudo, a nossa arquitetura de CA centralizada no host permite plugar mecanismos de revogação no futuro sem retrabalho.</p>
</div>

<h3 id="downloads-atômicos-e-reaproveitamento-de-binários">Downloads atômicos e reaproveitamento de binários</h3>

<p>A role <a href="https://github.com/vndmtrx/k8s-in-a-box/blob/main/ansible/infra-artefatos">ansible/infra-artefatos</a> segue o mesmo princípio de eficiência. Em vez de várias máquinas virtuais baixarem os binários do <code class="language-plaintext highlighter-rouge">kubelet</code>, <code class="language-plaintext highlighter-rouge">kubectl</code> e plugins CNI simultaneamente da internet (disputando banda e correndo risco de throttling ou timeout), o Ansible baixa cada pacote uma única vez na máquina física, confere o hash SHA256 e guarda em <code class="language-plaintext highlighter-rouge">artefatos/bin/</code>. A partir dali, os binários são distribuídos localmente via SSH em segundos.</p>

<h3 id="cache-local-de-imagens-oci-com-skopeo-e-importação-direta">Cache local de imagens OCI com Skopeo e importação direta</h3>

<p>O mesmo raciocínio dos binários aplica-se às pesadas imagens de contêiner da infraestrutura (<code class="language-plaintext highlighter-rouge">pause</code>, <code class="language-plaintext highlighter-rouge">etcd</code>, <code class="language-plaintext highlighter-rouge">kube-apiserver</code>, etc.). Baixar essas imagens repetidamente a cada nova criação de cluster desperdiça banda e tempo.</p>

<p>A role <code class="language-plaintext highlighter-rouge">k8s-kubelet</code> (<code class="language-plaintext highlighter-rouge">tasks/07-download-imagens-sistema.yml</code>) implementa um mecanismo de cache local no host:</p>

<ol>
  <li><strong>Detecção</strong>: o Ansible verifica se os tarballs das imagens necessárias já existem na pasta <code class="language-plaintext highlighter-rouge">imagens/</code> no host físico.</li>
  <li><strong>Download centralizado</strong>: se ausente, o primeiro nó manager (<code class="language-plaintext highlighter-rouge">manager1</code>) faz o download da imagem via <code class="language-plaintext highlighter-rouge">skopeo copy</code> direto do registro de contêineres para um tarball local e o transfere de volta para o host de desenvolvimento via <code class="language-plaintext highlighter-rouge">ansible.builtin.fetch</code>.</li>
  <li><strong>Distribuição e carga direta no runtime</strong>: para cada nó do cluster, o Ansible copia o tarball do host para a VM e o importa diretamente na camada de armazenamento do runtime configurado:
    <ul>
      <li>No <strong>CRI-O</strong>: a importação ocorre via <code class="language-plaintext highlighter-rouge">skopeo copy</code> diretamente para o <code class="language-plaintext highlighter-rouge">containers-storage</code>.</li>
      <li>No <strong>Containerd</strong>: a importação ocorre via <code class="language-plaintext highlighter-rouge">ctr -n k8s.io images import</code>.</li>
    </ul>
  </li>
</ol>

<p>Esse fluxo viabiliza um ambiente "offline-ready": após o primeiro provisionamento, você pode destruir todas as VMs e recriar o laboratório completo sem depender de conexão externa com registries.</p>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Cuidado com o espaço temporário no manager1</p><p>Como o download da imagem do contêiner é feito inicialmente dentro do nó <code class="language-plaintext highlighter-rouge">manager1</code> antes de ser transferido de volta para o host físico, certifique-se de que o disco virtual do <code class="language-plaintext highlighter-rouge">manager1</code> tenha espaço livre suficiente para manipular esses tarballs temporários durante o primeiro ciclo de provisionamento.</p>
</div>

<h3 id="extração-dinâmica-de-ferramentas-via-overlayfs">Extração dinâmica de ferramentas via OverlayFS</h3>

<p>Outra solução prática do projeto está na forma como instalamos o utilitário <code class="language-plaintext highlighter-rouge">etcdctl</code> no bastion <code class="language-plaintext highlighter-rouge">kubox</code>. No passado, baixávamos da internet o arquivo compactado completo de lançamento do etcd (um tarball pesado para extrair um único binário).</p>

<p>Adiciona complexidade fazer isso direto do contêiner? Sim, admito que foi uma escolha arquitetural deliberada da minha parte: é um download pesado a menos da internet e uma garantia de paridade exata de versão entre o binário administrativo e o etcd que está rodando no cluster.</p>

<p>Aproveitamos o fato de que a imagem oficial do etcd já está rodando no cluster como Static Pod no <code class="language-plaintext highlighter-rouge">manager1</code>. O Ansible inspeciona o contêiner ativo com <code class="language-plaintext highlighter-rouge">crictl inspect</code>, localiza o caminho exato da camada mesclada no OverlayFS (<code class="language-plaintext highlighter-rouge">merged layer</code>) no disco do nó e copia o executável <code class="language-plaintext highlighter-rouge">/usr/local/bin/etcdctl</code> diretamente dessa montagem do Linux para o bastion. É economia de banda, eliminação de dependência externa e uso inteligente da infraestrutura que já está no ar.</p>

<h3 id="padronização-semântica-de-roles-e-tasklists">Padronização semântica de roles e tasklists</h3>

<p>No passado do projeto, as roles usavam prefixos numéricos herdados (<code class="language-plaintext highlighter-rouge">00-artefatos</code>, <code class="language-plaintext highlighter-rouge">01-pki</code>, <code class="language-plaintext highlighter-rouge">02-sistema</code>, etc.). Conforme o cluster cresceu e incorporou novos CNIs e opções de runtime, essa numeração rígida virou uma armadilha de manutenção: para inserir um componente intermediário, era preciso renomear pastas inteiras.</p>

<p>Fizemos uma transição para uma taxonomia puramente semântica:</p>

<ol>
  <li><strong>infra-</strong>*: roles de fundação que preparam o terreno antes do Kubernetes existir (artefatos, PKI, balanceadores HAProxy e Keepalived, NFS e sistema operacional base).</li>
  <li><strong>k8s-</strong>*: componentes centrais do Kubernetes (nós base, agente kubelet e os manifests dos pods estáticos do control plane).</li>
  <li><strong>ops-</strong>*: ferramental administrativo e automações executadas a partir do bastion host <code class="language-plaintext highlighter-rouge">kubox</code>.</li>
  <li><strong>cni-</strong>* e <strong>addon-</strong>*: redes de sobreposição intercambiáveis e serviços auxiliares de valor agregado.</li>
</ol>

<p>Cada role possui tasklists enxutas e focadas em uma única responsabilidade. As tarefas são agrupadas logicamente (instalação de pacotes, renderização de templates Jinja2 e ativação de serviços systemd), permitindo que qualquer pessoa leia a pasta <code class="language-plaintext highlighter-rouge">tasks/</code> de uma role e entenda exatamente o que está acontecendo sem adivinhações.</p>

<h2 id="diagnóstico-e-validação-rápida">Diagnóstico e validação rápida</h2>

<p>Para validar se o seu inventário e suas variáveis estão configurados corretamente antes de rodar qualquer playbook, o Ansible oferece comandos de introspecção nativos.</p>

<h3 id="inspecionando-a-árvore-de-grupos">Inspecionando a árvore de grupos</h3>

<p>Para verificar como o Ansible enxerga a hierarquia de nós do cluster, execute a partir do diretório raiz:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ansible-inventory <span class="nt">-i</span> inventario/hosts.yml <span class="nt">--graph</span>
</code></pre></div></div>

<p>Esse comando renderiza a árvore completa de relações entre todos os grupos e nós. O ponto principal de atenção nessa inspeção é validar se os agrupamentos compostos (como <code class="language-plaintext highlighter-rouge">@cluster</code> unindo managers e workers, ou <code class="language-plaintext highlighter-rouge">@suporte</code> unindo balanceadores e NFS) estão aninhados corretamente. Se alguma máquina aparecer solta dentro de <code class="language-plaintext highlighter-rouge">@ungrouped</code> ou um grupo intermediário não for listado, significa que há um erro de indentação no seu arquivo <code class="language-plaintext highlighter-rouge">hosts-*.yml</code>.</p>

<h3 id="verificando-variáveis-resolvidas-por-nó">Verificando variáveis resolvidas por nó</h3>

<p>Para confirmar quais variáveis e valores chegam a uma máquina específica, use a flag <code class="language-plaintext highlighter-rouge">--host</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ansible-inventory <span class="nt">-i</span> inventario/hosts.yml <span class="nt">--host</span> manager1 | <span class="nb">grep</span> <span class="nt">-E</span> <span class="s1">'versao_kubernetes|plugin_cni|memory'</span>
</code></pre></div></div>

<p>A saída confirma a consolidação das variáveis de host com as variáveis de grupo:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">    </span><span class="nl">"memory"</span><span class="p">:</span><span class="w"> </span><span class="mi">3072</span><span class="err">,</span><span class="w">
    </span><span class="nl">"plugin_cni"</span><span class="p">:</span><span class="w"> </span><span class="s2">"cilium"</span><span class="err">,</span><span class="w">
    </span><span class="nl">"versao_kubernetes"</span><span class="p">:</span><span class="w"> </span><span class="s2">"v1.37.0"</span><span class="err">,</span><span class="w">
</span></code></pre></div></div>

<h3 id="testando-variáveis-ativas-com-o-módulo-debug">Testando variáveis ativas com o módulo debug</h3>

<p>Caso você queira inspecionar o valor de uma variável em todos os nós simultaneamente sem rodar nenhum playbook longo, o módulo <code class="language-plaintext highlighter-rouge">debug</code> em modo ad-hoc é a ferramenta ideal. Lembre-se apenas de que essa checagem ad-hoc exige que as máquinas virtuais estejam ligadas e acessíveis na rede privada (<code class="language-plaintext highlighter-rouge">make cluster-up</code>):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ ANSIBLE_CONFIG</span><span class="o">=</span>./ansible/.ansible.cfg ansible <span class="nt">-m</span> debug <span class="nt">-a</span> <span class="s2">"var=versao_kubernetes"</span> cluster
</code></pre></div></div>

<p>Saída esperada:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>manager1 | SUCCESS =&gt; {
    "versao_kubernetes": "v1.37.0"
}
worker1 | SUCCESS =&gt; {
    "versao_kubernetes": "v1.37.0"
}
worker2 | SUCCESS =&gt; {
    "versao_kubernetes": "v1.37.0"
}
</code></pre></div></div>

<h2 id="conclusão">Conclusão</h2>

<p>Compreender o inventário e a estrutura de variáveis é a diferença entre operar automação como quem copia comandos cegamente e operar com controle total da arquitetura. No <strong>k8s-in-a-box</strong>, cada arquivo tem um propósito bem delimitado: o <code class="language-plaintext highlighter-rouge">hosts-*.yml</code> dimensiona e posiciona as peças, o <code class="language-plaintext highlighter-rouge">all.yml</code> dita as regras e versões da stack, e a tríade de playbooks executa as tarefas na ordem estrita de dependências.</p>

<p>Com o inventário compreendido e o nosso cluster configurado com o <strong>Kubernetes v1.37.0</strong>, estamos prontos para a próxima etapa: no próximo post (<a href="/posts/k8sbox-preparacao-sistema-base/">Parte 4: Preparação do Sistema Base</a>), vamos entrar dentro das VMs e aplicar a role <code class="language-plaintext highlighter-rouge">infra-sistema</code>, configurando pacotes essenciais, módulos de kernel (<code class="language-plaintext highlighter-rouge">br_netfilter</code>, <code class="language-plaintext highlighter-rouge">overlay</code>), parâmetros de <code class="language-plaintext highlighter-rouge">sysctl</code>, desativação de swap e a política de SELinux para receber os binários do Kubernetes.</p>

<h2 id="referências">Referências</h2>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p><strong>Kubernetes v1.37: Garhwal Release Notes</strong> {<em>Kubernetes Blog</em>} (<a href="https://kubernetes.io/blog/2026/08/26/kubernetes-v1-37-release/">Link</a>) <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>[&quot;Eduardo N. S. R.&quot;]</name></author><category term="Tutoriais" /><category term="Kubernetes" /><category term="Ansible" /><category term="DevOps" /><category term="Infraestrutura" /><category term="Linux" /><summary type="html"><![CDATA[No post anterior, deixamos nossas máquinas virtuais ligadas e conversando em uma rede privada isolada no KVM. Temos load balancers, managers, workers e um servidor NFS com endereços estáticos e resolução de nomes básica. Se tentarmos rodar qualquer comando do Kubernetes nelas agora, nada vai acontecer: não há binários instalados, não há certificados emitidos e nenhum processo do control plane existe. Antes de enviar a primeira instrução para as máquinas, precisamos responder a uma pergunta de arquitetura: onde vive a verdade sobre as versões, as faixas de rede e os papéis de cada nó?]]></summary></entry><entry><title type="html">A História dos Contêineres</title><link href="https://vndmtrx.github.io/posts/conteineres-historia/" rel="alternate" type="text/html" title="A História dos Contêineres" /><published>2026-09-18T14:08:00+00:00</published><updated>2026-09-18T14:08:00+00:00</updated><id>https://vndmtrx.github.io/posts/conteineres-historia</id><content type="html" xml:base="https://vndmtrx.github.io/posts/conteineres-historia/"><![CDATA[<p>Tenho um amigo que usa Slackware no trabalho. Não por nostalgia, não por teimosia, não por falta de opção. Por escolha técnica deliberada, com a convicção serena de quem conhece o sistema até a raiz e não quer camadas de abstração que ele não pediu. É o tipo de pessoa que abre um <code class="language-plaintext highlighter-rouge">man 8 chroot</code> por lazer, tem opinião formada sobre hierarquia de processo em *BSD e consegue passar horas discutindo as diferenças filosóficas entre o modelo de jails do FreeBSD e o isolamento de processos do OpenBSD. Um entusiasta no sentido mais verdadeiro da palavra: aquele que estuda porque gosta, e não porque a certificação exige.</p>

<p>Numa conversa sobre Docker e Kubernetes, num desses papos técnicos que não tem hora pra acabar, ele fez uma observação que ficou martelando na minha cabeça. Disse que entendia a utilidade das ferramentas, mas que sentia que estava aprendendo conceitos novos. Que o mundo dos contêineres parecia um universo próprio, com vocabulário e princípios específicos que ele ainda estava assimilando.</p>

<p>Fiquei olhando pra ele por um segundo.</p>

<p>Eu mesmo não vim do nada. Passei por Mandrake, Conectiva e Kurumin nos anos das instalações que levavam três horas e uma reza. Depois Slackware, Gentoo, Arch. Fiz RHEL em ambiente corporativo. Hoje uso Debian. No meio do caminho, fui fundo o suficiente para montar um sistema do zero com o LFS e o BLFS, inclusive em contextos de sistemas embarcados, só para entender o que estava acontecendo debaixo do capô. <code class="language-plaintext highlighter-rouge">chroot</code>? Eu conhecia. Usava com menos frequência que meu amigo, que compila os próprios pacotes no Slackware dele até hoje, mas conhecia.</p>

<p>Ele estava começando a se aventurar pelo mundo dos contêineres agora. Experiências simples por aqui e por ali, e aquela conversa era parte do processo de entrar de cabeça no assunto. Eu já tinha um bom caminho percorrido nesse terreno: Docker, Compose, Kubernetes, o ecossistema todo.</p>

<p>A dúvida que ele trouxe era essa: com um hipervisor, um sistema operacional convidado e um contêiner rodando no topo, não seriam três camadas de indireção empilhadas? Não seria isso mais pesado do que simplesmente usar uma VM diretamente?</p>

<p>É exatamente o tipo de pergunta que faz sentido vir de quem pensa em termos de hardware real, latência e overhead. Não é ingenuidade. É um engenheiro de infraestrutura levantando uma objeção legítima com base no modelo mental que a indústria construiu em torno dos contêineres: o de que eles são VMs mais leves. Esse modelo é errado. E é esse erro que explica tudo.</p>

<p>A conversa que se seguiu foi a inspiração direta para essa série. Porque o que ficou evidente ali é que o problema não é falta de conhecimento técnico. É a narrativa da indústria que faz parecer que Docker caiu do céu em 2013 e inventou o isolamento de processos. Não inventou. Continuou uma história que começa muito antes.</p>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Nota da Série</p><p>Este post abre a série <strong>"Contêineres de Cabo a Rabo"</strong>, que explora os contêineres desde a história até a prática avançada. Se você já conhece bem a linha do tempo e quer ir direto ao mecanismo interno, pule para a <a href="/posts/conteineres-fundamentos/">Parte 1: Fundamentos e Anatomia</a>. Se quiser entender as ferramentas do dia a dia, vai para a <strong>Parte 2: Contêineres na Prática</strong> <em>(em breve)</em>.</p>
</div>

<details class="toc-box">
  <summary><strong>Sumário &amp; Índice de Seções (TOC)</strong></summary>
<ul id="markdown-toc">
  <li><a href="#o-que-o-docker-realmente-fez" id="markdown-toc-o-que-o-docker-realmente-fez">O que o Docker realmente fez</a></li>
  <li><a href="#1979-a-primeira-jaula" id="markdown-toc-1979-a-primeira-jaula">1979: A Primeira Jaula</a></li>
  <li><a href="#2000-a-primeira-prisão-de-verdade" id="markdown-toc-2000-a-primeira-prisão-de-verdade">2000: A Primeira Prisão de Verdade</a></li>
  <li><a href="#2001-2004-o-linux-tenta-acompanhar" id="markdown-toc-2001-2004-o-linux-tenta-acompanhar">2001-2004: O Linux Tenta Acompanhar</a></li>
  <li><a href="#2006-o-google-resolve-seu-próprio-problema" id="markdown-toc-2006-o-google-resolve-seu-próprio-problema">2006: O Google Resolve Seu Próprio Problema</a></li>
  <li><a href="#2008-lxc-e-o-problema-da-ergonomia" id="markdown-toc-2008-lxc-e-o-problema-da-ergonomia">2008: LXC e o Problema da Ergonomia</a></li>
  <li><a href="#2013-cinco-minutos-que-mudaram-a-indústria" id="markdown-toc-2013-cinco-minutos-que-mudaram-a-indústria">2013: Cinco Minutos que Mudaram a Indústria</a></li>
  <li><a href="#2015-a-indústria-faz-as-pazes" id="markdown-toc-2015-a-indústria-faz-as-pazes">2015: A Indústria Faz as Pazes</a></li>
  <li><a href="#2017-em-diante-o-ecossistema-adulto" id="markdown-toc-2017-em-diante-o-ecossistema-adulto">2017 em diante: O Ecossistema Adulto</a></li>
  <li><a href="#o-que-a-história-diz-sobre-como-a-indústria-funciona" id="markdown-toc-o-que-a-história-diz-sobre-como-a-indústria-funciona">O que a história diz sobre como a indústria funciona</a></li>
  <li><a href="#conclusão" id="markdown-toc-conclusão">Conclusão</a></li>
  <li><a href="#referências" id="markdown-toc-referências">Referências</a></li>
</ul>

</details>

<h2 id="o-que-o-docker-realmente-fez">O que o Docker realmente fez</h2>

<p>Solomon Hykes não inventou o isolamento de processos. Não inventou namespaces, não inventou cgroups, não inventou sistemas de arquivos em camadas.</p>

<p>O que ele fez foi pegar uma série de tecnologias que existiam espalhadas no ecossistema Linux, que qualquer administrador de sistemas experiente conhecia individualmente mas que eram trabalhosas de configurar juntas, e empacotou tudo isso numa experiência de usuário tão fluida que mudou a maneira como a indústria pensa sobre entrega de software. É a diferença entre saber que existe motor a combustão, suspensão e sistema de freios e ter um carro na garagem pronto pra usar.</p>

<p>A história dos contêineres é, nesse sentido, uma história de convergência. Tecnologias desenvolvidas em contextos diferentes, por equipes sem coordenação direta, ao longo de décadas, que foram gradualmente se encontrando até o momento em que alguém teve o discernimento de perceber que elas podiam andar juntas.</p>

<h2 id="1979-a-primeira-jaula">1979: A Primeira Jaula</h2>

<p>Tudo começa em 1979, durante o desenvolvimento da versão 7 do Unix. Bill Joy e outros pioneiros adicionaram ao sistema a chamada de sistema <code class="language-plaintext highlighter-rouge">chroot</code> <sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>. A ideia era simples: alterar o diretório raiz aparente para um processo e todos os seus filhos. Se você rodasse um programa sob <code class="language-plaintext highlighter-rouge">chroot /jail</code>, para aquele programa a barra (<code class="language-plaintext highlighter-rouge">/</code>) passava a ser a pasta <code class="language-plaintext highlighter-rouge">/jail</code>.</p>

<p>O caso de uso original era pragmático. Compilar pacotes de software em um ambiente limpo sem riscos de afetar o sistema principal. Testar instalações sem estragar a máquina. A lógica faz sentido até hoje.</p>

<p>O problema é que o <code class="language-plaintext highlighter-rouge">chroot</code> isolava <em>apenas</em> o sistema de arquivos. A aplicação enjaulada ainda enxergava todos os processos do sistema operacional, abria conexões de rede sem restrição e consumia memória livremente até derrubar a máquina. E escapar de uma jaula <code class="language-plaintext highlighter-rouge">chroot</code> sendo <code class="language-plaintext highlighter-rouge">root</code> sempre foi, no Linux, um truque de entrada de manual: um passo, três linhas, pronto.</p>

<p>A primeira jaula existia. Mas as paredes tinham buracos por todos os lados.</p>

<h2 id="2000-a-primeira-prisão-de-verdade">2000: A Primeira Prisão de Verdade</h2>

<p>A solução veio de fora do Linux, no ano 2000, quando o sistema FreeBSD introduziu as <strong>FreeBSD Jails</strong> <sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>, desenvolvidas por Poul-Henning Kamp. Meu amigo do Slackware, se estiver lendo isso, está provavelmente assentindo nesse ponto.</p>

<p>As Jails foram a primeira tecnologia real de virtualização em nível de sistema operacional. Elas isolavam não só o sistema de arquivos, mas a árvore de processos completa, o endereço IP e os privilégios administrativos. Cada jail tinha seu próprio hostname, sua própria tabela de processos e seu próprio usuário <code class="language-plaintext highlighter-rouge">root</code> restrito ao escopo daquela jaula. O <code class="language-plaintext highlighter-rouge">root</code> da jail não era o <code class="language-plaintext highlighter-rouge">root</code> da máquina.</p>

<p>Isso era filosoficamente diferente do <code class="language-plaintext highlighter-rouge">chroot</code>. Não era uma pasta com paredes. Era um compartimento com porta, teto e vigilância.</p>

<p>O modelo influenciou o restante do ecossistema Unix. Se você usa *BSD por escolha técnica, sabe exatamente por quê essa abordagem é elegante: é isolamento sem hipervisor, sem emulação, sem overhead. É o sistema operacional fazendo o que deveria fazer desde o começo.</p>

<h2 id="2001-2004-o-linux-tenta-acompanhar">2001-2004: O Linux Tenta Acompanhar</h2>

<p>O universo Linux não ficou parado. Em 2001 nasceu o projeto <strong>Linux-VServer</strong>, seguido pelo <strong>OpenVZ</strong> em 2005. Ambos entregavam virtualização leve com desempenho excelente, mas carregavam um defeito de origem que limitou sua adoção: exigiam patches pesados aplicados diretamente no código-fonte do kernel.</p>

<p>Como esses patches nunca foram aceitos na árvore oficial mantida por Linus Torvalds, usar contêineres em Linux exigia rodar kernels customizados mantidos por terceiros. Para uma equipe corporativa que precisa de suporte, atualização de segurança e previsibilidade, isso era um pesadelo logístico.</p>

<p>Em 2004, a Sun Microsystems deu um passo diferente com o Solaris 10 e as <strong>Solaris Zones</strong> <sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>. As Zones combinaram isolamento de processos com o inovador sistema de arquivos ZFS, permitindo criar clones e snapshots instantâneos de sistemas inteiros com custo ínfimo de disco. Foi a demonstração de que isolamento e gerenciamento de artefatos podiam andar juntos com elegância.</p>

<p>O Linux precisava de uma resposta que não exigisse patches externos. E ela viria de um lugar inesperado.</p>

<h2 id="2006-o-google-resolve-seu-próprio-problema">2006: O Google Resolve Seu Próprio Problema</h2>

<p>Por volta de 2006, os engenheiros do Google enfrentavam um problema de escala que poucos outros times no mundo podiam compreender. Dezenas de milhares de servidores rodando a busca, o Gmail e outros serviços, todos competindo por recursos de CPU e memória sem nenhum mecanismo formal de governança. Um processo mal comportado podia afetar serviços vizinhos. A telemetria era imprecisa. A previsibilidade era uma ilusão bem disfarçada.</p>

<p>Paul Menage e Rohit Seth criaram uma solução interna chamada inicialmente de <em>Process Containers</em> <sup id="fnref:4"><a href="#fn:4" class="footnote" rel="footnote" role="doc-noteref">4</a></sup>. O nome era descritivo: um mecanismo para agrupar processos e aplicar limites rígidos de recursos sobre esse grupo. CPU, memória, operações de disco. Limites que o kernel respeitaria independentemente do que o processo tentasse fazer.</p>

<p>Para evitar confusão com o termo "contêineres" que estava surgindo em outros contextos, a tecnologia foi renomeada para <strong>cgroups</strong> (<em>control groups</em>). Em 2008, foi aceita na árvore principal do kernel Linux 2.6.24.</p>

<p>Diferente do Linux-VServer e do OpenVZ, os cgroups entraram no kernel oficial. Qualquer distribuição Linux que rodasse um kernel moderno tinha acesso a essa tecnologia sem patches, sem customização, sem dependência de repositórios externos.</p>

<p>A fundação estava pronta.</p>

<h2 id="2008-lxc-e-o-problema-da-ergonomia">2008: LXC e o Problema da Ergonomia</h2>

<p>Com os cgroups no kernel e os namespaces sendo gradualmente maturados, o <strong>LXC</strong> (<em>Linux Containers</em>) surgiu em 2008 combinando essas primitivas em um conjunto de utilitários de linha de comando. Era possível subir contêineres completos sem modificar o kernel, sem aplicar patches, sem depender de projetos externos.</p>

<p>O problema era que fazer isso funcionava, mas não era divertido. Configurar redes virtuais, pontos de montagem, permissões e limites de recursos no LXC exigia conhecimento profundo de administração de sistemas e uma dose generosa de paciência. Não era uma ferramenta que você recomendaria para um desenvolvedor que queria rodar sua aplicação localmente.</p>

<p>Era a tecnologia certa, mas ainda faltava a interface certa.</p>

<h2 id="2013-cinco-minutos-que-mudaram-a-indústria">2013: Cinco Minutos que Mudaram a Indústria</h2>

<p>Em março de 2013, uma pequena empresa de plataforma como serviço chamada dotCloud decidiu abrir o código de uma ferramenta interna. Solomon Hykes subiu ao palco da PyCon <sup id="fnref:5"><a href="#fn:5" class="footnote" rel="footnote" role="doc-noteref">5</a></sup> para uma palestra relâmpago. Cinco minutos. Sem slides elaborados. Sem demo preparada com horas de ensaio.</p>

<p>A sala não era grande. O público não estava esperando uma revolução. Hykes mostrou como baixar uma imagem, rodar um processo isolado, destruir o contêiner e começar de novo. Tudo em segundos, com um único comando por vez.</p>

<p>A plateia entendeu imediatamente o que estava vendo. Não era magia. Era LXC com uma interface que qualquer pessoa conseguia usar, um formato de imagem baseado em camadas reutilizáveis e um repositório centralizado para compartilhamento. Mas a combinação dessas três coisas, apresentada com aquela fluência operacional, mudou o ponto de equilíbrio do mercado em tempo real.</p>

<p>O Docker não foi a tecnologia mais sofisticada. Foi a que chegou no momento certo, com a embalagem certa, para um mercado que já estava pronto para adotar contêineres mas não sabia como começar.</p>

<h2 id="2015-a-indústria-faz-as-pazes">2015: A Indústria Faz as Pazes</h2>

<p>O sucesso do Docker gerou um problema novo. Toda a cadeia de ferramentas de entrega de software começou a depender de um formato proprietário controlado por uma única empresa. Kubernetes, as grandes nuvens públicas, as ferramentas de CI/CD: tudo falava Docker.</p>

<p>Para evitar esse aprisionamento tecnológico, em 2015 as grandes empresas do ecossistema se uniram sob a Linux Foundation e criaram a <strong>Open Container Initiative (OCI)</strong> <sup id="fnref:6"><a href="#fn:6" class="footnote" rel="footnote" role="doc-noteref">6</a></sup>. Docker, Red Hat, Google, IBM, Microsoft. O objetivo era padronizar tanto o formato de imagem quanto o mecanismo de execução de contêineres em especificações abertas e neutras.</p>

<p>O Docker doou seu motor de execução central (<code class="language-plaintext highlighter-rouge">libcontainer</code>), que foi reescrito como a ferramenta independente <code class="language-plaintext highlighter-rouge">runc</code>. A especificação de imagem OCI garantiu que qualquer ferramenta que seguisse o padrão pudesse criar e consumir imagens compatíveis com qualquer outra.</p>

<p>A guerra de formatos que poderia ter fragmentado o ecossistema foi evitada antes de começar.</p>

<h2 id="2017-em-diante-o-ecossistema-adulto">2017 em diante: O Ecossistema Adulto</h2>

<p>Nos anos seguintes, a padronização abriu espaço para uma diversidade saudável de implementações. O projeto <code class="language-plaintext highlighter-rouge">containerd</code> foi doado para a Cloud Native Computing Foundation (CNCF) e virou o runtime de referência. A Red Hat desenvolveu o <strong>Podman</strong> e o <strong>CRI-O</strong> para ambientes corporativos e Kubernetes, resolvendo preocupações reais de segurança com o modelo de daemon centralizado do Docker.</p>

<p>Implementações alternativas de runtime de baixo nível surgiram com objetivos específicos: o <code class="language-plaintext highlighter-rouge">crun</code> em C puro, mais rápido e com menor pegada de memória; o <code class="language-plaintext highlighter-rouge">youki</code> em Rust, com garantias de segurança de memória; o <code class="language-plaintext highlighter-rouge">gVisor</code> da Google, interceptando chamadas de sistema em espaço de usuário para isolamento extremo. O Kata Containers integrou micro-máquinas virtuais para os casos onde o compartilhamento de kernel era inaceitável.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1979        2000        2004        2006-2008        2008        2013        2015        2017+
 │           │           │              │             │           │           │           │
 ▼           ▼           ▼              ▼             ▼           ▼           ▼           ▼
chroot    FreeBSD     Solaris        cgroups         LXC       Docker        OCI      containerd
(Unix)     Jails       Zones       (Google no      (Kernel    (dotCloud/   (Padrão     Podman
             │       (com ZFS)     mainline)       nativo)      Hykes)     aberto)     CRI-O
             │                                                   │
        Linux-VServer                                       libcontainer
           (2001)                                             -&gt; runc
</code></pre></div></div>

<h2 id="o-que-a-história-diz-sobre-como-a-indústria-funciona">O que a história diz sobre como a indústria funciona</h2>

<p>Existe um padrão aqui.</p>

<p>As tecnologias fundamentais raramente surgem prontas. Elas emergem em contextos distintos, resolvendo problemas específicos de suas épocas, e passam anos existindo em paralelo sem se encontrar. O <code class="language-plaintext highlighter-rouge">chroot</code> era uma ferramenta de build. As Jails eram uma resposta de segurança do ecossistema *BSD. Os cgroups nasceram de uma necessidade operacional do Google. Os union filesystems existiam para gerenciamento de sistemas de arquivos em live CDs.</p>

<p>O Docker não inventou nenhuma dessas coisas. O Docker percebeu que elas podiam funcionar juntas.</p>

<p>Meu amigo do Slackware conhecia <code class="language-plaintext highlighter-rouge">chroot</code>, conhecia jails, tinha até experimentado LXC em algum momento. O que ele não havia conectado era que o Docker era, essencialmente, uma interface para os mesmos conceitos que ele já dominava. A terminologia mudou. A experiência de uso ficou polida. A abstração ganhou uma camada de marketing pesada. Mas o kernel debaixo continuava fazendo o mesmo trabalho que sempre fez.</p>

<p>É uma lição que vale além do universo dos contêineres. A inovação real raramente é <em>ex nihilo</em>. Quase sempre é síntese, integração, timing.</p>

<p>E às vezes é uma palestra de cinco minutos na hora certa.</p>

<h2 id="conclusão">Conclusão</h2>

<p>Na <a href="/posts/conteineres-fundamentos/">Parte 1 da série</a>, vamos descer ao nível do kernel Linux para entender exatamente o que acontece quando você digita <code class="language-plaintext highlighter-rouge">docker run</code>: namespaces, cgroups v2, OverlayFS e a especificação OCI. Se você é o tipo de pessoa que prefere entender o mecanismo antes de usar a ferramenta, é pra lá que você vai querer ir.</p>

<h2 id="referências">Referências</h2>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p><strong>chroot(2) Linux Manual Page</strong> {<em>Michael Kerrisk, man7.org</em>} (<a href="https://man7.org/linux/man-pages/man2/chroot.2.html">Link</a>) <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p><strong>Jails: Confining the omnipotent root</strong> {<em>Poul-Henning Kamp e Robert N. M. Watson, Proceedings of the 2nd International SANE Conference, 2000</em>} (<a href="https://papers.freebsd.org/2000/phk-jails/">Link</a>) <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3">
      <p><strong>Solaris Zones: System Administration Guide: Solaris Containers-Resource Management and Solaris Zones</strong> {<em>Sun Microsystems / Oracle Documentation</em>} (<a href="https://docs.oracle.com/cd/E19253-01/817-1592/">Link</a>) <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4">
      <p><strong>Process containers</strong> {<em>Jonathan Corbet sobre patches de Paul Menage (Google), LWN.net, 2007</em>} (<a href="https://lwn.net/Articles/236038/">Link</a>) <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5">
      <p><strong>The Future of Linux Containers</strong> {<em>Solomon Hykes, PyCon US Lightning Talks, 2013</em>} (<a href="https://www.youtube.com/watch?v=wW9CAH9nSLs">Link</a>) <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:6">
      <p><strong>Open Container Initiative (OCI) Specifications</strong> {<em>The Linux Foundation, OpenContainers GitHub</em>} (<a href="https://opencontainers.org/">Link</a>) <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>[&quot;Eduardo N. S. R.&quot;]</name></author><category term="Artigos" /><category term="Docker" /><category term="Contêineres" /><category term="Linux" /><category term="História" /><summary type="html"><![CDATA[Tenho um amigo que usa Slackware no trabalho. Não por nostalgia, não por teimosia, não por falta de opção. Por escolha técnica deliberada, com a convicção serena de quem conhece o sistema até a raiz e não quer camadas de abstração que ele não pediu. É o tipo de pessoa que abre um man 8 chroot por lazer, tem opinião formada sobre hierarquia de processo em *BSD e consegue passar horas discutindo as diferenças filosóficas entre o modelo de jails do FreeBSD e o isolamento de processos do OpenBSD. Um entusiasta no sentido mais verdadeiro da palavra: aquele que estuda porque gosta, e não porque a certificação exige.]]></summary></entry><entry><title type="html">A Odisséia da Lanternagem Estética do Blog</title><link href="https://vndmtrx.github.io/posts/lanternagem-blog/" rel="alternate" type="text/html" title="A Odisséia da Lanternagem Estética do Blog" /><published>2026-09-13T20:00:00+00:00</published><updated>2026-09-13T20:00:00+00:00</updated><id>https://vndmtrx.github.io/posts/lanternagem-blog</id><content type="html" xml:base="https://vndmtrx.github.io/posts/lanternagem-blog/"><![CDATA[<p>Existe uma sensação física de incômodo que todo desenvolvedor com mania de artesão conhece bem: olhar para a própria criação e sentir que ela não tem a sua cara. Durante anos, este blog operou sob a sombra acolhedora e cinzenta do Minima, o tema padrão que o Jekyll entrega quando você cria um repositório no GitHub Pages. Ele funciona. Compila sem chiar. Carrega rápido e não quebra.</p>

<p>Só que a sensação constante era a de morar de aluguel em uma casa mobiliada por outra pessoa. Você não pode pintar uma parede sem o reboco inteiro desabar sobre a sua cabeça.</p>

<p>A gota d'água não foi uma falha técnica repentina. Foi o acúmulo silencioso do tempo.</p>

<p>Conforme o blog foi crescendo, acumulando dezenas de artigos analíticos, tutoriais de infraestrutura, trilhas de Kubernetes e reflexões sobre engenharia, o contraste entre o conteúdo e o continente foi ficando incômodo. Eu passava horas polindo argumentos, formatando blocos de código e desenhando diagramas em ASCII, para no final ver tudo espremido em uma tipografia genérica de sistema, com espaçamentos desajeitados e uma paleta sem vida.</p>

<p>O invólucro não fazia justiça ao que estava dentro.</p>

<p>Tomei coragem, abri o terminal e decretei a reforma geral. Foram dias de faxina pesada: arranquei o tema Minima pela raiz, expurguei camadas acumuladas de CSS utilitário, adotei o Pico CSS v2 como fundação semântica, criei uma paleta própria com modos claro e escuro calibrados, trouxe fontes e scripts para dentro do repositório em modo 100% offline-first e validei cada link com o implacável HTMLProofer.</p>

<p>Se acomode na cadeira e pegue um café. A jornada teve tropeços divertidos, algumas teimosias e muita escovação de bytes.</p>

<details class="toc-box" open="">
  <summary><strong>Sumário &amp; Índice de Seções (TOC)</strong></summary>
<ul id="markdown-toc">
  <li><a href="#o-inventário-do-caos-a-herança-maldita-do-minima-e-do-open-props" id="markdown-toc-o-inventário-do-caos-a-herança-maldita-do-minima-e-do-open-props">O inventário do caos: a herança maldita do Minima e do Open Props</a></li>
  <li><a href="#a-revelação-semântica-por-que-o-pico-css-v2" id="markdown-toc-a-revelação-semântica-por-que-o-pico-css-v2">A revelação semântica: por que o Pico CSS v2?</a></li>
  <li><a href="#a-alquimia-das-cores-construindo-a-paleta-própria-classic-vs-dark" id="markdown-toc-a-alquimia-das-cores-construindo-a-paleta-própria-classic-vs-dark">A alquimia das cores: construindo a paleta própria (Classic vs Dark)</a></li>
  <li><a href="#a-faxina-dos-ativos-a-paranóia-saudável-do-100-offline-first" id="markdown-toc-a-faxina-dos-ativos-a-paranóia-saudável-do-100-offline-first">A faxina dos ativos: a paranóia saudável do 100% offline-first</a></li>
  <li><a href="#tipografia-e-ritmo-vertical-o-design-para-leitura-imersiva" id="markdown-toc-tipografia-e-ritmo-vertical-o-design-para-leitura-imersiva">Tipografia e ritmo vertical: o design para leitura imersiva</a></li>
  <li><a href="#a-saga-da-matemática-katex-local-sem-nós-cegos" id="markdown-toc-a-saga-da-matemática-katex-local-sem-nós-cegos">A saga da matemática: KaTeX local sem nós cegos</a></li>
  <li><a href="#sumários-dinâmicos-e-componentes-modulares-o-poder-do-kramdown-nativo" id="markdown-toc-sumários-dinâmicos-e-componentes-modulares-o-poder-do-kramdown-nativo">Sumários dinâmicos e componentes modulares: o poder do Kramdown nativo</a></li>
  <li><a href="#o-teste-de-fogo-compilando-101-páginas-e-domando-o-htmlproofer" id="markdown-toc-o-teste-de-fogo-compilando-101-páginas-e-domando-o-htmlproofer">O teste de fogo: compilando 101 páginas e domando o HTMLProofer</a></li>
  <li><a href="#conclusão-e-o-prazer-do-jardim-digital" id="markdown-toc-conclusão-e-o-prazer-do-jardim-digital">Conclusão e o prazer do jardim digital</a></li>
  <li><a href="#referências" id="markdown-toc-referências">Referências</a></li>
</ul>

</details>

<h2 id="o-inventário-do-caos-a-herança-maldita-do-minima-e-do-open-props">O inventário do caos: a herança maldita do Minima e do Open Props</h2>

<p>O ponto de partida de qualquer reforma honesta é olhar a bagunça de frente. Sem filtro e sem vergonha.</p>

<p>O ecossistema do Jekyll tem uma comodidade perigosa chamada <em>theme-gem</em>. Quando você declara <code class="language-plaintext highlighter-rouge">theme: minima</code> no seu arquivo <code class="language-plaintext highlighter-rouge">_config.yml</code>, o Jekyll não coloca nenhum arquivo de estilo dentro da sua pasta de trabalho. Ele busca os templates e os parciais de SCSS compilados dentro da instalação global da gem do Ruby, em um diretório oculto do sistema operacional.</p>

<p>A princípio, isso soa como mágica pura:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>meu-blog/
├── _config.yml
├── _posts/
└── index.md
</code></pre></div></div>
<p><em>A ilusão da simplicidade: o repositório parece limpo porque toda a complexidade está oculta na gem.</em></p>

<p>O problema começa quando você decide customizar qualquer detalhe mínimo da interface.</p>

<p>Quer mudar a largura da coluna principal de leitura? Você precisa criar uma pasta <code class="language-plaintext highlighter-rouge">_sass/</code>, descobrir qual era o nome exato da variável que o tema original usava e torcer para que o seu arquivo seja importado na ordem correta. Quer alterar o comportamento do cabeçalho da página? Precisa copiar o template <code class="language-plaintext highlighter-rouge">header.html</code> original da gem para dentro de <code class="language-plaintext highlighter-rouge">_includes/</code> e começar a fazer alterações cirúrgicas no código alheio.</p>

<p>Em pouco tempo, o repositório vira uma colcha de retalhos de arquivos sobrepostos.</p>

<p>Em uma tentativa anterior de modernizar a aparência do site sem ter o trabalho de reconstruir tudo do zero, cometi o clássico erro de empilhar soluções. Resolvi enxertar o Open Props, uma biblioteca moderna de variáveis CSS e tokens de design.</p>

<p>A intenção era nobre. O resultado prático foi uma guerra civil de especificidade.</p>

<p>No DevTools do navegador, inspecionar um simples título de postagem se transformou em uma sessão de arqueologia digital. Eram três camadas distintas de estilos disputando a prioridade de cada seletor: as regras padrão do Minima brigando com as variáveis do Open Props, que por sua vez eram atropeladas por declarações com <code class="language-plaintext highlighter-rouge">!important</code> que eu mesmo havia escrito na pressa meses antes para consertar alinhamentos tortos.</p>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* O cemitério de especificidade herdada que acumulamos */</span>
<span class="nc">.site-header</span> <span class="nc">.wrapper</span> <span class="nc">.site-title</span> <span class="p">{</span>
  <span class="nl">color</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">gray-9</span><span class="p">)</span> <span class="o">!</span><span class="n">important</span><span class="p">;</span>
  <span class="nl">font-family</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">font-sans</span><span class="p">);</span>
  <span class="nl">letter-spacing</span><span class="p">:</span> <span class="m">-0</span><span class="mi">.05em</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p><em>Guerra de especificidade no inspetor: seletores profundos brigando por atenção.</em></p>

<p>A manutenção virou um pesadelo constante. Mudar o tamanho de uma fonte no rodapé fazia o cabeçalho quebrar em telas de celular; ajustar a cor de um link na Home estragava o contraste das tags nos artigos.</p>

<p>A casa não era minha. Eu me sentia como um inquilino assustado com medo de ligar o chuveiro e queimar a fiação inteira do prédio.</p>

<p>Chega.</p>

<p>Fui até o <code class="language-plaintext highlighter-rouge">Gemfile</code> e deletei a linha <code class="language-plaintext highlighter-rouge">gem "minima"</code>. Em seguida, passei a vassoura na pasta de estilos antigos e limpei os arquivos temporários com <code class="language-plaintext highlighter-rouge">bundle exec jekyll clean</code>.</p>

<p>Assumi a posse definitiva de cada linha de código que seria servida aos leitores deste blog: zero dependências ocultas e zero mágicas de bastidor.</p>

<h2 id="a-revelação-semântica-por-que-o-pico-css-v2">A revelação semântica: por que o Pico CSS v2?</h2>

<p>Quando você decide jogar fora um tema pronto, a primeira encruzilhada que aparece no caminho é a escolha do novo motor de CSS. Em pleno 2026, as opções disponíveis no mercado parecem caminhar para extremos opostos: de um lado, os gigantes utilitários corporativos liderados pelo Tailwind CSS; do outro, bibliotecas pesadas de componentes como Bootstrap e Bulma, carregando dezenas de estilos que fazem qualquer site parecer um painel administrativo de telecomunicações dos anos 2010.</p>

<p>Considerei o Tailwind por cerca de cinco minutos. Descartei logo em seguida.</p>

<p>Não me entenda mal: o Tailwind é uma ferramenta fantástica para construir aplicações web dinâmicas e reativas com React, Vue ou Svelte. Mas em um blog estático de artigos longos, centrado puramente em texto, a abordagem utilitária é um contra-senso desastroso.</p>

<p>O fluxo de escrita em um blog é abrir o editor de texto, criar um arquivo Markdown e redigir a postagem usando a sintaxe clássica e limpa do formato:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gu">## Título da Seção</span>

Este é um parágrafo normal de texto com um <span class="p">[</span><span class="nv">link</span><span class="p">](</span><span class="sx">https://exemplo.com</span><span class="p">)</span>.
<span class="gt">
&gt; Uma citação relevante de algum autor histórico.</span>
</code></pre></div></div>

<p>O processador do Jekyll (o Kramdown) converte esse arquivo diretamente para tags HTML5 puras: <code class="language-plaintext highlighter-rouge">&lt;h2&gt;</code>, <code class="language-plaintext highlighter-rouge">&lt;p&gt;</code>, <code class="language-plaintext highlighter-rouge">&lt;a&gt;</code> e <code class="language-plaintext highlighter-rouge">&lt;blockquote&gt;</code>, sem injetar classes utilitárias no meio do caminho. Para usar Tailwind nesse cenário, você seria obrigado a instalar o plugin <code class="language-plaintext highlighter-rouge">@tailwindcss/typography</code>, aceitar o conjunto opinativo de classes <code class="language-plaintext highlighter-rouge">.prose</code>, configurar um pipeline pesado de compilação em Node.js com PostCSS e vigiar constantemente o tamanho da saída gerada.</p>

<p>Eu não queria mais esteiras complexas de frontend para rodar um blog de texto. Eu queria simplicidade objetiva.</p>

<p>Foi nesse momento que o <strong>Pico CSS v2</strong> <sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> entrou em cena como a resposta que eu procurava.</p>

<p>O Pico pertence aos chamados frameworks CSS semânticos (ou <em>classless</em>). A proposta central dele é de uma elegância desconcertante: em vez de exigir que você decore centenas de classes utilitárias proprietárias para aplicar no seu HTML, o Pico estiliza diretamente os elementos semânticos nativos do próprio HTML5.</p>

<p>Olhe a comparação estrutural entre os dois paradigmas:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─────────────────────────────────────────────────────────────┐
│                    O PARADIGMA UTILITÁRIO                   │
│                                                             │
│   Markdown  ──&gt;  Kramdown  ──&gt;  HTML Cru  ──&gt;  ???          │
│                                                 │           │
│   (Exige plugins, wrappers e injeção de classes utilitárias │
│    como: class="text-xl font-bold mb-4 text-slate-800")     │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                    O PARADIGMA SEMÂNTICO                    │
│                                                             │
│   Markdown  ──&gt;  Kramdown  ──&gt;  HTML5 Nativo  ──&gt;  Navegador│
│                                       │                     │
│                &lt;article&gt;, &lt;header&gt;, &lt;h2&gt;, &lt;blockquote&gt;      │
│                                       │                     │
│             Pico CSS v2 aplica tipografia, contraste        │
│             e ritmo vertical diretamente nas tags!          │
└─────────────────────────────────────────────────────────────┘
</code></pre></div></div>
<p><em>Diferença conceitual: a elegância da renderização semântica direta sem intermediários.</em></p>

<p>Se você escreve uma tabela em Markdown, o Kramdown cospe a tag <code class="language-plaintext highlighter-rouge">&lt;table&gt;</code>, e o Pico CSS intercepta essa tag nativa aplicando instantaneamente bordas sutis, tipografia alinhada, espaçamento interno proporcional e rolagem horizontal em telas estreitas, sem exigir uma única classe auxiliar. A mesma dinâmica acontece com caixas retráteis de <code class="language-plaintext highlighter-rouge">&lt;details&gt;</code>, formulários, cabeçalhos e citações.</p>

<p>O Markdown continua puro, o HTML gerado permanece limpo e o navegador recebe exatamente o que precisa.</p>

<p>Além disso, a versão 2 do Pico CSS foi reescrita inteiramente em SCSS modular, trazendo duas vantagens fundamentais que selaram a minha decisão: todas as escolhas estéticas são expostas por variáveis nativas de CSS com o prefixo <code class="language-plaintext highlighter-rouge">--pico-*</code>, e a alternância entre temas claro e escuro é controlada diretamente pelo atributo <code class="language-plaintext highlighter-rouge">data-theme</code> na tag raiz <code class="language-plaintext highlighter-rouge">&lt;html&gt;</code>.</p>

<p>O casamento foi perfeito. Conquistamos um design system limpo, responsivo e adaptável, com uma folha de estilos que pesa menos de 15 KB compactada e que respeita rigorosamente a semântica da web.</p>

<h2 id="a-alquimia-das-cores-construindo-a-paleta-própria-classic-vs-dark">A alquimia das cores: construindo a paleta própria (Classic vs Dark)</h2>

<p>Com a fundação semântica estabelecida, chegou o momento mais saboroso e arriscado de todo o projeto: escolher as cores do blog. Eu tinha um objetivo muito claro e intransigente em mente: fugir a todo custo dos dois erros capitais que dominam a maioria dos temas modernos na internet.</p>

<p>O primeiro erro é o Dark Mode amador, com fundo preto absoluto (<span class="color-swatch" style="background:#000000;"></span><code class="language-plaintext highlighter-rouge">#000000</code>) e tipografia em branco puro (<span class="color-swatch" style="background:#ffffff;"></span><code class="language-plaintext highlighter-rouge">#ffffff</code>). Esse contraste artificial extremo de 21:1 agride os olhos do leitor, causa fadiga visual rápida em ambientes escuros e deixa aquele efeito fantasma incômodo de pós-imagem na retina quando você desvia o olhar da tela.</p>

<p>O segundo erro é o Light Mode hospitalar: aquele fundo branco puro reflexivo que transforma a tela de um monitor grande de 27 polegadas em uma lâmpada fluorescente ligada a meio metro do seu rosto. Em ambos os casos, a leitura prolongada se torna um exercício de resistência física.</p>

<p>A inspiração para o visual não veio de nenhum tratado acadêmico de teoria das cores. Veio de um hábito muito simples e antigo meu: eu sempre adorei rabiscar e rascunhar ideias no papel com cores fortes e marcantes, do tipo que você encontra nas dezenas de canetas Stabilo ponta fina que acumulei pela mesa ao longo dos anos. Aquele contraste vivo de tinta pigmentada sobre folha de caderno pautado sempre me deu prazer na hora de pensar.</p>

<p>No mundo digital, as minhas referências de conforto visual sempre foram o Monokai Pro <sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> e o Solarized <sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>. Eu queria trazer essa mesma sensação para o blog: fundos que não agridem a vista, superfícies foscas e acentos de destaque que saltam aos olhos como marca-texto bem passado, sem transformar a página em um carnaval de neon.</p>

<p>Ajustei os valores hexadecimais no arquivo <code class="language-plaintext highlighter-rouge">_sass/base/_variables.scss</code> buscando equilíbrio prático.</p>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Confissão de Bastidor</p><p>Sejamos honestos: o meu nível de apego ao Monokai Pro e ao Solarized Dark é quase vício de fábrica. A linha entre "forte inspiração conceitual" e "kibar a paleta inteira com o conta-gotas aberto" é extremamente tênue. Tive que segurar a mão para não copiar os temas na cara dura, calibrando os contrastes para a leitura do blog, mas se você bater o olho e sentir aquele ar familiar de um terminal bem ajeitado de madrugada, já sabe de onde veio.</p>
</div>

<p>Para os destaques primários (links e botões), definimos um ciano fechado (<span class="color-swatch" style="background:#007a8a;"></span><code class="language-plaintext highlighter-rouge">#007a8a</code>) no tema claro para dar contraste firme sobre o fundo claro, e um ciano elétrico (<span class="color-swatch" style="background:#78dce8;"></span><code class="language-plaintext highlighter-rouge">#78dce8</code>) no tema escuro. O restante dos acentos vibrantes segue a lógica das canetas coloridas: vermelho (<span class="color-swatch" style="background:#d62850;"></span><code class="language-plaintext highlighter-rouge">#d62850</code> / <span class="color-swatch" style="background:#ff6188;"></span><code class="language-plaintext highlighter-rouge">#ff6188</code>) para callouts de perigo e erros críticos, amarelo (<span class="color-swatch" style="background:#9c6f00;"></span><code class="language-plaintext highlighter-rouge">#9c6f00</code> / <span class="color-swatch" style="background:#ffd866;"></span><code class="language-plaintext highlighter-rouge">#ffd866</code>) para avisos e pontos de atenção, e verde (<span class="color-swatch" style="background:#46851b;"></span><code class="language-plaintext highlighter-rouge">#46851b</code> / <span class="color-swatch" style="background:#a9dc76;"></span><code class="language-plaintext highlighter-rouge">#a9dc76</code>) para confirmações e dicas práticas.</p>

<p>A tabela abaixo resume os pilares dessa arquitetura de cores em ambos os modos:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Papel Semântico</th>
      <th style="text-align: left">Variável SCSS</th>
      <th style="text-align: left">Tema Claro (<code class="language-plaintext highlighter-rouge">light</code>)</th>
      <th style="text-align: left">Tema Escuro (<code class="language-plaintext highlighter-rouge">dark</code>)</th>
      <th style="text-align: left">Finalidade de Uso</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>Destaque Primário</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">--pico-primary</code></td>
      <td style="text-align: left"><span class="color-swatch" style="background:#007a8a;"></span><code class="language-plaintext highlighter-rouge">#007a8a</code></td>
      <td style="text-align: left"><span class="color-swatch" style="background:#78dce8;"></span><code class="language-plaintext highlighter-rouge">#78dce8</code></td>
      <td style="text-align: left">Links, botões ativos e âncoras</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Acento Alerta</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">--accent-red</code></td>
      <td style="text-align: left"><span class="color-swatch" style="background:#d62850;"></span><code class="language-plaintext highlighter-rouge">#d62850</code></td>
      <td style="text-align: left"><span class="color-swatch" style="background:#ff6188;"></span><code class="language-plaintext highlighter-rouge">#ff6188</code></td>
      <td style="text-align: left">Callouts de perigo e erros</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Acento Destaque</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">--accent-yellow</code></td>
      <td style="text-align: left"><span class="color-swatch" style="background:#9c6f00;"></span><code class="language-plaintext highlighter-rouge">#9c6f00</code></td>
      <td style="text-align: left"><span class="color-swatch" style="background:#ffd866;"></span><code class="language-plaintext highlighter-rouge">#ffd866</code></td>
      <td style="text-align: left">Avisos e pontos de atenção</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Acento Sucesso</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">--accent-green</code></td>
      <td style="text-align: left"><span class="color-swatch" style="background:#46851b;"></span><code class="language-plaintext highlighter-rouge">#46851b</code></td>
      <td style="text-align: left"><span class="color-swatch" style="background:#a9dc76;"></span><code class="language-plaintext highlighter-rouge">#a9dc76</code></td>
      <td style="text-align: left">Confirmações e dicas práticas</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Bordas Estruturais</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">$theme-*-border</code></td>
      <td style="text-align: left"><span class="color-swatch" style="background:#dee2e6;"></span><code class="language-plaintext highlighter-rouge">#dee2e6</code></td>
      <td style="text-align: left"><span class="color-swatch" style="background:#403e41;"></span><code class="language-plaintext highlighter-rouge">#403e41</code></td>
      <td style="text-align: left">Divisores e contornos de cartões</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Texto Atenuado</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">$theme-*-muted</code></td>
      <td style="text-align: left"><span class="color-swatch" style="background:#6c757d;"></span><code class="language-plaintext highlighter-rouge">#6c757d</code></td>
      <td style="text-align: left"><span class="color-swatch" style="background:#939293;"></span><code class="language-plaintext highlighter-rouge">#939293</code></td>
      <td style="text-align: left">Metadados, datas e legendas</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Tipografia Principal</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">$theme-*-fg</code></td>
      <td style="text-align: left"><span class="color-swatch" style="background:#212529;"></span><code class="language-plaintext highlighter-rouge">#212529</code></td>
      <td style="text-align: left"><span class="color-swatch" style="background:#fcfcfa;"></span><code class="language-plaintext highlighter-rouge">#fcfcfa</code></td>
      <td style="text-align: left">Corpo do texto e parágrafos</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Blocos de Código</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">$theme-*-code</code></td>
      <td style="text-align: left"><span class="color-swatch" style="background:#f1f3f5;"></span><code class="language-plaintext highlighter-rouge">#f1f3f5</code></td>
      <td style="text-align: left"><span class="color-swatch" style="background:#221f22;"></span><code class="language-plaintext highlighter-rouge">#221f22</code></td>
      <td style="text-align: left">Fundo do realce de sintaxe</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Painéis / Cards</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">$theme-*-panel</code></td>
      <td style="text-align: left"><span class="color-swatch" style="background:#f8f9fa;"></span><code class="language-plaintext highlighter-rouge">#f8f9fa</code></td>
      <td style="text-align: left"><span class="color-swatch" style="background:#363337;"></span><code class="language-plaintext highlighter-rouge">#363337</code></td>
      <td style="text-align: left">Fundo de caixas e resumos</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Superfície Base</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">$theme-*-bg</code></td>
      <td style="text-align: left"><span class="color-swatch" style="background:#ffffff;"></span><code class="language-plaintext highlighter-rouge">#ffffff</code></td>
      <td style="text-align: left"><span class="color-swatch" style="background:#2d2a2e;"></span><code class="language-plaintext highlighter-rouge">#2d2a2e</code></td>
      <td style="text-align: left">Fundo principal da página</td>
    </tr>
  </tbody>
</table>

<p>Para sustentar esses acentos coloridos, a fundação neutra cuida do conforto óptico contínuo durante a leitura longa.</p>

<p>No <strong>Tema Escuro (<code class="language-plaintext highlighter-rouge">dark</code>)</strong>, a base usa um fundo escuro suave em <span class="color-swatch" style="background:#2d2a2e;"></span><code class="language-plaintext highlighter-rouge">#2d2a2e</code> que não tenta fazer uma cirurgia de catarata nos olhos do leitor, com cards e painéis em <span class="color-swatch" style="background:#363337;"></span><code class="language-plaintext highlighter-rouge">#363337</code> e blocos de código em <span class="color-swatch" style="background:#221f22;"></span><code class="language-plaintext highlighter-rouge">#221f22</code>. O texto principal fica em <span class="color-swatch" style="background:#fcfcfa;"></span><code class="language-plaintext highlighter-rouge">#fcfcfa</code>, enquanto bordas e textos secundários usam cinzas neutros para não poluir a leitura.</p>

<p>No <strong>Tema Claro (<code class="language-plaintext highlighter-rouge">light</code>)</strong>, fomos no clássico limpo: fundo branco <span class="color-swatch" style="background:#ffffff;"></span><code class="language-plaintext highlighter-rouge">#ffffff</code>, cartões em <span class="color-swatch" style="background:#f8f9fa;"></span><code class="language-plaintext highlighter-rouge">#f8f9fa</code> e código em <span class="color-swatch" style="background:#f1f3f5;"></span><code class="language-plaintext highlighter-rouge">#f1f3f5</code>. O texto principal usa o grafite <span class="color-swatch" style="background:#212529;"></span><code class="language-plaintext highlighter-rouge">#212529</code>, garantindo leitura nítida e sem reflexos chatos de luz.</p>

<div class="markdown-alert markdown-alert-tip"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-light-bulb" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"></path></svg> Por que está tudo em inglês nas variáveis?</p><p>Você deve estar se perguntando por que o blog é em português, mas os nomes das variáveis SCSS e tokens estão todos em inglês (<code class="language-plaintext highlighter-rouge">panel</code>, <code class="language-plaintext highlighter-rouge">code</code>, <code class="language-plaintext highlighter-rouge">muted</code>, <code class="language-plaintext highlighter-rouge">border</code>, <code class="language-plaintext highlighter-rouge">cyan</code>). A resposta é simples: misturar o inglês nativo do Pico CSS (<code class="language-plaintext highlighter-rouge">--pico-background-color</code>, <code class="language-plaintext highlighter-rouge">--pico-primary</code>) com termos soltos em português tipo <code class="language-plaintext highlighter-rouge">$tema-fundo-cartao</code> ou <code class="language-plaintext highlighter-rouge">$cor-destaque</code> cria um Franken-código bizarro e doloroso de ler. No código, consistência estética é paz de espírito.</p>
</div>

<p>Essas variáveis alimentam os seletores nativos do Pico CSS diretamente no SCSS, unificando links, botões e âncoras sob a mesma identidade.</p>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* Mapeamento semântico no _sass/base/_variables.scss */</span>
<span class="nd">:root:not</span><span class="o">([</span><span class="nt">data-theme</span><span class="o">=</span><span class="s2">"dark"</span><span class="o">]),</span>
<span class="o">[</span><span class="nt">data-theme</span><span class="o">=</span><span class="s2">"light"</span><span class="o">]</span> <span class="p">{</span>
  <span class="na">color-scheme</span><span class="p">:</span> <span class="n">light</span><span class="p">;</span>
  <span class="na">--pico-background-color</span><span class="p">:</span> <span class="si">#{</span><span class="nv">$theme-light-bg</span><span class="si">}</span><span class="p">;</span>
  <span class="na">--pico-color</span><span class="p">:</span> <span class="si">#{</span><span class="nv">$theme-light-fg</span><span class="si">}</span><span class="p">;</span>
  <span class="na">--pico-card-background-color</span><span class="p">:</span> <span class="si">#{</span><span class="nv">$theme-light-panel</span><span class="si">}</span><span class="p">;</span>
  <span class="na">--pico-code-background-color</span><span class="p">:</span> <span class="si">#{</span><span class="nv">$theme-light-code</span><span class="si">}</span><span class="p">;</span>
  <span class="na">--pico-primary</span><span class="p">:</span> <span class="si">#{</span><span class="nv">$theme-light-cyan</span><span class="si">}</span><span class="p">;</span>
<span class="p">}</span>

<span class="o">[</span><span class="nt">data-theme</span><span class="o">=</span><span class="s2">"dark"</span><span class="o">]</span> <span class="p">{</span>
  <span class="na">color-scheme</span><span class="p">:</span> <span class="n">dark</span><span class="p">;</span>
  <span class="na">--pico-background-color</span><span class="p">:</span> <span class="si">#{</span><span class="nv">$theme-dark-bg</span><span class="si">}</span><span class="p">;</span>
  <span class="na">--pico-color</span><span class="p">:</span> <span class="si">#{</span><span class="nv">$theme-dark-fg</span><span class="si">}</span><span class="p">;</span>
  <span class="na">--pico-card-background-color</span><span class="p">:</span> <span class="si">#{</span><span class="nv">$theme-dark-panel</span><span class="si">}</span><span class="p">;</span>
  <span class="na">--pico-code-background-color</span><span class="p">:</span> <span class="si">#{</span><span class="nv">$theme-dark-code</span><span class="si">}</span><span class="p">;</span>
  <span class="na">--pico-primary</span><span class="p">:</span> <span class="si">#{</span><span class="nv">$theme-cyan</span><span class="si">}</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p><em>Vinculação elegante: o SCSS alimenta as variáveis CSS nativas que o Pico consome.</em></p>

<p>Com a paleta pronta, restava resolver um detalhe técnico clássico: alternar entre os temas sem causar o temido FOUC (<em>Flash of Unstyled Content</em>).</p>

<p>Sabe quando você entra em um site à noite com o modo escuro ativado no sistema operacional, mas durante meio segundo a página dá aquele clarão branco cegante antes de carregar o estilo correto? Isso acontece quando o script de chaveamento é posicionado no final do arquivo HTML ou dentro de um evento assíncrono <code class="language-plaintext highlighter-rouge">DOMContentLoaded</code>.</p>

<p>A nossa solução foi um script síncrono enxuto de vinte linhas, posicionado diretamente dentro da tag <code class="language-plaintext highlighter-rouge">&lt;head&gt;</code> no template <code class="language-plaintext highlighter-rouge">_includes/head.html</code>:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script&gt;</span>
  <span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">stored</span> <span class="o">=</span> <span class="nx">localStorage</span><span class="p">.</span><span class="nf">getItem</span><span class="p">(</span><span class="dl">'</span><span class="s1">theme</span><span class="dl">'</span><span class="p">);</span>
    <span class="kd">var</span> <span class="nx">theme</span> <span class="o">=</span> <span class="nx">stored</span> <span class="o">||</span> <span class="p">(</span><span class="nb">window</span><span class="p">.</span><span class="nf">matchMedia</span><span class="p">(</span><span class="dl">'</span><span class="s1">(prefers-color-scheme: dark)</span><span class="dl">'</span><span class="p">).</span><span class="nx">matches</span> <span class="p">?</span> <span class="dl">'</span><span class="s1">dark</span><span class="dl">'</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">light</span><span class="dl">'</span><span class="p">);</span>
    <span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">.</span><span class="nf">setAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">data-theme</span><span class="dl">'</span><span class="p">,</span> <span class="nx">theme</span><span class="p">);</span>
  <span class="p">})();</span>
<span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>
<p><em>Script síncrono anti-clarão: executa antes mesmo do primeiro pixel ser desenhado na tela.</em></p>

<p>O navegador interpreta esse bloco antes de começar a desenhar o corpo da página. Se o leitor já escolheu um tema anteriormente clicando no botão do cabeçalho, a preferência gravada no <code class="language-plaintext highlighter-rouge">localStorage</code> é respeitada de imediato; se for a primeira visita, o script consulta a preferência do sistema via <code class="language-plaintext highlighter-rouge">prefers-color-scheme</code>.</p>

<p>O resultado é uma transição suave: zero piscadas na tela e zero surpresas desagradáveis no meio da noite.</p>

<h2 id="a-faxina-dos-ativos-a-paranóia-saudável-do-100-offline-first">A faxina dos ativos: a paranóia saudável do 100% offline-first</h2>

<p>Se tem uma coisa que me incomoda profundamente na web contemporânea é a dependência cega de CDNs externas para carregar qualquer elemento básico. Você inspeciona um blog simples e encontra chamadas para servidores do Google Fonts, links para scripts do Cloudflare, folhas de estilo puxadas do unpkg e ícones baixados do jsDelivr.</p>

<p>Parece prático no primeiro dia de desenvolvimento, mas na vida real representa uma dívida técnica e ética pesada.</p>

<p>Existem três razões muito sólidas para você exterminar chamadas externas do seu site: privacidade, resiliência e autonomia. Cada vez que o navegador de um leitor faz uma requisição para carregar uma fonte nos servidores de uma Big Tech, ele envia no pacote o endereço IP, detalhes do dispositivo e a página exata que está sendo lida. Além disso, se a CDN sofrer instabilidade ou bloqueio de rotas, o seu site inteiro congela esperando o timeout da conexão.</p>

<p>Por fim, eu adoro escrever e revisar textos enquanto estou viajando de avião ou em locais remotos. Um blog que depende da internet para carregar o próprio visual localmente é um produto quebrado por concepção.</p>

<p>Comecei a faxina pelas fontes tipográficas, eliminando as chamadas remotas do Google Fonts e baixando diretamente da fundição de código aberto da Adobe as famílias <code class="language-plaintext highlighter-rouge">Source Serif 4</code> (para o corpo do texto) e <code class="language-plaintext highlighter-rouge">Source Code Pro</code> (para código e terminais). Ao descompactar os pacotes originais, me deparei com a clássica floresta de formatos legados da web: dezenas de arquivos com extensões <code class="language-plaintext highlighter-rouge">.ttf</code>, <code class="language-plaintext highlighter-rouge">.otf</code>, <code class="language-plaintext highlighter-rouge">.eot</code> e <code class="language-plaintext highlighter-rouge">.woff</code>.</p>

<p>Perguntei a mim mesmo se precisávamos de todo esse entulho histórico. A resposta foi um sonoro não.</p>

<p>O formato <code class="language-plaintext highlighter-rouge">.woff2</code> (Web Open Font Format 2.0) utiliza o algoritmo de compressão Brotli nativo e é suportado por praticamente 100% dos navegadores modernos em circulação no mundo há muitos anos. Apaguei todos os formatos antigos da pasta de fontes e deixei exclusivamente os arquivos <code class="language-plaintext highlighter-rouge">.woff2</code> leves, mantendo as declarações limpas em <code class="language-plaintext highlighter-rouge">_sass/base/_typography.scss</code>:</p>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* Declaração enxuta em _sass/base/_typography.scss */</span>
<span class="k">@font-face</span> <span class="p">{</span>
  <span class="nl">font-family</span><span class="p">:</span> <span class="s1">'Source Serif 4'</span><span class="p">;</span>
  <span class="nl">font-style</span><span class="p">:</span> <span class="nb">normal</span><span class="p">;</span>
  <span class="nl">font-weight</span><span class="p">:</span> <span class="m">400</span><span class="p">;</span>
  <span class="nl">font-display</span><span class="p">:</span> <span class="nb">swap</span><span class="p">;</span>
  <span class="nl">src</span><span class="p">:</span> <span class="sx">url('/assets/fonts/source-serif-4-regular.woff2')</span> <span class="nf">format</span><span class="p">(</span><span class="s1">'woff2'</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">@font-face</span> <span class="p">{</span>
  <span class="nl">font-family</span><span class="p">:</span> <span class="s1">'Source Code Pro'</span><span class="p">;</span>
  <span class="nl">font-style</span><span class="p">:</span> <span class="nb">normal</span><span class="p">;</span>
  <span class="nl">font-weight</span><span class="p">:</span> <span class="m">400</span><span class="p">;</span>
  <span class="nl">font-display</span><span class="p">:</span> <span class="nb">swap</span><span class="p">;</span>
  <span class="nl">src</span><span class="p">:</span> <span class="sx">url('/assets/fonts/source-code-pro-regular.woff2')</span> <span class="nf">format</span><span class="p">(</span><span class="s1">'woff2'</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p><em>Fontes 100% locais: carregamento ultra-rápido sem depender de servidores alheios.</em></p>

<p>A mesma postura rigorosa foi aplicada aos ícones do FontAwesome: nada de carregar folhas de estilo mastodônticas de meio megabyte com milhares de ícones que o blog jamais usaria. Isolamos apenas os vetores e classes essenciais para as redes sociais e marcadores em <code class="language-plaintext highlighter-rouge">assets/fonts/</code> e <code class="language-plaintext highlighter-rouge">_sass/</code>.</p>

<p>Hoje, se você desligar o cabo de rede ou ativar o modo avião, o blog compila perfeitamente, renderiza com fidelidade absoluta no navegador e não dispara uma única requisição para fora do servidor local.</p>

<p>Autonomia de verdade é isso.</p>

<h2 id="tipografia-e-ritmo-vertical-o-design-para-leitura-imersiva">Tipografia e ritmo vertical: o design para leitura imersiva</h2>

<p>Se o layout do site é o seu esqueleto, a tipografia é a sua respiração. Você pode ter a melhor arquitetura do mundo, mas se a tipografia for apertada, o contraste for deficiente ou a largura das linhas for exagerada, o leitor vai cansar os olhos e fechar a aba em menos de três minutos de leitura.</p>

<p>Para um blog focado em ensaios e artigos aprofundados com milhares de palavras, a legibilidade é uma prioridade inegociável.</p>

<p>A escolha da serifa (<code class="language-plaintext highlighter-rouge">Source Serif 4</code>) para o corpo do texto foi uma decisão consciente contra a tendência contemporânea de colocar fontes sem serifa em tudo quanto é canto. Fontes serifadas clássicas bem desenhadas criam uma linha de base imaginária sutil que guia o olhar com suavidade ao longo dos períodos, reduzindo o esforço cognitivo em leituras longas.</p>

<p>Para acompanhar, travamos o contêiner do post em <code class="language-plaintext highlighter-rouge">760px</code>, garantindo uma média saudável entre 65 e 75 caracteres por linha (a zona áurea do design editorial clássico) com um entrelinhas generoso de <code class="language-plaintext highlighter-rouge">1.75</code>.</p>

<p>A hierarquia de títulos também foi aliviada: no tema antigo, os títulos H1 vinham em negrito ultrapesado (<code class="language-plaintext highlighter-rouge">font-weight: 800</code>), parecendo uma manchete gritada de tabloide sensacionalista. Reduzimos o peso do H1 para <code class="language-plaintext highlighter-rouge">500</code> com um leve ajuste de aproximação de caracteres (<code class="language-plaintext highlighter-rouge">letter-spacing: -0.5px</code>), complementando com o subtítulo em <code class="language-plaintext highlighter-rouge">&lt;p class="post-subtitle"&gt;</code> logo abaixo para contextualizar o tom do artigo sem competir com o cabeçalho.</p>

<p>Outro ajuste detalhado foi a calibração dos blocos de código Rouge em <code class="language-plaintext highlighter-rouge">_sass/_syntax.scss</code>. O realce de sintaxe herdado embutia regras que aplicavam negrito em quase todas as palavras-chave de linguagens como Java, Python e Rust, transformando trechos longos em festivais de manchas escuras.</p>

<p>Nivelamos palavras-chave, variáveis, funções e operadores no peso regular (<code class="language-plaintext highlighter-rouge">font-weight: 400</code>), fazendo o código renderizar de forma plana, limpa e confortável, exatamente como nos editores modernos.</p>

<p>Na página Sobre (<a href="/about/"><code class="language-plaintext highlighter-rouge">about.md</code></a>), preservamos a nossa tradicional marca visual: o caractere em escrita vertical japonesa flutuando na lateral direita da tela. Ajustamos o elemento <code class="language-plaintext highlighter-rouge">.jp-float</code> com opacidade suave para atuar como textura visual sem obstruir o texto corrido, e adicionamos <code class="language-plaintext highlighter-rouge">aria-hidden="true"</code> para garantir acessibilidade e evitar que leitores de tela tentem pronunciar o caractere decorativo.</p>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* O kanji vertical na página Sobre */</span>
<span class="nc">.jp-float</span> <span class="p">{</span>
  <span class="nl">writing-mode</span><span class="p">:</span> <span class="n">vertical-rl</span><span class="p">;</span>
  <span class="nl">text-orientation</span><span class="p">:</span> <span class="n">upright</span><span class="p">;</span>
  <span class="nl">opacity</span><span class="p">:</span> <span class="m">0</span><span class="mi">.15</span><span class="p">;</span>
  <span class="na">user-select</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p><em>Tipografia vertical japonesa com acessibilidade: atributo aria-hidden garante silêncio aos leitores de tela.</em></p>

<p>Por fim, resolvemos o clássico bug do rodapé flutuante (o famoso <em>Sticky Footer</em>).</p>

<p>Em páginas curtas, como a de erro 404 ou em listagens de tags com apenas um post, o rodapé teimava em subir e ficar flutuando no meio da tela como uma faixa perdida. A cura veio em dez linhas de Flexbox moderno em <code class="language-plaintext highlighter-rouge">_sass/layout/_footer.scss</code>:</p>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* A cura definitiva para rodapés flutuantes */</span>
<span class="nt">body</span> <span class="p">{</span>
  <span class="nl">display</span><span class="p">:</span> <span class="nb">flex</span><span class="p">;</span>
  <span class="nl">flex-direction</span><span class="p">:</span> <span class="nb">column</span><span class="p">;</span>
  <span class="nl">min-height</span><span class="p">:</span> <span class="m">100vh</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">main</span><span class="nc">.container</span> <span class="p">{</span>
  <span class="nl">flex</span><span class="p">:</span> <span class="m">1</span> <span class="m">0</span> <span class="nb">auto</span><span class="p">;</span>
<span class="p">}</span>

<span class="nc">.site-footer</span> <span class="p">{</span>
  <span class="nl">margin-top</span><span class="p">:</span> <span class="nb">auto</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p><em>Comportamento de mola: o container principal empurra o rodapé para o piso em qualquer resolução.</em></p>

<p>O <code class="language-plaintext highlighter-rouge">body</code> assume altura mínima de <code class="language-plaintext highlighter-rouge">100vh</code>, o contêiner principal preenche todo o espaço vertical livre com <code class="language-plaintext highlighter-rouge">flex: 1 0 auto</code>, e o rodapé recebe <code class="language-plaintext highlighter-rouge">margin-top: auto</code>. Seja em uma página com vinte parágrafos ou em uma tela com uma única frase de aviso, o rodapé repousa com firmeza no chão da janela, sem uma linha sequer de JavaScript.</p>

<h2 id="a-saga-da-matemática-katex-local-sem-nós-cegos">A saga da matemática: KaTeX local sem nós cegos</h2>

<p>Para quem escreve sobre ciência da computação, criptografia, sistemas distribuídos e física, ter suporte de primeira classe à renderização de equações matemáticas em LaTeX é mandatório. Mas aqui mora uma das maiores armadilhas de performance da web técnica: o MathJax.</p>

<p>Embora venerável e completo, o MathJax é colossal. Ele despeja megabytes de scripts no navegador do leitor, demora preciosos segundos para inicializar e redesenha a árvore DOM várias vezes até conseguir formatar uma fórmula simples. Em conexões móveis, a página inteira engasga.</p>

<p>A alternativa consagrada é o <strong>KaTeX</strong>, mantido pela equipe do Khan Academy, que desenha equações de forma síncrona, leve e sem refazer o layout da página.</p>

<p>Nossa primeira ideia foi tentar a gem <code class="language-plaintext highlighter-rouge">kramdown-math-katex</code>, que prometia interceptar os delimitadores <code class="language-plaintext highlighter-rouge">$$</code> durante a compilação do site e gerar o HTML estático das equações diretamente no servidor. No papel soava perfeito, mas na prática foi um desastre silencioso.</p>

<p>A gem depende de uma ponte de comunicação com o Node.js no sistema operacional via ExecJS, e no meu ambiente essa amarração falhou de forma opaca: o build do Jekyll passava sem acusar erro no terminal, mas as fórmulas apareciam na página como blocos de texto cru com barras invertidas escapadas (<code class="language-plaintext highlighter-rouge">\[ \mathcal{F} ... \]</code>).</p>

<p>Parei de brigar com a compilação no servidor e adotei a abordagem pragmática: o KaTeX oficial rodando no client-side sob demanda.</p>

<p>Criamos um include inteligente no arquivo <code class="language-plaintext highlighter-rouge">_includes/head.html</code> que injeta os scripts e folhas de estilo exclusivamente se o post declarar <code class="language-plaintext highlighter-rouge">math: true</code> no front matter:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{% if page.math %}
  <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"{{ '/assets/katex/katex.min.css' | relative_url }}"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;script </span><span class="na">defer</span> <span class="na">src=</span><span class="s">"{{ '/assets/katex/katex.min.js' | relative_url }}"</span><span class="nt">&gt;&lt;/script&gt;</span>
  <span class="nt">&lt;script </span><span class="na">defer</span> <span class="na">src=</span><span class="s">"{{ '/assets/katex/contrib/auto-render.min.js' | relative_url }}"</span>
          <span class="na">onload=</span><span class="s">"renderMathInElement(document.body);"</span><span class="nt">&gt;&lt;/script&gt;</span>
{% endif %}
</code></pre></div></div>
<p><em>Injeção condicional por demanda: quem lê tutoriais sem fórmulas baixa zero bytes de matemática.</em></p>

<p>Se o artigo que você está lendo é uma crônica ou um tutorial de Linux, a variável <code class="language-plaintext highlighter-rouge">page.math</code> não existe e o Jekyll nem sequer inclui essas tags no HTML compilado; zero scripts adicionais são trafegados.</p>

<p>E na pasta de fontes da biblioteca, aplicamos a mesma faxina: deletamos todos os formatos legados <code class="language-plaintext highlighter-rouge">.ttf</code> e <code class="language-plaintext highlighter-rouge">.woff</code>, deixando estritamente os vinte arquivos <code class="language-plaintext highlighter-rouge">.woff2</code> necessários para símbolos AMS e alfabetos gregos. A pasta de fontes despencou de 1.2 MB para meros 250 KB, garantindo a renderização instantânea de fórmulas matemáticas como esta:</p>

\[\mathcal{F}\{f\}(\omega) = \frac{1}{\sqrt{2\pi}} \int_{-\infty}^{\infty} f(t) e^{-i\omega t} \, dt\]

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Que equação é essa?</p><p>Para quem ficou curioso: essa é a boa e velha <strong>Transformada de Fourier</strong> para fazer mágica na área de processamento de sinais. Eu poderia entrar nos detalhes matemáticos, mas sou uma pessoa que quer evitar a fadiga, e a internet já tem material de sobra sobre isso. E olha que bonito, o KaTeX renderizou como se fosse uma drag queen se maquiando, com direito a brilho, contorno e cílios postiços. Um luxo!</p>
</div>

<p>O resultado é uma matemática com nitidez vetorial absoluta, sem atrasos de carregamento e sem saltos de layout na rolagem da página. Rápido, previsível e leve como deve ser.</p>

<h2 id="sumários-dinâmicos-e-componentes-modulares-o-poder-do-kramdown-nativo">Sumários dinâmicos e componentes modulares: o poder do Kramdown nativo</h2>

<p>Quem escreve artigos extensos sabe que um sumário funcional no início do texto não é um luxo decorativo, mas uma ferramenta essencial de navegação para o leitor. No meu artigo sobre a <a href="/posts/historia-internet/">História da Internet</a>, por exemplo, o texto é tão longo que navegar sem um índice de seções é como viajar em uma rodovia sem placas.</p>

<p>Durante muito tempo eu mantinha uma lista manual de tópicos no começo dos artigos, o que invariavelmente gerava links quebrados toda vez que eu renomeava um subtítulo.</p>

<p>Quando pesquisei como automatizar a geração do índice de seções (TOC - <em>Table of Contents</em>), a recomendação habitual da comunidade foi instalar o plugin <code class="language-plaintext highlighter-rouge">jekyll-toc</code>. Em vez de adicionar mais uma dependência ao <code class="language-plaintext highlighter-rouge">Gemfile</code>, fui consultar a documentação do Kramdown <sup id="fnref:4"><a href="#fn:4" class="footnote" rel="footnote" role="doc-noteref">4</a></sup>, o conversor padrão de Markdown do Jekyll.</p>

<p>Descobri que o Kramdown já possui um gerador nativo de sumários integrado desde as suas primeiras versões, dispensando qualquer gem auxiliar. Basta configurar o comportamento dos níveis de captura no arquivo <code class="language-plaintext highlighter-rouge">_config.yml</code> para ignorar o H1 do título principal e capturar estritamente os subtítulos H2 e H3:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Configuração nativa no _config.yml</span>
<span class="na">kramdown</span><span class="pi">:</span>
  <span class="na">input</span><span class="pi">:</span> <span class="s">GFM</span>
  <span class="na">smart_quotes</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">apos"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">apos"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">quot"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">quot"</span><span class="pi">]</span>
  <span class="na">toc_levels</span><span class="pi">:</span> <span class="s">2..3</span>
</code></pre></div></div>
<p><em>Ajuste do Kramdown: o índice ignora o H1 do título e captura estritamente os subtítulos H2 e H3.</em></p>

<p>No corpo do artigo, envolvemos a diretiva semântica <code class="language-plaintext highlighter-rouge">{:toc}</code> dentro de uma tag nativa <code class="language-plaintext highlighter-rouge">&lt;details class="toc-box" markdown="1" open&gt;</code>. O atributo <code class="language-plaintext highlighter-rouge">markdown="1"</code> é fundamental para que o Kramdown processe o conteúdo interno da tag HTML, varrendo os cabeçalhos do documento e gerando uma lista aninhada com identificadores de âncora funcionais automaticamente.</p>

<p>Para completar, estilizamos a caixa retrátil no arquivo <code class="language-plaintext highlighter-rouge">_sass/components/_toc.scss</code>:</p>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* Estilo semântico da caixa de sumário */</span>
<span class="nc">.toc-box</span> <span class="p">{</span>
  <span class="nl">background-color</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">pico-card-background-color</span><span class="p">);</span>
  <span class="nl">border</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">pico-muted-border-color</span><span class="p">);</span>
  <span class="nl">border-radius</span><span class="p">:</span> <span class="m">6px</span><span class="p">;</span>
  <span class="nl">padding</span><span class="p">:</span> <span class="m">1rem</span> <span class="m">1</span><span class="mi">.25rem</span><span class="p">;</span>
  <span class="nl">margin</span><span class="p">:</span> <span class="m">2rem</span> <span class="m">0</span><span class="p">;</span>

  <span class="nt">summary</span> <span class="p">{</span>
    <span class="nl">cursor</span><span class="p">:</span> <span class="nb">pointer</span><span class="p">;</span>
    <span class="nl">font-size</span><span class="p">:</span> <span class="m">1</span><span class="mi">.05rem</span><span class="p">;</span>
    <span class="nl">color</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">pico-color</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="nt">ul</span> <span class="p">{</span>
    <span class="nl">margin-top</span><span class="p">:</span> <span class="m">0</span><span class="mi">.75rem</span><span class="p">;</span>
    <span class="nl">padding-left</span><span class="p">:</span> <span class="m">1</span><span class="mi">.5rem</span><span class="p">;</span>

    <span class="nt">li</span> <span class="nt">a</span> <span class="p">{</span>
      <span class="nl">color</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">pico-primary</span><span class="p">);</span>
      <span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>

      <span class="k">&amp;</span><span class="nd">:hover</span> <span class="p">{</span>
        <span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">underline</span><span class="p">;</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><em>Sumário dinâmico, elegante e retrátil sem uma linha sequer de JavaScript.</em></p>

<p>A mesma disciplina de modularidade e reaproveitamento de código (DRY - <em>Don't Repeat Yourself</em>) guiou a reconstrução dos demais componentes do blog. O include <code class="language-plaintext highlighter-rouge">_includes/card.html</code> unificou a renderização dos cartões de categorias e trilhas com contadores dinâmicos de posts; o <code class="language-plaintext highlighter-rouge">_includes/post-list.html</code> centralizou a exibição de metadados e paginação entre a Home e as taxonomias; o modal de apoio ao criador (<code class="language-plaintext highlighter-rouge">_includes/pix-modal.html</code>) foi reconstruído com travamento suave de scroll; e os diagramas dinâmicos do <strong>Mermaid.js</strong> foram desacoplados em um componente próprio (<code class="language-plaintext highlighter-rouge">_sass/components/_mermaid.scss</code> e <code class="language-plaintext highlighter-rouge">_includes/mermaid.html</code>), renderizando fluxogramas e diagramas de sequência com adaptação automática aos temas claro e escuro.</p>

<p>Menos código duplicado, mais coerência visual em cada recanto do site.</p>

<h2 id="o-teste-de-fogo-compilando-101-páginas-e-domando-o-htmlproofer">O teste de fogo: compilando 101 páginas e domando o HTMLProofer</h2>

<p>Depois de mexer na fundação inteira do blog, renomear variáveis de ponta a ponta, excluir centenas de linhas de código antigo e refatorar cada template HTML, qualquer desenvolvedor é assaltado pela dúvida natural: o que foi que quebrou sem que eu percebesse?</p>

<p>Em um site pessoal com mais de cem páginas publicadas ao longo de anos, fazer uma checagem visual clicando em cada link pelo navegador é humanamente inviável. Fatalmente você esquecerá um link antigo, deixará uma âncora órfã ou esquecerá de fechar uma tag semântica.</p>

<p>Para auditar o site com rigor de engenharia, convoquei o auditor definitivo: o <strong>HTMLProofer</strong> <sup id="fnref:5"><a href="#fn:5" class="footnote" rel="footnote" role="doc-noteref">5</a></sup>, adicionando a ferramenta ao <code class="language-plaintext highlighter-rouge">Gemfile</code> dentro do grupo de testes. O utilitário compila o site estático completo e realiza uma varredura minuciosa em cada arquivo gerado na pasta <code class="language-plaintext highlighter-rouge">_site</code>, inspecionando links internos, imagens ausentes, scripts inexistentes e marcações malformadas.</p>

<p>Ao disparar o teste pela primeira vez, tomei de cara um erro seco de biblioteca C ausente:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/home/debian/.gem/ruby/3.3.0/gems/ffi-1.17.4/lib/ffi/dynamic_library.rb:94:in `load_library':
Could not open library 'libcurl': libcurl: cannot open shared object file: No such file or directory. (LoadError)
Could not open library 'libcurl.so.4': libcurl.so.4: cannot open shared object file: No such file or directory.
</code></pre></div></div>
<p><em>O susto clássico de ambiente Linux: dependência C dinâmica ausente no host.</em></p>

<p>O HTMLProofer utiliza a gem <code class="language-plaintext highlighter-rouge">typhoeus</code> para disparar verificações HTTP paralelas de alta velocidade, conversando diretamente com a biblioteca nativa <code class="language-plaintext highlighter-rouge">libcurl</code> do sistema via FFI em linguagem C. Em instalações limpas e minimalistas do Debian no WSL, a biblioteca <code class="language-plaintext highlighter-rouge">libcurl4</code> não vem pré-instalada.</p>

<p>Um rápido <code class="language-plaintext highlighter-rouge">sudo apt update &amp;&amp; sudo apt install -y libcurl4</code> resolveu a pendência no host, mas ao reexecutar o comando esbarrei nas mudanças arquiteturais da versão 5.2.2 do auditor.</p>

<p>As antigas flags <code class="language-plaintext highlighter-rouge">--check-html</code> e <code class="language-plaintext highlighter-rouge">--check-images</code> foram descontinuadas porque no v5 essas checagens rodam ativadas por padrão em todas as execuções. Além disso, a versão 5 força a validação estrita de HTTPS em links externos, o que quebrava a auditoria em posts históricos antigos que citam papers acadêmicos e repositórios de universidades que ainda operam sob HTTP tradicional.</p>

<p>Com as opções ajustadas para o nosso cenário, o terminal processou todo o acervo sem travar em servidores acadêmicos externos:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>bundle <span class="nb">exec </span>htmlproofer ./_site <span class="nt">--disable-external</span> <span class="nt">--no-enforce-https</span>
Running 3 checks <span class="o">(</span>Images, Links, Scripts<span class="o">)</span> <span class="k">in</span> <span class="o">[</span><span class="s2">"./_site"</span><span class="o">]</span> on <span class="k">*</span>.html files ...

Checking 287 internal links
Checking internal <span class="nb">link </span>hashes <span class="k">in </span>41 files
Ran on 104 files!

HTML-Proofer finished successfully.
Finished <span class="k">in </span>12.34 seconds
</code></pre></div></div>
<p><em>Certidão de nascimento do novo tema: 104 páginas auditadas e zero inconsistências.</em></p>

<p>Cento e quatro páginas estáticas compiladas, duzentos e oitenta e sete links internos auditados e quarenta e um arquivos com sumários e âncoras validados em profundidade com zero falhas. A nova arquitetura estava oficialmente homologada e pronta para produção.</p>

<h2 id="conclusão-e-o-prazer-do-jardim-digital">Conclusão e o prazer do jardim digital</h2>

<p>Existe uma satisfação indescritível e genuína em abrir o próprio blog no navegador e conhecer o motivo de cada pixel desenhado na tela. Saber exatamente por que cada margem foi posicionada daquela forma, entender por que cada família tipográfica foi escolhida e ter a certeza de que nenhuma requisição secreta está sendo feita pelas costas do leitor para servidores de publicidade ou telemetria é uma forma rara de paz de espírito.</p>

<p>Este blog deixou de ser um inquilino assustado morando de favor em um tema genérico do GitHub Pages. Ele se transformou naquilo que a cultura do software livre convencionou chamar de <em>digital garden</em> <sup id="fnref:6"><a href="#fn:6" class="footnote" rel="footnote" role="doc-noteref">6</a></sup>: um pedaço acolhedor de terra na internet que você cultiva com as suas próprias mãos, com as suas preferências estéticas, cuidando das podas, adubando a tipografia e garantindo que quem passe por aqui tenha uma experiência de leitura confortável, leve e segura.</p>

<p>Há uma importância quase terapêutica em cuidar do seu espaço digital como quem cuida de um jardim (afinal de contas, como já defendi por aqui, não somos todos jardineiros de software <sup id="fnref:7"><a href="#fn:7" class="footnote" rel="footnote" role="doc-noteref">7</a></sup>?).</p>

<p>Se você ficou curioso para ver como todos esses elementos visuais se comportam juntos na prática, convido você a visitar o nosso laboratório vivo no <a href="/showcase/">Showcase de Elementos &amp; Design System</a>. Lá você encontrará a escala tipográfica completa, os cinco tipos de callouts personalizados, as amostras de código em múltiplas linguagens de programação, as matrizes matemáticas do KaTeX e os detalhes expansíveis reunidos em uma única página de testes.</p>

<p>A casa está arrumada. O chassi está calibrado. As ferramentas estão no lugar.</p>

<p>Agora, o único trabalho restante é o mais prazeroso de todos: sentar na cadeira e voltar a escrever.</p>

<h2 id="referências">Referências</h2>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p><strong>Pico CSS v2: Semantic CSS Framework</strong> {<em>Pico CSS Documentation</em>} (<a href="https://picocss.com/">Link</a>) <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p><strong>Monokai Pro: Color scheme &amp; custom UI theme</strong> {<em>Wimer Hazenberg</em>} (<a href="https://monokai.pro/">Link</a>) <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3">
      <p><strong>Solarized: Precision colors for machines and people</strong> {<em>Ethan Schoonover</em>} (<a href="https://ethanschoonover.com/solarized/">Link</a>) <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4">
      <p><strong>kramdown: Fast, pure-Ruby Markdown-to-HTML converter</strong> {<em>Thomas Leitner</em>} (<a href="https://kramdown.gettalong.org/">Link</a>) <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5">
      <p><strong>HTML-Proofer: Test your HTML files to make sure they're accurate</strong> {<em>Garen Torikian</em>} (<a href="https://github.com/gjtorikian/html-proofer">Link</a>) <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:6">
      <p><strong>A Brief History &amp; Ethos of the Digital Garden</strong> {<em>Maggie Appleton</em>} (<a href="https://maggieappleton.com/garden-history">Link</a>) <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:7">
      <p><strong>Você não é um Engenheiro de Software</strong> {<em>Eduardo N. S. R., Ago/2024</em>} (<a href="/posts/jardineiro-software/">Link</a>) <a href="#fnref:7" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>[&quot;Eduardo N. S. R.&quot;]</name></author><category term="Artigos" /><category term="Web" /><category term="CSS" /><category term="Jekyll" /><category term="Minimalismo" /><summary type="html"><![CDATA[Existe uma sensação física de incômodo que todo desenvolvedor com mania de artesão conhece bem: olhar para a própria criação e sentir que ela não tem a sua cara. Durante anos, este blog operou sob a sombra acolhedora e cinzenta do Minima, o tema padrão que o Jekyll entrega quando você cria um repositório no GitHub Pages. Ele funciona. Compila sem chiar. Carrega rápido e não quebra.]]></summary></entry><entry><title type="html">O teorema da caneta BIC</title><link href="https://vndmtrx.github.io/posts/o-teorema-da-caneta-bic/" rel="alternate" type="text/html" title="O teorema da caneta BIC" /><published>2026-09-11T16:20:00+00:00</published><updated>2026-09-11T16:20:00+00:00</updated><id>https://vndmtrx.github.io/posts/o-teorema-da-caneta-bic</id><content type="html" xml:base="https://vndmtrx.github.io/posts/o-teorema-da-caneta-bic/"><![CDATA[<p>Existe uma regra não escrita sobre a sobrevivência em repartição pública: nunca abra o grupo de WhatsApp do trabalho antes da segunda xícara de café. Se você cometer essa imprudência com o estômago vazio, corre o risco sério de descobrir que o seu colega de sala encontrou uma justificativa no livro de Samuel para passar pano em escândalo financeiro antes mesmo das nove da manhã.</p>

<p>A manhã começou na lentidão habitual de uma instituição federal.</p>

<p>Entre conferir se os sistemas corporativos decidiram funcionar, tentar bater o ponto eletrônico sem receber uma mensagem críptica de erro e descobrir quem foi a alma sebosa que deixou o bule de café com um dedo de borra fria, o expediente tentava se equilibrar na sua rotina anestesiada. Era uma sexta-feira típica.</p>

<p>Até que alguém compartilhou no grupo uma reportagem sobre operações policiais investigando fraudes bilionárias, desvios em fundos de pensão de aposentados e a promiscuidade explícita entre figurões de terno, banqueiros e a bancada mais reacionária do Congresso.</p>

<p>Um assunto indigesto, mas corriqueiro.</p>

<p>O tipo de notícia que a maioria lê em silêncio, balança a cabeça com o cinismo resignado de quem já viu esse mesmo enredo se repetir cinquenta vezes e volta para as suas planilhas. Mas o grupo da repartição não é composto apenas por almas resignadas. Há sempre aquele indivíduo que enxerga em cada notícia desconfortável uma afronta pessoal à sua visão de mundo.</p>

<p>Foi aí que Josafá decidiu entrar em cena.</p>

<div class="markdown-alert markdown-alert-warning"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-alert" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg> Admissão do próprio veneno</p><p>Apelar para esse <em>argumentum ad verecundiam</em> é uma covardia retórica? É, com certeza. Mas se é pra fazer exposed, que comece pelo meu: não tem ninguém pleno ou iluminado aqui não. Eu sou mesquinho, rancoroso e desci do salto com gosto. A paciência foi com Jezuis antes das nove da manhã, o ranço bateu no teto e resolvi canalizar a minha <em>"vilã de novela"</em> interior, porque eu sou ser humano e vocês não têm nada a ver com isso.</p>
</div>

<p>Para não dar nome ao coleguinha, resolvi batizá-lo de Josafá simplesmente porque sim. Tem aquela sonoridade bíblica empoeirada do Antigo Testamento, carrega um peso de cartório do século retrasado e casa sob medida com a solenidade arcaica com que ele tenta justificar o injustificável.</p>

<p>A figura é uma entidade sociológica fascinante.</p>

<p>Ele jura de pés juntos que não tem partido, que abomina a política e que suas opiniões brotam de uma razão límpida e desinteressada. Mas basta qualquer manchete arranhar a lataria da extrema-direita que o alarme interno do homem dispara como sirene de quartel.</p>

<p>Em menos de três minutos, ele já estava na arena digital: dedos frenéticos no teclado, pontuação errática, caixa alta para simular autoridade moral e aquele tom condescendente de quem se julga o último adulto sóbrio numa creche de funcionários públicos iludidos.</p>

<details class="toc-box">
  <summary><strong>Sumário &amp; Índice de Seções (TOC)</strong></summary>
<ul id="markdown-toc">
  <li><a href="#a-teologia-freestyle-de-josafá-e-o-aviso-ignorado-de-samuel" id="markdown-toc-a-teologia-freestyle-de-josafá-e-o-aviso-ignorado-de-samuel">A teologia freestyle de Josafá e o aviso ignorado de Samuel</a></li>
  <li><a href="#entra-feuerbach-sai-bela-gil" id="markdown-toc-entra-feuerbach-sai-bela-gil">Entra Feuerbach, sai Bela Gil</a></li>
  <li><a href="#o-teorema-da-caneta-bic-e-os-fariseus-do-mosquito" id="markdown-toc-o-teorema-da-caneta-bic-e-os-fariseus-do-mosquito">O teorema da caneta BIC e os fariseus do mosquito</a></li>
  <li><a href="#a-servidão-voluntária-e-a-síndrome-de-estocolmo-funcional" id="markdown-toc-a-servidão-voluntária-e-a-síndrome-de-estocolmo-funcional">A servidão voluntária e a Síndrome de Estocolmo funcional</a></li>
  <li><a href="#o-simulacro-de-independência-e-o-abismo-cognitivo" id="markdown-toc-o-simulacro-de-independência-e-o-abismo-cognitivo">O simulacro de independência e o abismo cognitivo</a></li>
  <li><a href="#o-canário-na-mina-de-carvão" id="markdown-toc-o-canário-na-mina-de-carvão">O canário na mina de carvão</a></li>
  <li><a href="#referências" id="markdown-toc-referências">Referências</a></li>
</ul>

</details>

<h2 id="a-teologia-freestyle-de-josafá-e-o-aviso-ignorado-de-samuel">A teologia freestyle de Josafá e o aviso ignorado de Samuel</h2>

<p>O primeiro número circense do colega foi nos brindar com um tratado improvisado sobre regimes políticos no Antigo Testamento.</p>

<p>Diante da crítica de colegas sobre a escalada teocrática que sequestra o debate público no país, em que clérigos milionários ditam votações orçamentárias e impõem agendas medievais, o homem não vacilou. Soltou no grupo que governos teocráticos, na verdade, não existem.</p>

<p>A tese era de uma audácia estarrecedora: para que houvesse uma teocracia, o próprio Deus precisaria estar sentado na cadeira presidencial assinando atos no Diário Oficial.</p>

<p>Como isso só teria ocorrido com o povo hebreu no deserto até o profeta Samuel ungir o Rei Saul há mais de três mil anos, a conclusão dele foi direta: qualquer líder religioso ocupando ministérios, operando emendas secretas ou usando púlpito de igreja como comício partidário é apenas um cidadão exercendo sua legítima vocação civil.</p>

<p>O delírio semântico foi tamanho que uma colega do setor não aguentou e jogou no chat a captura de tela de uma busca no Google: o significado literal de teocracia segundo o dicionário, com o texto grifado para não deixar margem a dúvidas.</p>

<p>O verbete explicava com a paciência didática que faltava ao expediente: sistema de governo em que o poder político é exercido por líderes religiosos, unindo a autoridade espiritual à do Estado. E trazia logo abaixo a constatação óbvia que desmontava a tese do iluminado em duas linhas: <em>na prática, como uma divindade não governa diretamente, o poder fica nas mãos de sacerdotes, clérigos ou líderes que se dizem representantes de Deus na Terra</em>.</p>

<p>Bastou uma consulta elementar de três segundos para implodir o castelo de cartas.</p>

<p>Mas para quem opera na trincheira do alinhamento cego, nem o dicionário tem autoridade. O sujeito simplesmente ignorou a definição formal da língua portuguesa. Pouco importava se colegas lembravam que o operador financeiro do esquema sob investigação era frequentador assíduo dos mesmos templos reluzentes que vendem prosperidade aos fiéis e sangram fundos de pensão de velhinhos; para o nosso profeta de repartição, admitir o fato significaria admitir a hipocrisia da própria bancada de estimação.</p>

<p>Diante de formulações desse calibre, é impossível não lembrar de Harry Frankfurt e de seu clássico ensaio sobre a natureza do <em>bullshit</em> <sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>. Frankfurt estabelece uma distinção preciosa entre a mentira deliberada e a conversa fiada institucionalizada.</p>

<p>O mentiroso conhece a verdade e se esforça para escondê-la; ele respeita, pelo menos no plano conceitual, a autoridade dos fatos. O propagador de <em>bullshit</em>, por outro lado, opera num vácuo de total indiferença factual. Ele não está preocupado se sua afirmação é verdadeira ou falsa: sua única meta é produzir um ruído sonoro conveniente que o tire da defensiva e proteja sua imagem perante o público.</p>

<p>Essa ginástica hermenêutica é o suprassumo do <em>bullshit</em>. Ele inventou uma definição absurda de teocracia para neutralizar o perigo visível de um Estado confessional paralelo.</p>

<p>Mas a ironia maior foi o coleguinha ter evocado o livro de Samuel confiando que ninguém no grupo se daria ao trabalho de abrir o texto bíblico.</p>

<p>Se tivesse lido o oitavo capítulo do primeiro livro de Samuel, teria descoberto que a transição para a monarquia de Saul foi descrita nas escrituras como uma desgraça consentida <sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>. Quando os anciãos de Israel imploraram por um monarca para "serem como todas as outras nações", a versão arcaica de clamar por um salvador da pátria autoritário, Deus ordenou que o profeta Samuel advertisse o povo com todas as letras sobre o que um governante forte faria com eles.</p>

<p>Samuel avisou que o rei tomaria os filhos do povo para comandar seus carros de guerra, confiscaria as filhas para serem criadas do palácio, desapropriaria os melhores campos, vinhas e olivais para enriquecer seus asseclas e cobraria um dízimo sobre as sementes para sustentar a corte dos seus oficiais.</p>

<p>O texto bíblico é a dissecação mais violenta da natureza predatória de um autocrata na literatura antiga.</p>

<p>Ele citou o Rei Saul como álibi teológico sem perceber que a Bíblia estava descrevendo com séculos de antecedência o perfil exato do projeto político que venera: a pilhagem do Estado em benefício de uma oligarquia armada.</p>

<h2 id="entra-feuerbach-sai-bela-gil">Entra Feuerbach, sai Bela Gil</h2>

<p>Quando encurralado pelas próprias contradições, o roteiro do mártir de repartição nunca falha: ele veste o manto do injustiçado, acusa os colegas de intolerância e solta no grupo que os outros é que estão de "mimimi" e que ninguém ali sabe dialogar.</p>

<p>É a clássica inversão de papéis do debatedor de aplicativo: passa a manhã inteira relativizando roubo de aposentadorias, mas na primeira contestação firme se faz de vítima da agressividade alheia.</p>

<p>A minha reação imediata não foi de indignação, foi de puro, cristalino e destilado deboche.</p>

<p>A vontade real era soltar uma daquelas frases que encerram qualquer diálogo, do tipo perguntar se o sujeito estava digitando com os cascos ou se alguém precisava esconder o vermífugo do rebanho. Mas eu tenho desconfiômetro, amor à minha paz de espírito e apego suficiente ao meu contracheque para não dar munição de graça a quem adora bancar o perseguido. O deboche precisava caber dentro de uma margem segura.</p>

<p>A saída foi recorrer à mais refinada diplomacia visual da internet: soltei uma figurinha da cantora Sabrina Carpenter com a mão no fone de ouvido e a legenda direta que dispensa qualquer contestação: <em>se for chorar, manda áudio</em>.</p>

<p>Há momentos na vida corporativa em que nenhuma trégua formal é capaz de traduzir a preguiça cósmica que uma lamúria dessas provoca. A figurinha operou o milagre da elegância: desarmou o coitadismo do sujeito sem deixar rastro para melindres burocráticos.</p>

<p>Mas a provocação teológica continuava no ar.</p>

<p>Assistir à fé ser domesticada e torcida como pano de chão para blindar esquema de colarinho branco em plena sexta-feira de manhã é uma provação física. O cinismo era tamanho que precisei quebrar de vez a quarta parede do expediente e soltar no chat a máxima com a qual Ludwig Feuerbach implodiu a teologia conservadora no século XIX: não foi Deus quem criou o homem à sua imagem e semelhança; foi o homem quem esculpiu Deus à sua própria imagem, projetando na divindade suas próprias fraquezas, seus preconceitos, seus interesses econômicos e seu pavor da liberdade <sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>.</p>

<p>Ou, parafraseando a apresentadora Bela Gil naquele clássico meme sobre substituições cotidianas: você pode substituir a palavra "Deus" por "Ninguém" em praticamente qualquer frase que o sentido continua rigorosamente intacto.</p>

<p>Quando o colega invoca a Bíblia para justificar a promiscuidade entre religião e corrupção no poder, a mecânica é rigorosamente essa. Ele não está prestando contas a uma entidade transcendental. Está conversando com o próprio eco e reverenciando suas próprias obsessões ideológicas.</p>

<p>O deus dessa turma é um burocrata reacionário que odeia direitos sociais, abençoa o enriquecimento de pastores inescrupulosos e se preocupa muito mais com a moral alheia do que com a justiça distributiva.</p>

<p>A hipocrisia ganha contornos trágicos quando confrontada com a tradição profética das próprias escrituras judaico-cristãs.</p>

<p>O profeta Amós registrou a aversão divina contra quem usa a liturgia como biombo para esconder a podridão social: <em>"Odeio, desprezo as vossas festas religiosas, e não suporto as vossas assembleias solenes… Afastai de mim o ruído dos vossos cânticos; corra, porém, a justiça como as águas, e a retidão como um ribeiro perene!"</em> <sup id="fnref:4"><a href="#fn:4" class="footnote" rel="footnote" role="doc-noteref">4</a></sup>.</p>

<p>No mesmo tom, o profeta Isaías pronunciou a sentença definitiva sobre a politicagem que assalta os desprotegidos: <em>"Ai daqueles que fazem leis injustas e decretos de opressão para privar os pobres dos seus direitos e negar justiça aos aflitos do meu povo, fazendo das viúvas a sua presa e roubando os órfãos!"</em> <sup id="fnref:5"><a href="#fn:5" class="footnote" rel="footnote" role="doc-noteref">5</a></sup>.</p>

<p>Roubar fundo de previdência de velhinho aposentado e esmagar a renda de quem trabalhou a vida inteira é precisamente o que Isaías condenou sob o fogo profético.</p>

<p>Não há um milímetro de consonância entre o texto bíblico e o projeto de desmonte social que seduz o rebanho. Mas o homem prefere ignorar Isaías e Amós. Encarar os profetas exigiria encarar a própria cumplicidade.</p>

<h2 id="o-teorema-da-caneta-bic-e-os-fariseus-do-mosquito">O teorema da caneta BIC e os fariseus do mosquito</h2>

<p>Encurralado pelas contradições teológicas e pelos dados factuais sobre o escândalo financeiro do momento, o coleguinha recorreu ao último refúgio de quem perdeu o chão argumentativo: a falsa equivalência moral universal.</p>

<p>Ele digitou no grupo, com o ar compungido de quem distribui sabedoria do alto de uma montanha moral, a seguinte pérola: <em>"só queria saber quem aqui nunca cometeu nenhum ato de corrupção… até levar uma caneta fornecida pela instituição pra casa ou imprimir uma folha pessoal na impressora do setor tá valendo. Dá pra juntar todo mundo no mesmo saco"</em>.</p>

<p>É o que passei a chamar de o Teorema da Caneta BIC.</p>

<p>Saboreie a monstruosidade dessa lógica por alguns instantes.</p>

<p>Na cabeça do cidadão, desviar centenas de milhões de reais de pensões públicas em conluio com banqueiros e esquecer uma caneta esferográfica de cinquenta centavos no fundo da mochila pertencem rigorosamente à mesma gaveta penal e ética. Imprimir uma folha de papel com um comprovante de residência exigido pelo próprio órgão empregador faz de você um criminoso com a mesma gravidade moral de quem lava dinheiro de milícia em contas no exterior.</p>

<p>Jesus de Nazaré, ao confrontar os religiosos hipócritas do seu tempo no Evangelho de Mateus, cravou uma imagem que desmonta essa falácia com quase dois mil anos de antecedência: <em>"Guias cegos! Coais um mosquito e engolis um camelo!"</em> <sup id="fnref:6"><a href="#fn:6" class="footnote" rel="footnote" role="doc-noteref">6</a></sup>.</p>

<p>O colega e seus semelhantes são os coadores de mosquitos do serviço público.</p>

<p>Eles passam o dia fiscalizando se alguém levou o clipe de papel para prender o contracheque, mas engolem, com um sorriso de aprovação e sem engasgar, o camelo inteiro da pilhagem orçamentária patrocinada por seus ídolos políticos.</p>

<p>Peter Sloterdijk, em sua obra monumental sobre a razão cínica, descreve com exatidão esse estado de espírito da modernidade <sup id="fnref:7"><a href="#fn:7" class="footnote" rel="footnote" role="doc-noteref">7</a></sup>. O cinismo contemporâneo não é a ingenuidade do ignorante que não sabe o que faz; é a postura daquele que <em>sabe perfeitamente o que faz e, mesmo assim, continua fazendo</em>.</p>

<p>A falsa equivalência não nasce de um erro honesto de proporção. Ela é um mecanismo psicológico deliberado para dissolver a responsabilidade: se todo mundo é corrupto porque todo mundo já pegou uma caneta emprestada, então ninguém é culpado por nada. A corrupção sistêmica é normalizada, o grande larápio é anistiado no imaginário coletivo e o debate público é nivelado por baixo num pântano de cinismo generalizado.</p>

<p>A sordidez desse argumento se torna ainda mais berrante quando olhamos para a realidade material das instituições públicas brasileiras.</p>

<p>Nenhum trabalhador dessas casas enriquece às custas do Estado.</p>

<p>O fluxo cotidiano é o exato inverso: quem atua na linha de frente do serviço público frequentemente tira dinheiro do próprio contracheque para comprar pó de café, açúcar, régua de tomadas, cabo de rede, fita adesiva e canetas, simplesmente porque os sucessivos cortes orçamentários evaporaram as verbas de custeio mais básicas.</p>

<p>O trabalhador subsidia o funcionamento precário da máquina pública para não deixar a população desassistida. E, ao final da jornada, ainda tem que engolir o colega de setor insinuando que ele é sócio de quadrilha financeira porque levou para casa a caneta que muitas vezes ele mesmo comprou na papelaria da esquina.</p>

<h2 id="a-servidão-voluntária-e-a-síndrome-de-estocolmo-funcional">A servidão voluntária e a Síndrome de Estocolmo funcional</h2>

<p>Chegamos então ao nó górdio da questão: por que tanta gente se comporta assim?</p>

<p>O que leva um servidor público, cuja estabilidade, subsistência e plano de aposentadoria dependem integralmente da higidez do Estado e da força do trabalho coletivo, a aplaudir com tanto fervor os coveiros da sua própria carreira?</p>

<p>Em 1548, com apenas dezoito anos de idade, o jovem francês Étienne de La Boétie formulou a pergunta que ecoa através dos séculos: qual é o vício misterioso que faz com que milhares de indivíduos suportem um tirano que não possui outro poder além daquele que eles mesmos lhe conferem <sup id="fnref:8"><a href="#fn:8" class="footnote" rel="footnote" role="doc-noteref">8</a></sup>?</p>

<p>La Boétie chamou essa aberração de <em>servidão voluntária</em>. Os povos, escreveu ele, deixam-se engolir e oprimir porque se acostumaram a lamber as mãos que os acorrentam. Eles próprios fornecem as armas ao tirano e sustentam o capataz que depois usará o açoite sobre suas costas.</p>

<p>A figura é a encarnação burocrática da servidão voluntária descrita por La Boétie.</p>

<p>Ele é vítima, sim, mas de uma Síndrome de Estocolmo institucional tão arraigada que chega a ser patológica.</p>

<p>Quando o projeto político da extrema-direita sobe ao palanque prometendo congelar salários por quase uma década, extinguir adicionais de carreira, sucatear prédios públicos, terceirizar serviços essenciais e tratar o funcionalismo como um antro de "parasitas", o coração do servidor patriota palpita de emoção.</p>

<p>Ele admira a truculência do capataz. Enxerga no corte de direitos alheios uma demonstração de força, sem perceber que o pescoço que repousa na lâmina da guilhotina é o seu próprio.</p>

<p>O escárnio atinge o ápice na contradição prática do dia a dia.</p>

<p>Essa trupe passou mais de cem dias assistindo seus colegas enfrentarem piquetes de greve debaixo de sol escaldante para arrancar negociações salariais e reestruturar a carreira. Eles nunca foram a uma assembleia, nunca seguraram uma faixa, nunca perderam o sono com a possibilidade de corte de ponto. Ao contrário: murmuravam pelos cantos que a paralisação era baderna de desocupados.</p>

<p>No entanto, quando os acordos foram firmados e os novos direitos foram homologados, quem foram os primeiros a abrir processo eletrônico para solicitar os adicionais de qualificação por competência?</p>

<p>Quem correu para cadastrar plano de trabalho e usufruir do regime de teletrabalho flexível?</p>

<p>Quem olhou o contracheque com o reajuste nos auxílios e comemorou em silêncio a aceleração dos padrões de vencimento?</p>

<p>Eles mesmos.</p>

<p>Eles usufruem avidamente de cada centavo arrancado pela luta coletiva dos trabalhadores, mas votam com orgulho no carrasco que prometeu queimar a CLT e aniquilar o serviço público. Eles querem o socialismo dos benefícios e o anarcocapitalismo do WhatsApp.</p>

<p>Paulo Freire decifrou esse mecanismo psíquico com a precisão dos grandes mestres: quando a formação crítica é amputada, o oprimido não sonha em libertar a si mesmo e aos seus pares; seu sonho secreto é se transformar no opressor <sup id="fnref:9"><a href="#fn:9" class="footnote" rel="footnote" role="doc-noteref">9</a></sup>. O oprimido hospeda a ideologia do dominador dentro da sua própria mente. Ele passa a odiar os seus semelhantes e a enxergar a si mesmo pelos olhos de quem o despreza.</p>

<p>Theodor Adorno e os pesquisadores de Frankfurt aprofundaram essa ferida ao mapearem a chamada <em>personalidade autoritária</em> <sup id="fnref:10"><a href="#fn:10" class="footnote" rel="footnote" role="doc-noteref">10</a></sup>.</p>

<p>O indivíduo autoritário é marcado por uma covardia estrutural: ele curva a espinha e beija o anel de quem está acima dele na hierarquia social, mas descarrega todo o seu ressentimento e fúria sobre quem está abaixo ou ao seu lado. A pessoa não tem coragem de enfrentar os grandes potentados que fraudam o sistema financeiro e expropriam a riqueza da nação; sua fúria é reservada para a caneta BIC do colega de mesa e para o sindicato que luta pelo seu próprio salário.</p>

<h2 id="o-simulacro-de-independência-e-o-abismo-cognitivo">O simulacro de independência e o abismo cognitivo</h2>

<p>O aspecto mais melancólico desse espetáculo é a capa de isenção sob a qual essa turma se esconde.</p>

<p>O bordão do "não tenho político de estimação" virou o salvo-conduto da covardia retórica. É a blindagem perfeita: permite atacar sem trégua todas as conquistas do campo progressista enquanto se mantém uma condescendência infinita com a barbárie, a milícia e a destruição ambiental.</p>

<p>Se questionado, o sujeito levanta as mãos fingindo santidade: <em>"ora, eu cobro de todos igualmente"</em>.</p>

<p>Não cobra. A indignação do coleguinha tem endereço fixo, CEP e lado ideológico.</p>

<p>E quando a máscara cai, vem a chantagem moral preferida do extremismo disfarçado de virtude: a exigência de que a democracia seja infinitamente tolerante com quem trabalha dia e noite para destruí-la. O cidadão apela para a liberdade de expressão como quem saca um salvo-conduto para propagar desinformação e violência, exigindo que qualquer contestação seja recebida com tapete vermelho em nome do "respeito à divergência".</p>

<p>É a armadilha clássica que Karl Popper batizou de o <em>paradoxo da tolerância</em> em 1945 <sup id="fnref:13"><a href="#fn:13" class="footnote" rel="footnote" role="doc-noteref">11</a></sup>.</p>

<p>A tese de Popper é cristalina e não admite relativismos: a tolerância ilimitada leva necessariamente ao desaparecimento da tolerância. Se uma sociedade aberta for complacente com quem prega a intolerância, se não estiver disposta a defender as instituições contra o assalto de quem rejeita o pacto democrático, os tolerantes serão destruídos, e a tolerância desaparecerá com eles.</p>

<p>Ele quer a garantia da imunidade diplomática da democracia para poder roer as suas vigas de sustentação.</p>

<p>Ele quer o direito irrestrito de defender a barbárie, o golpismo e o desmonte do bem comum no grupo da firma, mas exige que qualquer reação contrária seja recebida com flores e reverência. Não há pluralidade no autoritarismo. A exigência de civilidade vinda de quem apoia a truculência não é um apelo democrático: é apenas a tática do predador que exige que os outros respeitem as regras enquanto ele as rasga em praça pública.</p>

<p>Hannah Arendt, ao dissecar a gênese do totalitarismo moderno, alertou que o sujeito ideal para a dominação autoritária não é o fascista convicto ou o militante fanático, mas o homem comum para quem a distinção entre fato e ficção, entre verdade e mentira, simplesmente deixou de importar <sup id="fnref:11"><a href="#fn:11" class="footnote" rel="footnote" role="doc-noteref">12</a></sup>.</p>

<p>Quando a realidade material é substituída pela narrativa tribal, o tecido do senso comum se rompe. Fatos perdem a relevância. Dados estatísticos são descartados como conspirações. A realidade concreta é substituída pelo que Jean Baudrillard chamou de <em>simulacro</em>: uma representação hiper-real construída por algoritmos e propaganda que se descola totalmente do mundo físico <sup id="fnref:12"><a href="#fn:12" class="footnote" rel="footnote" role="doc-noteref">13</a></sup>.</p>

<p>Essa mente habita o simulacro.</p>

<p>Para ele, as evidências de esquemas financeiros envolvendo sua turma política não passam de perseguição injusta, enquanto qualquer boato apócrifo recebido em grupos fechados é tratado como revelação divina. Não há ponte de diálogo com quem abandonou o princípio da realidade em favor de uma alucinação digital compartilhada.</p>

<h2 id="o-canário-na-mina-de-carvão">O canário na mina de carvão</h2>

<p>Por volta das dez da manhã, o grupo de WhatsApp silenciou.</p>

<p>Após o confronto sobre Feuerbach, o Rei Saul e a fábula da caneta BIC, o alecrim dourado, que nasceu sem ser semeado, aplicou uma manobra de saída à francesa. Soltou um evasivo <em>"bom, vou trabalhar porque preciso produzir, ao contrário de uns e outros"</em>, colocou um emoji de joinha e desconectou.</p>

<p>O expediente então logo voltou à sua lentidão cinzenta.</p>

<p>A tentação imediata seria reduzir todo esse episódio a uma anedota de escritório, mais um causinho hilário de repartição para contar numa mesa de bar. Mas a verdade é infinitamente mais sombria.</p>

<p>O funcionalismo público federal, com toda a sua estabilidade e acesso privilegiado ao conhecimento, não é uma bolha desconectada da realidade nacional. Pelo contrário: ele funciona como um microcosmo privilegiado da fratura moral e cognitiva que está apodrecendo a sociedade brasileira por dentro.</p>

<div class="markdown-alert markdown-alert-warning"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-alert" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg> O canário na mina</p><p>Nas antigas minas de carvão britânicas, os mineradores desciam às galerias subterrâneas carregando um canário em uma pequena gaiola de ferro. Por ter um sistema respiratório minúsculo e ultra-sensível, o pássaro detectava a presença invisível do monóxido de carbono muito antes dos pulmões humanos. Quando o canário parava de cantar e tombava na gaiola, os homens sabiam que o ar havia se tornado veneno e que a fuga imediata era a única chance de sobrevivência.</p>

<p>A universidade pública e as instituições de pensamento crítico deveriam ser o ambiente mais protegido contra a asfixia da razão. Aqui, em tese, as pessoas convivem com bibliotecas, métodos científicos, debate filosófico e evidências empíricas. Se dentro deste espaço um trabalhador instruído é capaz de justificar a pilhagem do seu país com versículos descontextualizados, igualar o roubo de bilhões a uma caneta esferográfica e aplaudir com paixão o carrasco que prometeu ceifar o seu próprio sustento, então o ar da mina já acabou. O canário não está apenas silenciado: ele já caiu estatelado no chão da gaiola.</p>
</div>

<p>Se os indivíduos que deveriam guardar o pensamento crítico se tornaram reféns voluntários da servidão e do cinismo, o que resta para o restante de um país bombardeado vinte e quatro horas por dia por algoritmos de ódio e púlpitos de extorsão?</p>

<p>O debate no WhatsApp não foi um desentendimento passageiro entre colegas de setor. Foi o sintoma microscópico de um país que perdeu a vergonha do absurdo, que trocou a solidariedade de classe pelo fetiche do chicote e que continua cavando o próprio abismo, convicto de que a culpa pela escuridão é da caneta que alguém esqueceu no bolso.</p>

<h2 id="referências">Referências</h2>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p><strong>Sobre a verdade e a mentira: On Bullshit</strong> {<em>Harry G. Frankfurt, 2005</em>} (<a href="https://www.amazon.com.br/Bullshit-English-Harry-G-Frankfurt-ebook/dp/B001EQ4OJW">Amazon</a>) <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p><strong>A Bíblia Sagrada: Antigo Testamento</strong> {<em>1 Samuel 8:1-22</em>} (<a href="https://desciclopedia.ws/wiki/A_B%C3%ADblia_Sagrada">Desciclopédia</a>) <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3">
      <p><strong>A Essência do Cristianismo</strong> {<em>Ludwig Feuerbach, 1841</em>} (<a href="https://www.amazon.com.br/Ess%C3%AAncia-do-cristianismo-Ludwig-Feuerbach/dp/8532634885">Amazon</a>) <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4">
      <p><strong>A Bíblia Sagrada: Livro do Profeta Amós</strong> {<em>Amós 5:21-24</em>} (<a href="https://desciclopedia.ws/wiki/A_B%C3%ADblia_Sagrada">Desciclopédia</a>) <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5">
      <p><strong>A Bíblia Sagrada: Livro do Profeta Isaías</strong> {<em>Isaías 10:1-2</em>} (<a href="https://desciclopedia.ws/wiki/A_B%C3%ADblia_Sagrada">Desciclopédia</a>) <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:6">
      <p><strong>A Bíblia Sagrada: Evangelho de Mateus</strong> {<em>Mateus 23:23-24</em>} (<a href="https://desciclopedia.ws/wiki/A_B%C3%ADblia_Sagrada">Desciclopédia</a>) <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:7">
      <p><strong>Crítica da Razão Cínica</strong> {<em>Peter Sloterdijk, 1983</em>} (<a href="https://www.amazon.com.br/CRITICA-RAZAO-CINICA-PETER-SLOTERDIJK-ebook/dp/B0D6BLY49G">Amazon</a>) <a href="#fnref:7" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:8">
      <p><strong>Discurso sobre a Servidão Voluntária</strong> {<em>Étienne de La Boétie, 1576</em>} (<a href="https://www.amazon.com.br/Discurso-Servid%C3%A3o-Volunt%C3%A1ria-%C3%89tienne-Bo%C3%A9tie-ebook/dp/B08P9V4KBQ">Amazon</a>) <a href="#fnref:8" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:9">
      <p><strong>Pedagogia do Oprimido</strong> {<em>Paulo Freire, 1968</em>} (<a href="https://www.amazon.com.br/Pedagogia-do-oprimido-Paulo-Freire-ebook/dp/B00I3PUV12">Amazon</a>) <a href="#fnref:9" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:10">
      <p><strong>Estudos sobre a Personalidade Autoritária</strong> {<em>Theodor W. Adorno, 1950</em>} (<a href="https://www.amazon.com.br/Estudos-sobre-personalidade-autorit%C3%A1ria-edi%C3%A7%C3%A3o/dp/6557112988">Amazon</a>) <a href="#fnref:10" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:13">
      <p><strong>A Sociedade Aberta e Seus Inimigos</strong> {<em>Karl R. Popper, 1945</em>} (<a href="https://www.amazon.com.br/Society-Enemies-Routledge-Classics-English-ebook/dp/B0FCCXGM5J">Amazon</a>) <a href="#fnref:13" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:11">
      <p><strong>Origens do Totalitarismo</strong> {<em>Hannah Arendt, 1951</em>} (<a href="https://www.amazon.com.br/Origens-totalitarismo-Antissemitismo-imperialismo-ebook/dp/B00BI8HK38">Amazon</a>) <a href="#fnref:11" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:12">
      <p><strong>Simulacros e Simulação</strong> {<em>Jean Baudrillard, 1981</em>} (<a href="https://www.amazon.com.br/Simulacros-simulacao-Jean-Baudrillard/dp/972708141X">Amazon</a>) <a href="#fnref:12" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>[&quot;Eduardo N. S. R.&quot;]</name></author><category term="Crônicas" /><category term="Sociedade" /><category term="Política" /><category term="Filosofia" /><category term="Trabalho" /><summary type="html"><![CDATA[Existe uma regra não escrita sobre a sobrevivência em repartição pública: nunca abra o grupo de WhatsApp do trabalho antes da segunda xícara de café. Se você cometer essa imprudência com o estômago vazio, corre o risco sério de descobrir que o seu colega de sala encontrou uma justificativa no livro de Samuel para passar pano em escândalo financeiro antes mesmo das nove da manhã.]]></summary></entry><entry><title type="html">Kubernetes in a Box, Parte 2 - Ferramentas e Ambiente de Laboratório</title><link href="https://vndmtrx.github.io/posts/k8sbox-ferramentas-ambiente/" rel="alternate" type="text/html" title="Kubernetes in a Box, Parte 2 - Ferramentas e Ambiente de Laboratório" /><published>2026-09-07T11:33:00+00:00</published><updated>2026-09-07T11:33:00+00:00</updated><id>https://vndmtrx.github.io/posts/k8sbox-ferramentas-ambiente</id><content type="html" xml:base="https://vndmtrx.github.io/posts/k8sbox-ferramentas-ambiente/"><![CDATA[<p>Na Parte 1 desta série, gastamos o post inteiro falando sobre <em>por que</em> construir um cluster Kubernetes na mão e <em>o que</em> pretendemos montar. Conceitos, diagramas, motivação. Agora é hora de sujar as mãos de verdade: vamos configurar o computador hospedeiro, dissecar linha por linha como o Vagrant lê nosso inventário Ansible para criar máquinas virtuais e resolver o problema mais traiçoeiro de qualquer laboratório local de Kubernetes: fazer a rede funcionar direito.</p>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Nota da Série e Contexto de Laboratório</p><p>Este post faz parte da série <strong>"Kubernetes in a Box"</strong>, onde dissecamos e construímos, do zero e de forma totalmente reproduzível via Ansible, um cluster Kubernetes completo, com alta disponibilidade, armazenamento persistente, rede moderna e observabilidade. Todo o código do projeto está disponível no repositório parceiro <a href="https://github.com/vndmtrx/k8s-in-a-box">vndmtrx/k8s-in-a-box</a>.</p>

<p><strong>Aviso de escopo:</strong> as decisões de arquitetura e parâmetros deste projeto foram pensadas sob medida para a nossa realidade de laboratório local em estações Linux com Vagrant e KVM/Libvirt. Elas priorizam aprendizado profundo e reprodutibilidade rápida sobre convenções corporativas de produção. O cluster foi originalmente projetado em versões anteriores e recentemente atualizado para o <strong>Kubernetes v1.37.0</strong>.</p>
</div>

<p>Se você já perdeu horas tentando entender por que as VMs do Vagrant conseguiam pingar a internet mas não se enxergavam entre si, ou por que um <code class="language-plaintext highlighter-rouge">vagrant up</code> subia seis máquinas idênticas com a mesma rota padrão apontando pro lugar errado, esse post é pra você. Vamos resolver cada um desses problemas, explicar as decisões por trás de cada linha de configuração e, no final, ter um ambiente local completamente funcional com VMs prontas para receber o cluster nas próximas partes.</p>

<details class="toc-box">
  <summary><strong>Sumário &amp; Índice de Seções (TOC)</strong></summary>
<ul id="markdown-toc">
  <li><a href="#o-que-você-precisa-ter-instalado" id="markdown-toc-o-que-você-precisa-ter-instalado">O que você precisa ter instalado</a></li>
  <li><a href="#por-que-kvm-e-não-virtualbox" id="markdown-toc-por-que-kvm-e-não-virtualbox">Por que KVM e não VirtualBox</a></li>
  <li><a href="#almalinux-10-a-distro-das-vms" id="markdown-toc-almalinux-10-a-distro-das-vms">AlmaLinux 10: a distro das VMs</a></li>
  <li><a href="#o-vagrantfile-dissecado" id="markdown-toc-o-vagrantfile-dissecado">O Vagrantfile dissecado</a>    <ul>
      <li><a href="#leitura-dinâmica-do-inventário" id="markdown-toc-leitura-dinâmica-do-inventário">Leitura dinâmica do inventário</a></li>
      <li><a href="#geração-automática-de-chaves-ssh" id="markdown-toc-geração-automática-de-chaves-ssh">Geração automática de chaves SSH</a></li>
      <li><a href="#configuração-global-do-provider-libvirt" id="markdown-toc-configuração-global-do-provider-libvirt">Configuração global do provider LibVirt</a></li>
      <li><a href="#criação-das-vms-e-a-rede-privada" id="markdown-toc-criação-das-vms-e-a-rede-privada">Criação das VMs e a rede privada</a></li>
    </ul>
  </li>
  <li><a href="#as-duas-placas-de-rede-e-a-armadilha-do-eth0" id="markdown-toc-as-duas-placas-de-rede-e-a-armadilha-do-eth0">As duas placas de rede (e a armadilha do eth0)</a>    <ul>
      <li><a href="#a-guerra-dos-gateways-via-nmcli" id="markdown-toc-a-guerra-dos-gateways-via-nmcli">A guerra dos gateways via nmcli</a></li>
    </ul>
  </li>
  <li><a href="#o-sistema-de-topologias-três-clusters-um-repositório" id="markdown-toc-o-sistema-de-topologias-três-clusters-um-repositório">O sistema de topologias: três clusters, um repositório</a>    <ul>
      <li><a href="#anatomia-de-um-inventário" id="markdown-toc-anatomia-de-um-inventário">Anatomia de um inventário</a></li>
    </ul>
  </li>
  <li><a href="#o-makefile-como-interface-de-comandos" id="markdown-toc-o-makefile-como-interface-de-comandos">O Makefile como interface de comandos</a>    <ul>
      <li><a href="#o-mecanismo-garante-config" id="markdown-toc-o-mecanismo-garante-config">O mecanismo garante-config</a></li>
      <li><a href="#snapshots-seu-seguro-contra-desastres" id="markdown-toc-snapshots-seu-seguro-contra-desastres">Snapshots: seu seguro contra desastres</a></li>
    </ul>
  </li>
  <li><a href="#do-zero-ao-ambiente-funcional-o-fluxo-completo" id="markdown-toc-do-zero-ao-ambiente-funcional-o-fluxo-completo">Do zero ao ambiente funcional: o fluxo completo</a></li>
  <li><a href="#diagnóstico-e-troubleshooting" id="markdown-toc-diagnóstico-e-troubleshooting">Diagnóstico e Troubleshooting</a>    <ul>
      <li><a href="#status-das-vms" id="markdown-toc-status-das-vms">Status das VMs</a></li>
      <li><a href="#verificação-de-rede-dentro-das-vms" id="markdown-toc-verificação-de-rede-dentro-das-vms">Verificação de rede dentro das VMs</a></li>
      <li><a href="#teste-de-conectividade-entre-vms" id="markdown-toc-teste-de-conectividade-entre-vms">Teste de conectividade entre VMs</a></li>
      <li><a href="#problemas-comuns" id="markdown-toc-problemas-comuns">Problemas comuns</a></li>
    </ul>
  </li>
  <li><a href="#conclusão" id="markdown-toc-conclusão">Conclusão</a></li>
  <li><a href="#referências" id="markdown-toc-referências">Referências</a></li>
</ul>

</details>

<h2 id="o-que-você-precisa-ter-instalado">O que você precisa ter instalado</h2>

<p>Antes de qualquer coisa, seu hospedeiro Linux precisa de cinco ferramentas. Essas são as únicas instalações manuais que você vai fazer em todo o processo: tudo o que acontece <em>dentro</em> das VMs (chaves SSH, configuração de rede, pacotes, provisionamento) é gerenciado automaticamente pelo Vagrantfile e pelo Ansible quando você roda <code class="language-plaintext highlighter-rouge">make up</code>.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Ferramenta</th>
      <th style="text-align: left">Função no Projeto</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>KVM</strong></td>
      <td style="text-align: left">Hipervisor nativo do kernel Linux</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>libvirt</strong></td>
      <td style="text-align: left">API de gerenciamento de VMs (daemon <code class="language-plaintext highlighter-rouge">libvirtd</code>)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Vagrant</strong></td>
      <td style="text-align: left">Orquestrador de máquinas virtuais via CLI</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>vagrant-libvirt</strong></td>
      <td style="text-align: left">Plugin que conecta o Vagrant ao KVM/libvirt</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Ansible</strong></td>
      <td style="text-align: left">Motor de automação declarativa</td>
    </tr>
  </tbody>
</table>

<p>A forma de instalar cada uma varia conforme a sua distribuição, então não vou prescrever comandos aqui. O importante é que todas estejam disponíveis no <code class="language-plaintext highlighter-rouge">PATH</code> do seu usuário. Uma coisa que pode pegar desprevenido: o KVM precisa de permissões de grupo. Sem elas, o Vagrant vai pedir senha de <code class="language-plaintext highlighter-rouge">root</code> a cada operação ou simplesmente falhar silenciosamente. Garanta que o seu usuário pertence aos grupos <code class="language-plaintext highlighter-rouge">libvirt</code> e <code class="language-plaintext highlighter-rouge">kvm</code> (e faça logout/login depois de adicioná-los).</p>

<div class="markdown-alert markdown-alert-tip"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-light-bulb" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"></path></svg> Dica</p><p>O projeto inclui o comando <code class="language-plaintext highlighter-rouge">make check-deps</code> que verifica todas essas dependências de uma vez. Ele testa se o Ansible, Vagrant, KVM, libvirt e o plugin vagrant-libvirt estão instalados e com as permissões corretas. É o primeiro comando que você deveria rodar depois de clonar o repositório.</p>
</div>

<p>A saída do <code class="language-plaintext highlighter-rouge">check-deps</code> é direta ao ponto:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>make check-deps
Verificando dependências <span class="k">do </span>host...
  - Ansible: OK
  - Vagrant: OK
  - KVM <span class="o">(</span>/dev/kvm<span class="o">)</span>: OK
  - Conexão Libvirt <span class="o">(</span>virsh<span class="o">)</span>: OK
  - Vagrant Libvirt Plugin: OK
  - Porta 6443 <span class="o">(</span>Conflitos locais<span class="o">)</span>: LIVRE
Tudo OK! Pronto para iniciar o provisionamento.
</code></pre></div></div>

<p>Se alguma linha aparecer como <code class="language-plaintext highlighter-rouge">NÃO ENCONTRADO</code> ou <code class="language-plaintext highlighter-rouge">SEM PERMISSÃO</code>, o comando aborta a execução e te diz exatamente o que falta. Sem surpresas no meio do provisionamento.</p>

<div class="markdown-alert markdown-alert-warning"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-alert" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg> Atenção com Recursos de Hardware (Memória, Disco e Portas)</p><p>Antes de disparar a criação das máquinas virtuais, atente-se a três pontos práticos:</p>
<ul>
  <li><strong>Memória RAM no limite:</strong> a configuração padrão (<code class="language-plaintext highlighter-rouge">mini</code>) aloca ~11 GB de RAM para as VMs. Se o seu computador tiver 16 GB no total, feche abas pesadas do navegador e IDEs vorazes antes de subir o cluster para evitar que o <em>OOM Killer</em> do Linux derrube uma VM no meio do caminho. Para máquinas com recursos mais modestos, use sem medo a configuração <code class="language-plaintext highlighter-rouge">nano</code> (~6.5 GB de RAM).</li>
  <li><strong>Espaço em Disco:</strong> o download da imagem base do AlmaLinux 10 e a criação dos discos virtuais das VMs exigem espaço livre considerável. Garanta pelo menos 25 a 30 GB disponíveis no seu sistema de arquivos antes de começar.</li>
  <li><strong>Conflito de Portas:</strong> se você utiliza Docker Desktop, Minikube ou outro cluster local na máquina hospedeira, certifique-se de que serviços locais não estejam disputando a porta <code class="language-plaintext highlighter-rouge">6443</code> ou interferindo nas pontes de rede do Libvirt.</li>
</ul>
</div>

<h2 id="por-que-kvm-e-não-virtualbox">Por que KVM e não VirtualBox</h2>

<p>A decisão de usar LibVirt <sup id="fnref:5"><a href="#fn:5" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> com KVM <sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> em vez do VirtualBox foi, antes de tudo, uma questão de manter a stack o mais <em>open source</em> possível. O KVM é um módulo nativo do kernel Linux, mantido no <em>mainline</em> pela própria comunidade do kernel. O libvirt é LGPL. Tudo roda com ferramentas que vêm nos repositórios da sua distribuição, sem precisar baixar pacotes proprietários ou aceitar licenças de uso restritivo.</p>

<p>Sim, o VirtualBox também usa um módulo de kernel (<code class="language-plaintext highlighter-rouge">vboxdrv</code>) para acessar as extensões de virtualização do processador, então não é uma questão de "um usa hardware e o outro não". Ambos fazem virtualização assistida por hardware. Mas o módulo do VirtualBox é mantido fora da árvore do kernel, distribuído pela Oracle sob uma licença que mistura GPLv3 com componentes proprietários no <em>Extension Pack</em>, e precisa ser recompilado a cada atualização de kernel. Com KVM, esse atrito simplesmente não existe.</p>

<p>Na prática, pra um cluster Kubernetes com cinco a nove VMs rodando simultaneamente e disputando CPU, memória e disco, tanto KVM quanto VirtualBox vão entregar performance razoável. A diferença real é filosófica e operacional: com KVM você está usando a infraestrutura nativa do seu sistema operacional, sem dependências externas.</p>

<p>O Vagrant <sup id="fnref:4"><a href="#fn:4" class="footnote" rel="footnote" role="doc-noteref">3</a></sup> funciona como uma camada de abstração sobre o hipervisor. Ele não cria VMs sozinho: ele precisa de um <em>provider</em> que saiba efetivamente gerenciar o ciclo de vida das máquinas. O plugin <code class="language-plaintext highlighter-rouge">vagrant-libvirt</code> <sup id="fnref:6"><a href="#fn:6" class="footnote" rel="footnote" role="doc-noteref">4</a></sup> faz essa ponte, traduzindo as instruções do <code class="language-plaintext highlighter-rouge">Vagrantfile</code> para chamadas à API do libvirt, que por sua vez comanda o KVM.</p>

<h2 id="almalinux-10-a-distro-das-vms">AlmaLinux 10: a distro das VMs</h2>

<p>Para as máquinas virtuais do laboratório, escolhemos o <strong>AlmaLinux 10</strong> <sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">5</a></sup> como sistema operacional base. Essa escolha não é um capricho estético.</p>

<p>O AlmaLinux é uma distribuição binariamente compatível com o Red Hat Enterprise Linux (RHEL), o que significa que tudo o que funciona no RHEL funciona aqui sem adaptação. Isso é relevante porque a maior parte da documentação corporativa de Kubernetes e os guias de hardening de segurança assumem um ecossistema RHEL-<em>like</em> (CentOS, Rocky, AlmaLinux). Do ponto de vista prático, a versão 10 traz o NetworkManager como gerenciador de rede padrão com o <code class="language-plaintext highlighter-rouge">nmcli</code> já integrado, <code class="language-plaintext highlighter-rouge">systemd</code> atualizado, e <em>boxes</em> oficiais para Vagrant publicados e mantidos ativamente no Vagrant Cloud.</p>

<div class="markdown-alert markdown-alert-warning"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-alert" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg> Atenção</p><p>A série e o projeto estão atualmente travados na versão <strong>AlmaLinux 10</strong>. Mudanças futuras de versão do sistema operacional podem exigir ajustes no Vagrantfile e nas <em>roles</em> do Ansible, especialmente na configuração de rede e nos pacotes disponíveis nos repositórios. Se você estiver acompanhando a série em um momento posterior, verifique o repositório do projeto para a versão atual suportada.</p>
</div>

<p>Se você preferir usar outra distribuição (Ubuntu, Debian, openSUSE), o conceito por trás de cada etapa será idêntico, mas os comandos de pacote, os caminhos de configuração e o gerenciador de rede podem ser diferentes. No nosso caso, o <code class="language-plaintext highlighter-rouge">nmcli</code> é protagonista absoluto na configuração de rede das VMs, e ele é cidadão de primeira classe no ecossistema RHEL.</p>

<h2 id="o-vagrantfile-dissecado">O Vagrantfile dissecado</h2>

<p>Agora vamos ao arquivo que orquestra toda a criação das máquinas virtuais. O <code class="language-plaintext highlighter-rouge">Vagrantfile</code> do k8s-in-a-box não é um arquivo estático com meia dúzia de VMs <em>hardcoded</em>. Ele é um programa Ruby que lê dinamicamente o inventário do Ansible e constrói as VMs a partir das definições que encontra ali.</p>

<h3 id="leitura-dinâmica-do-inventário">Leitura dinâmica do inventário</h3>

<p>A mágica começa logo nas primeiras linhas:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'yaml'</span>

<span class="n">inventario</span> <span class="o">=</span> <span class="no">YAML</span><span class="p">.</span><span class="nf">load_file</span><span class="p">(</span><span class="s2">"inventario/hosts.yml"</span><span class="p">)</span>
<span class="n">grupos</span> <span class="o">=</span> <span class="n">inventario</span><span class="p">[</span><span class="s2">"all"</span><span class="p">][</span><span class="s2">"children"</span><span class="p">]</span>

<span class="n">nodes</span> <span class="o">=</span> <span class="p">{}</span>

<span class="n">grupos</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">grupo</span><span class="p">,</span> <span class="n">dados</span><span class="o">|</span>
  <span class="k">next</span> <span class="k">unless</span> <span class="n">dados</span><span class="p">[</span><span class="s2">"hosts"</span><span class="p">]</span>

  <span class="n">dados</span><span class="p">[</span><span class="s2">"hosts"</span><span class="p">].</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">nome</span><span class="p">,</span> <span class="n">props</span><span class="o">|</span>
    <span class="n">nodes</span><span class="p">[</span><span class="n">nome</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
      <span class="s2">"ip"</span>        <span class="o">=&gt;</span> <span class="n">props</span><span class="p">[</span><span class="s2">"ansible_host"</span><span class="p">],</span>
      <span class="s2">"memory"</span>    <span class="o">=&gt;</span> <span class="n">props</span><span class="p">[</span><span class="s2">"memory"</span><span class="p">],</span>
      <span class="s2">"cpus"</span>      <span class="o">=&gt;</span> <span class="n">props</span><span class="p">[</span><span class="s2">"cpus"</span><span class="p">],</span>
      <span class="s2">"autostart"</span> <span class="o">=&gt;</span> <span class="n">props</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s2">"autostart"</span><span class="p">,</span> <span class="kp">true</span><span class="p">)</span>
    <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p><em>O Vagrant carrega o arquivo <code class="language-plaintext highlighter-rouge">inventario/hosts.yml</code> (que é um symlink para a topologia ativa), percorre todos os grupos do inventário e extrai IP, memória, CPUs e a flag de autostart de cada host.</em></p>

<p>A beleza desse mecanismo é que o Vagrantfile nunca precisa ser editado. Quer adicionar um worker? Bota no inventário YAML. Quer trocar a topologia inteira? Muda o symlink. A mesma lógica em Ruby vai gerar as VMs corretas independente de quantas máquinas estejam definidas no inventário.</p>

<p>O hash <code class="language-plaintext highlighter-rouge">nodes</code> resultante é um dicionário simples onde a chave é o nome da VM e o valor contém as especificações de hardware. Pra topologia <code class="language-plaintext highlighter-rouge">mini</code>, por exemplo, ele vai conter seis entradas: <code class="language-plaintext highlighter-rouge">loadbalancer1</code>, <code class="language-plaintext highlighter-rouge">nfs1</code>, <code class="language-plaintext highlighter-rouge">manager1</code>, <code class="language-plaintext highlighter-rouge">worker1</code>, <code class="language-plaintext highlighter-rouge">worker2</code> e <code class="language-plaintext highlighter-rouge">kubox</code>.</p>

<h3 id="geração-automática-de-chaves-ssh">Geração automática de chaves SSH</h3>

<p>Um dos objetivos do projeto é que você não precise configurar nada manualmente dentro das VMs. Isso inclui o acesso SSH: antes de criar qualquer máquina, o Vagrantfile gera um par de chaves ed25519 caso elas ainda não existam:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">unless</span> <span class="no">ARGV</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="s2">"destroy"</span><span class="p">)</span>
  <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">exist?</span><span class="p">(</span><span class="s1">'id_ed25519'</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="no">File</span><span class="p">.</span><span class="nf">exist?</span><span class="p">(</span><span class="s1">'id_ed25519.pub'</span><span class="p">)</span>
    <span class="nb">system</span><span class="p">(</span><span class="s1">'ssh-keygen -t ed25519 -f id_ed25519 -N "" &gt;/dev/null 2&gt;&amp;1'</span><span class="p">)</span>
    <span class="nb">puts</span> <span class="s2">"Nova chave SSH gerada."</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p><em>Gera uma chave ed25519 sem passphrase, apenas se ela não existir e se o comando não for <code class="language-plaintext highlighter-rouge">vagrant destroy</code>.</em></p>

<p>Durante a criação de cada VM, essa chave pública é automaticamente injetada no <code class="language-plaintext highlighter-rouge">authorized_keys</code> do usuário <code class="language-plaintext highlighter-rouge">vagrant</code>. É com ela que o Ansible vai se conectar às máquinas depois, sem que você precise copiar chaves manualmente, editar <code class="language-plaintext highlighter-rouge">authorized_keys</code> ou configurar qualquer coisa. O <code class="language-plaintext highlighter-rouge">config.ssh.insert_key = false</code> logo adiante desativa a inserção da chave padrão do Vagrant, garantindo que a única chave de acesso seja a do projeto. O arquivo <code class="language-plaintext highlighter-rouge">ssh_config</code> na raiz do repositório já aponta para essa chave e desabilita a verificação de <em>host keys</em>, fechando o ciclo: Vagrant gera, injeta e o Ansible usa.</p>

<p>E quando o cluster é destruído? Um <em>trigger</em> pós-destruição cuida da limpeza:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">config</span><span class="p">.</span><span class="nf">trigger</span><span class="p">.</span><span class="nf">after</span> <span class="ss">:destroy</span> <span class="k">do</span> <span class="o">|</span><span class="n">trigger</span><span class="o">|</span>
  <span class="n">trigger</span><span class="p">.</span><span class="nf">ruby</span> <span class="k">do</span> <span class="o">|</span><span class="n">env</span><span class="p">,</span> <span class="n">machine</span><span class="o">|</span>
    <span class="n">arquivos</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'id_ed25519'</span><span class="p">,</span> <span class="s1">'id_ed25519.pub'</span><span class="p">]</span>
    <span class="k">if</span> <span class="n">arquivos</span><span class="p">.</span><span class="nf">any?</span> <span class="p">{</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span> <span class="no">File</span><span class="p">.</span><span class="nf">exist?</span><span class="p">(</span><span class="n">f</span><span class="p">)</span> <span class="p">}</span>
      <span class="n">arquivos</span><span class="p">.</span><span class="nf">each</span> <span class="p">{</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span> <span class="no">File</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="n">f</span><span class="p">)</span> <span class="k">if</span> <span class="no">File</span><span class="p">.</span><span class="nf">exist?</span><span class="p">(</span><span class="n">f</span><span class="p">)</span> <span class="p">}</span>
      <span class="nb">puts</span> <span class="s2">"Chaves SSH removidas."</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p><em>Remove as chaves SSH quando todas as VMs são destruídas. Ciclo de vida limpo.</em></p>

<h3 id="configuração-global-do-provider-libvirt">Configuração global do provider LibVirt</h3>

<p>A configuração do provider KVM é definida uma única vez e aplicada a todas as VMs:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">provider</span> <span class="ss">:libvirt</span> <span class="k">do</span> <span class="o">|</span><span class="n">libvirt</span><span class="o">|</span>
  <span class="n">libvirt</span><span class="p">.</span><span class="nf">driver</span> <span class="o">=</span> <span class="s2">"kvm"</span>
  <span class="n">libvirt</span><span class="p">.</span><span class="nf">memorybacking</span> <span class="ss">:source</span><span class="p">,</span> <span class="ss">:type</span> <span class="o">=&gt;</span> <span class="s2">"memfd"</span>
  <span class="n">libvirt</span><span class="p">.</span><span class="nf">memorybacking</span> <span class="ss">:access</span><span class="p">,</span> <span class="ss">:mode</span> <span class="o">=&gt;</span> <span class="s2">"shared"</span>

  <span class="n">libvirt</span><span class="p">.</span><span class="nf">cpu_mode</span> <span class="o">=</span> <span class="s2">"host-model"</span>
  <span class="n">libvirt</span><span class="p">.</span><span class="nf">nested</span> <span class="o">=</span> <span class="kp">true</span>

  <span class="n">libvirt</span><span class="p">.</span><span class="nf">nic_model_type</span> <span class="o">=</span> <span class="s2">"virtio"</span>
  <span class="n">libvirt</span><span class="p">.</span><span class="nf">management_network_name</span> <span class="o">=</span> <span class="s2">"vagrant"</span>
  <span class="n">libvirt</span><span class="p">.</span><span class="nf">management_network_address</span> <span class="o">=</span> <span class="s2">"192.168.250.0/24"</span>
  <span class="n">libvirt</span><span class="p">.</span><span class="nf">management_network_mode</span> <span class="o">=</span> <span class="s2">"none"</span>
  <span class="n">libvirt</span><span class="p">.</span><span class="nf">management_network_autostart</span> <span class="o">=</span> <span class="kp">true</span>
<span class="k">end</span>
</code></pre></div></div>

<p><em>Configuração do hipervisor: KVM com virtio, CPU passthrough em modo host-model, e rede de gerenciamento do Vagrant em <code class="language-plaintext highlighter-rouge">192.168.250.0/24</code> sem NAT.</em></p>

<p>Duas decisões chamam atenção aqui. Primeiro, o <code class="language-plaintext highlighter-rouge">cpu_mode = "host-model"</code> expõe os <em>flags</em> da CPU do hospedeiro diretamente para as VMs, o que é essencial para o eBPF do Cilium funcionar corretamente. Segundo, a rede de gerenciamento do Vagrant (<code class="language-plaintext highlighter-rouge">192.168.250.0/24</code>) é configurada com <code class="language-plaintext highlighter-rouge">mode = "none"</code>, sem NAT e sem DHCP. Essa é a rede <code class="language-plaintext highlighter-rouge">eth0</code> que o Vagrant usa internamente pra comunicação via SSH. O cluster não usa essa rede pra nada.</p>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Pastas Compartilhadas Desativadas</p><p>Por padrão, o Vagrant tenta montar o diretório do projeto dentro de cada VM em <code class="language-plaintext highlighter-rouge">/vagrant</code> usando 9p, virtiofs ou NFS. No ecossistema KVM/libvirt, isso costuma ser uma fonte clássica de lentidão e erros de montagem caso o host não tenha os módulos correspondentes configurados. Como o nosso laboratório é 100% provisionado remotamente pelo Ansible via SSH, desativamos essa sincronização explicitamente com <code class="language-plaintext highlighter-rouge">config.vm.synced_folder "./", "/vagrant", disabled: true</code>. Menos atrito, boot mais veloz e zero dependências de filesystem compartilhado.</p>
</div>

<h3 id="criação-das-vms-e-a-rede-privada">Criação das VMs e a rede privada</h3>

<p>O loop principal itera sobre cada nó do hash <code class="language-plaintext highlighter-rouge">nodes</code> e cria uma VM correspondente:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">nodes</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">nome_no</span><span class="p">,</span> <span class="n">specs</span><span class="o">|</span>
  <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">define</span> <span class="n">nome_no</span><span class="p">,</span> <span class="ss">autostart: </span><span class="n">specs</span><span class="p">[</span><span class="s2">"autostart"</span><span class="p">]</span> <span class="k">do</span> <span class="o">|</span><span class="n">node</span><span class="o">|</span>
    <span class="n">node</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">hostname</span> <span class="o">=</span> <span class="n">nome_no</span>
    <span class="n">node</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">network</span> <span class="s2">"private_network"</span><span class="p">,</span> <span class="ss">ip: </span><span class="n">specs</span><span class="p">[</span><span class="s2">"ip"</span><span class="p">],</span>
      <span class="ss">libvirt__network_name: </span><span class="s2">"k8sbox_mgmt"</span><span class="p">,</span>
      <span class="ss">libvirt__forward_mode: </span><span class="s2">"nat"</span><span class="p">,</span>
      <span class="ss">libvirt__dhcp_enabled: </span><span class="kp">false</span>

    <span class="n">node</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">provider</span> <span class="ss">:libvirt</span> <span class="k">do</span> <span class="o">|</span><span class="n">libvirt_host</span><span class="o">|</span>
      <span class="n">libvirt_host</span><span class="p">.</span><span class="nf">default_prefix</span> <span class="o">=</span> <span class="s2">"k8sbox_"</span>
      <span class="n">libvirt_host</span><span class="p">.</span><span class="nf">memory</span> <span class="o">=</span> <span class="n">specs</span><span class="p">[</span><span class="s2">"memory"</span><span class="p">]</span>
      <span class="n">libvirt_host</span><span class="p">.</span><span class="nf">cpus</span> <span class="o">=</span> <span class="n">specs</span><span class="p">[</span><span class="s2">"cpus"</span><span class="p">]</span>
    <span class="k">end</span>
    <span class="c1"># ... provisionamento de rede (próxima seção)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p><em>Para cada host no inventário, cria uma VM com o IP fixo definido, conecta à rede <code class="language-plaintext highlighter-rouge">k8sbox_mgmt</code> em modo NAT sem DHCP, e aplica as configurações de memória e CPU.</em></p>

<p>Aqui o detalhe crucial: a rede <code class="language-plaintext highlighter-rouge">k8sbox_mgmt</code> é criada com <code class="language-plaintext highlighter-rouge">forward_mode: "nat"</code> e <code class="language-plaintext highlighter-rouge">dhcp_enabled: false</code>. Isso significa que as VMs terão acesso à internet (via NAT do host) mas sem servidor DHCP atribuindo IPs automaticamente. Os IPs são estáticos e definidos pelo inventário. Essa rede será a <code class="language-plaintext highlighter-rouge">eth1</code> dentro das VMs e é por onde todo o tráfego do cluster vai transitar.</p>

<p>Note também o <code class="language-plaintext highlighter-rouge">autostart: specs["autostart"]</code>. No inventário, o <code class="language-plaintext highlighter-rouge">kubox</code> é definido com <code class="language-plaintext highlighter-rouge">autostart: false</code>. Ele não sobe junto com as demais VMs quando você roda <code class="language-plaintext highlighter-rouge">vagrant up</code>, porque durante a construção do cluster ele ainda não é necessário. O Makefile cuida de subi-lo separadamente no momento certo.</p>

<h2 id="as-duas-placas-de-rede-e-a-armadilha-do-eth0">As duas placas de rede (e a armadilha do eth0)</h2>

<p>Toda VM criada pelo nosso Vagrantfile recebe duas interfaces de rede. Isso é proposital, mas causa um problema que se você não tratar, vai quebrar todo o funcionamento do cluster.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Configuração</th>
      <th style="text-align: left"><code class="language-plaintext highlighter-rouge">eth0</code> (<code class="language-plaintext highlighter-rouge">net_vagrant</code>)</th>
      <th style="text-align: left"><code class="language-plaintext highlighter-rouge">eth1</code> (<code class="language-plaintext highlighter-rouge">net_mgmt</code>)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>Finalidade</strong></td>
      <td style="text-align: left">Gerenciamento do Vagrant (SSH, provisionamento)</td>
      <td style="text-align: left">Rede interna do cluster Kubernetes</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>IP (ex: <code class="language-plaintext highlighter-rouge">manager1</code>)</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">192.168.250.x</code> (DHCP)</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">172.24.0.31</code> (Estático)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Métrica de Rota</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">500</code> (Baixa prioridade)</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">50</code> (Alta prioridade)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Gateway / Rota Padrão</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">never-default: yes</code> (Não assume rota default)</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">172.24.0.1</code> (Gateway principal)</td>
    </tr>
  </tbody>
</table>

<p>A <code class="language-plaintext highlighter-rouge">eth0</code> é a rede de gerenciamento do Vagrant. Ela existe porque o Vagrant precisa de uma forma de se comunicar com a VM via SSH para provisionamento e comandos como <code class="language-plaintext highlighter-rouge">vagrant ssh</code>. Mas essa rede é completamente inútil para o cluster Kubernetes. Pior: se deixada sem tratamento, ela se torna a rota padrão da VM, e todo o tráfego de saída vai por ela em vez de ir pela <code class="language-plaintext highlighter-rouge">eth1</code> onde o cluster mora.</p>

<p>A <code class="language-plaintext highlighter-rouge">eth1</code> é a rede privada <code class="language-plaintext highlighter-rouge">172.24.0.0/24</code>, que é o verdadeiro sistema circulatório do laboratório. Todo tráfego do <code class="language-plaintext highlighter-rouge">etcd</code>, do <code class="language-plaintext highlighter-rouge">kube-apiserver</code>, dos pods e dos serviços transita por essa interface.</p>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> O Gateway 172.24.0.1 e a ponte de rede do Libvirt</p><p>O IP <code class="language-plaintext highlighter-rouge">172.24.0.1</code> é a ponta da interface de ponte virtual (<em>bridge</em>) mantida pelo Libvirt no computador hospedeiro (<code class="language-plaintext highlighter-rouge">k8sbox_mgmt</code>). É ele quem recebe e roteia os pacotes da <code class="language-plaintext highlighter-rouge">eth1</code>. Se o seu host tiver regras customizadas de iptables/nftables ou redes virtuais antigas colidindo nessa mesma faixa, certifique-se de que o host responde com <code class="language-plaintext highlighter-rouge">ping 172.24.0.1</code> antes de prosseguir.</p>
</div>

<h3 id="a-guerra-dos-gateways-via-nmcli">A guerra dos gateways via nmcli</h3>

<p>Aqui entra o trecho mais importante do Vagrantfile: o provisionamento de rede via shell que resolve a prioridade entre as interfaces. Esse script foi resultado de bastante experimentação, especialmente na migração do AlmaLinux 9 para o 10, onde o comportamento do NetworkManager <sup id="fnref:7"><a href="#fn:7" class="footnote" rel="footnote" role="doc-noteref">6</a></sup> mudou significativamente.</p>

<p>No AlmaLinux 9, os nomes das conexões eram previsíveis (<code class="language-plaintext highlighter-rouge">eth0</code>, <code class="language-plaintext highlighter-rouge">System eth1</code>). Na versão 10, o NetworkManager passou a nomear as conexões de forma genérica (<code class="language-plaintext highlighter-rouge">Wired connection 1</code>, <code class="language-plaintext highlighter-rouge">Wired connection 2</code>), o que quebrou todas as referências estáticas que eu tinha no Vagrantfile. A solução foi implementar uma detecção dinâmica dos nomes das conexões por dispositivo:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># --- Ajustes de nomenclatura dinâmicos ---</span>
<span class="nv">CON_VAGRANT</span><span class="o">=</span><span class="si">$(</span>nmcli <span class="nt">-t</span> <span class="nt">-f</span> NAME,DEVICE connection show <span class="se">\</span>
  | <span class="nb">grep</span> <span class="s1">':eth0$'</span> | <span class="nb">cut</span> <span class="nt">-d</span>: <span class="nt">-f1</span> | <span class="nb">head</span> <span class="nt">-n1</span><span class="si">)</span>
<span class="nv">CON_MGMT</span><span class="o">=</span><span class="si">$(</span>nmcli <span class="nt">-t</span> <span class="nt">-f</span> NAME,DEVICE connection show <span class="se">\</span>
  | <span class="nb">grep</span> <span class="s1">':eth1$'</span> | <span class="nb">cut</span> <span class="nt">-d</span>: <span class="nt">-f1</span> | <span class="nb">head</span> <span class="nt">-n1</span><span class="si">)</span>

<span class="k">if</span> <span class="o">[</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$CON_VAGRANT</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
  </span>nmcli con mod <span class="s2">"</span><span class="nv">$CON_VAGRANT</span><span class="s2">"</span> connection.id net_vagrant ifname eth0
<span class="k">fi
if</span> <span class="o">[</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$CON_MGMT</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
  </span>nmcli con mod <span class="s2">"</span><span class="nv">$CON_MGMT</span><span class="s2">"</span> connection.id net_mgmt ifname eth1
<span class="k">fi
</span>nmcli con reload
</code></pre></div></div>

<p><em>Descobre dinamicamente qual conexão está associada a cada dispositivo físico (<code class="language-plaintext highlighter-rouge">eth0</code>, <code class="language-plaintext highlighter-rouge">eth1</code>) e renomeia para nomes previsíveis (<code class="language-plaintext highlighter-rouge">net_vagrant</code>, <code class="language-plaintext highlighter-rouge">net_mgmt</code>).</em></p>

<p>Depois de renomear, o script trata cada interface individualmente:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># --- Configurações net_vagrant ---</span>
nmcli con mod net_vagrant <span class="se">\</span>
  connection.autoconnect <span class="nb">yes</span> <span class="se">\</span>
  connection.autoconnect-priority <span class="nt">-999</span>  <span class="se">\</span>
  ipv4.route-metric 500 <span class="se">\</span>
  ipv4.ignore-auto-dns <span class="nb">yes</span> <span class="se">\</span>
  ipv4.never-default <span class="nb">yes</span> <span class="se">\</span>
  ipv6.method ignore

nmcli con reload <span class="o">&amp;&amp;</span> nmcli con up net_vagrant
</code></pre></div></div>

<p><em>Rebaixa a eth0: métrica 500 (baixa prioridade), ignora DNS do DHCP, <code class="language-plaintext highlighter-rouge">never-default</code> impede que ela se torne gateway. IPv6 desabilitado.</em></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># --- Configurações net_mgmt ---</span>
nmcli con mod net_mgmt <span class="se">\</span>
  ipv4.method manual <span class="se">\</span>
  ipv4.addresses <span class="s2">"172.24.0.31/24"</span> <span class="se">\</span>
  ipv4.gateway <span class="s2">"172.24.0.1"</span> <span class="se">\</span>
  ipv4.route-metric <span class="s2">"50"</span> <span class="se">\</span>
  ipv4.dns <span class="s2">"1.1.1.1,8.8.8.8"</span> <span class="se">\</span>
  ipv6.method ignore <span class="se">\</span>
  ipv4.dns-search <span class="s2">"k8sbox.local"</span>

nmcli con reload <span class="o">&amp;&amp;</span> nmcli con up net_mgmt
</code></pre></div></div>

<p><em>Configura a eth1 como interface principal: IP manual, gateway apontando para o NAT do libvirt, métrica 50 (alta prioridade), DNS públicos e domínio de busca do cluster.</em></p>

<div class="markdown-alert markdown-alert-warning"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-alert" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg> Variáveis Dinâmicas no Repositório</p><p>No <code class="language-plaintext highlighter-rouge">Vagrantfile</code> do repositório parceiro, esses comandos utilizam interpolações dinâmicas do Ruby, como <code class="language-plaintext highlighter-rouge">#{specs["ip"]}</code> e <code class="language-plaintext highlighter-rouge">#{PROJETO}.local</code>. Aqui no artigo, colocamos os valores já resolvidos para a máquina <code class="language-plaintext highlighter-rouge">manager1</code> (<code class="language-plaintext highlighter-rouge">172.24.0.31/24</code> e <code class="language-plaintext highlighter-rouge">k8sbox.local</code>) como referência fixa para facilitar a visualização e a leitura dos comandos.</p>
</div>

<p>A jogada é a combinação de <code class="language-plaintext highlighter-rouge">never-default yes</code> na eth0 com <code class="language-plaintext highlighter-rouge">route-metric 50</code> na eth1. O <code class="language-plaintext highlighter-rouge">never-default</code> diz ao NetworkManager que a eth0 nunca deve ser usada como gateway padrão, independente do que o DHCP sugira. E a métrica 50 na eth1 garante que, se por qualquer motivo ambas as rotas existirem na tabela, a eth1 vence.</p>

<p>Antes da migração para o AlmaLinux 10, o provisionamento de rede usava referências estáticas como <code class="language-plaintext highlighter-rouge">nmcli con mod eth0</code> e criava conexões novas do zero com <code class="language-plaintext highlighter-rouge">nmcli con add</code>. A versão 10.1 mudou o esquema de nomenclatura das conexões e exigiu a abordagem dinâmica com detecção por dispositivo. Se você estiver portando o projeto para outra distro, esse é um dos trechos que pode precisar de ajustes.</p>

<p>E pra fechar o script de provisionamento: tem um trecho crucial que lida com o xerife do sistema operacional:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># --- Configura o SELinux como Permissive ---</span>
setenforce 0
<span class="nb">sed</span> <span class="nt">-i</span> <span class="s1">'s/^SELINUX=enforcing/SELINUX=permissive/'</span> /etc/selinux/config

<span class="c"># --- Corrige rotulos de SELinux dos arquivos criados pelo Vagrant ---</span>
restorecon <span class="nt">-R</span> /etc/NetworkManager/system-connections/
</code></pre></div></div>

<p><em>Coloca o SELinux em modo permissivo e restaura os rótulos dos arquivos de conexão do NetworkManager criados pelo Vagrant.</em></p>

<p>O Vagrant cria arquivos de configuração de rede dentro da VM durante o processo de boot, mas esses arquivos nascem sem os rótulos de SELinux corretos. O NetworkManager simplesmente recusa processar conexões com rótulos inválidos quando o SELinux está ativo. Esse <code class="language-plaintext highlighter-rouge">restorecon</code> corrige o problema silenciosamente.</p>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Mas Dudu, você não disse que não desabilitava o SELinux?!</p><p>Calma, jovem padawan, respira. A gente <strong>não</strong> desabilitou o SELinux. Desabilitar (<code class="language-plaintext highlighter-rouge">SELINUX=disabled</code>) é o que tutorial preguiçoso de internet faz para varrer sujeira pra baixo do tapete e fingir que segurança não existe. O que fizemos aqui foi colocar em modo permissivo (<code class="language-plaintext highlighter-rouge">permissive</code>). A diferença não é sutil: no modo permissivo o kernel não bloqueia as ações, mas continua de olhos bem abertos, auditando rigorosamente cada chamada no <code class="language-plaintext highlighter-rouge">audit.log</code> e dedurando exatamente o que o Kubernetes, o Kubelet ou o CNI fizerem que não está nas regras implementadas. O importante na engenharia de verdade não é desligar cegamente, mas botar no permissivo, ver o que está disparando de alertas, acomodar tudo nas regras e aí então fechar as portas virando a chave para <em>enforcing</em>. Teremos um post dedicado exclusivamente a compilar essa política customizada (<code class="language-plaintext highlighter-rouge">k8s-custom-selinux.te</code>) quando instalarmos o Kubelet.</p>
</div>

<h2 id="o-sistema-de-topologias-três-clusters-um-repositório">O sistema de topologias: três clusters, um repositório</h2>

<p>Na Parte 1, apresentamos as três configurações de cluster disponíveis: <code class="language-plaintext highlighter-rouge">nano</code>, <code class="language-plaintext highlighter-rouge">mini</code> e <code class="language-plaintext highlighter-rouge">completo</code>. Agora vamos ver como esse sistema funciona por dentro.</p>

<p>A engrenagem é simples e elegante: um arquivo de configuração (<code class="language-plaintext highlighter-rouge">config.mk</code>), um diretório com os inventários pré-definidos (<code class="language-plaintext highlighter-rouge">configs/</code>) e um symlink que conecta os dois.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>k8s-in-a-box/
├── config.mk                        &lt;-- Escolha da topologia
├── configs/
│   ├── hosts-nano.yml               &lt;-- 5 VMs (~6.5GB RAM)
│   ├── hosts-mini.yml               &lt;-- 6 VMs (~11GB RAM)
│   └── hosts-completo.yml           &lt;-- 9 VMs (~19.5GB RAM)
└── inventario/
    └── hosts.yml -&gt; ../configs/hosts-mini.yml  &lt;-- Symlink ativo
</code></pre></div></div>

<p>O <code class="language-plaintext highlighter-rouge">config.mk</code> é um fragmento de Makefile que define uma única variável:</p>

<div class="language-makefile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">CLUSTER</span> <span class="o">=</span> mini
</code></pre></div></div>

<p><em>O arquivo inteiro. Uma linha. Altera esse valor para <code class="language-plaintext highlighter-rouge">nano</code> ou <code class="language-plaintext highlighter-rouge">completo</code> e rode <code class="language-plaintext highlighter-rouge">make init</code>.</em></p>

<p>Quando você executa <code class="language-plaintext highlighter-rouge">make init</code>, o Makefile verifica se o arquivo de inventário correspondente existe e cria o symlink:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>make init
Configuração mini ativada
</code></pre></div></div>

<p>O <code class="language-plaintext highlighter-rouge">make status</code> mostra qual configuração está ativa em qualquer momento:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>make status
Configuração ativa: mini
</code></pre></div></div>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Cuidado com Sistemas de Arquivos sem suporte a Symlinks</p><p>O mecanismo de alternância de topologia baseia-se em links simbólicos do Linux (<code class="language-plaintext highlighter-rouge">ln -s</code>). Se você clonar o repositório em partições formatadas como NTFS ou FAT (cenário comum ao compartilhar pastas em dual-boot com Windows), a criação do symlink pode falhar silenciosamente. Mantenha o projeto sempre em partições Linux nativas (<code class="language-plaintext highlighter-rouge">ext4</code>, <code class="language-plaintext highlighter-rouge">btrfs</code>, <code class="language-plaintext highlighter-rouge">xfs</code>).</p>
</div>

<h3 id="anatomia-de-um-inventário">Anatomia de um inventário</h3>

<p>Cada arquivo de topologia é um inventário YAML do Ansible padrão. Vamos olhar o <code class="language-plaintext highlighter-rouge">hosts-mini.yml</code> como exemplo:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">all</span><span class="pi">:</span>
  <span class="na">vars</span><span class="pi">:</span>
    <span class="na">ansible_user</span><span class="pi">:</span> <span class="s">vagrant</span>

  <span class="na">children</span><span class="pi">:</span>
    <span class="na">loadbalancers</span><span class="pi">:</span>
      <span class="na">hosts</span><span class="pi">:</span>
        <span class="na">loadbalancer1</span><span class="pi">:</span>
          <span class="na">ansible_host</span><span class="pi">:</span> <span class="s">172.24.0.21</span>
          <span class="na">fqdn</span><span class="pi">:</span> <span class="s">loadbalancer1.k8sbox.local</span>
          <span class="na">memory</span><span class="pi">:</span> <span class="m">384</span>
          <span class="na">cpus</span><span class="pi">:</span> <span class="m">1</span>

    <span class="na">managers</span><span class="pi">:</span>
      <span class="na">hosts</span><span class="pi">:</span>
        <span class="na">manager1</span><span class="pi">:</span>
          <span class="na">ansible_host</span><span class="pi">:</span> <span class="s">172.24.0.31</span>
          <span class="na">fqdn</span><span class="pi">:</span> <span class="s">manager1.k8sbox.local</span>
          <span class="na">memory</span><span class="pi">:</span> <span class="m">3072</span>
          <span class="na">cpus</span><span class="pi">:</span> <span class="m">2</span>

    <span class="na">workers</span><span class="pi">:</span>
      <span class="na">hosts</span><span class="pi">:</span>
        <span class="na">worker1</span><span class="pi">:</span>
          <span class="na">ansible_host</span><span class="pi">:</span> <span class="s">172.24.0.41</span>
          <span class="na">fqdn</span><span class="pi">:</span> <span class="s">worker1.k8sbox.local</span>
          <span class="na">memory</span><span class="pi">:</span> <span class="m">3072</span>
          <span class="na">cpus</span><span class="pi">:</span> <span class="m">2</span>
        <span class="na">worker2</span><span class="pi">:</span>
          <span class="na">ansible_host</span><span class="pi">:</span> <span class="s">172.24.0.42</span>
          <span class="na">fqdn</span><span class="pi">:</span> <span class="s">worker2.k8sbox.local</span>
          <span class="na">memory</span><span class="pi">:</span> <span class="m">3072</span>
          <span class="na">cpus</span><span class="pi">:</span> <span class="m">2</span>

    <span class="na">clientes</span><span class="pi">:</span>
      <span class="na">hosts</span><span class="pi">:</span>
        <span class="na">kubox</span><span class="pi">:</span>
          <span class="na">ansible_host</span><span class="pi">:</span> <span class="s">172.24.0.254</span>
          <span class="na">fqdn</span><span class="pi">:</span> <span class="s">kubox.k8sbox.local</span>
          <span class="na">memory</span><span class="pi">:</span> <span class="m">1024</span>
          <span class="na">cpus</span><span class="pi">:</span> <span class="m">1</span>
          <span class="na">autostart</span><span class="pi">:</span> <span class="kc">false</span>
</code></pre></div></div>

<p><em>Topologia <code class="language-plaintext highlighter-rouge">mini</code>: 1 LB, 1 NFS, 1 manager, 2 workers e o bastion host. Total de 6 VMs.</em></p>

<p>Cada host carrega quatro campos obrigatórios: <code class="language-plaintext highlighter-rouge">ansible_host</code> (o IP na rede <code class="language-plaintext highlighter-rouge">172.24.0.0/24</code>), <code class="language-plaintext highlighter-rouge">fqdn</code> (o nome completo no domínio <code class="language-plaintext highlighter-rouge">k8sbox.local</code>), <code class="language-plaintext highlighter-rouge">memory</code> (RAM em MB) e <code class="language-plaintext highlighter-rouge">cpus</code> (número de vCPUs). Esses são os mesmos campos que o Vagrantfile lê para criar as VMs.</p>

<p>A estrutura de grupos (<code class="language-plaintext highlighter-rouge">loadbalancers</code>, <code class="language-plaintext highlighter-rouge">managers</code>, <code class="language-plaintext highlighter-rouge">workers</code>, <code class="language-plaintext highlighter-rouge">clientes</code>) não é cosmética. O Ansible usa esses grupos para decidir quais <em>roles</em> aplicar em quais máquinas. Os managers recebem <code class="language-plaintext highlighter-rouge">etcd</code> e <em>control plane</em>. Os workers recebem apenas o <code class="language-plaintext highlighter-rouge">kubelet</code>. Os load balancers recebem HAProxy e Keepalived. E o <code class="language-plaintext highlighter-rouge">kubox</code> recebe as ferramentas de operação.</p>

<p>Comparando as três topologias em números:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Configuração</th>
      <th style="text-align: center">LBs</th>
      <th style="text-align: center">Managers</th>
      <th style="text-align: center">Workers</th>
      <th style="text-align: center">NFS</th>
      <th style="text-align: center">kubox</th>
      <th style="text-align: center">Total VMs</th>
      <th style="text-align: center">RAM Estimada</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">nano</code></td>
      <td style="text-align: center">1</td>
      <td style="text-align: center">1</td>
      <td style="text-align: center">1</td>
      <td style="text-align: center">1</td>
      <td style="text-align: center">1</td>
      <td style="text-align: center">5</td>
      <td style="text-align: center">~6.5 GB</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">mini</code></td>
      <td style="text-align: center">1</td>
      <td style="text-align: center">1</td>
      <td style="text-align: center">2</td>
      <td style="text-align: center">1</td>
      <td style="text-align: center">1</td>
      <td style="text-align: center">6</td>
      <td style="text-align: center">~11 GB</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">completo</code></td>
      <td style="text-align: center">2</td>
      <td style="text-align: center">3</td>
      <td style="text-align: center">2</td>
      <td style="text-align: center">1</td>
      <td style="text-align: center">1</td>
      <td style="text-align: center">9</td>
      <td style="text-align: center">~19.5 GB</td>
    </tr>
  </tbody>
</table>

<p>A configuração <code class="language-plaintext highlighter-rouge">completo</code> é a única que viabiliza testes reais de alta disponibilidade: com três managers, o <code class="language-plaintext highlighter-rouge">etcd</code> forma <em>quorum</em> e tolera a perda de um nó. Com dois load balancers, o Keepalived faz failover do VIP. Mas ela exige quase 20 GB de RAM livre no host, o que nem todo mundo tem à disposição.</p>

<h2 id="o-makefile-como-interface-de-comandos">O Makefile como interface de comandos</h2>

<p>Rodar o Ansible manualmente para testar uma única <em>role</em> envolve montar um comando com variáveis de ambiente, caminho do arquivo de configuração, caminho do <em>playbook</em>, <em>flags</em> de <em>tags</em> e eventuais opções de verbosidade. É o tipo de coisa que você digita uma vez, erra um caractere no meio e perde cinco minutos descobrindo por quê. O Makefile <sup id="fnref:8"><a href="#fn:8" class="footnote" rel="footnote" role="doc-noteref">7</a></sup> do projeto encapsula toda essa complexidade em <em>targets</em> legíveis:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>════════════════════════════════════════════════════════════
  K8s in a Box - Makefile
════════════════════════════════════════════════════════════

Uso:
  make init                 # Ativa a configuração definida no config.mk
  make k8s-in-a-box         # Executa a esteira completa (ou 'make build')
  make status               # Mostra o tamanho de cluster ativo

Lista de targets:
  make init                  Ativa uma configuração de cluster
  make status                Mostra a configuração de cluster ativa
  make check-deps            Verifica se todas as dependências locais estão instaladas
  make up                    Sobe todas as VMs do cluster
  make down                  Interrompe todas as VMs do cluster
  make destroy               Exclui permanentemente todas as VMs
  make clean                 Deleta as VMs e limpa todos os artefatos
  make infra                 Prepara infraestrutura pré-k8s (PKI, SO, Balanceador, NFS)
  make control-plane         Instala o Kubernetes core (Kubelet, Etcd, Static Pods)
  make cluster               Executa o provisionamento do cluster completo
  make ops                   Provisiona completamente a VM de operações
  make addons                Instala ferramentas operacionais adicionais
  make exemplos              Faz deploy das aplicações de demonstração
  make snapshot              Cria uma snapshot única de todas as VMs
  make restore               Restaura o cluster para a última snapshot criada
</code></pre></div></div>

<p>Os targets são organizados em camadas que refletem a ordem de dependência do cluster:</p>

<ol>
  <li><strong>Configuração</strong>: <code class="language-plaintext highlighter-rouge">init</code>, <code class="language-plaintext highlighter-rouge">status</code>, <code class="language-plaintext highlighter-rouge">check-deps</code></li>
  <li><strong>Ciclo de vida das VMs</strong>: <code class="language-plaintext highlighter-rouge">up</code>, <code class="language-plaintext highlighter-rouge">down</code>, <code class="language-plaintext highlighter-rouge">destroy</code>, <code class="language-plaintext highlighter-rouge">clean</code></li>
  <li><strong>Provisionamento do Kubernetes</strong>: <code class="language-plaintext highlighter-rouge">infra</code>, <code class="language-plaintext highlighter-rouge">control-plane</code>, <code class="language-plaintext highlighter-rouge">cluster</code></li>
  <li><strong>Operações e rede</strong>: <code class="language-plaintext highlighter-rouge">ops</code>, <code class="language-plaintext highlighter-rouge">cni</code></li>
  <li><strong>Aplicações</strong>: <code class="language-plaintext highlighter-rouge">addons</code>, <code class="language-plaintext highlighter-rouge">exemplos</code></li>
  <li><strong>Snapshots</strong>: <code class="language-plaintext highlighter-rouge">snapshot</code>, <code class="language-plaintext highlighter-rouge">restore</code></li>
</ol>

<p>O target <code class="language-plaintext highlighter-rouge">k8s-in-a-box</code> (aliás de <code class="language-plaintext highlighter-rouge">build</code>) é o comando de esteira completa. Ele executa toda a cadeia na ordem correta:</p>

<div class="language-makefile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">build</span><span class="o">:</span> <span class="nf">up cluster ops addons exemplos</span>
</code></pre></div></div>

<p><em>Um target, cinco etapas em sequência: sobe VMs, provisiona cluster, configura operações e rede, instala addons e deploya exemplos.</em></p>

<h3 id="o-mecanismo-garante-config">O mecanismo garante-config</h3>

<p>Todo target que interage com o Vagrant ou o Ansible depende do target interno <code class="language-plaintext highlighter-rouge">garante-config</code>:</p>

<div class="language-makefile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">garante-config</span><span class="o">:</span>
	<span class="p">@</span><span class="nv">CURRENT_LINK</span><span class="o">=</span><span class="p">$$(</span><span class="nb">readlink</span> <span class="s2">"</span><span class="p">$(</span><span class="s2">CLUSTER_LINK</span><span class="p">)</span><span class="s2">"</span> 2&gt;/dev/null <span class="o">||</span> <span class="nb">echo</span> <span class="s2">""</span><span class="p">);</span> <span class="se">\</span>
	<span class="nv">EXPECTED_LINK</span><span class="o">=</span><span class="s2">"../</span><span class="p">$(</span><span class="s2">CLUSTER_SOURCE</span><span class="p">)</span><span class="s2">"</span><span class="p">;</span> <span class="se">\</span>
	<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$$</span><span class="s2">CURRENT_LINK"</span> <span class="o">!=</span> <span class="s2">"</span><span class="nv">$$</span><span class="s2">EXPECTED_LINK"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span> <span class="se">\</span>
		<span class="nb">echo</span> <span class="s2">"Sincronizando configuração do inventário para </span><span class="p">$(</span><span class="s2">CLUSTER</span><span class="p">)</span><span class="s2">..."</span><span class="p">;</span> <span class="se">\</span>
		<span class="p">$(</span>MAKE<span class="p">)</span> init<span class="p">;</span> <span class="se">\</span>
	<span class="k">fi</span>
</code></pre></div></div>

<p><em>Verifica se o symlink atual corresponde à topologia definida no <code class="language-plaintext highlighter-rouge">config.mk</code>. Se estiver dessincronizado, re-executa o <code class="language-plaintext highlighter-rouge">make init</code> automaticamente.</em></p>

<p>Esse mecanismo evita um cenário traiçoeiro: você edita o <code class="language-plaintext highlighter-rouge">config.mk</code> para mudar de <code class="language-plaintext highlighter-rouge">mini</code> para <code class="language-plaintext highlighter-rouge">completo</code>, esquece de rodar <code class="language-plaintext highlighter-rouge">make init</code>, e acaba provisionando o cluster com a topologia errada. Com o <code class="language-plaintext highlighter-rouge">garante-config</code>, esse desalinhamento é corrigido automaticamente antes de qualquer operação.</p>

<h3 id="snapshots-seu-seguro-contra-desastres">Snapshots: seu seguro contra desastres</h3>

<p>Quando o cluster está funcionando e você quer experimentar uma mudança arriscada (trocar o CNI, rotacionar certificados, testar uma versão diferente do Kubernetes), as <em>snapshots</em> salvam a sua vida:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>make snapshot    <span class="c"># Salva o estado atual de todas as VMs</span>
make restore     <span class="c"># Restaura todas as VMs para o último snapshot</span>
</code></pre></div></div>

<p>Cada <code class="language-plaintext highlighter-rouge">make snapshot</code> sobrescreve o snapshot anterior (não é um sistema de múltiplos pontos de restauração). A ideia é simples: antes de uma operação potencialmente destrutiva, salva. Se deu errado, restaura. Sem precisar reconstruir o cluster inteiro do zero, que dependendo da topologia leva de dez a trinta minutos.</p>

<h2 id="do-zero-ao-ambiente-funcional-o-fluxo-completo">Do zero ao ambiente funcional: o fluxo completo</h2>

<p>Pra quem quer ver tudo funcionando de uma vez, o caminho mínimo é:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/vndmtrx/k8s-in-a-box.git
<span class="nb">cd </span>k8s-in-a-box
make check-deps
make init
make up
</code></pre></div></div>

<p>O <code class="language-plaintext highlighter-rouge">make up</code> é onde a mágica visível acontece. Ele cria o diretório de artefatos, executa <code class="language-plaintext highlighter-rouge">vagrant up</code>, e o Vagrant cuida de absolutamente tudo sozinho: lê o inventário, cria as VMs no KVM, gera as chaves SSH, injeta a chave pública em cada máquina, configura as duas interfaces de rede via <code class="language-plaintext highlighter-rouge">nmcli</code> e corrige os rótulos de SELinux. Você não precisa entrar em nenhuma VM pra configurar nada. Quando o comando termina, as máquinas já estão acessíveis pelo Ansible.</p>

<p>O resultado final são VMs AlmaLinux 10 com:</p>

<ul>
  <li>Rede <code class="language-plaintext highlighter-rouge">eth0</code> (<code class="language-plaintext highlighter-rouge">net_vagrant</code>) rebaixada e sem rota padrão</li>
  <li>Rede <code class="language-plaintext highlighter-rouge">eth1</code> (<code class="language-plaintext highlighter-rouge">net_mgmt</code>) como interface principal com IP estático e gateway</li>
  <li>SELinux em modo permissivo (estágio transitório de auditoria para compilar as regras antes do <em>enforcing</em>)</li>
  <li>Chave SSH do projeto no <code class="language-plaintext highlighter-rouge">authorized_keys</code> (gerada e injetada automaticamente)</li>
  <li>Conectividade plena entre todas as VMs na faixa <code class="language-plaintext highlighter-rouge">172.24.0.0/24</code></li>
  <li>Ansible pronto para se conectar via o <code class="language-plaintext highlighter-rouge">ssh_config</code> do projeto</li>
</ul>

<p>Nenhum componente Kubernetes instalado. Nenhuma <em>role</em> Ansible executada. Apenas máquinas virtuais com o sistema operacional base, rede configurada e prontas para receber o cluster.</p>

<h2 id="diagnóstico-e-troubleshooting">Diagnóstico e Troubleshooting</h2>

<p>Se algo der errado no processo de subida das VMs, aqui estão os comandos de diagnóstico essenciais:</p>

<h3 id="status-das-vms">Status das VMs</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vagrant status
</code></pre></div></div>

<p><em>Mostra o estado atual de cada VM (running, shutoff, not created).</em></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>virsh list <span class="nt">--all</span>
</code></pre></div></div>

<p><em>Lista todas as VMs no hipervisor KVM, incluindo as que estão desligadas. Útil quando o Vagrant perde a referência para uma VM.</em></p>

<h3 id="verificação-de-rede-dentro-das-vms">Verificação de rede dentro das VMs</h3>

<p>Conecte-se a uma VM e inspecione a configuração de rede:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vagrant ssh manager1
</code></pre></div></div>

<p>Dentro da VM:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nmcli con show
</code></pre></div></div>

<p><em>Lista todas as conexões do NetworkManager. Você deve ver <code class="language-plaintext highlighter-rouge">net_vagrant</code> (eth0) e <code class="language-plaintext highlighter-rouge">net_mgmt</code> (eth1) com os nomes que definimos.</em></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nmcli con show net_mgmt | <span class="nb">grep</span> <span class="nt">-E</span> <span class="s2">"ipv4</span><span class="se">\.</span><span class="s2">(addresses|gateway|route-metric|dns)"</span>
</code></pre></div></div>

<p><em>Verifica se a eth1 está configurada corretamente com o IP, gateway, métrica e DNS.</em></p>

<h3 id="teste-de-conectividade-entre-vms">Teste de conectividade entre VMs</h3>

<p>De qualquer VM, teste o acesso a outra:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ping <span class="nt">-c</span> 3 172.24.0.31    <span class="c"># manager1</span>
ping <span class="nt">-c</span> 3 172.24.0.41    <span class="c"># worker1</span>
ping <span class="nt">-c</span> 3 172.24.0.21    <span class="c"># loadbalancer1</span>
</code></pre></div></div>

<p><em>Se algum ping falhar, verifique se a rede <code class="language-plaintext highlighter-rouge">k8sbox_mgmt</code> existe no libvirt com <code class="language-plaintext highlighter-rouge">virsh net-list --all</code>.</em></p>

<h3 id="problemas-comuns">Problemas comuns</h3>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Sintoma</th>
      <th style="text-align: left">Causa Provável</th>
      <th style="text-align: left">Solução</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">vagrant up</code> pede senha de root</td>
      <td style="text-align: left">Usuário não está nos grupos <code class="language-plaintext highlighter-rouge">libvirt</code>/<code class="language-plaintext highlighter-rouge">kvm</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">sudo usermod -a -G libvirt,kvm $(whoami)</code> + logout/login</td>
    </tr>
    <tr>
      <td style="text-align: left">VM sobe mas não pinga outra VM</td>
      <td style="text-align: left">Rede <code class="language-plaintext highlighter-rouge">k8sbox_mgmt</code> não foi criada</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">virsh net-list --all</code> e <code class="language-plaintext highlighter-rouge">vagrant destroy -f &amp;&amp; vagrant up</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">nmcli con show</code> mostra nomes genéricos</td>
      <td style="text-align: left">Script de provisionamento falhou</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">vagrant provision &lt;nome-vm&gt;</code> para re-executar</td>
    </tr>
    <tr>
      <td style="text-align: left">Acesso à internet funciona via eth0 mas não via eth1</td>
      <td style="text-align: left">Gateway da eth1 não configurado</td>
      <td style="text-align: left">Verificar se <code class="language-plaintext highlighter-rouge">172.24.0.1</code> responde com <code class="language-plaintext highlighter-rouge">ping 172.24.0.1</code></td>
    </tr>
  </tbody>
</table>

<h2 id="conclusão">Conclusão</h2>

<p>Se tudo correu bem, neste momento você tem um ambiente de laboratório completo rodando na sua máquina: cinco a nove VMs AlmaLinux 10 interconectadas numa rede privada isolada, com IPs estáticos previsíveis, chaves SSH distribuídas e uma interface de Makefile que simplifica tudo.</p>

<p>Não tem Kubernetes instalado ainda. Não tem certificado gerado. Não tem nenhum serviço rodando. E isso é proposital. A ideia de separar a infraestrutura do hospedeiro do provisionamento do cluster permite que você destrua e recrie as VMs quantas vezes precisar sem perder tempo reinstalando ferramentas no seu sistema operacional.</p>

<p>No próximo post (<a href="/posts/k8sbox-inventario-ansible/">Parte 3: O Inventário Ansible e as Variáveis do Cluster</a>), vamos mergulhar na outra metade do cérebro do projeto: o arquivo <code class="language-plaintext highlighter-rouge">all.yml</code> onde todas as variáveis do cluster moram, o <code class="language-plaintext highlighter-rouge">.ansible.cfg</code> com suas decisões de configuração, e como uma única mudança de variável propaga por todo o ecossistema de <em>roles</em> e <em>playbooks</em>. É onde o Ansible deixa de ser apenas uma ferramenta de automação e vira o mapa completo do cluster que estamos construindo.</p>

<h2 id="referências">Referências</h2>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:5">
      <p><strong>LibVirt - The virtualization API</strong> {<em>libvirt.org</em>} (<a href="https://libvirt.org/">Link</a>) <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3">
      <p><strong>KVM - Kernel-based Virtual Machine</strong> {<em>Linux Kernel Documentation</em>} (<a href="https://www.linux-kvm.org/">Link</a>) <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4">
      <p><strong>Vagrant by HashiCorp</strong> {<em>HashiCorp</em>} (<a href="https://www.vagrantup.com/">Link</a>) <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:6">
      <p><strong>vagrant-libvirt</strong> {<em>GitHub</em>} (<a href="https://github.com/vagrant-libvirt/vagrant-libvirt">Link</a>) <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p><strong>AlmaLinux OS</strong> {<em>AlmaLinux</em>} (<a href="https://almalinux.org/">Link</a>) <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:7">
      <p><strong>NetworkManager Reference Manual</strong> {<em>freedesktop.org</em>} (<a href="https://networkmanager.dev/docs/api/latest/nmcli.html">Link</a>) <a href="#fnref:7" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:8">
      <p><strong>GNU Make Manual</strong> {<em>Free Software Foundation</em>} (<a href="https://www.gnu.org/software/make/manual/">Link</a>) <a href="#fnref:8" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>[&quot;Eduardo N. S. R.&quot;]</name></author><category term="Tutoriais" /><category term="Kubernetes" /><category term="Ansible" /><category term="DevOps" /><category term="Infraestrutura" /><summary type="html"><![CDATA[Na Parte 1 desta série, gastamos o post inteiro falando sobre por que construir um cluster Kubernetes na mão e o que pretendemos montar. Conceitos, diagramas, motivação. Agora é hora de sujar as mãos de verdade: vamos configurar o computador hospedeiro, dissecar linha por linha como o Vagrant lê nosso inventário Ansible para criar máquinas virtuais e resolver o problema mais traiçoeiro de qualquer laboratório local de Kubernetes: fazer a rede funcionar direito.]]></summary></entry><entry><title type="html">Ensaio sobre dois cegos</title><link href="https://vndmtrx.github.io/posts/ensaio-sobre-dois-cegos/" rel="alternate" type="text/html" title="Ensaio sobre dois cegos" /><published>2026-09-04T06:53:00+00:00</published><updated>2026-09-04T06:53:00+00:00</updated><id>https://vndmtrx.github.io/posts/ensaio-sobre-dois-cegos</id><content type="html" xml:base="https://vndmtrx.github.io/posts/ensaio-sobre-dois-cegos/"><![CDATA[<p>Vamos tirar o elefante da sala logo no primeiro parágrafo: sim, eu uso inteligência artificial para escrever. E sim, este ensaio que você começou a ler foi parido a quatro mãos em uma conversa direta entre mim e um modelo de linguagem. Podem rasgar as vestes, acionar a inquisição dos puristas da folha em branco ou me denunciar para a polícia da literatura orgânica: a verdade é que não tenho a menor vergonha disso. Fingir que eu psicografei cada vírgula sozinho em um transe solitário diante de uma tela escura para manter pose de gênio romântico seria uma hipocrisia tão patética que nem o Linkedin teria coragem de publicar.</p>

<p>Existe uma vergonha velada na internet quando o assunto é usar IA para escrever. De um lado, a seita dos puristas: gente que jura de pés juntos que cada frase brotou de um nirvana diante da tela, como se fossem poetas do século XIX sofrendo por amor (quando na verdade só têm vergonha de admitir que usam corretor ortográfico até pro "bom dia" no chat corporativo). Do outro, a horda do prompt mágico: os gênios da automação que colaram um comando genérico, ou construíram um prompt altamente complexo e específico, copiaram o vômito sintático da máquina e publicaram achando que inventaram a roda.</p>

<p>Os dois lados compartilham a mesmíssima ilusão: acham que escrever com IA é apertar um botão e esperar o milagre pronto <sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>.</p>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Mea culpa</p><p>Eu já passei por todas as fases do luto na escrita com IA. Negação (<em>"isso é modinha, não vai durar"</em>), raiva (<em>"se essa coisa cuspir mais um 'vale ressaltar' eu jogo o notebook pela janela"</em>), barganha (<em>"só vou usar pra revisar gramática, prometo"</em>), depressão (<em>"meu texto tá saindo pior que o da máquina"</em>) e aceitação (<em>"ok, vamos ver como essa conversa funciona de verdade"</em>). O tiro aqui não é em quem usa ou em quem ainda está transitando entre as fases. É na demagogia de quem finge que nunca precisou de ajuda e na hipocrisia de quem finge que colar output bruto é a mesma coisa que escrever.</p>
</div>

<p>Quem realmente senta para escrever de verdade sabe que não é nada disso. Não é automação; é pura briga de foice no escuro. É uma sessão de <em>pair writing</em> regada a atrito, onde o texto só sai porque duas entidades completamente cegas de seus próprios vícios entram em rota de colisão constante: uma dinâmica que tem muito menos a ver com "produtividade corporativa" e muito mais com a teoria dialógica de Bakhtin.</p>

<h2 id="a-dança-das-duas-cegueiras">A dança das duas cegueiras</h2>

<p>O título deste post não é metáfora fofa (Saramago que o diga) <sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>. Escrever a quatro mãos com uma LLM é basicamente colocar dois cegos para discutir a cor da parede de uma casa:</p>

<p>A primeira é a <strong>cegueira da máquina</strong>. O modelo tem bilhões de parâmetros decorados, lê uma enciclopédia em três milissegundos, mas tem a sensibilidade de uma britadeira. Ele não tem corpo, nunca passou pela humilhação de reprovar numa prova de lógica e não faz a menor ideia da diferença entre uma ironia ácida e uma piada de tiozão no churrasco de domingo. Se você deixar a IA solta, ela vai invariavelmente produzir um texto engomado, prolixo e com aquele tom insuportável de coach motivacional de firma.</p>

<p>A segunda é a <strong>cegueira do autor</strong>. E aqui entra a minha própria incompetência: quando estou escrevendo, perco completamente o senso de proporção. Fico apegado àquela piada ruim que achei genial às duas da manhã, empilho analogias (entre parênteses) como se não houvesse amanhã, repito as mesmas três palavras de efeito em todo santo parágrafo e tenho a ilusão de que o leitor tem a obrigação moral de acompanhar as voltas do meu labirinto mental.</p>

<p>A mágica não acontece quando um cede ao outro. Acontece quando a gente se cansa das tosquices mútuas e começa a cortar excessos.</p>

<h2 id="bakhtin-e-a-palavra-meio-alheia">Bakhtin e a palavra meio alheia</h2>

<p>O filósofo russo Mikhail Bakhtin tinha uma tese que resume perfeitamente essa bagunça <sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>:</p>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> The Dialogic Imagination: Four Essays</p><p><em>"The word in language is half someone else's. It becomes 'one's own' only when the speaker populates it with his own intention, his own accent, when he appropriates the word, adapting it to his own semantic and expressive intention. … it exists in other people's mouths, in other people's contexts, serving other people's intentions: it is from there that one must take the word, and make it one's own."</em></p>
</div>

<p>Para ele, a linguagem nunca é 100% nossa desde o berço. A palavra é sempre "meio alheia" até o momento em que você a ocupa com a sua intenção, o seu sotaque e o seu contexto.</p>

<p>Na prática, isso destrói tanto a pose do purista quanto a preguiça de quem copia e cola.</p>

<p>Eu não abro o chat pedindo "escreva um artigo sobre tal coisa". Fazer isso é pedir para receber um monte de clichês plastificados que parecem gerados para agradar algoritmo de busca. Mas também não tenho a frescura de fingir que nunca travo numa frase. Às vezes estou no meio de um raciocínio truncado e solto na lata: <em>"como você desenrolaria esse parágrafo?"</em>. A máquina me cospe três variações, eu leio a primeira com cara de desdém, acho a segunda intragável, pesco uma única conjunção que prestou na terceira e reescrevo tudo por cima com a minha própria voz (ou, às vezes, jogo de volta na mesa pedindo para ela reescrever o parágrafo em volta daquela ideia).</p>

<p>Mas a direção fundamental é sempre minha. Eu não peço pra máquina criar: eu crio. Depois largo o rascunho na frente dela e pergunto onde o texto perdeu o fio, onde a analogia forçou, onde o parágrafo ficou pesado. A dinâmica se parece com ter um secretário que domina o lado operacional da escrita: alguém que percebe quando uma transição engasgou, que avisa quando repeti a mesma palavra três vezes, que sugere uma conjunção melhor quando a frase empacou. Mas quem decide o que o texto vai dizer, qual posição defender, qual piada fica e qual analogia morre: sou eu. A máquina ajuda a fechar o texto depois que eu já decidi o que ele precisa ser.</p>

<p>O processo vira um <em>sparring</em> cínico:</p>
<ul>
  <li>Eu testo analogias e pergunto se aquilo faz sentido ou se é só loucura da minha cabeça;</li>
  <li>Peço para a máquina simular o leitor chato: <em>"qual contra-argumento óbvio alguém na defensiva vai jogar na minha cara aqui?"</em>;</li>
  <li>Peço diagnósticos secos: <em>"o que ficou forçado, palestrinha demais ou com cara de redação nota mil do ENEM?"</em>.</li>
</ul>

<h2 id="o-atrito-dialógico-na-prática">O atrito dialógico na prática</h2>

<p>Isso só funciona porque eu não trato a IA como um script de autocompletar e muito menos como uma divindade consciente. Trato como uma entidade interlocutora: alguém que está do outro lado do ringue pronto para tomar um jab retórico e também devolver um diagnóstico direto sem melindre.</p>

<p>A dinâmica é uma via de mão dupla de desconfiança mútua.</p>

<p>De um lado, o que a IA aponta sobre os meus cacoetes:</p>
<ul>
  <li><strong>O espelho do ritmo:</strong> Avisa que empilhei analogias até o leitor perder o ar, que usei o mesmo adjetivo dramático duas vezes na mesma página ou que uma explicação pareceu um livro didático enciclopédico em vez de um relato honesto;</li>
  <li><strong>O teste de estresse:</strong> Levanta furos lógicos e contra-argumentos que eu ignorei porque estava apaixonado demais pela minha própria tese;</li>
  <li><strong>A elasticidade:</strong> Oferece rapidamente caminhos sintáticos diferentes quando uma transição fica estranha.</li>
</ul>

<p>Do outro lado, o que eu faço como a vigilância sanitária do texto:</p>
<ul>
  <li><strong>Podo o verniz corporativo:</strong> Corto na hora a tentação da máquina de soar como texto de Linkedin ou de meter metáforas quinta-série;</li>
  <li><strong>Puxo a orelha da superficialidade:</strong> Se o modelo traz um consenso frouxo, eu jogo autores reais e contraexemplos na roda para forçar a discussão a descer um degrau;</li>
  <li><strong>Auditoria implacável de fontes:</strong> Cobro a validação exata de cada referência para garantir que ela não inventou uma citação com cara de verdade absoluta.</li>
</ul>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Como a peneira funciona</p><p>Não é instinto, é método. Há um conjunto explícito de restrições que funciona como peneira dupla: de um lado, regras que domesticam os meus vícios de escrita; do outro, regras que sanitizam os outputs da máquina. Mas esse conjunto não nasceu pronto. Ele foi construído a quatro mãos, regra por regra, conversa por conversa. O ciclo é sempre o mesmo: eu leio uma saída, faço cara de nojo e solto <em>"isso aqui nunca sairia da minha boca"</em>. A IA propõe uma restrição. Eu acho genérica demais, ajusto, testo no próximo parágrafo, vejo que ainda vazou um <em>"vale ressaltar"</em> e aperto o cerco. Na conversa seguinte, ela flagra um cacoete meu que tinha passado batido, e as regras crescem de novo. Travessões, expressões vazias, adjetivos pomposos (<em>"magistral"</em>, <em>"deveras"</em>): cada item dessa lista tem um histórico de atrito. Codifiquei o nojo em código, e a cada sessão de escrita o filtro fica mais afiado.</p>
</div>

<p>Identificado o problema de um lado ou de outro, a gente volta para a mesa de cirurgia textual:</p>
<ol>
  <li><strong>O diagnóstico:</strong> Apontamos onde o texto virou lixo, perdeu o ritmo ou ficou pedante;</li>
  <li><strong>O debate:</strong> Discutimos abordagens diferentes para resolver aquele impasse;</li>
  <li><strong>A navalha:</strong> Eu reescrevo sem dó, garantindo que o sotaque e o sangue do autor fiquem na página.</li>
</ol>

<p>Um parágrafo que presta nunca sai de um comando milagroso. Ele sai da paciência cínica de rejeitar versões ruins até a frase parar em pé.</p>

<p>E aqui mora um detalhe que a turma do "a IA escreve tudo" ignora: o processo é lento. Cada seção de um post passa por múltiplas rodadas de reescrita. A maioria das sugestões que a máquina oferece morre na mesa de edição, não porque estejam gramaticalmente erradas, mas porque não soam como eu. O filtro final nunca é "está correto?". É "eu escreveria isso?". Se a frase não sobrevive a esse teste, não importa o quão bem construída ela esteja: vai pro lixo.</p>

<h2 id="onde-mora-a-autoria">Onde mora a autoria?</h2>

<p>A discussão sobre "a IA vai matar a autoria" é de uma preguiça monumental. Ela assume que escrever é apenas a atividade física de bater dedos descontroladamente num teclado. Se fosse só isso, um gerador de lero-lero programado nos anos 90 seria autor de enormes <em>best-sellers</em>.</p>

<p>A verdade é que esse processo escancara uma realidade desconfortável: a máquina tem o viés probabilístico dela (aquela atração fatal pela média insossa e pelo clichê), mas eu tenho o viés das vozes da minha própria cabeça (com toda a dissonância cognitiva, rancores teóricos e certezas infundadas que acumulei pela vida). Nenhum dos dois sistemas é confiável sozinho.</p>

<p>A autoria não mora na digitação; mora no <strong>discernimento estético</strong>. Mora no papel de ser o árbitro desse confronto: na recusa obstinada, no mau humor de ler uma frase empolada e dizer "isso aqui tá uma bosta", e na decisão consciente sobre cada vírgula que tem permissão para ficar no texto final (ok, não tão extremo, mas às vezes acontece).</p>

<p>E isso vale para todos os tipos de conteúdo que eu produzo. O mesmo método sustenta desde posts técnicos com código até posts onde eu destilo meu ranço com alguma situação ou ferramenta. A máquina ajuda a manter consistência terminológica, a checar se um bloco de código conversa com o que foi explicado antes, a organizar uma sequência didática. A decisão final sobre o que discutir e o que defender continua sendo minha. A ferramenta muda, o papel de cada um na mesa não.</p>

<div class="markdown-alert markdown-alert-tip"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-light-bulb" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"></path></svg> A sexta fase</p><p>Dizem que existe uma sexta fase do luto: encontrar significado. Eu não sei se encontrei significado nisso tudo. Mas encontrei um jeito de escrever que me obriga a ser mais honesto do que eu seria sozinho, me forçando a encarar os vícios que eu fingiria não ter. Se isso conta, fico com a sexta fase.</p>
</div>

<p>No fim das contas, continuamos dois cegos tateando a linguagem, talvez buscando significado onde parece só ter atrito <sup id="fnref:4"><a href="#fn:4" class="footnote" rel="footnote" role="doc-noteref">4</a></sup>. Mas a graça é que, batendo uma cegueira contra a outra com a dose certa de deboche e ceticismo, a gente consegue, pelo menos por alguns parágrafos, fazer o texto enxergar.</p>

<h2 id="referências">Referências</h2>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:3">
      <p><strong>Modelo do sofrimento de Kübler-Ross</strong> {<em>Elisabeth Kübler-Ross, 1969</em>} (<a href="https://pt.wikipedia.org/wiki/Modelo_de_K%C3%BCbler-Ross">Wikipédia</a>) <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p><strong>Ensaio sobre a cegueira</strong> {<em>José Saramago, 1995</em>} (<a href="https://www.amazon.com.br/Ensaio-sobre-cegueira-Edi%C3%A7%C3%A3o-anos-ebook/dp/B0GKCC7T6L">Companhia das Letras</a>) <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:1">
      <p><strong>The Dialogic Imagination: Four Essays</strong> {<em>Bakhtin, M. M.</em>} (<a href="https://www.amazon.com.br/Dialogic-Imagination-Essays-University-English-ebook/dp/B003HS5HMI">University of Texas Press</a>) <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4">
      <p><strong>Finding Meaning: The Sixth Stage of Grief</strong> {<em>David Kessler, 2019</em>} (<a href="https://www.amazon.com.br/Finding-Meaning-Sixth-Stage-English-ebook/dp/B07P5GCND6">Scribner</a>) <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>[&quot;Eduardo N. S. R.&quot;]</name></author><category term="Ensaios" /><category term="Inteligência Artificial" /><category term="Escrita" /><category term="Filosofia" /><category term="Opinião" /><summary type="html"><![CDATA[Vamos tirar o elefante da sala logo no primeiro parágrafo: sim, eu uso inteligência artificial para escrever. E sim, este ensaio que você começou a ler foi parido a quatro mãos em uma conversa direta entre mim e um modelo de linguagem. Podem rasgar as vestes, acionar a inquisição dos puristas da folha em branco ou me denunciar para a polícia da literatura orgânica: a verdade é que não tenho a menor vergonha disso. Fingir que eu psicografei cada vírgula sozinho em um transe solitário diante de uma tela escura para manter pose de gênio romântico seria uma hipocrisia tão patética que nem o Linkedin teria coragem de publicar.]]></summary></entry><entry><title type="html">Vinte Minutos para Explicar Meio Século de Aprendizado de Máquina</title><link href="https://vndmtrx.github.io/posts/banca-ia-aprendizado-supervisionado/" rel="alternate" type="text/html" title="Vinte Minutos para Explicar Meio Século de Aprendizado de Máquina" /><published>2026-09-01T14:37:00+00:00</published><updated>2026-09-01T14:37:00+00:00</updated><id>https://vndmtrx.github.io/posts/banca-ia-aprendizado-supervisionado</id><content type="html" xml:base="https://vndmtrx.github.io/posts/banca-ia-aprendizado-supervisionado/"><![CDATA[<p>Botar um cronômetro de vinte minutos na sua frente e ter que explicar aprendizado supervisionado e aprendizado por reforço é um exercício de síntese homérico, na melhor definição que consigo pensar agora (e olha que eu gosto de uma boa dose de masoquismo). Estamos falando de duas áreas monumentais da computação que consumiram décadas de pesquisas, acumularam centenas de variações algorítmicas e hoje sustentam quase tudo o que chamamos de IA moderna.</p>

<p>Na aula dos próximos dias, o passador de slides estará na minha mão. A plateia é a mesma turma que vem acompanhando os seminários da disciplina, e o desafio é passar pelos capítulos 19 e 23 do Russell e Norvig <sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>, costurando com os fundamentos clássicos do livro de Sutton e Barto <sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> (que eu mesmo coloquei porque aparentemente não tenho amor à vida), sem parecer um papagaio recitando índices de livros ou um leitor de tela acelerado em duas vezes.</p>

<p>O perigo clássico desse tipo de apresentação é cair na tentação de colocar todas as fórmulas no slide para parecer erudito. O resultado disso todo mundo conhece: o apresentador se perde nos subscritos das matrizes, o tempo estoura no terceiro slide e ninguém na sala consegue pescar o fio da meada.</p>

<p>Para colocar ordem na minha própria cabeça e criar uma bússola de apresentação, organizei estas notas. Elas funcionam como o meu roteiro de estudo e reflexão: a história técnica de como ensinamos máquinas a reconhecer padrões em dados do passado e como damos autonomia para que elas aprendam a agir no futuro.</p>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg data-component="Octicon" class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Disclaimer</p><p>Este texto é o meu ensaio pessoal de preparação para a arguição da banca. O foco aqui não é a demonstração formal de teoremas, mas a intuição por trás das decisões de engenharia e os motivos que levaram a comunidade a criar cada uma dessas abordagens.</p>
</div>

<details class="toc-box">
  <summary><strong>Sumário &amp; Índice de Seções (TOC)</strong></summary>
<ul id="markdown-toc">
  <li><a href="#o-desafio-da-síntese-escolher-as-brigas-certas" id="markdown-toc-o-desafio-da-síntese-escolher-as-brigas-certas">O desafio da síntese: escolher as brigas certas</a></li>
  <li><a href="#o-problema-da-indução-caçando-funções-no-escuro" id="markdown-toc-o-problema-da-indução-caçando-funções-no-escuro">O problema da indução: caçando funções no escuro</a>    <ul>
      <li><a href="#viés-contra-variância-o-equilíbrio-entre-a-teimosia-e-o-exagero" id="markdown-toc-viés-contra-variância-o-equilíbrio-entre-a-teimosia-e-o-exagero">Viés contra variância: o equilíbrio entre a teimosia e o exagero</a></li>
      <li><a href="#o-papel-das-perdas-e-a-geometria-da-regularização" id="markdown-toc-o-papel-das-perdas-e-a-geometria-da-regularização">O papel das perdas e a geometria da regularização</a></li>
    </ul>
  </li>
  <li><a href="#quatro-pilares-do-aprendizado-supervisionado" id="markdown-toc-quatro-pilares-do-aprendizado-supervisionado">Quatro pilares do aprendizado supervisionado</a>    <ul>
      <li><a href="#1-árvores-de-decisão-e-a-medida-da-desordem" id="markdown-toc-1-árvores-de-decisão-e-a-medida-da-desordem">1. Árvores de decisão e a medida da desordem</a></li>
      <li><a href="#2-modelos-lineares-do-perceptron-à-regressão-logística" id="markdown-toc-2-modelos-lineares-do-perceptron-à-regressão-logística">2. Modelos lineares: do Perceptron à Regressão Logística</a></li>
      <li><a href="#3-support-vector-machines-svm-e-o-salto-dimensional" id="markdown-toc-3-support-vector-machines-svm-e-o-salto-dimensional">3. Support Vector Machines (SVM) e o salto dimensional</a></li>
      <li><a href="#4-métodos-de-ensemble-a-força-da-combinação" id="markdown-toc-4-métodos-de-ensemble-a-força-da-combinação">4. Métodos de Ensemble: a força da combinação</a></li>
    </ul>
  </li>
  <li><a href="#a-fronteira-por-que-o-aprendizado-por-reforço-é-diferente" id="markdown-toc-a-fronteira-por-que-o-aprendizado-por-reforço-é-diferente">A fronteira: por que o aprendizado por reforço é diferente</a></li>
  <li><a href="#rl-passivo-estimando-valores-no-tempo" id="markdown-toc-rl-passivo-estimando-valores-no-tempo">RL Passivo: estimando valores no tempo</a></li>
  <li><a href="#rl-ativo-o-controle-e-o-equilíbrio-da-exploração" id="markdown-toc-rl-ativo-o-controle-e-o-equilíbrio-da-exploração">RL Ativo: o controle e o equilíbrio da exploração</a>    <ul>
      <li><a href="#q-learning-contra-sarsa-duas-filosofias-de-controle" id="markdown-toc-q-learning-contra-sarsa-duas-filosofias-de-controle">Q-Learning contra SARSA: duas filosofias de controle</a></li>
    </ul>
  </li>
  <li><a href="#a-tríade-mortal-e-a-escalabilidade-no-mundo-real" id="markdown-toc-a-tríade-mortal-e-a-escalabilidade-no-mundo-real">A Tríade Mortal e a escalabilidade no mundo real</a></li>
  <li><a href="#roteiro-cronometrado-para-a-apresentação" id="markdown-toc-roteiro-cronometrado-para-a-apresentação">Roteiro cronometrado para a apresentação</a></li>
  <li><a href="#considerações-finais" id="markdown-toc-considerações-finais">Considerações finais</a></li>
  <li><a href="#referências" id="markdown-toc-referências">Referências</a></li>
</ul>

</details>

<h2 id="o-desafio-da-síntese-escolher-as-brigas-certas">O desafio da síntese: escolher as brigas certas</h2>

<p>Vinte minutos passam como um estalar de dedos quando você está falando em público. Se você gastar cinco minutos deduzindo uma regressão linear, faltará tempo para explicar por que o Q-Learning se diferencia do SARSA ou como o truque do kernel salvou as máquinas de vetores de suporte.</p>

<p>O segredo de uma apresentação curta não é correr com a fala, mas ter clareza absoluta sobre o papel de cada bloco:</p>

<ol>
  <li><strong>A base conceitual:</strong> O que é indução estatística e por que o equilíbrio entre viés e variância governa tudo.</li>
  <li><strong>O cardápio supervisionado:</strong> Como árvores, modelos lineares, SVMs e comitês de modelos resolvem problemas com dados rotulados.</li>
  <li><strong>A virada para o reforço:</strong> Por que a supervisão direta falha em problemas sequenciais e como a interação com o ambiente substitui o professor.</li>
  <li><strong>O controle ótimo e a generalização:</strong> Como estimar utilidades no tempo e o que acontece quando colocamos redes neurais dentro do loop de decisão.</li>
</ol>

<p>Com essa estrutura na cabeça, fica muito mais fácil cortar o excesso de detalhes sem perder a espinha dorsal do assunto.</p>

<h2 id="o-problema-da-indução-caçando-funções-no-escuro">O problema da indução: caçando funções no escuro</h2>

<p>Todo aprendizado supervisionado parte de uma premissa simples: você tem um conjunto de treino com pares de entrada e saída <code class="language-plaintext highlighter-rouge">(x_i, y_i)</code>. Alguma regra desconhecida da natureza gerou essas respostas, e o seu objetivo é encontrar uma função candidata <code class="language-plaintext highlighter-rouge">h</code> (a hipótese) que aproxime essa regra para qualquer dado que aparecer depois.</p>

<p>A primeira bifurcação clássica desse caminho:</p>

<ul>
  <li><strong>Classificação:</strong> O objetivo é prever uma categoria discreta (identificar se uma transação bancária é legítima ou fraude, se uma imagem contém um gato ou um cachorro).</li>
  <li><strong>Regressão:</strong> O objetivo é prever um valor numérico contínuo (estimar o tempo de resposta de um servidor com base na carga, prever o consumo de memória de um processo).</li>
</ul>

<p>Toda a matemática repousa sobre a suposição de estacionariedade (<code class="language-plaintext highlighter-rouge">i.i.d.</code>): assume-se que os dados futuros seguirão a mesma distribuição probabilística dos dados passados. No mundo real da infraestrutura e dos sistemas isso é uma meia-verdade constante (o tráfego muda, o perfil dos usuários evolui, o comportamento de ataque se transforma), mas sem essa âncora inicial, nenhum modelo consegue sair do lugar.</p>

<h3 id="viés-contra-variância-o-equilíbrio-entre-a-teimosia-e-o-exagero">Viés contra variância: o equilíbrio entre a teimosia e o exagero</h3>

<p>Qualquer modelo de aprendizado supervisionado vive sob a tensão de duas forças opostas:</p>

<ul>
  <li><strong>Viés (<em>Bias</em>):</strong> Representa a rigidez do modelo. Se o espaço de hipóteses for limitado demais (como insistir em passar uma linha reta sobre dados que descrevem uma curva acentuada), o modelo falha em capturar o padrão essencial. É o chamado <em>underfitting</em>.</li>
  <li><strong>Variância:</strong> Representa a sensibilidade excessiva aos dados de treino. Se o modelo for flexível demais, ele passa a memorizar o ruído estatístico e as peculiaridades daquela amostra específica. Ele acerta tudo no treino, mas desaba diante de qualquer dado inédito. É o clássico <em>overfitting</em>.</li>
</ul>

<p>A Navalha de Ockham entra aqui como princípio orientador: entre duas explicações que justificam igualmente bem as observações, a mais simples costuma ser a mais confiável para generalizar.</p>

<h3 id="o-papel-das-perdas-e-a-geometria-da-regularização">O papel das perdas e a geometria da regularização</h3>

<p>Para calibrar os parâmetros de um modelo, usamos funções de perda. A perda quadrática (<code class="language-plaintext highlighter-rouge">L2</code>) penaliza desvios grandes com muito mais rigor, sendo a escolha natural quando assumimos ruído Gaussiano. A perda absoluta (<code class="language-plaintext highlighter-rouge">L1</code>) é mais estável na presença de dados discrepantes (<em>outliers</em>), pois não eleva os erros ao quadrado.</p>

<p>Para evitar que os pesos cresçam descontroladamente e gerem sobreajuste, aplicamos técnicas de regularização:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Custo = Perda nos Dados + lambda * Penalidade dos Pesos
</code></pre></div></div>

<p>A escolha da penalidade altera completamente a natureza do modelo:</p>

<ul>
  <li><strong>Regularização L1 (Lasso):</strong> Penaliza a magnitude absoluta dos coeficientes. Devido à sua geometria em formato de losango, cujos vértices coincidem com os eixos cartesianos, ela força vários coeficientes a se tornarem exatamente zero durante a otimização. Na prática, funciona como uma seleção automática de variáveis, descartando o que não tem relevância.</li>
  <li><strong>Regularização L2 (Ridge):</strong> Penaliza o quadrado dos coeficientes. Com sua geometria esférica e suave, ela puxa todos os pesos para valores próximos de zero de maneira uniforme, mas raramente anula qualquer parâmetro por completo.</li>
</ul>

<h2 id="quatro-pilares-do-aprendizado-supervisionado">Quatro pilares do aprendizado supervisionado</h2>

<p>Para cobrir a diversidade de abordagens nos vinte minutos, escolhi quatro famílias conceituais que mostram como a área evoluiu.</p>

<h3 id="1-árvores-de-decisão-e-a-medida-da-desordem">1. Árvores de decisão e a medida da desordem</h3>

<p>Árvores de decisão organizam o espaço de busca como uma sequência hierárquica de testes condicionais. A grande vantagem é a interpretabilidade direta: cada caminho da raiz até a folha pode ser lido como uma regra lógica compreensível.</p>

<p>Para construir a árvore sem testar todas as combinações possíveis (o que seria computacionalmente inviável), usa-se uma estratégia gulosa guiada pela <strong>Entropia de Shannon</strong>, que mede a incerteza dos dados:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>H(V) = - somatorio( P(v_k) * log2(P(v_k)) )
</code></pre></div></div>

<p>A cada nó, o algoritmo escolhe a variável que proporciona o maior <strong>Ganho de Informação</strong> (a maior redução líquida de incerteza após a divisão).</p>

<p>Como árvores profundas decoram os dados com facilidade, a poda é essencial. Parar o crescimento antes da hora (<em>early stopping</em>) é arriscado porque o algoritmo não enxerga interações complexas entre atributos (em um problema de porta lógica XOR, por exemplo, nenhum atributo sozinho parece informativo no primeiro corte). A melhor prática é deixar a árvore crescer por completo e aplicar a <strong>Poda Chi-quadrado (<code class="language-plaintext highlighter-rouge">chi^2</code>)</strong> retrospectivamente, eliminando nós cujas divisões não apresentem relevância estatística.</p>

<h3 id="2-modelos-lineares-do-perceptron-à-regressão-logística">2. Modelos lineares: do Perceptron à Regressão Logística</h3>

<p>A regressão linear busca o hiperplano que minimiza o erro quadrático médio em relação aos dados. Pode ser calculada de forma exata pela Equação Normal quando a matriz cabe na memória, ou otimizada iterativamente via Gradiente Descendente quando lidamos com grandes volumes de informação.</p>

<p>Na classificação, o <strong>Perceptron</strong> histórico utilizava um limiar rígido: se a combinação linear ultrapassasse zero, a resposta era 1; caso contrário, 0. A limitação é evidente: se os dados não forem perfeitamente separáveis por uma reta, o algoritmo nunca se estabiliza.</p>

<p>A <strong>Regressão Logística</strong> contorna essa fragilidade aplicando a função sigmoide sobre a saída linear:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>h(x) = 1 / (1 + e^(-w . x))
</code></pre></div></div>

<p>Em vez de tomar uma decisão binária abrupta, o modelo entrega uma probabilidade contínua e bem calibrada entre 0 e 1. Por ser suave e derivável, sua superfície de custo permite uma otimização convexa e robusta mesmo quando as classes apresentam sobreposição natural.</p>

<h3 id="3-support-vector-machines-svm-e-o-salto-dimensional">3. Support Vector Machines (SVM) e o salto dimensional</h3>

<p>O conceito central das Máquinas de Vetores de Suporte é a busca pela <strong>Margem Máxima</strong>: entre todos os hiperplanos capazes de separar duas classes, o algoritmo escolhe aquele que mantém a maior distância possível em relação aos pontos mais próximos de cada categoria.</p>

<p>Esses pontos críticos que determinam a fronteira são os <strong>vetores de suporte</strong>. Qualquer outro ponto distante da margem pode ser alterado ou removido do conjunto de dados sem afetar a decisão final.</p>

<p>Quando os dados não possuem separação linear no espaço original, entra em cena o <strong>Kernel Trick</strong>: uma formulação matemática fundamentada no Teorema de Mercer que calcula o produto escalar das amostras em um espaço de características de alta dimensão sem precisar projetar os dados explicitamente. É o equivalente a desembolar pontos misturados em uma mesa bidimensional elevando-os em diferentes alturas para conseguir passar um plano reto entre eles.</p>

<h3 id="4-métodos-de-ensemble-a-força-da-combinação">4. Métodos de Ensemble: a força da combinação</h3>

<p>Nenhum modelo precisa carregar todo o peso da decisão sozinho. Os métodos de comitê combinam múltiplos aprendizes base para produzir estimativas mais robustas:</p>

<ul>
  <li><strong>Bagging:</strong> Treina instâncias do mesmo algoritmo em diferentes amostras aleatórias com reposição (<em>bootstrapping</em>) e agrega os votos por maioria ou média. Seu principal efeito é a redução drástica de <strong>variância</strong> em modelos instáveis.</li>
  <li><strong>Random Forests:</strong> Sofistica o Bagging ao introduzir aleatoriedade também na escolha das variáveis avaliadas a cada divisão de nó (usando tipicamente a raiz quadrada do total de atributos). Isso quebra a correlação entre as árvores da floresta, gerando um conjunto muito mais diverso.</li>
  <li><strong>Boosting (AdaBoost e Gradient Boosting):</strong> Opera de forma sequencial. Cada novo modelo é construído com foco prioritário nas instâncias que os modelos anteriores classificaram incorretamente. O Gradient Boosting refina essa ideia ajustando os novos modelos diretamente aos resíduos (o gradiente negativo da função de perda), reduzindo progressivamente o <strong>viés</strong>.</li>
</ul>

<h2 id="a-fronteira-por-que-o-aprendizado-por-reforço-é-diferente">A fronteira: por que o aprendizado por reforço é diferente</h2>

<p>Até este ponto, o paradigma foi sempre o mesmo: existia um conjunto de dados anotado, com entradas e respostas corretas fornecidas por um agente externo. O aprendizado supervisionado é essencialmente um processo de inferência passiva sobre amostras prévias.</p>

<p>O <strong>Aprendizado por Reforço (RL)</strong> quebra essa estrutura:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>             ┌─────────────────────────┐
             │         Agente          │
             └───────────┬─────────────┘
                         │ Ação (A_t)
                         ▼
             ┌─────────────────────────┐
             │        Ambiente         │
             └───────────┬─────────────┘
                         │ Estado (S_t+1) e Recompensa (R_t+1)
                         ▼
</code></pre></div></div>

<p>Não há um professor dizendo qual era a ação ótima a cada instante. O agente interage ativamente com um mundo estocástico, toma decisões e recebe apenas sinais escalares de <strong>recompensa</strong> ou punição ao longo do tempo.</p>

<p>O problema central passa a ser a <strong>atribuição de crédito temporal</strong>: quando o agente alcança um resultado relevante após centenas de ações consecutivas, como determinar quais decisões intermediárias foram realmente determinantes para aquele desfecho?</p>

<p>Formalizamos esse ambiente através de um <strong>Processo de Decisão de Markov (MDP)</strong>, definido pela quádrupla <code class="language-plaintext highlighter-rouge">(S, A, P, R, gamma)</code>:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">S</code>: Espaço de estados do ambiente.</li>
  <li><code class="language-plaintext highlighter-rouge">A</code>: Conjunto de ações disponíveis.</li>
  <li><code class="language-plaintext highlighter-rouge">P(s' | s, a)</code>: Dinâmica de transição do mundo.</li>
  <li><code class="language-plaintext highlighter-rouge">R(s, a, s')</code>: Função de recompensa imediata.</li>
  <li><code class="language-plaintext highlighter-rouge">gamma</code>: Fator de desconto no tempo (entre 0 e 1), que pondera o valor de retornos futuros em relação a ganhos imediatos.</li>
</ul>

<p>O objetivo do agente é encontrar uma política <code class="language-plaintext highlighter-rouge">pi(s)</code> que mapeie estados em ações de modo a maximizar o retorno acumulado esperado:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>G_t = R_t+1 + gamma * R_t+2 + gamma^2 * R_t+3 + ...
</code></pre></div></div>

<h2 id="rl-passivo-estimando-valores-no-tempo">RL Passivo: estimando valores no tempo</h2>

<p>Antes de aprender a agir, o agente precisa aprender a avaliar a qualidade dos estados sob uma política fixa <code class="language-plaintext highlighter-rouge">pi</code>. A literatura clássica divide essa tarefa em três abordagens fundamentais:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Métodos de Avaliação de Políticas
├── Direct Utility Estimation (Monte Carlo puro, sem modelo, sem bootstrapping)
├── Adaptive Dynamic Programming (Model-based, aprende transições P, resolve Bellman)
└── Temporal-Difference TD(0) (Model-free, com bootstrapping local)
</code></pre></div></div>

<ol>
  <li><strong>Direct Utility Estimation (Monte Carlo):</strong> O agente executa trajetórias completas até o estado terminal, soma os retornos observados a partir de cada estado e calcula médias amostrais. O problema é que essa abordagem trata cada estado de forma isolada, ignorando que o valor de um estado está estruturalmente amarrado ao valor de seus sucessores pelas equações de Bellman.</li>
  <li><strong>Adaptive Dynamic Programming (ADP):</strong> O agente adota uma postura <em>model-based</em>. Ele conta as frequências de transição observadas para construir uma estimativa explícita de <code class="language-plaintext highlighter-rouge">P(s' | s, a)</code> e resolve o sistema de equações de Bellman por programação dinâmica. É a abordagem com maior eficiência amostral, mas se torna computacionalmente intratável quando o número de estados cresce exponencialmente.</li>
  <li><strong>Temporal-Difference Learning (TD(0)):</strong> Proposto por Rich Sutton em 1988, o método TD une o melhor dos dois mundos. Ele dispensa o modelo de transição do ambiente e atualiza a estimativa de utilidade logo no passo seguinte:</li>
</ol>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>U(s) &lt;- U(s) + alpha * [ R + gamma * U(s') - U(s) ]
</code></pre></div></div>

<p>A mágica reside no conceito de <strong>bootstrapping</strong>: o algoritmo usa a sua própria estimativa do estado seguinte <code class="language-plaintext highlighter-rouge">U(s')</code> para atualizar a estimativa do estado anterior <code class="language-plaintext highlighter-rouge">U(s)</code>, sem precisar esperar o final do episódio e sem precisar calcular matrizes de transição completas.</p>

<h2 id="rl-ativo-o-controle-e-o-equilíbrio-da-exploração">RL Ativo: o controle e o equilíbrio da exploração</h2>

<p>Quando o agente precisa escolher ativamente as ações para maximizar o retorno, ele se depara imediatamente com o conflito entre <strong>Exploração</strong> e <strong>Explotação</strong>:</p>

<ul>
  <li><strong>Explotação (<em>Exploitation</em>):</strong> Escolher a ação que o agente atualmente acredita ser a melhor para garantir recompensas a curto prazo.</li>
  <li><strong>Exploração (<em>Exploration</em>):</strong> Tomar ações com retornos incertos na tentativa de descobrir rotas e estratégias potencialmente superiores.</li>
</ul>

<p>Um agente puramente guloso (<em>greedy</em>) costuma convergir para políticas medíocres: ele descobre um caminho razoável nas primeiras tentativas e passa a repeti-lo indefinidamente, sem jamais explorar caminhos vizinhos que poderiam ser ordens de magnitude melhores.</p>

<p>Para contornar isso, usamos estratégias como o <strong>epsilon-greedy</strong>, onde o agente escolhe a melhor ação com probabilidade <code class="language-plaintext highlighter-rouge">1 - epsilon</code> e seleciona uma ação aleatória com probabilidade <code class="language-plaintext highlighter-rouge">epsilon</code>. Ao reduzir gradualmente o valor de <code class="language-plaintext highlighter-rouge">epsilon</code> ao longo do tempo, o sistema atende à condição de exploração infinita (GLIE), garantindo convergência para a política ótima.</p>

<h3 id="q-learning-contra-sarsa-duas-filosofias-de-controle">Q-Learning contra SARSA: duas filosofias de controle</h3>

<p>No controle livre de modelo (<em>model-free</em>), aprendemos a função de valor de ação <code class="language-plaintext highlighter-rouge">Q(s, a)</code>, que representa o retorno esperado ao tomar a ação <code class="language-plaintext highlighter-rouge">a</code> no estado <code class="language-plaintext highlighter-rouge">s</code>. Dois algoritmos clássicos dominam essa categoria:</p>

<p><strong>Q-Learning (Watkins, 1989 - Off-Policy):</strong></p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Q(s, a) &lt;- Q(s, a) + alpha * [ R + gamma * max_a'( Q(s', a') ) - Q(s, a) ]
</code></pre></div></div>

<p><strong>SARSA (Rummery &amp; Niranjan, 1994 - On-Policy):</strong></p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Q(s, a) &lt;- Q(s, a) + alpha * [ R + gamma * Q(s', a') - Q(s, a) ]
</code></pre></div></div>

<p>A distinção entre as duas fórmulas é sutil no índice, mas fundamental no comportamento:</p>

<ul>
  <li>O <strong>Q-Learning</strong> atualiza a estimativa assumindo que a próxima ação será a melhor teoricamente possível (<code class="language-plaintext highlighter-rouge">max_a' Q</code>), independentemente da ação que o agente realmente venha a executar durante a fase exploratória. Por ser <em>off-policy</em>, ele aprende diretamente a política ótima, mas pode ser arriscado durante a fase de treino se o ambiente contiver armadilhas severas.</li>
  <li>O <strong>SARSA</strong> atualiza a estimativa utilizando a ação <code class="language-plaintext highlighter-rouge">a'</code> que foi <strong>efetivamente sorteada</strong> pela política do agente no passo seguinte. Sendo <em>on-policy</em>, ele leva em consideração os próprios desvios exploratórios. Em ambientes com penalidades catastróficas próximas da borda, o SARSA aprende uma rota mais conservadora e segura para evitar quedas acidentais.</li>
</ul>

<h2 id="a-tríade-mortal-e-a-escalabilidade-no-mundo-real">A Tríade Mortal e a escalabilidade no mundo real</h2>

<p>Em problemas reais de grande porte, o espaço de estados é infinito ou excessivamente vasto para caber em uma tabela de memória. A solução é usar aproximadores de função (como redes neurais) para estimar <code class="language-plaintext highlighter-rouge">Q(s, a)</code> a partir de vetores de características.</p>

<p>No livro canônico de Sutton e Barto, os autores destacam um alerta fundamental para a engenharia de sistemas de IA: a <strong>Tríade Mortal</strong> (<em>The Deadly Triad</em>). A combinação simultânea de três fatores pode tornar o treinamento matematicamente instável e levar à divergência:</p>

<ol>
  <li><strong>Aproximação de funções:</strong> Uso de modelos não lineares parametrizados no lugar de representações tabulares.</li>
  <li><strong>Bootstrapping:</strong> Atualização de estimativas baseada em estimativas seguintes (como no TD e Q-Learning).</li>
  <li><strong>Treinamento Off-policy:</strong> Aprender sobre uma política enquanto os dados são coletados por outra política de exploração.</li>
</ol>

<p>O marco do algoritmo Deep Q-Network (DQN) da DeepMind em 2015 foi justamente conseguir estabilizar essa tríade ao jogar títulos do Atari a partir de pixels puros. A solução central foi o mecanismo de <strong>Experience Replay</strong>: as transições observadas são armazenadas em um buffer de memória e o modelo é atualizado a partir de mini-lotes sorteados aleatoriamente, quebrando a correlação temporal dos dados e restaurando a estabilidade do aprendizado.</p>

<h2 id="roteiro-cronometrado-para-a-apresentação">Roteiro cronometrado para a apresentação</h2>

<p>Para garantir que os vinte minutos cubram toda essa trajetória sem atropelos, estruturei a apresentação nos seguintes marcos de tempo:</p>

<ul>
  <li><strong>[00:00 a 03:00] Fundamentos da Aprendizagem e Indução:</strong>
    <ul>
      <li>O problema de aproximar a função desconhecida <code class="language-plaintext highlighter-rouge">f</code> via hipótese <code class="language-plaintext highlighter-rouge">h</code>.</li>
      <li>A fronteira entre classificação e regressão.</li>
      <li>O dilema central de viés contra variância e a parcimônia da Navalha de Ockham.</li>
      <li>O papel das funções de perda e o contraste geométrico entre regularização <code class="language-plaintext highlighter-rouge">L1</code> (esparsidade) e <code class="language-plaintext highlighter-rouge">L2</code> (suavização).</li>
    </ul>
  </li>
  <li><strong>[03:00 a 11:00] Quatro Pilares do Aprendizado Supervisionado:</strong>
    <ul>
      <li>Árvores de Decisão: Entropia de Shannon, ganho de informação e por que a poda Chi-quadrado é superior ao corte precoce em problemas como XOR.</li>
      <li>Modelos Lineares: O Perceptron com limiar rígido versus a Regressão Logística com sigmoide contínua e probabilística.</li>
      <li>Máquinas de Vetores de Suporte (SVM): O princípio da Margem Máxima, vetores de suporte e a projeção implícita do Kernel Trick.</li>
      <li>Ensembles: A redução de variância com Bagging/Random Forests e a redução de viés com Boosting sequencial.</li>
    </ul>
  </li>
  <li><strong>[11:00 a 18:00] A Transição para o Aprendizado por Reforço:</strong>
    <ul>
      <li>A ausência de gabarito e a dinâmica de tentativa e erro nos Processos de Decisão de Markov.</li>
      <li>O problema da atribuição de crédito em sequências longas de ação.</li>
      <li>Avaliação passiva: o salto conceitual do Monte Carlo para o TD(0) com bootstrapping.</li>
      <li>Controle ativo: o equilíbrio da exploração (<em>epsilon-greedy</em>) e o contraste entre o otimismo do Q-Learning e o realismo do SARSA.</li>
      <li>A Tríade Mortal de Sutton &amp; Barto e o papel do Experience Replay no DQN.</li>
    </ul>
  </li>
  <li><strong>[18:00 a 20:00] Síntese e Fechamento:</strong>
    <ul>
      <li>O arco evolutivo: da indução passiva de padrões estruturados para o controle autônomo em ambientes dinâmicos.</li>
      <li>Conclusão aberta para a arguição da banca.</li>
    </ul>
  </li>
</ul>

<h2 id="considerações-finais">Considerações finais</h2>

<p>Apresentar um panorama denso em uma janela curta de tempo é um exercício de foco naquilo que realmente importa. Não adianta tentar transformar vinte minutos em uma leitura apressada de enciclopédia; o objetivo é guiar a banca através das grandes ideias que moldaram o aprendizado de máquina nas últimas décadas.</p>

<p>Entender a intuição por trás do compromisso entre viés e variância, a elegância do cálculo de margem no SVM, a ousadia do <em>bootstrapping</em> nas diferenças temporais e os limites teóricos da aproximação de funções em reforço é o que dá sustentação a qualquer discussão técnica séria. Se esses fundamentos ficarem claros para quem está assistindo, o tempo terá sido muito bem investido.</p>

<h2 id="referências">Referências</h2>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p><strong>Artificial Intelligence: A Modern Approach (4th Edition)</strong> {<em>Stuart Russell &amp; Peter Norvig, Pearson, 2020</em>} (<a href="https://aima.cs.berkeley.edu/">Link</a>) <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p><strong>Reinforcement Learning: An Introduction (2nd Edition)</strong> {<em>Richard S. Sutton &amp; Andrew G. Barto, MIT Press, 2018</em>} (<a href="http://incompleteideas.net/book/the-book.html">Link</a>) <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>[&quot;Eduardo N. S. R.&quot;]</name></author><category term="Ensaios" /><category term="Inteligência Artificial" /><category term="Machine Learning" /><category term="Algoritmos" /><category term="Opinião" /><summary type="html"><![CDATA[Botar um cronômetro de vinte minutos na sua frente e ter que explicar aprendizado supervisionado e aprendizado por reforço é um exercício de síntese homérico, na melhor definição que consigo pensar agora (e olha que eu gosto de uma boa dose de masoquismo). Estamos falando de duas áreas monumentais da computação que consumiram décadas de pesquisas, acumularam centenas de variações algorítmicas e hoje sustentam quase tudo o que chamamos de IA moderna.]]></summary></entry></feed>