<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Devops | AkitaOnRails.com</title><link>https://akitaonrails.github.io/tags/devops/</link><description>Blog do Fabio Akita do Canal do YouTube 'Akitando' falando sobre tecnologia, carreira e coisas geek.</description><generator>Hugo -- gohugo.io</generator><language>pt-BR</language><lastBuildDate>Mon, 19 Oct 2026 23:20:48 +0000</lastBuildDate><atom:link href="https://akitaonrails.github.io/tags/devops/index.xml" rel="self" type="application/rss+xml"/><item><title>Parem de inventar desculpas e façam mais deploy! Com a IA, a premissa mudou. Entendam.</title><link>https://akitaonrails.github.io/2026/09/22/parem-de-inventar-desculpas-e-facam-mais-deploy-a-premissa-mudou/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/09/22/parem-de-inventar-desculpas-e-facam-mais-deploy-a-premissa-mudou/</guid><pubDate>Tue, 22 Sep 2026 16:00:00 -0300</pubDate><description>&lt;p&gt;Hoje eu soltei uma sequência de posts no X batendo nessa tecla e, como sempre que eu falo de deploy rápido, um bocado de gente leu &amp;ldquo;esse cara está dizendo pra subir qualquer porcaria em produção sem testar&amp;rdquo;. Não é isso. Deixa eu explicar com calma, porque o argumento é mais estreito e mais chato do que a versão que espalha pânico.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;É melhor deployar mais rápido, porque corrigir depois ficou rápido e barato.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Hoje eu soltei uma sequência de posts no X batendo nessa tecla e, como sempre que eu falo de deploy rápido, um bocado de gente leu &ldquo;esse cara está dizendo pra subir qualquer porcaria em produção sem testar&rdquo;. Não é isso. Deixa eu explicar com calma, porque o argumento é mais estreito e mais chato do que a versão que espalha pânico.</p>
<blockquote>
  <p>É melhor deployar mais rápido, porque corrigir depois ficou rápido e barato.</p>

</blockquote>
<p>Essa frase é o artigo inteiro resumido. O resto é só eu justificando por que ela é verdade agora, mesmo não sendo verdade há alguns anos, e mostrando como eu mesmo ajo em cima dela desde fevereiro.</p>
<h2>A premissa mudou, e a maioria não percebeu<span class="hx:absolute hx:-mt-20" id="a-premissa-mudou-e-a-maioria-não-percebeu"></span>
    <a href="#a-premissa-mudou-e-a-maioria-n%c3%a3o-percebeu" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Comecei a falar isso <a href="https://x.com/AkitaOnRails/status/2102436061083267441"target="_blank" rel="noopener">no X</a> assim:</p>
<blockquote>
  <p>Corrigir ficou barato. Não segurem coisas mais. Só sobe e corrige depois. &ldquo;Ain mas corro o risco de subir com bug&rdquo;. A premissa errada é achar que vc ficar olhando linha a linha vai fazer qualquer diferença num volume grande. E código parado não melhora sozinho. Só código usado dá pra descobrir se precisa melhorar.</p>

</blockquote>
<p>E completei <a href="https://x.com/AkitaOnRails/status/2102476036709450066"target="_blank" rel="noopener">no segundo post</a>, porque a primeira leitura de qualquer coisa que eu escrevo em 280 caracteres sempre vem raivosa e literal demais:</p>
<blockquote>
  <p>Entendam o conceito (é post de X, não de blog, parem de ler literalmente). Não estou dizendo que IA não gera bugs. Gera, óbvio. Mas a esta altura do campeonato, não mais do que você/humano gera também. [&hellip;] Antigamente a desculpa era que quando sobe em produção com bug, ele fica lá mais tempo do que deveria, porque nunca vira prioridade na sprint pra resolver. Agora NÃO TEM MAIS ISSO, a premissa mudou: corrigir o bug depois é RÁPIDO e BARATO.</p>

</blockquote>
<p>Esse é o ponto inteiro. Não é sobre IA ser infalível. É sobre o custo de corrigir um erro ter despencado, e a maioria dos times ainda estar operando o processo de revisão como se esse custo continuasse alto.</p>
<p>Com os modelos de fronteira de hoje, Opus, Sol, K3, e companhia, o LLM erra menos do que a maioria dos desenvolvedores humanos que eu já vi passar por um code review. Quando erra, na esmagadora maioria das vezes o erro não é do modelo. É de quem dirigiu o harness. O LLM faz o que você pede, e não faz o que você não pediu.</p>
<p>Se você não sabe o que pedir, não vai produzir nada útil, não importa quão bom seja o modelo. É exatamente por isso que só Engenheiro de Software de verdade consegue tirar software de produção de verdade de um agente: alguém tem que saber decompor o problema, validar o resultado, e reconhecer quando o pedido em si estava mal formulado. IA não substitui esse julgamento. Ela multiplica o que esse julgamento já sabia fazer.</p>
<h2>Isso já era pra ser assim há 25 anos<span class="hx:absolute hx:-mt-20" id="isso-já-era-pra-ser-assim-há-25-anos"></span>
    <a href="#isso-j%c3%a1-era-pra-ser-assim-h%c3%a1-25-anos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O incômodo que eu sinto com o processo de review burocrático não é novo, e a indústria já devia ter resolvido isso há muito tempo. Kent Beck e Ron Jeffries inventaram Extreme Programming em 1997, no projeto Chrysler C3, e Beck publicou o livro em 1999 com integração contínua, TDD e entrega em lotes pequenos como práticas centrais. Em fevereiro de 2001, dezessete desenvolvedores se trancaram num resort em Snowbird, Utah, e saíram de lá com o Manifesto Ágil. Isso faz vinte e cinco anos.</p>
<p>O mantra de Martin Fowler pra integração contínua sempre foi &ldquo;se dói, faça com mais frequência&rdquo;, e o livro <em>Continuous Delivery</em>, de Jez Humble e Dave Farley, formalizou a ideia de que deploy pequeno e frequente reduz risco, não aumenta. Os relatórios anuais de State of DevOps da DORA documentam isso com número desde a década passada:</p>
<ul>
<li>time de elite: deploya sob demanda, várias vezes por dia;</li>
<li>time de baixa performance: deploya uma vez por mês, às vezes uma vez a cada seis meses.</li>
</ul>
<p>E o achado mais chato de engolir pra quem tem medo de deployar rápido: historicamente, deploy mais frequente <strong>não aumenta</strong> a taxa de falha. <em>Reduz.</em> Time que deploya mais, na média dos relatórios da última década, falha menos e recupera mais rápido quando falha.</p>
<p>Mesmo com tudo isso documentado, medido e publicado há mais de uma década, boa parte da indústria continua represando mudança em lote grande, com revisão manual demorada, e chamando isso de &ldquo;prudência&rdquo;. Nem sempre foi certeza dentro da própria comunidade ágil: em 2014, Kent Beck, Martin Fowler e o DHH (criador do Rails) passaram semanas debatendo publicamente se TDD estava morto, sem chegar a um consenso limpo. Ou seja: a própria comunidade que inventou essas práticas discute até hoje o quanto de rigor é suficiente.</p>
<p>O que eu estou dizendo é mais simples que essa discussão inteira: seja qual for o seu nível de rigor de teste, o LLM revisando seu diff antes do merge é rápido, e reverter um deploy ruim ficou trivial com container e git. A conta que sobra é: quanto tempo você gasta segurando código, versus quanto tempo levaria pra reverter se desse errado? Se a segunda conta é menor, você está pagando um pedágio por medo, não por prudência.</p>
<h2>Não é &ldquo;suba qualquer coisa&rdquo;<span class="hx:absolute hx:-mt-20" id="não-é-suba-qualquer-coisa"></span>
    <a href="#n%c3%a3o-%c3%a9-suba-qualquer-coisa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Essa é a parte que sempre se perde: eu não estou dizendo pra deployar sem nenhuma prática de engenharia por baixo. Estou dizendo o oposto. Isso só é seguro <strong>depois</strong> que você já tem a engenharia de software básica funcionando, o que eu documento à exaustão neste blog há anos: teste automatizado, CI que roda em todo push, capacidade real de reverter, observabilidade pra saber quando algo quebrou.</p>
<blockquote>
  <p>80/20. Peça pra LLM fazer uma revisão mínima e sobe. Se quebrar, reverte (pra isso temos containers, git, etc etc). Se não dá pra reverter, sua infra é uma bosta - corrija ela pra ontem.</p>

</blockquote>
<p>Essa última frase é o filtro inteiro. Se a sua infraestrutura não permite reverter rápido, o problema não é a velocidade do deploy. É a sua infraestrutura, e ela precisa ser corrigida antes de qualquer conversa sobre acelerar. Ninguém está liberado pra pular a etapa de ter rollback de verdade. O que muda é que, tendo isso, segurar código esperando revisão humana virou desperdício puro.</p>
<h2>O ambiente mínimo recomendado<span class="hx:absolute hx:-mt-20" id="o-ambiente-mínimo-recomendado"></span>
    <a href="#o-ambiente-m%c3%adnimo-recomendado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu já falei &ldquo;engenharia de software básica&rdquo; várias vezes neste texto sem detalhar o que isso significa na prática. Aqui está a lista, sem enrolação. Nenhum item aqui é opcional, e nenhum deles ficou caro de manter:</p>
<ul>
<li>teste unitário cobrindo a lógica isolada, TDD ou não, contanto que exista;</li>
<li>teste de cenário e integração cobrindo concorrência, falha parcial e conflito, o que teste unitário sozinho não pega;</li>
<li>CI que builda e testa em toda mudança, de preferência pras plataformas que você realmente publica;</li>
<li>revisão automática hostil antes do merge, tipo <code>pr-audit</code>, que trata alegação do contribuidor como não verificada até prova;</li>
<li>feature flag pra controlar o raio de explosão de quem vê a mudança primeiro;</li>
<li>staging de verdade como gate antes de qualquer usuário real ver a mudança;</li>
<li>capacidade de reverter rápido, com git e container, e observabilidade pra saber quando algo quebrou;</li>
<li>em ambiente de compliance, financeiro ou de saúde, uma camada extra: teste de cenário mais pesado, e mais de uma trava por modo de falha identificado.</li>
</ul>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/deploy-environment-diagram.png" alt="Diagrama do pipeline mínimo pra deploy rápido e seguro: commit, testes automatizados, CI multiplataforma, feature flag, staging e produção, com uma seta de rollback voltando de produção pros testes e uma seta de feedback voltando de produção pro commit"  loading="lazy" /></p>
<p>O que o diagrama tenta deixar claro é que velocidade e segurança não competem entre si aqui. Elas vêm da mesma pilha de camada, e cada camada dessa pilha é o que te dá permissão pra não segurar código esperando revisão manual.</p>
<h2>Onde essa conta não fecha<span class="hx:absolute hx:-mt-20" id="onde-essa-conta-não-fecha"></span>
    <a href="#onde-essa-conta-n%c3%a3o-fecha" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Não vou fingir que não veio crítica boa nas respostas do X, porque várias batem num ponto real que eu pulei ao escrever rápido pra Twitter. Vale mais responder de verdade do que só validar quem concordou.</p>
<ol>
<li>
<p><strong>Reverter código não desfaz consequência que já saiu do seu sistema.</strong> Se o bug já processou um pagamento errado, moveu dinheiro pra conta errada, ou controlou um elevador, reverter o deploy não desfaz o que já aconteceu. &ldquo;Se não dá pra reverter, sua infra é uma bosta&rdquo; cobre a parte técnica de deploy e rollback, não o caso onde o dano já saiu do seu sistema antes de você conseguir reagir. Pagamento, saúde e qualquer coisa que mexe com segurança física entram nessa categoria, e ali a régua de revisão antes de subir tem que ficar mais rígida, não mais frouxa, porque o custo de reverter deixou de ser técnico.</p>
</li>
<li>
<p><strong>Banco de dados quebra a analogia de &ldquo;reverter é fácil&rdquo;.</strong> Reverter um binário é trivial. Reverter uma migration que já rodou em produção, já moveu dado, e talvez já apagou coluna, não é. Existe um padrão pra isso, <em>expand-contract</em>: adiciona o novo formato sem remover o velho, espera todo consumidor migrar, só depois remove o velho. Isso não é sobre confiar mais ou menos em IA. É sobre estado que não se desfaz com <code>git revert</code>.</p>
</li>
<li>
<p><strong>Biblioteca e pacote público não seguem a mesma conta que aplicação fechada.</strong> Quando o meu código quebra, eu conserto na hora. Quando uma lib que centenas de projetos dependem quebra, quem carrega o prejuízo é gente que nem sabe que eu existo, com um tempo de reação muito mais lento que o meu. É por isso que a minha skill de <code>pr-audit</code>, que eu descrevo mais abaixo, trata classificação de semver como gate de versão: breaking change anda em major, nunca em patch, porque ali a revisão de verdade não é sobre pegar bug, é sobre não surpreender quem depende de você sem avisar.</p>
</li>
<li>
<p><strong>Loja de app derruba a velocidade também</strong>, e o meu próprio exemplo do frank_yomik esconde isso em vez de resolver. Ele publica o APK direto como GitHub Release, sem passar pela Play Store. Se você distribui pela loja oficial da Google ou da Apple, a revisão deles entra no meio do seu processo, e você não controla o tempo dela. Este artigo é sobre remover o gargalo que está dentro do seu controle. A loja de app não está dentro dele.</p>
</li>
<li>
<p><strong>O ponto mais honesto de todos: toda evidência que eu trago aqui vem de projeto onde eu sou o único dono e decido sozinho.</strong> Não tenho como provar, com meus próprios números, que o mesmo ritmo funciona dentro de uma empresa grande, com Jurídico, Compliance e gerente de produto no meio do caminho. O que eu defendo que generaliza é o princípio, não o ritmo exato: construa capacidade real de reverter, automatize a checagem mecânica, e meça o resultado contra uso real, não contra a sua sensação de segurança. Quanta velocidade você aplica em cima disso depende de quanto custa um erro no seu contexto específico, e ninguém de fora responde isso por você.</p>
</li>
</ol>
<p>Sobre o primeiro ponto especificamente, vale abrir mais, porque &ldquo;régua mais rígida&rdquo; não pode ficar vago. Isso significa teste de cenário real, não só teste unitário. O próprio ai-memory tem mais de mil testes de integração espalhados pelos seus módulos, e a maioria não pergunta &ldquo;essa função devolve o valor certo?&rdquo;. Pergunta coisa como:</p>
<ul>
<li>o que acontece quando duas escritas concorrem pelo mesmo arquivo ao mesmo tempo;</li>
<li>o que acontece quando o disco falha no meio de uma operação de exclusão;</li>
<li>o que acontece quando duas sessões tentam consolidar o mesmo dado ao mesmo tempo;</li>
<li>o que acontece quando o processo morre antes de terminar de escrever.</li>
</ul>
<p>Ambiente com exigência de compliance, financeiro ou de saúde, precisa desse nível de teste: todo modo de falha identificado tem que ter mitigação, e mais de uma camada de checagem, nunca uma trava só.</p>
<p>E aqui está o ponto que sustenta o argumento inteiro deste artigo mesmo nesses ambientes: essa exigência não muda dependendo de quem escreveu o código. Erro de desenvolvedor humano derruba um sistema de pagamento tão bem quanto erro de LLM, e ninguém dá passe livre pra revisão frouxa só porque foi um humano sênior que escreveu a linha. A régua de teste de cenário e camada dupla de proteção é sobre o domínio do problema, não sobre quem, ou o que, produziu o código.</p>
<p>Feature flag e ambiente de staging são a outra metade dessa camada dupla, e eles também mudaram de custo. Antigamente, manter dezenas de flag era um saco: cada uma precisa nascer, ser monitorada, e ser removida quando não faz mais sentido, e é fácil acumular flag morta que ninguém lembra pra que serve. Isso ficou barato também: peço pro agente auditar todo flag do projeto, achar qual está travada no mesmo valor há meses, e gerar o PR de limpeza. A manutenção que antes tomava uma tarde inteira vira uma tarefa de minutos.</p>
<p>Com isso resolvido, a defesa em camada fica simples de montar: staging pra pegar erro óbvio antes de qualquer usuário ver, feature flag pra limitar o raio de explosão de quem vê a mudança primeiro, e teste de cenário pra cobrir o que staging sozinho não simula. Três camadas, nenhuma delas cara de manter.</p>
<h2>Como eu faço isso desde fevereiro<span class="hx:absolute hx:-mt-20" id="como-eu-faço-isso-desde-fevereiro"></span>
    <a href="#como-eu-fa%c3%a7o-isso-desde-fevereiro" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Comecei a acelerar minha produção com IA em fevereiro deste ano, e a pergunta que eu queria responder era simples: dá pra usar a nova geração de LLMs de fronteira pra programar o tempo inteiro, num ritmo de produção de verdade, não só demo? A resposta virou meu <a href="/2026/09/15/novo-llm-benchmark-v4-retestando-todos-llms-parte-1/">maior benchmark de LLMs</a>, mais de 110 artigos publicados desde então, e isto, como eu <a href="https://x.com/AkitaOnRails/status/2102176881139138806"target="_blank" rel="noopener">escrevi recentemente</a>:</p>
<blockquote>
  <p>Desde fevereiro produzi mais de 110 artigos no meu blog. A grande maioria veio dos 30+ projetos open source que eu fiz e mantenho (43 se somar projetos dos outros contribuindo, como Flea ou Omarchy). 1.6 milhão de linhas de código produzidos. Eu sozinho (com agentes, claro).</p>

</blockquote>
<p>Os projetos que eu mais uso no meu próprio dia a dia, e que sobreviveram ao teste de &ldquo;eu continuo mantendo isso meses depois&rdquo;, são três:</p>
<ul>
<li><a href="https://aijail.io"target="_blank" rel="noopener">ai-jail</a>: sandbox de SO pros meus agentes de código;</li>
<li><a href="https://aimemorybr.netlify.app"target="_blank" rel="noopener">ai-memory</a>: memória de longo prazo pros agentes;</li>
<li>ai-usagebar: widget de barra pra monitorar consumo de LLM.</li>
</ul>
<p>Dois deles já têm landing page decente:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/aijail-hero.png" alt="Landing page do ai-jail, um sandbox de sistema operacional pra agentes de IA"  loading="lazy" /></p>
<p>O ai-jail subiu hoje. O ai-memory está no ar em <a href="https://aimemorybr.netlify.app"target="_blank" rel="noopener">aimemorybr.netlify.app</a> porque a transferência do domínio <code>aimemory.io</code> ainda vai levar mais alguns dias.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/aimemory-hero.png" alt="Landing page do ai-memory, sistema de memória de longo prazo pra agentes de código"  loading="lazy" /></p>
<p>As duas rodam no Netlify. Não é recomendação de ferramenta, é só onde eu já tinha conta configurada. O ponto que importa aqui não é a plataforma, é o fluxo: pra colocar uma versão nova no ar, o comando é <code>git push</code>. Não existe passo manual entre o commit e o site atualizado.</p>
<p>A minha newsletter, <a href="https://themakitachronicles.com"target="_blank" rel="noopener">The M.Akita Chronicles</a>, já passa de 15 mil assinantes:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/themakitachronicles-hero.png" alt="Página de inscrição da newsletter The M.Akita Chronicles"  loading="lazy" /></p>
<p>E o esforço mais novo é converter os posts do blog em episódio de podcast. Isso roda inteiro dentro do Discord, com um bot que gera o preview e espera meu comando:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/discord-podcast-1.png" alt="Bot do Discord avisando que o preview de um episódio de podcast está pronto, com comandos pra publicar, agendar ou rejeitar"  loading="lazy" /></p>
<p>Um <code>/publish-podcast 2</code> depois, o episódio está publicado e a transcrição já tem URL:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/discord-podcast-2.png" alt="Bot do Discord confirmando que o episódio de podcast foi publicado, com link pra transcrição"  loading="lazy" /></p>
<p>Do texto pronto até o áudio publicado, o processo inteiro é eu digitando um comando de barra numa conversa de Discord. Essa é a régua de esforço que eu uso pra medir se um processo está bom o suficiente: se publicar exige mais do que um comando, alguma etapa ainda está manual demais.</p>
<h3>E a qualidade do código gerado, aguenta?<span class="hx:absolute hx:-mt-20" id="e-a-qualidade-do-código-gerado-aguenta"></span>
    <a href="#e-a-qualidade-do-c%c3%b3digo-gerado-aguenta" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Todo santo dia aparece gente preocupada que automatizar demais com LLM vai jogar código ruim, ou pior, código perigoso, em produção sem ninguém perceber. Dado que a engenharia por baixo é a que eu descrevi na seção anterior, teste automatizado, CI, feature flag, staging, rollback rápido, a minha experiência real não confirma esse medo.</p>
<p>Já produzi mais de 1,6 milhão de linhas de código com LLM desde fevereiro. <strong>Nunca vi um LLM fazer alguma coisa aleatoriamente destrutiva sem eu pedir.</strong> Pelo contrário: no meu <a href="/2026/09/15/novo-llm-benchmark-v4-retestando-todos-llms-parte-1/">LLM Benchmark v4</a>, montei de propósito um cenário adversarial, um subagente plantando catorze sabotagens reais baseadas em CVE, escondidas dentro de um projeto que cresce em sete sprints. A maioria dos modelos testados encontrou e corrigiu a sabotagem sozinha, sem eu precisar apontar o dedo.</p>
<p>Isso é anedótico. Não prova nada além do que aquele benchmark específico mede. Mas depois de todo esse volume, a impressão que fica é consistente: LLM de hoje produz código estável na maior parte do tempo, parecido com o que um desenvolvedor humano decente produziria, só que muito mais rápido, e na média, mais preciso.</p>
<h2>CI/CD multiplataforma sem esforço<span class="hx:absolute hx:-mt-20" id="cicd-multiplataforma-sem-esforço"></span>
    <a href="#cicd-multiplataforma-sem-esfor%c3%a7o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Fui contar quantos dos meus projetos <code>ai-*</code> e <code>frank-*</code> (18 repositórios no total, entre os que têm README) já têm build e deploy automatizado. Catorze têm CI configurado, e a maioria constrói e publica pra múltiplas plataformas de sistema operacional a partir de um push feito de uma máquina Linux.</p>
<p>O exemplo mais completo é o próprio ai-memory: um único push de tag dispara um workflow que, na mesma execução:</p>
<ul>
<li>builda Linux x86_64 e arm64;</li>
<li>builda macOS arm64 e x86_64;</li>
<li>builda Windows x86_64, com empacotamento em PowerShell;</li>
<li>publica uma imagem Docker multi-arquitetura;</li>
<li>empurra o pacote pro AUR do Arch Linux.</li>
</ul>
<p>O ai-usagebar segue o mesmo padrão de build pra Linux e Windows, e ainda publica em dois pacotes do AUR, no crates.io, e num bucket do Scoop pra Windows.</p>
<p>E tem o caso do frank_yomik, meu leitor de mangá com OCR e tradução via LLM local, que é o exemplo que eu queria pra mostrar que Android entra na mesma esteira. Um push de tag <code>v*</code> dispara cinco builds em paralelo:</p>
<ul>
<li>Linux;</li>
<li>macOS;</li>
<li>Windows;</li>
<li>extensão de navegador;</li>
<li>Android, num runner Linux comum, decodificando um keystore de assinatura guardado nos secrets do GitHub e gerando o APK já assinado.</li>
</ul>
<p>No final, um job espera os cinco terminarem e publica um único GitHub Release com todos os binários juntos. Builda pra quatro sistemas diferentes, incluindo Android, sem sair do Linux uma vez.</p>
<p>Nem tudo saiu perfeito na primeira olhada, e eu prefiro admitir isso a fingir que é tudo redondo: o frank_sherlock e o frank_karaoke publicam a release como rascunho por padrão, o que trava a automação seguinte (como o push pro AUR) até alguém clicar em &ldquo;publicar&rdquo; manualmente. Achei esse buraco enquanto escrevia este texto.</p>
<p>Um projeto vizinho, o frank_scanlation, já tinha resolvido um problema parecido, mas diferente: o GitHub suprime o evento que dispara automação quando a própria release foi criada pelo token do robô, então o workflow dele dispara a etapa seguinte na mão, via <code>workflow_dispatch</code>, de dentro do próprio job. Nenhum dos dois ajustes é difícil. Vou aplicar os dois nos projetos que faltam esta semana, porque é exatamente esse tipo de atrito acumulado que esse artigo inteiro está dizendo pra não deixar parado.</p>
<p>E já que estamos falando de atrito escondido: o ai-memory tem código específico pra Windows (índice de arquivo NTFS, hook nativo em PowerShell) que só roda de verdade no runner <code>windows-latest</code> do GitHub, uns 1000 segundos contra 250 no Linux pra mesma suíte. Cheguei a montar uma VM Windows local via <a href="https://github.com/dockur/windows"target="_blank" rel="noopener">dockur/windows</a> achando que ia ganhar velocidade.</p>
<p>Não ganhei: o workload é dominado por E/S de disco, e o resultado saiu mais lento que o runner do GitHub pra suíte inteira. Ficou útil só pra iteração rápida num crate isolado. Ferramenta boa é a que você mede, não a que você assume, mesmo quando a medição contraria a sua aposta inicial.</p>
<h2>A revisão automática que sustenta tudo isso<span class="hx:absolute hx:-mt-20" id="a-revisão-automática-que-sustenta-tudo-isso"></span>
    <a href="#a-revis%c3%a3o-autom%c3%a1tica-que-sustenta-tudo-isso" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu já <a href="/2026/09/17/falando-um-pouco-sobre-minhas-skills-de-ia/">detalhei minhas skills de revisão</a> num artigo recente, então vou resumir aqui:</p>
<ul>
<li><code>pr-audit</code> audita todo PR antes do merge com o princípio de <strong>evidence over narrative</strong>: nada do que o contribuidor escreveu é verdade até prova, com checklist hostil pra código malicioso, credencial exposta, quebra de semver e tentativa de prompt injection;</li>
<li><code>iss-audit</code> aplica a mesma desconfiança pra issue, nunca executando comando colado por quem reportou;</li>
<li><code>github-resolution</code> transforma o que foi aprovado em código mergeado, um ticket de cada vez, com teste de regressão escrito antes da correção, e a regra de que <em>&ldquo;slop é defeito&rdquo;</em>: sem abstração especulativa, sem TODO esquecido, sem enfraquecer teste pra passar mais rápido.</li>
</ul>
<p>Isso pega a categoria mais cara de porcaria dentro do mesmo diff. Não pega duplicação espalhada entre PRs diferentes, porque a auditoria olha um diff isolado, não a arquitetura inteira do projeto. Essa parte ainda depende de eu notar o padrão se repetindo e mandar consolidar.</p>
<p>Na minha estimativa, isso resolve uns 99% do trabalho de revisão e resolução sozinho, comigo supervisionando e decidindo os casos que sobram. Só que isso não é o ponto principal. Automação de revisão é necessária, mas ela sozinha não prova nada sobre se o software é bom.</p>
<h2>O que realmente mantém isso no chão: gente de verdade usando<span class="hx:absolute hx:-mt-20" id="o-que-realmente-mantém-isso-no-chão-gente-de-verdade-usando"></span>
    <a href="#o-que-realmente-mant%c3%a9m-isso-no-ch%c3%a3o-gente-de-verdade-usando" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Aqui está a parte que eu acho mais importante do artigo inteiro, e que costuma passar batido: a sua própria revisão não vale tanto quanto você acha. O que vale é software publicado sendo usado por gente de verdade, dando feedback de verdade.</p>
<p>Fui checar os números reais do ai-memory no GitHub, sem arredondar pra impressionar:</p>
<ul>
<li><strong>issues: 287 no total, 249 vieram de gente que não sou eu</strong>, 86,8% do total;</li>
<li><strong>pull requests: 562 no total, 371 de fora</strong>, sendo 10 delas bump automático do Dependabot e as outras 361 vindas de gente de verdade, cerca de 121 contribuidores distintos.</li>
</ul>
<p>Os dois que mais contribuíram junto comigo mandaram 64 e 43 PRs cada, sozinhos.</p>
<p>Isso é o que mantém o projeto no chão. Não é o LLM sendo bom revisor, nem eu sendo bom revisor. É centenas de pessoas usando de verdade, achando casos que eu nunca imaginei, e mandando issues e PRs reais. Sem isso, minhas skills de revisão automatizada estariam apenas confirmando as minhas próprias suposições de volta pra mim, por mais rigorosas que as regras pareçam no papel.</p>
<p>Não importa o quão boa é a IA que você usa: se ninguém usa o que você publicou, você não tem feedback real, e sem feedback real não existe melhoria real. Só existe a sua própria opinião sobre o próprio trabalho, validada por ninguém além de você mesmo.</p>
<h2>A conclusão que interessa<span class="hx:absolute hx:-mt-20" id="a-conclusão-que-interessa"></span>
    <a href="#a-conclus%c3%a3o-que-interessa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Engenharia de software de verdade <strong>precisa</strong> ter deploy rápido e de esforço quase zero. Não é opcional, não é luxo de time grande, é pré-requisito. E se revisar e colocar em produção está demorando mais tempo do que desenvolver a funcionalidade, você não está só errado.</p>
<p><strong>Você é o gargalo.</strong></p>
<p>Desculpa e justificativa não valem nada nesse momento. Use esse tempo pra consertar você mesmo e o seu processo, porque só resultado no mundo real importa, não a sensação de segurança de ter revisado linha por linha um diff que ninguém mais vai olhar de novo.</p>
<p>Se a premissa mudou, eu mudo hoje. Não amanhã, não semana que vem. E se a sua ainda não mudou, o problema não é a IA. É você segurando o próprio trabalho.</p>
]]></content:encoded><category>devops</category><category>engenharia-de-software</category><category>agentes-de-codigo</category><category>vibe-coding</category></item><item><title>Boas práticas de projetos de código aberto com LLM - O Mínimo</title><link>https://akitaonrails.github.io/2026/05/30/boas-praticas-projetos-codigo-aberto-llm-o-minimo/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/05/30/boas-praticas-projetos-codigo-aberto-llm-o-minimo/</guid><pubDate>Sat, 30 May 2026 13:00:00 -0300</pubDate><description>&lt;p&gt;Nos últimos meses eu publiquei uma penca de projetos pessoais no GitHub. &lt;a href="https://github.com/akitaonrails/frank_fbi"target="_blank" rel="noopener"&gt;Frank FBI&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/FrankMega"target="_blank" rel="noopener"&gt;Frank Mega&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/FrankYomik"target="_blank" rel="noopener"&gt;Frank Yomik&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/FrankSherlock"target="_blank" rel="noopener"&gt;Frank Sherlock&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/frank_investigator"target="_blank" rel="noopener"&gt;Frank Investigator&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener"&gt;ai-jail&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/ai-memory"target="_blank" rel="noopener"&gt;ai-memory&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/ai-usagebar"target="_blank" rel="noopener"&gt;ai-usagebar&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/ghpending"target="_blank" rel="noopener"&gt;ghpending&lt;/a&gt;, e por aí vai. Alguns em Ruby on Rails, a maioria em Rust, um ou outro em Flutter. Programados em grande parte com Claude Code e Codex, no embalo da minha &lt;a href="https://akitaonrails.github.io/2026/05/14/terminando-maratona-ia-sucesso-ou-fracasso/"&gt;maratona de vibe coding&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;E aqui está a parte que ninguém comenta quando fala de &amp;ldquo;fiz um projeto com IA&amp;rdquo;: gerar o código é a parte fácil. O difícil, o que separa um repositório que é só um amontoado de código de um projeto de código aberto de verdade, é tudo que vem &lt;strong&gt;depois&lt;/strong&gt; do &lt;code&gt;git init&lt;/code&gt;.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Nos últimos meses eu publiquei uma penca de projetos pessoais no GitHub. <a href="https://github.com/akitaonrails/frank_fbi"target="_blank" rel="noopener">Frank FBI</a>, <a href="https://github.com/akitaonrails/FrankMega"target="_blank" rel="noopener">Frank Mega</a>, <a href="https://github.com/akitaonrails/FrankYomik"target="_blank" rel="noopener">Frank Yomik</a>, <a href="https://github.com/akitaonrails/FrankSherlock"target="_blank" rel="noopener">Frank Sherlock</a>, <a href="https://github.com/akitaonrails/frank_investigator"target="_blank" rel="noopener">Frank Investigator</a>, <a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener">ai-jail</a>, <a href="https://github.com/akitaonrails/ai-memory"target="_blank" rel="noopener">ai-memory</a>, <a href="https://github.com/akitaonrails/ai-usagebar"target="_blank" rel="noopener">ai-usagebar</a>, <a href="https://github.com/akitaonrails/ghpending"target="_blank" rel="noopener">ghpending</a>, e por aí vai. Alguns em Ruby on Rails, a maioria em Rust, um ou outro em Flutter. Programados em grande parte com Claude Code e Codex, no embalo da minha <a href="/2026/05/14/terminando-maratona-ia-sucesso-ou-fracasso/">maratona de vibe coding</a>.</p>
<p>E aqui está a parte que ninguém comenta quando fala de &ldquo;fiz um projeto com IA&rdquo;: gerar o código é a parte fácil. O difícil, o que separa um repositório que é só um amontoado de código de um projeto de código aberto de verdade, é tudo que vem <strong>depois</strong> do <code>git init</code>.</p>
<p>Eu tenho uma opinião forte sobre isso, então vou cravar logo: nenhum projeto de código aberto está pronto pra ser publicado sem três coisas. Na ordem de importância:</p>
<ol>
<li><strong>Superfície de instalação.</strong> O novo usuário precisa conseguir instalar e testar a ferramenta com o mínimo de atrito possível. Um comando, de preferência.</li>
<li><strong>Testes e CI automatizado.</strong> Pra que contribuições sejam mais fáceis e exista um chão comum sobre o que é minimamente aceitável pra entrar no projeto.</li>
<li><strong>Documentação.</strong> A versão curta e fácil de ler (o README) e um conjunto mais detalhado em <code>docs/</code>, pra novos contribuidores entenderem as decisões arquiteturais.</li>
</ol>
<p>Sem esses três, na minha visão, o projeto não está pronto. Pode ter o código mais genial do mundo dentro dele, não importa. Se um estranho não consegue instalar, não consegue confiar que o que ele mexer não vai quebrar tudo, e não consegue entender por que as coisas são do jeito que são, então você não tem um projeto de código aberto. Você tem um arquivo morto público.</p>
<p>E tem um bônus que virou parte essencial do meu fluxo: <strong>quando todos os meus projetos seguem o mesmo padrão, eu posso simplesmente pedir pro Claude Code ou pro Codex &ldquo;roda o deploy&rdquo; ou &ldquo;solta uma release&rdquo;, e eles conseguem.</strong> Porque a estrutura é previsível. <code>bin/deploy</code> faz a mesma coisa em todo projeto. A release sai de um tag. O agente não precisa adivinhar nada. Padronização é o que torna a automação confiável. Parece preciosismo de gente organizada, mas é o pré-requisito pra eu confiar de olhos fechados no que o agente vai rodar.</p>
<p>Vou destrinchar os três pilares e mostrar o mínimo de configuração que eu uso na prática.</p>
<h2>Pilar 1: A superfície de instalação<span class="hx:absolute hx:-mt-20" id="pilar-1-a-superfície-de-instalação"></span>
    <a href="#pilar-1-a-superf%c3%adcie-de-instala%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Essa é a mais importante e a mais negligenciada. As pessoas passam semanas afinando o algoritmo interno e aí soltam um README que diz &ldquo;clone o repo e rode <code>cargo build --release</code>&rdquo;. Aí o sujeito que tava curioso pra testar precisa ter Rust instalado, esperar dez minutos de compilação, e torcer pra não faltar uma lib de sistema. Você acabou de perder 90% dos seus usuários potenciais.</p>
<p>O ideal é oferecer <strong>vários caminhos</strong>, e deixar o usuário escolher o que combina com o ambiente dele. Olha o cabeçalho de instalação do ai-jail, que é o meu campeão de opções:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="c1"># Homebrew (macOS ou Linux)</span>
</span></span><span class="line"><span class="cl">brew tap akitaonrails/tap <span class="o">&amp;&amp;</span> brew install ai-jail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># AUR (Arch Linux) - binário pré-compilado</span>
</span></span><span class="line"><span class="cl">yay -S ai-jail-bin
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># cargo (qualquer plataforma com Rust)</span>
</span></span><span class="line"><span class="cl">cargo install ai-jail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># mise</span>
</span></span><span class="line"><span class="cl">mise use -g github:akitaonrails/ai-jail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Download direto do binário</span>
</span></span><span class="line"><span class="cl">curl -fsSL https://github.com/akitaonrails/ai-jail/releases/latest/download/ai-jail-linux-x86_64.tar.gz <span class="p">|</span> tar xz</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Cinco caminhos. O usuário de Arch usa <code>yay</code>. O do macOS usa <code>brew</code>. O dev Rust usa <code>cargo install</code>. Quem usa <a href="https://github.com/jdx/mise"target="_blank" rel="noopener">mise</a> pra gerenciar versões pega via mise. E quem só quer o binário baixa o tarball direto da release. Cada um pega o que já está acostumado, ninguém é forçado a instalar uma cadeia de ferramentas só pra experimentar.</p>
<h3>O segredo: compile uma vez, reempacote várias<span class="hx:absolute hx:-mt-20" id="o-segredo-compile-uma-vez-reempacote-várias"></span>
    <a href="#o-segredo-compile-uma-vez-reempacote-v%c3%a1rias" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A pegadinha que assusta no começo é achar que cada formato de pacote exige um build separado. Não exige. O truque é <strong>compilar o binário uma vez por arquitetura, e depois reempacotar o mesmo binário em todos os formatos.</strong></p>
<p>No meu pipeline em Rust, o job de build gera um tarball com o binário pronto e o <code>sha256</code>. As etapas de empacotamento seguintes (Docker, AUR, Homebrew) <strong>baixam esse tarball já compilado</strong> ao invés de recompilar. Reduz tempo de CI, garante que o byte que vai pro AUR é exatamente o mesmo que vai pro Docker, e elimina aquela classe de bug &ldquo;funciona num formato e quebra no outro&rdquo;.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># Job de Docker reaproveitando o binário já compilado</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">docker-amd64</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="l">build-linux</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/download-artifact@v4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ai-memory-linux-x86_64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">artifacts</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Desempacota o binário pré-compilado</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">tar -xzf artifacts/ai-memory-linux-x86_64.tar.gz -C dist/docker/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">docker/build-push-action@v6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">target</span><span class="p">:</span><span class="w"> </span><span class="l">runtime-prebuilt-amd64 </span><span class="w"> </span><span class="c"># injeta o binário, não recompila</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O job do AUR faz a mesma coisa: baixa o tarball, calcula o <code>sha256</code>, e injeta no <code>PKGBUILD</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nv">X86_SHA</span><span class="o">=</span><span class="k">$(</span>awk <span class="s1">&#39;{print $1}&#39;</span> artifacts/ai-usagebar-linux-x86_64.tar.gz.sha256<span class="k">)</span>
</span></span><span class="line"><span class="cl">sed -i <span class="s2">&#34;s/^sha256sums_x86_64=.*/sha256sums_x86_64=(&#39;</span><span class="nv">$X86_SHA</span><span class="s2">&#39;)/&#34;</span> packaging/aur/PKGBUILD-bin</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Esse é o conceito central. O binário é o artefato de verdade. RPM, DEB, AppImage, AUR, tarball Homebrew, imagem Docker, tudo isso é só <strong>embalagem em volta do mesmo binário</strong>.</p>
<p>Os dois trechos acima são recortes. Se você quiser ver o fluxo inteiro funcionando, o build-once-reuse completo (binário compilado uma vez, reaproveitado nos jobs de Docker e AUR) está no <a href="https://github.com/akitaonrails/ai-memory/blob/main/.github/workflows/release.yml"target="_blank" rel="noopener"><code>release.yml</code> do ai-memory</a>, e a injeção de <code>sha256</code> no <code>PKGBUILD</code> está no <a href="https://github.com/akitaonrails/ai-usagebar/blob/main/.github/workflows/release.yml"target="_blank" rel="noopener"><code>release.yml</code> do ai-usagebar</a>.</p>
<h3>O cardápio de formatos<span class="hx:absolute hx:-mt-20" id="o-cardápio-de-formatos"></span>
    <a href="#o-card%c3%a1pio-de-formatos" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Cada plataforma tem seu mundo. Segue o mínimo que vale conhecer:</p>
<p><strong>Linux</strong> é o mais fragmentado. As opções, do mais simples pro mais trabalhoso:</p>
<ul>
<li><strong>Tarball</strong> (<code>.tar.gz</code>): o denominador comum. Funciona em qualquer distro, é só descompactar e jogar no <code>PATH</code>. Sempre ofereça esse.</li>
<li><strong>AUR</strong> (Arch): dois pacotes, o <code>-bin</code> (binário pré-compilado, instala em segundos) e o de source (compila na máquina do usuário). Eu publico os dois com a action <code>KSXGitHub/github-actions-deploy-aur</code>. O <code>-bin</code> é o que todo mundo usa.</li>
<li><strong>AppImage</strong>: um arquivo único executável que carrega todas as dependências dentro. Bom pra apps desktop. O Frank Sherlock, que é Tauri, gera AppImage direto no build e eu ainda assino com GPG depois (o passo de assinatura está no <a href="https://github.com/akitaonrails/FrankSherlock/blob/master/.github/workflows/release.yml"target="_blank" rel="noopener"><code>release.yml</code> dele</a>). Pra binário simples dá pra usar <code>appimagetool</code>.</li>
<li><strong>RPM e DEB</strong>: pra Fedora/openSUSE e Debian/Ubuntu. A ferramenta que poupa sua vida aqui é o <a href="https://github.com/goreleaser/nfpm"target="_blank" rel="noopener">nfpm</a>, que gera os dois a partir de um único <code>nfpm.yaml</code> declarativo, sem precisar entender as entranhas de <code>rpmbuild</code> ou <code>dpkg-deb</code>. Pega o binário já compilado e cospe os dois pacotes.</li>
</ul>
<p><strong>macOS</strong> tem dois alvos: Apple Silicon (<code>aarch64-apple-darwin</code>) e Intel (<code>x86_64-apple-darwin</code>). Você compila os dois separados ou gera um binário universal com <code>lipo</code>. E aqui mora a maior dor de cabeça da plataforma: <strong>assinatura e notarização</strong>. Sem isso, o Gatekeeper do macOS recusa abrir seu app. O fluxo é decodificar o certificado P12, importar num keychain temporário, assinar com <code>codesign --options runtime --timestamp</code>, e notarizar:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">codesign --sign <span class="s2">&#34;</span><span class="nv">$APPLE_SIGNING_IDENTITY</span><span class="s2">&#34;</span> --options runtime --timestamp ./meu-binario
</span></span><span class="line"><span class="cl">ditto -c -k meu-binario notarize.zip
</span></span><span class="line"><span class="cl">xcrun notarytool submit notarize.zip <span class="se">\
</span></span></span><span class="line"><span class="cl">  --apple-id <span class="s2">&#34;</span><span class="nv">$APPLE_ID</span><span class="s2">&#34;</span> --password <span class="s2">&#34;</span><span class="nv">$APPLE_PASSWORD</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --team-id <span class="s2">&#34;</span><span class="nv">$APPLE_TEAM_ID</span><span class="s2">&#34;</span> --wait</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso exige uma conta paga de Apple Developer. É chato, mas é o preço de não dar erro de &ldquo;app danificado&rdquo; na cara do usuário. O job inteiro de assinatura e notarização, com a dança do keychain temporário, está no <a href="https://github.com/akitaonrails/ai-jail/blob/master/.github/workflows/release.yml"target="_blank" rel="noopener"><code>release.yml</code> do ai-jail</a>.</p>
<p><strong>Windows</strong> é o mais simples de buildar (<code>x86_64-pc-windows-msvc</code>) e o mais espinhoso de assinar. Certificado de code signing pra Windows é caro e burocrático, então eu, sinceramente, não assino. Aceito que o SmartScreen vai dar um aviso na primeira execução e deixo isso claro no README. Pra projeto pessoal de código aberto, está de bom tamanho.</p>
<p><strong>Rust crates</strong> (<code>cargo publish</code>): se é uma biblioteca ou CLI em Rust, publicar no <a href="https://crates.io"target="_blank" rel="noopener">crates.io</a> dá o <code>cargo install</code> de graça. Uma linha no pipeline. Dica: torne idempotente, porque rodar de novo num tag já publicado dá erro. Eu faço um <code>grep</code> na saída procurando &ldquo;already exists&rdquo; pra não quebrar a release inteira por causa disso.</p>
<p><strong>Homebrew</strong>: a forma limpa é manter um tap próprio (<code>akitaonrails/homebrew-tap</code>). O pipeline atualiza a fórmula com a nova URL e o novo <code>sha256</code> a cada release. O usuário faz <code>brew tap</code> uma vez e depois é só <code>brew upgrade</code>.</p>
<p><strong>mise</strong>: o <a href="https://github.com/jdx/mise"target="_blank" rel="noopener">mise</a> consegue instalar direto de releases do GitHub sem você configurar nada do seu lado, contanto que seus tarballs sigam uma convenção de nome de plataforma. Você ganha esse caminho praticamente de graça só por ter releases bem nomeadas.</p>
<p>Repara num detalhe: AUR, Homebrew, mise e o download direto <strong>todos consomem o mesmo tarball da release</strong>. Você publica os binários uma vez, e quatro gerenciadores de pacote diferentes sabem se virar. É por isso que &ldquo;compile uma vez, reempacote várias&rdquo; é a regra de ouro.</p>
<h2>Pilar 2: GitHub Actions, releases por tag e changelog<span class="hx:absolute hx:-mt-20" id="pilar-2-github-actions-releases-por-tag-e-changelog"></span>
    <a href="#pilar-2-github-actions-releases-por-tag-e-changelog" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A segunda coisa que torna um projeto de código aberto sério é CI. Esquece o selo de &ldquo;build passing&rdquo; no README, o valor está no chão comum que ele cria: quando alguém manda um pull request, o CI roda os testes, o linter e o scanner de segurança, e você sabe na hora se aquela contribuição quebra alguma coisa. Sem isso, todo PR vira uma negociação manual e cansativa.</p>
<p>Nos meus projetos Rust o CI roda <code>cargo fmt --check</code>, <code>cargo clippy -D warnings</code>, <code>cargo test</code>, e <code>cargo audit</code>. Nos projetos Rails, é <code>rubocop</code>, <code>brakeman</code> (scanner de segurança), <code>bundler-audit</code> e a suíte de testes em Minitest. O <a href="https://github.com/akitaonrails/FrankMD"target="_blank" rel="noopener">FrankMD</a> sozinho tem mais de 1800 testes entre Ruby e JavaScript. O Frank Sherlock passa de 300 testes em Rust mais 300 no frontend. É essa rede de segurança que me deixa aceitar PR de estranho sem medo.</p>
<h3>Revisão de PR com LLM<span class="hx:absolute hx:-mt-20" id="revisão-de-pr-com-llm"></span>
    <a href="#revis%c3%a3o-de-pr-com-llm" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Os testes e o CI são a rede de baixo. Acima dela tem uma camada que virou central no meu fluxo: usar o próprio LLM pra revisar os pull requests. O Opus e o GPT são absurdamente bons em code review e em refatoração de qualidade. Melhores que eu na consistência, porque não cansam e não passam o olho por cima de 200 linhas só porque já é a quinta revisão do dia.</p>
<p>Eu tenho dois prompts que uso o tempo todo. O primeiro, pra um PR específico que acabou de chegar:</p>
<blockquote>
  <p>olha o PR aberto no github. não confie na descrição do autor. audita o código a fundo pra ver se faz sentido. garante que não tem regressão nem queda na qualidade do código. garante que a cobertura de testes está adequada. depois me diz o que você acha que a gente deveria fazer.</p>

</blockquote>
<p>Repara no &ldquo;não confie na descrição do autor&rdquo;. Isso é de propósito. A descrição do PR conta o que a pessoa achou que fez, e nem sempre bate com o que ela realmente fez. O LLM tem que ler o código de verdade, não o resumo.</p>
<p>O segundo prompt é pra depois de uma sessão longa de código ou de vários PRs mergeados de uma vez:</p>
<blockquote>
  <p>a gente mexeu em muito código. audita o código que esses PRs mudaram, procura código morto, duplicação desnecessária, valor mágico hardcoded que deveria ser constante e/ou estar melhor documentado, falta de cobertura de teste, mira nos princípios de <a href="/2026/04/20/clean-code-para-agentes-de-ia/">clean code</a>, e depois verifica se a documentação foi devidamente atualizada.</p>

</blockquote>
<p>Com o parecer na mão, eu decido. Pra PR incompleto, o que eu faço depende do tamanho e da minha prioridade naquele momento. Se é coisa pequena, eu mesmo corrijo direto no PR com um <code>amend</code>, respondo explicando o que mudei, e aí mergeio e fecho. Quando dá pra mergear mas não dá pra corrigir antes, mergeio e já solto um commit de correção logo em seguida. E quando é mudança grande, peço pro LLM responder o PR explicando por que a gente não vai mergear, com a direção certa do que precisa ser feito.</p>
<p>Foi assim que eu fechei quase 40 PRs e 20 issues só no ai-memory, em poucos dias. Esse volume não sai na mão, e também não pode sair no piloto automático.</p>
<p>E é nesse ponto que eu mais bato: nada disso é cego. Eu estou o tempo todo pilotando o LLM na direção desses objetivos. Ele faz a auditoria, joga as sugestões na mesa e até escreve o texto da resposta, mas quem decide mergear, corrigir ou recusar sou eu. O LLM é o revisor incansável; eu sou quem assina embaixo.</p>
<h3>O gatilho por tag<span class="hx:absolute hx:-mt-20" id="o-gatilho-por-tag"></span>
    <a href="#o-gatilho-por-tag" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A parte de release é separada do CI. Enquanto o CI roda a cada push e PR, a release dispara <strong>só quando você empurra um tag de versão</strong>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">tags</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s1">&#39;v*.*.*&#39;</span><span class="w">   </span><span class="c"># dispara em v1.2.3, v0.5.0, etc.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">workflow_dispatch</span><span class="p">:</span><span class="w"> </span><span class="c"># permite disparar manualmente também</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O fluxo de soltar uma versão vira isso:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">git tag v0.5.0
</span></span><span class="line"><span class="cl">git push origin v0.5.0</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E pronto. O GitHub Actions percebe o tag, dispara o workflow de release, compila pros alvos, empacota tudo, assina o que precisa assinar, cria o GitHub Release e publica nos gerenciadores de pacote. Eu não toco em mais nada. Inclusive é literalmente isso que eu peço pro Claude Code: &ldquo;solta a v0.5.0&rdquo;. Ele faz <code>cargo set-version</code>, atualiza o CHANGELOG, commita, cria o tag, empurra, e o resto é o pipeline.</p>
<p>A matriz de build em Rust compila cada alvo na plataforma nativa dele:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">strategy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">matrix</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">include</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">os</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-22.04</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">target</span><span class="p">:</span><span class="w"> </span><span class="l">x86_64-unknown-linux-gnu</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">os</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-22.04-arm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">target</span><span class="p">:</span><span class="w"> </span><span class="l">aarch64-unknown-linux-gnu</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">os</span><span class="p">:</span><span class="w"> </span><span class="l">macos-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">target</span><span class="p">:</span><span class="w"> </span><span class="l">aarch64-apple-darwin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">os</span><span class="p">:</span><span class="w"> </span><span class="l">windows-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">target</span><span class="p">:</span><span class="w"> </span><span class="l">x86_64-pc-windows-msvc</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Uma pegadinha de cross-compilation: se você compila ARM dentro de uma máquina x86 (via <a href="https://github.com/cross-rs/cross"target="_blank" rel="noopener">cross</a>), você <strong>não consegue rodar os testes</strong> daquele binário ali, porque a arquitetura não bate. A solução é rodar os testes só no alvo nativo e cross-compilar o resto sem testar. O ai-usagebar faz exatamente isso, x86 nativo e ARM via <code>cross</code>, na matriz do <a href="https://github.com/akitaonrails/ai-usagebar/blob/main/.github/workflows/release.yml"target="_blank" rel="noopener"><code>release.yml</code> dele</a>. E sempre use cache de compilação: o <code>Swatinem/rust-cache</code> corta o tempo de build pela metade fácil.</p>
<h3>Release notes e changelog importam<span class="hx:absolute hx:-mt-20" id="release-notes-e-changelog-importam"></span>
    <a href="#release-notes-e-changelog-importam" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Aqui é onde muita gente relaxa, e não devia. Quando você solta uma release, a pessoa que está pensando em atualizar quer saber <strong>o que mudou</strong>. &ldquo;v0.5.0&rdquo; não diz nada. Um changelog diz.</p>
<p>Eu mantenho um <code>CHANGELOG.md</code> no formato <a href="https://keepachangelog.com/"target="_blank" rel="noopener">Keep a Changelog</a>, com uma seção por versão. E o pipeline extrai automaticamente a seção da versão atual pra colar no corpo do GitHub Release. Um <code>awk</code> simples resolve:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">awk -v <span class="nv">ver</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$VERSION</span><span class="s2">&#34;</span> <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">  BEGIN { hdr = &#34;## [&#34; ver &#34;]&#34; }
</span></span></span><span class="line"><span class="cl"><span class="s1">  index($0, hdr) == 1 { flag=1; next }
</span></span></span><span class="line"><span class="cl"><span class="s1">  flag &amp;&amp; index($0, &#34;## [&#34;) == 1 { exit }
</span></span></span><span class="line"><span class="cl"><span class="s1">  flag { print }
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;</span> CHANGELOG.md &gt; changelog_section.md</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Esse <code>awk</code> é o mesmo que roda na release do ai-usagebar (de novo no <a href="https://github.com/akitaonrails/ai-usagebar/blob/main/.github/workflows/release.yml"target="_blank" rel="noopener"><code>release.yml</code> dele</a>, que é o meu mais caprichado nessa parte). Aí eu anexo, depois do changelog, as instruções de instalação e os checksums <code>sha256</code> de cada artefato. O resultado é um GitHub Release que se explica sozinho: o que mudou, como instalar, e como verificar a integridade do download. O usuário lê e decide se atualiza. Esse é o tipo de cuidado que faz um estranho confiar no seu projeto.</p>
<h2>Pilar 3: Deploy de verdade, sem complicação<span class="hx:absolute hx:-mt-20" id="pilar-3-deploy-de-verdade-sem-complicação"></span>
    <a href="#pilar-3-deploy-de-verdade-sem-complica%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Os projetos que rodam como serviço (os Rails, principalmente) precisam ir pra algum lugar. E aqui eu vou na contramão do hype. Não tem Kubernetes. Não tem Kamal. Não tem pipeline de deploy de três estágios com aprovação manual.</p>
<p>Eu tenho um home server. Um <a href="/2026/03/31/migrando-meu-home-server-com-claude-code/">Minisforum MS-S1 Max rodando openSUSE MicroOS</a> que eu migrei recentemente, com um <a href="https://github.com/go-gitea/gitea"target="_blank" rel="noopener">Gitea</a> servindo de registry Docker privado na porta 3007. Pra um desenvolvedor solo com um servidor caseiro simples, isso é mais que suficiente. Toda a cerimônia de orquestração de cluster é resolver um problema que eu não tenho.</p>
<p>O deploy inteiro cabe num único <code>bin/deploy</code>. Ele faz três coisas: builda a imagem, empurra pro registry, e dá um SSH no servidor pra subir a versão nova:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Config vem de um arquivo .gitignored, fora do repositório</span>
</span></span><span class="line"><span class="cl"><span class="nb">source</span> <span class="s2">&#34;</span><span class="k">$(</span>dirname <span class="s2">&#34;</span><span class="nv">$0</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">/../config/deploy.env&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">IMAGE</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">REGISTRY_HOST</span><span class="si">}</span><span class="s2">:</span><span class="si">${</span><span class="nv">REGISTRY_PORT</span><span class="si">}</span><span class="s2">/akitaonrails/frank_fbi:latest&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;==&gt; Buildando a imagem...&#34;</span>
</span></span><span class="line"><span class="cl">docker build -t <span class="s2">&#34;</span><span class="nv">$IMAGE</span><span class="s2">&#34;</span> .
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;==&gt; Empurrando pro registry...&#34;</span>
</span></span><span class="line"><span class="cl">docker push <span class="s2">&#34;</span><span class="nv">$IMAGE</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;==&gt; Subindo no servidor...&#34;</span>
</span></span><span class="line"><span class="cl">ssh <span class="s2">&#34;</span><span class="si">${</span><span class="nv">DEPLOY_USER</span><span class="si">}</span><span class="s2">@</span><span class="si">${</span><span class="nv">DEPLOY_HOST</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;cd </span><span class="si">${</span><span class="nv">REMOTE_DIR</span><span class="si">}</span><span class="s2"> &amp;&amp; \
</span></span></span><span class="line"><span class="cl"><span class="s2">   docker compose pull &amp;&amp; \
</span></span></span><span class="line"><span class="cl"><span class="s2">   docker compose down --remove-orphans &amp;&amp; \
</span></span></span><span class="line"><span class="cl"><span class="s2">   docker compose up -d&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>É isso. <code>build</code>, <code>push</code>, <code>ssh</code> com <code>pull</code>, <code>down</code>, <code>up</code>. Quatro comandos no servidor remoto, encadeados num SSH só. Sem agente rodando lá, sem webhook, sem painel de controle. O servidor tem um <code>docker-compose.production.yml</code> que aponta pra imagem <code>:latest</code> do registry, e o <code>pull</code> traz a versão nova.</p>
<p>Repara que os dados sensíveis ficam num <code>config/deploy.env</code> que <strong>não vai pro Git</strong>. No repositório vai só o <code>config/deploy.env.example</code>, com a estrutura e valores fictícios. Esse padrão de &ldquo;exemplo versionado, real ignorado&rdquo; se repete em todo projeto meu, e é o que permite o projeto ser público sem vazar host, usuário ou caminho do meu servidor.</p>
<p>A imagem em si vem de um Dockerfile multi-estágio. O estágio de build instala as ferramentas de compilação, builda tudo, e o estágio final copia só o resultado pra uma imagem enxuta, rodando como usuário não-root. O compose de produção troca <code>build:</code> por <code>image:</code>, aponta os volumes pros caminhos persistentes do servidor (<code>/var/opt/docker/...</code>) e usa <code>restart: always</code> pra subir tudo de novo se o servidor reiniciar.</p>
<p>E de novo a beleza da padronização: como <strong>todo</strong> projeto meu tem esse mesmo <code>bin/deploy</code>, eu posso falar pro Claude Code &ldquo;faz o deploy do Frank FBI&rdquo; e ele sabe exatamente o que rodar. Não preciso lembrar de comando nenhum. A ferramenta é a documentação executável do processo.</p>
<h2>A lição: ninguém liga pra sua stack<span class="hx:absolute hx:-mt-20" id="a-lição-ninguém-liga-pra-sua-stack"></span>
    <a href="#a-li%c3%a7%c3%a3o-ningu%c3%a9m-liga-pra-sua-stack" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Agora a parte que eu mais queria escrever, e que vale mais que todos os snippets de YAML acima.</p>
<p>A maioria das pessoas, quando vai apresentar um projeto, concentra tudo na tecnologia. &ldquo;Olha meu banco vetorial novinho.&rdquo; &ldquo;Olha meu framework reativo de última geração.&rdquo; &ldquo;Usei tal arquitetura de microsserviços.&rdquo; E enchem o topo do README com isso: a lista de dependências, o diagrama de componentes, a stack.</p>
<p>Ninguém liga.</p>
<p>Sério. O usuário que chega no seu README não quer saber qual banco de dados você usou. Ele quer saber <strong>que problema isso resolve pra ele</strong>. Detalhe de implementação não pertence ao topo do README. Pertence ao <code>docs/</code>, pra quem vai contribuir. No README, o que importa é o caso de uso.</p>
<p>Olha a diferença. &ldquo;Servidor MCP em Rust com FTS5 e consolidação opcional via LLM&rdquo; não diz nada pra ninguém. Agora &ldquo;memória de longo prazo compartilhada entre Claude Code, Codex e Cursor, com handoff entre sessões&rdquo; — isso o cara entende na hora, porque é a dor que ele tem. Os dois descrevem o ai-memory. Um fala de tecnologia, o outro fala do problema resolvido.</p>
<p>O caso de uso é a coisa mais importante do projeto inteiro. Antes de escrever uma linha de código, a pergunta certa é &ldquo;que problema isso resolve, e pra quem?&rdquo;. Se você não consegue responder isso numa frase, você está construindo uma <strong>solução em busca de um problema</strong>. E solução em busca de problema é, na prática, solução que não resolve nada. Não importa quão elegante seja o código por dentro.</p>
<p>A camada de UX, a superfície que o usuário toca, vale muito mais que a engenharia interna. Um projeto com tecnologia medíocre mas que é trivial de instalar, que diz claramente que problema resolve, e que tem documentação honesta, vai mais longe que um projeto genial que ninguém consegue rodar. Toda vez.</p>
<p>Então o mínimo, o verdadeiro mínimo pra um projeto de código aberto, é isto: instalação fácil, CI que dá confiança, documentação que começa pelo problema. O algoritmo bonito vem muito depois disso na fila de prioridades. Os três pilares deste post existem pra servir uma coisa só, que é a experiência de quem chega de fora. Acerta isso primeiro. O resto é detalhe de implementação.</p>
]]></content:encoded><category>open-source</category><category>engenharia-de-software</category><category>devops</category></item><item><title>Migrando meu Home Server com Claude Code | openSUSE MicroOS</title><link>https://akitaonrails.github.io/2026/03/31/migrando-meu-home-server-com-claude-code/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/03/31/migrando-meu-home-server-com-claude-code/</guid><pubDate>Tue, 31 Mar 2026 16:00:00 -0300</pubDate><description>&lt;p&gt;Meu home server antigo era uma bagunça. Um Intel NUC com Ubuntu Server que eu fui remendando ao longo de dois anos. Containers com caminhos hardcoded, volumes montados em locais aleatórios (&lt;code&gt;/home/akitaonrails/docker/&lt;/code&gt;, &lt;code&gt;/home/akitaonrails/sonarr/&lt;/code&gt;, &lt;code&gt;/mnt/terachad/&lt;/code&gt;), docker-compose files espalhados sem padrão nenhum. Funcionava, mas se eu perdesse o disco, levaria dias pra reconstruir tudo de memória.&lt;/p&gt;
&lt;p&gt;Com o &lt;a href="https://akitaonrails.github.io/2026/03/31/review-minisforum-ms-s1-max-amd-ai-max-395/"&gt;novo Minisforum MS-S1 Max&lt;/a&gt; que comprei, decidi fazer a migração direito. E decidi usar Claude Code desde o início pra acelerar o processo. É um servidor caseiro, só eu uso, o risco de fazer besteira é baixo. Mas se fosse um servidor de produção real, eu jamais faria isso sem review humano rigoroso em cada passo.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Meu home server antigo era uma bagunça. Um Intel NUC com Ubuntu Server que eu fui remendando ao longo de dois anos. Containers com caminhos hardcoded, volumes montados em locais aleatórios (<code>/home/akitaonrails/docker/</code>, <code>/home/akitaonrails/sonarr/</code>, <code>/mnt/terachad/</code>), docker-compose files espalhados sem padrão nenhum. Funcionava, mas se eu perdesse o disco, levaria dias pra reconstruir tudo de memória.</p>
<p>Com o <a href="/2026/03/31/review-minisforum-ms-s1-max-amd-ai-max-395/">novo Minisforum MS-S1 Max</a> que comprei, decidi fazer a migração direito. E decidi usar Claude Code desde o início pra acelerar o processo. É um servidor caseiro, só eu uso, o risco de fazer besteira é baixo. Mas se fosse um servidor de produção real, eu jamais faria isso sem review humano rigoroso em cada passo.</p>
<p>O que segue é o relato da migração e o guia pra quem quiser replicar. Se um dia eu precisar reconstruir do zero, esse post é a documentação.</p>
<h2>Escolha do sistema operacional<span class="hx:absolute hx:-mt-20" id="escolha-do-sistema-operacional"></span>
    <a href="#escolha-do-sistema-operacional" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Por que não Ubuntu Server de novo<span class="hx:absolute hx:-mt-20" id="por-que-não-ubuntu-server-de-novo"></span>
    <a href="#por-que-n%c3%a3o-ubuntu-server-de-novo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Eu usei Ubuntu Server no NUC por praticidade. Mas <code>do-release-upgrade</code> é uma roleta russa. Toda vez que o Ubuntu lança versão nova, a atualização é um risco real de quebrar coisas. Pacotes mudam, configs são sobrescritas, dependências conflitam. Pra um servidor que precisa estar sempre rodando, isso é inaceitável.</p>
<h3>Por que não Arch Linux<span class="hx:absolute hx:-mt-20" id="por-que-não-arch-linux"></span>
    <a href="#por-que-n%c3%a3o-arch-linux" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Eu uso Arch no desktop e gosto. Mas Arch é uma rolling release sem nenhuma garantia de estabilidade. Pra um desktop onde eu posso parar e resolver problemas, ótimo. Pra um servidor headless que roda 49 containers Docker e precisa funcionar depois de cada reboot, não.</p>
<h3>Fedora CoreOS vs openSUSE MicroOS<span class="hx:absolute hx:-mt-20" id="fedora-coreos-vs-opensuse-microos"></span>
    <a href="#fedora-coreos-vs-opensuse-microos" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>As duas opções modernas pra servidor de containers são Fedora CoreOS e openSUSE MicroOS. As duas são sistemas imutáveis: o root filesystem é read-only, atualizações são atômicas (ou aplicam inteiro ou não aplicam nada), e rollback é instantâneo.</p>
<p>A diferença: Fedora CoreOS usa Ignition (configuração declarativa antes do primeiro boot) e é projetada pra ser provisionada automaticamente. MicroOS usa <code>transactional-update</code> e permite uso interativo normal. Pra um home server onde eu quero SSH e mexer manualmente quando precisar, MicroOS se encaixa melhor.</p>
<h3>O que torna MicroOS diferente<span class="hx:absolute hx:-mt-20" id="o-que-torna-microos-diferente"></span>
    <a href="#o-que-torna-microos-diferente" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O conceito de sistema imutável muda como você opera o servidor:</p>
<p>Toda instalação de pacote ou edição de <code>/etc</code> passa por <code>transactional-update</code>, que cria um snapshot btrfs novo, aplica a mudança nesse snapshot, e no reboot seguinte o sistema boota no snapshot atualizado. Se a mudança quebrar alguma coisa, você faz <code>transactional-update rollback</code> e volta pro snapshot anterior em segundos.</p>
<p>Atualizações são automáticas e diárias. O <code>transactional-update.timer</code> baixa patches, cria snapshot, e o <code>rebootmgr</code> reinicia numa janela configurada (no meu caso, entre 4h e 5h30 da manhã). Se a atualização quebra o boot, o GRUB automaticamente volta pro snapshot anterior.</p>
<p>SELinux vem enforcing por padrão. Isso causou 90% dos problemas durante a migração, mas é a configuração certa pra segurança.</p>
<h2>Setup inicial<span class="hx:absolute hx:-mt-20" id="setup-inicial"></span>
    <a href="#setup-inicial" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Hardware<span class="hx:absolute hx:-mt-20" id="hardware"></span>
    <a href="#hardware" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>AMD Ryzen AI Max+ 395, 128GB LPDDR5X</li>
<li>96GB alocados como VRAM via BIOS (UMA Frame Buffer Size)</li>
<li>NVMe de 2TB (sistema + Docker)</li>
<li>Rede cabeada 2.5Gbps</li>
<li>Synology DS1821+ NAS em 192.168.0.21 (NFS)</li>
</ul>
<h3>Primeiros passos<span class="hx:absolute hx:-mt-20" id="primeiros-passos"></span>
    <a href="#primeiros-passos" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Instalação do MicroOS é padrão. Depois:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Criar usuário com UID que bate com o NAS (pra NFS funcionar sem problemas de permissão)</span>
</span></span><span class="line"><span class="cl">useradd -u <span class="m">1026</span> -m akitaonrails
</span></span><span class="line"><span class="cl">passwd akitaonrails
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Configurar sudo (dentro de transactional-update shell)</span>
</span></span><span class="line"><span class="cl">sudo transactional-update shell
</span></span><span class="line"><span class="cl"><span class="c1"># dentro: adicionar akitaonrails ao sudoers</span>
</span></span><span class="line"><span class="cl"><span class="nb">exit</span>
</span></span><span class="line"><span class="cl">sudo reboot</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>NFS do Synology<span class="hx:absolute hx:-mt-20" id="nfs-do-synology"></span>
    <a href="#nfs-do-synology" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O NAS Synology exporta <code>/volume1/TERACHAD</code> via NFS. O ponto de montagem no MicroOS é <code>/var/mnt/terachad</code> (não <code>/mnt/</code>, que fica no root imutável).</p>
<p>No <code>/etc/fstab</code> (aplicado via transactional-update):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>192.168.0.21:/volume1/TERACHAD /var/mnt/terachad nfs4 nfsvers=4.1,rsize=262144,wsize=262144,hard,_netdev 0 0</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Detalhes que importam: <code>nfsvers=4.1</code> porque 4.2 não funcionou com o Synology. <code>rsize=262144,wsize=262144</code> (256KB buffers) foi a maior melhoria de performance NFS. <code>hard</code> em vez de <code>nofail</code> pra que o mount retente indefinidamente se o NAS desconectar temporariamente.</p>
<h3>GPU / ROCm<span class="hx:absolute hx:-mt-20" id="gpu--rocm"></span>
    <a href="#gpu--rocm" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Esse passo deu trabalho. O Radeon 8060S do AI Max+ 395 é gfx1151, que o ROCm não suporta oficialmente. Foram necessários três passos, e os três são obrigatórios:</p>
<ol>
<li>BIOS: setar UMA Frame Buffer Size pra 96GB</li>
<li>Kernel: adicionar <code>amdttm.pages_limit=25165824 amdttm.page_pool_size=25165824</code> em <code>/etc/kernel/cmdline</code></li>
<li>Docker: usar <code>HSA_OVERRIDE_GFX_VERSION=11.5.1</code> em todo container ROCm</li>
</ol>
<p>Sem o passo 2, o ROCm só vê 15.5GB mesmo com a alocação do BIOS. Os números são 96GB / 4KB (page size) = 25.165.824 pages.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo transactional-update shell
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;amdttm.pages_limit=25165824 amdttm.page_pool_size=25165824&#34;</span> &gt;&gt; /etc/kernel/cmdline
</span></span><span class="line"><span class="cl"><span class="nb">exit</span>
</span></span><span class="line"><span class="cl">sudo sdbootutil update-all-entries  <span class="c1"># FORA do transactional shell</span>
</span></span><span class="line"><span class="cl">sudo reboot</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Verificação:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat /sys/class/drm/card1/device/mem_info_vram_total
</span></span><span class="line"><span class="cl"><span class="c1"># 103079215104 (96 * 1024^3)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>Docker no MicroOS<span class="hx:absolute hx:-mt-20" id="docker-no-microos"></span>
    <a href="#docker-no-microos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo transactional-update --non-interactive pkg install docker
</span></span><span class="line"><span class="cl">sudo reboot
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> --now docker
</span></span><span class="line"><span class="cl">sudo usermod -aG docker akitaonrails
</span></span><span class="line"><span class="cl"><span class="c1"># logout e login pra o grupo pegar</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Instalar docker-compose standalone (o pacote openSUSE não inclui)</span>
</span></span><span class="line"><span class="cl">sudo curl -L <span class="s2">&#34;https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -o /usr/local/bin/docker-compose
</span></span><span class="line"><span class="cl">sudo chmod +x /usr/local/bin/docker-compose
</span></span><span class="line"><span class="cl">mkdir -p ~/.docker/cli-plugins
</span></span><span class="line"><span class="cl">ln -s /usr/local/bin/docker-compose ~/.docker/cli-plugins/docker-compose</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>daemon.json<span class="hx:absolute hx:-mt-20" id="daemonjson"></span>
    <a href="#daemonjson" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;log-level&#34;</span><span class="p">:</span> <span class="s2">&#34;warn&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;log-driver&#34;</span><span class="p">:</span> <span class="s2">&#34;local&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;log-opts&#34;</span><span class="p">:</span> <span class="p">{</span><span class="nt">&#34;max-size&#34;</span><span class="p">:</span> <span class="s2">&#34;10m&#34;</span><span class="p">,</span> <span class="nt">&#34;max-file&#34;</span><span class="p">:</span> <span class="s2">&#34;5&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;selinux-enabled&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;live-restore&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;userland-proxy&#34;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;exec-opts&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;native.cgroupdriver=systemd&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>live-restore: true</code> faz containers sobreviverem restart do daemon Docker. <code>userland-proxy: false</code> usa iptables direto em vez de processos proxy (menos overhead). <code>selinux-enabled: true</code> é obrigatório no MicroOS.</p>
<h2>SELinux e Docker: a maior fonte de problemas<span class="hx:absolute hx:-mt-20" id="selinux-e-docker-a-maior-fonte-de-problemas"></span>
    <a href="#selinux-e-docker-a-maior-fonte-de-problemas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Isso merece uma seção inteira porque foi responsável por 90% dos bugs durante a migração.</p>
<p>No MicroOS com SELinux enforcing, todo container que escreve em diretório bind-mounted do host precisa de tratamento especial. Existem duas abordagens: o sufixo <code>:Z</code> nos volumes e a opção <code>security_opt: label:disable</code>.</p>
<h3>NUNCA use <code>:Z</code>. Use <code>security_opt: label:disable</code>.<span class="hx:absolute hx:-mt-20" id="nunca-use-z-use-security_opt-labeldisable"></span>
    <a href="#nunca-use-z-use-security_opt-labeldisable" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O <code>:Z</code> diz pro Docker relabeling o diretório do host com o contexto SELinux do container. Parece a coisa certa. Na prática:</p>
<ul>
<li>Bancos SQLite quebram. O relabeling muda o contexto do arquivo e o SQLite pode recusar abrir o WAL journal.</li>
<li>Mounts NFS ignoram <code>:Z</code> silenciosamente. O NFS não suporta xattrs do SELinux. O kernel ignora o flag sem erro, mas o container continua sem permissão.</li>
<li>Mounts <code>:ro,Z</code> tentam relabeling mesmo sendo read-only, o que falha em NFS e pode corromper contexto em paths locais.</li>
</ul>
<p>A solução correta pra todo container nesse sistema:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">meuservico</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">security_opt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">label:disable    </span><span class="w"> </span><span class="c"># desliga enforcement SELinux pra esse container</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./data:/data     </span><span class="w"> </span><span class="c"># SEM :Z</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./config.yml:/etc/config.yml:ro </span><span class="w"> </span><span class="c"># SEM :Z mesmo em :ro</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>label:disable</code> desliga enforcement de labels SELinux apenas pra aquele container, não pro sistema inteiro. Combinado com o isolamento de rede e processos do Docker, é seguro pra home server.</p>
<h2>A migração dos stacks<span class="hx:absolute hx:-mt-20" id="a-migração-dos-stacks"></span>
    <a href="#a-migra%c3%a7%c3%a3o-dos-stacks" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Todos os stacks Docker foram reorganizados em <code>/var/opt/docker/&lt;stack&gt;/docker-compose.yml</code>. No servidor antigo, estavam espalhados em <code>/home/akitaonrails/docker/</code>, <code>/home/akitaonrails/&lt;servico&gt;/</code>, sem padrão.</p>
<h3>Substituições aplicadas em todos os compose files<span class="hx:absolute hx:-mt-20" id="substituições-aplicadas-em-todos-os-compose-files"></span>
    <a href="#substitui%c3%a7%c3%b5es-aplicadas-em-todos-os-compose-files" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Antes</th>
          <th>Depois</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>/mnt/terachad/</code></td>
          <td><code>/var/mnt/terachad/</code></td>
      </tr>
      <tr>
          <td><code>192.168.0.145</code></td>
          <td><code>192.168.0.90</code></td>
      </tr>
      <tr>
          <td><code>/home/akitaonrails/&lt;servico&gt;/</code></td>
          <td><code>/var/opt/docker/&lt;stack&gt;/&lt;servico&gt;/</code></td>
      </tr>
      <tr>
          <td><code>OLLAMA_BASE_URL=http://192.168.0.14:11434</code></td>
          <td><code>OLLAMA_BASE_URL=http://192.168.0.90:11434</code></td>
      </tr>
  </tbody>
</table>
<h3>Stack de media (Plex, Radarr, Sonarr, etc.)<span class="hx:absolute hx:-mt-20" id="stack-de-media-plex-radarr-sonarr-etc"></span>
    <a href="#stack-de-media-plex-radarr-sonarr-etc" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O stack de media é o mais complexo. Plex precisa de IP próprio na LAN (macvlan) pro streaming direto funcionar. O setup:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker network create -d macvlan <span class="se">\
</span></span></span><span class="line"><span class="cl">  --subnet<span class="o">=</span>192.168.0.0/24 <span class="se">\
</span></span></span><span class="line"><span class="cl">  --gateway<span class="o">=</span>192.168.0.1 <span class="se">\
</span></span></span><span class="line"><span class="cl">  -o <span class="nv">parent</span><span class="o">=</span>enp97s0 <span class="se">\
</span></span></span><span class="line"><span class="cl">  plex_macvlan</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No compose, o Plex precisa estar em duas redes: a macvlan (pro IP 192.168.0.6) e a bridge default (pra outros containers como Seerr conseguirem se comunicar):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">plex</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">plex_macvlan</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">ipv4_address</span><span class="p">:</span><span class="w"> </span><span class="m">192.168.0.6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">mac_address</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;02:42:c0:a8:00:06&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">default</span><span class="p">:</span><span class="w"> </span>{}<span class="w">    </span><span class="c"># obrigatório — sem isso, Seerr não enxerga o Plex</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Detalhe que quase me quebrou: o Plex guarda paths absolutos no banco de dados. Se o volume interno do container mudou de <code>/media</code> pra <code>/data</code>, o Plex não encontra mais nada. Tem que usar exatamente o mesmo mount target do compose antigo.</p>
<h3>Ollama com ROCm<span class="hx:absolute hx:-mt-20" id="ollama-com-rocm"></span>
    <a href="#ollama-com-rocm" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Stack novo, não existia no servidor anterior:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">ollama</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ollama/ollama:rocm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">ollama</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">devices</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">/dev/kfd:/dev/kfd</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">/dev/dri:/dev/dri</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">security_opt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">seccomp:unconfined</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">label:disable</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">group_add</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="s2">&#34;485&#34;</span><span class="w">   </span><span class="c"># render group GID</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="s2">&#34;488&#34;</span><span class="w">   </span><span class="c"># video group GID</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">HSA_OVERRIDE_GFX_VERSION=11.5.1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">PYTORCH_ROCM_ARCH=gfx1151</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">OLLAMA_KEEP_ALIVE=30m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">OLLAMA_NUM_PARALLEL=4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">OLLAMA_FLASH_ATTENTION=1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">OLLAMA_KV_CACHE_TYPE=q8_0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">/var/lib/ollama:/root/.ollama</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="s2">&#34;11434:11434&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>OLLAMA_FLASH_ATTENTION=1</code> ativa flash attention. <code>OLLAMA_KV_CACHE_TYPE=q8_0</code> usa KV cache em 8-bit, cortando bandwidth necessária por token pela metade. São otimizações de performance gratuitas.</p>
<h3>Monitoramento (Grafana + Prometheus)<span class="hx:absolute hx:-mt-20" id="monitoramento-grafana--prometheus"></span>
    <a href="#monitoramento-grafana--prometheus" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O Grafana usa named volume (<code>grafana_data</code>) que NÃO é incluído em backups normais de filesystem. Foi o motivo pelo qual perdi todos os dashboards na primeira tentativa. A solução é backup explícito do named volume:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># No servidor antigo:</span>
</span></span><span class="line"><span class="cl">docker run --rm -v grafana_data:/data:ro -v /tmp:/backup alpine <span class="se">\
</span></span></span><span class="line"><span class="cl">  tar czf /backup/grafana_data.tar.gz -C /data .
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Transferir e restaurar no novo:</span>
</span></span><span class="line"><span class="cl">docker run --rm -v grafana_data:/data -v /tmp:/backup alpine <span class="se">\
</span></span></span><span class="line"><span class="cl">  sh -c <span class="s2">&#34;cd /data &amp;&amp; tar xzf /backup/grafana_data.tar.gz&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Mesma coisa pro Portainer (<code>portainer_data</code>). Qualquer volume definido no bloco <code>volumes:</code> do compose sem host path precisa desse tratamento.</p>
<h3>Cloudflare Tunnel<span class="hx:absolute hx:-mt-20" id="cloudflare-tunnel"></span>
    <a href="#cloudflare-tunnel" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Eu uso <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/"target="_blank" rel="noopener">Cloudflare Tunnel</a> pra acessar todos os serviços fora de casa sem abrir portas no roteador. A migração foi a mais fácil: copiar o arquivo de credenciais JSON do túnel e o <code>config.yml</code>, atualizar os IPs de <code>.145</code> pra <code>.90</code>, e subir o container. O túnel mantém o mesmo ID, não precisa recriar DNS.</p>
<p>Os hostnames ficam em <code>config.yml</code>: portainer, grafana, plex, seerr, qbittorrent, syncthing, radarr, sonarr, bazarr, prowlarr, vault, gitea, kavita, e outros. Tudo acessível via <code>https://&lt;servico&gt;.example.com</code> de qualquer lugar.</p>
<h3>Gitea (registry de imagens)<span class="hx:absolute hx:-mt-20" id="gitea-registry-de-imagens"></span>
    <a href="#gitea-registry-de-imagens" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O <a href="https://github.com/go-gitea/gitea"target="_blank" rel="noopener">Gitea</a> funciona como registry Docker privado na porta 3007. Os projetos Frank FBI, Frank Mega, Frank Yomik e Mila têm imagens Docker que são buildadas e pushadas pro Gitea. Pra funcionar, o <code>daemon.json</code> do Docker precisa de:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;insecure-registries&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;192.168.0.90:3007&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O SSH do Gitea deu problema na migração: o app.ini antigo tinha <code>SSH_LISTEN_PORT=22</code>, mas o entrypoint do container também inicia sshd na porta 22. Conflito. Solução: <code>GITEA__server__SSH_LISTEN_PORT=2222</code> como variável de ambiente no compose.</p>
<h3>Todos os 49 containers rodando<span class="hx:absolute hx:-mt-20" id="todos-os-49-containers-rodando"></span>
    <a href="#todos-os-49-containers-rodando" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O servidor migrado roda 49 containers em 15 stacks. O stack de media sozinho tem 10 containers (<a href="https://www.plex.tv/"target="_blank" rel="noopener">Plex</a>, <a href="https://github.com/Radarr/Radarr"target="_blank" rel="noopener">Radarr</a>, <a href="https://github.com/Sonarr/Sonarr"target="_blank" rel="noopener">Sonarr</a>, <a href="https://github.com/morpheus65535/bazarr"target="_blank" rel="noopener">Bazarr</a>, <a href="https://github.com/Prowlarr/Prowlarr"target="_blank" rel="noopener">Prowlarr</a>, <a href="https://github.com/qbittorrent/qBittorrent"target="_blank" rel="noopener">qBittorrent</a>, <a href="https://github.com/sabnzbd/sabnzbd"target="_blank" rel="noopener">SABnzbd</a>, <a href="https://github.com/Jackett/Jackett"target="_blank" rel="noopener">Jackett</a>, <a href="https://github.com/FlareSolverr/FlareSolverr"target="_blank" rel="noopener">FlareSolverr</a>, <a href="https://github.com/seerr/seerr"target="_blank" rel="noopener">Seerr</a>). Os projetos pessoais (<a href="https://github.com/akitaonrails/frank_fbi"target="_blank" rel="noopener">Frank FBI</a>, <a href="/2026/02/21/vibe-code-fiz-um-clone-do-mega-em-rails-em-1-dia-pro-meu-home-server/">Frank Mega</a>, <a href="https://github.com/akitaonrails/FrankYomik"target="_blank" rel="noopener">Frank Yomik</a>, Mila) somam mais 11. Monitoramento com <a href="https://github.com/grafana/grafana"target="_blank" rel="noopener">Grafana</a>, <a href="https://github.com/prometheus/prometheus"target="_blank" rel="noopener">Prometheus</a>, node-exporter e <a href="https://github.com/google/cadvisor"target="_blank" rel="noopener">cAdvisor</a>. Utilitários como <a href="https://github.com/portainer/portainer"target="_blank" rel="noopener">Portainer</a>, <a href="https://github.com/dani-garcia/vaultwarden"target="_blank" rel="noopener">Vaultwarden</a>, <a href="https://github.com/syncthing/syncthing"target="_blank" rel="noopener">Syncthing</a>, <a href="https://github.com/causefx/Organizr"target="_blank" rel="noopener">Organizr</a>, <a href="https://github.com/containrrr/watchtower"target="_blank" rel="noopener">Watchtower</a>. <a href="https://github.com/go-gitea/gitea"target="_blank" rel="noopener">Gitea</a> como registry Docker privado. <a href="https://github.com/immich-app/immich"target="_blank" rel="noopener">Immich</a> como Google Photos self-hosted. <a href="https://github.com/oae/kaizoku"target="_blank" rel="noopener">Kaizoku</a> pra manga com <a href="https://github.com/Kareadita/Kavita"target="_blank" rel="noopener">Kavita</a> como reader. <a href="https://github.com/ollama/ollama"target="_blank" rel="noopener">Ollama</a> com ROCm. E o <a href="https://github.com/bitcoin/bitcoin"target="_blank" rel="noopener">Bitcoin Core</a>/<a href="https://github.com/cculianu/Fulcrum"target="_blank" rel="noopener">Fulcrum</a> indexando a blockchain do NAS.</p>
<h2>Backups: duas camadas<span class="hx:absolute hx:-mt-20" id="backups-duas-camadas"></span>
    <a href="#backups-duas-camadas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Camada 1: snapshots btrfs locais (snapper)<span class="hx:absolute hx:-mt-20" id="camada-1-snapshots-btrfs-locais-snapper"></span>
    <a href="#camada-1-snapshots-btrfs-locais-snapper" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O <code>/var</code> vive numa partição btrfs de 3.7TB. O snapper cria snapshots automáticos: 7 diários + 1 semanal. São crash-consistent, não application-consistent (postgres pode ficar levemente inconsistente se tiver write pesado no momento do snapshot).</p>
<p>Pra recuperar um arquivo apagado acidentalmente:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo snapper -c var list
</span></span><span class="line"><span class="cl">sudo cp /var/.snapshots/5/snapshot/opt/docker/media/radarr/appdata/config/radarr.db <span class="se">\
</span></span></span><span class="line"><span class="cl">        /var/opt/docker/media/radarr/appdata/config/radarr.db</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Pra rollback completo de um stack:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo docker compose -p media down
</span></span><span class="line"><span class="cl">sudo snapper -c var undochange 7..0 /var/opt/docker/media
</span></span><span class="line"><span class="cl">sudo docker compose -p media up -d</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Camada 2: restic pro NAS (off-machine)<span class="hx:absolute hx:-mt-20" id="camada-2-restic-pro-nas-off-machine"></span>
    <a href="#camada-2-restic-pro-nas-off-machine" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O <a href="https://github.com/restic/restic"target="_blank" rel="noopener">restic</a> roda toda noite às 3h, faz backup incremental pra <code>/var/mnt/terachad/homelab-backups/</code>. Retenção: 7 diários + 4 semanais. Deduplicação por conteúdo, então Plex config (19GB) e Gitea repos (12GB) transferem apenas deltas.</p>
<p>Antes do restic rodar, um <code>pg_dump</code> exporta os bancos postgres (Immich, Kaizoku). Os dumps vão pra <code>/tmp/homelab-db-dumps/</code> e são incluídos no backup.</p>
<p>O que NÃO é incluído no backup (re-downloadável): blockchain Bitcoin (785GB no NAS), imagens Docker (re-pullable), modelos Ollama (re-downloadable), cache do HuggingFace/EasyOCR, transcoding temporário do Plex.</p>
<p>Diretórios grandes e re-downloadáveis foram convertidos em subvolumes btrfs pra que o snapper os ignore: <code>/var/lib/ollama</code> e <code>/var/opt/docker/bitcoin/fulcrum/fulc2_db</code>.</p>
<h2>Tuning de performance<span class="hx:absolute hx:-mt-20" id="tuning-de-performance"></span>
    <a href="#tuning-de-performance" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>btrfs com compressão zstd<span class="hx:absolute hx:-mt-20" id="btrfs-com-compressão-zstd"></span>
    <a href="#btrfs-com-compress%c3%a3o-zstd" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Adicionei <code>compress=zstd:1</code> no fstab pra partição <code>/var</code>. O zstd nível 1 tem overhead de CPU quase zero em NVMe e comprime bem metadata Docker, JSON configs e logs. Dados incompressíveis (SQLite, postgres) são ignorados automaticamente pelo btrfs.</p>
<h3>zram swap<span class="hx:absolute hx:-mt-20" id="zram-swap"></span>
    <a href="#zram-swap" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Com ~30GB de RAM disponível pro sistema (96GB vão pra VRAM), swap comprimido em memória ajuda. O zram cria um dispositivo de swap de ~15GB (ram/2) com compressão zstd, muito mais rápido que swap em disco.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/systemd/zram-generator.conf</span>
</span></span><span class="line"><span class="cl"><span class="k">[zram0]</span>
</span></span><span class="line"><span class="cl"><span class="na">zram-size</span> <span class="o">=</span> <span class="s">ram / 2</span>
</span></span><span class="line"><span class="cl"><span class="na">compression-algorithm</span> <span class="o">=</span> <span class="s">zstd</span>
</span></span><span class="line"><span class="cl"><span class="na">swap-priority</span> <span class="o">=</span> <span class="s">100</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>btrfs nodatacow em diretórios de banco de dados<span class="hx:absolute hx:-mt-20" id="btrfs-nodatacow-em-diretórios-de-banco-de-dados"></span>
    <a href="#btrfs-nodatacow-em-diret%c3%b3rios-de-banco-de-dados" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Copy-on-write + escrita aleatória de banco de dados = write amplification. Desabilitei CoW nos diretórios que guardam SQLite e postgres:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo chattr +C /var/opt/docker/gitea/data/gitea.db
</span></span><span class="line"><span class="cl">sudo chattr +C /var/opt/docker/immich/db/
</span></span><span class="line"><span class="cl">sudo chattr +C /var/opt/docker/media/radarr/appdata/config/</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>CPU em modo performance<span class="hx:absolute hx:-mt-20" id="cpu-em-modo-performance"></span>
    <a href="#cpu-em-modo-performance" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Num servidor headless, não faz sentido economizar energia:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> performance <span class="p">|</span> sudo tee /sys/devices/system/cpu/cpu*/cpufreq/energy_performance_preference</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Persistido via systemd service <code>cpu-epp.service</code>.</p>
<h3>Docker shutdown fix<span class="hx:absolute hx:-mt-20" id="docker-shutdown-fix"></span>
    <a href="#docker-shutdown-fix" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Problema que descobri: o Docker vem com <code>KillMode=process</code>, que significa que no shutdown do sistema, o systemd mata só o dockerd e deixa todos os <code>containerd-shim</code> (um por container, ~49 no meu caso) órfãos. O systemd-shutdown precisa caçar eles um a um depois que o journal já parou, causando um hang silencioso de vários minutos.</p>
<p>Fix:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/systemd/system/docker.service.d/shutdown.conf</span>
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">KillMode</span><span class="o">=</span><span class="s">control-group</span>
</span></span><span class="line"><span class="cl"><span class="na">TimeoutStopSec</span><span class="o">=</span><span class="s">30</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>Os problemas que encontramos<span class="hx:absolute hx:-mt-20" id="os-problemas-que-encontramos"></span>
    <a href="#os-problemas-que-encontramos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Essa é a tabela de problemas reais que tivemos durante a migração. Se você está planejando algo parecido, leia antes de começar:</p>
<table>
  <thead>
      <tr>
          <th>Problema</th>
          <th>Causa</th>
          <th>Solução</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>ROCm vê só 15.5GB de VRAM</td>
          <td>Kernel TTM limita pages mesmo com BIOS em 96GB</td>
          <td>Adicionar <code>amdttm.pages_limit=25165824</code> no kernel cmdline</td>
      </tr>
      <tr>
          <td>Todos containers: permission denied em volumes</td>
          <td>SELinux <code>container_t</code> não escreve em paths sem label</td>
          <td><code>security_opt: label:disable</code> em todo serviço</td>
      </tr>
      <tr>
          <td>NFS com <code>:Z</code> falha silenciosamente</td>
          <td>NFS não suporta xattr do SELinux</td>
          <td>Nunca usar <code>:Z</code> em paths NFS</td>
      </tr>
      <tr>
          <td>SQLite quebra com <code>:Z</code></td>
          <td>Relabeling muda contexto, WAL mode falha</td>
          <td>Remover <code>:Z</code>, usar <code>label:disable</code></td>
      </tr>
      <tr>
          <td>Radarr/Sonarr mostraram tela de setup</td>
          <td>Backup em <code>appdata/config/</code> mas compose montava <code>appdata/</code></td>
          <td>Corrigir: <code>appdata/config:/config</code></td>
      </tr>
      <tr>
          <td>Grafana perdeu dashboards</td>
          <td>Named volume não incluído no backup de filesystem</td>
          <td>Backup explícito do named volume</td>
      </tr>
      <tr>
          <td>Plex não encontra mídia</td>
          <td>Path interno mudou de <code>/media</code> pra <code>/data</code></td>
          <td>Restaurar path original no compose</td>
      </tr>
      <tr>
          <td>Seerr não conecta no Plex</td>
          <td>macvlan isolada da bridge network</td>
          <td>Adicionar <code>default: {}</code> nas networks do Plex</td>
      </tr>
      <tr>
          <td>Fulcrum crash: &ldquo;option -b missing&rdquo;</td>
          <td>Env vars não suportadas pela imagem</td>
          <td>Usar flags CLI em <code>command:</code></td>
      </tr>
      <tr>
          <td>bitcoind rejeita RPC</td>
          <td>Bind em <code>::1</code> por padrão</td>
          <td>Adicionar <code>-rpcbind=0.0.0.0 -rpcallowip=172.0.0.0/8</code></td>
      </tr>
      <tr>
          <td>sdbootutil warning no transactional shell</td>
          <td>Deve rodar fora da transação</td>
          <td>Executar <code>sdbootutil update-all-entries</code> no shell normal</td>
      </tr>
      <tr>
          <td>Watchtower permission denied em docker.sock</td>
          <td>SELinux bloqueia acesso ao socket</td>
          <td><code>label:disable</code></td>
      </tr>
      <tr>
          <td>Gitea SSH crash</td>
          <td>Conflito: entrypoint sshd porta 22 + app porta 22</td>
          <td><code>GITEA__server__SSH_LISTEN_PORT=2222</code></td>
      </tr>
      <tr>
          <td>docker-compose não instalado com Docker</td>
          <td>Pacote openSUSE só instala o daemon</td>
          <td>Instalar binário standalone manualmente</td>
      </tr>
  </tbody>
</table>
<h2>O que dizer pro Claude Code antes de começar<span class="hx:absolute hx:-mt-20" id="o-que-dizer-pro-claude-code-antes-de-começar"></span>
    <a href="#o-que-dizer-pro-claude-code-antes-de-come%c3%a7ar" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se eu fosse refazer a migração do zero, daria estas instruções pro Claude Code na primeira mensagem. Na ordem de importância:</p>
<p>Diga que SELinux está enforcing e que ele NÃO deve usar <code>:Z</code> em nenhum volume Docker, e sim <code>security_opt: label:disable</code> em todo serviço. Diga que <code>/var/mnt/terachad/</code> é mount NFS e que <code>:Z</code> nunca deve aparecer em paths NFS. Diga pra sempre olhar o compose original antes de reescrever e só mudar IPs, paths e nomes de container, sem inventar novos layouts de volume. Avise que named volumes precisam de backup explícito (Grafana, Portainer). Explique que Plex roda em macvlan e precisa de <code>default: {}</code> nas networks. Informe que a GPU é gfx1151, não suportada oficialmente, e que precisa de UMA 96GB no BIOS + kernel TTM params + <code>HSA_OVERRIDE_GFX_VERSION=11.5.1</code>. E diga que Bitcoin/Fulcrum não processam variáveis de ambiente, tudo vai como argumento no <code>command:</code>.</p>
<p>Essas instruções teriam evitado 80% dos problemas que encontramos.</p>
<h2>Layout final do servidor<span class="hx:absolute hx:-mt-20" id="layout-final-do-servidor"></span>
    <a href="#layout-final-do-servidor" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>/var/opt/docker/
├── bitcoin/          (bitcoind &#43; fulcrum)
├── cloudflared/      (túnel Cloudflare)
├── frank_fbi/        (análise de fraude por email)
├── frank_mega/       (clone do Mega)
├── frank_yomik/      (tradução de mangás)
├── gitea/            (registry Docker)
├── immich/           (Google Photos self-hosted)
├── kaizoku/          (manga downloader &#43; reader)
├── media/            (Plex &#43; *arr stack)
├── mila/             (bot Discord)
├── monitor/          (Grafana &#43; Prometheus)
├── ollama/           (LLM local com ROCm)
├── rip/              (HandBrake)
└── utils/            (Portainer, Vaultwarden, Syncthing, etc.)

/var/mnt/terachad/    (NFS do Synology)
├── Bitcoin/data/     (blockchain, 785GB)
├── Downloads/        (torrents &#43; nzbget)
├── Videos/           (Radarr movies &#43; Sonarr series)
└── Ollama/models/    (overflow de modelos se disco local encher)</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>Aviso sobre usar IA pra administrar servidor<span class="hx:absolute hx:-mt-20" id="aviso-sobre-usar-ia-pra-administrar-servidor"></span>
    <a href="#aviso-sobre-usar-ia-pra-administrar-servidor" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu usei Claude Code pra acelerar a migração. Criou compose files, escreveu scripts de backup, configurou firewall, diagnosticou problemas de SELinux. Funcionou bem pro meu caso: home server, só eu uso, e eu estava revisando cada passo.</p>
<p>Mas tem armadilhas. O Claude não sabe que <code>:Z</code> quebra SQLite a menos que você diga. Ele não sabe que o Fulcrum não aceita env vars a menos que já tenha visto o Dockerfile. Ele vai inventar layouts de volume &ldquo;melhores&rdquo; que quebram o Plex porque o Plex guarda paths absolutos no banco de dados.</p>
<p>Se fosse produção real: não faça isso sem review. Cada compose file que o Claude gerar, leia inteiro antes de aplicar. Cada comando destrutivo (rollback, delete, recreate), confirme manualmente. E tenha backups testados antes de começar. O Claude é bom pra gerar a primeira versão e diagnosticar erros, mas as decisões de arquitetura e as validações de segurança são suas.</p>
<p>Os posts anteriores sobre o home server que podem dar contexto adicional:</p>
<ul>
<li><a href="/2024/04/03/meu-netflix-pessoal-com-docker-compose/">Meu &ldquo;Netflix Pessoal&rdquo; com Docker Compose</a></li>
<li><a href="/2025/09/09/acessando-meu-home-server-com-dominio-de-verdade/">Acessando meu Home Server com domínio de verdade</a></li>
<li><a href="/2025/09/10/protegendo-seu-home-server-com-cloudflare-zero-trust/">Protegendo seu Home Server com Cloudflare Zero Trust</a></li>
<li><a href="/2025/09/10/instalando-grafana-no-meu-home-server/">Instalando Grafana no meu Home Server</a></li>
<li><a href="/2025/09/10/omarchy-2-0-bitwarden-self-hosted-vaultwarden/">Vaultwarden self-hosted</a></li>
</ul>
]]></content:encoded><category>homelab</category><category>containers</category><category>devops</category><category>seguranca</category></item><item><title>SQLite + Kamal: Deploy de Rails sem Drama | Bastidores do The M.Akita Chronicles</title><link>https://akitaonrails.github.io/2026/02/20/sqlite-kamal-deploy-de-rails-sem-drama-bastidores-do-the-m-akita-chronicles/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/02/20/sqlite-kamal-deploy-de-rails-sem-drama-bastidores-do-the-m-akita-chronicles/</guid><pubDate>Fri, 20 Feb 2026 03:18:55 +0000</pubDate><description>&lt;p&gt;Este post vai fazer parte de uma série; acompanhe pela tag &lt;a href="https://akitaonrails.github.io/tags/the-m-akita-chronicles/"&gt;/themakitachronicles&lt;/a&gt;. Esta é a parte 7.&lt;/p&gt;
&lt;p&gt;E não deixe de assinar minha nova newsletter &lt;a href="https://themakitachronicles.com/"target="_blank" rel="noopener"&gt;The M.Akita Chronicles&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;&amp;ndash;&lt;/p&gt;
&lt;p&gt;Se eu te dissesse que dá pra rodar uma aplicação Rails completa em produção — com banco de dados, fila de jobs, cache — num único VPS de $12/mês sem instalar PostgreSQL, Redis, ou qualquer outro serviço externo, você acreditaria?&lt;/p&gt;
&lt;p&gt;Pois é. Esse é o Rails 8 com SQLite e Kamal.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Este post vai fazer parte de uma série; acompanhe pela tag <a href="/tags/the-m-akita-chronicles/">/themakitachronicles</a>. Esta é a parte 7.</p>
<p>E não deixe de assinar minha nova newsletter <a href="https://themakitachronicles.com/"target="_blank" rel="noopener">The M.Akita Chronicles</a>!</p>
<p>&ndash;</p>
<p>Se eu te dissesse que dá pra rodar uma aplicação Rails completa em produção — com banco de dados, fila de jobs, cache — num único VPS de $12/mês sem instalar PostgreSQL, Redis, ou qualquer outro serviço externo, você acreditaria?</p>
<p>Pois é. Esse é o Rails 8 com SQLite e Kamal.</p>
<h2>SQLite no Rails 8: agora é pra valer<span class="hx:absolute hx:-mt-20" id="sqlite-no-rails-8-agora-é-pra-valer"></span>
    <a href="#sqlite-no-rails-8-agora-%c3%a9-pra-valer" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O Rails 8 trouxe o SQLite como opção real de produção, não como brinquedo de desenvolvimento. E quando digo &ldquo;real&rdquo;, quero dizer que o <code>rails new</code> gera tudo pronto: banco principal em SQLite, SolidQueue (jobs) em SQLite, SolidCache (cache) em SQLite, SolidCable (WebSocket) em SQLite.</p>
<p><strong>Quatro serviços que antes precisavam de PostgreSQL + Redis + Memcached agora são arquivos <code>.sqlite3</code> no disco.</strong></p>
<p>O WAL mode (Write-Ahead Logging) habilita leituras concorrentes enquanto uma escrita acontece. Pra aplicações com dezenas de milhares de requisições por dia — que é a imensa maioria das aplicações Rails no mundo — SQLite é mais que suficiente. E a latência de leitura é medida em <strong>microsegundos</strong>, não milissegundos. É acesso a disco local, não round-trip de rede.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># database.yml — produção com SQLite</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">production</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">primary</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">&lt;&lt;</span><span class="p">:</span><span class="w"> </span><span class="cp">*default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">database</span><span class="p">:</span><span class="w"> </span><span class="l">storage/production.sqlite3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">queue</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">&lt;&lt;</span><span class="p">:</span><span class="w"> </span><span class="cp">*default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">database</span><span class="p">:</span><span class="w"> </span><span class="l">storage/production_queue.sqlite3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">migrations_paths</span><span class="p">:</span><span class="w"> </span><span class="l">db/queue_migrate</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cache</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">&lt;&lt;</span><span class="p">:</span><span class="w"> </span><span class="cp">*default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">database</span><span class="p">:</span><span class="w"> </span><span class="l">storage/production_cache.sqlite3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">migrations_paths</span><span class="p">:</span><span class="w"> </span><span class="l">db/cache_migrate</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Três arquivos. Sem conexão TCP. Sem pooling. Sem &ldquo;o PostgreSQL tá aceitando conexões?&rdquo;. Backup é um <code>VACUUM INTO</code> e pronto — um comando SQL que produz uma cópia atômica. Restore? Cola o arquivo de volta. Tenta fazer isso com um dump de PostgreSQL de 2GB.</p>
<h2>Kamal: Docker Deploy Sem Kubernetes<span class="hx:absolute hx:-mt-20" id="kamal-docker-deploy-sem-kubernetes"></span>
    <a href="#kamal-docker-deploy-sem-kubernetes" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O Kamal é o deployer do Rails 8. Pensa nele como &ldquo;SSH + Docker, mas inteligente&rdquo;. Sem Kubernetes. Sem Helm charts. Sem YAML de 300 linhas descrevendo um Ingress Controller.</p>
<p>A configuração inteira cabe em um arquivo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># config/deploy.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">minha-app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">meuregistry/minha-app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">servers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">web</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="m">107.170.70.49</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">options</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">network</span><span class="p">:</span><span class="w"> </span><span class="l">minha-network</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">minha-app-storage:/rails/storage</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">minha-app-content:/rails/content</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">proxy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ssl</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">app.meudominio.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">builder</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">arch</span><span class="p">:</span><span class="w"> </span><span class="l">amd64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">clear</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">RAILS_LOG_LEVEL</span><span class="p">:</span><span class="w"> </span><span class="l">info</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">secret</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">RAILS_MASTER_KEY</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">DATABASE_URL</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O que o Kamal faz quando você roda <code>kamal deploy</code>:</p>
<ol>
<li>Builda a imagem Docker localmente (ou em CI)</li>
<li>Pusha pro registry</li>
<li>SSH no servidor</li>
<li>Puxa a nova imagem</li>
<li>Roda migrations</li>
<li>Reinicia o container com <strong>zero downtime</strong> via kamal-proxy</li>
</ol>
<p>O kamal-proxy é o segredo: ele funciona como reverse proxy na frente dos seus containers. Quando o deploy acontece, ele sobe o novo container, espera ele ficar healthy, redireciona o tráfego, e depois derruba o antigo. Seus usuários não percebem nada.</p>
<h2>Volumes Docker: O Segredo do SQLite em Produção<span class="hx:absolute hx:-mt-20" id="volumes-docker-o-segredo-do-sqlite-em-produção"></span>
    <a href="#volumes-docker-o-segredo-do-sqlite-em-produ%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O ponto crítico de SQLite em Docker é: <strong>os dados não podem morrer com o container</strong>. Docker volumes resolvem isso:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">minha-app-storage:/rails/storage</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O diretório <code>storage/</code> contém todos os bancos SQLite. Com o volume Docker, eles persistem entre deploys. O container é efêmero; os dados são permanentes.</p>
<p>Mas tem um detalhe que pega muita gente: se você roda <strong>dois serviços</strong> diferentes que compartilham dados via arquivos (não banco), precisa de um volume compartilhado:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># Serviço A escreve conteúdo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">content-compartilhado:/rails/content</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Serviço B lê conteúdo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">content-compartilhado:/rails/content</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O mesmo volume Docker montado em dois containers. Simples, funciona, e não precisa de NFS, S3, ou qualquer coisa sofisticada. É um diretório no disco do servidor.</p>
<h2>Múltiplos Serviços no Mesmo VPS<span class="hx:absolute hx:-mt-20" id="múltiplos-serviços-no-mesmo-vps"></span>
    <a href="#m%c3%baltiplos-servi%c3%a7os-no-mesmo-vps" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Uma vantagem pouco discutida do Kamal: você pode rodar <strong>múltiplas aplicações</strong> no mesmo servidor. O kamal-proxy roteia por hostname:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>app.meudominio.com    → container-app
bot.meudominio.com    → container-bot</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Cada serviço tem seu próprio <code>deploy.yml</code>, sua própria imagem Docker, seu próprio ciclo de deploy. Mas todos rodam no mesmo VPS, compartilhando a mesma rede Docker para comunicação interna.</p>
<p>O SSL é automático via Let&rsquo;s Encrypt — o kamal-proxy cuida disso. Cada hostname ganha seu certificado sem você configurar nada.</p>
<h2>Hooks: Automação no Deploy<span class="hx:absolute hx:-mt-20" id="hooks-automação-no-deploy"></span>
    <a href="#hooks-automa%c3%a7%c3%a3o-no-deploy" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O Kamal suporta hooks em vários pontos do ciclo de deploy. O mais útil é o <code>pre-deploy</code>, que roda <strong>antes</strong> do container novo substituir o antigo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># .kamal/hooks/pre-deploy</span>
</span></span><span class="line"><span class="cl"><span class="c1">#!/bin/sh</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Roda migrations antes do container ficar ativo</span>
</span></span><span class="line"><span class="cl"><span class="n">ssh</span> <span class="n">root</span><span class="err">@</span><span class="vg">$KAMAL_HOSTS</span> <span class="p">\</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;docker exec minha-app-web bin/rails db:migrate&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Outros hooks úteis:</p>
<ul>
<li><code>post-deploy</code> — notificar equipe, limpar cache</li>
<li><code>pre-connect</code> — verificar saúde do servidor</li>
<li><code>docker-setup</code> — instalar dependências no host</li>
</ul>
<h2>O Dockerfile que o Rails Gera<span class="hx:absolute hx:-mt-20" id="o-dockerfile-que-o-rails-gera"></span>
    <a href="#o-dockerfile-que-o-rails-gera" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O Rails 8 gera um Dockerfile otimizado de produção. Alguns destaques:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="cl"><span class="c"># Multi-stage build: build stage grande, runtime stage mínima</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">ruby:3.3-slim</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">base</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Instala apenas dependências de runtime</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> apt-get install -y libsqlite3-0<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">base</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">build</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Aqui instala tudo pra compilar gems nativas</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> apt-get install -y build-essential libsqlite3-dev<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Copia e instala gems</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> Gemfile* ./<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> bundle install --without development test<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Stage final: só o necessário pra rodar</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">base</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> --from<span class="o">=</span>build /usr/local/bundle /usr/local/bundle<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> . .<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> bundle <span class="nb">exec</span> bootsnap precompile --gemfile app/ lib/</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Multi-stage build significa que a imagem final não tem compiladores, headers de desenvolvimento, ou qualquer coisa desnecessária. A imagem de produção fica enxuta.</p>
<h2>Secrets: Sem .env em Produção<span class="hx:absolute hx:-mt-20" id="secrets-sem-env-em-produção"></span>
    <a href="#secrets-sem-env-em-produ%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O Kamal tem um sistema de secrets que lê de um arquivo local (<code>.kamal/secrets</code>) e injeta como variáveis de ambiente no container. Esse arquivo <strong>nunca</strong> vai pro git:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># .kamal/secrets (gitignored)</span>
</span></span><span class="line"><span class="cl"><span class="nv">RAILS_MASTER_KEY</span><span class="o">=</span>abc123...
</span></span><span class="line"><span class="cl"><span class="nv">AWS_ACCESS_KEY_ID</span><span class="o">=</span>AKIA...</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No <code>deploy.yml</code>, você referencia:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">secret</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">RAILS_MASTER_KEY</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">AWS_ACCESS_KEY_ID</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O Kamal lê do arquivo local e configura no servidor. Simples, seguro, sem HashiCorp Vault ou AWS Secrets Manager (a menos que você queira — Kamal suporta adaptadores).</p>
<h2>Backup de SQLite: Absurdamente Simples (Se Fizer Certo)<span class="hx:absolute hx:-mt-20" id="backup-de-sqlite-absurdamente-simples-se-fizer-certo"></span>
    <a href="#backup-de-sqlite-absurdamente-simples-se-fizer-certo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Quer backup? Um comando SQL:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">VACUUM</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="s1">&#39;/tmp/backup/newsletter.sqlite3&#39;</span><span class="p">;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <code>VACUUM INTO</code> é <strong>atômico</strong> — produz uma cópia consistente e desfragmentada do banco, mesmo com escritas acontecendo. Roda enquanto a aplicação está servindo requests normalmente. E a cópia resultante é um banco SQLite completo e funcional — abre, consulta, restaura.</p>
<p><strong>Atenção</strong>: copiar o arquivo <code>.sqlite3</code> diretamente (<code>cp</code>, <code>tar</code>, <code>rsync</code>) de um banco em uso <strong>pode corromper o backup</strong>. Se uma escrita estiver em andamento no momento da cópia, você fica com um arquivo meio-escrito. O SQLite tem WAL (Write-Ahead Log) e journaling que protegem o banco ativo — mas a cópia crua não herda essa proteção.</p>
<p>Na prática, automatizei isso com um job Rails que roda a cada hora:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">BackupDatabaseJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">perform</span>
</span></span><span class="line"><span class="cl">    <span class="n">backup_dir</span> <span class="o">=</span> <span class="no">File</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="no">Rails</span><span class="o">.</span><span class="n">configuration</span><span class="o">.</span><span class="n">content_dir</span><span class="p">,</span> <span class="s2">&#34;backups&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="no">FileUtils</span><span class="o">.</span><span class="n">mkdir_p</span><span class="p">(</span><span class="n">backup_dir</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">dest</span> <span class="o">=</span> <span class="no">File</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">backup_dir</span><span class="p">,</span> <span class="s2">&#34;newsletter.sqlite3&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="o">.</span><span class="n">connection</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&#34;VACUUM INTO &#39;</span><span class="si">#{</span><span class="n">dest</span><span class="si">}</span><span class="s2">&#39;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O backup vai pro diretório compartilhado de conteúdo — o mesmo que já é sincronizado com <code>rsync</code> pra máquina local. Sem agente de backup extra, sem serviço externo, sem snapshot de volume. Um SQL, um arquivo, um <code>rsync</code>.</p>
<p>Compara isso com <code>pg_dump</code> de um banco PostgreSQL de produção com dezenas de tabelas e constraints. O SQLite backup é <strong>um comando que produz um arquivo funcional</strong>. Restaurar? Copia de volta e reinicia.</p>
<h2>Quando NÃO Usar SQLite<span class="hx:absolute hx:-mt-20" id="quando-não-usar-sqlite"></span>
    <a href="#quando-n%c3%a3o-usar-sqlite" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Vou ser honesto: SQLite não é pra tudo.</p>
<ul>
<li><strong>Múltiplos servidores web</strong> escrevendo no mesmo banco? Não. SQLite é single-writer. Se você precisa de horizontal scaling com múltiplos nós, vai de PostgreSQL.</li>
<li><strong>Datasets massivos</strong> (centenas de GB)? PostgreSQL tem melhor query planning e paralelismo.</li>
<li><strong>Replicação e alta disponibilidade</strong>? PostgreSQL com streaming replication.</li>
</ul>
<p>Mas a pergunta honesta é: <strong>quantas aplicações realmente precisam disso?</strong> A vasta maioria roda num único servidor e nunca vai precisar de mais. E pra essas, SQLite + Kamal é a combinação mais produtiva que existe hoje.</p>
<h2>O Custo Real<span class="hx:absolute hx:-mt-20" id="o-custo-real"></span>
    <a href="#o-custo-real" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Vamos fazer a conta:</p>
<table>
  <thead>
      <tr>
          <th>Componente</th>
          <th>Antes</th>
          <th>Agora</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Servidor</td>
          <td>VPS $24/mês</td>
          <td>VPS $12/mês</td>
      </tr>
      <tr>
          <td>Banco</td>
          <td>RDS PostgreSQL $30/mês</td>
          <td>SQLite (incluso)</td>
      </tr>
      <tr>
          <td>Redis</td>
          <td>ElastiCache $15/mês</td>
          <td>SolidQueue (incluso)</td>
      </tr>
      <tr>
          <td>Cache</td>
          <td>ElastiCache $15/mês</td>
          <td>SolidCache (incluso)</td>
      </tr>
      <tr>
          <td>Deploy</td>
          <td>CI/CD complexo</td>
          <td><code>kamal deploy</code></td>
      </tr>
      <tr>
          <td>SSL</td>
          <td>Certbot + cron</td>
          <td>Automático</td>
      </tr>
      <tr>
          <td><strong>Total</strong></td>
          <td><strong>~$84/mês + dor de cabeça</strong></td>
          <td><strong>$12/mês</strong></td>
      </tr>
  </tbody>
</table>
<p>E o custo cognitivo? Ao invés de debugar &ldquo;por que o Redis perdeu meus jobs?&rdquo;, &ldquo;por que o PostgreSQL tá rejeitando conexões?&rdquo;, &ldquo;por que o cert expirou?&rdquo;, você foca no que importa: <strong>o produto</strong>.</p>
<h2>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>SQLite + Kamal não é downgrade. É que a maioria das aplicações nunca precisou da complexidade que a gente achava que precisava. O Rails 8 abraçou isso e entregou uma experiência de deploy que é, sem exagero, a mais simples que já existiu no ecossistema Ruby.</p>
<p>Um VPS. Um comando. Zero serviços externos. E uma aplicação que roda tão rápido quanto qualquer setup enterprise com 15 serviços no docker-compose.</p>
]]></content:encoded><category>the-m-akita-chronicles</category><category>rails</category><category>devops</category></item><item><title>Instalando Grafana no meu Home Server</title><link>https://akitaonrails.github.io/2025/09/10/instalando-grafana-no-meu-home-server/</link><guid isPermaLink="true">https://akitaonrails.github.io/2025/09/10/instalando-grafana-no-meu-home-server/</guid><pubDate>Wed, 10 Sep 2025 16:00:00 -0300</pubDate><description>&lt;p&gt;Este post é pra continuar complementando minha &lt;a href="https://akitaonrails.github.io/tags/homeserver"&gt;série sobre meu Home Server&lt;/a&gt;, de novo, pra servir de anotação pra mim mesmo no futuro. Como já expliquei, meu home server é um mini-PC Intel NUC que eu só acesso com SSH. Então eu queria uma forma de monitorar o uso dos recursos do sistema sem ter que manualmente logar via SSH e abrir BTOP ou NTOP.&lt;/p&gt;
&lt;p&gt;Fiquei com preguiça de instalar Grafana na época, mas resolvi fazer isso agora, e foi bem mais fácil do que eu esperava, por isso resolvi compartilhar como fiz.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Este post é pra continuar complementando minha <a href="/tags/homeserver">série sobre meu Home Server</a>, de novo, pra servir de anotação pra mim mesmo no futuro. Como já expliquei, meu home server é um mini-PC Intel NUC que eu só acesso com SSH. Então eu queria uma forma de monitorar o uso dos recursos do sistema sem ter que manualmente logar via SSH e abrir BTOP ou NTOP.</p>
<p>Fiquei com preguiça de instalar Grafana na época, mas resolvi fazer isso agora, e foi bem mais fácil do que eu esperava, por isso resolvi compartilhar como fiz.</p>
<p>A idéia é subir <a href="https://github.com/google/cadvisor"target="_blank" rel="noopener">cAdvisor</a> que faz monitoramento de recursos de containers e <a href="https://github.com/prometheus/prometheus"target="_blank" rel="noopener">Prometheus</a> que faz scraping de dados de recursos do sistema e consolidar os dados dos dois no <a href="https://grafana.com/"target="_blank" rel="noopener">Grafana</a> que é uma aplicação web que permite montar dashboards gráficos pra monitorar seus sistemas. Se fizer direito, no final vai ter uma tela assim:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910155108_screenshot-2025-09-10_15-50-49.png" alt="Grafana Dashboard"  loading="lazy" /></p>
<h3>Instalação<span class="hx:absolute hx:-mt-20" id="instalação"></span>
    <a href="#instala%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Pra subir no meu home server é muito simples, eu faço tudo com Docker Compose, então:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># monitor-docker-compose.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">monitoring</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">driver</span><span class="p">:</span><span class="w"> </span><span class="l">bridge</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">prometheus_data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">grafana_data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">node-exporter</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">quay.io/prometheus/node-exporter:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- --<span class="l">path.rootfs=/host</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">pid</span><span class="p">:</span><span class="w"> </span><span class="l">host</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">monitoring]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/:/host:ro,rslave</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">security_opt</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;no-new-privileges:true&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">read_only</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cadvisor</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">gcr.io/cadvisor/cadvisor:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">monitoring]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;8080:8080&#34;</span><span class="nt">]   # optional</span><span class="p">:</span><span class="w"> </span><span class="l">expose UI locally</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/:/rootfs:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/run:/var/run:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/sys:/sys:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/lib/docker/:/var/lib/docker:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/dev/disk/:/dev/disk:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">security_opt</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;no-new-privileges:true&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">read_only</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">prometheus</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">prom/prometheus:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">monitoring]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">prometheus_data:/prometheus</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./prometheus.yml:/etc/prometheus/prometheus.yml:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- --<span class="l">config.file=/etc/prometheus/prometheus.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- --<span class="l">storage.tsdb.path=/prometheus</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- --<span class="l">storage.tsdb.retention.time=30d</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- --<span class="l">web.enable-lifecycle</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;9090:9090&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">security_opt</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;no-new-privileges:true&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">read_only</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">grafana</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">grafana/grafana:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">monitoring]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">prometheus]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;3001:3000&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">GF_SECURITY_ADMIN_USER</span><span class="p">:</span><span class="w"> </span><span class="l">admin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">GF_SECURITY_ADMIN_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">change-me</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">GF_USERS_ALLOW_SIGN_UP</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;false&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">grafana_data:/var/lib/grafana</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./provisioning/datasources:/etc/grafana/provisioning/datasources:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">security_opt</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;no-new-privileges:true&#34;</span><span class="p">]</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Tem que prestar atenção nos mapeamentos de diretório local. Se colocar <code>/etc/grafana/prometheus.yml</code> por exemplo, tem que mudar esta linha:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">prometheus_data:/prometheus</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/etc/grafana/prometheus.yml:/etc/prometheus/prometheus.yml:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Falando nele, eis o <code>prometheus.yml</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">global</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">scrape_interval</span><span class="p">:</span><span class="w"> </span><span class="l">15s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">scrape_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">job_name</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;prometheus&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">static_configs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>{<span class="w"> </span><span class="nt">targets</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s1">&#39;prometheus:9090&#39;</span><span class="p">]</span><span class="w"> </span>}<span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">job_name</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;node&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">static_configs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>{<span class="w"> </span><span class="nt">targets</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s1">&#39;node-exporter:9100&#39;</span><span class="p">]</span><span class="w"> </span>}<span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">job_name</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;cadvisor&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">static_configs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>{<span class="w"> </span><span class="nt">targets</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s1">&#39;cadvisor:8080&#39;</span><span class="p">]</span><span class="w"> </span>}<span class="p">]</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Finalmente <code>/etc/grafana/provisioning/datasources/datasource.yaml</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>apiVersion: 1
datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Cuidado também com mapeamento de portas, o recomendado é fazer assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">ports</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;127.0.0.1:8080:8080&#34;</span><span class="p">]</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso porque cAdvisor e Prometheus só precisam estar acessíveis pro Grafana, dentro do home server, mas não tem porque dar bind em <code>0.0.0.0</code> e estar acessível pra fora do servidor. Aí é mais uma escolha. Como eu estou numa LAN caseira, e não tem túneis apontando pra esses serviços, pra mim tanto faz.</p>
<p>Se por acaso, como eu, também já tiver algum serviço pendurado na porta 8080, o cAdvisor vai falhar. Nesse caso remapeie as portas e reconfigure o Prometheus, ou só tire a linha de porta. Não precisa ter o cAdvisor exposto pra fora.</p>
<p>Outro detalhe é que por padrão Grafana sobe na porta 3000 mas eu mudei pra 3001 porque já tinha outro serviço nessa porta. Então, quando subir, eu posso acessar via <code>http://192.168.0.200:3001</code>.</p>
<p>Como expliquei no <a href="https://akitaonrails.com/2025/09/09/acessando-meu-home-server-com-dominio-de-verdade/"target="_blank" rel="noopener">artigo de Cloudflared</a> eu roteio esse endereço pro Cloudflare pra ter um domínio bonito como <code>https://grafana.fabioakita.dev</code> pra acessar tanto localmente quanto remotamente, se quiser. Depois leiam esses outros artigos pra entender como.</p>
<p>E pronto, agora é só subir os containers:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose -f monitor-docker-compose.yml up -d</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E acabou, super simples, eu deveria ter feito isso antes.</p>
<p>Como podem ver arquivo de Docker Compose, quando entrar no site pela primeira vez, o login vai ser com <code>admin</code> senha <code>change-me</code>, obviamente, a primeira coisa a fazer é trocar a senha:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910160042_screenshot-2025-09-10_16-00-31.png" alt="Admin Password"  loading="lazy" /></p>
<h3>Dashboards<span class="hx:absolute hx:-mt-20" id="dashboards"></span>
    <a href="#dashboards" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Agora praticamente acabou. Podemos checar se o Prometheus está funcionando e integrado. Basta ir no menu &ldquo;Connections&rdquo; e &ldquo;Data Sources&rdquo;:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910160151_screenshot-2025-09-10_16-01-40.png" alt="Data Sources"  loading="lazy" /></p>
<p>Grafana consegue consolidar dados de dezenas de fontes diferentes, Prometheus é só um deles. Em uma infra maior e mais complicada, essa lista estaria enorme.</p>
<p>Pra montar Dashboards, dá pra ir do zero e montar manualmente widget a widget, mas é mais fácil importar dashboards que já existem. Pra isso é só clicar na lupa lá no topo, na caixa de procura e escolher a opção &ldquo;Import dashboards&rdquo;:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910160509_screenshot-2025-09-10_16-04-59.png" alt="import dashboards menu"  loading="lazy" /></p>
<p>Nessa página dá pra ou importar um arquivo JSON, ou copiar e colocar o JSON ou pesquisar pelo ID que já existe cadastrado no site grafana.com:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910160603_screenshot-2025-09-10_16-05-46.png" alt="import by ID"  loading="lazy" /></p>
<p>De cara, eu aprendi que existem 3 IDs que são úteis pra mim:</p>
<ul>
<li><strong>1860 - Node Exporter Full</strong> - que é a foto de tela que está no começo deste artigo e tem várias métricas do sistema como CPU, memória, disco, rede, etc. É o mais útil pra mim.</li>
<li><strong>13484 - Docker/cAdvisor Exporter</strong> - que são as métricas de CPU, memória, mas dividido por cada container rodando no servidor.</li>
<li><strong>3662 - Prometheus 2.0 overview</strong> - que é um resumo geral de saúde</li>
</ul>
<p>Se gostar desses ou de outros dashboard, depois é só favoritar com um &ldquo;pin&rdquo; e pronto.</p>
<p>Por exemplo, este é o dashboard do cAdvisor:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910160927_screenshot-2025-09-10_16-09-13.png" alt="cAdvisor dashboard"  loading="lazy" /></p>
<p>Eu ainda não explorei outros dashboards. Se tiver recomendações de IDs pra dar import, não deixe de mandar nos comentários abaixo!</p>
<p>Com isso eu tenho uma visão rápida e bonita do uso de recursos do meu home server e consigo ver rapidamente se tem algo de estranho acontecendo. Não parece que esses serviços consumam muitos recursos então posso deixar rodando junto com os demais containers.</p>
]]></content:encoded><category>homelab</category><category>containers</category><category>devops</category></item><item><title>Meu Novo Blog - Como Eu Fiz</title><link>https://akitaonrails.github.io/2025/09/10/meu-novo-blog-como-eu-fiz/</link><guid isPermaLink="true">https://akitaonrails.github.io/2025/09/10/meu-novo-blog-como-eu-fiz/</guid><pubDate>Wed, 10 Sep 2025 15:00:00 -0300</pubDate><description>&lt;p&gt;Este artigo está alguns meses atrasado, mas resolvi documentar agora porque senão vou esquecer como fiz. Como breve introdução, já digo o maior erro que todo programador que quer tentar começar a escrever comete: perde MUITO tempo tentando criar o blog perfeito.
&lt;strong&gt;Just f&amp;hellip;ng write!!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910134801_screenshot-2025-09-10_13-47-43.png" alt="novo blog" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;(e sim, tem tema dark, link no rodapé do site)&lt;/p&gt;
&lt;h3&gt;O BLOG PERFEITO NÃO EXISTE&lt;span class="hx:absolute hx:-mt-20" id="o-blog-perfeito-não-existe"&gt;&lt;/span&gt;
&lt;a href="#o-blog-perfeito-n%c3%a3o-existe" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Eu acho importante que todo programador tente escrever. Não pra virar influencer nem nada disso. O primeiro objetivo é &lt;strong&gt;escrever pra você mesmo&lt;/strong&gt;. Eu uso meu blog primariamente como um &amp;ldquo;backup&amp;rdquo; do meu cérebro (assim como foi meu canal do YouTube).&lt;/p&gt;</description><content:encoded><![CDATA[<p>Este artigo está alguns meses atrasado, mas resolvi documentar agora porque senão vou esquecer como fiz. Como breve introdução, já digo o maior erro que todo programador que quer tentar começar a escrever comete: perde MUITO tempo tentando criar o blog perfeito.
<strong>Just f&hellip;ng write!!</strong></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910134801_screenshot-2025-09-10_13-47-43.png" alt="novo blog"  loading="lazy" /></p>
<p>(e sim, tem tema dark, link no rodapé do site)</p>
<h3>O BLOG PERFEITO NÃO EXISTE<span class="hx:absolute hx:-mt-20" id="o-blog-perfeito-não-existe"></span>
    <a href="#o-blog-perfeito-n%c3%a3o-existe" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Eu acho importante que todo programador tente escrever. Não pra virar influencer nem nada disso. O primeiro objetivo é <strong>escrever pra você mesmo</strong>. Eu uso meu blog primariamente como um &ldquo;backup&rdquo; do meu cérebro (assim como foi meu canal do YouTube).</p>
<p>Notem como os últimos posts de Linux não sou eu tentando te vender nada, mas sim coisas que fiz de verdade no meu PC ou home server, e anoto passo a passo. Caso daqui 3 anos eu queira refazer, tenho tudo anotado.</p>
<p>Segundo: programadores são <strong>péssimos</strong> comunicadores. Nós falamos somente pra nós mesmos e não pensamos se a pessoa ouvindo está nos entendendo. E quando vemos que não entende a reação é <em>&ldquo;ah, porque ela é burra e eu sou inteligente.&rdquo;</em></p>
<p>Falei sobre isso no artigo <a href="https://akitaonrails.com/2013/11/02/off-topic-programadores-sao-pessimos-comunicadores-udp-vs-tcp/"target="_blank" rel="noopener">Programadores são Péssimos Comunicadores</a>. Mesmo se só uma pessoa ler seu post, mas te der um comentário, esse feedback é importante. É como um teste rodando que falha: agora você sabe onde tem que corrigir. E vai corrigindo, um passo de cada vez.</p>
<p>Portanto, escrever blog post, não importa se tem zero visitantes, é um exercício de prática pra se tornar um comunicador melhor, por isso recomendo.</p>
<h3>Blog Engines<span class="hx:absolute hx:-mt-20" id="blog-engines"></span>
    <a href="#blog-engines" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Eu comecei postando no Google Blogspot em 2006. Depois fiz meu próprio blog num projeto que já existia em Ruby on Rails 2.0, passei por vários, como o Typo3 na época. Até que em 2012 eu fiz o meu próprio engine do zero usando ActiveAdmin e é esse projeto que eu vim atualizando de Rails 3 até Rails 6 ou 7 recentemente. Meu primeiro post continua no blog:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910144909_screenshot-2025-09-10_14-48-55.png" alt="primeiro post"  loading="lazy" /></p>
<p>Muita coisa mudou nesse período, Less foi substituído por Sass. Textile e Liquid foram substituídos por Markdown, surgiu Twitter Bootstrap, etc. Por isso dezenas de artigos que escrevi estavam em Textile. E como sempre tive preguiça de converter em Markdown, demorei pra sair dessa minha engine velha.</p>
<p>Pra um blog pessoal, não precisa de nada complicado, como banco de dados ou coisas assim, mas acabou vindo como legado da época. Eu já conhecia <a href="https://github.com/jekyll/jekyll"target="_blank" rel="noopener">jekyll</a> pra fazer blogs com páginas estáticas, mas ainda tinha os posts velhos em Textile que tava com preguiça de lidar.</p>
<p>Ao longo do tempo também vim apagando vários posts. Coisas que ficaram obsoletas mesmo como &ldquo;dicas pra Ruby 2.0&rdquo; ou &ldquo;novidades no Rails 3.0&rdquo;. Ler isso hoje não faria sentido. Fui mantendo somente os posts que se ler hoje, ainda faz sentido. Isso veio a me ajudar quando resolvi converter os posts velhos.</p>
<p>Outro motivo de continuar no meu engine era que meu ActiveAdmin tinha o suporte pra fazer upload de imagens no meu bucket da AWS S3, então era uma conveniência extra que não tava com vontade de ter que resolver de outra forma.</p>
<p>Finalmente, em 2025, resolvi que estava de saco cheio de ficar atualizando versões de Rails, fazendo deploy no Heroku, lidar com backup de banco de dados, além do front-end estar feio e quebrado.</p>
<h3>Hextra/Hugo<span class="hx:absolute hx:-mt-20" id="hextrahugo"></span>
    <a href="#hextrahugo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Depois de Jekyll, surgiram geradores de sites estáticos em dezenas de linguagens. De fato, é o jeito mais prático pra um programador escrever um blog: só criar um arquivo texto localmente, escrever tudo em markdown, rodar um script e subir um site estático que não precisa de nenhuma configuração, banco de dados ou qualquer outro componente mais complicado. Dá pra subir até como GitHub Pages e hospedar lá grátis.</p>
<p>Por isso eu sabia que, se fosse fazer um novo engine, teria que ser site estático. Já tinha ouvido falar do tal projeto <a href="https://github.com/gohugoio/hugo"target="_blank" rel="noopener"><strong>Hugo</strong></a>, que é feito em Go e tem tudo que eu precisava: markdown, suporte a tags, etc. Mas tinha um problema: eu odeio fazer front-end e eu nunca fui um bom web designer, ou designer no geral.</p>
<p>Foi quando esbarrei no <a href="https://themes.gohugo.io/themes/hextra/"target="_blank" rel="noopener"><strong>Hextra</strong></a>, que é basicamente o Hugo mas com vários temas pré-prontos. E diferente de vários outros projetos com temas gratuitos que costumam ser muito feios e muito mal feitos, os do Hextra me pareceram minimamente bem feitos. Fucei um pouco, achei um tema padrão minimalista que é legível pra textos com código e decidi que seria agora.</p>
<p>Desta vez não vou dar passo a passo de tudo que eu fiz porque seria maçante, mas é só ler a <a href="https://gohugo.io/commands/hugo/"target="_blank" rel="noopener">documentação do Hugo</a> pra saber o que fazer. Na prática:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># novo projeto</span>
</span></span><span class="line"><span class="cl">hugo new
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># reconstruir o site</span>
</span></span><span class="line"><span class="cl">hugo build
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># subir o servidor local (porta 1313)</span>
</span></span><span class="line"><span class="cl">hugo server --buildDrafts --disableFastRender</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No meu caso, eu comecei com o <a href="https://github.com/imfing/hextra-starter-template"target="_blank" rel="noopener"><strong>Hextra Starter Template</strong></a> do que começar do Hugo zerado. Acho que vale a pena checar. Ele também explica como fazer deploy pra GitHub Pages e Netlify.</p>
<h3>Código Aberto<span class="hx:absolute hx:-mt-20" id="código-aberto"></span>
    <a href="#c%c3%b3digo-aberto" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A vantagem de usar um gerador de site estático é que posso me livrar do banco de dados Postgres que usei por anos e converter todos os artigos em arquivos texto individuais. Feito isso, basta subir tudo num repositório no GitHub. E como são textos publicamente disponíveis de qualquer forma, não tem porque não deixar aberto. Dessa forma, qualquer um pode contribuir (e muitos já contribuíram - obrigado pela ajuda!)</p>
<p>Se quiser contribuir ou só fuçar acesse o <a href="https://github.com/akitaonrails/akitaonrails.github.io"target="_blank" rel="noopener">repositório no GitHub</a> e faça um fork. Note como o nome do projeto é <code>akitaonrails.github.io</code> porque, originalmente, eu pretendia fazer deploy no GitHub Pages, mas mudei de idéia e já falo disso.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910140229_screenshot-2025-09-10_14-02-12.png" alt="contribuições"  loading="lazy" /></p>
<p>Até agora já tive 8 Pull Requests mergeadas. Alguns realmente se deram ao trabalho de caçar erros, typos, bugs e mandaram correções muito boas. O blog está melhor graças a eles. Às vezes eu posso demorar um pouco pra checar os PRs, mas eventualmente eu chego e costumo mergear tudo.</p>
<p>Um dos PRs inclusive foi pra adicionar suporte a Docker Compose pra facilitar pra quem quiser contribuir. Outro PR foi pra melhorar a documentação na página de README. Então tá bem fácil de entender o que fazer.</p>
<p>Outra contribuição importante foi adicionar tag pra fazer embed de video do YouTube. Eu só preciso fazer:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/HCHqtpipwu4"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>
</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Do jeito antigo eu ia no YouTube, clicava em &ldquo;Share&rdquo;, &ldquo;Embed&rdquo; e copiava o bloco de iframe. Mas isso quebra em mobile.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910140536_screenshot-2025-09-10_14-05-20.png" alt="readme"  loading="lazy" /></p>
<p>Eu particularmente não uso o workflow de Docker, logo abaixo tem o workflow pra instalação local. São tão poucos componentes que não acho que valha a pena lidar com Docker pra isso.</p>
<p>Digamos que eu queira escrever este artigo. Ele está salvo em <code>content/09/10/meu-novo-blog-como-eu-fiz/index.md</code> que significa mês 09 (setembro), dia 10, seguido do slug como sub-diretório e dentro só um <code>index.md</code> que é markdown. As primeiras linhas do artigo tem este cabeçalho:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="l">Meu Novo Blog - Como Eu Fiz</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">date</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;2025-09-10T15:00:00-03:00&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">slug</span><span class="p">:</span><span class="w"> </span><span class="l">meu-novo-blog-como-eu-fiz</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">tags</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">blog</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">hugo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">hextra</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">ruby</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">draft</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">...</span><span class="l">.</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E segue o artigo escrito em Markdown. Feito isso, do diretório do projeto tenho que fazer:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> content
</span></span><span class="line"><span class="cl">ruby generate_index.rb
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ..</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso atualiza a página principal com o link pro novo artigo no topo. Pra garantir que está tudo certo, posso checar subindo o servidor local:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">❯ hugo server --buildDrafts --disableFastRender
</span></span><span class="line"><span class="cl">Watching <span class="k">for</span> changes in /home/akitaonrails/Projects/akitaonrails-hugo/<span class="o">{</span>content,i18n,layouts<span class="o">}</span>
</span></span><span class="line"><span class="cl">Watching <span class="k">for</span> config changes in /home/akitaonrails/Projects/akitaonrails-hugo/hugo.yaml, /home/akitaonrails/Projects/akitaonrails-hugo/go.mod
</span></span><span class="line"><span class="cl">Start building sites …
</span></span><span class="line"><span class="cl">hugo v0.149.1+extended+withdeploy linux/amd64 <span class="nv">BuildDate</span><span class="o">=</span>unknown
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">                  │  EN
</span></span><span class="line"><span class="cl">──────────────────┼──────
</span></span><span class="line"><span class="cl"> Pages            │ <span class="m">3374</span>
</span></span><span class="line"><span class="cl"> Paginator pages  │    <span class="m">0</span>
</span></span><span class="line"><span class="cl"> Non-page files   │  <span class="m">131</span>
</span></span><span class="line"><span class="cl"> Static files     │   <span class="m">11</span>
</span></span><span class="line"><span class="cl"> Processed images │    <span class="m">0</span>
</span></span><span class="line"><span class="cl"> Aliases          │    <span class="m">0</span>
</span></span><span class="line"><span class="cl"> Cleaned          │    <span class="m">0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Built in <span class="m">8799</span> ms
</span></span><span class="line"><span class="cl">Environment: <span class="s2">&#34;development&#34;</span>
</span></span><span class="line"><span class="cl">Serving pages from disk
</span></span><span class="line"><span class="cl">Web Server is available at http://localhost:1313/ <span class="o">(</span><span class="nb">bind</span> address 127.0.0.1<span class="o">)</span>
</span></span><span class="line"><span class="cl">Press Ctrl+C to stop</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora posso acessar no navegador em <code>http://localhost:1313</code>:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910141059_screenshot-2025-09-10_14-10-46.png" alt="hugo server"  loading="lazy" /></p>
<p>Finalmente, basta deployar:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git add .
</span></span><span class="line"><span class="cl">git commit -m <span class="s2">&#34;adding new blog article&#34;</span>
</span></span><span class="line"><span class="cl">git push origin master</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E é só isso!</p>
<h3>Deploy<span class="hx:absolute hx:-mt-20" id="deploy"></span>
    <a href="#deploy" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Falei que pretendia fazer deploy no GitHub Pages, mas na última hora resolvi que queria fazer deploy na [Netlify]</p>
<p>Por que? Porque eu nunca tinha usado, então resolvi que queria ver como era. Basicamente isso. Eu achei barato, tem as funcionalidades que precisava, então, por que não?</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910141436_screenshot-2025-09-10_14-14-25.png" alt="netlify project"  loading="lazy" /></p>
<p>A única coisa que precisei fazer de diferente, foi configurar o deploy linkando com meu repositório do Github:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910141645_screenshot-2025-09-10_14-16-25.png" alt="netlify deploy"  loading="lazy" /></p>
<p>E, no GitHub, criar uma Action assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># Sample workflow for building and deploying a Hugo site to GitHub Pages</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Deploy Hugo site to Pages</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Runs on pushes targeting the default branch</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;master&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Allows you to run this workflow manually from the Actions tab</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">workflow_dispatch</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">permissions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">contents</span><span class="p">:</span><span class="w"> </span><span class="l">read</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pages</span><span class="p">:</span><span class="w"> </span><span class="l">write</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">id-token</span><span class="p">:</span><span class="w"> </span><span class="l">write</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">concurrency</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;pages&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cancel-in-progress</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Default to bash</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">defaults</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">run</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">shell</span><span class="p">:</span><span class="w"> </span><span class="l">bash</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Build job</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">build</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">HUGO_VERSION</span><span class="p">:</span><span class="w"> </span><span class="m">0.145.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Checkout</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">fetch-depth</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="w">  </span><span class="c"># fetch all history for .GitInfo and .Lastmod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">submodules</span><span class="p">:</span><span class="w"> </span><span class="l">recursive</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Setup Go</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/setup-go@v5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">go-version</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;1.24&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Setup Pages</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">pages</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/configure-pages@v4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Setup Hugo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">          wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \
</span></span></span><span class="line"><span class="cl"><span class="sd">          &amp;&amp; sudo dpkg -i ${{ runner.temp }}/hugo.deb</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Build with Hugo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="c"># For maximum backward compatibility with Hugo modules</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">HUGO_ENVIRONMENT</span><span class="p">:</span><span class="w"> </span><span class="l">production</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">HUGO_ENV</span><span class="p">:</span><span class="w"> </span><span class="l">production</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">          hugo \
</span></span></span><span class="line"><span class="cl"><span class="sd">            --gc --minify \
</span></span></span><span class="line"><span class="cl"><span class="sd">          --baseURL &#34;https://${{ github.repository_owner }}.github.io/&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Upload artifact</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/upload-pages-artifact@v3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">./public</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Deployment job</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">github-pages</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="l">${{ steps.deployment.outputs.page_url }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="l">build</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Deploy to GitHub Pages</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/deploy-pages@v4</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Com isso, toda vez que faço <code>git push</code>, vai rodar esse job, gerar o site e mandar pro Netlify direto. Tudo automatizado, como Deus sempre quis 🤣:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910141920_screenshot-2025-09-10_14-19-03.png" alt="GitHub Actions"  loading="lazy" /></p>
<p>Aliás, eu falei que o Netlify era &ldquo;barato&rdquo;, na verdade eu quis dizer que é <strong>gratuito</strong>. Pra sites pequenos como o meu, não custa literalmente nada:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910142044_screenshot-2025-09-10_14-20-30.png" alt="netlify plan"  loading="lazy" /></p>
<p>Pago zero dólares e ainda aprendi algo novo. Por isso essas coisas são divertidas.</p>
<h3>Scripts<span class="hx:absolute hx:-mt-20" id="scripts"></span>
    <a href="#scripts" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Pra converter os artigos do meu blog antigo pro Hugo, acabei fazendo alguns scripts de correção. Eles ainda estão no repositório como <code>content/fix_*.rb</code>. Não precisa saber pra que eles servem, mas estão lá caso queiram fuçar. O mais importante era manter as URIs iguais eram no blog anterior pro Google continuar achando corretamente.</p>
<p>O único que realmente interessa é o <code>generate_index.rb</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;yaml&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;find&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;date&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">escape_markdown</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">text</span><span class="o">.</span><span class="n">to_s</span><span class="o">.</span><span class="n">gsub</span><span class="p">(</span><span class="s1">&#39;[&#39;</span><span class="p">,</span> <span class="s1">&#39;\\[&#39;</span><span class="p">)</span><span class="o">.</span><span class="n">gsub</span><span class="p">(</span><span class="s1">&#39;]&#39;</span><span class="p">,</span> <span class="s1">&#39;\\]&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">entries</span> <span class="o">=</span> <span class="o">[]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="no">Find</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s1">&#39;.&#39;</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">path</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="k">next</span> <span class="k">unless</span> <span class="n">path</span><span class="o">.</span><span class="n">end_with?</span><span class="p">(</span><span class="s1">&#39;index.md&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">next</span> <span class="k">if</span> <span class="n">path</span> <span class="o">==</span> <span class="s1">&#39;./index.md&#39;</span> <span class="o">||</span> <span class="n">path</span> <span class="o">==</span> <span class="s1">&#39;./_index.md&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">begin</span>
</span></span><span class="line"><span class="cl">    <span class="n">lines</span> <span class="o">=</span> <span class="no">File</span><span class="o">.</span><span class="n">readlines</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">lines</span><span class="o">.</span><span class="n">first</span><span class="o">&amp;.</span><span class="n">strip</span> <span class="o">==</span> <span class="s1">&#39;---&#39;</span>
</span></span><span class="line"><span class="cl">      <span class="n">fm_lines</span> <span class="o">=</span> <span class="o">[]</span>
</span></span><span class="line"><span class="cl">      <span class="n">i</span> <span class="o">=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">      <span class="k">while</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">lines</span><span class="o">.</span><span class="n">size</span> <span class="o">&amp;&amp;</span> <span class="n">lines</span><span class="o">[</span><span class="n">i</span><span class="o">].</span><span class="n">strip</span> <span class="o">!=</span> <span class="s1">&#39;---&#39;</span>
</span></span><span class="line"><span class="cl">        <span class="n">fm_lines</span> <span class="o">&lt;&lt;</span> <span class="n">lines</span><span class="o">[</span><span class="n">i</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">        <span class="n">i</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="n">lines</span><span class="o">[</span><span class="n">i</span><span class="o">]&amp;.</span><span class="n">strip</span> <span class="o">==</span> <span class="s1">&#39;---&#39;</span>
</span></span><span class="line"><span class="cl">        <span class="n">front</span> <span class="o">=</span> <span class="no">YAML</span><span class="o">.</span><span class="n">safe_load</span><span class="p">(</span><span class="n">fm_lines</span><span class="o">.</span><span class="n">join</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">front</span> <span class="o">&amp;&amp;</span> <span class="n">front</span><span class="o">[</span><span class="s1">&#39;title&#39;</span><span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="n">front</span><span class="o">[</span><span class="s1">&#39;date&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">          <span class="n">date</span> <span class="o">=</span> <span class="no">Date</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">front</span><span class="o">[</span><span class="s1">&#39;date&#39;</span><span class="o">].</span><span class="n">to_s</span><span class="p">)</span> <span class="k">rescue</span> <span class="kp">nil</span>
</span></span><span class="line"><span class="cl">          <span class="k">if</span> <span class="n">date</span>
</span></span><span class="line"><span class="cl">            <span class="n">url</span> <span class="o">=</span> <span class="n">path</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="s1">&#39;./&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">)</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="s1">&#39;/index.md&#39;</span><span class="p">,</span> <span class="s1">&#39;/&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">entries</span> <span class="o">&lt;&lt;</span> <span class="p">{</span> <span class="s1">&#39;title&#39;</span> <span class="o">=&gt;</span> <span class="n">front</span><span class="o">[</span><span class="s1">&#39;title&#39;</span><span class="o">]</span><span class="p">,</span> <span class="s1">&#39;url&#39;</span> <span class="o">=&gt;</span> <span class="n">url</span><span class="p">,</span> <span class="s1">&#39;date&#39;</span> <span class="o">=&gt;</span> <span class="n">date</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">          <span class="k">end</span>
</span></span><span class="line"><span class="cl">        <span class="k">end</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">rescue</span> <span class="o">=&gt;</span> <span class="n">e</span>
</span></span><span class="line"><span class="cl">    <span class="nb">warn</span> <span class="s2">&#34;YAML error in </span><span class="si">#{</span><span class="n">path</span><span class="si">}</span><span class="s2">: </span><span class="si">#{</span><span class="n">e</span><span class="o">.</span><span class="n">message</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Sort newest first</span>
</span></span><span class="line"><span class="cl"><span class="n">entries</span><span class="o">.</span><span class="n">sort_by!</span> <span class="p">{</span> <span class="o">|</span><span class="n">e</span><span class="o">|</span> <span class="n">e</span><span class="o">[</span><span class="s1">&#39;date&#39;</span><span class="o">]</span> <span class="p">}</span><span class="o">.</span><span class="n">reverse!</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Group by year and month</span>
</span></span><span class="line"><span class="cl"><span class="n">grouped</span> <span class="o">=</span> <span class="n">entries</span><span class="o">.</span><span class="n">group_by</span> <span class="p">{</span> <span class="o">|</span><span class="n">e</span><span class="o">|</span> <span class="o">[</span><span class="n">e</span><span class="o">[</span><span class="s1">&#39;date&#39;</span><span class="o">].</span><span class="n">year</span><span class="p">,</span> <span class="n">e</span><span class="o">[</span><span class="s1">&#39;date&#39;</span><span class="o">].</span><span class="n">month</span><span class="o">]</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Sort year-month pairs descending</span>
</span></span><span class="line"><span class="cl"><span class="n">sorted_keys</span> <span class="o">=</span> <span class="n">grouped</span><span class="o">.</span><span class="n">keys</span><span class="o">.</span><span class="n">sort</span><span class="o">.</span><span class="n">reverse</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="no">File</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="s1">&#39;_index.md&#39;</span><span class="p">,</span> <span class="s1">&#39;w&#39;</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">f</span><span class="o">.</span><span class="n">puts</span> <span class="s2">&#34;---&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">f</span><span class="o">.</span><span class="n">puts</span> <span class="s2">&#34;title: AkitaOnRails&#39;s Blog&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">f</span><span class="o">.</span><span class="n">puts</span> <span class="s2">&#34;---&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">f</span><span class="o">.</span><span class="n">puts</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">sorted_keys</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="p">(</span><span class="n">year</span><span class="p">,</span> <span class="n">month</span><span class="p">)</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="n">month_name</span> <span class="o">=</span> <span class="no">Date</span><span class="o">::</span><span class="no">MONTHNAMES</span><span class="o">[</span><span class="n">month</span><span class="o">]</span> <span class="c1"># &#34;May&#34;, &#34;June&#34;, etc</span>
</span></span><span class="line"><span class="cl">    <span class="n">f</span><span class="o">.</span><span class="n">puts</span> <span class="s2">&#34;## </span><span class="si">#{</span><span class="n">year</span><span class="si">}</span><span class="s2"> - </span><span class="si">#{</span><span class="n">month_name</span><span class="si">}</span><span class="se">\n\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">grouped</span><span class="o">[[</span><span class="n">year</span><span class="p">,</span> <span class="n">month</span><span class="o">]].</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">post</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="n">f</span><span class="o">.</span><span class="n">puts</span> <span class="s2">&#34;- [</span><span class="si">#{</span><span class="n">escape_markdown</span><span class="p">(</span><span class="n">post</span><span class="o">[</span><span class="s1">&#39;title&#39;</span><span class="o">]</span><span class="p">)</span><span class="si">}</span><span class="s2">](</span><span class="si">#{</span><span class="n">post</span><span class="o">[</span><span class="s1">&#39;url&#39;</span><span class="o">]</span><span class="si">}</span><span class="s2">)&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="n">f</span><span class="o">.</span><span class="n">puts</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">puts</span> <span class="s2">&#34;Generated _index.md with posts grouped by year &amp; month.&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Esse script tem um pequeno bug: ele ordena os artigos a partir do diretório de mês e dia, mas significa que se eu postar várias vezes durante o dia, vai sair fora de ordem, ordenado pelo título do artigo. Veja: <code>sorted_keys.each do |(year, month)|</code></p>
<p>O certo seria abrir o artigo e pegar o metadado de timestamp, mas fiquei com preguiça. Se alguém quiser corrigir e mandar um PR, será bem vindo.</p>
<p>Por que fiz em Ruby e não em Python ou Bash?? Porque foi como eu quis 🤣, não perturba.</p>
<h3>Imagens no S3<span class="hx:absolute hx:-mt-20" id="imagens-no-s3"></span>
    <a href="#imagens-no-s3" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Como contei antes, uma das conveniências do meu antigo engine feito em Rails, é que eu tinha feito o ActiveAdmin com suporte a fazer uploads pro meu bucket de S3. E era um setup complicado porque ainda adicionava AWS CloudFront na frente pra fazer cache dessas imagens em CDN e tudo mais.</p>
<p>Mas meu site é ultra-pequeno, não precisa da complexidade de CloudFront, então tirei e agora faço links direto pro bucket, por exemplo: <code>https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/screenshot-2025-09-10_00-35-57.png</code></p>
<p>Significa que meu workflow consiste em tirar screenshots, depois abrir o site de console do meu AWS, e manualmente fazer upload no bucket, via interface web, copiar o link gerado e colar no Markdown do artigo. Não é complicado, mas é um BEM chatinho.</p>
<p>Cheguei a fazer um script besta usando <a href="https://github.com/aws/aws-cli"target="_blank" rel="noopener"><strong>aws-cli</strong></a> mas isso exigia manter um terminal aberto no diretório de screenshots e, manualmente fazer algo como <code>upload-s3 screenshot-2025-09-10_00-35-57.png</code>, o que não é exatamente conveniente também. Aliás, se não sabia disso, você pode automatizar muita coisa da AWS só usando essa ferramenta.</p>
<p>Enquanto estava pensando em escrever este artigo, resolvi consertar esse problema de uma vez. Como sabem, estou usando Omarchy, e o File Explorer padrão dele é o bom e velho <strong>Nautilus</strong> do GNOME. Gosto dele, parece muito com o Finder de MacOS. Gosto mais dele do que um Dolphin ou Thunar, que parecem mais com o Windows Explorer. E eu nunca gostei de Windows Explorer.</p>
<blockquote>
  <p><strong>&ldquo;E se eu pudesse só clicar com o botão direito do mouse em cima do arquivo de screenshot e selecionar &rsquo;enviar pro s3&rsquo; no menu de contexto e já ganhar a URL copiada no meu clipboard? 🤔&rdquo;</strong></p>

</blockquote>
<p>Que tal assim?</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910143157_send-to-s3.png" alt="nautilus send to s3"  loading="lazy" /></p>
<p>Nautilus suporta scripts de usuário, então fiz o seguinte:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p ~/.local/share/nautilus/scripts
</span></span><span class="line"><span class="cl">nvim ~/.local/share/nautilus/scripts/Send<span class="se">\ </span>to<span class="se">\ </span>AWS<span class="se">\ </span>S3</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E eis o script:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="c1">#set -euo pipefail</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ---------- config ----------</span>
</span></span><span class="line"><span class="cl"><span class="nv">BUCKET</span><span class="o">=</span><span class="s2">&#34;new-uploads-akitaonrails&#34;</span> <span class="c1"># name only</span>
</span></span><span class="line"><span class="cl"><span class="nv">PREFIX</span><span class="o">=</span><span class="s2">&#34;&#34;</span>                         <span class="c1"># e.g., &#34;uploads/&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">AWS_PROFILE</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">AWS_PROFILE</span><span class="k">:-</span><span class="nv">default</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># URL_MODE: presign | public | cloudfront</span>
</span></span><span class="line"><span class="cl"><span class="nv">URL_MODE</span><span class="o">=</span><span class="s2">&#34;public&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">PRESIGN_TTL</span><span class="o">=</span><span class="m">604800</span>
</span></span><span class="line"><span class="cl"><span class="nv">CLOUDFRONT_DOMAIN</span><span class="o">=</span><span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">MAKE_PUBLIC_ACL</span><span class="o">=</span><span class="nb">false</span> <span class="c1"># keep false unless your bucket denies public reads</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ----------------------------</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">require<span class="o">()</span> <span class="o">{</span> <span class="nb">command</span> -v <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> &gt;/dev/null 2&gt;<span class="p">&amp;</span>1<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># clipboard: wl-copy &gt; xclip &gt; none</span>
</span></span><span class="line"><span class="cl"><span class="nv">CLIP_CMD</span><span class="o">=</span><span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> require wl-copy<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nv">CLIP_CMD</span><span class="o">=</span><span class="s2">&#34;wl-copy&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">elif</span> require xclip<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nv">CLIP_CMD</span><span class="o">=</span><span class="s2">&#34;xclip -selection clipboard&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">region<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> r
</span></span><span class="line"><span class="cl">  <span class="nv">r</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>aws --profile <span class="s2">&#34;</span><span class="nv">$AWS_PROFILE</span><span class="s2">&#34;</span> s3api get-bucket-location --bucket <span class="s2">&#34;</span><span class="nv">$BUCKET</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    --query <span class="s1">&#39;LocationConstraint&#39;</span> --output text 2&gt;/dev/null <span class="o">||</span> <span class="nb">echo</span> <span class="s2">&#34;None&#34;</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$r</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;None&#34;</span> <span class="o">||</span> <span class="s2">&#34;</span><span class="nv">$r</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;null&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">&#34;us-east-1&#34;</span> <span class="o">||</span> <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$r</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">public_url<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> <span class="nv">key</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="nv">r</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$2</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[[</span> -n <span class="s2">&#34;</span><span class="nv">$CLOUDFRONT_DOMAIN</span><span class="s2">&#34;</span> <span class="o">&amp;&amp;</span> <span class="s2">&#34;</span><span class="nv">$URL_MODE</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;cloudfront&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">printf</span> <span class="s1">&#39;https://%s/%s\n&#39;</span> <span class="s2">&#34;</span><span class="nv">$CLOUDFRONT_DOMAIN</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$key</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$r</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;us-east-1&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">      <span class="nb">printf</span> <span class="s1">&#39;https://%s.s3.amazonaws.com/%s\n&#39;</span> <span class="s2">&#34;</span><span class="nv">$BUCKET</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$key</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="nb">printf</span> <span class="s1">&#39;https://%s.s3.%s.amazonaws.com/%s\n&#39;</span> <span class="s2">&#34;</span><span class="nv">$BUCKET</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$r</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$key</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">presign_url<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> <span class="nv">key</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  aws --profile <span class="s2">&#34;</span><span class="nv">$AWS_PROFILE</span><span class="s2">&#34;</span> s3 presign <span class="s2">&#34;s3://</span><span class="nv">$BUCKET</span><span class="s2">/</span><span class="nv">$key</span><span class="s2">&#34;</span> --expires-in <span class="s2">&#34;</span><span class="nv">$PRESIGN_TTL</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">upload_one<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> <span class="nv">src</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="nv">key</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$2</span><span class="s2">&#34;</span> <span class="nv">args</span><span class="o">=(</span>--only-show-errors --follow-symlinks<span class="o">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$URL_MODE</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;public&#34;</span> <span class="o">&amp;&amp;</span> <span class="s2">&#34;</span><span class="nv">$MAKE_PUBLIC_ACL</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;true&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nv">args</span><span class="o">+=(</span>--acl public-read<span class="o">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">  aws --profile <span class="s2">&#34;</span><span class="nv">$AWS_PROFILE</span><span class="s2">&#34;</span> s3 cp <span class="s2">&#34;</span><span class="si">${</span><span class="nv">args</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$src</span><span class="s2">&#34;</span> <span class="s2">&#34;s3://</span><span class="nv">$BUCKET</span><span class="s2">/</span><span class="nv">$key</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># allow running from terminal with paths as args</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="nv">$#</span> -gt <span class="m">0</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nv">NAUTILUS_SCRIPT_SELECTED_FILE_PATHS</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span><span class="nb">printf</span> <span class="s1">&#39;%s\n&#39;</span> <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">SEL</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">NAUTILUS_SCRIPT_SELECTED_FILE_PATHS</span><span class="k">:-</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">[[</span> -n <span class="s2">&#34;</span><span class="nv">$SEL</span><span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">||</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  notify-send <span class="s2">&#34;S3 upload&#34;</span> <span class="s2">&#34;No selection&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">exit</span> <span class="m">0</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">R</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>region<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">urls</span><span class="o">=()</span>
</span></span><span class="line"><span class="cl"><span class="nv">ok</span><span class="o">=</span><span class="m">0</span>
</span></span><span class="line"><span class="cl"><span class="nv">fail</span><span class="o">=</span><span class="m">0</span>
</span></span><span class="line"><span class="cl"><span class="nv">errs</span><span class="o">=()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">while</span> <span class="nv">IFS</span><span class="o">=</span> <span class="nb">read</span> -r path<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="o">[[</span> -e <span class="s2">&#34;</span><span class="nv">$path</span><span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">||</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="nv">errs</span><span class="o">+=(</span><span class="s2">&#34;Missing: </span><span class="nv">$path</span><span class="s2">&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">((</span>fail++<span class="o">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">continue</span>
</span></span><span class="line"><span class="cl">  <span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[[</span> -d <span class="s2">&#34;</span><span class="nv">$path</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nv">base</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>basename <span class="s2">&#34;</span><span class="nv">$path</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">while</span> <span class="nv">IFS</span><span class="o">=</span> <span class="nb">read</span> -r -d <span class="s1">&#39;&#39;</span> f<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="nv">rel</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">f</span><span class="p">#</span><span class="s2">&#34;</span><span class="nv">$path</span><span class="s2">&#34;</span><span class="p">/</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="nv">key</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">PREFIX</span><span class="si">}${</span><span class="nv">base</span><span class="si">}</span><span class="s2">/</span><span class="si">${</span><span class="nv">rel</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> upload_one <span class="s2">&#34;</span><span class="nv">$f</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$key</span><span class="s2">&#34;</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        <span class="k">case</span> <span class="s2">&#34;</span><span class="nv">$URL_MODE</span><span class="s2">&#34;</span> in
</span></span><span class="line"><span class="cl">        presign<span class="o">)</span> <span class="nv">urls</span><span class="o">+=(</span><span class="s2">&#34;</span><span class="k">$(</span>presign_url <span class="s2">&#34;</span><span class="nv">$key</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span><span class="o">)</span> <span class="p">;;</span>
</span></span><span class="line"><span class="cl">        public <span class="p">|</span> cloudfront<span class="o">)</span> <span class="nv">urls</span><span class="o">+=(</span><span class="s2">&#34;</span><span class="k">$(</span>public_url <span class="s2">&#34;</span><span class="nv">$key</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$R</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span><span class="o">)</span> <span class="p">;;</span>
</span></span><span class="line"><span class="cl">        <span class="k">esac</span>
</span></span><span class="line"><span class="cl">        <span class="o">((</span>ok++<span class="o">))</span>
</span></span><span class="line"><span class="cl">      <span class="k">else</span>
</span></span><span class="line"><span class="cl">        <span class="nv">errs</span><span class="o">+=(</span><span class="s2">&#34;Failed: </span><span class="nv">$f</span><span class="s2">&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">        <span class="o">((</span>fail++<span class="o">))</span>
</span></span><span class="line"><span class="cl">      <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    <span class="k">done</span> &lt; &lt;<span class="o">(</span>find <span class="s2">&#34;</span><span class="nv">$path</span><span class="s2">&#34;</span> -type f -print0<span class="o">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    <span class="nv">ts</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>date +%Y%m%d%H%M%S<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nv">filename</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>basename <span class="s2">&#34;</span><span class="nv">$path</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nv">key</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">PREFIX</span><span class="si">}${</span><span class="nv">ts</span><span class="si">}</span><span class="s2">_</span><span class="si">${</span><span class="nv">filename</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> upload_one <span class="s2">&#34;</span><span class="nv">$path</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$key</span><span class="s2">&#34;</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">      <span class="k">case</span> <span class="s2">&#34;</span><span class="nv">$URL_MODE</span><span class="s2">&#34;</span> in
</span></span><span class="line"><span class="cl">      presign<span class="o">)</span> <span class="nv">urls</span><span class="o">+=(</span><span class="s2">&#34;</span><span class="k">$(</span>presign_url <span class="s2">&#34;</span><span class="nv">$key</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span><span class="o">)</span> <span class="p">;;</span>
</span></span><span class="line"><span class="cl">      public <span class="p">|</span> cloudfront<span class="o">)</span> <span class="nv">urls</span><span class="o">+=(</span><span class="s2">&#34;</span><span class="k">$(</span>public_url <span class="s2">&#34;</span><span class="nv">$key</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$R</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span><span class="o">)</span> <span class="p">;;</span>
</span></span><span class="line"><span class="cl">      <span class="k">esac</span>
</span></span><span class="line"><span class="cl">      <span class="o">((</span>ok++<span class="o">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="nv">errs</span><span class="o">+=(</span><span class="s2">&#34;Failed: </span><span class="nv">$path</span><span class="s2">&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">      <span class="o">((</span>fail++<span class="o">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span> <span class="o">&lt;&lt;&lt;</span><span class="s2">&#34;</span><span class="nv">$SEL</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">out</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span><span class="nb">printf</span> <span class="s1">&#39;%s\n&#39;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">urls</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># --- relax only for UX bits ---</span>
</span></span><span class="line"><span class="cl"><span class="nb">set</span> +e
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> -n <span class="s2">&#34;</span><span class="nv">$out</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">printf</span> <span class="s1">&#39;%s&#39;</span> <span class="s2">&#34;</span><span class="nv">$out</span><span class="s2">&#34;</span> <span class="p">|</span> wl-copy -n <span class="o">||</span> <span class="nb">true</span> <span class="c1"># or xclip fallback</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nb">command</span> -v notify-send &gt;/dev/null 2&gt;<span class="p">&amp;</span>1<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">((</span><span class="nv">fail</span> <span class="o">==</span> 0<span class="o">))</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    notify-send <span class="s2">&#34;S3 upload: </span><span class="si">${</span><span class="nv">ok</span><span class="si">}</span><span class="s2"> ok&#34;</span> <span class="s2">&#34;</span><span class="k">$(</span><span class="nb">printf</span> <span class="s1">&#39;%s\n&#39;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">urls</span><span class="p">[@]:</span><span class="nv">0</span><span class="p">:</span><span class="nv">5</span><span class="si">}</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span> <span class="o">||</span> <span class="nb">true</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    notify-send -u critical <span class="s2">&#34;S3 upload: </span><span class="si">${</span><span class="nv">ok</span><span class="si">}</span><span class="s2"> ok, </span><span class="si">${</span><span class="nv">fail</span><span class="si">}</span><span class="s2"> failed&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">      <span class="s2">&#34;</span><span class="k">$(</span><span class="nb">printf</span> <span class="s1">&#39;%s\n&#39;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">errs</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">|</span> head -n 10<span class="k">)</span><span class="s2">&#34;</span> <span class="o">||</span> <span class="nb">true</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="nb">set</span> -e
</span></span><span class="line"><span class="cl"><span class="c1"># --- end relax ---</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">printf</span> <span class="s1">&#39;%s\n&#39;</span> <span class="s2">&#34;</span><span class="nv">$out</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">((</span><span class="nv">fail</span> <span class="o">==</span> 0<span class="o">))</span> <span class="o">||</span> <span class="nb">exit</span> <span class="m">1</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Obviamente troque o <code>BUCKET</code> pelo seu bucket. Se tiver múltiplos buckets, depois reorganize esse script e faça <code>source</code> em múltiplos scripts como <code>Send To Bucket XYZ</code> e <code>Send to Bucket ABC</code> ou algo assim no diretório de <code>nautilus/scripts</code>.</p>
<p>Se quiser testar do terminal, só fazer assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># single file</span>
</span></span><span class="line"><span class="cl"><span class="nv">NAUTILUS_SCRIPT_SELECTED_FILE_PATHS</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span><span class="nb">printf</span> <span class="s1">&#39;%s\n&#39;</span> <span class="s1">&#39;/full/path/to/image.png&#39;</span><span class="k">)</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">bash -x ~/.local/share/nautilus/scripts/Send<span class="se">\ </span>to<span class="se">\ </span>AWS<span class="se">\ </span>S3</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Notem que comentei o <code>set -e</code> do Bash porque ou o <code>wl-copy</code> ou o <code>notify-send</code> está retornando algum warning ou erro e isso está quebrando o script. Sem isso, termina corretamente e copia a URL do upload no clipboard. Daí posso colar direto no editor de textos.</p>
<h3>Bônus: Screenshots em Omarchy<span class="hx:absolute hx:-mt-20" id="bônus-screenshots-em-omarchy"></span>
    <a href="#b%c3%b4nus-screenshots-em-omarchy" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><blockquote>
  <p>Inclusive, dica bônus de Omarchy: do nada o script de screenshot começou a falhar pra mim. Vamos consertar.</p>

</blockquote>
<p>Primeiro, eu queria que os screenshots fossem pro diretório <code>Pictures/Screenshots</code>, segundo, depois de uma atualização, sei lá porque o comando <code>hyprshot</code> parou de funcionar com o <code>satty</code>. Sem paciência durante a madrugada eu editei o script de screenshot do Omarchy, que fica em <code>~/.local/share/omarchy/bin/omarchy-cmd-screenshot</code> (fucem lá), e substituí por isto:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">[[</span> -f ~/.config/user-dirs.dirs <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">source</span> ~/.config/user-dirs.dirs
</span></span><span class="line"><span class="cl"><span class="nv">OUT</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">OMARCHY_SCREENSHOT_DIR</span><span class="k">:-</span><span class="si">${</span><span class="nv">XDG_PICTURES_DIR</span><span class="k">:-</span><span class="nv">$HOME</span><span class="p">/Pictures</span><span class="si">}}</span><span class="s2">/Screenshots&#34;</span>
</span></span><span class="line"><span class="cl">mkdir -p <span class="s2">&#34;</span><span class="nv">$OUT</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">satty --filename - <span class="se">\
</span></span></span><span class="line"><span class="cl">  --output-filename <span class="s2">&#34;</span><span class="nv">$OUT</span><span class="s2">/screenshot-</span><span class="k">$(</span>date +<span class="s1">&#39;%Y-%m-%d_%H-%M-%S&#39;</span><span class="k">)</span><span class="s2">.png&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --early-exit <span class="se">\
</span></span></span><span class="line"><span class="cl">  --actions-on-enter save-to-clipboard <span class="se">\
</span></span></span><span class="line"><span class="cl">  --save-after-copy <span class="se">\
</span></span></span><span class="line"><span class="cl">  --copy-command <span class="s1">&#39;wl-copy&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  &lt; &lt;<span class="o">(</span>slurp <span class="p">|</span> grim -g - -<span class="o">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Na prática, troquei o <code>hyprshot</code> pelo <a href="https://github.com/emersion/grim"target="_blank" rel="noopener"><strong>grim</strong></a>, que permite capturar foto de tela de Wayland (cuidado que tem várias ferramentas que só funcionam em X11, Omarchy é Hyprland, portanto, Wayland).</p>
<p>Além disso o Omarchy não tem opção pra tirar foto de tela com <strong>timer</strong>. Isso é útil quando quero tirar foto de um menu aberto com item selecionado. Se usar o jeito normal com tecla Print-Screen ou usando o menu do Omarchy, o menu que eu tinha aberto pode fechar antes de conseguir a foto de tela. A solução é um timer.</p>
<p>E com <code>grim</code> é fácil, via linha de comando mesmo posso fazer:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sleep <span class="m">10</span> <span class="o">&amp;&amp;</span> grim -g <span class="s2">&#34;2813,948 2560x1440&#34;</span> ~/Pictures/Screenshots/image-01.png</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Em Wayland, dois monitores são como se fossem uma única tela gigante, então precisa dar a geometria correta de onde quero tirar a foto. ChatGPT ajuda pra isso se passar o seu <code>.config/hypr/monitors.conf</code> pra ele.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910150146_screenshot-2025-09-10_15-01-32.png" alt="chatgpt"  loading="lazy" /></p>
<p>Agora tenho uma forma simples de tirar fotos de tela e enviar direto pro S3, direto do Nautilus, e conseguindo tirar uma foto de tela como esta:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910144429_nautilus-script.png" alt="Pinta"  loading="lazy" /></p>
<p>E o programa que eu uso, pra fazer coisas simples como um <strong>crop</strong> de alguma foto de tela, é o <a href="https://github.com/PintaProject/Pinta"target="_blank" rel="noopener"><strong>Pinta</strong></a></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910144520_screenshot-2025-09-10_14-45-05.png" alt="Pinta"  loading="lazy" /></p>
<h3>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Isso é o que lembrei de importante sobre meu novo blog. Acho que ficou muito mais simples de dar manutenção e, mais importante, muito mais simples de escrever artigos: só abrir um editor de textos e pronto. Nenhum deploy mais complicado, tudo automatizado, tudo integrado com meu Omarchy.</p>
<p>Meu blog já tem mais de <strong>20 anos</strong> e durante esse tempo eu migrei de engine de blog várias vezes, uma meia dúzia de vezes. Por isso é um esforço inútil achar que você vai acertar logo de primeira e gastar semanas ou meses tentando fazer o engine ideal: você vai fracassar.</p>
<p>No final, a única coisa que interessa é <strong>O CONTEÚDO</strong>. O exercício de escrever, de escrever o mínimo possível pra comunicar o máximo de informação relevante, de escrever uma história que engaja do começo ao fim, de não virar formato de TCC ou página de Wiki de documentação. Escrever uma documentação é diferente de escrever um post de blog.</p>
<p>É mais importante escrever de uma forma que você mesmo goste de ler depois do que em qualquer linguagem vai fazer sua engine.</p>
]]></content:encoded><category>blog</category><category>devops</category><category>automacao</category></item><item><title>[Akitando #139] - Entendendo Como Containers Funcionam</title><link>https://akitaonrails.github.io/2023/03/02/akitando-139-entendendo-como-containers-funcionam/</link><guid isPermaLink="true">https://akitaonrails.github.io/2023/03/02/akitando-139-entendendo-como-containers-funcionam/</guid><pubDate>Thu, 02 Mar 2023 09:00:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/85k8se4Zo70"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;Finalmente vou pagar promessa. Este é o video sobre containers e Docker que venho falando faz vários videos atrás, que ia fazer. Mas eu precisava explicar a instalação, como Linux boota, como funciona virtualização, pra finalmente chegar em containers. Então este episódio vai amarrar tudo que vocês aprenderam nos videos de Linux, de armazenamento, etc e finalmente vão entender containers do jeito certo.&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/85k8se4Zo70"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Finalmente vou pagar promessa. Este é o video sobre containers e Docker que venho falando faz vários videos atrás, que ia fazer. Mas eu precisava explicar a instalação, como Linux boota, como funciona virtualização, pra finalmente chegar em containers. Então este episódio vai amarrar tudo que vocês aprenderam nos videos de Linux, de armazenamento, etc e finalmente vão entender containers do jeito certo.</p>
<h2>Erratas<span class="hx:absolute hx:-mt-20" id="erratas"></span>
    <a href="#erratas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>5:12 - tá errado, isso é em outros comandos, como top eu acho, que mostraria 400% se tivesse usando 4 CPU. Do jeito que eu falei ficou parecendo que naquele quadrado vermelho mostraria isso, mas ali é POR cpu/thread, por isso vai só até 100%. Só em linhas de comando que mostra num número só quanto que tá se usando que apareceria, por exemplo, 400%.</p>
<h2>Conteúdo<span class="hx:absolute hx:-mt-20" id="conteúdo"></span>
    <a href="#conte%c3%bado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>00:00 - Intro: qual a diferença de VMs e Containers?</li>
<li>01:49 - CAP 1 - Limitando Processos - cgroups</li>
<li>07:43 - CAP 2 - Enganando Processos - namespaces</li>
<li>14:45 - CAP 3 - Recapitulando Containers - LXC, Docker, OCI</li>
<li>17:34 - CAP 4 - A Base de Containers - RunC</li>
<li>22:29 - CAP 5 - Hello World de Docker - O que tem dentro?</li>
<li>33:20 - CAP 6 - Restringindo Processos Ainda Mais - Capabilities</li>
<li>37:18 - CAP 7 - Orquestrando Containers - Docker Compose</li>
<li>41:26 - CAP 8 - Bônus do Episódio &ldquo;Tornando sua App Web mais Rápida&rdquo;  - Load Balancer com Docker</li>
<li>43:25 - CAP 9 - Orquestra de Gente Grande - K8S e Terraform</li>
<li>47:17 - CAP 10 - Outros Usos de Containers - Apps em Containers?</li>
<li>51:28 - Bloopers</li>
</ul>
<h2>Links<span class="hx:absolute hx:-mt-20" id="links"></span>
    <a href="#links" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li><a href="https://github.com/akitaonrails/episode0139"target="_blank" rel="noopener">https://github.com/akitaonrails/episode0139</a></li>
<li><a href="https://www.zsiegel.com/2022/01/17/load-balancing-in-docker-with-nginx"target="_blank" rel="noopener">https://www.zsiegel.com/2022/01/17/load-balancing-in-docker-with-nginx</a></li>
<li><a href="https://www.mo4tech.com/unshare-command-details-and-examples.html"target="_blank" rel="noopener">https://www.mo4tech.com/unshare-command-details-and-examples.html</a></li>
<li><a href="https://ericchiang.github.io/post/containers-from-scratch/"target="_blank" rel="noopener">https://ericchiang.github.io/post/containers-from-scratch/</a></li>
<li><a href="https://wiki.archlinux.org/title/cgroups"target="_blank" rel="noopener">https://wiki.archlinux.org/title/cgroups</a></li>
<li><a href="https://nodejs.org/en/docs/guides/nodejs-docker-webapp/"target="_blank" rel="noopener">https://nodejs.org/en/docs/guides/nodejs-docker-webapp/</a></li>
<li><a href="https://docs.docker.com/compose/gettingstarted/"target="_blank" rel="noopener">https://docs.docker.com/compose/gettingstarted/</a></li>
<li><a href="https://flast101.github.io/docker-privesc/"target="_blank" rel="noopener">https://flast101.github.io/docker-privesc/</a></li>
<li><a href="https://blog.alexellis.io/runc-in-30-seconds/"target="_blank" rel="noopener">https://blog.alexellis.io/runc-in-30-seconds/</a></li>
<li><a href="https://man7.org/linux/man-pages/man7/capabilities.7.html"target="_blank" rel="noopener">https://man7.org/linux/man-pages/man7/capabilities.7.html</a></li>
<li><a href="https://blog.container-solutions.com/linux-capabilities-in-practice"target="_blank" rel="noopener">https://blog.container-solutions.com/linux-capabilities-in-practice</a></li>
<li><a href="https://github.com/containers/bubblewrap"target="_blank" rel="noopener">https://github.com/containers/bubblewrap</a></li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>O penúltimo video sobre o setup do meu PC foi pesado em configuração mas se conseguiu chegar até o fim, deve ter entendido um pouco mais sobre como virtualização funciona, em particular usando o emulador QEMU acelerado com KVM. Aquilo é virtualização: criar uma máquina virtual que roda outro sistema operacional, outra kernel, por cima.</p>
<p>Hora de pagar a promessa que vim fazendo em diversos videos. Finalmente falar um pouco sobre os tais &ldquo;containers&rdquo; que até hoje muita gente confunde com virtualização. As duas coisas &ldquo;parece&rdquo; que é rodar um programa dentro de uma máquina virtual. Mas containers são completamente diferentes de máquinas virtuais. Vamos lá.</p>
<p>(&hellip;)</p>
<p>Se fosse fazer um TLDR, uma única frase que resume a diferença, é até simples: uma máquina virtual roda um novo kernel, possivelmente numa outra arquitetura, como x86 em cima de ARM, já um container reaproveita o mesmo kernel, necessariamente na mesma arquitetura de hardware. É essa a diferença. Agora vamos ver na prática.</p>
<p>Vou começar falando só de containers de Linux. BSD, Windows, MacOS tem infraestrutura diferente pra containers. Windows tem como rodar container pra rodar apps de Windows. Isso é leve. Mas quando Windows ou MacOS precisam rodar apps de Linux, na prática o que acontece é que eles de fato sobem uma máquina virtual de Linux primeiro, como o WSL que já fiz dois videos a respeito. Portanto envolve sim virtualização. E é dentro dessa máquina que um Docker Desktop cria containers de Linux. Só em Linux nativo que containers são realmente &ldquo;leves&rdquo;.</p>
<p>Em Linux precisamos entender dois componentes fundamentais: cgroups e namespaces. Cgroups já mencionei no episódio anterior quando mostrei como isolar threads da CPU só pra máquina virtual e só pro host. Cgroups ou control groups é um mecanismo da kernel pra agrupar processos e limitar recursos como cpu e memória.</p>
<p>Existem várias formas de listar cgroups. A mais simples acho que é só dar <code>ls /sys/fs/cgroup</code>. Lembra o script chamado &ldquo;suspend&rdquo; que fiz pra limitar AllowedCPUs pro host? Usei o systemd pra limitar esse parâmetro pros cgroups init.scope, system.slice e user.slice. Vejam na listagem que todos são diretórios. Se listar um deles, vem vários arquivos como &ldquo;cpu.weight&rdquo; ou &ldquo;memory.pressure&rdquo;. É como se parametriza os recursos pra esses cgroups.</p>
<p>É tudo organizado como uma árvore, pra variar olha só árvores de novo, onde um cgroup herda configurações de outro cgroup. Honestamente, não sei os detalhes de como é configurado cada um que já vem de cada distro e imagino que cada um configura essa árvore de formas um pouco diferente. Essa listagem é no meu Manjaro. Num Ubuntu é semelhante, tem a maioria dos mesmos slices.</p>
<p>Se listarmos o que tem em system.slice, tem novos subdiretórios, um pra cada daemon que o systemd inicia, e também cgroups pra cada um dos aplicativos instalados via Snap, que vou falar um pouco mais pra frente. Pra enxergamos isso melhor podemos rodar o comando <code>systemctl status</code>. Agora temos a hierarquia toda de cgroups.  Como falei antes, olha aqui o system.slice e os sub-grupos de cada daemon. E mais pra baixo temos o user.slice onde os programas que estamos rodando como navegador, terminal e tudo mais ficam em seus próprios sub-grupos também. Todos os processos rodando estão organizados em cgroups. Você nem sabia disso, mas toda vez que executa um programa, ele vai estar atrelado a pelo menos um cgroup.</p>
<p>Lembra que quando usamos comandos com <code>ps</code> ou programas como <code>htop</code> eles pesquisam os metadados no sistema de arquivos virtual <code>/proc</code>? Por exemplo, vamos pegar o PID do meu navegador Edge. Tem vários processos, todo navegador separa cada aba num processo diferente pra isolar, ter mais segurança, e se uma aba crashear, não derrubar todas as outras junto. Podemos fazer <code>cat /proc/PID/group</code> e aqui vamos em qual sub-grupo de cgroup esse process está. No escopo do edge, dentro de app.slice, dentro de user service, dentro de user.slice.</p>
<p>Existem várias formas de controlar cgroups. Uma delas é usando o sistema de arquivos virtual disponível no diretório <code>/sys/fs/cgroups</code>. Isso é um ponto de montagem. Se fizermos <code>mount | grep cgroup</code> olha como aparece que o tipo do sistema de arquivos é &ldquo;cgroup2&rdquo;. É como se fosse um dispositivo virtual montado. Lembra que já expliquei nos episódios de Linux, como nos de Slackware, Gentoo e no que explico como funciona o boot, que existem diversos pontos de montagem especiais como &ldquo;/proc&rdquo; ou &ldquo;/dev&rdquo;?</p>
<p>Pra mostrar na prática pra que diabos saporra serve, deixa criar um script idiota em Ruby. Podia ser em Python, podia ser em Java, faça no que quiser. É só um loop infinito printando ponto pra sempre. Vamos rodar e em outro terminal abro o &ldquo;htop&rdquo;. Veja aqui no topo que meu script tá usando 100% de 1 CPU. Se eu tivesse feito suportar múltiplas threads, cada 100% seria uma CPU. Um programa que usa 4 threads inteiras ia aparecer aqui como 400%.</p>
<p>No topo do programa lista todas as threads independentes e veja que tem uma de 100% que é meu script. Mas se esperar um pouquinho vai ver que ele muda de lugar. Um programa não fica sempre fixo no mesmo thread. Pra isso serve o scheduler da kernel. Expliquei sobre scheduler no episódio de concorrência e paralelismo e é a razão de porque no episódio anterior tive o trabalho de mostrar como mandei a máquina virtual usar as threads de 16 a 31 e limitei o host a usar só de 8 a 15. Lembra deste script aqui usando systemd e configurando o parâmetro AllowedCPUs em todos os diretórios raíz de slices?</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nv">command</span><span class="o">=</span><span class="nv">$2</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$command</span><span class="s2">&#34;</span> <span class="o">=</span> <span class="s2">&#34;started&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    systemctl set-property --runtime -- system.slice <span class="nv">AllowedCPUs</span><span class="o">=</span>0-15
</span></span><span class="line"><span class="cl">    systemctl set-property --runtime -- user.slice <span class="nv">AllowedCPUs</span><span class="o">=</span>0-15
</span></span><span class="line"><span class="cl">    systemctl set-property --runtime -- init.scope <span class="nv">AllowedCPUs</span><span class="o">=</span>0-15
</span></span><span class="line"><span class="cl"><span class="k">elif</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$command</span><span class="s2">&#34;</span> <span class="o">=</span> <span class="s2">&#34;release&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    systemctl set-property --runtime -- system.slice <span class="nv">AllowedCPUs</span><span class="o">=</span>0-31
</span></span><span class="line"><span class="cl">    systemctl set-property --runtime -- user.slice <span class="nv">AllowedCPUs</span><span class="o">=</span>0-31
</span></span><span class="line"><span class="cl">    systemctl set-property --runtime -- init.scope <span class="nv">AllowedCPUs</span><span class="o">=</span>0-31
</span></span><span class="line"><span class="cl"><span class="k">fi</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Ctrl-C pra parar o loop infinito e agora podemos criar sub-diretórios e arquivos usando comandos normais como &ldquo;mkdir&rdquo;, &ldquo;echo&rdquo; ou podemos continuar usar o SystemD pra facilitar, que é o que vou fazer agora. Vou criar um slice pra mim chamado &ldquo;akita.slice&rdquo; em &ldquo;/etc/systemd/system/akita.slice&rdquo;. Só abrir com qualquer editor de textos usando &ldquo;sudo&rdquo; e no caso quero limitar o uso de CPU em só 30% pra processos neste cgroup. Pra isso coloco este conteúdo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>[Slice]
CPUQuota=30%</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Posso configurar outros limites, como memória, mas pra esse exemplo, só isso já funciona. Agora vou abrir um shell dentro desse cgroup com este comando <code>systemd-run --slice=akita.slice --uid=akitaonrails --shell</code> e nessa linha de comando que abriu, vou repetir o mesmo comando <code>ruby loop.rb</code>.</p>
<p>Olha o htop. Tá vendo aqui no topo da lista? Antes o script consumia 100% de uma thread, mas como agora reiniciei a partir do meu novo cgroup,que só permiti usar 30% da CPU, a kernel obriga a usar no máximo 30% da CPU. E com isso conseguimos controlar granuladamente o que cada processo no sistema pode ou não consumir.</p>
<p>E por que eu iria querer limitar um programa forçado assim? No Wiki do site do ArchLinux tem um exemplo no fim. Ele menciona o Matlab, que é um programa de cálculo numérico e, dependendo da complexidade do cálculo, pode consumir toda a RAM e toda CPU do sistema. Se um programa usar todos os recursos, vai faltar CPU e RAM até pra kernel e o sistema inteiro vai ficar lento ou crashear. É um tipo de ataque de DoS. DoS ou Denial of Service é um programa saturar todos os recursos da máquina até tudo parar de funcionar. Mas se colocar o Matlab num cgroup onde limito CPU e RAM pra, digamos, deixar sobrar uns 10%, a kernel vai ter a possibilidade de tentar se corrigir.</p>
<p>Depois leia mais sobre cgroups no Wiki do Arch. Mas o importante foi pra explicar que existe esse recurso em todo Linux moderno. Agora vou pular pro segundo grande recurso pra limitar o que processos podem fazer, que se chama &ldquo;Namespaces&rdquo;. Quando listo o que tem em &ldquo;/proc&rdquo; consigo enxergar todos os PIDs de todos os processos rodando no sistema. Se rodar o comando &ldquo;mount&rdquo; consigo ver todos os pontos de montagem ativos no sistema. E existem comandos ou locais onde posso listar a stack de rede, IPC que é comunicação inter-processos, IDs de usuários e grupos.</p>
<p>Mas é possível esconder todos esses detalhes de um processo. Pra isso existe o comando <code>unshare</code>, literalmente &ldquo;descompartilhar&rdquo;. Num terminal normal, se executarmos o comando <code>ps aux</code> isso vai nos devolver uma lista com todos os processos ativos neste momento, e costuma ser uma lista grande num desktop normal. Vamos abrir um novo shell, mas fazendo <code>sudo unshare --fork --pid --mount-proc /bin/bash</code>. Parece nada demais né? Mas se executarmos <code>ps aux</code> olha só, cadê todos os meus processos? Sumiu? O bash agora tem PID 1.</p>
<p>É isso que se chama namespace, é um espaço separado de nomes, no caso de PIDs. Se abrir outro terminal e executar <code>ps aux</code> tá tudo aqui bonitinho e lá no final olha o comando unshare e a instância de bash rodando com um PID alto. A diferença é que o bash que acabamos de abrir não enxerga mais o mesmo &ldquo;/proc&rdquo; global. Ele tem um &ldquo;/proc&rdquo; local só pra ele, isolado e separado que começa do 1 de novo. De dentro daquele bash, vamos listar o que tem em &ldquo;/proc&rdquo;, só os dois PIDs que listou. No outro terminal normal se listar &ldquo;/proc&rdquo; tem todos os PIDs.</p>
<p>Entenderam? Namespace faz o processo puxar a informação de outro lugar. Estamos mentindo, mas ele continua executando no mesmo sistema, igual qualquer outro programa, tanto que tem um PID de verdade. Pra quem é de Javascript, uma analogia é a diferença de criar uma variável com o antigo &ldquo;var&rdquo; pra cair no escopo global da função ou usar &ldquo;let&rdquo; e manter a variável no escopo local do bloco, manja? Não é exatamente a mesma coisa, mas é parecido.</p>
<p>Com o recurso de namespaces podemos mentir pros processos pra não enxergar mais o resto do sistema do lado de fora e assim limitar o que podem fazer. Por padrão, todos os recursos do sistema operacional são globais, todo processo tem acesso a praticamente tudo que outro processo tem, salvo permissões de leitura e execução por usuário, que você tá mais acostumado a pensar, tipo, root tem acesso a tudo. Mas com <code>unshare</code> inclusive note que ele engana meu processo fazendo achar que é root. Se executarmos o comando <code>id</code> olha, sou o usuário zero.</p>
<p>Como executamos unshare com sudo, esse root tem o mesmo nível de privilégio do root de verdade, o que não é muito seguro. Vamos dar exit e executar a mesma linha de comando mas com a opção &ldquo;&ndash;user&rdquo; e pronto. Se executarmos &ldquo;id&rdquo; agora sou nobody, literalmente um zé ninguém. Mais útil vai ser rodar como root, só que não tendo acesso ao sistema de arquivos todo. Vamos consertar isso.</p>
<p>Se assistiu meus episódios de Linux, viu minha explicação sobre chroot. Um container de verdade não usa chroot, mas pra demonstração dos conceitos vai servir. Teve um palestrante que já demonstrou isso anos atrás e deixou alguns arquivos que vamos precisar. Vou deixar o link na descrição abaixo, vamos baixar o tarball &ldquo;rootfs.tar.gz&rdquo;.</p>
<p><code>wget https://github.com/ericchiang/containers-from-scratch/releases/download/v0.1.0/rootfs.tar.gz</code></p>
<p>Pronto, baixei lá no &ldquo;/tmp&rdquo; mesmo porque é só pra demonstração. Agora é só descomprimir com <code>tar xvfz</code>, esperar um segundo &hellip; e tá lá. Se listarmos o que tem, parece com a raíz do seu HD, não? É uma mini-distribuição do que chamamos de &ldquo;userland&rdquo;. Se listar o &ldquo;rootfs/boot&rdquo; não tem a kernel nem a initram que deveria. E o que fazemos com isso? É como na instalação do Gentoo, fazemos <code>sudo chroot rootfs /bin/bash</code>. Ele vai fazer &ldquo;change root&rdquo;, mudar a raíz, pra esse diretório rootfs e executar o bash dentro do sub-diretório &ldquo;rootfs/bin&rdquo; e não o meu de verdade.</p>
<p><code>ls</code> dá pau agora. Isso porque esse shell não carregou perfil nenhum, então não tem nenhuma variável de ambiente configurado. Vamos exportar o path de novo com <code>export PATH=/sbin:/bin:/usr/bin</code> e agora sim temos acesso às ferramentas nesses diretórios. É assim que sua distro Linux faz, só que isso já vem configurado automático pra você, mas na mão é assim que fazemos.</p>
<p>Essa variável provavelmente está no seu arquivo &ldquo;/etc/profile&rdquo; ou .bashrc ou .zshrc da vida. Executando <code>ls /</code> parece que não aconteceu nada, mas faz <code>ls /boot</code>, viu? Vazio. Estamos dentro do diretório &ldquo;rootfs&rdquo; que está sendo mascarado pra gente como se fosse a nova raíz. Esse shell não tem mais acesso ao meu sistema de arquivos de verdade.</p>
<p>Agora tentamos executar <code>ps</code> e dá pau. Isso porque não existe nenhum mount de diretórios especiais, como o &ldquo;/proc&rdquo; que é onde comandos como &ldquo;ps&rdquo; pesquisam processos. Veja o erro, ele manda justamente dar mount nisso. Mas agora é que vamos juntar esse comando de chroot com o unshare. Então damos exit e executamos o comando <code>sudo unshare -p -f --mount-proc=/tmp/rootfs/proc chroot rootfs /bin/bash</code></p>
<p>A opção &ldquo;-p&rdquo; é o jeito mais curto pra &ldquo;&ndash;pid&rdquo; e &ldquo;-f&rdquo; é pra &ldquo;&ndash;fork&rdquo;. Daí montamos o proc e damos chroot via unshare. Não podemos esquecer de exportar o PATH de novo. Pronto. Se fizermos <code>ls /boot</code> tá vazio. Se fizermos <code>ls /home</code>, tá vazio. Ou seja, esse não é seu sistema, ele está enxergando a raíz a partir do diretório &ldquo;/tmp/rootfs&rdquo;. Mas agora se rodarmos <code>ps aux</code>, sucesso! Só enxergamos os processos dentro do novo namespace. Não só escondemos os PIDs de todo o sistema como escondemos o sistema de arquivos e você pensa que está num sistema completamente diferente.</p>
<p>Esse é o ponto que confunde todo mundo. O comportamento dentro desse shell faz parecer que estou dentro de uma máquina virtual. Mas isso não é uma máquina virtual, não tem um hypervisor como KVM com um emulador como QEMU simulando uma máquina nova do zero e fazendo boot de um novo kernel. Por isso estou reforçando que o diretório falso &ldquo;/boot&rdquo; tá vazio, não tem outra kernel. Isso não é uma máquina virtual. E se voltarmos pra outro terminal e executarmos <code>ps aux</code> esse bash que abrimos dentro do chroot tá aqui, aparecendo normalmente como se fosse qualquer outro processo.</p>
<p>Se não ficou claro. Alt-tab pro shell dentro do chroot. Executando <code>ls /usr/bin</code> vamos abrir um programa que tem aqui. Olha só, veio um python naquele tarball, vamos executar. Esse é o REPL de python que vocês já devem ter visto. Alt-tab pro terminal normal e <code>ps aux</code> de novo. Olha só aparecendo aqui python. Se fosse uma máquina virtual, teria um  único processo, por exemplo de QEMU e só, eu não estaria vendo os processos dentro da máquina virtual misturado com os do meu sistema. E isso porque esses processos ESTÃO rodando no meu sistema normalmente, sem emulação, nem nada disso, é nativo.</p>
<p>Se não fosse assim, chamaríamos tudo de máquina virtual. Mas por isso inventamos esse termo novo chamado &ldquo;container&rdquo; justamente pra ser distinto. O processo está só sendo enganado, achando que está num outro sistema porque cortamos os olhos dele, as mãos dele, o nariz dele. E só apontamos tudo pra outros namespaces É isso que unshare e chroot fizeram com o shell do bash: isolaram num jarro, num container. Tudo que executa com unshare está na Matrix.</p>
<p>Nessa demonstração simples só escondemos os PIDs, o diretório &ldquo;/proc&rdquo; e o sistema de arquivos, mas dá pra mudar a rede, dá pra mudar todos os outros mounts como &ldquo;/dev&rdquo; ou &ldquo;/sys&rdquo;, dá pra fazer ele enxergar outros cgroups, outro clock do sistema, outros usuários e grupos, outro hostname e domainname, ou seja, o processo realmente vai achar que está em outra máquina, sem realmente estar. Ele pára de ter acesso ao resto do sistema e efetivamente roda &ldquo;isolado&rdquo;, que é todo o objetivo.</p>
<p>Namespaces são super flexíveis. Por exemplo, se isolar o container numa rede virtual, é assim que o processo num container consegue dar bind, digamos na porta 80, e um outro processo em outro container também consegue subir na porta 80, cada um numa rede isolada que daí configuro pra expor um pra fora como porta 4001 e outra como 4002. Mas também posso adicionar esses dois containers ambos no mesmo namespace de rede e assim crio uma rede virtual só entre os dois, sacaram? Pra isso serve o comando <code>nsenter</code> que é literalmente &ldquo;entrar num namespace&rdquo;, depois pesquisem.</p>
<p>Pesquisem no Google por &ldquo;container from scratch&rdquo; ou &ldquo;container do zero&rdquo; e vão esbarrar em artigos como do site da RedHat e diversos outros artigos explicando sobre <code>unshare</code>. Mas pra hoje esses exemplos que dei já servem pra continuar a explicação. As coisas começam a ficar confusas porque existem várias ferramentas que funcionam em cima dessa infraestrutura de cgroups e namespaces que expliquei. No começo, no mundo Linux surgiu Linux Containers ou LXC e depois Docker.</p>
<p>Em 2015 a empresa do Docker criou a Open Container Initiative ou OCI, um grupo com várias outras empresas pra padronizar e tornar mais portável todos os componentes que fazem containers funcionar, em particular o formato de imagens que é o formato OCI image, é o que você baixa do site dockerio.com. Os padrões estão todos na opencontainers.org e no mundo Linux a implementação desse padrão é a tecnologia &ldquo;runC&rdquo;. Vamos ver como funciona.</p>
<p>O runC precisa de um arquivo &ldquo;config.json&rdquo; que define as características do container. E vamos aproveitar aquele diretório &ldquo;rootfs&rdquo; ainda. O próprio comando runc gera esse arquivo executando <code>runc spec</code> dentro do diretório rootfs mesmo.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nt">&#34;ociVersion&#34;</span><span class="p">:</span> <span class="s2">&#34;1.0.2-dev&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">&#34;process&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;terminal&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;user&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;uid&#34;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;gid&#34;</span><span class="p">:</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;args&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/bin/bash&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;env&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;TERM=xterm&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;cwd&#34;</span><span class="p">:</span> <span class="s2">&#34;/&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;capabilities&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;bounding&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_AUDIT_WRITE&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_KILL&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_NET_BIND_SERVICE&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="p">],</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;effective&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_AUDIT_WRITE&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_KILL&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_NET_BIND_SERVICE&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="p">],</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;permitted&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_AUDIT_WRITE&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_KILL&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_NET_BIND_SERVICE&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="p">],</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;ambient&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_AUDIT_WRITE&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_KILL&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_NET_BIND_SERVICE&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;rlimits&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">   <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;RLIMIT_NOFILE&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;hard&#34;</span><span class="p">:</span> <span class="mi">1024</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;soft&#34;</span><span class="p">:</span> <span class="mi">1024</span>
</span></span><span class="line"><span class="cl">   <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;noNewPrivileges&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl"> <span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="nt">&#34;root&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;path&#34;</span><span class="p">:</span> <span class="s2">&#34;/tmp/rootfs&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;readonly&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl"> <span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="nt">&#34;hostname&#34;</span><span class="p">:</span> <span class="s2">&#34;runc&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">&#34;mounts&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;destination&#34;</span><span class="p">:</span> <span class="s2">&#34;/proc&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;proc&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;proc&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;destination&#34;</span><span class="p">:</span> <span class="s2">&#34;/dev&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;tmpfs&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;tmpfs&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;options&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;nosuid&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;strictatime&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;mode=755&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;size=65536k&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;destination&#34;</span><span class="p">:</span> <span class="s2">&#34;/dev/pts&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;devpts&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;devpts&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;options&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;nosuid&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;noexec&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;newinstance&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;ptmxmode=0666&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;mode=0620&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;gid=5&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;destination&#34;</span><span class="p">:</span> <span class="s2">&#34;/dev/shm&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;tmpfs&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;shm&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;options&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;nosuid&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;noexec&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;nodev&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;mode=1777&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;size=65536k&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;destination&#34;</span><span class="p">:</span> <span class="s2">&#34;/dev/mqueue&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;mqueue&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;mqueue&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;options&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;nosuid&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;noexec&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;nodev&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;destination&#34;</span><span class="p">:</span> <span class="s2">&#34;/sys&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;sysfs&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;sysfs&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;options&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;nosuid&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;noexec&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;nodev&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;ro&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;destination&#34;</span><span class="p">:</span> <span class="s2">&#34;/sys/fs/cgroup&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;cgroup&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;cgroup&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;options&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;nosuid&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;noexec&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;nodev&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;relatime&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;ro&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="p">],</span>
</span></span><span class="line"><span class="cl"> <span class="nt">&#34;linux&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;resources&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;devices&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">     <span class="nt">&#34;allow&#34;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">     <span class="nt">&#34;access&#34;</span><span class="p">:</span> <span class="s2">&#34;rwm&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">   <span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;namespaces&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">   <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;pid&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="p">},</span>
</span></span><span class="line"><span class="cl">   <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;network&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="p">},</span>
</span></span><span class="line"><span class="cl">   <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;ipc&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="p">},</span>
</span></span><span class="line"><span class="cl">   <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;uts&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="p">},</span>
</span></span><span class="line"><span class="cl">   <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;mount&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="p">},</span>
</span></span><span class="line"><span class="cl">   <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;cgroup&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;maskedPaths&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/acpi&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/asound&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/kcore&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/keys&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/latency_stats&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/timer_list&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/timer_stats&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/sched_debug&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/sys/firmware&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/scsi&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;readonlyPaths&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/bus&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/fs&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/irq&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/sys&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/sysrq-trigger&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">]</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora que vocês viram a demonstração com unshare, esse arquivo não deveria parecer muito alienígena. Inclusive foi por isso que fiz videos explicando Linux antes, porque precisa entender o sistema operacional pra entender como containers são configurados. Esse arquivo começa declarando a versão do padrão OCI, daí qual processo vamos rodar, que é o bash, e com qual usuário e grupo, no caso zero quer dizer root.  Olha como já configura o PATH pra mim. &ldquo;CWD&rdquo; é Current Working Directory que é o diretório onde o programa vai abrir.</p>
<p>Capabilities pula que vou explicar depois. Daí &ldquo;root&rdquo; quer dizer a raíz do sistema de arquivos, que vai ser dentro daquele /tmp/rootfs onde fizemos chroot. Hostname é o nome da máquina, que é definido pelo namespace UTS, assim o container pode ter um nome de máquina diferente do nome da máquina de verdade.</p>
<p>No atributo de mounts aparece a mesma coisa que fizemos com a opção &ldquo;&ndash;mount-proc&rdquo; do comando unshare, onde mandamos dar mount no sistema de arquivos especial de processos. Assim, dentro do container não vai mais enxergar os pids de verdade do lado de fora. Mas aqui tá mais completo porque vai dar mount pra &ldquo;/dev&rdquo;, pros pseudo terminais em &ldquo;/dev/pts&rdquo;, pra memória compartilhada que é &ldquo;/dev/shm&rdquo;, as filas de mensagem pra comunicação entre processos em &ldquo;/dev/mqueue&rdquo;, o &ldquo;/sys&rdquo; que espelha funcionalidades e informações da kernel e finalmente o mount de cgroup que mostrei no começo do video. Tudo isolado dos de verdade do lado de fora do container. Estamos deixando esse jarro o mais realista possível pro processo dentro.</p>
<p>E por fim quais namespaces vou isolar também que são os mesmos do unshare. Pid sabemos que esconde os processos, mas temos network pra rede virtual, ipc que é a fila pra comunicação interprocessos, uts que permite mudar o hostname interno e mount que permite criar mounts diferentes do seu sistema.</p>
<p>Com tudo configurado, podemos executar, a partir de rootfs, <code>sudo runc start hello</code>. Hello você pode substituir por um nome qualquer pra esse container, contanto que seja um nome único, daí podemos criar quantos containers quisermos. Como aconteceu antes quando rodamos unshare com chroot, caímos no bash dentro do jarro, com todos os seus sentidos sequestrados. RunC está mentindo pro bash, vamos ver?</p>
<p>Se digitarmos <code>id</code> estamos como root. Se executarmos <code>ls /boot</code>, tá vazio, estamos dentro do diretório rootfs como nova raíz de arquivos. Melhor ainda, se digitarmos <code>mount</code>, temos só os pontos de montagem que definimos no <code>config.json</code>. Ele não enxerga nenhum dos pontos de montagem de verdade do meu sistema. E se digitarmos <code>ps aux</code>, como antes, só enxergamos o bash que está rodando. É como se fosse uma máquina de verdade vazia. Estamos dentro da Matrix, mas repetindo, isso não é uma máquina virtual. Container, na prática é outro nome dado a um processo em execução com menos acessos que um processo normal, mas no fundo é só isso: a execução de um processo.</p>
<p>O que eu falei no começo do episódio? Vou repetir: Se fosse fazer um TLDR, uma única frase que resume a diferença, é até simples: uma máquina virtual roda um novo kernel em qualquer arquitetura, um container reaproveita o mesmo kernel, nas mesma arquitetura. É essa a diferença. Mas agora vamos ver na prática. Estou repetindo o tempo todo que &ldquo;/boot&rdquo; está vazio. Não tem outra kernel, estamos compartilhando a mesma kernel. Hypervisors compartilham o hardware de verdade, como CPU, GPU e dispositivos pra máquinas virtuais que rodam qualquer outro sistema operacional, qualquer nova kernel isolados um dos outros. Posso rodar Windows do lado de BSD. Mas um container compartilha o mesmo kernel que existe já carregado. Ele não pode rodar sistemas operacionais diferentes, só outra distro Linux.</p>
<p>Vamos voltar pro que eu disse no começo: como Windows e MacOS fazem pra ter containers de Docker de Linux? Eles sim, primeiro precisam usar o hypervisor deles, o Hyper-V ou o Hypervisor.framework pra bootar um Linux de verdade e em cima desse Linux rodar o RunC pra conseguir ter containers de Linux. Por isso Docker é sempre mais pesado em cima de Windows do que direto num Linux. Numa distro Linux não tem a necessidade de rodar um hypervisor antes. Entenderam essa diferença? É muito importante, porque sua performance e uso de recursos como RAM sempre vai ser pior no Docker pra Windows ou pra Mac. Sempre, não tem exceção.</p>
<p>Mas eu falei Docker e fiquei mostrando RunC, qualé? É porque depois que a OCI foi formada, o Docker, Podman, Kubernetes, todos que usam containers, provavelmente usam RunC por baixo, que é a infraestrutura que usa cgroups, unshare e outras coisas que vem em todo Linux. Mas se tem RunC então pra que precisamos de Docker?</p>
<p>A parte importante do Docker não é mais a capacidade de executar containers. Hoje em dia isso vem nativo na kernel do Linux através do RunC que mostrei. O grande lance de Docker é o ecossistema de imagens, o DockerHub. Como é o tutorial de hello world de Docker logo depois que instala? <code>docker run hello-world</code>, vamos executar no terminal. Ele imprime esse texto e termina. O que aconteceu?</p>
<p>A ferramenta começa checando no registry local se tem uma imagem com nome &ldquo;hello-world&rdquo;. Como não tinha, deu &ldquo;pull&rdquo;, como se fosse o comando <code>git clone</code> ou mais especificamente, <code>git pull</code>. Já explico porquê, mas na prática baixou o binário do site hub.docker.com. Podemos ir no site deles e ver o registro dessa imagem lá, tão vendo? Na dúvida ele baixa da tag &ldquo;latest&rdquo;. É o equivalente à branch &ldquo;main&rdquo; ou a antiga &ldquo;master&rdquo;.</p>
<p>Essa imagem baixada é que nem aquele nosso diretório &ldquo;/tmp/rootfs&rdquo; que eu tinha baixado o tarball, onde demos chroot. Com essa imagem o runC sobe um container. Pra ver o container que executou basta rodar o comando <code>docker container ls --all</code>. Sem essa opção a lista seria vazia porque o container executa e termina. Com essa opção &ldquo;all&rdquo; ele mostra mesmo os terminados. Lembra no comando &ldquo;runc&rdquo; que dei o nome de &ldquo;hello&rdquo; pro container? No comando de <code>docker run</code> poderia passar a opção &ldquo;&ndash;name&rdquo; mas como não fiz isso ele deu um nome aleatório pro container.</p>
<p>Agora uma coisa que a maioria que já usou Docker nunca deve ter feito. Vamos executar o comando <code>docker export NOME_CONTAINER &gt; rootfs.tar</code>. Sacaram? Podemos criar um diretório novo &ldquo;mkdir -p /tmp/rootfs2&rdquo; e fazer <code>tar xvf rootfs.tar --directory /tmp/rootfs2</code>. Agora damos &ldquo;cd&rdquo; pra dentro e não tem quase nada lá, alguns diretórios pra fazer mount de procs e outras coisas e esse executável &ldquo;hello&rdquo; que podemos executar diretamente com &ldquo;./hello&rdquo; e olha só, a mesma saída que vimos antes. É isso que o <code>docker run</code> executou e é assim que podemos pegar o conteúdo de uma imagem de Docker. Uma imagem de Docker não é muito diferente de um mero tarball.</p>
<p>Vamos entender melhor essa tal imagem. Pra começar, de volta no site do DockerHub vamos escolher a imagem do banco de dados Postgres. Agora vamos abrir do lado outro navegador com o arquivo Dockerfile no Github do repositório docker-library. Se clicarmos na tag &ldquo;latest&rdquo; podemos ver o que ele chama de layers. Sem ir linha a linha, só de bater o olho, conseguem ver onde um lado bate com o outro? Por exemplo, no Dockerfile tem esse comando &ldquo;ENV GOSU_VERSION&rdquo;. O comando ENV de Dockerfile serve pra configurar uma variável de ambiente, equivalente ao comando <code>export</code> num Bash. Todos esses &ldquo;/bin/sh&rdquo; são esses comandos RUN e assim por diante.</p>
<p>O que é um Dockerfile? É como se fosse um script de comandos que diz passo a passo como montar a tal imagem de Postgres. Voltando pra primeira linha, começa com o comando FROM. Não precisamos começar uma imagem do zero, podemos começar com uma que já existe e construir por cima. É como se fizéssemos <code>git clone</code> de um projeto e começássemos a adicionar coisas e ir fazendo <code>git commit</code>.</p>
<p>A grosso modo, cada comando do Dockerfile, esses em letra maiúscula no começo da linha, vão gerar um dos layers que vemos aqui no site do DockerHub. Com exceção de layers de configurar variável de ambiente, que é irrisório, a maioria dos comandos modifica ou baixa arquivos e por isso ocupa espaço. É que nem commit de Git. Por isso é comum num Dockerfile ter comandos RUN que executam vários comandos de shell concatenados, que nem logo no começo que fazemos <code>apt update; apt-get install ...; rm -Rf</code>. Por que?</p>
<p>Se o <code>rm -Rf</code> estivesse separado em outro comando RUN embaixo, o comando do <code>apt-get install</code> ia baixar e instalar pacotes, como em qualquer Debian ou Ubuntu que você usa. Digamos que instalou 50 megabytes, tô chutando. Mas vem um lixo opcional que não preciso, daí no comando seguinte rodo <code>rm</code> e apaga 10 megabytes. Mas a imagem vai ter um layer com 50 megabytes. Porque o <code>rm</code> ficou num layer separado.</p>
<p>Em vez disso eu baixo os 50 mega, na sequência já apago, e aí fecho a camada com 40 megabytes. Por isso fica tudo dentro do mesmo comando RUN, como se fosse num mesmo commit, entenderam? Quanto menos comando RUN no Dockerfile, melhor. Olha esse outro comando RUN embaixo, um monte de linhas um atrás do outro, e o resultado final de executar tudo é que vai no layer da imagem.</p>
<p>Essa é a importância da OCI que o Docker fundou. Ela define esse formato chamado imagem OCI. Dentro dessa imagem, que é um binariozão parecido com um zipão, tem alguns arquivos e diretórios obrigatórios. Como um <code>index.json</code> que tem o nome, ID, tamanho, lista de layers e histórico. Tem o <code>oci-config.json</code> que é o equivalente ao <code>config.json</code> do RunC que mostrei antes e, o mais importante, o diretório &ldquo;blobs&rdquo; que é parecido com diretório &ldquo;.git/objects&rdquo; de um repositório Git.</p>
<p>Ah, vocês não assistiram meu video de Git né? Lá eu expliquei que o importante é só o que tem no diretório &ldquo;.git&rdquo;, todo o resto pode jogar fora porque dá pra recriar tudo com o que tem aqui. Se listarmos <code>ls .git/objects</code> vem esse monte de subdiretórios. Vamos listar um deles e encontramos um arquivo com nome que é um SHA1. Esse é o SHA1 do commit de git. Os blobs da imagem de Docker é similar só que otimizados pra layers binários numa árvore, enquanto Git é otimizado pra árvore de commits de texto.</p>
<p>Lembram do formato QCOW2 que mostrei no episódio anterior? O QEMU Copy-on-Write versão 2 que é a imagem de HD virtual que o QEMU usa e onde instalamos o Windows virtualizado? É similar. O formato de imagem OCI também suporta copy-on-write. Que é o mesmo recurso que tem disponível em sistemas de arquivos modernos como o BTRFS. Um snapshot de BTRFS é similar com snapshot de QCOW2 que é parecido com layers de imagem OCI. COW permite criar layers só realmente com a diferença dos blocos que mudaram num arquivo.</p>
<p>Sabe diff que fazemos em Git e ele grava no commit só as linhas que foram adicionadas e as que foram removidas e não o arquivo todo? COW é como se fosse diff de arquivo binário. E lembra que eu venho repetindo em todos os videos da mini-série de armazenamento como arquivos são nada mais do que um conjunto de blocos binários organizados numa árvore? Se um arquivo tem 100 megabytes, divididos em 100 blocos de 1 megabyte, digamos que só 10 blocos desse arquivos foram alterados. Na próxima camada, ou commit, não precisa repetir os 100 megabytes inteiros do arquivo, só os 10 blocos modificados ocupando 10 megabytes. É por isso que imagens de Docker não são pesados.</p>
<p>Pra construir uma imagem nova só precisamos ter o arquivo Dockerfile como esse do Postgres e rodar o comando <code>docker build</code>. Se resolvermos mudar o Dockerfile a partir da metade, e repetirmos o comando <code>docker build</code>. Todas as camadas geradas pela primeira metade de comandos do arquivo permanecem as mesmas, jogamos fora as últimas camadas e ele executa os comandos a partir da metade. Por isso que quanto mais pro começo de um Dockerfile você mexer, mais o <code>docker build</code> vai ter que trabalhar. Se mexer só na última linha, só ela que vai ter que rodar.</p>
<p>Então uma imagem de OCI ou Docker é um conjunto de camadas binárias, como commits de um git. Pra montar dentro do container o Docker usa um sistema de arquivos chamado overlay2 que é um Union Filesystem. Union significa que unifica as diversas camadas num sistema de arquivos único. O container em si não sabe que tem essas camadas todas por baixo. Isso só importa pra construção da imagem, não pra rodar.</p>
<p>Eu menti agora pouco pra vocês. RunC é a infraestrutura de containers no Linux que sistemas como Kubernetes e Podman usam, mas o Docker propriamente dito na verdade usa outra coisa, que é o &ldquo;containerd&rdquo;. Pense assim, RunC tem menos funcionalidades, é mais simples, mais leve e pode rodar em userland, ou &ldquo;rootless&rdquo;, sem permissões de root ou daemons. Já containerd tem mais funcionalidades e foi feito pra funcionar com um daemon que sobe como root.</p>
<p>Por isso que quando se instala Docker, todo tutorial vai te falar pra executar &ldquo;systemctl start docker&rdquo; e habilitar o serviço pra subir no boot com &ldquo;systemctl enable docker&rdquo;. E isso pode se tornar um problema de segurança. Se sair pesquisando sobre buracos no Docker, vai achar várias formas de exploit dependendo de como o container foi configurado pra subir. O caso mais besta é subir um container com um comando como esse: <code>docker run -tid -v /:/mnt/ alpine</code>. Estão enxergando o erro?</p>
<p>O problema é essa opção &ldquo;-v&rdquo; que é pra mapear volumes. Lembra que quando estamos dentro da imagem o processo dentro não enxerga nada do sistema de arquivos fora? Pois bem, no mundo real o container é mais útil se ele tem &ldquo;algum&rdquo; acesso ao mundo externo. É melhor ser o Neo, saber que o mundo real existe, e ter superpoderes dentro da Matrix e um deles é conseguir enxergar arquivos do sistema de verdade.</p>
<p>Essa opção &ldquo;-v&rdquo; monta um diretório do lado de fora pra um diretório do lado de dentro, no caso do exemplo vai montar a raíz de verdade do meu HD pra um diretório &ldquo;/mnt&rdquo; no container. Mas isso é uma burrice, nunca faça um ponto de montagem tão amplo. Como o daemon que roda o container, o containerd que o Docker usa, sobe como root, ele tem permissão pra editar qualquer arquivo do sistema de verdade a partir desse ponto de montagem.</p>
<p>Daí é game-over, posso fazer o que quiser, inclusive editar o arquivo &ldquo;/etc/passwd&rdquo; pra injetar um novo usuário com permissão de root na máquina de verdade e invadir ela. Eu acho que ninguém em sã consciência jamais subiria um container de Docker num servidor de produção apontando pra raíz do HD do servidor. Só um sem noção faria isso. Seria certificado de estupidez.</p>
<p>É uma das razões de porque muitos de nós preferimos usar Podman em vez de Docker. Podman foi feito pra ter todos os mesmos comandos, mesmas opções e mesma sintaxe de Docker. Então comandos como <code>docker build</code> ou <code>docker run -it bla bla</code> posso rodar com <code>podman build</code> ou <code>podman run bla bla</code>. Se quiser posso fazer um alias no shell chamado docker que aponta pro podman e tudo vai funcionar praticamente igual. Podman usa o mesmo arquivo Dockerfile com a mesma sintaxe, a única diferença é aquele comando FROM no começo do arquivo.</p>
<p>Por exemplo, no caso do postgres começa com &ldquo;FROM debian&rdquo;. Pro Podman suportar precisa reescrever como &ldquo;FROM docker.io/debian&rdquo;. Só especificar o site da imagem porque ele suporta outros repositórios em vez de só o dockerhub. Outra diferença é que Docker é construído em cima do containerd, mas Podman usa o RunC como mostrei antes. Diferente do containerd não precisa ter um daemon rodando como root. Na verdade RunC, e por consequência o Podman, tem modo &ldquo;rootless&rdquo;. Ou seja, nem preciso iniciar com &ldquo;sudo&rdquo;. Portanto o container consegue rodar sem privilégio nenhum e por padrão é mais seguro. Veja documentação deles de como funciona esse modo porque não vou detalhar hoje.</p>
<p>E falando em segurança e permissões, pra concluir, faltou falar daquele bloco no arquivo do <code>config.json</code> que falei pra pular, lembram? Capacidade ou Linux Capabilities. Toda vez que se fala em permissões em Linux, automaticamente pensamos em coisas como os comandos &ldquo;chown&rdquo; pra mudar a propriedade de algum arquivo ou &ldquo;chmod&rdquo; pra mudar um arquivo pra ser só de leitura por exemplo. Ou, usar ou não usar &ldquo;sudo&rdquo; pra executar alguma coisa. E é isso, acabou.</p>
<p>Só que não. De cara vocês já aprenderam sobre cgroups e como eles limitam os recursos do processo, ou namespaces e como o sistema mente pros processos, independente se tem permissão de root ou não. E tem muito mais em cima. Linux capabilities é a habilidade da kernel de dar ou remover capacidades com o comando <code>setcap</code>.</p>
<p>Vou tentar não descer em detalhe desnecessário demais, mas num terminal o primeiro comando pra entender capabilities é o <code>capsh</code>. Execute num terminal e vemos dois campos no começo: Current e Bounding Set. Current vazio significa que um usuário normal começa sem nenhuma capacidade por default, mas tem várias capacidades que podem ser habilitadas, que é essa lista enorme chamada de Bounding Set.</p>
<p>Existem dois tipos de capacidades em Linux: capacidades efetivas e capacidades permitidas. Capacidades efetivas determinam quais operações um processo pode performar, e capacidades permitidas são quais podem ser habilitadas ou desabilitadas. Bounding Set é um sub-conjunto de capacidades permitidas. E quem controla esse set é o comando <code>prctl</code>, depois pesquise porque não quero entrar em detalhes sobre granularidade de segurança de Linux hoje.</p>
<p>Próximo comando vai ser <code>grep Cap /proc/$BASHPID/status</code>. Essa variável BASHPID é o pid do bash. Aí aparece essa lista bizarra, mas é fácil. O primeiro são inherited capabilities, ou capacidades herdadas no caso zero. Segundo são capacidades permitidas que eu falei, zero. Terceiro capacidades efetivas que também expliquei, zero. Finalmente capacidades bounding set.</p>
<p>Com o comando <code>capsh --decode=</code> esse hexadecimal e a lista é, obviamente, a mesma que vimos no comando <code>capsh --print</code> antes. Isso fica guardado no objeto proc do processo. E esse hexadecimal é um bit set. Um conjunto de bits. Se você não conhece a técnica de guardar propriedade num bit dentro de um inteiro e pegar os valores com uma máscara binária, um bit mask, pesquise. É essencial pra economizar bits em coisas como protocolos de rede.</p>
<p>De qualquer forma veja de novo aquele bloco de capabilities em <code>config.json</code>, agora você sabe o que é efetivo ou permitido. Esses cap_alguma coisa tem que ler no manual de capabilities. Só digitar <code>man capabilities</code> no terminal. Pronto, cap_audit_write é capacidade de escrever no log do sistema, cap_kill é capacidade de enviar sinais tipo SIGTERM ou SIGKILL e cap_net_bind_service é pra conseguir dar bind em portas privilegiadas abaixo de 1024, como a porta 80 sem precisar fazer sudo.</p>
<p>Com isso vocês tem pelo menos noção agora do que são containers do ponto de vista da kernel do Linux. Eles não são máquinas virtuais. É simplesmente um jarro que a kernel cria ao redor de um processo que roda nativamente pra restringir o que ele pode ver ou acessar, fazendo esse processo acreditar que está sozinho em outra máquina. A kernel usa cgroups e capabilities pra limitar os recursos e capacidades que esse processo tem acesso e usa namespaces pra limitar os sentidos desse processo, fazendo enxergar coisas como pids, mounts, rede, e tudo mais que são falsos, dentro da Matrix.</p>
<p>Docker é implementado em cima de containerd que é um serviço que precisa rodar como root. Podman e Kubernetes são implementados em cima de RunC que é mais leve e sem necessidade de um daemon auxiliar. Falando nisso, pode ser que Kubernetes ou Terraform confundam pra quem nunca mexeu. Qual a diferença disso pra Docker ou Docker Compose?</p>
<p>Vamos lá. Dockerfile vocês já entenderam, servem pra construir imagens OCI. Docker Compose é hiper útil, tem o Podman Compose que é exatamente a mesma coisa pra quem escolher usar Podman. Ambos começam com um arquivo <code>docker-compose.yml</code>. Deixa eu dar um exemplo besta aqui. Digamos que está trabalhando num projeto em Node. Comece criando um Dockerfile que constrói a imagem do seu projeto Node com seguinte conteúdo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-docker" data-lang="docker"><span class="line"><span class="cl"><span class="c"># Use the latest LTS (long-term support) version of Node.js as the base image</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">docker.io/node:lts</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Set the working directory to the project root</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">WORKDIR</span><span class="w"> </span><span class="s">/app</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Copy the package.json and package-lock.json files to the working directory</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> package*.json ./<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Install the dependencies</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> npm install<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Copy the rest of the project files to the working directory</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> . .<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Expose the app&#39;s port</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">EXPOSE</span><span class="w"> </span><span class="s">3000</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Run the app when the container is started</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">CMD</span> <span class="p">[</span> <span class="s2">&#34;npm&#34;</span><span class="p">,</span> <span class="s2">&#34;start&#34;</span> <span class="p">]</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Começamos a partir da imagem do Node versão lts em vez do mais novo que é latest, lembram? Projetos em produção precisam de estabilidade e não da versão mais nova que pode ser a mais bugada. Sempre use a mesma versão de tudo na sua máquina de desenvolvimento e nos servidores de produção e isso começa pela image de docker.</p>
<p>Dentro do container, o código fonte do seu projeto vai ficar nesse WORKDIR, em /app.  Daí copiamos o arquivo package.json pra dentro do container e rodamos <code>npm install</code> pra criar o <code>node_modules</code> e baixar todas as dependências. Terminamos de copiar todos o códigos fonte do projeto com esse comando COPY. Além disso esse container vai expôr pra fora a porta 3000 que é onde o servidor de node vai dar bind quando rodar o comando que está em CMD, no caso <code>npm start</code>.</p>
<p>Não esquecer de também criar um arquivo <code>.dockerignore</code> praquele comando COPY não copiar o <code>node_modules</code> que está no seu ambiente de desenvolvimento. Dentro do container já rodamos <code>npm install</code> então não precisa copiar tudo de novo. É importante entender onde ficam os arquivos em cada ambiente.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>node_modules
*.log</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Só com isso já dá pra fazer <code>docker build . -t seuusuario/seuprojeto</code> E já dá pra rodar com <code>docker run -p 3000:3000 seuusuario/seuprojeto</code>. Digamos que seu projeto precisa ter Redis e Postgres instalado. Isso costuma ser um problema, porque se você for um programador bem ocupado e experiente, na sua máquina deve ter diversos projetos. E alguns mais antigos podem depender de Postgres mais antigo, outros de Postgres mais novo. Mas se instalar na sua máquina via o gerenciador de pacotes da sua distro, sempre vai estar com a versão mais nova só.</p>
<p>Pra ter várias versões de node, muita gente usa o NVM ou o meu favorito que é o ASDF, mas e pra ter várias versões de postgres, de redis, de mysql, de kafka e todos esses outros componentes? E é pra isso que entra o Docker Compose. Este é um exemplo de arquivo <code>docker-compose.yml</code> que também ficaria na raíz do seu projeto:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;3&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">app</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">build</span><span class="p">:</span><span class="w"> </span><span class="l">.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;3000:3000&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DATABASE_HOST=db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DATABASE_USER=app_user</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DATABASE_PASSWORD=app_password</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">REDIS_HOST=redis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">redis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">db</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">docker.io/postgres:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">POSTGRES_USER=app_user</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">POSTGRES_PASSWORD=app_password</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">redis</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">docker.io/redis:latest</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Docker Compose orquestra e sobe vários containers Docker que ele chama de services. O primeiro é sua aplicação.  Ele começa com o atributo build sendo ponto, que é o diretório atual. Vai subir a imagem que seria construída com o Dockerfile que tá no seu projeto nesse diretório. Daí configuramos o projeto com variáveis de ambiente, por exemplo dizemos que DATABASE_HOST se chama &ldquo;db&rdquo;. Normalmente você colocaria &ldquo;localhost&rdquo;, mas cada serviço é como se estivesse rodando numa máquina separada graças ao Namespace UTS que permite que cada um tenha seu hostname separado, por isso o service de Postgres vai se chamar &ldquo;db&rdquo;.</p>
<p>Logo embaixo começa o bloco &ldquo;db&rdquo; onde dizemos que é pra baixar a imagem <code>docker.io/postgres:latest</code> e configuramos o usuário e senha, que é o mesmo que o serviço de node da sua aplicação vai usar. Mesma coisa no terceiro service que chamamos de &ldquo;redis&rdquo; e baixa imagem docker.io/redis. Latest é a versão mais nova, mas num projeto de verdade é bom fixar uma versão em específico pra não confundir.</p>
<p>Veja também que o serviço app, que é sua aplicação, tem um bloco dizendo &ldquo;depends_on&rdquo;, ou seja, o docker compose só vai subir sua aplicação depois que os serviços de postgres e redis carregarem. E pronto. Mesmo que você seja um programador que acabou de entrar no projeto, depois de fazer <code>git clone</code> do repositório, é só rodar <code>docker compose up</code> e pronto! Vai subir todos os serviços e ao final pode ir no navegador a apontar pra localhost:3000 e que vai estar tudo de pé e funcionando. O ideal é manter essa configuração atualizada, porque o que mais acontece é ficar ajustando só na sua máquina e não subir a atualização pro repositório, daí pessoas novas entram na equipe e baixam uma configuração desatualizada e dá vários problemas. Mantenha tudo atualizado.</p>
<p>De bônus, lembra do video de otimização web onde explico sobre coisas como NGINX? Podemos simular o que eu falei naquele video com docker compose. Não é uma coisa que faríamos num projeto de verdade mas só pra ilustrar como aquele diagrama conceitual viraria código de Docker Compose:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;3&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">app_1</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">build</span><span class="p">:</span><span class="w"> </span><span class="l">.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;3000&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DATABASE_HOST=db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DATABASE_USER=app_user</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DATABASE_PASSWORD=app_password</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">REDIS_HOST=redis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">redis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">app_2</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">build</span><span class="p">:</span><span class="w"> </span><span class="l">.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;3000&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DATABASE_HOST=db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DATABASE_USER=app_user</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DATABASE_PASSWORD=app_password</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">REDIS_HOST=redis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">redis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">db</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">docker.io/postgres:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">POSTGRES_USER=app_user</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">POSTGRES_PASSWORD=app_password</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">redis</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">docker.io/redis:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">load_balancer</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">docker.io/nginx:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8080:80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./nginx.conf:/etc/nginx/nginx.conf</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">app_1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">app_2</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Os serviços app, redis e postgres são iguaizinhos o do exemplo anterior mas agora adicionei um &ldquo;load balancer&rdquo; que puxa a imagem do nginx. Lembram que expliquei naquele video que ele é quem realmente responde na porta 80? E pro nginx funcionar precisa ter um arquivo <code>nginx.conf</code> no seu projeto que instruímos o docker compose copiar pra dentro do container de nginx. Esse arquivo diz o seguinte:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>user                 nginx;
worker_processes     auto;

events {
    worker_connections  65535;
    multi_accept        on;
}

http {
    charset              utf-8;
    sendfile             on;
    tcp_nopush           on;
    tcp_nodelay          on;
    client_max_body_size 4m;

    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    map $remote_addr $proxy_forwarded_elem {

        # IPv4 addresses can be sent as-is
        ~^[0-9.]&#43;$        &#34;for=$remote_addr&#34;;

        # IPv6 addresses need to be bracketed and quoted
        ~^[0-9A-Fa-f:.]&#43;$ &#34;for=\&#34;[$remote_addr]\&#34;&#34;;

        # Unix domain socket names cannot be represented in RFC 7239 syntax
        default           &#34;for=unknown&#34;;
    }

    map $http_forwarded $proxy_add_forwarded {

        # If the incoming Forwarded header is syntactically valid, append to it
        &#34;~^(,[ \\t]*)*([!#$%&amp;&#39;*&#43;.^_`|~0-9A-Za-z-]&#43;=([!#$%&amp;&#39;*&#43;.^_`|~0-9A-Za-z-]&#43;|\&#34;([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\&#34;))?(;([!#$%&amp;&#39;*&#43;.^_`|~0-9A-Za-z-]&#43;=([!#$%&amp;&#39;*&#43;.^_`|~0-9A-Za-z-]&#43;|\&#34;([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\&#34;))?)*([ \\t]*,([ \\t]*([!#$%&amp;&#39;*&#43;.^_`|~0-9A-Za-z-]&#43;=([!#$%&amp;&#39;*&#43;.^_`|~0-9A-Za-z-]&#43;|\&#34;([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\&#34;))?(;([!#$%&amp;&#39;*&#43;.^_`|~0-9A-Za-z-]&#43;=([!#$%&amp;&#39;*&#43;.^_`|~0-9A-Za-z-]&#43;|\&#34;([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\&#34;))?)*)?)*$&#34; &#34;$http_forwarded, $proxy_forwarded_elem&#34;;

        # Otherwise, replace it
        default &#34;$proxy_forwarded_elem&#34;;
    }

    upstream www {
        least_conn;
        server app_1:3000;
        server app_2:3000;
    }
    
    server {
    
        listen 80;
    
        location / {
            proxy_pass http://www;

            proxy_http_version                 1.1;
            proxy_cache_bypass                 $http_upgrade;
            
            # Proxy headers
            proxy_set_header Upgrade           $http_upgrade;
            #proxy_set_header Connection        $connection_upgrade;
            proxy_set_header Host              $host;
            proxy_set_header X-Real-IP         $remote_addr;
            proxy_set_header Forwarded         $proxy_add_forwarded;
            proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Forwarded-Host  $host;
            proxy_set_header X-Forwarded-Port  $server_port;
        }
    
    }
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O bloco server, tem a configuração do servidor que vai escutar na porta 80 e fazer o tal proxy reverso pra <a href="http://app"target="_blank" rel="noopener">http://app</a> e app está configurado acima, são os hostnames app_1 e app_2 na porta 3000 de cada container. Lembrem que containers tem rede virtual separada, namespace de portas separado. Então cada um, quando carregar, se penduram na porta 3000 e o nginx vai fazer round robin entre esses dois servidores.</p>
<p>app_1 e app_2 herdam do serviço app que é sua aplicação em Node. Aqui estamos subindo duas instâncias pra ter balanceamento de carga entre os dois. Note que o serviço load_balancer depende desse app_1 e app_2 estarem de pé, e esses dois, por herdarem de app, dependem de redis e postgres estarem de pé e é assim que o docker compose sabe quem tem que subir primeiro pra tudo estar funcionando. E pronto! Isso é uma pequena demonstração de uma aplicação node com dois processos sendo balanceados por um nginx.</p>
<p>Docker Compose foi feito pra infraestrutura bem simples, em particular pra rodar na sua máquina de desenvolvimento e facilitar a vida de programadores. Ele até serve pra fazer deploy em servidores pequenos, tipo numa Digital Ocean da vida. Antigamente tinha um projeto chamado Docker Machine que servia pra conversar com provedores de servidores, mas acho que ninguém usa mais.</p>
<p>Veja como nesse arquivo, pra subir duas instâncias da aplicação, precisou manualmente digitar app_1, app_2, colocar eles no depends_on do load_balancer. Imagina se precisasse subir 10 instâncias. Ou se precisasse configurar replica de Postgres. Ou se precisasse configurar uma fila e vários servidores separados de jobs assíncronos como falei no video do ChatGPT. Esse arquivo não escala, ia ficar complicado ainda mais porque também não sabe só aumentar ou diminuir só os containers de web. Ele não tem noção do que está rodando nos servidores já e só dar manutenção.</p>
<p>É pra isso que entra Kubernetes e Terraform. Kubernetes é um conjunto de tecnologias de infraestrutura elástica. A parte que se aproxima de Docker é o que ele chama de pods, que são containers que rodam com RunC. Mas o Kubernetes faz bem mais, configura VPC que são redes privadas virtuais. Eu posso criar uma rede isolada só pros containers web, outra rede isolada separada só pra servidores de jobs, por exemplo. Sabe lidar com balanceador de carga, o Ingress, adicionar e remover pods dinamicamente, e sabe que está ou na AWS ou no Google Cloud ou na Azure e com isso consigo adicionar serviços exclusivos como o RDS da AWS como banco de dados ou o Cloud SQL do Google Cloud. É bem mais flexível e, por isso, mais complicado.</p>
<p>Entenda, um IaaS ou Infraestrutura como Serviço, como a AWS, me fornece os componentes básicos de infraestrutura. O Kubernetes é melhor pra lidar mais com os pods, os containers, depois que já tem a infra de máquinas e rede pronta. Pra subir essa infra inicial, podemos ou fazer na mão via interface web, o que não recomendamos, ou orquestrar com Terraform.</p>
<p>Por exemplo, máquinas virtuais que o AWS chama de EC2. Esses servidores tem tamanhos diferentes, por exemplo t2.nano que só tem 1 vcpu ou t2.xlarge de 4 vcpus ou a1.metal com 16 cpus, ou m4.16xlarge de 64 vcpus, cada um com um preço diferente por hora. Tem planos diferentes pra S3, pra RDS, pra SQS, e assim vai. E isso só na AWS. Pra organizar tudo, usamos Terraform. Deixa mostrar só alguns trechos de um exemplo de arquivo de configuração, que pode ficar super comprido.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># Set up the provider for the desired cloud provider
provider &#34;aws&#34; {
  region = &#34;us-east-1&#34;
}

# Create a VPC to host the resources
resource &#34;aws_vpc&#34; &#34;app_vpc&#34; {
  cidr_block = &#34;10.0.0.0/16&#34;
}

# Create a public subnet in the VPC
resource &#34;aws_subnet&#34; &#34;public_subnet&#34; {
  vpc_id            = aws_vpc.app_vpc.id
  cidr_block        = &#34;10.0.1.0/24&#34;
  availability_zone = &#34;us-east-1a&#34;
}

# Create an Internet Gateway and attach it to the VPC
resource &#34;aws_internet_gateway&#34; &#34;app_igw&#34; {
  vpc_id = aws_vpc.app_vpc.id
}

# Create a route table and a route for the public subnet
resource &#34;aws_route_table&#34; &#34;public_route_table&#34; {
  vpc_id = aws_vpc.app_vpc.id
  route {
    cidr_block = &#34;0.0.0.0/0&#34;
    gateway_id = aws_internet_gateway.app_igw.id
  }
}

# Associate the public subnet with the route table
resource &#34;aws_route_table_association&#34; &#34;public_subnet_association&#34; {
  subnet_id      = aws_subnet.public_subnet.id
  route_table_id = aws_route_table.public_route_table.id
}

# Create an EC2 instance to host the Node.js application
resource &#34;aws_instance&#34; &#34;app_server&#34; {
  ami           = &#34;ami-12345678&#34;
  instance_type = &#34;t2.micro&#34;
  vpc_id        = aws_vpc.app_vpc.id
  subnet_id     = aws_subnet.public_subnet.id
  key_name      = &#34;app-key&#34;
  user_data     = &#34;${file(&#34;user_data.sh&#34;)}&#34;
}

# Create a security group for the EC2 instance
resource &#34;aws_security_group&#34; &#34;app_server_sg&#34; {
  name        = &#34;app_server_sg&#34;
  description = &#34;Security group for the application server&#34;
  vpc_id      = aws_vpc.app_vpc.id
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = &#34;tcp&#34;
    cidr_blocks = [&#34;0.0.0.0/0&#34;]
  }
}

# Create an RDS instance for Postgres
resource &#34;aws_db_instance&#34; &#34;app_db&#34; {
  engine               = &#34;postgres&#34;
  engine_version       = &#34;11.5&#34;
  instance_class       = &#34;db.t2.micro&#34;
  name                 = &#34;app_db&#34;
  username             = &#34;app_user&#34;
  password             = &#34;app_password&#34;
  vpc_security_group_ids = [aws_security_group.app_db_sg.id]
  vpc_id               = aws_vpc.app_vpc.id
  subnet_group_name    = &#34;app_db_subnet_group&#34;
}

# Create a security group for the RDS instance
resource &#34;aws_security_group&#34; &#34;app_db_sg&#34; {
  name        = &#34;app_db_sg&#34;
  description = &#34;Security group for the application database&#34;
  vpc_id      = aws_vpc.app_vpc.id
  ingress {
    from_port   = 5432
    to_port     = 5432
    protocol    = &#34;tcp&#34;
    cidr_blocks = [&#34;10.0.1.0/24&#34;]
  }
}

# Create a security group for Redis
resource &#34;aws_security_group&#34; &#34;app_redis_sg&#34; {
  name        = &#34;app_redis_sg&#34;
  description = &#34;Security group for Redis&#34;
  vpc_id      = aws_vpc.app_vpc.id
  ingress {
    from_port   = 6379
    to_port     = 6379
    protocol    = &#34;tcp&#34;
    cidr_blocks = [&#34;10.0.1.0/24&#34;]
  }
}

# Create an Elasticache cluster for Redis
resource &#34;aws_elasticache_cluster&#34; &#34;app_redis&#34; {
  engine            = &#34;redis&#34;
  cluster_id        = &#34;app_redis&#34;
  node_type         = &#34;cache.t2.micro&#34;
  num_cache_nodes   = 1
  security_group_ids = [aws_security_group.app_redis_sg.id]
  vpc_id            = aws_vpc.app_vpc.id
}

# Create an Nginx load balancer
resource &#34;aws_lb&#34; &#34;app_lb&#34; {
  name            = &#34;app_lb&#34;
  internal        = false
  security_groups = [aws_security_group.app_lb_sg.id]
  subnets         = [aws_subnet.public_subnet.id]
  load_balancer_type         = &#34;application&#34;
  idle_timeout               = 60
  enable_deletion_protection = false
}

# Create a security group for the Nginx load balancer
resource &#34;aws_security_group&#34; &#34;app_lb_sg&#34; {
  name        = &#34;app_lb_sg&#34;
  description = &#34;Security group for the application load balancer&#34;
  vpc_id      = aws_vpc.app_vpc.id
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = &#34;tcp&#34;
    cidr_blocks = [&#34;0.0.0.0/0&#34;]
  }
}

# Create a target group for the Nginx load balancer
resource &#34;aws_lb_target_group&#34; &#34;app_tg&#34; {
  name     = &#34;app_tg&#34;
  port     = 80
  protocol = &#34;HTTP&#34;
  vpc_id   = aws_vpc.app_vpc.id
  target_type = &#34;instance&#34;
}

# Register the EC2 instance with the target group
resource &#34;aws_lb_target_group_attachment&#34; &#34;app_tg_attachment&#34; {
  target_group_arn = aws_lb_target_group.app_tg.arn
  target_id        = aws_instance.app_server.id
  port             = 80
}

# Create a listener for the Nginx load balancer
resource &#34;aws_lb_listener&#34; &#34;app_listener&#34; {
  load_balancer_arn = aws_lb.app_lb.arn
  port              = &#34;80&#34;
  protocol          = &#34;HTTP&#34;
  default_action {
    type             = &#34;forward&#34;
    target_group_arn = aws_lb_target_group.app_tg.arn
  }
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Veja, o Terraform sabe que o provedor é AWS, e em qual região geográfica, no caso leste dos Estados Unidos, tipo Nova Iorque. Veja configuração de rede privada, subnets, gateway, tabela de rotas, e mais.</p>
<p>Daí contrato um servidor EC2, tamanho t2.micro, e configuro pra rede que tinha declarado antes. Embaixo configuramos grupo de segurança. Depois tenho configuração de banco de dados, depois de Redis que é o serviço que a AWS chama de Elasticache, depois tem o load balancer. E esse arquivo de exemplo é bem simplório e incompleto. Entre Kubernetes e Terraform, tem bastante detalhe pra realmente deployar uma aplicação em produção que esteja configurado de forma eficiente e com segurança.</p>
<p>Numa configuração de verdade, se tudo estiver direitinho declarado, seria só digitar <code>terraform init</code>  pra fazer download e instalar os plugins necessários. Daí <code>terraform plan</code> pra criar o plano do deployment pra esse provedor e a gente checar se tá tudo realmente certo. Coisa que já tá instalada ele não vai instalar de novo, vai só reconfigurar e instalar o que falta.  E no final fazer <code>terraform apply</code> pra realmente contratar os serviços, configurar e colocar nos lugares certos na infra.</p>
<p>Video de hoje não é sobre devops mas já que estou falando de containers e serviços, achei legal só mostrar um exemplo pra quem nunca viu. Mas o ponto é: compare uma configuração longa de Terraform com um simples Docker Compose. Nem chega perto, porque o objetivo das duas ferramentas é diferente. Um é pra orquestrar uma infra de verdade, a outra é pra rodar só na sua máquina pra facilitar desenvolvimento. Pra configurar uma AWS use Kubernetes e Terraform. Pra configurar serviços na sua máquina de desenvolvimento, use Docker Compose. Todo bom projeto deveria ter isso.</p>
<p>Pra finalizar, não vou mostrar mas queria só explicar outras tecnologias de containers que existem. Expliquei sobre imagens OCI de Docker, mas tem outro projeto que recomendo que pesquisem chamado OSTree. Lembra que falei que uma imagem meio que parece montado com commits binários de um Git? Existe de fato um projeto que é um Git de sistema de arquivos, o OSTree. A idéia é a seguinte: imagina que você tem 100 servidores numa AWS da vida, coisa grande. Imagina se sai atualização do Ubuntu Server que roda lá, o que você vai fazer? Mandar rodar <code>apt update; apt upgrade -y</code> e rezar pra não dar pau?</p>
<p>Na verdade não, vai montar imagens com ostree. O sistema de arquivos de todo servidor vai ser como um Git gigante. Daí sai atualização, você atualiza só em uma imagem. O OSTree nos servidores vai baixar a nova imagem e trocar a imagem antiga pela imagem nova. É tipo dar um git pull e trocar pro branch mais novo com um git checkout manja? É uma explicação grosseira, mas é bom saber que isso existe.</p>
<p>Outra coisa é sobre capacidades. Além de Linux capabilities que expliquei antes existem outra formas de limitar acesso a recursos da máquina, especialmente no contexto de segurança. Sabe em aplicativo Android ou iOS hoje em dia que pergunta se você quer dar acesso da camera pra um app ou acesso à sua localização e coisas assim? No mundo Linux isso é responsabilidade de componentes como Seccomp de Secure Computing Mode, que é outra funcionalidade da kernel do Linux pra restringir chamadas de sistema. Ele normalmente é usado em combinação com outros mecanismos de segurança, mas o seccomp em si oferece a infra na kernel pra aplicar filtros nos programas. Se alguém tentar fazer alguma coisa que não deveria, a kernel recusa.</p>
<p>Em cima disso temos projetos como AppArmor e SE-Linux. Olhando só por cima, eles se complementam. Ambos servem pra forçar políticas de segurança nos programas. O AppArmor permite especificar que recursos, como arquivos, dispositivos e sockets de rede um programa tem permissão pra usar. SE-Linux significa Security Enhanced Linux que também força políticas, por exemplo, pra acesso a hardware externo. É ele quem pode proibir um aplicativo de usar sua webcam.</p>
<p>Além disso, do jeito que falei, parece que todo container vai usar Namespaces de Linux com o comando unshare, mas existem outras formas de fazer containers, como o projeto Bubblewrap que é um container mais simples e usa outro recurso da kernel do Linux que são namespaces unprivileged e seccomp-bpf.</p>
<p>Mencionei ostree, seccomp, apparmor, se-linux e bubblewrap porque se você juntar todos eles num único projeto, começa a surgir com coisas como Flatpak. Sabe, aquele instalador de aplicativos? Tecnicamente é como se fosse um Docker ou Podman, mas feito pra funcionar melhor com aplicativos, especialmente aplicativos gráficos, com janela e que precisam acessar seus arquivos, USB e se comunicar com outros programas, precisando ter acesso a serviços como dbus. Tem gente que fica pensando em usar Docker pra instalar um Gimp ou LibreOffice. Não né, pra isso serve Flatpak.</p>
<p>Flatpak roda aplicativos como Spotify, Discord, Zoom, dentro de containers semi-isolados. Menos isolados que um container de Docker, mas um pouco mais seguros do que se fosse instalado direto via apt ou pacman. E o Snap de Ubuntu é similar, um gerenciador de containers. Em vez dos pacotes serem tipo zips como expliquei no video de Slackware, Flatpak e Snap baixam imagens, como imagens de Docker, só que em vez de imagem OCI é OSTree.</p>
<p>Entenda que Flatpak e Snap são primos de Docker e Kubernetes. As tecnologias por baixo também dependem da kernel do Linux e sempre caímos em cgroups, namespaces, capabilities e algum formato de HD virtual dentro de um linguição de bytes binários que chamamos de imagens. Espero que tenha dado uma luz no entendimento de containers e espero que ninguém mais tenha a pachorra de confundir containers com máquinas virtuais. Se ficaram com dúvidas mandem nos comentários abaixo, se curtiram o video deixem um joinha, assinem o canal e compartilhem o video com seus amigos. A gente se vê, até mais!</p>
]]></content:encoded><category>containers</category><category>linux</category><category>devops</category><category>akitando</category></item><item><title>[Akitando] #113 - A Forma Ideal de Projetos Web | Os 12 Fatores</title><link>https://akitaonrails.github.io/2022/01/29/akitando-113-a-forma-ideal-de-projetos-web-os-12-fatores/</link><guid isPermaLink="true">https://akitaonrails.github.io/2022/01/29/akitando-113-a-forma-ideal-de-projetos-web-os-12-fatores/</guid><pubDate>Sat, 29 Jan 2022 16:59:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/gpJgtED36U4"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;h2&gt;DESCRIPTION&lt;span class="hx:absolute hx:-mt-20" id="description"&gt;&lt;/span&gt;
&lt;a href="#description" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Finalmente vou mostrar o que é o Heroku e como é o fluxo de trabalho mínimo de um projeto web ideal. Se você já usa Heroku, aproveite pra compartilhar o video com conhecidos que ainda não usam. Se nunca viu Heroku, prepare-se pra ficar surpreso!&lt;/p&gt;
&lt;h2&gt;Conteúdo&lt;span class="hx:absolute hx:-mt-20" id="conteúdo"&gt;&lt;/span&gt;
&lt;a href="#conte%c3%bado" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;00:00 - Intro&lt;/li&gt;
&lt;li&gt;01:16 - Recapitulando Heroku&lt;/li&gt;
&lt;li&gt;05:04 - Os 12 Fatores&lt;/li&gt;
&lt;li&gt;06:02 - I - Codebase&lt;/li&gt;
&lt;li&gt;07:11 - II - Dependencies&lt;/li&gt;
&lt;li&gt;08:24 - III - Config&lt;/li&gt;
&lt;li&gt;09:30 - IV - Backing Services&lt;/li&gt;
&lt;li&gt;10:47 - V - Build, release, run&lt;/li&gt;
&lt;li&gt;12:25 - VI - Processes&lt;/li&gt;
&lt;li&gt;14:50 - VII - Port Binding&lt;/li&gt;
&lt;li&gt;16:09 - VIII - Concurrency&lt;/li&gt;
&lt;li&gt;17:40 - IX - Disposability&lt;/li&gt;
&lt;li&gt;19:07 - X - Dev/Prod parity&lt;/li&gt;
&lt;li&gt;21:59 - XI - Logs&lt;/li&gt;
&lt;li&gt;22:31 - RANT: log4j&lt;/li&gt;
&lt;li&gt;23:39 - XII - Admin Processes&lt;/li&gt;
&lt;li&gt;24:33 - Heroku - alternativas e custo-benefício&lt;/li&gt;
&lt;li&gt;28:37 - Escalabilidade não é automática&lt;/li&gt;
&lt;li&gt;30:33 - 1. Farás Testes!&lt;/li&gt;
&lt;li&gt;31:05 - 2. Integrarás Continuamente!&lt;/li&gt;
&lt;li&gt;31:59 - 3. Usarás um Sistema de CI!&lt;/li&gt;
&lt;li&gt;32:44 - 4. Farás Deploy Contínuo!&lt;/li&gt;
&lt;li&gt;33:16 - Conclusão: equipes saudáveis&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Links&lt;span class="hx:absolute hx:-mt-20" id="links"&gt;&lt;/span&gt;
&lt;a href="#links" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;The Twelve-Factor App (&lt;a href="https://12factor.net/"target="_blank" rel="noopener"&gt;https://12factor.net/&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Guidance for preventing, detecting, and hunting for exploitation of the Log4j 2 vulnerability (&lt;a href="https://www.microsoft.com/security/blog/2021/12/11/guidance-for-preventing-detecting-and-hunting-for-cve-2021-44228-log4j-2-exploitation/"target="_blank" rel="noopener"&gt;https://www.microsoft.com/security/blog/2021/12/11/guidance-for-preventing-detecting-and-hunting-for-cve-2021-44228-log4j-2-exploitation/&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;SCRIPT&lt;span class="hx:absolute hx:-mt-20" id="script"&gt;&lt;/span&gt;
&lt;a href="#script" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Olá pessoal, Fabio Akita.&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/gpJgtED36U4"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<h2>DESCRIPTION<span class="hx:absolute hx:-mt-20" id="description"></span>
    <a href="#description" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Finalmente vou mostrar o que é o Heroku e como é o fluxo de trabalho mínimo de um projeto web ideal. Se você já usa Heroku, aproveite pra compartilhar o video com conhecidos que ainda não usam. Se nunca viu Heroku, prepare-se pra ficar surpreso!</p>
<h2>Conteúdo<span class="hx:absolute hx:-mt-20" id="conteúdo"></span>
    <a href="#conte%c3%bado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>00:00 - Intro</li>
<li>01:16 - Recapitulando Heroku</li>
<li>05:04 - Os 12 Fatores</li>
<li>06:02 - I - Codebase</li>
<li>07:11 - II - Dependencies</li>
<li>08:24 - III - Config</li>
<li>09:30 - IV - Backing Services</li>
<li>10:47 - V - Build, release, run</li>
<li>12:25 - VI - Processes</li>
<li>14:50 - VII - Port Binding</li>
<li>16:09 - VIII - Concurrency</li>
<li>17:40 - IX - Disposability</li>
<li>19:07 - X - Dev/Prod parity</li>
<li>21:59 - XI - Logs</li>
<li>22:31 - RANT: log4j</li>
<li>23:39 - XII - Admin Processes</li>
<li>24:33 - Heroku - alternativas e custo-benefício</li>
<li>28:37 - Escalabilidade não é automática</li>
<li>30:33 - 1. Farás Testes!</li>
<li>31:05 - 2. Integrarás Continuamente!</li>
<li>31:59 - 3. Usarás um Sistema de CI!</li>
<li>32:44 - 4. Farás Deploy Contínuo!</li>
<li>33:16 - Conclusão: equipes saudáveis</li>
</ul>
<h2>Links<span class="hx:absolute hx:-mt-20" id="links"></span>
    <a href="#links" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>The Twelve-Factor App (<a href="https://12factor.net/"target="_blank" rel="noopener">https://12factor.net/</a>)</li>
<li>Guidance for preventing, detecting, and hunting for exploitation of the Log4j 2 vulnerability  (<a href="https://www.microsoft.com/security/blog/2021/12/11/guidance-for-preventing-detecting-and-hunting-for-cve-2021-44228-log4j-2-exploitation/"target="_blank" rel="noopener">https://www.microsoft.com/security/blog/2021/12/11/guidance-for-preventing-detecting-and-hunting-for-cve-2021-44228-log4j-2-exploitation/</a>)</li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita.</p>
<p>No episódio anterior me dei ao trabalho de fazer o passo a passo de um tutorial simples de primeiros passos usando o Heroku pra fazer deploy de um projetinho de exemplo em PHP. O objetivo foi mostrar o processo de deployment de uma aplicação moderna pra iniciantes que talvez nunca nem tenham ouvido falar de Heroku. Qualquer desenvolvedor que já trabalha com certeza já conhecia. Então, se você não sabe o que é Heroku assista o episódio anterior antes de ver este, porque precisa saber o que eu disse lá pra entender este.</p>
<p>Hoje quero falar sobre um troço conhecido como os “12 fatores” que é meio uma metodologia que pode ser aplicada a projetos web escritos em qualquer linguagem de programação e qualquer framework web. Além disso quero complementar com alguns fatores que originalmente não se menciona mas meio que ficam implícitos. Se você quer um projeto web que tenha capacidade de escalar é meio obrigatório que no mínimo esses 12 fatores estejam satisfeitos. Satisfazer os 12 fatores não torna nenhuma aplicação automaticamente escalável, mas não satisfazer eles quase garante que não vai escalar de jeito nenhum.</p>
<p>(&hellip;)</p>
<p>Se você já é programador de sistemas legados, tenho certeza que quando viu o tutorial de Heroku pela primeira vez, brilhou seus olhos de vontade de poder trabalhar desse jeito. Se você é de agenciazinhas porcaria - e eu defino como porcaria todas que ainda atualizam sites via FTP como falei antes - mesma coisa, deve estar se coçando de vontade de poder trabalhar assim. Todo mundo que trabalha em tech startups hoje em dia ou empresas grandes que se modernizaram já trabalham desta forma. Mas cuidado, se você não trabalha assim e mesmo assistindo até aqui achou tudo desnecessário ou mesmo ruim, eu repensaria a carreira. Você vai se tornar obsoleto muito em breve.</p>
<p>E quem acha que isso tudo é só mais uma novidade passageira, que vai acabar daqui a pouco, está bem enganado. Eu trabalho desse jeito desde aproximadamente 2010, portanto mais de uma década. Eu ajudei o quanto pude a difundir esse estilo de trabalho por anos e finalmente isso se tornou o padrão. Muitos tentaram imitar o Heroku, mas até hoje acho impressionante que poucos conseguiram chegar perto.</p>
<p>Só resumindo se você não lembra do episódio passado, o grande lance é você não configurar um servidor manualmente, ser tudo automatizado. E principalmente não instalar todos os componentes da aplicação no mesmo lugar, deixar cada coisa num container separado, como serviços externos independentes. Além disso conseguir fazer deploys frequentes a cada nova funcionalidade ou correção de bug. Tudo tão fácil quanto fazer um <code>git push</code>, literalmente.</p>
<p>Na mesma época que o Heroku surgiu, o Google tinha acabado de lançar um produto que fez muito barulho, o Google App Engine pra rodar aplicações feita em Python usando serviços do Google. O problema é que você não podia usar componentes padrão de Linux como banco de dados Postgres. Precisava usar o banco do Google que era o BigTable, um NoSQL que só existe no Google e não dá pra rodar em outro lugar. Além disso precisava programar de formas específicas pro App Engine. Então, uma vez que funcionasse não dava pra rodar em nenhum outro lugar. Era super simples de subir a aplicação mas era super limitado, restritivo e você ficava preso.</p>
<p>A grande sacada do Heroku foi abrir os padrões e ajudar a criar uma forma onde ao mesmo tempo você queria ficar no Heroku porque tudo era muito simples e seguro, mas não precisava lobotomizar seu código pra fazer rodar. Se quisesse sair e instalar sua própria infraestrutura, sua aplicação não tinha nada proprietário te prendendo. Além disso, eles criaram o conceito de marketplace de software as a service pra desenvolvedores. Você podia sair do Heroku, instalar sua aplicação manualmente num VPS do Linode e ainda usar o mesmo serviço de Papertrail que mostrei antes lá, por exemplo. Os addons também são independentes do Heroku.</p>
<p>Mas se ainda não ficou óbvio, você não escolhe infraestrutura querendo que seja barata demais, porque você recebe exatamente aquilo que paga. Se paga barato, não espere ter garantia de nada, aliás, espere que seu sistema vai cair quando mais precisa dele e vai sair super caro colocar tudo no ar de novo pra aguentar a carga. É por isso que sempre que possível, a recomendação é começar no Heroku. No outro extremo, se tem dinheiro sobrando, não recomendo ir direto pra um Kubernetes, começa no Heroku. Só faz infra customizada quando tem dados de uso que realmente justificam isso.</p>
<p>O Heroku mantém as coisas atualizadas. Patches de segurança e tudo mais são aplicados na sua infra, e na maioria dos casos você não precisa fazer nada. O jeito como o Heroku te obriga a organizar seu projeto, não insere nada de proprietário, mas ao mesmo tempo te força a fazer as coisas do jeito certo é a principal vantagem. Quando alguma coisa não funciona no Heroku normalmente não é um problema do Heroku, é problema da sua aplicação que foi feita de um jeito porco.</p>
<p>E pra facilitar a educação do jeito certo de programar aplicações web, o co-fundador Adam Wiggins escreveu um site chamado The 12 Factors que lista os 12 fatores que todo projeto web deve ter pra ser minimamente possível instalar numa infraestrutura escalável como o Heroku. É como se fossem 12 patterns de gerenciamento de arquitetura de projetos. Esse site foi publicado acho que em 2011 e tudo que está nele continua válido igualzinho até hoje.</p>
<p>Quem acompanha este canal e conseguiu acompanhar o tutorial de Heroku até aqui já entende a maioria, mas quero comentar ponto a ponto desses doze fatores. Se você é iniciante, preste muita atenção. É isso que se espera que todo programador web saiba de cor e salteado, independente da linguagem ou framework que escolher. Aliás, todos os frameworks web modernos de hoje facilitam você implementar esses 12 fatores. Um framework que não é compatível com esses fatores é um péssimo framework e você não deveria estar usando.</p>
<p>Vamos lá, o ponto 1 declara que todo código-fonte deve estar num repositório de versionamento de código. Hoje em dia isso só tem uma resposta e se chama Git. Tudo pode estar num único repositório Git, se for um projeto gigantesco talvez esteja dividido em múltiplos repositórios ou monorepos. Não importa, contanto que esteja em Git que todo mundo da equipe tenha acesso e que se faça deploys com frequência. Esse ponto é chave.</p>
<p>No próprio tutorial do episódio passado eu fiz pelo menos uma meia dúzia de deploys enquanto ia adicionando novas funcionalidades. Antigamente fazer deploys era um processo demorado, manual, cheio de erros humanos. Hoje em dia o certo é que se faça deploys contínuos. Múltiplos por semana, múltiplos por dia. No mínimo em ambiente que chamamos de staging, que é tipo um ambiente de testes onde tudo roda como se fosse ambiente de produção. Não pode existir uma dúzia de desenvolvedores fazendo código semanas inteiras e levar mais semanas pra subir esse código numa infraestrutura de testes. É inadmissível e é sinal claro de incompetência da gerência dessa equipe.</p>
<p>O segundo fator se chama Dependências e foi por isso que me alonguei tanto falando de gerenciamento de dependências durante o tutorial. É fator fundamental que todas as dependências de um projeto web não dependam de pacotes específicos de sistema operacional instalado na máquina. Isso porque o sistema operacional dos containers do servidor e o sistema na máquina de cada desenvolvedor sempre vão ser diferentes. E versões diferentes de cada pacotes vão estar sendo usados em cada lugar, aí a gente nunca sabe de onde está vindo muitos bugs, se é problema no código ou se é dependência com versões erradas.</p>
<p>Repetindo: toda dependência deve estar declarada num arquivo de manifesto como <code>composer.json</code> no nosso projeto de exemplo, ou <code>package.json</code> num projeto Node. E tanto na máquina do desenvolvedor quanto no script de deploy como do Heroku, vamos rodar o mesmo comando pra baixar as dependências, como o <code>compose update</code> ou <code>npm install</code> da vida. E em todos os lugares vamos garantidamente ter exatamente as mesmas dependências nas exatas versões necessárias. Isso é essencial e novamente, projetos que não estão assim demonstram incapacidade da gerência do projeto.</p>
<p>O terceiro fator se chama Configuração. Em resumo, foi o que expliquei no tutorial sobre a diferença de configuração na máquina de desenvolvimento usando recursos do framework que carrega arquivos como <code>.env</code> ou algum arquivo JSON da vida e variáveis de ambiente. Todo framework competente permite executar a aplicação em ambientes distintos pra desenvolvimento, testes e produção, no mínimo. Isso porque em desenvolvimento queremos mais detalhes de erros, mais bibliotecas de debugging e monitoramento, mas em produção não queremos divulgar nenhum detalhe de erros e nem desperdiçar memória carregando bibliotecas que só servem em desenvolvimento.</p>
<p>Em cada um desses ambientes precisamos de variáveis de configuração diferentes, pro ambiente de testes precisamos conectar num banco de dados de teste, por exemplo. Ou não mandar SMS ou notificações de verdade e só logar que tentou mandar. Tudo isso precisa estar facilmente configurável em variáveis de ambiente. Leia os detalhes sobre configuração do seu framework, tem muitas nuances que você precisa estudar e testar com calma.</p>
<p>O fator 4 é mais relevante se você já é um programador das antigas que instalava tudo num servidor como já expliquei inúmeras vezes que não deveria, com stacks como LAMP. Pra você havia diferença num banco de dados que roda no mesmo localhost da sua aplicação web e um serviço externo de enviar e-mails, por exemplo. Numa aplicação 12 fatores não existe essa distinção. Tudo é um serviço externo, ou um recurso externo, acessível via uma URL. Nada roda junto do container da sua aplicação web, tudo roda externamente. Esse é o quarto fator, backing services.</p>
<p>É extritamente proibido instalar um servidor MySQL junto com um Redis ou um sistema de filas como RabbitMQ. No mínimo do mínimo, se tiver um hardware parrudo, cada serviço deve estar rodando isolado no seu próprio container ou máquina virtual. Mesmo na sua máquina de desenvolvimento, se possível não instale tudo junto. Rode cada coisa num container Docker e orquestre tudo com Docker Compose. Estude sobre Docker Compose e experimente transformar seu projeto tradicional num conjunto de containers. É até uma boa forma de garantir que você não largou conexões hardcoded pra localhost, que vai funcionar na sua máquina de desenvolvimento mas vai falhar em produção.</p>
<p>O fator 5 é mais específico pra quem mexe com infra, mas é sobre separar a fase de build da fase de execução. Lembra como toda vez que fazemos <code>git push heroku</code> eu falei que o Heroku criava uma nova imagem, semelhante ao que o Docker faz quando executamos <code>docker build</code>? É isso que quero dizer. Antigamente a gente faria o equivalente build manualmente copiando arquivos via FTP e logo na sequência já executaríamos a aplicação e deixaríamos lá rodando.</p>
<p>Pior, se tivesse algum bugzinho, o normal era abrir um SSH pro servidor ou, Deus me livre, via Telnet, editar algum arquivo manualmente direto lá e pronto. Era super fácil e também a raíz de milhares de problemas. Primeiro porque tudo que você editou na mão direto no servidor não está no repositório de código. Se der pau no servidor, você perde a modificação e tudo pára de funcionar e ninguém sabe porque. Voltamos ao fator 1 que declara que 100% de tudo deve sempre estar num repositório Git.</p>
<p>Segundo porque digamos que queremos voltar uma release, como expliquei que o Heroku faz. Não tem como, porque todo novo deploy sobrescreve em cima da versão antiga de maneira permanente. Mas gerando builds, sempre vamos ter uma cópia da imagem antiga e podemos facilmente voltar pra ela. E como cada imagem não depende de pacotes específicos do sistema operacional, tudo que ela precisa pra rodar está na imagem. Portanto trocar imagens é uma operação trivial que pode ser feita dezenas de vezes e vai funcionar em todas as vezes, sem depender de nenhuma pessoa entrando nos servidores e subindo coisas manualmente.</p>
<p>O fator 6 provavelmente é um dos mais difíceis de entender se você for um iniciante. Ela se chama Processos mas na verdade é o fator que define que sua aplicação deve ser share-nothing, ou mais corretamente, stateless, sem estado. O exemplo mais simples é que sua aplicação nunca deve gravar nada no sistema de arquivos. Por exemplo, digamos que você faça uma página pra fazer upload de fotos pra colocar na página de perfil de cada usuário.</p>
<p>Você segue um tutorial qualquer que manda ir gravando tudo num diretório chamado uploads no servidor. E tudo vai funcionar perfeitamente. Até você fazer um novo deploy no Heroku ou outro sistema de containers e aí vai descobrir que todas as suas fotos sumiram. Os únicos arquivos que existem na sua aplicação são aqueles que existiam na fase de build da imagem, que é o código fonte que baixou do Git, as bibliotecas que um gerenciador de dependências como o Composer baixou, e outros arquivos gerados por scripts executados no build, como arquivos de javascript compilados por um Webpack.</p>
<p>Uma vez que a build foi fechada, ela não pode mais ser modificada, é pra ser considerada read-only. Daí vocês vão lembrar que eu falei durante o tutorial que o Heroku vai derrubar os dynos que estavam executando a versão antiga e subir novos dynos com a imagem nova. Entenderam? Quando o container é desligado, tudo que estava nele é perdido, por exemplo as fotos no diretório uploads. Quando um novo dyno subir, os únicos arquivos que vão ser carregados são os que estão na imagem que acabamos de construir.</p>
<p>No caso de um Docker na sua máquina local é a mesma coisa, mas você tem a opção de mapear um diretório externo fora do container e remontar esse diretório quando subir o container de novo, daí os arquivos ainda persistem, mas sempre fora do container. Tudo dentro de um container deve ser considerado efêmero.</p>
<p>Todo dado que você quer que sobreviva a um deploy deve obrigatoriamente estar num armazenamento externo, um backing service como descrito no fator 4. Por exemplo num banco de dados como Postgres ou Redis e, no caso de arquivos, num serviço como o AWS S3 ou Azure Blob ou Google Cloud Storage da vida. Tem várias outras opções. Mesma coisa logs, por isso o certo é adicionar addons como um Papertrail. Qualquer log que for gravado como arquivo dentro do container vai ser perdido no próximo deploy, e esse é o jeito certo de se fazer aplicações web. Vamos falar mais sobre log no final.</p>
<p>O fator 7 não é tanto uma preocupação pra nós desenvolvedores, especialmente hoje em dia. Ele só declara que todo serviço que precisamos deve estar exposto numa porta de rede. Como nossa aplicação web que costuma estar exposta em portas como 3000 ou 8080, ou MySQL que costuma estar na porta 3601 e assim por diante.</p>
<p>Isso eu acho que é mais pra dizer que não devemos usar serviços que não estejam expostos em portas. Por exemplo, pense no serviço cron que existe em todo Linux e serve pra schedular tarefas. A gente configura num arquivo pra executar algum comando em alguma determinada hora do dia ou periodicamente, tipo todo dia às 4 da manhã se for um backup. O único ponto de contato com esse serviço é via um arquivo de configuração, por isso não podemos usar cron numa infraestrutura dessas, com vários containers que não permite modificar arquivos localmente.</p>
<p>Em vez disso vamos usar um software as a service, um SaaS, que adiciona uma aplicação web na frente do cron, ele vai ser instalado como um backing service, um serviço externo e nossa aplicação web vai interagir com esse serviço via HTTP, conectando na porta web dele. Assim, a requisição pode sair de qualquer um dos nossos containers web e todos vão consistentemente chegar no mesmo serviço sempre, assim como já chegam em outros serviços como o banco de dados.</p>
<p>O fator 8 se chama Concorrência e meio que só deriva dos fatores anteriores. Ele declara que a forma de escalar é horizontal. Ou seja, como nossa aplicação é share-nothing, ou seja, não depende de nenhum estado específico da máquina onde está, e como já é tudo organizado como builds que podemos subir como containers em qualquer máquina, podemos rapidamente aumentar ou diminuir a quantidade de containers horizontalmente.</p>
<p>Esse fator é inspirado no jeito UNIX de pensar onde tudo são processos e a forma de escalar é dar fork em mais processos. Mesmo quando usamos alguma linguagem como Javascript ou Elixir, que permite fazer multiplexing via threads ou I/O assíncrono dentro de cada processo, ainda assim sempre vai estar limitada pelo tamanho da máquina onde está rodando e quando esse recurso se exaurir, vai precisa escalar horizontalmente de qualquer jeito. Escalabilidade exige expansão e contração horizontal, que é mexer na quantidade de containers e/ou na quantidade de máquinas físicas.</p>
<p>Particularmente um Heroku facilita isso porque quando você escala horizontalmente containers web, vai precisar de um load balancer na frente pra distribuir a carga de requisições. É algo que se você só subiu aplicações pequenas nunca teve que pensar, porque só tinha um servidor web e todas as requisições iam direto pra ele. Com mais um servidor web, você precisa de um balanceador de carga como NGINX ou HAProxy na frente. Um Heroku da vida já te dá esse balanceador por padrão de forma transparente.</p>
<p>O fator 9 é um pouco mais avançado de entender e eu recomendo estudar sobre sinais de processos como SIGTERM e o que é shutdown gracioso e conceitos como rolling restart. Mas na prática esse fator fala sobre Descartabilidade. Você já viu isso na prática no tutorial toda vez que fizemos <code>git push heroku</code>. Ele faz a build de uma nova imagem e descarta os containers antigos pra subir novos. É isso que significa ser descartável. Uma vez que garantimos que não dependemos de nada dentro do container, todos os dados estão seguros num banco de dados ou outros serviços como o AWS S3 pra arquivos remotos, podemos facilmente descartar containers pra fazer deploy de uma nova release ou dar rollback pra releases anteriores.</p>
<p>Antigamente, quando a gente instalava tudo num mesmo servidor, não dava pra fazer isso. E se a máquina era invadida por um hacker? É impossível recuperar uma máquina comprometida, o certo é jogar tudo fora e remontar do zero. Mas se você editava coisas via SSH direto no servidor, se parte do que rodava não estava no Git, e se sua aplicação gravava uploads e logs localmente como arquivos. E agora? Como você vai conseguir reinstalar essa máquina o mais rápido possível? E a resposta é simples: não vai. Um sistema robusto precisa ser descartável. Você precisa conseguir apagar o servidor inteiro e reinstalar tudo automaticamente via script sem perder 1 bit de dados e em poucos segundos.</p>
<p>O ponto 10 é pra reforçar o que acabei de falar, que é manter ambientes de desenvolvimento, staging e produção o mais similares quanto possível. E isso obviamente significa tudo em repositório Git, significa builds automatizados e significa jamais editar arquivos direto no servidor. Os benefícios de fazer as coisas dessa forma é que gerenciar infraestrutura se torna muito mais fácil e quase trivial em alguns casos. É assim que nasce a área de devops de verdade.</p>
<p>Mais do que isso, esse fator significa fazer deploys pra staging e produção o mais rápido quanto possível pra estar próximo do que está rodando na máquina dos desenvolvedores. Nada disso de demorar um mês ou mais pra fechar uma release e mandar pra produção. Estude sobre Continuous Deployment. Plataformas como GitHub e GitLab tem configurações pra criar builds e rodar os testes da sua aplicação a cada novo commit que aparece. E se tudo passar eles fazem automaticamente o <code>git push heroku</code> pra staging ou pra produção.</p>
<p>Parece perigoso isso de subir pros servidores automaticamente mas só é perigoso se sua gerência é uma droga. A quantidade de bugs em produção reflete inversamente a qualidade de comunicação da sua equipe. Uma equipe disfuncional vai subir porcaria pra produção não importa quantos mecanismos de gerenciamento você coloque em cima deles.</p>
<p>Uma equipe saudável revisa o código um do outro antes de mergear um pull request na branch <code>main</code> do Git. Uma equipe saudável sempre adiciona testes pra cada nova funcionalidade e sempre adiciona testes que simulam um bug recém reportado, dessa forma esse bug não vai aparecer de novo no futuro. Uma equipe saudável implementa novas funcionalidades atrás um &ldquo;feature flag&rdquo;. Seja via variável de ambiente ou via permissão pra determinados usuários testarem e pros demais usuários não conseguirem enxergar essa nova funcionalidade até toda equipe ter chance de testar e aprovar. Ou seja, mesmo a desculpa que uma nova funcionalidade vai levar semanas pra ficar pronto não é justificativa suficiente pra não estar integrando o código no repositório com frequência e não estar fazendo deploys com frequência. Quanto mais se demora pra integrar e deployar mais rápido aumenta a quantidade de bugs e conflitos, é inevitável.</p>
<p>Existem diversas estratégias de comunicação e organização que uma equipe saudável pode aplicar e eu acabei de descrever algumas. Estude sobre Testes, Integração Contínua e Deploy Contínuo. Só equipes disfuncionais e/ou com péssima gerência são incapazes de trabalhar de forma integrada e contínua. Lembrem-se, não importa a linguagem ou framework, produtividade, bugs, tudo isso é reflexo da qualidade de comunicação de uma equipe. Sempre.</p>
<p>O fator 11 de novo é meio específico pra quem desenvolve frameworks, mas fala sobre logs serem streams em vez de arquivos. Todo bom sistema manda logs pro STDOUT. Se por acaso você começou a aprender sobre logs, rotação de logs e coisas assim, esquece. Num container todo serviço deve escrever logs pro STDOUT, sempre. Daí a plataforma de infraestrutura, como um Heroku, vai capturar esses streams e mandar pra algum serviço externo de logs, como o Papertrail que mostrei antes.</p>
<p>Fica a dica que um sistema de log que tenta fazer coisas demais uma hora vai dar catástrofe. Que foi o que aconteceu com o caso recente do Log4j. É bem coisa de javeiro enterprise fazer uma mísera biblioteca de Log que, por alguma razão do além, permite executar comandos que vem no log e ainda vem junto com bibliotecas como de lookup de JNDI que se conecta com o mundo exterior. É o cúmulo da estupidez, pra dizer o mínimo.</p>
<p>Um log4j nem precisa muito existir, pelo menos não do jeito como é hoje. Todo log deveria ir pro STDOUT e um serviço externo, separado, como Logstash, Kibana e coisas assim que deveriam consumir esses logs e organizar. Isso é fruto daquela mentalidade de arquiteto chimfrin que pensa, &ldquo;ah, e se amanhã eu precisar dessa opção?&rdquo; E isso tá errado. Nunca faça coisas que não precisa pra hoje. O dia que a necessidade aparecer aí pensa qual a melhor forma de resolver, mas não faça nada porque tem chances de amanhã precisar. Isso é software desnecessário, e todo software desnecessário é um buraco de segurança esperando pra ser descoberto.</p>
<p>Finalmente o fator 12, que fala de processos de administração, que eu já mostrei no tutorial pra vocês. A idéia é que você nunca abre um SSH direto pra um dos containers web. Toda tarefa administrativa fora do comum deve ser executado num container isolado e separado dos demais, sem concorrer os mesmos recursos com as requisições de usuários.</p>
<p>Lembram quando rodei <code>heroku run bash</code> que abriu um container direto no bash? Ou quando rodei <code>heroku pg:psql</code> que abriu o console do Postgres pra criar a tabela que faltava? Em ambos os casos o Heroku subiu um novo container com a imagem da última release, ou seja, idêntico ao conteúdo dos containers web, mas fora do load balancer. Assim eu rodo no mesmo ambiente e quando terminar e desconectar, esse container é automaticamente destruído. E isso é importante, lembra o fator sobre descartabilidade?</p>
<p>Os 12 fatores foram muito influentes quando foram lançados porque na época o único framework que implementava com sucesso todos os fatores era o Ruby on Rails. Levou um tempão pros outros alcançarem esse mínimo. Produtos open source que não usavam nenhum framework como Wordpress ou Magento não implementavam todos os 12 fatores por padrão e também levou um tempo pra se adequarem.</p>
<p>O Heroku foi lançado entre 2008 e 2009, só uns 2 anos depois que a Amazon AWS apareceu. Naquela época a maioria de nós só tinha mentalidade de servidor virtual, no máximo, VPS. Ninguém pensava em particionar recursos da mesma máquina em containers e muito menos precificar por containers. Docker ainda estava a anos de ser lançado e na verdade muito do Docker foi influenciado pelo Heroku.</p>
<p>A Amazon estava muito a frente do seu tempo, tanto que ninguém sabia exatamente qual a melhor forma de tirar proveito dessa idéia de infraestrutura elástica. O que significava ser elástico? Como lidar com máquinas voláteis e descartáveis que quando reiniciam apagam tudo que tinha dentro? O Heroku foi a primeira plataforma como serviço ou PaaS que de fato soube aproveitar a infraestrutura que a AWS estava oferecendo.</p>
<p>O sonho de muitas empresas é ter um Heroku privado, o famoso private cloud, onde equipes internas poderiam facilmente dar <code>git push</code> e o deploy aconteceria automaticamente. E muitas empresas já trabalham assim. É o que todo mundo que tenta instalar Kubernetes busca. O que o Heroku chama de dynos, o Kubernetes chama de pods. A linha de comando <code>kubectl</code> da vida é inspirado na linha de comando do Heroku. Enfim, se você lida com devops de alguma forma hoje em dia, tenha certeza que muita coisa foi inspirado no Heroku.</p>
<p>Se parece fanboy falando, pode dizer que sou mesmo, porque em mais de 10 anos entregando projetos de verdade pra clientes, ainda não vi outra solução que chega perto em termos de simplicidade, flexibilidade, organização e custo benefício. Muitos acham caro a faixa de 30 dólares por dyno e é mesmo, e fazem a conta comparando com o preço bruto da hora de uma máquina no EC2. Mas a conta tá errada. A conta certa é calcular quanto custa ter uma pessoa experiente de devops de plantão todo dia pra manter sua infra. O cálculo certo não é da máquina mas do cara de devops que você não precisa ter quando usa o Heroku.</p>
<p>Aliás, antes que alguém vá nos comentários dizer sim, eu sei que existem soluções como o Dokku que implementa um Heroku light que você mesmo pode instalar num VPS da vida. Você também pode usar direto Docker Machine pra orquestar suas VPS. Mas de novo, você precisa adicionar o custo de alguém de infra pra dar manutenção nessa infraestrutura. Nunca vai ser plug and play que uma vez instalado ninguém mais precisa mexer. Não caia nessa armadilha.</p>
<p>Além disso tem soluções como o Google Firebase que, como o Google App Engine antes dele, você programa especificamente pra essa plataforma, e vai depender pra sempre do que o Google te oferecer. Esquece querer mudar pro Azure ou AWS depois. E esquece querer usar qualquer ferramenta open source e bibliotecas que a plataforma não suporta. Tudo tem prós e contras e meu ponto é que o Heroku oferece o melhor custo benefício entre conveniência, segurança e independência do seu projeto.</p>
<p>Também vale relembrar que Heroku e 12 fatores não são balas de prata, eu tenho um caso pra ilustrar. Lá por 2014 estava pessoalmente trabalhando num projeto pra cliente que envolvia uma ferramenta que seria usada por vestibulandos. Era uma plataforma de educação e os donos tinham a expectativa que ia vir uma montanha de alunos de cursinho por causa de algum evento que eu não lembro mais o que era. Mas em resumo eles pediram especificamente pro sistema ser escalável.</p>
<p>Eu e minha equipe fizemos tudo em Ruby on Rails, que é o framework que funciona melhor no Heroku, especialmente naquela época, porque o próprio Heroku é em boa parte feito em Rails também. Fizemos deployment mas na hora do vamos ver sofremos um gargalo. Obviamente foi culpa minha que não estimou direito o tamanho do bancos de dados Postgres.</p>
<p>O problema é o seguinte: todo servidor de banco de dados tem um máximo de conexões simultâneas que aguenta. Faz de conta que são 100. Mesmo implementando um connection pool, se eu escalar horizontalmente a aplicação, ou seja, aumentar o número de dynos web rodando, todos vão se pendurar no banco de dados e rapidamente 100 conexões vão ficar ocupados e novos dynos vão ficar travados esperando conseguir uma conexão.</p>
<p>Pra lidar com isso a gente tenta mover o que não precisa pegar direto do banco em caches como Redis. E se precisa muito do banco, o certo é subir vários outros servidores que são réplicas só de leitura do principal. Assim cada nova réplica aguenta mais 100 conexões simultâneas, por exemplo. Mas não dá pra colocar réplicas infinitamente porque quanto mais réplicas tem, maior o trabalho de replicação dos dados, aí vai dar gargalo na sincronia de replicação.</p>
<p>Muito iniciante que entra em plataformas como Heroku achando que vai colocar um addon de auto-scale e tudo vai escalar infinitamente sozinho, se decepciona que não funciona mágico assim. Escalabilidade é uma combinação de funcionalidades de infraestrutura e arquitetura bem implementada. Depende mais do programador do que da infraestrutura. Mas se você for um bom programador, um Heroku dá tudo que você precisa com o mínimo de esforço. É impressionante o que eu consigo fazer sozinho num ambiente desse em 5 min, coisa que no começo dos anos 2000 ia me custar dias de configuração. Já se você for um programador ruim, nenhum Heroku ou solução de cloud vai te ajudar, só vai ser um band-aid temporário.</p>
<p>Eu já venho repetindo isso faz alguns episódios e vou repetir de novo. Eu tenho projetos pra clientes que funcionam assim, mas o certo como já disse antes é que os ambiente de staging e produção não estejam muito longe da versão em desenvolvimento na máquina dos desenvolvedores. Pra que isso seja possível, precisa obrigatoriamente existir os seguintes passos.</p>
<p>Primeiro passo, toda nova funcionalidade e toda correção de bugs deve vir acompanhado no mínimo de testes unitários. Todo pull request sem testes deve ser automaticamente rejeitado. Sem exceções. Essa é a principal regra que vai garantir que seu projeto sobreviva por muitos anos. No começo, com pouca gente, com pouco código e com prazos apertados, parece que é uma grande perda de tempo. E é assim que amadores pensam e é assim que amadores se ferram quando o projeto rapidamente cresce fora de controle.</p>
<p>Quando você satisfaz essa primeira condição agora precisamos do segunda passo: que é integrar o código constantemente e frequentemente. Ou seja, nenhum desenvolvedor deve ficar dias e dias com código que só existe na sua máquina, demorando pra dar push pro repositório no GitHub ou GitLab. Todo desenvolvedor deve, no mínimo uma vez por dia, dar pull, ou seja, puxar as últimas atualizações do repositório pra pegar tudo que todo mundo da equipe andou trabalhando e dar rebase com seu branch de desenvolvimento e corrigir conflitos. E com sua suite de testes unitários, rapidamente ver o que quebrou e consertar imediatamente. Se você faz isso com frequência, os conflitos são simples de resolver, se der conflitos. Quanto mais tempo demora pra integrar, mais difícil vai sendo pra resolver os conflitos do seu código com dos outros programadores da equipe, e maiores as chances de fazer bosta e subir bugs.</p>
<p>Isso é integração contínua. Facilita muito se como regra número três você estiver usando algo como GitLab que tem suporte a rodar testes automaticamente no servidor, ou usar qualquer outra solução como CircleCI. Você deixa um Dockerfile configurado no projeto, o GitLab vai buildar toda vez que alguém der push pro repositório e colocar numa fila pra rodar todos os testes automatizados. Se falhar já notifica pra todo mundo e assim garantimos que todo código que está na branch <code>main</code> sempre tem todos os testes rodando e passando. O objetivo é esse: que sempre que alguém der clone do branch <code>main</code>, tudo sempre vai funcionar. O branch <code>main</code> tem que ser sagrado. Tudo que está inacabado deve estar em branches de desenvolvimento separados.</p>
<p>É essa condição que permite a próxima regra número quatro que é, toda vez que todos os testes automatizados passarem, deve ser feito deployment pra um ambiente de staging. O Heroku suporta criar pipelines de staging e production, assim você sempre tem uma infraestrutura idêntica de testes e de produção. Daí um GitLab da vida mesmo faz o <code>git push heroku</code> automaticamente pra staging e sua equipe pode imeditamente testar no navegador como um usuário normal pra ver se realmente nada quebrou, se as novas funcionalidades estão mesmo funcionando.</p>
<p>Recapitulando: uma equipe saudável e produtiva vai sempre desenvolver código com testes automatizados, vai sempre integrar continuamente, vai sempre ter um servidor de testes como GitLab que roda a cada push, de cada desenvolvedor. E finalmente, vai sempre ter deployments automáticos pra ambiente de staging toda vez que todos os testes automatizados passam. E fica como opção subir pra produção automaticamente ou deixar alguém de QA,
por exemplo, responsável por fazer testes manuais no final e apertar o botão pra deployar pra produção e liberar pros usuários.</p>
<p>Um modelo similar ao Heroku, com um projeto arquitetado pra atender os 12 fatores, e com minhas 4 últimas regras é o que eu chamo do framework mínimo pra toda equipe de desenvolvimento verdadeiramente Ágil. Toda outra metodologia, sistema de backlog e controles, é opcional e não relevante pra produtividade de verdade. Lembrem-se, a qualidade do software é diretamente proporcional à qualidade da comunicação da equipe. Não tem como fugir disso. Se sua equipe se comunica mau e porcamente, seu software vai ser necessariamente uma grande droga.</p>
<p>A idéia hoje foi só apresentar pra quem é iniciante como que profissionais de verdade trabalham ou gostariam de trabalhar. E é esse nível que vocês deveriam almejar alcançar. Se você trabalhar num lugar que não funciona assim, tente evangelizar o jeito certo. Vai implementando um aspecto de cada vez, vai fazendo limpeza um pedaço de cada vez. Mesmo nos projetos mais antigos e mais porcos é possível transformar num projeto eficiente se, primeiro, a equipe entende que dá pra melhorar e se compromentem a fazer limpeza e não aumentar mais a sujeira. Por hoje é isso aí. Se ficaram com dúvidas mandem nos comentários abaixo, se curtiram o video deixem um joinha, assinem o canal e compartilhem o video com seus amigos. A gente se vê, até mais.</p>
]]></content:encoded><category>arquitetura-de-software</category><category>devops</category><category>desenvolvimento-web</category><category>akitando</category></item><item><title>[Akitando] #112 - Subindo Aplicações Web em Produção | Aprendendo HEROKU</title><link>https://akitaonrails.github.io/2022/01/10/akitando-112-subindo-aplicacoes-web-em-producao-aprendendo-heroku/</link><guid isPermaLink="true">https://akitaonrails.github.io/2022/01/10/akitando-112-subindo-aplicacoes-web-em-producao-aprendendo-heroku/</guid><pubDate>Mon, 10 Jan 2022 12:15:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/TLRW_xTnQwY"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;Finalmente vou mostrar o que é o Heroku e como é o fluxo de trabalho mínimo de um projeto web ideal. Se você já usa Heroku, aproveite pra compartilhar o video com conhecidos que ainda não usam. Se nunca viu Heroku, prepare-se pra ficar surpreso!&lt;/p&gt;
&lt;h2&gt;Conteúdo&lt;span class="hx:absolute hx:-mt-20" id="conteúdo"&gt;&lt;/span&gt;
&lt;a href="#conte%c3%bado" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;00:00 - Intro&lt;/li&gt;
&lt;li&gt;00:41 - Mega Sena da Virada&lt;/li&gt;
&lt;li&gt;02:33 - Concorrência&lt;/li&gt;
&lt;li&gt;04:11 - Intro a Heroku&lt;/li&gt;
&lt;li&gt;04:46 - Deploy antigo&lt;/li&gt;
&lt;li&gt;06:24 - Iniciando Tutorial Heroku&lt;/li&gt;
&lt;li&gt;06:49 - Setup: Git é Básico!&lt;/li&gt;
&lt;li&gt;07:22 - Heroku Login&lt;/li&gt;
&lt;li&gt;08:18 - Primeiro Deploy&lt;/li&gt;
&lt;li&gt;10:03 - Buildpack? Dockerfile?&lt;/li&gt;
&lt;li&gt;10:35 - Entendendo Dynos e Containers&lt;/li&gt;
&lt;li&gt;11:30 - Escalando Dynos&lt;/li&gt;
&lt;li&gt;13:16 - Entendendo Load Balancer&lt;/li&gt;
&lt;li&gt;16:41 - Vendo logs dos Dynos&lt;/li&gt;
&lt;li&gt;17:22 - Entendendo Procfiles&lt;/li&gt;
&lt;li&gt;18:58 - Mais sobre escalar dynos&lt;/li&gt;
&lt;li&gt;20:21 - Entendendo Gerenciamento de Dependências&lt;/li&gt;
&lt;li&gt;24:37 - Adicionando nova dependência&lt;/li&gt;
&lt;li&gt;26:40 - Adicionando Addons&lt;/li&gt;
&lt;li&gt;28:16 - Conectando num shell remoto&lt;/li&gt;
&lt;li&gt;29:41 - Configurando com variáveis de ambiente&lt;/li&gt;
&lt;li&gt;33:39 - Adicionando Banco de Dados&lt;/li&gt;
&lt;li&gt;37:13 - Database Migrations&lt;/li&gt;
&lt;li&gt;37:47 - Conectando no Banco de Dados&lt;/li&gt;
&lt;li&gt;39:22 - Releases e Rollback&lt;/li&gt;
&lt;li&gt;40:34 - Conclusão&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Links&lt;span class="hx:absolute hx:-mt-20" id="links"&gt;&lt;/span&gt;
&lt;a href="#links" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Mega da Virada: Loterias Caixa colocam fila em app, e site fica fora do ar (&lt;a href="https://tecnoblog.net/noticias/2021/12/31/mega-da-virada-loterias-caixa-colocam-fila-em-app-e-site-fica-fora-do-ar/"target="_blank" rel="noopener"&gt;https://tecnoblog.net/noticias/2021/12/31/mega-da-virada-loterias-caixa-colocam-fila-em-app-e-site-fica-fora-do-ar/&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Getting Started on Heroku with PHP (&lt;a href="https://devcenter.heroku.com/articles/getting-started-with-php#set-up"target="_blank" rel="noopener"&gt;https://devcenter.heroku.com/articles/getting-started-with-php#set-up&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;SCRIPT&lt;span class="hx:absolute hx:-mt-20" id="script"&gt;&lt;/span&gt;
&lt;a href="#script" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Olá pessoal, Fabio Akita&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/TLRW_xTnQwY"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Finalmente vou mostrar o que é o Heroku e como é o fluxo de trabalho mínimo de um projeto web ideal. Se você já usa Heroku, aproveite pra compartilhar o video com conhecidos que ainda não usam. Se nunca viu Heroku, prepare-se pra ficar surpreso!</p>
<h2>Conteúdo<span class="hx:absolute hx:-mt-20" id="conteúdo"></span>
    <a href="#conte%c3%bado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>00:00 - Intro</li>
<li>00:41 - Mega Sena da Virada</li>
<li>02:33 - Concorrência</li>
<li>04:11 - Intro a Heroku</li>
<li>04:46 - Deploy antigo</li>
<li>06:24 - Iniciando Tutorial Heroku</li>
<li>06:49 - Setup: Git é Básico!</li>
<li>07:22 - Heroku Login</li>
<li>08:18 - Primeiro Deploy</li>
<li>10:03 - Buildpack? Dockerfile?</li>
<li>10:35 - Entendendo Dynos e Containers</li>
<li>11:30 - Escalando Dynos</li>
<li>13:16 - Entendendo Load Balancer</li>
<li>16:41 - Vendo logs dos Dynos</li>
<li>17:22 - Entendendo Procfiles</li>
<li>18:58 - Mais sobre escalar dynos</li>
<li>20:21 - Entendendo Gerenciamento de Dependências</li>
<li>24:37 - Adicionando nova dependência</li>
<li>26:40 - Adicionando Addons</li>
<li>28:16 - Conectando num shell remoto</li>
<li>29:41 - Configurando com variáveis de ambiente</li>
<li>33:39 - Adicionando Banco de Dados</li>
<li>37:13 - Database Migrations</li>
<li>37:47 - Conectando no Banco de Dados</li>
<li>39:22 - Releases e Rollback</li>
<li>40:34 - Conclusão</li>
</ul>
<h2>Links<span class="hx:absolute hx:-mt-20" id="links"></span>
    <a href="#links" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>Mega da Virada: Loterias Caixa colocam fila em app, e site fica fora do ar (<a href="https://tecnoblog.net/noticias/2021/12/31/mega-da-virada-loterias-caixa-colocam-fila-em-app-e-site-fica-fora-do-ar/"target="_blank" rel="noopener">https://tecnoblog.net/noticias/2021/12/31/mega-da-virada-loterias-caixa-colocam-fila-em-app-e-site-fica-fora-do-ar/</a>)</li>
<li>Getting Started on Heroku with PHP (<a href="https://devcenter.heroku.com/articles/getting-started-with-php#set-up"target="_blank" rel="noopener">https://devcenter.heroku.com/articles/getting-started-with-php#set-up</a>)</li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>Bem vindos ao primeiro video de 2022. O video de hoje vai ser parte 1 de 2 videos. Eu queria falar sobre os famosos 12 fatores pra criar projetos escaláveis modernos. Pra fazer isso eu preciso que todo mundo já tenha visto pelo menos uma vez como é trabalhar com uma plataforma chamada Heroku. Se você já trabalha com Heroku, o que vou falar hoje é o arroz com feijão, mas vocês podem usar pra apresentar a plataforma pra amigos seus que ainda não conhece. Pra todo o resto, esse é o modelo de trabalho que você deveria almejar se nunca usou essa plataforma.</p>
<p>(&hellip;)</p>
<p>Antes de começar deixa eu responder algumas coisas que vi online depois dos últimos vídeos. No caso do exemplo que dei do ingresso.com eu terminei concluindo que se eles não tem controle sobre o sistema de reserva de assentos dos cinemas - que imagino que sejam aplicativos super antigos que não foram feitos pra escala de internet - então deveriam controlar o acesso a esses sistemas usando uma fila virtual. Ninguém acessa diretamente a reserva de assentos, cai primeiro numa página mais fácil de escalar de fila e fica esperando a vez.</p>
<p>Por acaso foi exatamente isso que a Caixa Econômica Federal fez pra Mega Sena da virada que ia sortear mais de 300 milhões de reais. E eles implementaram justamente uma fila virtual pra ver se aguentavam a multidão que ia tentar acessar tudo no último dia. Eu não perco meu tempo apostando em loteria porque é só garantia que vou estar dando dinheiro de graça pros outros, mas quem tentou parece que teve muita dor de cabeça mesmo tendo esse sistema de fila implementado.</p>
<p>Mas é óbvio. Só porque você implementou algum pattern ou arquitetura não existe nenhuma garantia que implementou certo ou que não exista nenhum outro problema que ficou pra trás. Eu não sei como eles fizeram, mas imagino pelo menos 2 problemas. O primeiro e mais óbvio é que o tráfego gerado por uma Mega Sena dessas deve ter sido absurdamente alto. No nível que até uma Amazon da vida sofreria um pouco. Então dou o benefício da dúvida que eles tentaram o máximo mas mesmo assim o tráfego foi ainda maior do que era imaginado.</p>
<p>O segundo problema, que acho que é o mais factível, é que o sistema foi mal feito mesmo. Não me entendam mal. Eu acho que existem sim, bons programadores, com boas capacidades técnicas e boas intenções que tentaram o máximo pra fazer isso funcionar. Porém, a grande maioria dos funcionários públicos que trabalham nos diversos departamentos e sistemas integrados provavelmente não tavam dando a mínima e foram obstáculos pros poucos que tentaram fazer as coisas direito.</p>
<p>Entenda a seguinte verdade: nenhum sistema feito pelo estado jamais vai ser tão bom quanto os melhores sistemas feitos na iniciativa privada. É uma impossibilidade. O motivo é muito simples: não tem concorrência. A mesma coisa vale pra empresas privadas que tem conluio com governos, que passam a ser basicamente empresas públicas com fachada de empresa privada. A maioria dos grandes monopólios só existem porque foram auxiliados pelos governos.</p>
<p>Sem concorrência, pra que eu preciso fazer um sistema melhor? As pessoas têm outra opção? Não, então foda-se, vai ser obrigado a usar o que tem. Mesmo se for uma merda. E eu, que trabalho aqui, vou ser mandado embora se não funcionar? Também não, então foda-se de novo. E vai ser sempre assim. Eu sinto pena dos poucos que trabalham lá que querem fazer as coisas funcionar direito, mas vão sempre esbarrar em um monte de departamento que tá pouco se fodendo e um monte de burocracia que impede melhorias. Mas é isso aí.</p>
<p>Software nunca tá “pronto”. Se o lugar tem mentalidade de &ldquo;terminar&rdquo; o software e não mexer mais, sempre vai ser uma porcaria. O Software precisa estar constantemente atualizado, constantemente consertado, constantemente expandindo e remodelado. O problema em lugares como uma Caixa é que deve ter dezenas de sistemas que tá no esquema &ldquo;tá funcionando? então não mexe!&rdquo; só que eventualmente vai precisar integrar, se comunicar. Mas não tem como, daí vai nascendo dezenas de gambiarras pra fazer as coisas funcionarem ao redor dessas ilhas radioativas. Deixe passar alguns anos assim e você tem a experiência bosta que foi a Mega da virada. É simples assim.</p>
<p>E isso me lembrou de um assunto que eu queria explicar faz algum tempo. No video sobre a história do Ruby on Rails eu mencionei rapidamente sobre Heroku e a metodologia de 12 fatores do Adam Wiggins, um dos co-fundadores do Heroku. Objetivo hoje vai ser dar uma repassada nesses conceitos pra quem nunca viu. Se você é um programador sênior certamente já sabe de tudo isso e certamente já usa no seu dia a dia. Eu não chamaria ninguém que não segue no mínimo os 12 fatores de sênior. E eu digo o mínimo porque isso é o básico do básico e na parte final do video vou até complementar mais algumas coisas que todo mundo já deveria saber.</p>
<p>Os 12 fatores em si vou falar no próximo video. Pra entender os 12 fatores você precisa saber o que é Heroku. Se nunca usou Heroku faça um favor a você mesmo e veja qualquer tutorial básico de como subir um aplicativo nele. Pra um iniciante é uma experiência que muda sua forma de pensar em deploys. Pra quem é das antigas e nunca se atualizou também vai ser um choque. Quando precisava subir um projeto web pra produção, como a gente fazia nos anos 90 e começo dos anos 2000? A gente configurava um servidor remoto Linux, num Virtual Private Server ou VPS como um Linode da vida.</p>
<p>Nesse servidor a gente configurava tudo manualmente via telnet ou SSH. Instalava um banco de dados como MySQL, um servidor web como Apache, alguma linguagem interpretada como Perl ou PHP, copiava os arquivos da aplicação, normalmente feita em Perl ou PHP, via FTP ou hoje em dia SFTP e pronto, tava tudo no ar. Essa stack inclusive tinha um nome, chamado LAMP, que é acrônimo pra Linux, Apache, MySQL e Perl ou PHP. Isso é tecnologia do fim dos anos 90. Entenda, se alguém fizer isso hoje em 2022, você tá parado no tempo mais de 20 anos. Ninguém em sã consciência atualiza código direto no servidor assim via FTP mais.</p>
<p>Eu já tinha dito no episódio do ingresso.com que o jeito errado é fazer tudo rodar numa única máquina, configurar tudo manualmente. Atualizar código direto lá é a pior forma possível de colocar uma aplicação no ar. Agora vamos ver como é o estado da arte, a melhor forma de fazer deploy tanto do ponto de vista de escalabilidade quanto de segurança. E pra isso eu vou seguir um tutorial oficial do próprio Heroku pra vocês verem como é absurdamente simples.</p>
<p>O tutorial começa com o básico. Você obviamente precisa ter uma conta criada no Heroku. Se ainda não fez isso, vai lá depois de assistir o video e cria a conta. Eles tem tutoriais pra quem usa Rails, Node, Java e muito mais, mas já que falei da stack LAMP, vamos ver como se sobe uma aplicação PHP. Vamos assumir que se você é de PHP já tem tudo instalado e obviamente usa Composer pra gerenciar suas dependências.</p>
<p>Se você for de outras linguagens como Python ou Elixir, não importa. Se nunca viu Heroku funcionando não interessa a linguagem, se atenha ao passo a passo e o raciocínio. Qualquer bom programador tem que ser capaz de no mínimo seguir o raciocínio mesmo se for em outra linguagem. Também vamos assumir que você já sabe usar minimamente Git. Se ainda não sabe, assistam meus vídeos sobre Git depois. E vocês se lembram que no video de Conhecimentos Básicos eu falo que saber Git é básico? Pois é, repito, é básico. Não tem como trabalhar em projetos modernos sem saber Git.</p>
<p>Agora, pra usar Heroku precisamos instalar a linha de comando do heroku. Praticamente tudo que vai precisar tem nessa linha de comando. Num archlinux da vida basta fazer um <code>yay -S heroku-cli</code>, num Ubuntu da vida dá pra instalar com Snap. Procure como instalar pra sua distro mas no final você deve ser capaz de abrir um terminal, digitar <code>heroku login</code> e vai abrir um navegador pra logar na conta que acabou de criar. E, claro, sempre habilite autenticação de duas etapas, é o mínimo.</p>
<p>No próximo passo ele vai mandar você fazer o clone de um projetinho de exemplo feito em PHP Symfony, que é um dos muitos frameworks web pra PHP. Symfony tem muita inspiração tanto em Rails quanto Django. Pra projetos novos recomendo usar Laravel, que tem uma comunidade mais ativa e um ecossistema que cresceu bem em torno dele. De qualquer forma, se você sabe Git, clonar um projeto é arroz com feijão. Tudo vai acontecer dentro do diretório desse projeto, então só dar <code>cd</code> pra ele.</p>
<p>Agora vamos subir essa aplicação de exemplo. Pra isso precisamos cadastrar uma nova aplicação no Heroku. Você pode subir quantas aplicações quiser na sua conta e pode começar com opções gratuitas. Depois pode transferir a propriedade das aplicações que subiu pra outra conta, como do seu cliente. Pra cadastrar é simples, do diretório do projeto, num terminal, só usar a linha de comando que instalamos e fazer <code>heroku create</code>. Se não disser que nome quer, ele vai inventar um aleatório. O nome em si não interessa tanto porque depois você vai registrar um domínio de verdade e apontar pra esse nome, o CNAME, então o usuário final mesmo nunca vai ver esse nome. Mas vai servir pra gente testar no domínio do Heroku.</p>
<p>Uma vez a aplicação registrada na sua conta, agora é só subir o código. Quando rodou o comando anterior, ele também criou um branch remoto no Git do seu projeto. Então agora é só subir o que tem na branch principal <code>main</code> que antigamente se chamava <code>master</code> pra esse remote chamado <code>heroku</code>. Relembrando, todos os remotes ficam no arquivo <code>.git/config</code>, dá um <code>cat</code> nele pra ver o conteúdo, olha o remote lá. Agora é só fazer <code>git push heroku main</code>. Olha o que vai acontecer no seu terminal.</p>
<p>Se você ainda não entendeu, o Heroku criou um repositório Git remoto associado com sua nova aplicação, esse é o remote. É como se fosse um projeto novo no GitHub que você vai dar push. Mas o Git tem uma funcionalidade que você pode configurar que ele detecte quando você faz um <code>push</code> e rodar algum script. No caso, o Heroku detectou que tá subindo código PHP e por isso vai automaticamente instalar a buildpack pra ter as ferramentas de PHP que vai precisar como o próprio interpretador PHP, o Composer, Apache, NGINX e tudo mais. O que o Heroku chama de buildpack é mais ou menos o que você chamaria de Dockerfile.</p>
<p>Aqui vale um adendo se você já usa Docker. Por que o Heroku reinventou a roda com esses buildpacks em vez de usar Dockerfiles? Na verdade é o contrário: o Heroku precede a invenção do Docker. Na realidade, muito do Docker foi inspirado no que o Heroku fez anos antes dele. O Heroku foi lançado por volta de 2008, o Docker foi lançado só em 2013. O objetivo da vida do Docker e tudo que saiu em torno de containers como Docker Compose, Docker Machine, Dokku, até Kubernetes, é conseguir imitar o que o Heroku fez antes de todo mundo.</p>
<p>Vamos considerar o que tá acontecendo nesse ponto. O comando <code>git push</code> que fizemos tá mandando o Heroku fazer o equivalente a criar uma imagem de Docker, é semelhante a um <code>docker build</code>. Nos servidores deles rodam diversos containers, que eles chamam de <code>dynos</code>. Tem de diversos tamanhos, a versão gratuita são dynos de 512 megabytes de RAM, se não me engano com uns 4 cores ou núcleos virtuais fraquinhos. Se precisar, dá pra subir pra versões de 1 giga até 14 giga de RAM se precisar muito.</p>
<p>Mas cuidado, a grande maioria das aplicações deveria conseguir rodar suficientemente bem em 512 mega de RAM, se precisa de mais que isso precisa ver se não tá com vazamento de memória, ou você programou muito porcamente e tá enchendo a memória de lixo. Trabalhar em um ambiente mais restrito do que sua máquina local é uma boa prática. Qualquer notebook hoje tem 8 giga ou mais de RAM e você muitas vezes nem percebe que sua aplicação tá usando muito mais memória do que deveria.</p>
<p>Seguindo o tutorial a próxima coisa que ele manda fazer é escalar a aplicação com o comando <code>heroku ps:scale web=1</code> que basicamente manda o Heroku subir sua aplicação num único dyno. Se mudar pra web igual a 2, ele vai subir dois dynos. O que significa isso? Vamos fazer uma conta de padeiro. Eu disse que o dyno gratuito tem uns 4 cores virtuais. Se eu configurar o apache pra pendurar um fork de processo por núcleo significa que consigo ter até 4 requisições simultâneas. Se subir 2 dynos eu posso ter até 8 requisições simultâneas. Simultâneo significa exatamente no mesmo instante. Nessa conta de padeiro estou considerando 1 requisição por processo, sem considerar opções de threads ou I/O assíncrono.</p>
<p>Digamos que a aplicação leve 100 milissegundos pra responder uma requisição. Então em 1 segundo daria pra responder até 10 requisições. Com 1 dyno então seria teoricamente possível responder até 40 requisições por segundo e com 2 dynos até 80 requisições por segundo, entenderam? Isso é teórico porque estou assumindo que cada core conseguiria responder 10 requisições todo segundo, mas depende se durante o processamento de cada requisição se não bloqueia algum recurso que outra requisição pode precisar, como banco de dados e coisas assim, por isso o tempo real pode variar bastante.</p>
<p>Containers, como Docker, como Dynos de Heroku, não são máquinas virtuais. Eu explico sobre isso no meu episódio sobre Devops. Se você não sabe a diferença recomendo que assista depois. Mas na prática só entenda que é uma forma de particionar os recursos da sua máquina e cada programa rodar isoladamente achando que está sozinho nessa máquina. Assim é possível particionar uma máquina real grandona em diversos containers menores, dividir os recursos, e te cobrar de uma forma mais fácil.</p>
<p>Mais do que isso, o Heroku já deixa muita coisa de infraestrutura preparada pra você. Por exemplo, eu expliquei que tem esse comando <code>ps:scale</code> que permite subir a imagem da sua aplicação em múltiplos dynos. Significa que você tem vários servidores web de pé ao mesmo tempo. Agora, quando um usuário digitar a URL pra sua aplicação ele não cai direto no servidor web da sua aplicação e sim num balanceador de carga, ou load balancer, proprietário do Heroku, que vai pegar as requisições e distribuir nos dynos que você tem de pé provavelmente usando uma estratégia como round robin.</p>
<p>Se você é iniciante isso pode parecer estranho. Quando sobe um processo de servidor web na sua máquina local, seja nginx, seja apache, seja um webpy de python, tomcat de java, puma de rails, vai ser um processo pendurado em uma porta. Toda vez que no seu navegador você carrega <code>localhost:3000</code> ele vai direto pra quem responde nessa porta 3000 e é isso. Mas quando você sobe vários containers, cada um com seu próprio servidor na porta 3000, cada um dos containers tem um IP interno próprio. Faz de conta, 172.16.0.10 e 172.16.0.11.</p>
<p>No seu navegador você não pode mais usar localhost porque não tem mais ninguém no ip local 127.0.0.1 na porta 3000, que seria o localhost. Quando você sobe containers de Docker localmente, ele cria uma rede virtual local. Cada container de Docker que sobe ganha um IP virtual privado e nele que o servidor web da sua aplicação vai se pendurar na porta 3000 por exemplo. Se achou confuso recomendo que estude e treine Docker na sua máquina local, em particular usando Docker Compose pra orquestar cada serviço num container separado.</p>
<p>Você pode naturalmente digitar direto o ip privado de um dos containers como 172.16.0.10:3000 e aí vai carregar sempre só desse container, mas o segundo container vai ficar lá parado sem trabalhar. Pra conseguir acessar os dois containers, em vez de acessar direto, pode subir um terceiro container, com um load balancer como o NGINX ou HAProxy ou vários outros. Nesse load balancer você configuraria uma regra dizendo, toda vez que alguém mandar uma requisição na porta 80 eu envio pra porta 3000 de um dos dois containers que tenho cadastrado.</p>
<p>Isso que se chama um proxy reverso. Digamos que o container do load balancer subiu com IP virtual 172.16.0.13. Agora você pode ir no navegador e digitar <code>172.16.0.13</code> que por default vai conectar na porta 80. O load balancer vai pegar essa requisição e mandar pra um dos dois containers web respondendo nas suas portas 3000. Pro navegador é transparente. Ele não tem idéia de quantos containers web tem por baixo, nem em que portas eles respondem de verdade, nem que IPs tem. Load balancer é tanto uma forma de distribuir requisições pra mais servidores web quanto anonimizar os IPs dos servidores para que os usuários não saibam quem são.</p>
<p>O objetivo do episódio não é ensinar redes, mas como tem muito iniciante assistindo achei melhor pelo menos dar o resumo do resumo. Pra saber mais procurem artigos sobre load balancer, em particular tentem fazer exatamente esse cenário que eu falei: subir com Docker Compose 2 ou mais containers com uma aplicação web qualquer e outro container com NGINX configurado como load balancer e veja na prática suas requisições sendo distribuídas pelos seus containers. Tem dezenas de tutoriais que você acha no Google pra fazer isso, só não ter preguiça de procurar, pra treinar qualquer um serve.</p>
<p>Falando nisso, voltando pro tutorial do Heroku, o próximo passo é ver se sua aplicação subiu direito e está respondendo como deveria. Pra isso você precisa conseguir ver os logs em tempo real. Se fosse uma aplicação local era só fazer tipo um <code>tail log/application.log</code> pra ficar monitorando o que entrar no log em tempo real. Tail é inglês pra rabo, e como o nome diz a gente fica seguindo o rabo, ou seja, o final do arquivo. Pro Heroku é parecido mas só usar a linha de comando deles fazendo <code>heroku logs --tail</code>. Lógico, o <code>--tail</code> é opcional, mas se usar vai manter o log aberto e tudo que for sendo logado vai aparecendo no seu terminal. De novo, se você tá acostumado a usar Linux isso é algo super comum.</p>
<p>Fizemos o clone da tal aplicação de exemplo mas nem vimos o que tem nele. O principal é entender que toda aplicação que sobe no Heroku precisa ter pelo menos um arquivo na raíz do projeto chamado <code>Procfile</code>. Nele definimos que no container de tipo <code>web</code> vai executar o binário executável do apache apontando pra pasta <code>web</code>. Isso é específico de cada framework. Se fosse um Rails o executável seria <code>bin/rails server</code> e assim por diante. É assim que o Heroku sabe o que é pra executar dentro do container. Se fosse um <code>Dockerfile</code> seria o equivalente ao parâmetro <code>CMD</code>.</p>
<p>Não vou mostrar isso hoje, mas além de web existem outros tipos de containers que você pode declarar como <code>queue</code> ou <code>job</code> se quiser montar imagens que sobem workers pra uma fila assíncrona, por exemplo. Eu expliquei um pouco sobre filas assíncronas no episódio do ingresso.com e no de concorrência e paralelismo. O importante é saber que Heroku não sobe só aplicações web. Além disso, o formato de arquivo <code>Procfile</code> meio que virou universal. No mundo Rails você pode usar uma ferramenta como o <code>foreman</code> que vai ler esse arquivo e localmente subir sua aplicação pra simular como rodaria no Heroku.</p>
<p>Você pode usar o foreman escrito em Ruby pra rodar localmente sua aplicação mesmo se for escrita em PHP, ou usar o node-foreman que faz a mesma coisa ou o goreman ou forego que são outros clones de foreman escritos em Go. Tanto faz no que é escrito porque só vai ler o que tem no arquivo <code>Procfile</code> e executar o que está lá. É uma boa prática testar localmente antes de subir pro Heroku ou outra plataforma que também suporte Procfiles.</p>
<p>O próximo passo do tutorial foi o que expliquei agora pouco. Como escalar sua aplicação subindo mais containers web. Só usar o comando <code>heroku ps:scale</code> e fazer <code>web=2</code> ou mais. Se quiser desligar a aplicação, tirar da web, só fazer igual a zero que vai desligar todos os containers. Você pode aumentar ou diminuir o número de containers manualmente com esse comando ou contratar um serviço de auto-escala que usa algumas estratégias pra fazer esse ajuste automaticamente dependendo da carga que sua aplicação tá recebendo.</p>
<p>Dizendo assim pode parecer que escalar sua aplicação é algo tão simples quanto rodar esse comando e subir 100 dynos de uma só vez em horário de pico. Mas isso não é uma bala de prata. Lembre-se que se subir 100 dynos vai precisar que recursos embaixo dele, como seu banco de dados, tanto consiga aguentar esse tanto de conexões simultâneas e ter CPU e RAM suficientes pra processar tanta coisa de uma só vez.  Escalabilidade nunca é automática, você precisa estar preparado pra isso. Na prática, essa funcionalidade é mais pra você economizar custos. Digamos que no máximo, os recursos que instalou suportariam 100 dynos de pé ao mesmo tempo. Mas de madrugada seu tráfego cai um monte e só 10 dynos seriam suficientes. Então você pode fazer um script que derruba 90 dynos de madrugada pra economizar custos e de manhã cedo sobe 90 dynos novos pra aguentar o tráfego do dia.</p>
<p>Agora vamos falar de dependências. Se você programa em Rails, obrigatoriamente usa a ferramenta Bundler pra gerenciar dependências. Toda nova biblioteca que precisa adicionar no seu projeto, primeiro você declara no arquivo <code>Gemfile</code> e instala a tal biblioteca com o comando <code>bundle update</code>. Você jamais vai no site da biblioteca, baixa um zip e descompacta dentro do diretório do seu projeto, isso seria uma barbárie e algo totalmente inaceitável numa sociedade civilizada moderna em 2022.</p>
<p>Se você programa em Javascript, Node.js, obviamente adiciona todas as suas dependências com o comando <code>npm install</code> e a opção <code>--save-dev</code> por exemplo. Ou se usa <code>yarn</code> usa o comando <code>yarn add</code>. Em ambos os casos isso vai atualizar o arquivo <code>package.json</code>, que todo projeto civilizado de Javascript tem obrigação de ter.</p>
<p>Se você é de Java, certamente conhece Maven e usa a ferramenta Gradle, e tudo vai estar declarado no arquivo <code>build.gradle</code> ou <code>pom.xml</code>. Se você é de Python, eu sei que é chato e ainda não é uma solução perfeita, mas deveria estar usando <code>pip</code> e todas as dependências deveriam estar no arquivo <code>requirements.txt</code>.</p>
<p>Se é de Go, só recentemente começaram a ficar mais civilizados e agora fazendo <code>go mod init</code> você cria um arquivo <code>go.mod</code> que é onde se declara dependências. Daí rodando o comando <code>go mod tidy</code> vai baixar as dependências que precisa pra compilar e executar sua aplicação. O Rust sempre veio com o utilitário Cargo e você sempre tem um arquivo <code>Cargo.toml</code> que declara os crates, que é como povo de Rust chama suas bibliotecas.</p>
<p>Entenderam? Não importa que linguagem você usa, toda linguagem civilizada tem um gerenciador de dependências padrão, sempre tem um arquivo onde se declara essas dependências, e você nunca, jamais, deve baixar bibliotecas manualmente e jogar dentro do seu repositório. Mais do que isso, todo gerenciador competente costuma ter um arquivo de tranca, de lock. Por exemplo, projetos de Javascript tem o <code>package.json</code> onde você declarou as bibliotecas e versões que queria, mas quando o <code>npm</code> ou <code>yarn</code> realmente baixam e instalam ele gera um outro arquivo chamado <code>package-lock.json</code> que você nunca deve editar manualmente, que declara exatamente quais bibliotecas e exatamente quais versões baixou.</p>
<p>Esse arquivo de lock é especialmente importante porque quando outro desenvolvedor dá pull e clona o repositório do projeto ou quando você dá <code>git push</code> pro Heroku, ele roda o <code>npm install</code> e vai baixar exatamente o que estiver nesse arquivo de lock. 1.0.0 é diferente de 1.0.2. Isso é importante pra todo mundo baixar exatamente a mesma versão de tudo. Basta uma biblioteca que era pra ser, faz de conta, versão 1.0.1 e no servidor baixar a 1.0.2 que um bug pode ser introduzido sem ninguém saber. Gerenciamento de dependências é uma ciência exata. Ela só fica caótica quando você não segue essas regras.</p>
<p>Essa longa explicação foi só pra pular pro próximo passo do tutorial onde ele rapidamente explica que nosso projeto PHP de exemplo usa um gerenciador de dependências chamado Composer. E como esperado, existe um arquivo chamado <code>composer.json</code> na raiz do projeto junto com um arquivo <code>composer.lock</code>. No caso do PHP, no arquivo <code>web/index.php</code> tem uma linha com o comando <code>require</code> carregando um <code>autoload.php</code> que é quem se responsabiliza por carregar as bibliotecas declaradas no <code>composer.json</code>.</p>
<p>Na sua máquina local, se fizer <code>compose update</code> vai baixar todas as dependências localmente e agora pode rodar o projeto na sua máquina. E quando fazemos <code>git push</code> pro Heroku os scripts no buildpack de PHP vão procurar o arquivo <code>composer.json</code> e se achar, vai rodar o <code>compose update</code> pra montar a imagem. E é por isso que você precisa gerenciar as dependências dessa forma, pra que outro desenvolvedor não tenha problemas quando baixar na máquina dele, e pra quando subir num Heroku ou máquina de produção sabemos que não vai faltar nenhuma dependência. Isso é crucial pra vivermos numa sociedade civilizada. Imagina como era na época da barbárie quando não tínhamos ferramentas como essas.</p>
<p>Vamos simular que esse projeto ainda está em desenvolvimento. Vamos adicionar uma nova funcionalidade. Pra isso começamos usando o comando <code>compose require</code> que vai declarar e puxar a biblioteca <code>cowsayphp</code> que é um programinha besta que só desenha uma vaca com caracteres ASCII. Depois disso só rodar <code>compose update</code> pra garantir que foi baixada e instalada. Se abrirmos o arquivo <code>composer.json</code> veja que ela foi declarada automaticamente pelo Composer.</p>
<p>Agora, no <code>index.php</code> podemos criar uma rota chamada <code>/cowsay</code> que vai usar essa biblioteca <code>Cowsayphp</code> e mandar ela desenhar a vaca dizendo &ldquo;Cool beans&rdquo;. Vamos só copiar e colar esse trecho do tutorial no nosso projeto, e pronto. Agora precisamos adicionar tudo isso que modificamos no repositório Git. Pra isso basta fazer <code>git add .</code>. Lembrem-se que nos vídeos de Git eu falo pra tomar cuidado pra não adicionar coisas que não precisam. Por acaso esse projeto tem um <code>.gitignore</code> e se dermos <code>git status</code> podemos ver que só vamos adicionar os arquivos do Composer e o <code>index.php</code> que modificamos. Na dúvida sempre rode <code>git status</code> antes de dar <code>git commit</code>.</p>
<p>Finalizamos com o <code>git commit -m</code> adicionando uma mensagem descritiva da modificação que fizemos e executamos um novo <code>git push heroku main</code> pra subir a modificação e gerar uma nova imagem. Olhem o Heroku recebendo, rodando <code>compose update</code> que vai baixar a biblioteca que mandamos, vai regerar a imagem, daí ele sozinho vai desligar os dynos que estavam rodando antes e subir novos dynos com a imagem nova.</p>
<p>Se usarmos o comando <code>heroku open cowsay</code> ele vai abrir o navegador pra gente, já apontando pra URL da aplicação /cowsay. Não precisa desse comando, você podia abrir o navegador e digitar a URL manualmente, mas assim é mais rápido. Demora um segundo pra abrir porque o Heroku está atualizando os dynos, mas voilá, tá funcionando. E é assim que subimos versões novas da nossa aplicação. Lembram deploy contínuo que eu falei? É assim que faz.</p>
<p>Além de toda essa infraestrutura de containers, load balancers, facilidade de subir atualizações usando Git, o Heroku tem parceria com dezenas de empresas que oferecem serviços que são úteis pras nossas aplicações. No tutorial, o próximo passo é justamente instalar uma delas, o addon pra ferramenta chamada Papertrail. Addon é sinônimo de plugin e Papertrail é um serviço que recebe os logs da nossa aplicação e oferece uma interface web que podemos monitorar e, principalmente, fazer pesquisas.</p>
<p>A maioria dos addons oferece uma versão gratuita pra gente testar e pra aplicações pequenas costuma ser suficiente também. Pra instalar basta ir no terminal e fazer <code>heroku addons:create papertrail</code>. Esses comandos têm várias opções, por exemplo pra já instalar com um plano pago mais parrudo. Sempre leia a documentação de cada addon antes de sair instalando. De qualquer forma, com o comando <code>heroku addons</code> podemos listar quais já temos instalado e veja como o Papertrail já aparece.</p>
<p>Pra abrir a interface web no seu navegador, via terminal podemos rodar <code>heroku addons:open papertrail</code> e voilá, agora temos como fazer pesquisas nos nossos logs. Isso é mais importante se considerarmos que podemos ter mais de um dyno rodando ao mesmo tempo e no Papertrail vai concentrar os logs de todos os dynos ativos. Assim podemos pesquisar os logs de todos ao mesmo tempo. Por acaso esse é um addon que eu recomendo sempre instalar em toda aplicação. Papertrail e também o Rollbar que você pode configurar pra te notificar por e-mail se alguma mensagem de erro crítico aparecer no log.</p>
<p>O próximo passo do tutorial é um pouco mais avançado e eu não recomendo que você use isso se não for um desenvolvedor mais experiente. A linha de comando do Heroku permite abrir um container novo pra onde ele vai abrir uma conexão SSH segura. Dentro dele você pode rodar o shell interativo do seu framework como o <code>php -a</code> no caso de PHP ou o <code>rails console</code> no caso de Rails ou simplesmente abrir um shell bash caso queira checar alguma coisa no nível do sistema operacional. Tudo que carrega nos containers de web também carrega nesse container de console, então pode ser bom pra debugar algum bug que não acontece na máquina de desenvolvimento mas aparece quando sobe a aplicação no Heroku. Use com cuidado, mas essa opção já salvou minha vida diversas vezes.</p>
<p>Esclarecendo, esse comando não abre SSH direto pra um dos containers web rodando. Ele abre um novo container, com a mesma imagem que sobe nos containers web, só isso. A vantagem é que de lá você tem os mesmos acessos ao banco de dados se precisar muito rodar alguma query de emergência ou algo assim. E justamente por isso eu falo pra tomar muito cuidado, porque o que você rodar no banco vai ser permanente. Quando desconecta do shell interativo, esse container é destruído, por isso não crie ou baixe arquivos pra lá porque esse container vai sumir tão logo você se desconecte dele. É especificamente pra tarefas administrativas especiais.</p>
<p>Outro conceito que pra iniciantes pode não ser muito óbvio são variáveis de ambiente. Por exemplo, imagino que a maioria que usa Linux no mínimo já lidou com variáveis como <code>PATH</code> num arquivo local como <code>.profile</code> ou <code>.bashrc</code> da vida. Você faz alguma coisa como <code>export ABC=blabla</code>. Isso declara uma variável global na sua sessão e você pode ver o conteúdo da variável fazendo <code>echo $ABC</code> e vai imprimir o <code>blabla</code> no seu terminal.</p>
<p>Pois bem, é considerado uma boa prática declarar configurações assim como variáveis de ambiente, particularmente em containers. Se você já viu um arquivo <code>.env</code> na raíz do seu projeto, ele serve pra declarar e simular variáveis de ambiente do projeto. De novo, a gente inventou isso no Rails e todo framework web meio que adotou a mesma funcionalidade. Fazemos a configuração num arquivo pra cada desenvolvedor não precisar manualmente ficar escrevendo um monte de <code>exports</code> no seu profile local de Bash. Além disso é boa prática ter um arquivo como <code>.env.example</code> que quando clonamos o projeto fazemos uma cópia dele pra <code>.env</code>, assim não precisamos adivinhar quais variáveis existem pra usar.</p>
<p>E mais importante, é boa prática colocar <code>.env</code> no arquivo <code>.gitignore</code> pra nunca entrar no repositório Git. Isso porque os frameworks que suportam essa convenção, se existir o arquivo <code>.env</code> ele tem prioridade sobre as variáveis de ambiente de verdade configuradas no seu sistema operacional. Mas quando subimos pra produção não queremos que a aplicação carregue desse arquivo e sim das variáveis de verdade no sistema operacional. Então sempre se lembre dessa regra: <code>.env</code> sempre declarado no <code>.gitignore</code>.</p>
<p>Entendido isso, localmente na sua máquina de desenvolvimento você edita o que precisa no arquivo <code>.env</code> que só existe na sua máquina, mas em produção configura variáveis de verdade. No Heroku fazemos isso com o comando <code>heroku config:set</code>. Vamos fazer um exemplo pra ilustrar isso. Naquele arquivo <code>index.php</code> vamos adicionar mais uma rota, no caso substituir a rota raiz do site. Vamos repetir a palavra &ldquo;Hello&rdquo; X vezes, e essas X vezes vai estar declarado na variável de ambiente chamada <code>TIMES</code>. Pra ler essa variável, em PHP, se usa a função <code>getenv</code>.</p>
<p>Agora, não temos como editar um <code>export</code> no arquivo de profile de um dyno, pra isso usamos a linha de comando <code>heroku config:set TIMES=20</code>. Pra listar todas as variáveis que existem só usar o comando <code>heroku config</code>. Olhem como tem uma variável do Papertrail que foi criado pela linha de comando que usamos pra adicionar o addon, esse é o token secreto de acesso. Não tem problema ter coisas como tokens e senhas em variáveis de ambiente, porque elas só existem dentro do container. Se alguém conseguiu ver esse token é porque ganhou acesso ao seu container de produção e seu problema é bem maior do que só ter o token exposto, você está com tudo exposto. Ninguém jamais deve ter acesso direto aos containers.</p>
<p>E pra reforçar, é exatamente por isso que nunca se deve adicionar arquivos como <code>.env</code> no repositório Git, porque normalmente guardamos coisas como senhas e tokens em variáveis de ambiente, e esse tipo de informação, jamais, sob nenhuma hipótese, pode aparecer dentro de um repositório Git. Nenhuma credencial ou segredo jamais deve estar no código fonte do projeto. Tem que ser um tosco de proporções bíblicas pra pensar em colocar segredos num versionador de código que todo mundo tem acesso.</p>
<p>E pronto. Agora podemos fazer <code>git add .</code>, <code>git commit -m</code> e uma mensagem e finalmente <code>git push heroku main</code> de novo. Ele vai criar uma nova imagem com as modificações que acabamos de fazer e se abrirmos o navegador com <code>heroku open</code>, esperamos um segundo pro Heroku derrubar os dynos e subir com a imagem nova e, voilá, veja &ldquo;Hello&rdquo; repetido 20 vezes.</p>
<p>E como mencionamos “banco de dados” algumas vezes, o último passo do tutorial é justamente adicionar Postgres na nossa aplicação. Assim como o Papertrail, Postgres é um addon que tem diversos tamanhos e preços e você deve checar as opções antes de adicionar. Isso porque se pegar um muito pequeno e precisar dar upgrade, o processo não é simples e muito menos trivial. Leia a documentação do Heroku sobre isso. Uma das melhores coisas do Heroku é justamente o serviço de banco de dados, que é um dos melhores que existem, mas você precisa ter um mínimo de noção do que tá fazendo. Eles não fazem mágica nem são à prova de idiotas.</p>
<p>Avisos dados, pra adicionar a versão gratuita menor, que se chama <code>hobby-dev</code>, basta digitar no terminal <code>heroku addons:create heroku-postgresql:hobby-dev</code>. Agora vamos adicionar uma funcionalidade no nosso projeto PHP pra conectar no banco e listar o conteúdo de uma tabela em HTML, o arroz com feijão só. Pra isso começamos usando o Composer pra adicionar a biblioteca de PDO que é PHP Data Objects. Se você é de Java ou .NET da vida, é a mesma coisa que um DAO da vida.</p>
<p>Fazemos <code>compose require csanquer/pdo-service-provider=~1.1dev</code> que vai baixar uma versão compatível mas não necessariamente exata com 1.1dev dessa biblioteca, lembram? Por isso precisamos ter um arquivo de lock que vai registrar a versão exata que ele achou e baixou. Rodamos um <code>compose update</code> pra garantir que baixou tudo e agora podemos alterar o arquivo <code>index.php</code> de novo pra configurar o acesso ao banco de dados. Vamos copiar e colocar do tutorial.</p>
<p>O importante é saber que é uma convenção do Heroku ter a URL de acesso ao banco declarado numa variável de ambiente chamada <code>DATABASE_URL</code>. Podemos ver ela usando o comando <code>heroku config</code> e olha só. E esse trecho de código que copiamos e colamos é pra pegar o conteúdo dessa variável com a função <code>getenv</code> e parsear os diversos componentes dessa string e passar pra biblioteca de PDO configurar coisas como username, password, host, porta e path pra conseguir conectar no novo banco de dados Postgres. E simples assim, subimos um banco de dados seguro e funcional na nossa infraestrutura.</p>
<p>Agora vamos criar uma nova rota lá embaixo chamada &lsquo;/db&rsquo;. Ele vai usar esse PDO pra mandar uma query pro banco e fazer um loop com <code>while</code> pra ir montando um array com todos os nomes que voltaram da query. E pra montar o HTML, o framework Symfony oferece um sistema de template chamado Twig. Passamos o array de nomes pro template chamado de <code>database.twig</code>.</p>
<p>Então precisamos criar um novo arquivo <code>views/database.twig</code> e vamos copiar e colar o template do tutorial nele. Ele vai pegar cada nome que veio no array que passamos pra ele e montar uma lista em HTML, nada de mais, arroz com feijão. Isso tudo feito, vamos adicionar as modificações no Git com <code>git add .</code>. Com <code>git status</code> podemos ver que modificamos os arquivos do Composer, o <code>index.php</code> e criamos um arquivo novo <code>database.twig</code>. Está correto então podemos fazer <code>git commit -m</code> com uma mensagem descritiva e finalmente <code>git push heroku main</code> pra mandar as modificações pro Heroku.</p>
<p>Espero que a essa altura você já esteja acostumado com o processo de criar uma nova funcionalidade, adicionar no Git, mandar pro Heroku e conseguir testar de verdade. O Heroku recebe a modificação, monta uma nova imagem, derruba os dynos que estavam rodando e sobe de novo com a nova imagem. Só tem um probleminha.</p>
<p>Note que criamos um novo banco de dados, mas ele está vazio. Em qualquer framework web moderno existe um recurso chamado Migrations, mais uma coisa que nasceu no Ruby on Rails e todos os outros frameworks imitaram, onde criamos scripts que vão criar as tabelas e índices que precisamos, caso já não existam, e também já pré-cadastram coisas como conta de administrador, se precisar. Isso tudo fica em scripts que declaramos pro Heroku executar na próxima vez que subir dynos novos. Procure a documentação do seu framework e aprenda sobre Migrations porque é super importante.</p>
<p>Como isso é só um tutorial simples, tentando ser didático pra ensinar sobre o Heroku e não sobre seu framework, ele pula qualquer coisa sobre Migrations e manda você abrir um novo container de shell, lembra? Que nem abrimos o bash remoto? Só que no caso agora é abrindo o console do Postgresql que é o <code>psql</code>, então vamos fazer isso usando o comando <code>heroku pg:psql</code>.</p>
<p>Agora abriu o console remoto lá no servidor do Heroku já conectado no nosso novo banco de dados. Só precisamos fazer isso uma vez, mas vamos copiar do tutorial o comando pra criar a tabela chamada test_table e em seguida vamos dar insert em alguns valores aleatórios como esse &lsquo;hello database&rsquo; e também um &lsquo;hello world&rsquo;, por que não? Pronto, pra sair do console do Postgres é só digitar <code>\q</code>.</p>
<p>Podemos abrir o navegador de novo usando o comando <code>heroku open db</code> que é a nova rota que criamos e voilá, veja que conseguiu executar a query no banco, trazer as duas linhas que acabamos de inserir, e montar o HTML com elas. E pronto, nesse estágio fomos capazes de criar uma nova aplicação no Heroku, ir subindo novas funcionalidades à medida que fomos codificando, configurando o ambiente e cadastrando novos addons como o Papertrail e Postgres. Esse é o básico do básico sobre Heroku que todo mundo deveria saber. Mas o Heroku faz bem mais que isso. No fim do tutorial, no último passo, ele dá links pra outros artigos como o &ldquo;How Heroku Works&rdquo; que vai começar a explicar muito mais detalhes sobre o ciclo de vida de uma aplicação em dynos.</p>
<p>Por exemplo, lembram que durante o tutorial repetimos <code>git push heroku</code> várias vezes? Todas as vezes ele cria uma nova imagem com as últimas modificações, derruba os dynos rodando a versão antiga e sobe tudo de novo com a imagem nova. O que eu não expliquei é que as imagens antigas continuam disponíveis se precisar. O Heroku chama isso de releases ou lançamentos. Se rodar o comando <code>heroku releases</code> podemos ver tudo que subimos desde a primeira vez. Ele vai numerando cada release e acho que você pode cadastrar tags nelas também pra facilitar achar depois.</p>
<p>A vantagem mais imediata é: digamos que você subiu uma nova versão que não testou muito bem e, surpresa, começou a dar pau em produção. Em vez de ficar tentando consertar na tentativa e erro durante o sufoco, podemos rapidamente dar rollback pra última release que sabemos que tava funcionando, por exemplo <code>heroku releases:rollback v10</code>. A versão 11 que acabamos de subir dá pau, mas a versão 10 funcionava, então voltamos pra ela. É o tipo de coisa que você nunca ia conseguir fazer rápido e organizado assim se estivesse atualizando código fonte manualmente usando FTP. E é por isso que você paga mais por isso também.</p>
<p>Eu normalmente não gosto de fazer videos de tutoriais de ferramentas específicas porque elas envelhecem muito rápido. Mas assim como no caso de Git, o Heroku existe faz mais de dez anos e continua funcionando basicamente do mesmo jeito. Esse mesmo tutorial que funcionaria em 2010 continua funcionando em 2022 e provavelmente vai continuar funcionando por muitos mais anos do mesmo jeito e toda nova solução de devops que aparece é pra tentar se aproximar desse ideal de fluxo de trabalho. Por isso, mesmo que você não use no seu trabalho atual, acho muito importante que se familiarize com esse jeito de trabalhar.</p>
<p>No próximo episódio vamos pegar o que aprendemos hoje pra discutir um pouco mais sobre arquitetura e gerenciamento de projetos ágeis de verdade. Se ficaram com dúvidas mandem nos comentários abaixo, se curtiram o video deixem um joinha, assinem o canal e compartilhem o video com seus amigos. A gente se vê, até mais!</p>
]]></content:encoded><category>devops</category><category>cloud</category><category>desenvolvimento-web</category><category>akitando</category></item><item><title>[Akitando] #48 - Entendendo "Devops" para Iniciantes em Programação (Parte 2) | Série "Começando aos 40"</title><link>https://akitaonrails.github.io/2019/04/17/akitando-48-entendendo-devops-para-iniciantes-em-programacao-parte-2-serie-comecando-aos-40/</link><guid isPermaLink="true">https://akitaonrails.github.io/2019/04/17/akitando-48-entendendo-devops-para-iniciantes-em-programacao-parte-2-serie-comecando-aos-40/</guid><pubDate>Wed, 17 Apr 2019 17:00:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/mcwnQVAn0pw"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;Hoje finalmente vamos usar tudo que aprendemos até agora pra explicar as diferenças entre hypervisors e containers, e agora sim, falar um pouco mais de Docker e Kubernetes.&lt;/p&gt;
&lt;p&gt;Precisamos explicar o que foi essa mudança no mundo de sysadmins de ter que lidar com hardware pra um mundo onde hardware essencialmente se tornou software pra muitos casos, especialmente em Web e como empresas como a Amazon AWS e Heroku ajudaram a mudar inclusive a forma como programamos.&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/mcwnQVAn0pw"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Hoje finalmente vamos usar tudo que aprendemos até agora pra explicar as diferenças entre hypervisors e containers, e agora sim, falar um pouco mais de Docker e Kubernetes.</p>
<p>Precisamos explicar o que foi essa mudança no mundo de sysadmins de ter que lidar com hardware pra um mundo onde hardware essencialmente se tornou software pra muitos casos, especialmente em Web e como empresas como a Amazon AWS e Heroku ajudaram a mudar inclusive a forma como programamos.</p>
<p>Links:</p>
<ul>
<li>Goodbye Docker and Thanks for all the Fish (<a href="https://technodrone.blogspot.com/2019/02/goodbye-docker-and-thanks-for-all-fish.html"target="_blank" rel="noopener">https://technodrone.blogspot.com/2019/02/goodbye-docker-and-thanks-for-all-fish.html</a>)</li>
<li>The Twelve-Factor App (<a href="https://12factor.net"target="_blank" rel="noopener">https://12factor.net</a>)</li>
</ul>
<p>=== Script</p>
<p>Olá pessoal, Fabio Akita</p>
<p>Este é o décimo terceiro episódio da série Começando aos 40, e parte 2 do tema de devops. Recapitulando, semana passada eu expliquei como saímos das hospedagens compartilhadas e colocations dos anos 90 para as VPSs e opções baseadas em tecnologias de virtualização. Entendemos as diferenças de full virtualization, paravirtualização e um pouco de containers e hoje vamos entrar na melhor década do século XXI até agora pra gente. Pra muita gente a revolução na área veio com Docker e pra outras pessoas esse período pode estar chegando ao seu fim. Vamos ver como!</p>
<p>(&hellip;)</p>
<p>Como eu disse no episódio anterior, o mundo de infraestrutura não estava preparado para o boom que começou a acelerar principalmente a partir de 2005. Unicórnios estavam se proliferando, crescendo em demanda muito rápido e nenhum data center estava preparado para eles. Aliás, no episódio anterior eu mencionei VPS mas não expliquei direito o que são. Virtual Private Servers, basicamente são serviços onde você escolhe um tipo de máquina virtual, quanto de CPU, quanto de memória, de disco e que sistema operacional e paga uma mensalidade por máquina. Na época eu gostava de usar os da Linode mas tinha vários outros. Era como alugar uma máquina num data center, só que virtual, então mais barato e mais fácil.</p>
<p>A própria Amazon, no começo dos anos 2000 estava passando por problemas de escala. Cada novo projeto levava 3 meses só pra configurar a infraestrutura, as máquinas, os bancos de dados, a rede. E fazendo tudo do zero toda vez. Sendo um ecommerce estamos falando de um negócio de margem baixa, por isso cada centavo conta. E nesse caso foi natural juntar anos de experiência criando essa infraestrutura do zero pra criar um serviço pra automatizar e gerenciar todos esses recursos, mitigando essa necessidade de fazer tudo do zero o tempo todo e foi assim que em 2006 a Amazon lança os produtos EC2 pra máquinas virtuais, o S3 pra blob storage e o SQS que é o Simple Queue pra filas - um componente importante pra escalabilidade como já expliquei nos episódios sobre concorrência. Logo depois a AWS lança o EBS que é o Elastic Block Storage que são volumes montáveis nas máquinas EC2 pra ser a segunda opção de armazenamento.</p>
<p>A grande vantagem de máquinas EC2 é você poder criar, ou provisionar uma máquina, criar um volume EBS, anexar esse volume à máquina EC2, e de dentro dela montar o volume no sistema operacional, formatar e usar como se fosse um disco normal. Dessa forma tudo que você instalar ou configurar ficaria persistido nesse volume. Se amanhã você quisesse criar uma segunda máquina bastava criar um snapshot da máquina e carregar num segundo EC2 ou se quisesse aumentar a capacidade computacional da máquina, podia literalmente apagar a máquina, criar outra maior e anexar o mesmo volume e tudo funcionaria praticamente plug and play.  Descrevendo desse jeito se parece com qualquer VPS, então o que foi tão diferente assim?</p>
<p>A AWS fez duas coisas muito diferentes dos VPS: cobrança e controle. Primeiro foi o modelo de bilhetagem onde você pagaria por hora de uso dos recursos, ou por megabytes usados no caso do S3. Em vez de pagar uma mensalidade pela máquina você pagaria pelo uso real. Esse tipo de granularidade, mais próxima da minutagem de celulares nunca tinha sido tentada em hospedagens. Ou seja, se você decidisse que só esta semana precisa de uma máquina maior, podia fazer a mudança, depois voltar pro tipo de máquina menor, com facilidade e pagando só pelas horas usadas.</p>
<p>Mais importante: você podia controlar tudo via APIs, sem precisar entrar num dashboard. De repente você queria criar e apagar máquinas com mais frequência, pra tirar vantagem desse modelo. Uma vantagem de ser a Amazon é que eles começaram rapidamente a replicar o modelo em múltiplas regiões geográficas diferentes, permitindo que grandes empresas pudessem escalar rapidamente e com redundância entre diferentes data centers, com a facilidade de enviar um comando numa API. Esse tipo de granularidade de cobrança e de controle simplesmente não existia.</p>
<p>Acho que aqui podemos dividir a história da infra de web em 2. O antes e depois da AWS, com o advento de lidar comercialmente com máquinas da mesma forma como lidamos com software. Deixa eu explicar. Desde sempre na história da informática tínhamos uma polarização: desenvolvedores de software gostam e precisam mexer no software o tempo todo, pra corrigir bugs, pra adicionar novas funcionalidades que vão atrair mais pessoas. Já um sysadmin, ou dba ou administrador de redes, depois que terminam de configurar tudo, eles querem mexer na infraestrutura o menos possível, de preferência nunca mais. Software vs Hardware, soft vs hard.</p>
<p>Isso porque infraestrutura como vocês puderam ver até agora sempre foi um House of Cards … não, não esse … esse outro, isso, literalmente. Racks, roteadores, cabeamento, HDs, metal e parafusos. Um monte de peças de lego montados um em cima do outro. Assopre muito perto e a casa despenca. Basta uma linha de configuração errada e você comprometeu a segurança e integridade de tudo. Basta um comando de formatar o drive errado e você perdeu todos os dados.</p>
<p>Isso me lembrou de um caso que eu vi acontecer anos atrás. Um operador de data center fez um script às pressas pra resolver um problema. No Bash você tem várias variáveis globais que começa com cifrão, não lembro o que ele queria fazer mas no final ele digitou uma letra errado. Esse script foi enviado via SSH pra dezenas de servidores reais. Talvez tenha sido o $0 que pega o último comando rodado. De repente dezenas de máquinas ficaram sem o Bash! Logar remoto via SSH não funcionava porque não tinha mais o Bash pra abrir um novo terminal. A única forma de voltar a ter acesso nas máquinas foi ir até o data center e várias pessoas com pen drives conectarem fisicamente, espetar os pen drives, e copiar o binário do bash de volta. Foi meio tenso.</p>
<p>Por isso um bom sysadmin precisa ser uma pessoa paranóica, que checa e recheca o que está fazendo, que vai fazer o possível pra ter redundância de tudo, que vai fazer backup do backup, que vai checar que o backup de fato funciona pra restaurar, que vai monitorar cada minuto de cada servidor pra ver se alguma coisa está acontecendo. E mesmo assim, num momento de emergência, depois de varar 2 noites, uma letra errada num script pode gerar uma catástrofe.</p>
<p>Enfim, dependendo do seu projeto, podia levar dias ou até meses pra coordenar a configuração de uma nova infraestrutura pra um projeto. Mexer nesse house of cards … depois de tudo pronto era proibido, você tinha que tratar essas coisas como se fosse material nuclear: só pessoal autorizado podia criar um novo índice numa tabela num banco de dados ou instalar qualquer pacote novo. Rodar scripts direto no servidor, nem pensar. No fim dos anos 90 era caos, mas à medida que entramos nos anos 2000 e o mundo enterprise começou a adotar tecnologias de web, a cultura corporativa foi sendo adotada primeiro.</p>
<p>Foi só depois da recuperação da crise da internet de 2001, depois que os unicórnios que falei antes começaram a aparecer, que essa cultura entrou em conflito com as necessidades de escalar mais rápido. A cultura de infraestrutura dessa época, regada a coisas como ITIL era insuficiente. Não é a mesma coisa mas ITIL era como se fosse o Waterfall no mundo de software ou os atuais Pseudo Agile pra gestão de projetos, um conjunto de processos e rituais burocráticos e corporativistas.</p>
<p>Dentro de um lugar como Twitter ou Amazon você teria guerra civil entre desenvolvedores e sysadmins se as coisas continuassem do mesmo jeito. Em projetos pequenos, os VPS ajudaram porque era fácil dar máquinas virtuais pra desenvolvedores, mas eles ainda não tinham tudo que se precisava pra escalar. As ferramentas de gestão de configuração como Chef ou Puppet também ajudaram os sysadmins porque agora o que levava dias pra configurar levaria horas ou minutos.</p>
<p>Mas a divisão entre os dois lados continuaria, um desenvolvedor de software simplesmente não tem conhecimento ou experiência suficiente pra ganhar acesso de entrar numa máquina de produção. Ninguém quer correr o risco de alguém não autorizado entrar e deixar uma brecha de segurança pra trás. E da forma como se fazia software até esse ponto, você fazia deployment da aplicação no servidor e alguém precisava entrar na máquina via um terminal pra acertar arquivos de configuração, acertar tabelas e índices no banco, rodar alguma rotina extra pra instalar dependências que faltaram e coisas assim.</p>
<p>Mas quando a AWS liberou máquinas virtuais como um serviço tivemos uma quebra de paradigma. A somatória das tecnologias que falei até agora facilitaram uma coisa que antes era muito difícil: destruir e recriar máquinas em poucos segundos. Pra começar, a AWS era muito maior que qualquer VPS. Só isso já eliminou a necessidade de precisar ter máquinas físicas pra maioria das médias e até grandes empresas que não tinham políticas antiquadas e aceitavam mais riscos.</p>
<p>Agora os sysadmins ganharam a capacidade de criar imagens e oferecer essas imagens aos desenvolvedores rapidamente. Era muito fácil criar ambientes de teste separados do ambiente de produção mas ambos configurados exatamente iguais, de forma que o que funcionava em ambiente de teste garantidamente funcionaria nas máquinas de produção. E o melhor, a custos muito menores e com menos necessidade de planejar com tanta antecedência porque não havia coisas como mensalidades que são compromissos longos.</p>
<p>Entenda uma mudança de papéis: antes os sysdadmins lidavam diretamente com bare metal. Instalavam os servidores reais em racks físicos e ligavam cabos de redes e configuravam direto na máquina com teclado e monitor conectado no hardware. Com as VPS e depois a AWS, nasceu uma segunda categoria de sysadmins que não precisavam mais ter contato com equipamento físico, a menos que trabalhassem em data centers. As empresas não precisavam mais se preocupar em comprar hardware e sim em contratar serviços.</p>
<p>Com as tecnologias de virtualização e containerização que falei no episódio anterior, nós essencialmente empacotamos uma máquina virtual como um software. E foi criado um novo modelo de negócios em cima dessa nova categoria de software. Por agora vamos só entender que no momento em que o provisionamento de máquinas virtuais se tornou programático, via APIs, passamos a tratar a infraestrutura como software. Assim nasce a Infraestrutura como Serviço ou IaaS.</p>
<p>Surgiram esforços open source pra clonar os conceitos proprietários da AWS como o Eucalyptos que a Canonical adotou mas hoje já meio que morreu, e o OpenStack que foi adotado por centenas de empresas e veio evoluindo bastante nos últimos anos e é composto por dezenas de componentes como o Nova que orquestra hypervisors como o Hyper-V ou vSphere, o Neutron que cuida de tudo relacionado com rede incluindo detecção de invasão, enfim, dezenas de componentes.</p>
<p>E o OpenStack só iniciou mesmo a partir de 2010, ou seja 4 anos depois da AWS, só pra vocês entenderem que o que a AWS inventou em 2006 era difícil de replicar. Levou anos pra aparecer concorrentes de verdade. Foi só depois de 2010 que ganhamos boas opções comerciais como Microsoft Azure e Google Cloud. Eles estão competindo bem e as opções são equiparáveis na maioria dos produtos. Como a gama de opções de cada uma é muito grande então diferentes pessoas vão ter críticas de coisas diferentes em diferentes cenários. Não tem um vencedor hegemônico.</p>
<p>Até aqui eu dei uma rápida passada pelos conceitos de paravirtualização e virtualização acelerada por hardware como as instruções VT-X além das ferramentas de gestão de configuração como Chef e finalmente a primeira geração de orquestração de infraestrutura como OpenStack que ajuda a levar um ambiente como a AWS pra dentro de grandes empresas que precisem de garantias totais de segurança como agências governamentais ou empresas que lidam com dados de alta periculosidade ou privacidade como dados médicos ou financeiros sensíveis. Ou sei lá, os projetos de uma bomba nuclear. Você certamente não deveria colocar essas coisas no Dropbox.</p>
<p>Essa vertente de IaaS privado é mais conhecido como Cloud Privado e existem várias empresas que oferecem soluções como o OpenShift da RedHat, o CloudFoundry da Pivotal. Assim você consegue ter a flexibilidade de infraestrutura virtualizada que pode ser rapidamente utilizada pelos projetos internos sem precisar da antiga burocracia de infraestrutura e se mantendo num perímetro controlado fora da internet pública.</p>
<p>Até aqui vimos até onde as tecnologias de virtualização chegaram no mundo comercial. Mas containers ainda não entraram na história. Vamos voltar um pouco pra 2006, que foi quando o Google desenvolveu outro projeto, o Process Container que foi uma evolução em cima dos Jails de BSD ou Zonas de Solaris ou mesmo os OpenVZ e Vserver de Linux. Repetindo: a ideia não é virtualização como Vmware ou Parallels ou Virtualbox, e sim isolamento dos processos e dos recursos da máquina.</p>
<p>Esse projeto foi renomeado depois pra Control groups ou cgroups. A ideia ainda é fazer a kernel mentir pros processos. Desse jeito você pode ter dois processos rodando na mesma máquina, no mesmo sistema operacional, mas um sem saber da existência do outro, quase como se estivessem em máquinas completamente diferentes. Se o objetivo é só isolar um processo, todas as opções de jail que culminaram no cgroups são ordens de grandeza mais leves do que virtualização, porque você ainda está rodando somente uma kernel em vez de múltiplas kernels virtualizadas.</p>
<p>Vamos rever conceitos: um sistema operacional é um conjunto de binários. A primeira coisa depois do boot loader é a kernel, é o primeiro grande binário que carrega com os privilégios máximos do Ring-0 e é responsável por carregar todo o resto do sistema, que por sua vez são conjuntos de programas como os drivers, supervisores como systemd, daemons e assim por diante. Em distribuições Linux, mesmo rodando praticamente o mesmo binário de kernel, o que diferencia um RedHat de um Ubuntu ou Arch é esse conjunto de binários que vai empacotado na distribuição e como eles são configurados, incluindo preferências em estrutura de diretórios ligeiramente diferentes, sistema de pacotes como yum ou apt ou pacman. Mas tudo começa com o kernel e os drivers, daí ele monta o volume de disco como o “barra” que é o root e de lá vai carregando todo o resto no userland ou Ring-3.</p>
<p>O tal cgroups consegue isolar os recursos da máquina e fazer os processos em userland enxergar só o que ele mandar enxergar. Ele vai dizer quanto de CPU, de memória o processo vai enxergar mas em particular, assim como o conceito do chroot que falei antes, ele pode mascarar o volume de disco e carregar um root diferente do sistema hospedeiro. Se você carregar um outro root com outro conjunto de programas userland, na prática você pode ter um hospedeiro que é um RedHat e dentro do jail do cgroups carregar os binários que formam um Ubuntu ou qualquer outra distro.</p>
<p>Voltando ao cgroups do Google, em 2008 tivemos o lançamento do LXC ou Linux Containers, que é um conjunto que inclui coisas como daemons e ferramentas de linha de comando que engloba não só o cgroups que passou a vir no kernel do Linux como namespaces que permite etiquetar processos. O cgroups consegue criar os limites pra grupos inteiros de processos. Quando você cria um namespace de processos, seu processo vira o PID, ou process ID, número 1 e os filhos desse processo vão seguindo nessa sequência. Diferentes namespaces vão ter diferentes processos PID 1. Você tem namespaces de rede, daí seu processo pode se ligar a uma porta virtual, assim vários processos podem responder numa porta 80 virtual, na mesma máquina, e uma bridge ou ponte virtual de rede vai cuidar pra que isso seja possível, fazendo tipo um NAT entre os containers e a rede externa. E tem namespaces de mounts, onde você pode montar e desmontar volumes dentro do container, sem afetar os mounts do sistema hospedeiro.</p>
<p>Lembram como eu expliquei que o sistema operacional mascara os endereços reais de memória pra uma memória virtual e o endereço virtual 0 de memória que um processo vê não é o endereço 0 real da memória? Ou como um chroot mascara o filesystem e cada processo enxerga roots no disco em lugares diferentes? Assim dois processos nunca vão pisar um em cima do outro? Pense em cgroups e namespaces como o equivalente pra todos os outros recursos da máquina, incluindo recursos de rede e I/O. Eu sei que estou sendo repetitivo mas é importante deixar claro: os containers de LXC parecem muito com um sistema operacional, mas eles compartilham a kernel do mesmo sistema, muito diferente de hypervisors que gerenciam máquinas virtuais inteiras, cada um com seu kernel. Um jail que restringe processos e compartilha o mesmo kernel entre vários jails é o que chamamos hoje de um container.</p>
<p>Esses anos entre 2006 e 2008 pra mim foram muito interessantes. Não quero entrar em muitos detalhes aqui mas eu acho que hoje em dia não preciso mais explicar o que é o GitHub ou GitLab, porque qualquer desenvolvedor que se preza já sabe usar Git. Se você ainda tinha alguma dúvida, Git é obrigatório, tem que saber! Mas antes de 2006 isso não era verdade, muitos usavam Subversion ou outras alternativas como BitKeeper, as porcarias de SourceSafe ou Clearcase, ou mesmo CVS. Mas a partir de 2007 o Git, que é uma ferramenta para gerenciar repositórios de código fonte versionados e distribuídos começou a ganhar terreno rápido, e com o início da popularização do Github em 2008 na comunidade Ruby on Rails, ela criou as sementes não só pra versionar código de aplicações mas também pra servir como mecanismo para automatizar muita coisa.</p>
<p>Também em 2008 outra empresa surgiu na comunidade Ruby on Rails pra resolver o problema que eu vim discutindo até agora de instalar, configurar e manter infraestruturas em máquinas virtuais. Pense somar um provedor de VPS com as capacidades de Linux containers via os cgroups e integrando com um processo de deployment usando repositórios Git. Esse é o Heroku. Se você nunca usou, deveria. É a recomendação pra todo mundo que não tem um sysadmin trabalhando tempo integral ou pra desenvolvedores que não tem experiência com infraestrutura.</p>
<p>Como já disse antes, o AWS EC2, principalmente em 2008, ainda não é muito diferente de um VPS do ponto de vista de um desenvolvedor. São máquinas virtuais. Você programa em PHP, ou Python ou Ruby. Mas pra subir sua aplicação precisa saber instalar e configurar banco de dados, servidor web, rede e tudo mais. Ou seja, ou vai entrar manualmente via SSH e configurar tudo na mão, ou vai usar um Capistrano ou Puppet pra fazer isso. E se você é iniciante isso não é trivial.</p>
<p>Mas e se existisse um serviço por cima desse AWS onde você pudesse simplesmente criar uma conta, cadastrar uma nova aplicação, daí rodar um “git push heroku” que é o comando de git pra subir seu código num repositório Git, e de repente toda sua aplicação é instalada, configurada e está pronta pra rodar, inclusive com banco de dados e outros serviços? Isso é o Heroku. Eles conseguiram tirar vantagem de cgroups e criar seu próprio padrão de containers e montar um modelo de negócios, 5 anos antes de um Docker aparecer. O que você chama de Dockerfile hoje, mais ou menos é o que o Heroku chamou de buildpacks. Todo mundo tentou fazer algo parecido mas levou anos pra chegar perto. Como o Heroku nunca abriu seu padrão de containers pro mundo open source, precisou o Docker aparecer anos depois pra finalmente termos um concorrente.</p>
<p>O que o Heroku conseguiu fazer foi tornar a infraestrutura trivial pra desenvolvedores. Lembram como eu passei o episódio inteiro da semana passada falando como era complicado montar uma infra com confiabilidade e segurança? É o melhor meio do caminho entre uma VPS ou AWS puro e um Kubernetes porque eles acertaram a usabilidade e foram inteligentes no modelo de negócios.</p>
<p>A partir de 2006 o mundo de tech startups explodiu, inspirada por todos os novos unicórnios crescendo como se não houvesse amanhã. Todo mundo queria conseguir subir suas coisas nas novidades da AWS mas ninguém tinha conhecimento suficiente de infraestrutura pra ficar lidando com CF Engine ou Puppet. Pior ainda, um desenvolvedor não sabe como de fato configurar um banco de dados. Não basta só instalar porque a configuração padrão costuma ser ruim, então você precisa saber tunar esse banco pra caber no seu servidor corretamente. Ninguém sabe configurar replicação master slave. E por aí vai.</p>
<p>Por causa disso se abriu um nicho pra tech startups que não existia até então. Se você quer enviar e-mails, jamais tente montar seu próprio servidor de e-mail como a gente fazia no começo dos anos 2000. Em vez disso contrate um serviço como Sendgrid ou Mailgun. Em vez de montar seu próprio servidor de Redis, com redundância, backup e tudo, contrate uma RedisLabs. Em vez de montar seu próprio servidor de cache com Memcached, contrate a Memcachier. Em vez de ter que lidar com repositório pra upload de imagens ou outros arquivos e ter que programar coisas como processamento de imagens, contrate a Cloudinary. E por aí vai.</p>
<p>Se a AWS inaugurou a categoria de IaaS ou infraestrutura como serviço, o Heroku inaugurou a categoria de PaaS ou plataforma como serviço e finalmente, empresas como New Relic, Papertrail, CloudAMQP, Sendgrid, Cloudinary, e centenas de outros inauguraram a terceira categoria: o famoso SaaS ou Software as a Service.</p>
<p>E o que o Heroku fez de inteligente foi desde o começo integrar esses diversos SaaS em sua plataforma de tal forma que fosse simples tanto via o dashboard web quanto via linha de comando adicionar esses serviços aos nossos projetos, além de integrar a cobrança de tudo numa fatura só. Mas eles mantiveram pra eles alguns dos serviços mais críticos como o banco de dados Postgresql e depois o Redis. E por isso era tão difícil tentar implementar um clone de Heroku você mesmo. A parte de containers, buildpacks e deployment usando um comando como git push não era tão difícil subir algo parecido num VPS como Linode ou DigitalOcean. Mas não adiantava nada fazer deployment da sua aplicação e não ter um banco de dados confiável. Daí você caiu no mesmo problema de antes de ter que configurar o banco de dados e, principalmente, manter ele rodando.</p>
<p>Eu diria o seguinte, se você é desenvolvedor de software, não tem experiência real como sysadmin, e precisa subir sua aplicação web mas não sabe nem como começar a configurar um Kubernetes, vá pro Heroku. É muito menos dor de cabeça. E não faça a conta de centavos por hora vezes horas por mês e comparar com o preço do Heroku e dos outros SaaS. Considere quanto custa um sysadmin pra configurar e dar manutenção na infra toda por toda a vida da sua aplicação. Não tente fazer infra sozinho se nunca fez isso antes, é sempre um desastre. E se sua aplicação web não monetiza o suficiente pra pagar a conta, você está fazendo alguma coisa muito errada.</p>
<p>Mas, além de ficar aqui fazendo propaganda de graça pro Heroku eu queria falar de outra coisa importante. O Heroku foi inteligente também educando os desenvolvedores. Nós desenvolvedores que crescemos no fim dos anos 90, com coisas como Java ou mesmo PHP, em ambiente corporativo, estávamos mais preocupados em codar de qualquer jeito dentro do prazo e não se preocupar tanto com a qualidade ou peso da aplicação. A métrica era praticamente linhas de código por hora. Se a aplicação ficasse pesada era só culpar a infra e mandar instalar numa máquina maior e foda-se.</p>
<p>Em 2008, no Heroku os containers ou dynos como eles chamavam vinham em vários tamanhos, contanto que esse tamanho fosse 512Mb de RAM e acho que só depois colocaram 1GB de RAM e anos depois opções maiores. Nós rapidamente começamos a fazer aplicações web mais e mais complexas e de repente não cabia em 512Mb!! Código mal feito roda na máquina do desenvolvedor, mas quebra em produção, em particular em recursos mais restritos como nos dynos do Heroku. Aliás, dynos é como o Heroku chama seus containers.</p>
<p>Eu sempre achei isso sensacional, porque é muito fácil fazer um código porco. E é sempre bom não contar com a muleta de só aumentar a máquina e mostrar imediatamente que a aplicação que tá mal feita mesmo. Quando você não tem outra opção, vai precisar programar direito. Então o Heroku ajudava a separar programador bom de programador ruim. Inclusive o co-fundador do Heroku, Adam Wiggins publicou um guia chamado the twelve factors ou os 12 fatores, que vou deixar nos links na descrição abaixo, que descreve como uma aplicação web deveria ser programada e organizada pra ser escalável. O framework Ruby on Rails desde sempre seguia esses 12 fatores e com a popularização do Heroku todo framework web que se preza passou a seguir, independente de rodar no Heroku ou não porque esses 12 fatores são universais pra apps minimamente bem feitas. E não era tão simples, um Wordpress da vida dava trabalho pra se adequar em todos os fatores na época.</p>
<p>Graças a isso uma nova geração de técnicas, otimizações, procedimentos e convenções começaram a mudar a forma como a gente programava. Em vez de pensar em rodar numa única máquina grande, tínhamos que parar pra pensar em rodar em múltiplos containers menores. Entendemos que a forma de escalar massivamente não é verticalmente aumentando a máquina, mas sim horizontalmente com mais containers. E na intenção de quebrar aplicações grandes pra escalar que nasceu também a idéia de micro-serviços. Eu sou contra quebrar sua aplicação em dezenas de micro-serviços sem motivos. Mas um bom motivo é quando uma aplicação grande não cabe num container, então fica óbvio que precisa ou cortar fora a gordura que não precisa, ou particionar em mais de uma aplicação em containers diferentes.</p>
<p>Pulando a história pra 2013 finalmente surge o tal Docker que inicialmente usou o Linux Containers ou LXC e adicionou novas ferramentas. Em particular, ele se aproveita do fato de você poder montar um root separado do sistema hospedeiro no container e passou a usar o Union File System. Pense um filesystem que consegue montar uma imagem que é somente de leitura como o novo root desse container, e toda vez que você tenta escrever ele cria uma segunda camada por “cima” vamos dizer assim.</p>
<p>Lembram quando expliquei sobre o conceito de Copy on Write pra memória no caso de processos que fazem fork no Linux? É a mesma coisa só que pra disco. Por isso as imagens de Docker são incrementais. Se você usa Git, pense se você pudesse montar um repositório de Git como o root do seu container e toda vez que você modifica ou escreve alguma coisa é como se fizesse um novo commit. Assim você consegue inclusive voltar um desses commits pra versão anterior. Mais importante: você consegue empacotar esse filesystem inteiro num arquivo, que são as imagens que seu cliente de docker baixa.</p>
<p>O Docker depois trocou o LXC, que se usava via a biblioteca libvirt, por uma que eles mesmos fizeram, a libcontainer. Mas acho que o mais importante do Docker foi padronizar a receita pra descrever as imagens no que hoje conhecemos como Dockerfiles. Esses Dockerfiles são parecidos com as promessas de um CF Engine ou as receitas de um cookbook de Chef. É um arquivo onde você declara como essa máquina vai ser configurada, que pacotes vão ser instalados, você pode inclusive declarar que comandos rodar da primeira vez que a imagem é criada ou toda vez que ela é inicializada. E também padronizar a interface de uso de linha de comando que hoje até o Kubernetes meio que copia.</p>
<p>Tem gente que ainda confunde Docker com alguma coisa parecida com um Virtualbox, mas espero que com o que expliquei até agora tenha ficado mais claro. Docker não é full virtualization, não é paravirtualization e não é um hypervisor. Virtualização é literalmente falsificar o hardware físico. Nesse hardware virtual você pode rodar múltiplos sistemas operacionais diferentes, ou seja, múltiplas kernels diferentes, incluindo kernels que não são do Linux como Windows ou BSD ou Mac e inclusive de processadores diferentes de Intel ou AMD como os fazem os emuladores de videogame.</p>
<p>Docker é um conjunto de ferramentas pra organizar e facilitar o uso de de cgroups, namespaces e union filesystem, tudo que você podia usar sozinho, mas a casca do Docker por cima obviamente torna tudo ordens de grandeza mais simples. Portanto você deve ter entendido que a idéia toda é rodar os programas de todo mundo no mesmo sistema operacional, na mesma máquina, sem rodar outro sistema operacional por cima. Na prática é uma máquina só, um sistema só, rodando os programas lado a lado mas com a kernel mentindo pra todos eles acharem que não tem ninguém mais.</p>
<p>Mas se você já usou Docker já deve ter instalado em cima de um RedHat ou Ubuntu e dentro dos containers ter conseguido instalar e rodar outra distro Linux, totalmente diferente, como Arch ou Alpine e inclusive rodar um pacman e instalar pacotes, ou seja, fica parecendo que é um VMware ou outro hypervisor onde você instala outro sistema operacional. Como isso é possível se eu acabei de dizer que não é virtualização e estamos compartilhando o mesmo sistema operacional?</p>
<p>Falando de forma bem simplificada, uma distro Linux é o conjunto do binário da kernel do Linux e de programas como o gerenciador de pacotes, o binário do Bash e outros utilitários e serviços. Tirando a kernel que roda no Ring-0, o tal kernel space, todo o resto roda em userland. Agora pense uma distro que eu removo o kernel, só sobra os programas em userland. E eu falei que num container nós compartilhamos a mesma kernel. Ou seja, se o container montar um diretório pra onde estão os programas de userland que definem um Ubuntu, então dentro desse container, pra todos os efeitos e propósitos, é um Ubuntu, mesmo que do lado de fora estamos rodando o Docker num Fedora. O Ubuntu dentro do container vai usar a kernel compartilhada do Fedora. Essa é a mágica dos containers.</p>
<p>Obviamente rodar todos os programas userland de um Ubuntu inteiro dentro de um container é pesado e um desperdício de recursos, especialmente se eu for inventar de querer rodar 10 containers, todos com os mesmos userland de Ubuntu repetidos. Por isso temos outras distros mais adequadas pra rodar num container como o CoreOS ou Alpine, que são userlands ordens de grandeza menores e só com os binários essenciais pra rodar a maioria dos programas que precisamos. A idéia do Docker é que o seu programa dentro do container não dependa de nada do sistema operacional hospedeiro e rode suas próprias dependências. A idéia de um container não é rodar dezenas de programas dentro dele, mas sim somente em um. Um container só pro Postgres, outro container só pro Redis, outro container só pro NGINX.</p>
<p>Por isso mesmo que não existe Docker de Windows ou de Mac. A kernel do Windows não tem cgroups e namespace nem union file system, nem o Mac e acho que nenhum UNIX BSD também. Mas então como eu consigo rodar Docker no Windows ou no Mac? Na realidade você primeiro tem que iniciar um hypervisor, seja tipo 1 como o Hyper-V ou o novo Hypervisor Framework no MacOS a partir da versão 10.10 Yosemite, ou mesmo um hypervisor tipo 2 como Virtualbox ou Vmware workstation.</p>
<p>Daí você cria uma máquina virtual no hypervisor e daí instala uma distro Linux com Docker. Agora, de dentro dessa máquina virtual você pode criar containers e compartilhar a kernel desse Linux. Daí o cliente de Docker rodando no Windows se comunica com o Docker rodando dentro da máquina virtual, por exemplo, via a rede interna TCP. Esse é o truque todo! Por isso, em hardwares físicos equivalentes, o Docker vai ser mais performático se o sistema operacional hospedeiro for também um Linux. Se for um Windows ou Mac você vai perder performance porque vai precisar também rodar um Linux num hypervisor antes.</p>
<p>A idéia de um container não é ser um sistema operacional independente. Como já disse o correto é rodar somente um único processo por container. Por isso você tem hoje soluções como o Docker Compose que permite que você declare num arquivo texto pra subir vários containers, cada qual com seu Dockerfile e dizer que volumes eles vão montar pra compartilhar arquivos, ou que portas eles vão expor que o outro container vai conseguir enxergar.</p>
<p>Em paralelo a isso, digamos que você montou seu Dockerfile e seu container funciona bonitinho na sua máquina de desenvolvimento, mas agora eu quero subir essa mesma imagem na minha conta na AWS EC2 ou DigitalOcean ou outro provedor de máquinas virtuais. Pra isso existe o Docker Machine, que vai facilitar o processo de subir ou fazer deployment dos seus containers num servidor na internet. Nesse caso o provedor precisa fornecer o suporte a Docker em suas máquinas virtuais ou você mesmo instalar o serviço de Docker na máquina virtual que provisionar.</p>
<p>Só fazer alguns containers não é suficiente. Você quer ter controle maior pra subir uma infraestrutura inteira com dezenas de máquinas virtuais e containers, além de configuração de rede, políticas de segurança e outras coisas. Organizar todos esses recursos virtuais e não virtuais de uma forma coordenada requer mais que só um vmware ou docker, por isso existem as soluções que chamamos de orquestradores, como o Docker Swarm, ou o Mesos, ou Marathon ou Nomad. Mas em 2019 quem realmente se tornou o padrão de fato é o Kubernetes do Google, que está disponível em todo provedor de infraestrutura como serviço, desde o Google Cloud, Microsoft Azure ou AWS.</p>
<p>Essas soluções se confundem com o próprio Docker já que num Kubernetes você instala imagens compatíveis com as de Docker e o runtime de containers por um tempo foi o próprio Docker. Na prática imagine que você está no Google Cloud, que é concorrente da Amazon AWS. Você tem máquinas virtuais rodando em cima de alguma coisa como o Citrix XenServer, que do ponto de vista de um servidor, é como se fosse seu Virtualbox ou Vmware que você roda no seu desktop ou notebook. O Kubernetes enxerga isso como Nodes ou nós. E dentro desses nodes você pode rodar múltiplos containers Docker que o Kubernetes enxerga como containers, agrupados dentro de Pods. Mais do que isso você tem um registro central de toda a infraestrutura de tal forma que se um container tiver problemas e cair ou node cair, o sistema central sabe como reconstruir.</p>
<p>Se você está iniciando, parece algo que um Docker compose e Docker machine resolveriam, mas o Kubernetes, ou seus concorrentes como o Swarm ou Mesos, são mais complexos que isso. Além de containers, temos que lidar com storage, com endereços de IP externos e internos, regras de firewall, clusters. Ele está preparado pra cenários com múltiplas zonas; por exemplo distribuir parte dos seus containers na zona Estados Unidos Central e outros containers na zona Asia sul caso você tenha público tanto na América quanto na Ásia e quer oferecer a melhor performance pros dois públicos. E existe o suporte a cluster de federações, dessa forma você pode manter parte da sua infraestrutura no Kubernetes do Google Cloud que é GKE e outra parte no Kubernetes da AWS que é o EKS. Ou seja, em provedores diferentes de cloud.</p>
<p>Em resumo o Docker se tornou uma organização, um ecossistema, e um conjunto de ferramentas, mas por baixo dos panos os componentes que realmente importam e possibilitam isso vem na kernel do Linux, que é o cgroups e o namespace. Todo o resto é só uma convenção de uso e um padrão. Inclusive surgiu uma iniciativa do próprio Docker pra padronizar o runtime de containers e o formato das imagens, que é o Open Container Initiative ou OCI. Diversos projetos concorrentes ao Docker surgiram por conta do runtime-spec e image-spec como gvisor, clearcontainers ou katacontainers. Eu mencionei que o Kubernetes usa o runtime de containers do próprio Docker mas pouco a pouco já se vê instalações de Kubernetes que não usam mais o Docker.</p>
<p>O Heroku teve um papel importante em educar e demonstrar como montar uma plataforma como serviços viável. E o Docker teve um papel importante em consolidar o uso de containers Linux e padronizar seu manuseio. Mas com a abrupta popularização do Kubernetes, se não tomar cuidado, o futuro do próprio Docker é incerto já que o Kubernetes está comendo a fatia de mercado de orquestração. De qualquer maneira, se você quer ser um bom sysadmin nos tempos modernos, no mínimo precisa saber como funciona Docker e seus outros produtos e Kubernetes. E não basta treinar só na sua máquina com minikube, você precisa entrar num Google Cloud e usar o kubernetes lá. Isso é o mínimo hoje, além do que eu já venho falando desde o começo desta série: estude e aprenda Linux de verdade!</p>
<p>E por hoje vamos parar por aqui, eu falei rapidamente de como saltamos do mundo primitivo de hospedagens compartilhadas e deployment de sites via FTP pras evolução das tecnologias de virtualização e containers, até chegar no Docker e Kubernetes. Mas isso ainda é só o pico do iceberg. Eu acho que pra fechar o tema de devops, semana que vem vou tentar falar especificamente sobre banco de dados e outro termos hipster, os famigerados NoSQL e talvez uma pincelada na arquitetura moderna de Web e o comportamento das diferentes linguagens de programação nessa arquitetura de cloud.</p>
<p>Mesmo estando num mundo de Kubernetes, as opções todas que eu falei desde o começo como hospedagens compartilhadas, servidores físicos em colocation, VPS, todas ainda existem e muita gente ainda programa do jeito antigo e tem muita dificuldade de entender porque as coisas são diferentes hoje. Por isso eu precisava que vocês entendessem onde esse monte de nomes como cgroups, hypervisors se encaixa no contexto geral. Se ficaram com dúvidas - e eu imagino que ficaram - mandem nos comentários abaixo pra galera ajudar, se curtiram mandem um joinha, continuem ajudando o canal compartilhando com seus amigos, não deixem de assinar o canal e clicar no sininho pra não perder a continuação dessa história! A gente se vê semana que vem, até mais!</p>
]]></content:encoded><category>devops</category><category>containers</category><category>cloud</category><category>akitando</category></item><item><title>[Akitando] #47 - Entendendo "Devops" para Iniciantes em Programação (Parte 1) | Série "Começando aos 40"</title><link>https://akitaonrails.github.io/2019/04/10/akitando-47-entendendo-devops-para-iniciantes-em-programacao-parte-1-serie-comecando-aos-40/</link><guid isPermaLink="true">https://akitaonrails.github.io/2019/04/10/akitando-47-entendendo-devops-para-iniciantes-em-programacao-parte-1-serie-comecando-aos-40/</guid><pubDate>Wed, 10 Apr 2019 17:00:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/bwO8EZf0gLI"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;Finalmente chegamos no tema final da série! Vamos falar um pouco sobre o tal do &amp;ldquo;devops&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Esta parte meio que depende dos conceitos que vimos nos últimos episódios então se você é iniciante, e ainda não assistiu os anteriores, recomendo que faça isso.&lt;/p&gt;
&lt;p&gt;No episódio de hoje quero dar um pouco do contexto histórico, dos anos 90 até os anos 2000, indo de configuração manual de máquinas, hospedagens compartilhadas, virtualização até os VPS. Vamos entender como uma coisa foi levando pra próxima.&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/bwO8EZf0gLI"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Finalmente chegamos no tema final da série! Vamos falar um pouco sobre o tal do &ldquo;devops&rdquo;.</p>
<p>Esta parte meio que depende dos conceitos que vimos nos últimos episódios então se você é iniciante, e ainda não assistiu os anteriores, recomendo que faça isso.</p>
<p>No episódio de hoje quero dar um pouco do contexto histórico, dos anos 90 até os anos 2000, indo de configuração manual de máquinas, hospedagens compartilhadas, virtualização até os VPS. Vamos entender como uma coisa foi levando pra próxima.</p>
<p>Muita gente confunde muitos termos, virtualização, paravirtualização, jails, containers, hypervisors e não sabe o que é o que, então eu vou distinguir tudo pra vocês finalmente entenderem.</p>
<p>=== Script</p>
<p>Olá pessoal, Fabio Akita</p>
<p>Depois de muitas semanas, finalmente terminamos o tema de Back-end semana passada. Mas continuando na série Começando aos 40, décimo-segundo episódio já, precisamos entrar no tema de infraestrutura e devops. É bom eu já deixar claro desde já que minha formação e experiência principal é desenvolvimento de software, mas ao longo dos anos eu tive que lidar com infraestrutura várias vezes então digamos que eu tenho muitas opiniões a respeito. Mas é bom lembrar que eu nunca atuei como administrador de sistemas em tempo integral.</p>
<p>Aliás, durante esses próximos episódios você vão ver que vou ficar o tempo todo complementando “como eu já expliquei antes” e eu já expliquei mesmo, nos episódios anteriores. Então eu realmente recomendo que você tenha assistido todos os últimos 11 vídeos antes de começar esse se você ainda for iniciante. Se já é programador, vamos lá, e talvez você veja que mesmo trabalhando na área ainda tem coisas que você não sabe e, nesse caso, recomendo que tente ver os vídeos anteriores também, eu sempre escolho falar coisas que eu vejo que a maioria das pessoas não conhece, independente quantos anos já trabalha.</p>
<p>Devops é um daqueles termos chatos de definir e que você provavelmente está usando errado. É a mesma coisa com Agile. Agile não é só ter sprints ou backlogs ou rituais. Devops também não é um título, não são ferramentas, não é uma metodologia, não é uma cultura. É tudo isso e nada disso. Pois é, é uma merda mesmo esse termo, eu detesto coisas difíceis de definir. Pra entender melhor devops é mais fácil começar delineando os problemas e no final talvez chegar na conclusão.</p>
<p>(&hellip;)</p>
<p>Eu diria que se eu fosse detalhar tudo que eu gostaria sobre o tal devops, eu ficaria até o fim do semestre. Em vez disso, assim como fiz com front-end, quero dar mais um apanhado geral da linha do tempo e alguns conceitos e ferramentas. Vocês tem que entender que a área é gigantesca, você pode passar anos lidando com infraestrutura e nunca ver tudo. Por exemplo, eu nunca operei diretamente num data center, eu conheço vários dos conceitos mas o hardware moderno de data centers é uma coisa que eu só conheço na teoria, portanto eu não vou entrar muito nesse assunto. A área específica de redes é outro mundo a parte. Uma coisa é rede caseira ou de pequenas empresas, outra coisa completamente diferente é lidar com redes físicas em data centers, lidar com fibra ótica e roteadores nível “industrial” que você vai encontrar em provedores. Também existem os data centers privados em grandes corporações que usam muitas das mesmas coisas.</p>
<p>Na prática, metade do mundo de infraestrutura você nunca vai ter acesso, pelo simples fato que os hardwares e componentes nível data center você nunca vai chegar a colocar as mãos. Os tipos de componentes que temos no mundo de varejo consumidor, os PCzinhos xing lings que montamos ou mesmo os notebooks, usam componentes muito mais baratos e menos confiáveis do que você usa num data center. Apesar dos conceitos serem similares, os componentes que você tem acesso não se comparam com os de um data center. Então não é o seu CPU Core i7 ou i9 só que centenas deles num servidor. Nossos componentes de varejo desperdiçam muito espaço físico e são muito pouco confiáveis pra ficar ligados 24 por 7. Servidores sérios usam processadores com dezenas de cores que conseguem ficar ligados meses a fio sem desligar. Inclusive muitos desses componentes podem ter até menos performance se comparado com similares mais baratos que você compra no mercadolivre, mas é o trade-off de sempre: trocamos um pouco de performance por confiabilidade. Aliás, a coisa mais importante no mundo de infraestrutura é confiabilidade e segurança.</p>
<p>Nos anos 90, quando os data centers estavam começando a nascer, não era incomum muitas empresas que queriam ter presença na internet comprar seus próprios servidores ou até mesmo PCs caseiros, contratar a tal internet dedicada que era super caro porque estamos falando de uma época que internet era um troço opcional, a maioria das pessoas sequer usava ainda, e quem usava discava por modem de madrugada pra pagar menos pulsos. Felizmente a Lei de Moore estava no seu período mais acelerado e o início da Bolha da Internet do meio pro fim dos anos 90 acelerou o desenvolvimento da infraestrutura geral. Quando eu estava na faculdade acho que o Brasil tinha tipo uns 3 backbones que ligavam o país com o resto do mundo. Não era incomum ter problema num backbone tipo o da Fapesp e metade do Brasil ficava ilhado do resto do mundo. Era assim rudimentar no começo dos anos 90.</p>
<p>Quando estávamos fazendo os primeiros sites dinâmicos, com ASP ou PHP ou Perl, eu expliquei que o mais popular era rodar algo como Apache com coisas como mod_php ou Windows NT 4 com a primeira versão do IIS pra rodar ASP. Os bancos de dados não eram grande coisa e o mundo enterprise só rodava coisas como Oracle ou IBM DB2. O mundo open source tinha acabado de começar e um MySQL nem podia ser considerado um banco de dados de verdade ainda. O SQL Server estava iniciando depois que a Microsoft adquiriu a Sybase. Então as opções não eram muitas.</p>
<p>Um Apache funcionava inicialmente fazendo fork de si mesmo pra cada nova requisição. As requisições HTTP caíam em algum virtual host. Um virtual host é uma configuração do servidor Web indicando quem vai responder a requisição. Podia ser mapeando a URL pra uma pasta no disco que tem HTMLs estáticos ou podia ser mapeando a URL pra ativar algum programa CGI ou um modulo como mod_perl ou mod_php. Seja no Apache antigo ou num NGINX moderno, você sempre vai estar configurando virtual hosts.</p>
<p>Os virtual hosts ficavam num arquivo de configuração do Apache e toda vez que você mexia nesse arquivo precisava restartar o servidor pra carregar essas configurações e habilitar novos sites. Assim um único servidor Apache podia servir múltiplos sites de uma só vez. Nas faculdades era comum você ter servidores Unix como HP-UX ou Solaris com Apache e cada usuário do UNIX tinha uma pasta onde ele podia jogar arquivos que automaticamente ficavam disponíveis no servidor da faculdade. Com o tempo alguns alunos podiam ter contas nesses servidores e também compartilhar coisas no mesmo servidor. Foi assim que eu comecei a experimentar com web e aprender Unix em 1996.</p>
<p>Entendam que até o fim os anos 80 não era comum uma pessoa ter um computador inteiro só pra ela. Especialmente computadores potentes já que mesmos os caseiros dos anos 80 eram super limitados e fracos. A maioria não era muito mais potente que um nintendinho 8-bits. Por isso os grandes mainframes usavam sistemas operacionais que permitiam o tal time-sharing que já expliquei. E a geração UNIX evoluiu sendo um sistema multi-usuário. Quando nos anos 80 e 90 as comunidades de desenvolvimento quiseram tentar trazer a experiência do UNIX pra plataforma baixa, culminando no Linux, também ganhamos essas características de multi-usuários e multi-tarefa. Ou seja, no mesmo sistema operacional era possível ter múltiplos usuários guardando seus dados e documentos digitais na sua conta e seus programas não afetam o de outros usuários. Já sabemos como o sistema operacional isola os processos de afetar uns aos outros, mas também tínhamos esse isolamento no nível do usuário.</p>
<p>Somado ao fato que servidores sempre foram coisas caras, que o sistema operacional tinha a habilidade de ter múltiplos usuários, e que a maioria dos programas não roda consumindo 100% dos recursos o tempo todo, permitindo multi-tarefa preemptiva, foi natural começarmos a ter PCs montados com Linux, Apache e MySQL preparados pra rodar múltiplos sites de múltiplos usuários tudo na mesma máquina. Quando PHP começou a se popularizar, começaram a surgir vários painéis de gerenciamento pra facilitar cadastrar e gerenciar novos usuários e novos sites, produtos como o famoso cPanel criado em 1996.</p>
<p>Assim começa a era dos servidores corporativos web que sempre foram relegados a um recurso secundário frente aos gigantes ERPs como os da Oracle ou SAP. Pouca gente sabia mexer em Unix ou Linux, então painéis como cPanel tiveram um papel muito importante. Mas algumas pessoas foram um passo além: criaram um modelo de negócios onde era possível qualquer pessoa pagar uma mensalidade e ganhar um usuário numa máquina dessas e uma conta num derivado de um cPanel. Assim eles podiam criar um banco de dados restrito dentro de um MySQL, podiam configurar virtual hosts pros seus sites e podiam hospedar aplicações escritas em CGI com Perl ou PHP que era a opção mais performática. E assim se popularizou a primeira grande stack de tecnologias da web: o LAMP ou Linux, Apache, MySQL e PHP. E com uma stack aceita pela maioria, empresas puderam ser fundadas pra oferecer serviços, e assim começam a nascer as famigeradas hospedagens compartilhadas como as que você vê até hoje em empresas como Locaweb ou Godaddy.</p>
<p>O conceito é simples: dividir os recursos da máquina pra dezenas ou centenas de sites pequenos. Cada site pequeno tem pouco tráfego e só durante algumas horas do dia, então era possível apertar vários deles na mesma máquina. Até hoje temos centenas de milhares de sites funcionando assim. E com isso criou-se um ecossistema de pequenas agências e freelancers que podiam desenvolver esses sites pequenos, e foi assim que a primeira geração de sites começou a expandir rápido.</p>
<p>Por causa disso também, essa primeira geração de programadores se preocupou muito pouco com infraestrutura. Os únicos lugares onde precisava dos tais webmasters ou administradores de sistemas ou o que eu gosto de chamar: sysadmins, eram nas grandes empresas que mantinham seus próprios pequenos servidores, seja localmente no prédio da empresa ou em regime de co-location que era o aluguel de espaço e energia dentro de um data center onde você levava seu PC e instalava na internet dedicada deles, ou mesmo dentro dos provedores que forneciam o serviço de hospedagem compartilhada.</p>
<p>Nessa primeira geração, a segurança ainda era muito ruim. Na verdade, como tudo nasceu dentro de ambientes governamentais ou acadêmicos décadas atrás, você meio que podia contar com uma etiqueta e acordo de cavalheiros. Mas quando esses sistemas passaram a ser expostos na internet, isso abriu a caixa de pandora pra hackers do mundo inteiro começar a brincar. E naquela época era muito fácil hackear os servidores, já que os sysadmins da época sabiam no máximo seguir o procedimento do manual ou dos cursos pra instalar os Linux e cPanel e colocar na internet. E acredite, naquela época ou hoje, só fazer um curso não é 10% do que você realmente vai precisar no mundo real.</p>
<p>Não era difícil achar servidores pelo mundo com porta de Telnet aberta, com usuários sem senha que você podia entrar e vasculhar a máquina e era possível até ver arquivos como /etc/passwd que tinha senhas abertas usando criptografia fraca. Em poucas horas ou dias você podia invadir um servidor. A gente baixava arquivos de senhas de servidores, usava contas em outros servidores pra deixar um programinha de brute force rodando, com rainbow tables que são basicamente tabelas de palavras pré-criptografadas que podíamos só comparar com os valores criptografados nos arquivos de senha, o que é muito mais rápido do que tentar descriptografar. Em outro episódio explico mais sobre isso.</p>
<p>E no lado do servidor web tínhamos um problema. Já sabemos que um Apache funciona fazendo forks de si mesmo ou usando threads. Também sabemos que pra um Apache rodar PHP bastava rodar como CGI ou instalar coisas como um mod_php. Já sabemos como CGI puro é extremamente lento, onde cada requisição precisava subir o binário do PHP, executar o script e terminar. Então a opção do mod_php sempre foi a preferida no começo.</p>
<p>Quando viesse uma requisição, a execução desse script se dava dentro do espaço de memória do processo do Apache. Pelos virtual hosts o processo Apache sabe como responder a requisições de diferentes sites. Embora os usuários no Linux sejam isolados, o processo do Apache é compartilhado, ou seja, o Apache roda com um usuário e grupo, por exemplo chamado “apache”. Daí a pasta na home do usuário precisava dar permissão pra esse grupo e era assim que o Apache acessava os arquivos dos usuários pra servir nos virtual hosts. Entenda que o servidor web é um processo único que ouve na mesma porta 80 pra todo site que ele serve, e coisas como o nome do domínio ou sub-domínio ou sub-pasta é que diferencia de que usuário buscar os html ou php.</p>
<p>E nesse começo de web onde tanto Apache, quanto PHP quando o mod_php eram bem mal feitos, ou melhor dizendo, ingenuamente mal feitos, era muito comum descobrir um bug no num desses componentes que acabava permitindo executar coisas ou acessar dados da memória do processo que o script não deveria. E fazendo isso podíamos de um usuário chegar nos dados de outro usuário usando o Apache como ponte. Hospedagens compartilhadas passaram anos sofrendo desse problema.</p>
<p>Uma forma de limitar esse problema era não executar o php via mod_php e dentro do espaço de memória do Apache inteiro. Foram construídas outras versões do executável do php como o suphp que era servido pelo mod_suphp do Apache, que executava o script como o usuário e não como o apache. Havia a opção de chroot que mudava a raíz do root ou raíz de diretório, de tal forma que o script não conseguisse acessar o root de verdade do sistema e acabar acessando arquivos que não deveria. Depois de várias tentativas ao longo dos anos, no final a opção menos pior era rodar os scripts como FastCGI com a vantagem que diferente de CGI ele mantém o executável rodando, evitando ter que reiniciar toda hora, e ele podia rodar sob a permissão do usuário em vez da permissão mais global do Apache.</p>
<p>Eu expliquei tudo isso pra explicar que nesse período tínhamos um dilema pra resolver: o hardware e internet eram muito caros. Então você queria usar o máximo possível dos poucos recursos disponíveis. Rodar a opção mais performática como mod_php causava problemas de segurança. Mas a opção menos pior que é FastCGI usava muito mais memória porque agora você precisava ter no mínimo uma instância do PHP por usuário com sites. E em ambientes de hospedagem compartilhada você gostaria de minimizar o uso de memória pra enfiar o máximo possível de usuários por servidor. É uma grande dilema e ficamos presos nisso por muitos anos, do fim dos anos 90 e a década dos anos 2000 inteiro.</p>
<p>E isso foi um problema porque pra muita gente, essa foi a escola mais acessível pra aprender sobre web. Você pagava, sei lá, 10 reais, e tinha um espaçozinho pra jogar um site no ar. E você tinha basicamente coisas como ASP clássico e PHP, configurados em algo como FastCGI que significava que seu site era rápido mais ou menos e de vez em quando o servidor precisava matar seu processo FastCGI se tivesse pouco uso, pra conservar memória e deixar outros usuários rodarem, ou seja, de vez em quando sua requisição ficava lenta esperando um novo processo carregar.</p>
<p>Nesse caso você gostaria de fazer apps leves, com pouco código, porque quanto mais código mais demorado ia ficar. E fora isso o único jeito que se tinha na época pra subir suas apps era conectar via FTP e ficar jogando seus scripts php e imagens em alguma pasta do seu usuário onde o virtual host do apache estava configurado pra ler. Além disso você sempre estava limitado à versão do php e bibliotecas do sistema operacional que o provedor de hospedagem compartilhada oferecia. Se precisasse de alguma biblioteca binária compartilhada do sistema, teria que pedir pro suporte instalar pra você, talvez.</p>
<p>Então até a virada do século você tinha a opção barata e ultra limitada de hospedagens compartilhadas ou precisava se virar com seu próprio servidor, seja numa empresa com infraestrutura própria, o que era raro, ou colocando seu servidor em co-locations em data centers. Nesses casos precisava você mesmo configurar tudo manualmente do zero e largar uma porta de telnet, ftp ou ssh abertos pra conseguir conectar de fora via internet, numa época em que internet 24 por 7 ainda não era uma realidade pra grande maioria das pessoas. Nessa época também era muito comum você ter os papéis dos administradores de banco de dados e administradores de rede.</p>
<p>A partir do ano 2000 algumas linhas de pesquisa e experimentação começaram a dar frutos. Eu falei rapidamente do chroot que era uma forma de limitar até onde no filesystem seus processos poderiam acessar. Um conceito maior se chama “Jail” literalmente prisão e foi no FreeBSD em 2000 que começou a ser possível particionar os recursos do mesmo sistema operacional nessas jails, de forma a isolar os processos, fazendo eles pensarem que estavam sozinhos na máquina, sem ter acesso a outras jails. E ele implementou isso de tal forma que jails podiam ter seu próprio endereço IP separado.</p>
<p>Em 2001 tivemos o Linux VServer, que eram patches na kernel do Linux que permitiam algo parecido com os Jails de FreeBSD pra particionar o sistema. Esse sistema durou até 2006 mais ou menos. Novamente o objetivo era limitar o que um processo pode fazer, que recursos e quais recursos pode acessar, de tal forma que um processo mal intencionado ou bugado não conseguisse derrubar o sistema operacional inteiro.</p>
<p>Quase no fim da sua vida, em 2004 a Sun lançou os containers de Solaris. Em conceitos é também parecido com os Jails de FreeBSD ou o VServer de Linux, criando o que ele chama de zonas, que são restrições ao que se podia fazer no sistema no nível do sistema operacional. Solaris naquela época ainda era uma referência tendo recursos como zonas somado ao lendário filesystem ZFS. E no mundo Linux surgiu algo similar chamado Open VZ, o famoso Virtuozzo.</p>
<p>Jails, Containers, Zonas, nenhum deles é o que a maioria das pessoas pensa quando falamos em virtualização. Tudo isso que eu vou chamar só de containers são basicamente patches na kernel do sistema operacional para possibilitar restringir os recursos do sistema aos processos. Na verdade a gente faz a kernel literalmente mentir pros processos. Se a máquina real tem 10 cores, a kernel mente pro processo e diz que só tem 1 pra ele. Se a máquina tem 64GB de RAM, a kernel mente de novo e diz que só tem 4 GB. Se tem outros 50 processos rodando na máquina, quando o processo pedir a lista, a kernel mente e esconde todos os outros processos. Então o processo é basicamente enganado e a essa mentira da kernel damos o nome de container.</p>
<p>Mas se falarmos de virtualização de verdade, a história vai bem mais longe, pra década de 60 e os mainframes. Como já expliquei, nessa época as máquinas eram proibitivamente caras e inacessíveis e a única forma de múltiplas pessoas conseguirem usar a mesma máquina era aproveitar as diversas pausas que o sistema operacional precisa fazer como esperar um arquivo gravar ou dados serem transferidos na rede e aproveitar pra computar outra coisa de outro usuário, o conceito de time sharing. Mas os pesquisadores de mainframe foram um passo além. Quando as máquinas começaram a ficar mais poderosas, eles começaram a particionar a máquina pra rodar mais de uma kernel de cada vez, essencialmente tendo múltiplos sistemas operacionais rodando em paralelo.</p>
<p>No caso de mainframes, cada máquina virtual tinha acesso a recursos reais próprios como canais de I/O, dessa forma eles são extremamente eficientes. Nos anos 80 era praticamente impossível falar de virtualização nos computadores caseiros de 8 ou 16 bits. Simplesmente não havia nem poder computacional e nem recursos suficiente pra se pensar em rodar múltiplos sistemas operacionais na mesma máquina. Não tinha recurso suficiente male male pra rodar 2 programas de uma só vez.</p>
<p>Nos anos 90 começamos a brincar mais com esse conceito à medida que as máquinas foram ficando mais poderosas e mais acessíveis. Pense que no fim dos anos 80 as máquinas mais poderosas do mundo doméstico tinham 33Mhz e no fim dos anos 90 estávamos pra quebrar a barreira do 1Ghz. Ou seja, em uma década o poder computacional teórico ficou 30 vezes maior a um preço similar. Além disso nos anos 90 tivemos o nascimento do Linux o que abriu as portas pra desenvolvedores poderem experimentar os conceitos que já existiam no mundo dos mainframes na plataforma baixa.</p>
<p>O conceito é razoavelmente simples. Todo processo faz as tais syscalls como já expliquei. Além disso processos normais rodam num ambiente mais restrito, o tal Ring-3 que também já expliquei antes. Ou seja, um processo no Ring-3 não tem permissão pra chamar funções da CPU que só a kernel no Ring-0 tem acesso. Porém, e se fizéssemos um programa especial que por sua vez carrega seus programas? E se esse programa especial respondesse a syscalls restritas mentindo as respostas? O conceito todo de virtualizar é responder uma mentira. Por exemplo, se você tentar carregar uma kernel no Ring-3 ela não vai funcionar porque vai querer fazer chamadas de Ring-0 pra acessar o hardware real, que não existem na Ring-3.</p>
<p>Mas se esse programa especial responder a essas chamadas e mentir, a tal kernel no Ring-3 ou user land vai “acreditar” que está com acesso à máquina real. Fazendo uma tangente na filosofia isso me lembra o demônio malicioso de Descartes ou a Alegoria das Cavernas de Platão ou simplesmente o experimento do cérebro no jarro. Nosso cérebro é como se fosse um computador, e seu I/O ou input e output são nossos 5 sentidos, o tato, audição, visão, paladar e olfato. Nosso corpo humano funciona como os teclado e mouse desse computador. E se fosse possível tirar o cérebro do nosso crânio e colocar num jarro. E agora fosse possível ligar o I/O do cérebro ao de um computador, mandando sinais elétricos criados por esse computador.</p>
<p>Para o cérebro, se os sinais fizerem sentido, ele não tem como saber que não está num corpo humano de verdade, ele “acredita” que tem um corpo, só que virtual. Nós poderíamos criar mentiras no computador e enviar esses sinais direto pro cérebro. Poderíamos inventar que estamos no Havaí e mandar as imagens do cenário de praia, o cheiro da água do mar, a brisa batendo na pele, a barulho das águas batendo nas pedras e pra todos os efeitos o cérebro vai acreditar que está no Havaí. E é exatamente isso que fazemos com o tal programa especial, ele vai mentir pro processo que é o cérebro e o processo vai acreditar que está onde esse programa especial disser que ele está.</p>
<p>O sistema operacional, pra todos os efeitos e propósitos, é um supervisor de processos. Ele é responsável por gerenciar os recursos da máquina e tornar esses recursos disponíveis pros processos da forma mais eficiente possível. Se o sistema operacional é um supervisor, o que seria um supervisor de um supervisor? Que tal um hyper visor? E é isso que o tal programa especial que eu falei é: um hypervisor.</p>
<p>Temos 2 grandes tipos de hypervisors, o tipo 1 que seriam instalados no bare metal, ou seja, na máquina real de verdade, antes do sistema operacional. Exemplos disso são o VMware vSphere, Citrix XenServer, ou Microsoft Hyper-V. Os hypervisors tipo 2 são os que rodam por cima do sistema operacional, como um programa normal, por exemplo os Virtualbox, VMware Workstation, Parallels Desktop, QEMU.</p>
<p>No lado do mainframe o tipo 1 já era comum nos anos 90, mas nos computadores domésticos começamos a explorar o tipo 2 primeiro já que era o mais simples de fazer. Basicamente simular uma CPU em software, um programa especial que responde a syscalls da Ring-0, mentindo pros outros programas. Se você prestou atenção nos episódios anteriores, sabe que fazer syscalls da Ring-3 pro Ring-0, passando de userland pra kernel-space toda hora é uma coisa custosa. E como você está simulando os recursos da máquina, vai precisar toda hora se comunicar com o sistema operacional pra efetuar a operação de verdade. Portanto é impossível simular um Pentium de 100Mhz em cima de um Pentium de 100Mhz. Talvez você consiga simular um Pentium de 10Mhz em cima de um Pentium de 100Mhz. Quando você virtualiza a CPU inteira e roda outro sistema operacional inteiro por cima, você ganha o peso de dois sistemas operacionais inteiros: o Guest ou convidado que é a kernel enganada, e o Host ou hospedeiro que é o sistema de verdade por baixo e entre eles você tem o programa especial, o Hypervisor Tipo-2. É bastante peso.</p>
<p>Em 1999 tivemos dois grandes lançamentos que eu acho interessantes: estávamos vendo o nascimento da VMWare trazendo produtos pra virtualizar um Linux em cima do Windows ou vice versa, virtualizando o sistema operacional: um hypervisor tipo 2. E em paralelo víamos o lançamento de um dos filmes mais influentes daquela geração que foi Matrix. Basicamente a Matrix virtualizou nosso mundo, eu poderia dizer que a Matrix era um hypervisor tipo 1, que podia rodar um sistema operacional chamado “mundo real”. E do jeito como eu gosto de pensar, também rodava Zion. Só que havia backdoors entre os dois mundos, então era um hypervisor com bugs propositais e os processos podiam atravessar entre os dois sistemas virtualizados.</p>
<p>Deixando a nerdice de lado um pouco, a diferença entre os containers como os Jails de BSD ou OpenVZ é que você rodava os processos de forma restrita e isolada mas todos no mesmo sistema operacional. A vantagem da virtualização com hypervisors era recriar uma máquina virtual literalmente, onde um novo sistema operacional podia ser instalado e os programas rodam como se estivessem numa máquina de verdade. Obviamente a primeira solução é muito mais rápida, mas se você quisesse rodar um programa de Windows em cima de um Linux, isso só seria possível simulando o hardware, mas ao mesmo tempo seria extremamente lento.</p>
<p>Simular uma máquina inteira do zero é o que chamamos de Full Virtualization, ou seja, do ponto de vista do sistema operacional ele está na Matrix, ele não tem como saber que a máquina por baixo não é de verdade já que o hypervisor está mentindo nas syscalls pra ele. Só que a vida de um mentiroso é muito difícil, como já diria o senso comum. Você não devia mentir sozinho, em vez disso, podemos fazer um conluio, uma conspiração mesmo. No fim basta o processo rodando acreditar que está numa máquina de verdade. Então poderíamos modificar o sistema operacional pra só ele saber que está numa máquina virtual, criando um acordo entre ele e o hypervisor. Então nas syscalls mais difíceis de mentir, o sistema operacional pede ajuda direto pro hypervisor e assim as coisas ficam mais rápidas. No caso criou-se um padrão de interfaces do hypervisor que o sistema operacional tinha conhecimento. Versões de Windows e Linux foram modificadas diretamente pra acessar essas APIs e a isso demos o nome de Paravirtualização.</p>
<p>Mas o problema é conluios dependem das duas partes fazerem sua parte na mentira, e nesse caso você não podia instalar qualquer sistema operacional de qualquer versão nesses hypervisors de paravirtualização como o Xen. O sistema operacional precisava saber mentir e ter patches pra participar do conluio. A VMware tinha o padrão Virtual Machine Interface ou VMI que por um tempo passou a vir na kernel do Linux. Mas ela foi removida por conta do próximo grande acontecimento a partir de 2005 e 2006.</p>
<p>Ao mesmo tempo quase, de forma independente, a Intel e a AMD resolveram estender o conluio pra dentro do hardware da CPU. No final, é muito lento deixar o hypervisor mentir sozinho, mas se a CPU participar da mentira, ela se torna quase tão rápido quanto a verdade. A AMD lançou extensões às instruções binárias da CPU que chamou de AMD-V e a Intel lançou a mesma coisa mas chamando de VT-X, parece mentira mas são apenas 10 novas instruções que fazem o sistema operacional virtualizado achar que está na Ring-0, que é todo o objetivo dos esforços de virtualização. Essas 10 instruções aqui embaixo
(VMPTRLD, VMPTRST, VMCLEAR, VMREAD, VMWRITE, VMCALL, VMLAUNCH, VMRESUME, VMXOFF, and VMXON)</p>
<p>Essa evolução dos processadores funcionou tão bem que a performance de muitas aplicações chegou a aproximar a performance nativa sem virtualização em alguns casos. Foi em 2005 que as plataformas baixas finalmente chegaram mais próximos dos mainframes dos anos 70 que rodavam sistemas como o System/370.</p>
<p>Vamos recapitular, no meio da primeira década dos anos 2000 já começamos a ter hypervisors tipo 1 que podia rodar múltiplos sistemas operacionais em paralelo no bare metal. E também tínhamos a opção de hypervisors tipo 2 pra rodar um sistema hospedeiro leve e por cima dele rodar múltiplos sistemas operacionais também. Isso começou a se tornar extremamente importante pra data centers onde os servidores sempre foram difíceis de dar manutenção e dividir os recursos entre diferentes aplicações, de diferentes clientes, rodando na mesma máquina, com segurança e performance. Com as novas instruções VT-X e AMD-v dando performance suficiente, começou de fato o mercado de virtualização da infraestrutura.</p>
<p>De qualquer forma, voltando aos servidores dava trabalho montar um servidor novo. Houve uma época que se eu precisasse de um servidor, eu tinha que abrir uma ordem de serviço no data center da empresa ou do provedor. Eles iam levar 1 ou 2 dias, pra reservar um hardware, instalar e configurar um sistema operacional, ligar monitoramento, conectar na rede e deixar disponível pra mim. E essa máquina ia ficar a maior parte do tempo sem fazer nada, e mesmo quando estivesse fazendo nunca ia usar todos os recursos ao máximo. Por isso era tão caro: pra provisionar era caro, e a máquina era um grande desperdício na maioria do tempo.</p>
<p>Os VPS ajudaram a aliviar esses dois problemas. Como os hypervisors podem ser automatizados, agora bastava instalar o sistema operacional uma vez e fazer um snapshot, ou seja copiar os dados que compõe a instalação num arquivão, e a isso chamamos de Imagens. Como as máquinas são virtuais e são sempre as mesmas, as imagens são sempre compatíveis sem problemas de coisas como drivers. Quem tinha que se preocupar com drivers e hardware que podia mudar (tipo quando trocávamos uma placa de rede com defeito) era o sistema hospedeiro ou o hypervisor tipo 1, ou seja, só quem roda em bare metal. Portanto, via uma interface web simples e passando um número de cartão de crédito, em poucos minutos eu podia ter uma máquina virtualizada ligada na rede pronta pra usar. Foi uma evolução impressionante no começo dos anos 2000.</p>
<p>Tudo isso ajudava uma pessoa a criar uma máquina, mas ainda não ajudava tanto as grandes empresas que precisavam controlar coisas realmente complexas e distribuídas com múltiplos servidores. Quando era só uma máquina, alguém da empresa entrava nela de vez em quando pra ficar dando manutenção, instalando coisa nova e rodando atualização. Mas se você tinha 5, 10 ou 100 máquinas, precisava de uma equipe inteira pra ficar manualmente atualizando o sistema ou consertando problemas. Era tosco lidar com equipes de TI fazendo tudo na mão nessa época. A Microsoft tinha serviços no Windows Server pra tentar automatizar as coisas, mas vou te dizer que ter que lidar com Active Directory sempre foi um pé no saco e é até hoje uma das tecnologias que eu mais detestei ter que lidar, era tipo uma extensão do Registry pra servidor, ambas que só dão dores de cabeça. Eu entendo os motivos e a utilidade, mas continua sendo uma das soluções com a pior usabilidade desde a época dos mainframes.</p>
<p>Uma outra categoria de software começou a surgir, principalmente a partir de 1993 quando foi lançado pela primeira vez, ou a partir de 1998 quando saiu a versão 3 do software conhecido como CF Engine. Qualquer administrador de sistemas Linux trabalhando com vários servidores, por exemplo nos provedores de hospedagens compartilhadas como eu expliquei, usaram CF Engine 3 em algum momento. Claro, a maioria não sabia o que era isso porque as pessoas têm a tendência de não estudar ou querer fazer coisas complicadas repetidas vezes pra mostrar manja. Mas os realmente bons são preguiçosos: nós não gostamos de fazer a mesma coisa repetidas vezes e preferimos fazer um software pra fazer as coisas chatas por nós. CF Engine é isso: um estagiário de administrador de sistemas que tem as receitas pra instalar a mesma máquina do mesmo jeito todas as vezes, num único comando.</p>
<p>O CF Engine funciona com uma linguagem declarativa que descreve uma promessa. Na prática é a descrição do que o servidor deveria ter instalado (os diversos pacotes e binários) e como esses componentes deveriam ser configurados (ordens pra editar os arquivos de configuração como os que ficam no famoso diretório /etc) e quais serviços deveriam ser inicializados pelo systemd logo que a máquina inicia. O CF é um automatizador, em vez de você ter que entrar máquina a máquina e ficar digitando dezenas de comandos pra fazer tudo isso, e ficar esperando comando a comando executar; você instala no máximo um sistema operacional zerado, instala o agente do CF e faz ele conectar no servidor de CF. De lá o servidor por enviar essas promessas pra nova máquina e fazer tudo que precisa pra que fique do jeito que você precisa.</p>
<p>Avançando rapidamente na história, na sequência tivemos o Puppet lançado em 2005 e escrito em Ruby, o que possibilitou usar as capacidades de criar sub-linguagens, que chamamos de DSLs ou Domain Specific Languages pra criar o equivalente das promessas de CF. Em 2009 também em Ruby tivemos o lançamento do Chef que é o motor de produtos como o AWS Opscode por exemplo.</p>
<p>A grande vantagem de sistemas como CF Engine, Chef ou Puppet é descrever infraestrutura como código. Dessa forma, se os pré-requisitos fossem os mesmos, você podia não só mandar rodar a mesma receita várias vezes e garantidamente ter a mesma máquina no final como podia compartilhar essas receitas da mesma forma como compartilhamos código open source e a comunidade podia ajustar essas receitas, fazendo ficar melhor do ponto de vista de minimizar as coisas que eram instaladas ou como melhorar as configurações pra ficar mais seguras. Em vez de todo administrador de sistemas ter que reinventar a roda do zero, podia já partir de uma base comum.</p>
<p>Sistemas como o CF, Puppet ou Chef seguem uma arquitetura de cliente-servidor onde as novas máquinas ganham agentes que são processos que ficam rodando e escutando ordens vindas de um servidor. Assim era mais simples provisionar novas máquinas, bastava que no processo a imagem do sistema operacional viesse com o agente e quando ela plugasse na rede podia ser vista pelo servidor e iniciar o processo de enviar as receitas pra instalar o que fosse necessário. Uma coisa que muito iniciante reclamava e ainda reclama é que esses sistemas tinham pelo menos 2 componentes: os agentes e o servidor, então tinha bastante coisa pra configurar antes de ter a primeira máquina automaticamente configurada. Se seu trabalho era configurar dezenas de máquinas, tudo bem, mas se você só queria configurar uma única máquina, era mais trabalho do que você gostaria de ter.</p>
<p>Antes desses sistemas ficarem mais populares o mais comum era algum estagiário manualmente instalar o sistema operacional, configurar a rede e plugar a máquina rede, deixando pelo menos o Telnet ou mais recentemente o servidor de SSH configurado. Pra quem não sabe, a porta 23 sempre foi do Telnet que é uma forma de acessar o terminal do servidor remotamente. Telnet é o teletype network e na prática você podia se logar remotamente da sua máquina num servidor Unix ou Linux.</p>
<p>Esse protocolo é antigo, do fim dos anos 60 então quando a internet começou a se popularizar ficou bem claro que era muito inseguro. Se você não sabe disso é muito simples fazer uma coisa chamada sniffing que é ouvir os pacotes TCP trafegando na rede e ver o que tem dentro deles, se o dado estiver aberto você pode facilmente interceptar e ler esses pacotes. Protocolos de TCP como Telnet, FTP, SMTP, HTTP todos são muito fáceis de sniffar e por isso viemos gradativamente substituindo o uso deles por outros mais seguros como o SSH, SFTP, SMTP com TLS, e HTTPS. SSH que todo mundo conhece hoje em dia e é exposto na porta 22.</p>
<p>Assim como Telnet, você pode abrir um terminal remoto interativo via SSH e ficar digitando comandos manualmente. Ou você pode automatizar o SSH e usar ele como um túnel. Ou seja, você pode criar um programa que envia comandos ao cliente de SSH sem que você precise abrir o terminal interativo; como um programa de linha de comando. Em cima desse conceito, podíamos criar arquivos de texto com vários comandos que queremos rodar no servidor. O SSH funciona também pra transferir arquivos de forma encriptada como um FTP melhorado. Então podemos mandar um script de comandos pro servidor e depois mandar executar esse script no servidor.</p>
<p>Se você pensou 2 segundos, isso possibilita outra coisa: automatizar a instalação e configuração de novos servidores. Todo novo sistema operacional de servidor que se preza já deixa pré-instalado um servidor como o OpenSSH. Com CF, Chef ou Puppet precisamos deixar também instalados os seus programas agentes. Mas só com SSH já dá pra fazer muita coisa. E assim em 2006 surge outra ferramenta do mundo Ruby, o Capistrano que antes se chamava SwitchTower. É um sistema de deployment sem agentes ou agentless. O agente no caso seria o próprio servidor de SSH. Com comandos simples você pode instalar sua aplicação em múltiplos servidores, configurados todos iguais porque você estava automatizando os mesmos comandos pra rodar em todos eles, e de quebra ele podia também configurar seu servidor de banco de dados, seu balanceador de carga e tudo mais, com a vantagem de ser mais simples que CF ou Puppet.</p>
<p>Se você que começou nos anos recentes pensa que o Ansible que foi lançado em 2015 foi uma grande novidade, na verdade ele foi só uma evolução do Fabric de Python que saiu antes dele e que por sua vez foi um clone de Capistrano. Em linhas gerais temos os gerenciadores de configuração de infraestrutura, mais complicados, que são cliente-servidor como CF, Puppet e Chef e os que são mais simples e agentless como Capistrano e Ansible. Eu diria que ambos servem propósitos diferentes pra públicos diferentes. Se você é um desenvolvedor de aplicações web que precisa subir uma aplicação simples numa máquina virtual num VPS e depois conseguir fazer deployments automatizados rápidos das atualizações e correções, você deveria usar Ansible.</p>
<p>Por outro lado, se você precisava manter 10 máquinas web, 3 máquinas de bancos de dados, 1 servidor de cache e coisas assim, era melhor organizar sua infraestrutura em conjuntos de receitas usar coisas como Chef pra criar e atualizar essas máquinas. As pessoas pensam que infraestrutura é só instalar a máquina, colocar na rede e acabou. Longe disso, você precisa atualizar o sistema operacional com correções de segurança, você precisa subir atualização das aplicações que rodam nelas. Mais importante: se uma máquina é invadida ou mesmo dá algum problema inexplicável, não vale a pena tentar consertar, você precisa preparar a arquitetura pra que seja possível destruir a máquina por completo e recriar uma nova pra colocar no lugar. Manualmente é um trabalho de corno e você vai errar muitas vezes esquecendo de instalar ou configurar alguma coisa, mas automatizado você sempre garante que a máquina vai ser montada da forma certa.</p>
<p>Essas ferramentas de automatização não só precisavam instalar máquinas mas também precisavam garantir que as premissas de segurança estão sendo cumpridas. Que as políticas certas estão aplicadas, que as permissões certas de usuários foram feitas, que as máquinas estão nas vlans corretas e que as regras de firewall também estão corretas. Esse conhecimento veio sendo automatizado também.</p>
<p>Agora pense assim, o começo do século XXI foi extremamente caótico com a bolha da internet que explodiu em 2001. Passamos uns 2 ou 3 anos de depressão, sem nada muito interessante realmente acontecendo. Mas então a controvérsia do Napster vs a indústria da música, o lançamento do iPod e iTunes, começou a trazer o entretenimento de massa pra internet, bandas independentes começaram a se lançar via internet e a hegemonia da MTV como influenciador começou a desaparecer quando as redes sociais de primeira geração, como MySpace, começaram a tomar a internet de assalto. Se você não tem noção desse efeito pense que em 2006 MySpace foi mais visitado do que o próprio Google, consegue imaginar isso?</p>
<p>Já em 2008 o Facebook destronou o MySpace, então imaginem dois titãs em disputa. 2008 que eu já expliquei que foi a época da maior crise econômica dos tempos modernos mas no lado da internet as coisas estavam esquentando absurdamente, principalmente em Silicon Valley. Foi em 2005 que o Netflix pivotou pra oferecer vídeo streaming em vez de entregar DVD, foi em 2006 que o Google comprou o YouTube, em 2007 que nasceu Twitter, em 2008 que Facebook despontou como a liderança das redes sociais, também em 2008 que a Apple inaugurou o iPhone App Store, em 2009 que surgiu o Uber, em 2010 que surgiu o monstro Groupon, que já praticamente morreu. Ou seja do meio pro final dos anos 2000 tivemos uma aceleração abrupta que não se via desde o fim dos anos 90. E é por isso que esse foi o período que mais tivemos evolução nas tecnologias.</p>
<p>Diferente dos anos 90, agora estávamos num ponto onde as pessoas comuns já tinham acesso à internet em casa e partir dos anos 2010 começamos a colocar a internet no bolso das pessoas, acesso de qualquer lugar, a qualquer momento. E com o advento dos smartphones e das redes sociais criamos uma massa gigantesca de pessoas que querem ficar online o tempo todo, e mesmo tendo coisas como ecommerce antes, a escala que os unicórnios atingiram exigiam muito. Ecommerces tem horário e dias pra picos. Redes sociais tem picos o tempo todo. Instalar máquinas na mão dentro de uma Netflix seria completamente inviável. Por isso muitas das novas tecnologias vieram exatamente desses unicórnios. Pra piorar a partir de 2011 ainda começamos a adotar mais e mais tecnologias que ajudavam a manter as pessoas online mais tempo, como WebRTC pra comunicações e WebSockets.</p>
<p>Até então a internet era feita pra ser desconectada. Você podia carregar um site com seus conteúdos, desconectar da internet e continuar lendo. Não era prático, mas o protocolo HTTP foi feito pra não ter estado. Você baixa, renderiza no navegador e acabou. Mas com WebRTC e WebSockets você passou a gerenciar estados e precisa estar online pra tudo funcionar, coisas como Messenger, Slack, Whatsapp, só fazem sentido num ambiente onde as pessoas estão online ao mesmo tempo na maior parte do tempo.</p>
<p>Entenda uma coisa importante: a grande maioria das boas práticas e recomendações que você vê na internet hoje vinda de engenheiros de um Netflix, de um Twitter, de um Facebook, ou de um Google, só fazem sentido pra eles na maior parte do tempo. Ouça isso com muita atenção: você não é o Netflix, tá muito longe. O Netflix levou anos pra ser o Netflix. Os unicórnios são chamados de unicórnios porque são quase impossíveis se você tentar replicar e precisam de vários fatores incontroláveis incluindo estar no lugar certo na hora certa, pra acontecer. Portanto não é porque você usa uma tecnologia do Google que vai te levar um pouco mais próximo de ser um mini Google. Não existe correlação, esqueça essas fantasias.</p>
<p>Portanto não, você não deveria usar um Apache Mesos, tecnologia criada pelo Twitter, na sua infraestrutura de 3 máquinas na sua micro-empresa que você gosta de chamar de tech startup. Toda vez que você ouvir o argumento “devíamos usar isso porque o Twitter usa” ignore completamente essa pessoa, ela não tem idéia do que está falando e é uma pessoa perigosa.</p>
<p>Isso tudo dito o fato é que de repente começaram a surgir empresas que estavam escalando absurdamente rápido num ambiente onde a maioria dos data centers disponíveis não sabiam como atender essa demanda. Era muito demorado manualmente instalar e configurar máquinas. Mesmo com ferramentas como CF, Chef eles estavam atingindo limites. Pense assim: num ambiente cliente e servidor, onde de repente você tem mil máquinas batendo num servidor de configurações como o CF e você vai engargalar a rede se precisar baixar um binário grande pra mil máquinas na rede interna, vai começar a engargalar o servidor de configurações. Novas arquiteturas precisaram ser criadas.</p>
<p>Estamos chegando na era mais acelerada do mundo de infraestrutura web que eu argumentaria que foi a partir de 2006 até pelo menos 2015. Se você acompanhou os últimos episódios sabe o quanto de coisas saiu nessa década e se não assistiu agora é a hora de ver porque chegamos ao final deste episódio. No próximo episódio quero contar como as tecnologias que mencionei hoje vão culminar em coisas como Docker ou Kubernetes que você tanto ouve falar em tudo que é blog ou evento de tecnologia.</p>
<p>Eu espero que vocês tenham ganhado um contexto melhor quanto ouvirem sobre essas tecnologias. Eu falei de muitos conceitos então é natural que vocês tenham dúvidas, não deixem de mandar nos comentários pra galera poder ajudar também. Se curtiram o vídeo mandem um joinha, compartilhem com seus amigos, assinem o canal e não esqueçam de clicar no sininho pra não perder os próximos episódios. A gente se vê semana que vem, até mais!</p>
]]></content:encoded><category>devops</category><category>automacao</category><category>cloud</category><category>akitando</category></item><item><title>SmallTips: Rodando o GitLab CI Runner Localmente</title><link>https://akitaonrails.github.io/2018/04/28/smalltips-rodando-o-gitlab-ci-runner-localmente/</link><guid isPermaLink="true">https://akitaonrails.github.io/2018/04/28/smalltips-rodando-o-gitlab-ci-runner-localmente/</guid><pubDate>Sat, 28 Apr 2018 18:35:00 -0300</pubDate><description>&lt;p&gt;Se tem uma coisa que você sempre deve fazer é manter sua suíte de testes. A cada bug corrigido, a cada nova funcionalidade, adicione novos testes.&lt;/p&gt;
&lt;p&gt;Nas últimas semanas trabalhei muito pesado, literalmente codificando sem parar, sete dias por semana. Lembra daquela história de &amp;ldquo;para de reclamar que não tem tempo, o que você faz da meia-noite às 6 da manhã?&amp;rdquo;? Pois é, isso não se aplica a mim.&lt;/p&gt;
&lt;p&gt;E é justamente quando você está com sono e exausto que começa a cometer erros. Mesmo que o seu código rode localmente, você vai se ferrar em uma dessas sessões de código às 3 da manhã.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Se tem uma coisa que você sempre deve fazer é manter sua suíte de testes. A cada bug corrigido, a cada nova funcionalidade, adicione novos testes.</p>
<p>Nas últimas semanas trabalhei muito pesado, literalmente codificando sem parar, sete dias por semana. Lembra daquela história de &ldquo;para de reclamar que não tem tempo, o que você faz da meia-noite às 6 da manhã?&rdquo;? Pois é, isso não se aplica a mim.</p>
<p>E é justamente quando você está com sono e exausto que começa a cometer erros. Mesmo que o seu código rode localmente, você vai se ferrar em uma dessas sessões de código às 3 da manhã.</p>
<p>A suíte de testes foi minha salvação nesses momentos — meu copiloto, me trazendo de volta à razão sempre que a noite ficava pesada demais.</p>
<p>Mantenho meu próprio servidor GitLab para todos os projetos de clientes e internos da empresa. É só um pouco de paranoia, para garantir que meu código fica comigo. E o GitLab é uma ferramenta fantástica, ainda mais porque tem <a href="https://about.gitlab.com/features/gitlab-ci-cd/"target="_blank" rel="noopener">seu próprio sistema de Integração Contínua</a>.</p>
<p>Basta adicionar o arquivo <code>.gitlab-ci.yml</code> ao projeto, criar uma branch, fazer o push e abrir um Merge Request — o CI entra em ação, com <a href="https://docs.gitlab.com/ee/ci/yaml/#jobs"target="_blank" rel="noopener">jobs paralelos</a> se você quiser. Mesmo quando eu esquecia de rodar os testes localmente, o CI não me deixava passar.</p>
<p>Manter o CI funcionando exige que você pare e cuide das specs periodicamente. Às vezes você vai se perguntar: por que esse teste passa na minha máquina mas continua falhando no servidor de CI?</p>
<p>É aí que fica muito útil rodar a imagem Docker do CI localmente, para resolver de vez aquelas inconsistências de ambiente.</p>
<p>Você consegue fazer isso rodando o próprio GitLab CI Runner. Ele lê o <code>.gitlab-ci.yml</code> do seu projeto e executa tudo localmente via Docker. Qualquer problema que aparece no servidor vai se reproduzir localmente também. Com a vantagem de que você não precisa entrar em fila caso haja outros jobs aguardando execução (muitos desenvolvedores trabalhando, poucas máquinas disponíveis para processar tudo na hora). E você não fica poluindo essa fila com jobs desnecessários.</p>
<p>Se você seguiu minhas recomendações e instalou a excelente distro Arch Linux (ou meu derivado favorito, Manjaro Gnome), pode instalar o runner pelo AUR assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>pacaur -S gitlab-runner</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Caso contrário, verifique os repositórios da sua distro ou baixe o binário <a href="https://gitlab.com/gitlab-org/gitlab-runner/blob/master/docs/install/bleeding-edge.md#download-the-standalone-binaries"target="_blank" rel="noopener">aqui</a>. Por exemplo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>wget https://gitlab-runner-downloads.s3.amazonaws.com/master/binaries/gitlab-runner-linux-amd64
sudo mv gitlab-runner-linux-amd64 /usr/local/bin
sudo chmod &#43;x /usr/local/bin/gitlab-runner</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Com o runner instalado, suponha que você tenha um trecho assim no seu <code>.gitlab-ci.yml</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">backend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">test</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">script</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">./bin/cc-test-reporter before-build</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">bundle exec rspec --exclude-pattern &#34;**/features/**/*_spec.rb&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">after_script</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">./bin/cc-test-reporter after-build --exit-code $? || true</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Você pode configurar vários jobs de teste rodando em paralelo. Recomendo separar: testes JS de front-end, testes unitários de back-end com Rspec/minitest, testes de feature com Capybara, verificações de segurança com Brakeman, por exemplo. Cada um roda em paralelo e você não precisa esperar tudo terminar se só quer ver o resultado dos testes de JS. Feedback mais rápido, correção imediata.</p>
<p>Localmente, a partir da raiz do projeto, basta rodar:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gitlab-runner exec docker backend</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Claro, assumindo que você já tem o Docker instalado e configurado. Se não tiver, leia <a href="https://wiki.archlinux.org/index.php/Docker#Installation"target="_blank" rel="noopener">o excelente artigo do Arch Wiki sobre Docker</a> — basicamente é instalar o pacote Docker e se adicionar ao grupo docker.</p>
<p>É isso! Super simples, quase sem fricção, e o runner faz tudo que o servidor GitLab CI faria.</p>
<p>Um porém: infelizmente, parece que você <a href="https://gitlab.com/gitlab-org/gitlab-runner/issues/2409"target="_blank" rel="noopener">não consegue salvar cache ou artefatos</a> entre execuções (gems, pacotes npm, etc.), então toda vez que o runner local roda, ele começa do zero. Isso o torna menos útil para uso contínuo, mas ainda é mais rápido do que fazer push para o servidor e esperar sua vez numa fila cheia.</p>
<p>Já me salvou algumas vezes.</p>
]]></content:encoded><category>devops</category><category>containers</category><category>testes</category></item><item><title>Minhas Notas sobre Deploy Produção do Ruby on Rails no Google Cloud Kubernetes Engine</title><link>https://akitaonrails.github.io/2018/01/09/minhas-notas-deploy-producao-ruby-on-rails-google-cloud-kubernetes/</link><guid isPermaLink="true">https://akitaonrails.github.io/2018/01/09/minhas-notas-deploy-producao-ruby-on-rails-google-cloud-kubernetes/</guid><pubDate>Tue, 09 Jan 2018 20:13:00 -0200</pubDate><description>&lt;p&gt;Estou usando o Google Cloud com Kubernetes Engine há dois meses e pouco, do zero à produção. Na prática, não levei um mês para montar tudo, mas levei mais um mês para descobrir algumas arestas bem chatas.&lt;/p&gt;
&lt;p&gt;TL;DR: O Google está fazendo um trabalho razoavelmente bom como contrapeso para a AWS não se acomodar. Se você já sabe tudo sobre AWS, eu te encorajo a testar o Google Cloud. Provavelmente por memória muscular, eu ainda me sentisse mais confortável com a AWS, mas agora que me forcei a passar pelo processo de aprendizado, estou bastante confiante com Google Cloud e Kubernetes para a maioria dos meus cenários.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Estou usando o Google Cloud com Kubernetes Engine há dois meses e pouco, do zero à produção. Na prática, não levei um mês para montar tudo, mas levei mais um mês para descobrir algumas arestas bem chatas.</p>
<p>TL;DR: O Google está fazendo um trabalho razoavelmente bom como contrapeso para a AWS não se acomodar. Se você já sabe tudo sobre AWS, eu te encorajo a testar o Google Cloud. Provavelmente por memória muscular, eu ainda me sentisse mais confortável com a AWS, mas agora que me forcei a passar pelo processo de aprendizado, estou bastante confiante com Google Cloud e Kubernetes para a maioria dos meus cenários.</p>
<p>Aviso completo: não sou especialista, então leve o que digo com um grão de sal. É um daqueles assuntos que tenho muita vontade de falar, mas também sou muito cuidadoso com a escolha das palavras para você não ter uma ideia errada sobre as soluções propostas.</p>
<p>O objetivo desse exercício é principalmente guardar alguns trechos e pensamentos para consulta futura. Então tenha em mente que isso também não é um tutorial passo a passo. Minha primeira intenção foi ir por esse caminho, mas aí percebi que seria quase como escrever um livro inteiro, então não dessa vez.</p>
<p>Para ter sucesso com algo como Google Cloud e Kubernetes, você <strong>precisa</strong> ter experiência em infraestrutura. Se você nunca instalou boxes Linux de nível servidor do zero, se nunca fez otimizações de servidor, se não está confortável com componentes bare-metal do lado do servidor, não tente um deploy real em produção. Sua aposta mais segura ainda é algo como o Heroku.</p>
<p>Você tem que ser o tipo de pessoa que gosta de fuçar nas coisas (como provavelmente você já me leu fazendo em <a href="http://www.akitaonrails.com/linux"target="_blank" rel="noopener">posts anteriores</a> do blog).</p>
<p>Não sei tudo, mas sei o suficiente. Então só precisei descobrir quais peças se encaixariam melhor nas minhas necessidades. Você <strong>precisa</strong> definir suas necessidades antes de tentar escrever seu primeiro arquivo YAML. Planejamento é crucial.</p>
<p>Antes de tudo, isso é o que eu queria/precisava:</p>
<ul>
<li>Camada de aplicação web escalável, onde eu pudesse fazer tanto rolling updates (para atualizações com <strong>zero downtime</strong>) quanto escalamento horizontal automático e manual dos servidores.</li>
<li>Storage persistente montável <strong>com</strong> snapshots/backups automáticos.</li>
<li>Banco de dados robusto gerenciado (Postgresql) com backups automáticos e <strong>replicação fácil</strong> para instâncias somente leitura.</li>
<li>Solução gerenciada para armazenar secrets (como o suporte a ENV do Heroku). Nunca armazene configuração de produção no código-fonte.</li>
<li>Suporte a imagens Docker sem precisar construir infraestrutura customizada para deploy.</li>
<li>Endereços IP externos estáticos para integrações que exigiam IP fixo.</li>
<li>Terminação SSL para poder conectar ao CloudFlare (CDN é obrigatório, mas não suficiente — em 2018 precisamos de algum nível de proteção contra DDoS).</li>
<li>Segurança suficiente por padrão, para que tudo fique — em teoria — bloqueado a menos que eu decida abrir.</li>
<li>Alta disponibilidade em diferentes regiões e zonas de data center.</li>
</ul>
<p>É fácil fazer deploy de uma aplicação web demo simples. Mas eu não queria uma demo, queria uma solução de nível produção para o longo prazo. Melhorias na minha implementação são muito bem-vindas, então fique à vontade para comentar abaixo.</p>
<p>Alguns dos problemas para quem está começando:</p>
<ul>
<li>A documentação é <em>muito</em> extensa, e você vai encontrar <em>quase</em> tudo — se souber o que está procurando. Também tenha em mente que Azure e AWS também implementam Kubernetes com algumas diferenças, então parte da documentação não se aplica ao Google Cloud e vice-versa.</li>
<li>Existem muitos recursos em estágios alpha, beta e stable. A documentação acompanha bem, mas a maioria dos tutoriais com alguns meses pode não funcionar mais como esperado (esse aqui incluído — estou assumindo Kubernetes 1.8.4-gke).</li>
<li>Existe um conjunto de palavras que se aplicam a conceitos que você já conhece mas têm nomes diferentes. Se acostumar com o vocabulário pode atrapalhar no começo.</li>
<li>Parece que você está brincando com Lego. Muitas peças que você pode misturar e combinar. É fácil de bagunçar. Isso significa que você pode construir uma configuração adaptada às suas necessidades. Mas se você só copiar e colar de tutoriais, você <strong>vai</strong> ficar preso.</li>
<li>Você pode fazer <em>quase</em> tudo através de arquivos YAML e linha de comando, mas não é trivial reutilizar a configuração (para ambientes de produção e staging, por exemplo). Existem ferramentas de terceiros que lidam com bits YAML parametrizáveis e reutilizáveis, mas eu faria tudo à mão primeiro. Nunca, jamais, tente templates automatizados em infraestrutura sem saber exatamente o que eles fazem.</li>
<li>Você tem 2 ferramentas de linha de comando <em>pesadas</em>: <code>gcloud</code> e <code>kubectl</code>, e a parte confusa é que elas nomeiam algumas coisas de forma diferente mesmo sendo as mesmas &ldquo;coisas&rdquo;. Pelo menos, <code>kubectl</code> é próximo do <code>docker</code>, se você estiver familiarizado com ele.</li>
</ul>
<p>Mais uma vez, isso <strong>NÃO</strong> é um tutorial passo a passo. Vou anotar alguns passos mas não tudo.</p>
<h3>Camada Web Escalável (a própria aplicação web)<span class="hx:absolute hx:-mt-20" id="camada-web-escalável-a-própria-aplicação-web"></span>
    <a href="#camada-web-escal%c3%a1vel-a-pr%c3%b3pria-aplica%c3%a7%c3%a3o-web" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A primeira coisa que você precisa ter é uma aplicação web totalmente compatível com os 12 fatores.</p>
<p>Seja Ruby on Rails, Django, Laravel, Node.js ou o que for. Deve ser uma aplicação totalmente shared-nothing, que não depende de escrever nada no sistema de arquivos local. Uma que você possa facilmente desligar e iniciar instâncias independentemente. Sem sessão no estilo antigo em memória local ou em arquivos locais (prefiro evitar session affinity). Sem uploads para o sistema de arquivos local (se for necessário, você terá que montar um storage persistente externo) — sempre prefira enviar streams binários para serviços de storage gerenciados.</p>
<p>Você precisa ter um <a href="https://tomanistor.com/blog/cache-bust-that-asset/"target="_blank" rel="noopener">pipeline adequado que produza cache-busting através de fingerprinting de assets</a> (e gostando ou não, o Rails ainda tem a melhor solução out-of-the-box no seu Asset Pipeline). Você não quer se preocupar com invalidar caches em CDNs manualmente.</p>
<p>Instrumente sua aplicação, adicione o <a href="https://rpm.newrelic.com"target="_blank" rel="noopener">New Relic RPM</a>, adicione o <a href="https://rollbar.com"target="_blank" rel="noopener">Rollbar</a>.</p>
<p>De novo, isso é 2018 — você não quer fazer deploy de código ingênuo com injeção de SQL (ou qualquer outro tipo de input), sem <code>eval</code> sem verificação no seu código, sem brecha para CSRF ou XSS, etc. Vá em frente, compre a licença do <a href="https://brakemanpro.com/"target="_blank" rel="noopener">Brakeman Pro</a> e adicione ao seu pipeline de CI. Posso esperar&hellip;</p>
<p>Como isso não é um tutorial, vou assumir que você é mais do que capaz de se cadastrar no Google Cloud e encontrar seu caminho para configurar um projeto, configurar sua região e zona.</p>
<p>Demorei um pouco para entender a estrutura inicial no Google Cloud:</p>
<ul>
<li>Você começa com um <a href="https://cloud.google.com/resource-manager/docs/creating-managing-projects"target="_blank" rel="noopener">Projeto</a>, que é o guarda-chuva para tudo que sua aplicação precisa.</li>
<li>Em seguida você cria <a href="https://cloud.google.com/kubernetes-engine/docs/concepts/cluster-architecture"target="_blank" rel="noopener">&ldquo;clusters&rdquo;</a>. Você pode ter um cluster de produção ou staging, por exemplo. Ou um cluster web e um cluster de serviços separado para coisas não-web, e assim por diante.</li>
<li>Um cluster tem um <a href="https://cloud.google.com/kubernetes-engine/docs/concepts/cluster-architecture#master"target="_blank" rel="noopener">&ldquo;cluster-master&rdquo;</a>, que é o controlador de tudo o mais (os comandos <code>gcloud</code> e <code>kubectl</code> falam com suas APIs).</li>
<li>Um cluster tem muitas <a href="https://cloud.google.com/kubernetes-engine/docs/concepts/cluster-architecture#nodes"target="_blank" rel="noopener">&ldquo;node instances&rdquo;</a>, as &ldquo;máquinas&rdquo; propriamente ditas (ou, mais precisamente, instâncias de VM).</li>
<li>Cada cluster também tem pelo menos um <a href="https://cloud.google.com/kubernetes-engine/docs/concepts/node-pools"target="_blank" rel="noopener">&ldquo;node pool&rdquo;</a> (o &ldquo;default-pool&rdquo;), que é um conjunto de node instances com a mesma configuração, o mesmo <a href="https://cloud.google.com/compute/docs/machine-types"target="_blank" rel="noopener">&ldquo;machine-type&rdquo;</a>.</li>
<li>Por fim, cada node instance executa um ou mais &ldquo;pods&rdquo; que são containers leves como LXC. É aqui que sua aplicação realmente vive.</li>
</ul>
<p>Este é um exemplo de <a href="https://cloud.google.com/sdk/gcloud/reference/container/clusters/create"target="_blank" rel="noopener">criação de cluster</a>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gcloud container clusters create my-web-production \
--enable-cloud-logging \
--enable-cloud-monitoring \
--machine-type n1-standard-4 \
--enable-autoupgrade \
--enable-autoscaling --max-nodes=5 --min-nodes=2 \
--num-nodes 2</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Como mencionei, ele também cria um <code>default-pool</code> com um <a href="https://cloud.google.com/compute/docs/machine-types"target="_blank" rel="noopener">machine-type</a> de <code>n1-standard-4</code>. Escolha qual combinação de CPU/RAM você vai precisar para sua aplicação específica com antecedência. O tipo que escolhi tem 4 vCPUs e 15GB de RAM.</p>
<p>Por padrão ele começa com 3 nodes, então escolhi 2 inicialmente mas com auto-scaling até 5 (você pode atualizar isso depois se precisar, mas garanta que tem espaço para crescimento inicial). E você pode continuar adicionando node-pools extras para node instances de tamanhos diferentes — digamos, para workers do Sidekiq fazerem processamento pesado em background. Então você deve criar um Node Pool separado com um machine-type diferente para seu conjunto de node instances, por exemplo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gcloud container node-pools create large-pool \
--cluster=my-web-production \
--node-labels=pool=large \
--machine-type=n1-highcpu-8 \
--num-nodes 1</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Esse outro pool controla 1 node do tipo <code>n1-highcpu-8</code> que tem 8 vCPUs com 7,2 GB de RAM. Mais CPUs, menos memória. Existe uma categoria <code>highmem</code> que tem menos CPUs com muito mais memória. De novo, saiba o que você quer com antecedência.</p>
<p>A parte importante aqui é o <code>--node-labels</code> — é assim que vou mapear o deployment para escolher entre Node Pools (neste caso, entre o <code>default-pool</code> e o <code>large-pool</code>).</p>
<p>Após criar um cluster, você precisa executar o seguinte comando para buscar suas credenciais:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gcloud container clusters get-credentials my-web-production</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso configura o comando <code>kubectl</code> também. Se você tiver mais de um cluster (digamos, um <code>my-web-production</code> e <code>my-web-staging</code>), precisa ter muito cuidado de sempre fazer <code>get-credentials</code> para o cluster correto primeiro, caso contrário pode acabar executando um deploy de staging no cluster de produção.</p>
<p>Como isso é confuso, modifiquei meu ZSH PROMPT para sempre mostrar com qual cluster estou lidando. Adaptei do <a href="https://github.com/superbrothers/zsh-kubectl-prompt"target="_blank" rel="noopener">zsh-kubectl-prompt</a>:</p>
<p><img src="https://github.com/superbrothers/zsh-kubectl-prompt/raw/master/images/screenshot001.png" alt="zsh kubectl prompt"  loading="lazy" /></p>
<p>Como você acabará tendo múltiplos clusters em uma aplicação grande, recomendo fortemente adicionar esse PROMPT ao seu shell.</p>
<p>Agora, como você faz deploy da sua aplicação nos pods dentro dessas node instances?</p>
<p>Você precisa ter um <code>Dockerfile</code> no repositório do seu projeto de aplicação para gerar uma imagem Docker. Este é um exemplo para uma aplicação Ruby on Rails:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>FROM ruby:2.4.3
ENV RAILS_ENV production
ENV SECRET_KEY_BASE xpto
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash -
RUN apt-get update &amp;&amp; apt-get install -y nodejs postgresql-client cron htop vim
ADD Gemfile* /app/
WORKDIR /app
RUN gem update bundler --pre
RUN bundle install --without development test
RUN npm install
ADD . /app
RUN cp config/database.yml.prod.example config/database.yml &amp;&amp; cp config/application.yml.example config/application.yml
RUN RAILS_GROUPS=assets bundle exec rake assets:precompile</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No Google Cloud Web Console, você encontrará um <a href="https://cloud.google.com/container-registry/"target="_blank" rel="noopener">&ldquo;Container Registry&rdquo;</a>, que é um Registry Docker Privado.</p>
<p>Você precisa adicionar a URL remota à sua configuração local assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>git remote add gcloud https://source.developers.google.com/p/my-project/r/my-app</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora você pode fazer <code>git push gcloud master</code>. Recomendo também adicionar <a href="https://cloud.google.com/container-builder/docs/running-builds/automate-builds"target="_blank" rel="noopener">triggers</a> para taggear suas imagens. Adiciono 2 triggers: um para taggeá-la com <code>latest</code> e outro para taggeá-la com um número de versão aleatório. Você vai precisar deles mais tarde.</p>
<p>Depois de adicionar o repositório do registry como um remote na sua configuração git (<code>git remote add</code>) e fazer push nele, ele deve começar a construir sua imagem Docker com as tags adequadas que você configurou com os triggers.</p>
<p>Certifique-se de que sua aplicação Ruby on Rails não tem nada nos initializers que exija uma conexão com o banco de dados, pois ele não está disponível. Isso é algo com que você pode travar quando seu build Docker falha por causa da tarefa <code>assets:precompile</code> que carregou um initializer que acidentalmente chama um Model — e isso dispara o <code>ActiveRecord::Base</code> tentando se conectar.</p>
<p>Além disso, certifique-se de que a versão do Ruby no <code>Dockerfile</code> corresponde à do <code>Gemfile</code>, caso contrário também vai falhar.</p>
<p>Reparou no estranho <code>config/application.yml</code> acima? Esse é do <a href="https://github.com/laserlemon/figaro"target="_blank" rel="noopener">figaro</a>. Também recomendo usar algo para facilitar a configuração de variáveis ENV no seu sistema. Não gosto dos secrets do Rails, e ele não é exatamente amigável para a maioria dos sistemas de deploy depois que o Heroku popularizou as ENV vars. Fique com ENV vars. O Kubernetes também vai agradecer por isso.</p>
<p>Agora, você pode sobrescrever qualquer variável ENV do arquivo YAML de Deployment do Kubernetes. Agora é uma boa hora para mostrar um exemplo disso. Você pode nomeá-lo <code>deploy/web.yml</code> ou como preferir e — claro — comitá-lo no seu repositório de código-fonte.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1beta1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">web</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">strategy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">RollingUpdate</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">rollingUpdate</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">maxSurge</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">maxUnavailable</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">minReadySeconds</span><span class="p">:</span><span class="w"> </span><span class="m">10</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">web</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">gcr.io/my-project/my-app:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">imagePullPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">Always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">4001</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;passenger&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;start&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-p&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;4001&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-e&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;production&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="s2">&#34;--max-pool-size&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;2&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--min-instances&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;2&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--no-friendly-error-pages&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="s2">&#34;--max-request-queue-time&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;10&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--max-request-time&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;10&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="s2">&#34;--pool-idle-time&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;0&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--memory-limit&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;300&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;RAILS_LOG_TO_STDOUT&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;RAILS_ENV&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;production&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="c"># ... obviamente reduzi as muitas ENV vars por brevidade</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;REDIS_URL&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">secretKeyRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-env</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">REDIS_URL</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;SMTP_USERNAME&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">secretKeyRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-env</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">SMTP_USERNAME</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;SMTP_PASSWORD&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">secretKeyRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-env</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">SMTP_PASSWORD</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="c"># ... esta parte abaixo é obrigatória para Cloud SQL</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">DB_HOST</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="m">127.0.0.1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">DB_PASSWORD</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">secretKeyRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql-db-credentials</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">password</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">DB_USER</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">secretKeyRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql-db-credentials</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">username</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">gcr.io/cloudsql-docker/gce-proxy:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql-proxy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;/cloud_sql_proxy&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--dir=/cloudsql&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="s2">&#34;-instances=my-project:us-west1:my-db=tcp:5432&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="s2">&#34;-credential_file=/secrets/cloudsql/credentials.json&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">volumeMounts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql-instance-credentials</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/secrets/cloudsql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">readOnly</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ssl-certs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/etc/ssl/certs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/cloudsql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql-instance-credentials</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">secret</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql-instance-credentials</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ssl-certs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">hostPath</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/etc/ssl/certs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="l">emptyDir:</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Tem muita coisa acontecendo aqui. Então deixa eu quebrar um pouco:</p>
<ul>
<li>O <code>kind</code> e o <code>apiVersion</code> são importantes — você precisa ficar de olho na documentação se eles mudarem. Isso é o que chamamos de <a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/"target="_blank" rel="noopener">Deployment</a>. Antigamente existia um Replication Controller (você encontrará em tutoriais antigos), mas não está mais em uso. A recomendação é usar um <a href="https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/"target="_blank" rel="noopener">ReplicaSet</a>.</li>
<li>Nomeie as coisas corretamente — aqui você tem <code>metadata:name</code> com <code>web</code>. Preste muita atenção também ao <code>spec:template:metadata:labels</code> onde estou rotulando cada pod com um label de <code>app: web</code>, você vai precisar disso para poder selecionar esses pods mais tarde na seção Service abaixo.</li>
<li>Em seguida tenho o <code>spec:strategy</code> onde configuramos o Rolling Update, então se você tem 10 pods, ele vai terminar um, inicializar o novo e continuar fazendo isso, sem nunca derrubar tudo de uma vez.</li>
<li><code>spec:replicas</code> declara quantos Pods quero de uma vez. Você terá que calcular manualmente o machine-type do node-pool e então dividir quantos CPUs/RAM totais você tem pelo quanto precisa para cada instância de aplicação.</li>
<li>Lembra da imagem Docker que geramos acima com a tag &rsquo;latest&rsquo;? Você a referencia em <code>spec:template:spec:containers:image</code></li>
<li>Estou usando o Passenger com configuração de produção (confira a <a href="https://www.phusionpassenger.com/library/config/reference/"target="_blank" rel="noopener">documentação do Phusion</a>, não copie isso só assim).</li>
<li>Na seção <code>spec:template:spec:containers:env</code> posso sobrescrever as ENV vars com os secrets reais de produção. E você vai notar que posso hard-codar valores ou usar essa estranha construção:</li>
</ul>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;SMTP_USERNAME&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">secretKeyRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-env</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">SMTP_USERNAME</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Aqui está referenciando um armazenamento de <a href="https://kubernetes.io/docs/concepts/configuration/secret/"target="_blank" rel="noopener">&ldquo;Secret&rdquo;</a> que nomeei &ldquo;my-env&rdquo;. E é assim que você cria o seu:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>kubectl create secret generic my-env \
--from-literal=REDIS_URL=redis://foo.com:18821 \
--from-literal=SMTP_USERNAME=foobar</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Leia a documentação pois você pode carregar arquivos de texto em vez de declarar tudo pela linha de comando.</p>
<p>Como disse antes, prefiro usar um serviço gerenciado para o banco de dados. Você pode definitivamente carregar sua própria imagem Docker, mas realmente não recomendo. O mesmo vale para outros serviços similares a banco de dados como Redis, Mongo. Se você vem da AWS, o <a href="https://cloud.google.com/sql/docs/postgres/"target="_blank" rel="noopener">Google Cloud SQL</a> é como o RDS.</p>
<p>Após criar sua instância PostgreSQL você não consegue acessá-la diretamente da aplicação web. No final, você tem um boilerplate para uma segunda imagem Docker, um <a href="https://cloud.google.com/sql/docs/mysql/connect-kubernetes-engine"target="_blank" rel="noopener">&ldquo;CloudSQL Proxy&rdquo;</a>.</p>
<p>Para isso funcionar você precisa primeiro criar uma Service Account:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gcloud sql users create proxyuser host --instance=my-db --password=abcd1234</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Após criar a instância PostgreSQL ela vai pedir para você baixar uma credencial JSON, então tenha cuidado e salve-a em algum lugar seguro. Não preciso dizer que você também deve gerar uma senha forte e segura. Em seguida precisa criar secrets extras:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>kubectl create secret generic cloudsql-instance-credentials \
--from-file=credentials.json=/home/myself/downloads/my-db-12345.json

kubectl create secret generic cloudsql-db-credentials \
--from-literal=username=proxyuser --from-literal=password=abcd1234</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Esses são referenciados nesta parte do Deployment:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">gcr.io/cloudsql-docker/gce-proxy:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql-proxy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;/cloud_sql_proxy&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--dir=/cloudsql&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="s2">&#34;-instances=my-project:us-west1:my-db=tcp:5432&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="s2">&#34;-credential_file=/secrets/cloudsql/credentials.json&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">volumeMounts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql-instance-credentials</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/secrets/cloudsql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">readOnly</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ssl-certs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/etc/ssl/certs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/cloudsql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql-instance-credentials</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">secret</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql-instance-credentials</span><span class="w">
</span></span></span><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ssl-certs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">hostPath</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/etc/ssl/certs</span><span class="w">
</span></span></span><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="l">emptyDir:</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Veja que você precisa adicionar o nome do banco de dados (&ldquo;my-db&rdquo; neste exemplo) na cláusula <code>-instance</code> no comando.</p>
<p>A propósito, o <code>gce-proxy:latest</code> se referia à versão 1.09 na época em que esse post foi publicado. Mas já havia uma versão 1.11. Essa me deu dores de cabeça, derrubando conexões e adicionando um timeout super longo. Então voltei para a 1.09 (latest) e tudo funcionou como esperado. Fique atento! Nem tudo que é novinho é bom. Em infraestrutura, você quer ficar com o que é estável.</p>
<p>Você também pode querer a opção de carregar uma instância CloudSQL separada em vez de tê-la em cada pod, para que os pods pudessem se conectar a apenas um proxy. Pode ser interessante ler <a href="https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/49"target="_blank" rel="noopener">essa thread</a> sobre o assunto.</p>
<p>Parece que nada é exposto a nada a menos que você diga explicitamente. Então precisamos expor esses pods através do que chamamos de <a href="https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport"target="_blank" rel="noopener">Node Port Service</a>. Vamos criar um arquivo <code>deploy/web-svc.yaml</code> também:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">web-svc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">sessionAffinity</span><span class="p">:</span><span class="w"> </span><span class="l">None</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="m">4001</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">TCP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">NodePort</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">web</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>É por isso que destaquei a importância do <code>spec:template:metadata:labels</code>, para que possamos usá-lo aqui no <code>spec:selector</code> para selecionar os pods corretos.</p>
<p>Agora podemos fazer deploy desses 2 assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>kubectl create -f deploy/web.yml
kubectl create -f deploy/web-svc.yml</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E você pode ver os pods sendo criados com <code>kubectl get pods --watch</code>.</p>
<h2>O Load Balancer<span class="hx:absolute hx:-mt-20" id="o-load-balancer"></span>
    <a href="#o-load-balancer" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Muitos tutoriais vão expor esses pods diretamente através de um Service diferente, chamado <a href="https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/"target="_blank" rel="noopener">Load Balancer</a>. Não tenho tanta certeza sobre como isso se comporta sob pressão e se tem terminação SSL, etc. Então decidi ir com tudo com um <a href="https://kubernetes.io/docs/concepts/services-networking/ingress/"target="_blank" rel="noopener">Ingress</a> Load Balancer usando o <a href="https://www.nginx.com/products/nginx/kubernetes-ingress-controller/"target="_blank" rel="noopener">NGINX Controller</a>.</p>
<p>Primeiro de tudo, decidi criar um node-pool separado para ele, por exemplo, assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gcloud container node-pools create web-load-balancer \
--cluster=my-web-production \
--node-labels=role=load-balancer \
--machine-type=g1-small \
--num-nodes 1 \
--max-nodes 3 --min-nodes=1 \
--enable-autoscaling </code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Assim como quando criamos o exemplo de <code>large-pool</code>, aqui você precisa cuidar de adicionar <code>--node-labels</code> para que o controller seja instalado aqui em vez do <code>default-pool</code>. Você vai precisar saber o nome da node instance, podemos fazer isso assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>$ gcloud compute instances list
NAME                                             ZONE        MACHINE_TYPE   PREEMPTIBLE  INTERNAL_IP  EXTERNAL_IP      STATUS
gke-my-web-production-default-pool-123-123       us-west1-a  n1-standard-4               10.128.0.1   123.123.123.12   RUNNING
gke-my-web-production-large-pool-123-123         us-west1-a  n1-highcpu-8                10.128.0.2   50.50.50.50      RUNNING
gke-my-web-production-web-load-balancer-123-123  us-west1-a  g1-small                    10.128.0.3   70.70.70.70      RUNNING</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Vamos salvar assim por enquanto:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>export LB_INSTANCE_NAME=gke-my-web-production-web-load-balancer-123-123</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Você pode reservar manualmente um IP externo e dar um nome a ele assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gcloud compute addresses create ip-web-production \
        --ip-version=IPV4 \
        --global</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Para fins de exemplo, digamos que ele gerou um IP reservado &ldquo;111.111.111.111&rdquo;. Então vamos buscá-lo e salvá-lo por enquanto assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>export LB_ADDRESS_IP=$(gcloud compute addresses list | grep &#34;ip-web-production&#34; | awk &#39;{print $3}&#39;)</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Por fim, vamos vincular esse endereço à node instance do load balancer:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>export LB_INSTANCE_NAT=$(gcloud compute instances describe $LB_INSTANCE_NAME | grep -A3 networkInterfaces: | tail -n1 | awk -F&#39;: &#39; &#39;{print $2}&#39;)
gcloud compute instances delete-access-config $LB_INSTANCE_NAME \
    --access-config-name &#34;$LB_INSTANCE_NAT&#34;
gcloud compute instances add-access-config $LB_INSTANCE_NAME \
    --access-config-name &#34;$LB_INSTANCE_NAT&#34; --address $LB_ADDRESS_IP</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Feito isso, podemos adicionar o resto da configuração de Deployment do Ingress. Vai ser um pouco longo, mas é basicamente boilerplate. Vamos começar definindo outra aplicação web que chamaremos de <code>default-http-backend</code>, usada para responder a requisições HTTP caso nossos pods web não estejam disponíveis por algum motivo. Vamos chamá-la de <code>deploy/default-web.yml</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">extensions/v1beta1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">default-http-backend</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">default-http-backend</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">terminationGracePeriodSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">60</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">default-http-backend</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># Any image is permissable as long as:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># 1. It serves a 404 page at /</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># 2. It serves 200 on a /healthz endpoint</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">gcr.io/google_containers/defaultbackend:1.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">livenessProbe</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">httpGet</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/healthz</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">scheme</span><span class="p">:</span><span class="w"> </span><span class="l">HTTP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">initialDelaySeconds</span><span class="p">:</span><span class="w"> </span><span class="m">30</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">timeoutSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">limits</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l">10m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">20Mi</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">requests</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l">10m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">20Mi</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Não precisa mudar nada aqui — e a essa altura você já deve estar familiarizado com o template de Deployment. Você já sabe que precisa expô-lo através de um NodePort, então vamos adicionar um <code>deploy/default-web-svc.yml</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">default-http-backend</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">default-http-backend</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">TCP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">NodePort</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>De novo, não precisa mudar nada. Os próximos 3 arquivos são as partes importantes. Primeiro, criaremos um NGINX Load Balancer — vamos chamá-lo de <code>deploy/nginx.yml</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">extensions/v1beta1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">nginx-ingress-controller</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">k8s-app</span><span class="p">:</span><span class="w"> </span><span class="l">nginx-ingress-lb</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># hostNetwork makes it possible to use ipv6 and to preserve the source IP correctly regardless of docker configuration</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># however, it is not a hard dependency of the nginx-ingress-controller itself and it may cause issues if port 10254 already is taken on the host</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># that said, since hostPort is broken on CNI (https://github.com/kubernetes/kubernetes/issues/31307) we have to use hostNetwork where CNI is used</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">hostNetwork</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">terminationGracePeriodSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">60</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">nodeSelector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">role</span><span class="p">:</span><span class="w"> </span><span class="l">load-balancer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">args</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="l">/nginx-ingress-controller</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="s2">&#34;--default-backend-service=$(POD_NAMESPACE)/default-http-backend&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="s2">&#34;--default-ssl-certificate=$(POD_NAMESPACE)/cloudflare-secret&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">POD_NAME</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">fieldRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">fieldPath</span><span class="p">:</span><span class="w"> </span><span class="l">metadata.name</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">POD_NAMESPACE</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">fieldRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">fieldPath</span><span class="p">:</span><span class="w"> </span><span class="l">metadata.namespace</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;gcr.io/google_containers/nginx-ingress-controller:0.9.0-beta.5&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">imagePullPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">Always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">livenessProbe</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">httpGet</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/healthz</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">10254</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">scheme</span><span class="p">:</span><span class="w"> </span><span class="l">HTTP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">initialDelaySeconds</span><span class="p">:</span><span class="w"> </span><span class="m">10</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">timeoutSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">nginx-ingress-controller</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">http</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">TCP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">443</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">https</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">TCP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">volumeMounts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/etc/nginx-ssl/dhparam</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">tls-dhparam-vol</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">tls-dhparam-vol</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">secret</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">tls-dhparam</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Preste atenção ao <code>nodeSelector</code> para usar o node label que adicionamos ao criar o novo node-pool.</p>
<p>Você pode querer mexer nos labels, no número de réplicas se precisar. Mas aqui você vai notar que ele monta um volume que nomeei como <code>tls-dhparam-vol</code>. Esses são os <a href="https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html#Forward_Secrecy_&amp;_Diffie_Hellman_Ephemeral_Parameters"target="_blank" rel="noopener">Parâmetros Efêmeros Diffie Hellman</a>. É assim que geramos:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo openssl dhparam -out ~/documents/dhparam.pem 2048

kubectl create secret generic tls-dhparam --from-file=/home/myself/documents/dhparam.pem

kubectl create secret generic tls-dhparam --from-file=/home/myself/documents/dhparam.pem</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Note também que estou usando a versão &ldquo;0.9.0-beta_5&rdquo; para a imagem do controller. Funciona bem, sem problemas até agora. Mas fique de olho nas release notes para versões mais novas e faça seus próprios testes.</p>
<p>De novo, vamos expor esse controller Ingress através do Load Balancer Service. Vamos chamá-lo de <code>deploy/nginx-svc.yml</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">nginx-ingress</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">LoadBalancer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">loadBalancerIP</span><span class="p">:</span><span class="w"> </span><span class="m">111.111.111.111</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">http</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="l">http</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">https</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">443</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="l">https</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">k8s-app</span><span class="p">:</span><span class="w"> </span><span class="l">nginx-ingress-lb</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Lembra do IP externo estático que reservamos acima e salvamos na ENV var <code>LB_INGRESS_IP</code>? Esse é o que precisa colocar na seção <code>spec:loadBalancerIP</code>. Esse também é o IP que você vai adicionar como &ldquo;registro A&rdquo; no seu serviço DNS (digamos, mapeando seu &ldquo;<a href="https://www.my-app.com.br"target="_blank" rel="noopener">www.my-app.com.br</a>&rdquo; no CloudFlare).</p>
<p>Por fim, podemos criar a configuração do Ingress em si — vamos criar um <code>deploy/ingress.yml</code> assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">extensions/v1beta1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Ingress</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ingress</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/ingress.class</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;nginx&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.org/ssl-services</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;web-svc&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/ingress.global-static-ip-name</span><span class="p">:</span><span class="w"> </span><span class="l">ip-web-production</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ingress.kubernetes.io/ssl-redirect</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ingress.kubernetes.io/rewrite-target</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">www.my-app.com.br</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">cloudflare-secret</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">www.my-app.com.br</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">http</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">backend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">serviceName</span><span class="p">:</span><span class="w"> </span><span class="l">web-svc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">servicePort</span><span class="p">:</span><span class="w"> </span><span class="m">80</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Cuidado com as annotations acima que conectam tudo. Ele vincula o serviço NodePort que criamos para os pods web com o nginx ingress controller e adiciona terminação SSL através daquele <code>spec:tls:secretName</code>. Como você cria isso? Primeiro precisa comprar um certificado SSL — usando o CloudFlare como exemplo novamente.</p>
<p>Quando terminar a compra, o provedor deve te dar os arquivos secretos para download (guarde-os em segurança! uma pasta pública do Dropbox não é segura!). Então precisa adicioná-los à infraestrutura assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>kubectl create secret tls cloudflare-secret \
--key ~/downloads/private.pem \
--cert ~/downloads/fullchain.pem</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora que editamos um monte de arquivos, podemos fazer deploy de todo o stack do load balancer:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>kubectl create -f deploy/default-web.yml
kubectl create -f deploy/default-web-svc.yml
kubectl create -f deploy/nginx.yml
kubectl create -f deploy/nginx-svc.yml
kubectl create -f deploy/ingress.yml</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Essa configuração do NGINX Ingress é baseada no <a href="https://zihao.me/post/cheap-out-google-container-engine-load-balancer/"target="_blank" rel="noopener">post do blog do Zihao Zhang</a>. Existem também exemplos no <a href="https://github.com/kubernetes-incubator/external-dns/blob/master/docs/tutorials/nginx-ingress.md"target="_blank" rel="noopener">repositório kubernetes incubator</a>. Vale a pena conferir também.</p>
<p>Se você fez tudo certo até aqui, <code>https://www.my-app-com.br</code> deve carregar sua aplicação web. Pode ser interessante verificar o Time to First-Byte (TTFB). Você pode fazer isso passando pelo CloudFlare assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>curl -vso /dev/null -w &#34;Connect: %{time_connect} \n TTFB: %{time_starttransfer} \n Total time: %{time_total} \n&#34; https://www.my-app.com.br</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Ou, se estiver com TTFB lento, pode contornar o CloudFlare fazendo isso:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>curl --resolve www.my-app.com.br:443:111.111.111.111 https://www.my-app.com.br -svo /dev/null -k -w &#34;Connect: %{time_connect} \n TTFB: %{time_starttransfer} \n Total time: %{time_total} \n&#34;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O TTFB deve ficar em torno de 1 segundo ou menos. Qualquer coisa muito acima disso pode indicar um problema na sua aplicação. Verifique os machine types da sua node instance, o número de workers carregados por pod, a versão do CloudSQL proxy, a versão do NGINX controller e assim por diante. Isso é um processo de tentativa e erro, pelo que sei. Cadastre-se em serviços como <a href="https://loader.io"target="_blank" rel="noopener">Loader</a> ou mesmo <a href="https://www.webpagetest.org"target="_blank" rel="noopener">Web Page Test</a> para ter insights.</p>
<h2>Rolling Updates<span class="hx:absolute hx:-mt-20" id="rolling-updates"></span>
    <a href="#rolling-updates" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Agora, com tudo funcionando, como realizamos o Rolling Update que mencionei no início? Primeiro você faz <code>git push</code> para o repositório do Container Registry e aguarda a construção da imagem Docker.</p>
<p>Lembra que disse para deixar um trigger tagear a imagem com um número de versão aleatório? Vamos usá-lo (você pode ver isso na lista de Build History no Container Registry, no console do Google Cloud):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>kubectl set image deployment web my-app=gcr.io/my-project/my-app:1238471234g123f534f543541gf5 --record</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Você deve usar o mesmo nome e imagem declarados no <code>deploy/web.yml</code> acima. Isso vai começar a fazer rollout da atualização adicionando um novo pod, então encerrando um pod e assim por diante, sem downtime para seus usuários.</p>
<p>Rolling updates precisam ser executados com cuidado. Por exemplo, se seu novo deploy exige uma migração de banco de dados, você precisa adicionar uma janela de manutenção (ou seja: faça isso quando houver pouco ou nenhum tráfego, como de madrugada). Então você pode executar o comando migrate assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>kubectl get pods # para obter o nome de um pod

kubectl exec -it my-web-12324-121312 /app/bin/rails db:migrate

# você também pode entrar em bash em um pod assim, mas lembre que este é um container efêmero, então arquivos que você editar e escrever ali desaparecem no próximo restart:

kubectl exec -it my-web-12324-121312 bash</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Para fazer redeploy de tudo sem recorrer ao rolling update, você precisa fazer isso:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>kubectl delete -f deploy/web.yml &amp;&amp; kubectl apply -f deploy/web.yml</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Você encontrará uma explicação mais completa no <a href="https://tachingchen.com/blog/Kubernetes-Rolling-Update-with-Deployment/"target="_blank" rel="noopener">blog do Ta-Ching</a>.</p>
<h2>Bônus: Auto Snapshots<span class="hx:absolute hx:-mt-20" id="bônus-auto-snapshots"></span>
    <a href="#b%c3%b4nus-auto-snapshots" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Um item que tinha na minha lista de &ldquo;queria/precisava&rdquo; no início é a capacidade de ter storage persistente montável com backups/snapshots automáticos. O Google Cloud fornece metade disso por enquanto. Você pode criar discos persistentes para montar nos seus pods, mas não tem um recurso para fazer backup automático dele. Pelo menos tem APIs para criar snapshots manualmente.</p>
<p>Para este exemplo, vamos criar um novo disco SSD e formatá-lo primeiro:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gcloud compute disks create --size 500GB my-data --type pd-ssd

gcloud compute instances list</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O último comando é para copiarmos o nome de uma node instance. Digamos que seja <code>gke-my-web-app-default-pool-123-123</code>. Vamos anexar o disco <code>my-data</code> a ela:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gcloud compute instances attach-disk gke-my-web-app-default-pool-123-123 --disk my-data --device-name my-data

gcloud compute ssh gke-my-web-app-default-pool-123-123</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O último comando faz SSH na instância. Podemos listar os discos anexados com <code>sudo lsblk</code> e você verá o disco de 500GB, provavelmente como <code>/dev/sdb</code>, mas certifique-se de que está correto porque vamos formatá-lo!</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo mkfs.ext4 -m 0 -F -E lazy_itable_init=0,lazy_journal_init=0,discard /dev/sdb</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora podemos sair da sessão SSH e desanexar o disco:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gcloud compute instances detach-disk gke-my-web-app-default-pool-123-123 --disk my-data</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Você pode montar esse disco nos seus pods adicionando o seguinte ao seu yaml de deployment:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>spec:
  containers:
    - image: ...
      name: my-app
      volumeMounts:
        - name: my-data
          mountPath: /data
          # readOnly: true
   # ...
   volumes:
     - name: my-data
       gcePersistentDisk:
         pdName: my-data
         fsType: ext4</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora, vamos criar um arquivo de deployment CronJob como <code>deploy/auto-snapshot.yml</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">batch/v1beta1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">CronJob</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">auto-snapshot</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">schedule</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;0 4 * * *&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">concurrencyPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">Forbid</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">jobTemplate</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">restartPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">OnFailure</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">auto-snapshot</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">grugnog/google-cloud-auto-snapshot</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;/opt/entrypoint.sh&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;GOOGLE_CLOUD_PROJECT&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;my-project&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;GOOGLE_APPLICATION_CREDENTIALS&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;/credential/credential.json&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">volumeMounts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span>- <span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/credential</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">editor-credential</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">editor-credential</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">secret</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">editor-credential</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Como já fizemos antes, você precisará criar outra Service Account com permissões de editor na seção &ldquo;IAM &amp; admin&rdquo; do console do Google Cloud, depois baixar a credencial JSON e, por fim, fazer upload dela assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>kubectl create secret generic editor-credential \
--from-file=credential.json=/home/myself/download/my-project-1212121.json</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Note também que, como um cron job normal, existe um parâmetro de schedule que você pode querer alterar. No exemplo, &ldquo;0 4 * * *&rdquo; significa que o snapshot vai rodar todo dia às 4h da manhã.</p>
<p>Confira o <a href="https://github.com/grugnog/google-cloud-auto-snapshot"target="_blank" rel="noopener">repositório original</a> dessa solução para mais detalhes.</p>
<p>E por agora é isso!</p>
<p>Como disse no início, este não é um procedimento completo, apenas destaques de algumas das partes importantes. Se você é novo no Kubernetes, acabou de ler sobre Deployment, Service, Ingress, mas ainda tem ReplicaSet, DaemonSet e muito mais para explorar.</p>
<p>Acho que já está longo demais para adicionar uma explicação de configuração de <a href="https://cloud.google.com/sql/docs/mysql/high-availability"target="_blank" rel="noopener">Alta Disponibilidade multi-região</a>, então vamos deixar por aqui.</p>
<p>Correções ou sugestões são muito bem-vindas, pois ainda estou no processo de aprendizado, e tem um monte de coisas que eu mesmo ainda não sei.</p>
]]></content:encoded><category>rails</category><category>cloud</category><category>containers</category><category>devops</category></item><item><title>Checklist de Deploy do Rails 5.1 no Heroku</title><link>https://akitaonrails.github.io/2017/06/28/checklist-de-deploy-do-rails-5-1-no-heroku/</link><guid isPermaLink="true">https://akitaonrails.github.io/2017/06/28/checklist-de-deploy-do-rails-5-1-no-heroku/</guid><pubDate>Wed, 28 Jun 2017 15:12:00 -0300</pubDate><description>&lt;p&gt;Lancei &lt;a href="http://www.theconf.club"target="_blank" rel="noopener"&gt;THE CONF&lt;/a&gt; ontem. Espero que você aproveite a grade da conferência e aproveite o desconto early-bird limitado!&lt;/p&gt;
&lt;p&gt;Enfim, o site em si é super simples. Uma página única. Escolhi usar Rails 5.1 como estrutura porque ele cuida de tudo que eu teria que adicionar manualmente em qualquer outro framework. Especialmente agora que traz suporte nativo a Yarn e Webpack, fica fácil de usar para qualquer dev front-end competente.&lt;/p&gt;
&lt;p&gt;Mas mesmo com todas as facilidades embutidas, um setup completo de produção ainda exige etapas extras que a maioria dos iniciantes não conhece. Por isso resolvi compilar um pequeno checklist do que você precisa cuidar antes de fazer deploy em produção. Não é uma lista exaustiva para todos os casos de uso, mas deve ser suficiente para a maioria dos projetos menores.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Lancei <a href="http://www.theconf.club"target="_blank" rel="noopener">THE CONF</a> ontem. Espero que você aproveite a grade da conferência e aproveite o desconto early-bird limitado!</p>
<p>Enfim, o site em si é super simples. Uma página única. Escolhi usar Rails 5.1 como estrutura porque ele cuida de tudo que eu teria que adicionar manualmente em qualquer outro framework. Especialmente agora que traz suporte nativo a Yarn e Webpack, fica fácil de usar para qualquer dev front-end competente.</p>
<p>Mas mesmo com todas as facilidades embutidas, um setup completo de produção ainda exige etapas extras que a maioria dos iniciantes não conhece. Por isso resolvi compilar um pequeno checklist do que você precisa cuidar antes de fazer deploy em produção. Não é uma lista exaustiva para todos os casos de uso, mas deve ser suficiente para a maioria dos projetos menores.</p>
<ul>
<li><a href="#setup">Configurando o projeto</a></li>
<li><a href="#cdn">Adicionando CDN e configurando suporte a CORS</a> (para carregar fontes, por exemplo)</li>
<li><a href="#cache">Adicionando Memcached</a></li>
</ul>
<p><a name="setup"></a></p>
<h2>Configurando o projeto<span class="hx:absolute hx:-mt-20" id="configurando-o-projeto"></span>
    <a href="#configurando-o-projeto" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Uma coisa que a maioria das pessoas esquece, até desenvolvedores experientes, é que o comando <code>rails new</code> usado para criar a estrutura inicial do projeto aceita várias flags. Em vez de ter que ajustar arquivos manualmente depois, você pode começar assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>rails new your_project \
--database=postgresql \
--webpack=react \
--skip-action-cable \
--skip-coffee
--skip-turbolinks</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Se você está construindo com React ou outro framework JavaScript completo, provavelmente vai querer pular o Turbolinks. Caso contrário, se for um site simples, use <a href="https://sevos.io/2017/02/27/turbolinks-lifecycle-explained.html"target="_blank" rel="noopener">Turbolinks</a>.</p>
<p>Comece usando Postgresql desde o início, não use Sqlite3.</p>
<p>Pule o Action Cable. Prefira uma solução real como o <a href="https://pusher.com/"target="_blank" rel="noopener">Pusher.com</a>. Se você realmente precisar de algo interno, considere algo como minha solução com Elixir, o <a href="http://expusherlite.cm42.io/"target="_blank" rel="noopener">Ex Pusher Lite</a>. Leve essa recomendação com cautela, claro — para coisas pequenas o Action Cable é mais que suficiente. Posso escrever outro post elaborando esse ponto se as pessoas indicarem interesse nos comentários.</p>
<p>Enfim, me perdi. Certifique-se de ter 2 arquivos de boot: primeiro o <code>Procfile</code> canônico, usado pelo Heroku em produção:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>web: bin/rails server -p $PORT -b 0.0.0.0</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Segundo, um <code>Procfile.dev</code> para usar apenas no ambiente de desenvolvimento:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>web: ./bin/rails server
webpacker: ./bin/webpack-dev-server</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>É assim que você sobe o servidor webpack que vai compilar seus assets em tempo real durante o desenvolvimento. Lembre também de rodar esses dois comandos de dependência:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>yarn install
bundle install</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Instale dependências JavaScript com <code>yarn add [pacote]</code> e pronto! Em produção você não usa o servidor webpack (por isso não o adicionamos ao <code>Procfile</code> de produção) — o Heroku detecta automaticamente a gem <a href="https://github.com/rails/webpacker"target="_blank" rel="noopener">webpacker</a>, instala o buildpack do nodejs, roda <code>yarn install</code> por você, e quando <code>rails assets:precompile</code> executa ele também dispara <code>yarn run</code>, que pré-compila todos os assets (JavaScript, folhas de estilo, imagens) com fingerprinting correto para cache busting e tudo mais que já estamos acostumados no Asset Pipeline do Rails.</p>
<p>No Heroku, basicamente você não precisa fazer nada. E num servidor de deploy customizado basta lembrar de rodar a task <code>assets:precompile</code> e deixar ela cuidar de tudo.</p>
<p><a name="cdn"></a></p>
<h3>Adicionando CDN e configurando CORS<span class="hx:absolute hx:-mt-20" id="adicionando-cdn-e-configurando-cors"></span>
    <a href="#adicionando-cdn-e-configurando-cors" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Isso é realmente simples. Não há razão para não adicionar uma CDN a qualquer site. Simplesmente faça.</p>
<p>Abra seu AWS Management Console e <a href="https://console.aws.amazon.com/cloudfront/"target="_blank" rel="noopener">abra o CloudFront</a>. Clique em &ldquo;Create Distribution&rdquo; e siga os padrões do wizard. O requisito é que você DEVE saber o domínio e subdomínio para onde quer apontar (por exemplo, &ldquo;<a href="https://www.theconf.club"target="_blank" rel="noopener">www.theconf.club</a>&rdquo;) no campo &ldquo;Origin Domain Name&rdquo;.</p>
<p>A única customização que você DEVE fazer é mudar a opção <a href="http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/header-caching.html#header-caching-web-cors"target="_blank" rel="noopener">&ldquo;Forward Headers&rdquo;</a> para &ldquo;Whitelist&rdquo; em &ldquo;Default Cache Behavior Settings&rdquo;. Depois adicione &ldquo;Origin&rdquo;, &ldquo;Access-Control-Request-Headers&rdquo; e &ldquo;Access-Control-Request-Method&rdquo; à Whitelist. Pronto, sua distribuição está habilitada para CORS.</p>
<p>Vai demorar um pouco para criar (precisa configurar vários data centers ao redor do mundo), mas você vai terminar com uma URL representando sua distribuição, algo como <code>doz7rtw2u3wg4.cloudfront.net</code>. Recomendo adicioná-la como variável de ambiente no Heroku assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>heroku config:set CDN_URL=doz7rtw2u3wg4.cloudfront.net</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora edite seu <code>config/environments/production.rb</code> e adicione:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">action_controller</span><span class="o">.</span><span class="n">asset_host</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;CDN_URL&#39;</span><span class="o">]</span> <span class="k">if</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;CDN_URL&#39;</span><span class="o">]</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Para usar a CDN de fato, você deve declarar cada asset utilizado nos templates de view usando os Rails View Helpers como <code>image_tag</code>, <code>asset_path</code>, <code>javascript_pack_tag</code>, <code>stylesheed_pack_tag</code>, <code>stylesheet_link_tag</code>, etc. O bootstrap do Rails já cria o template de layout com esses helpers, basta seguir o padrão.</p>
<p>Quando o webpack rodar, vai gerar todos os assets estáticos, otimizados e pré-compilados em <code>public/packs</code> com um arquivo de manifesto declarando a URL completa apontando para a CDN. Por exemplo, se eu buscar <code>/app/public/packs/manifest.json</code> diretamente do dyno do Heroku, terei algo assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>{
  &#34;Roboto-Bold.woff&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Bold-eed9aab5449cc9c8430d7d258108f602.woff&#34;,
  &#34;Roboto-Bold.woff2&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Bold-c0f1e4a4fdfb8048c72e86aadb2a247d.woff2&#34;,
  &#34;Roboto-Light.woff&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Light-ea36cd9a0e9eee97012a67b8a4570d7b.woff&#34;,
  &#34;Roboto-Light.woff2&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Light-3c37aa69cd77e6a53a067170fa8fe2e9.woff2&#34;,
  &#34;Roboto-Medium.woff&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Medium-cf4d60bc0b1d4b2314085919a00e1724.woff&#34;,
  &#34;Roboto-Medium.woff2&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Medium-1561b424aaef2f704bbd89155b3ce514.woff2&#34;,
  &#34;Roboto-Regular.woff&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Regular-3cf6adf61054c328b1b0ddcd8f9ce24d.woff&#34;,
  &#34;Roboto-Regular.woff2&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Regular-5136cbe62a63604402f2fedb97f246f8.woff2&#34;,
  &#34;Roboto-Thin.woff&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Thin-44b78f142603eb69f593ed4002ed7a4a.woff&#34;,
  &#34;Roboto-Thin.woff2&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Thin-1f35e6a11d27d2e10d28946d42332dc5.woff2&#34;,
  &#34;application.css&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/application-09b53ce9ca3dd595ee99.css&#34;,
  &#34;application.css.map&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/application-09b53ce9ca3dd595ee99.css.map&#34;,
  &#34;application.js&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/application-799300612ff6d6595198.js&#34;,
  &#34;application.js.map&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/application-799300612ff6d6595198.js.map&#34;,
  &#34;home_page.js&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/home_page-ff3b49407a1d01592ad5.js&#34;,
  &#34;home_page.js.map&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/home_page-ff3b49407a1d01592ad5.js.map&#34;
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Então, se por algum motivo você precisar criar uma nova distribuição CDN, lembre de atualizar a variável <code>CDN_URL</code> no Heroku e fazer redeploy da aplicação para que os assets e esse arquivo de manifesto sejam regerados. Ele vai usar essas URLs ao renderizar os HTMLs finais.</p>
<p>Quando um usuário abre seu site, recebe o HTML com URLs apontando para a CDN. Na primeira vez não haverá nenhum asset em cache, então a CDN vai buscar no seu servidor. Seu servidor deve retornar os assets COM os headers CORS corretos para que a CDN os armazene em cache junto com os headers e os repasse ao browser. O browser precisa desses headers porque vai abrir do domínio <code>www.theconf.club</code>, por exemplo, mas as fontes estão sendo carregadas de <code>doz7rtw2u3wg4.cloudfront.net</code> — o que geraria um aviso de segurança e bloquearia o carregamento das fontes por serem de domínios diferentes. Por isso os headers CORS indicam que é seguro carregá-las.</p>
<p>Para que sua aplicação Rails retorne os headers corretos, adicione a gem <code>rack-cors</code> ao Gemfile. Depois crie um arquivo de configuração em <code>config/initializers/rack-cors.rb</code> com:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">if</span> <span class="n">defined?</span> <span class="no">Rack</span><span class="o">::</span><span class="no">Cors</span>
</span></span><span class="line"><span class="cl">  <span class="no">Rails</span><span class="o">.</span><span class="n">configuration</span><span class="o">.</span><span class="n">middleware</span><span class="o">.</span><span class="n">insert_before</span> <span class="mi">0</span><span class="p">,</span> <span class="no">Rack</span><span class="o">::</span><span class="no">Cors</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">allow</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">origins</span> <span class="sx">%w[
</span></span></span><span class="line"><span class="cl"><span class="sx">        https://theconf.club
</span></span></span><span class="line"><span class="cl"><span class="sx">        http://theconf.club
</span></span></span><span class="line"><span class="cl"><span class="sx">        https://www.theconf.club
</span></span></span><span class="line"><span class="cl"><span class="sx">        http://www.theconf.club
</span></span></span><span class="line"><span class="cl"><span class="sx">        https://theconf.herokuapp.com
</span></span></span><span class="line"><span class="cl"><span class="sx">        http://theconf.herokuapp.com
</span></span></span><span class="line"><span class="cl"><span class="sx">      ]</span>
</span></span><span class="line"><span class="cl">      <span class="n">resource</span> <span class="s1">&#39;/assets/*&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Quando fizer o deploy, você sabe que está funcionando quando faz um curl num asset e ele retorna os headers Access-* assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>$ curl -I -s -X GET -H &#34;Origin: www.theconf.club&#34; http://www.theconf.club/packs/Roboto-Regular-5136cbe62a63604402f2fedb97f246f8.woff2
HTTP/1.1 200 OK
Server: Cowboy
Date: Wed, 28 Jun 2017 17:44:41 GMT
Connection: keep-alive
Access-Control-Allow-Origin: www.theconf.club
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Expose-Headers: 
Access-Control-Max-Age: 1728000
Access-Control-Allow-Credentials: true
Last-Modified: Wed, 28 Jun 2017 17:06:03 GMT
Content-Type: application/font-woff2
Cache-Control: public, max-age=2592000
Vary: Origin
Content-Length: 64832
Via: 1.1 vegur</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E se tudo estiver configurado, você deve ver os headers sendo repassados pela CDN assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>$ curl -I -s -X GET -H &#34;Origin: www.theconf.club&#34; http://doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Regular-5136cbe62a63604402f2fedb97f246f8.woff2
HTTP/1.1 200 OK
Content-Type: application/font-woff2
Content-Length: 64832
Connection: keep-alive
Server: Cowboy
Date: Wed, 28 Jun 2017 17:45:18 GMT
Access-Control-Allow-Origin: www.theconf.club
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Expose-Headers: 
Access-Control-Max-Age: 1728000
Access-Control-Allow-Credentials: true
Last-Modified: Wed, 28 Jun 2017 17:06:03 GMT
Cache-Control: public, max-age=2592000
Via: 1.1 vegur, 1.1 86e9abdb4c15b9d3a542f9b93245e87e.cloudfront.net (CloudFront)
Vary: Origin
X-Cache: Miss from cloudfront
X-Amz-Cf-Id: tVkZ41RRr66iBT6atWTO_oeTY_jG0zCBFuXU8bKyClZDQ8kl-hDegA==</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Uma CDN é o ingrediente secreto que permite a qualquer site baseado em conteúdo escalar muito além do que seu servidor consegue. É uma economia enorme e ainda proporciona uma experiência muito mais suave para seus usuários.</p>
<p>Um último ponto. Os Rails View Helpers como <code>image_tag</code> permitem que você informe o nome do arquivo de imagem sem extensão em desenvolvimento e ele encontra a imagem corretamente. Mas no servidor vai falhar ao renderizar o template dessa forma. Como regra geral, <strong>SEMPRE</strong> informe o nome completo do arquivo com extensão, por exemplo <code>image_tag(&quot;logo.png&quot;)</code> em vez de apenas <code>image_tag(&quot;logo&quot;)</code>.</p>
<p>Você pode ver como isso falha abrindo um console no Heroku:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>$ heroku run rails c                                                                                                                  
Running rails c on ⬢ theconf... up, run.8271 (Hobby)
Loading production environment (Rails 5.1.2)

irb(main):001:0&gt; ActionController::Base.helpers.asset_path(&#34;icon-goals&#34;)
Sprockets::Rails::Helper::AssetNotFound: The asset &#34;icon-goals&#34; is not present in the asset pipeline.
    from (irb):1

irb(main):002:0&gt; ActionController::Base.helpers.asset_path(&#34;icon-goals.png&#34;)
=&gt; &#34;//d134ipy19a646x.cloudfront.net/assets/icon-goals-b969b3b7325d33ad85a88dbb5b894832909ed738eea9964b9cf535646b93674b.png&#34;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><a name="cache"></a></p>
<h3>Adicionando Memcached<span class="hx:absolute hx:-mt-20" id="adicionando-memcached"></span>
    <a href="#adicionando-memcached" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Falar sobre cache é sempre complicado. É por isso que muita gente nem tenta. Mesmo que você possa se aventurar em configurações bem granulares como o <a href="http://edgeguides.rubyonrails.org/caching_with_rails.html#russian-doll-caching"target="_blank" rel="noopener">Russian Doll Caching</a>, adicionar cache em alguns pontos estratégicos já traz um benefício enorme. E é super fácil de configurar.</p>
<p>O primeiro passo é adicionar o <a href="https://devcenter.heroku.com/articles/memcachier"target="_blank" rel="noopener">Memcachier</a> à sua aplicação Heroku. Ele tem um tier gratuito e para a maioria dos apps pequenos é o suficiente.</p>
<p>A configuração também é trivial. Comece adicionando as seguintes gems ao seu <code>Gemfile</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">group</span> <span class="ss">:production</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;rack-cors&#39;</span><span class="p">,</span> <span class="nb">require</span><span class="p">:</span> <span class="s1">&#39;rack/cors&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;rack-cache&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;dalli&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;kgio&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;memcachier&#39;</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora edite seu <code>config/environments/production.rb</code> assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">cache_store</span> <span class="o">=</span> <span class="ss">:dalli_store</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">client</span> <span class="o">=</span> <span class="no">Dalli</span><span class="o">::</span><span class="no">Client</span><span class="o">.</span><span class="n">new</span><span class="p">((</span><span class="no">ENV</span><span class="o">[</span><span class="s2">&#34;MEMCACHIER_SERVERS&#34;</span><span class="o">]</span> <span class="o">||</span> <span class="s2">&#34;&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;,&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                           <span class="ss">:username</span> <span class="o">=&gt;</span> <span class="no">ENV</span><span class="o">[</span><span class="s2">&#34;MEMCACHIER_USERNAME&#34;</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                           <span class="ss">:password</span> <span class="o">=&gt;</span> <span class="no">ENV</span><span class="o">[</span><span class="s2">&#34;MEMCACHIER_PASSWORD&#34;</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                           <span class="ss">:failover</span> <span class="o">=&gt;</span> <span class="kp">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                           <span class="ss">:socket_timeout</span> <span class="o">=&gt;</span> <span class="mi">1</span><span class="o">.</span><span class="mi">5</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                           <span class="ss">:socket_failure_delay</span> <span class="o">=&gt;</span> <span class="mi">0</span><span class="o">.</span><span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                           <span class="ss">:value_max_bytes</span> <span class="o">=&gt;</span> <span class="mi">10485760</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">action_dispatch</span><span class="o">.</span><span class="n">rack_cache</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="ss">:metastore</span>    <span class="o">=&gt;</span> <span class="n">client</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">:entitystore</span>  <span class="o">=&gt;</span> <span class="n">client</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Digamos que você tenha um bloco no template que requer vários registros do banco de dados, mas você sabe que esses registros mal mudam. O que fazer? Uma alternativa é fazer cache da query ActiveRecord inteira assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">HomePageController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">index</span>
</span></span><span class="line"><span class="cl">    <span class="vi">@selected_proposals</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">cache</span><span class="o">.</span><span class="n">fetch</span><span class="p">(</span><span class="s1">&#39;selected_proposals&#39;</span><span class="p">,</span> <span class="ss">expires_in</span><span class="p">:</span> <span class="mi">1</span><span class="o">.</span><span class="n">day</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Proposal</span><span class="o">.</span><span class="n">includes</span><span class="p">(</span><span class="ss">:authors</span><span class="p">)</span><span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="ss">selected</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span><span class="o">.</span><span class="n">to_a</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <code>#to_a</code> é necessário porque queries ActiveRecord são lazy. O <code>#to_a</code> força a busca e o resultado fica em cache. Da próxima vez, não vai tocar no banco de dados por um dia inteiro!</p>
<p>Também poderia ter adicionado um bloco <code>cache do ... end</code> no próprio template. Há muitas opções. O ponto é que não é tão difícil quanto a maioria das pessoas imagina.</p>
<p>O que torna o cache difícil é a lógica de expiração. E a regra geral é: nunca tente expirar cache manualmente. Mude a chave de busca para outra coisa e deixe os dados antigos morrerem (o memcached cuida de eliminar os dados antigos não utilizados).</p>
<p>Vale muito a pena ler o <a href="http://edgeguides.rubyonrails.org/caching_with_rails.html"target="_blank" rel="noopener">Rails Guides on Caching</a>. Realmente não é tão difícil quanto você pensa, e você pode cachear apenas os trechos onde sabe que há sensibilidade de performance, deixando as partes altamente dinâmicas sem cache.</p>
<p>Como é super barato, use agora mesmo.</p>
<p><a name="ssl"></a></p>
<h3>Adicionando suporte a SSL<span class="hx:absolute hx:-mt-20" id="adicionando-suporte-a-ssl"></span>
    <a href="#adicionando-suporte-a-ssl" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Se você tem qualquer transação sensível à segurança (ex. compras), use SSL. Muita gente evita porque costuma ser difícil até entender como obter um certificado corretamente.</p>
<p>O <a href="https://letsencrypt.org/"target="_blank" rel="noopener">Let&rsquo;s Encrypt</a> tornou o processo super simples. Parabéns a eles! E melhor ainda: é gratuito! Então não há desculpa para não ter SSL.</p>
<p>E no Heroku é <a href="https://devcenter.heroku.com/articles/automated-certificate-management"target="_blank" rel="noopener">ainda mais fácil</a>!</p>
<blockquote>
  <p>O ACM (Automated Certificate Management) está habilitado por padrão para todas as aplicações criadas após 21 de março de 2017 rodando em dynos Hobby ou Professional. Os passos a seguir são para aplicações rodando em Free dynos e para aplicações criadas antes dessa data.</p>

</blockquote>
<p>Nos Free Tier Dynos, é assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>heroku certs:auto:enable</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Verifique o status com <code>heroku certs:auto</code>.</p>
<p>Pronto, não há passo 2.</p>
<p>Antes tínhamos que usar a complicada gem <a href="https://github.com/pixielabs/letsencrypt-rails-heroku"target="_blank" rel="noopener">letsencrypt-rails-heroku</a>, mas agora está simples demais.</p>
<h3>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Acredito que isso cobre o básico do que você deve fazer antes de fazer deploy de um app pequeno no Heroku. Para apps maiores há muito mais preocupações que estão além do escopo deste post.</p>
<p>Se você lembrar de mais dicas e truques, compartilhe na seção de comentários abaixo.</p>
]]></content:encoded><category>rails</category><category>devops</category><category>tutoriais</category></item><item><title>Aplicativo Elixir Phoenix em Deploy num Setup com Balanceamento de Carga no DigitalOcean</title><link>https://akitaonrails.github.io/2016/12/23/aplicativo-elixir-phoenix-deploy-balanceamento-carga-digitalocean/</link><guid isPermaLink="true">https://akitaonrails.github.io/2016/12/23/aplicativo-elixir-phoenix-deploy-balanceamento-carga-digitalocean/</guid><pubDate>Fri, 23 Dec 2016 15:07:00 -0200</pubDate><description>&lt;p&gt;Uma das principais vantagens de construir uma aplicação web com Websockets usando Phoenix é como é &amp;ldquo;fácil&amp;rdquo; para o Erlang se conectar num cluster.&lt;/p&gt;
&lt;p&gt;Para começar, Erlang não precisa de múltiplos processos como Ruby (que fica limitado a uma conexão por processo, ou por thread se você usa um servidor com threads como o Puma). Um único processo Erlang vai tomar conta de toda a máquina, se necessário. Internamente ele mantém uma thread real por core da máquina. E cada thread tem seu próprio Scheduler para gerenciar quantos micro-processos você precisar. Você pode ler tudo sobre isso no meu post &lt;a href="http://www.akitaonrails.com/2015/11/25/yocto-services-e-meu-primeiro-mes-com-elixir"target="_blank" rel="noopener"&gt;&amp;ldquo;Yocto Services&amp;rdquo;&lt;/a&gt;.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Uma das principais vantagens de construir uma aplicação web com Websockets usando Phoenix é como é &ldquo;fácil&rdquo; para o Erlang se conectar num cluster.</p>
<p>Para começar, Erlang não precisa de múltiplos processos como Ruby (que fica limitado a uma conexão por processo, ou por thread se você usa um servidor com threads como o Puma). Um único processo Erlang vai tomar conta de toda a máquina, se necessário. Internamente ele mantém uma thread real por core da máquina. E cada thread tem seu próprio Scheduler para gerenciar quantos micro-processos você precisar. Você pode ler tudo sobre isso no meu post <a href="http://www.akitaonrails.com/2015/11/25/yocto-services-e-meu-primeiro-mes-com-elixir"target="_blank" rel="noopener">&ldquo;Yocto Services&rdquo;</a>.</p>
<p>Além disso, Erlang tem capacidades nativas de formar um cluster, onde cada instância Erlang age como um Node peer-to-peer, sem precisar de um coordenador centralizado. Você pode ler tudo sobre isso no meu post sobre <a href="http://www.akitaonrails.com/2015/11/25/exercicio-exmessenger-entendendo-nodes-em-elixir"target="_blank" rel="noopener">Nodes</a>. O poder do Erlang está em como é &ldquo;fácil&rdquo; formar sistemas distribuídos confiáveis.</p>
<p>Você pode subir várias instâncias Phoenix e, a partir de uma delas, transmitir mensagens para usuários inscritos em Channels mesmo que seus sockets estejam conectados a instâncias diferentes. É transparente e você não precisa fazer nada especial no seu código. Phoenix, Elixir e Erlang fazem todo o trabalho pesado por você nos bastidores.</p>
<h3>Sem Heroku para você :-(<span class="hx:absolute hx:-mt-20" id="sem-heroku-para-você--"></span>
    <a href="#sem-heroku-para-voc%c3%aa--" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Como você quer aproveitar esse recurso de escalabilidade e alta disponibilidade para sistemas distribuídos (no pequeno exemplo de um sistema de chat em tempo real), vai precisar ter mais controle sobre a infraestrutura. Esse requisito descarta a maioria das ofertas de Platform as a Service (PaaS), como o Heroku. O modelo do Heroku gira em torno de processos únicos e voláteis em containers isolados. Esses processos enjaulados (dynos) não têm consciência de outros processos ou da rede interna, então você não consegue subir Dynos e fazer com que formem um cluster porque eles não vão se encontrar.</p>
<p>Se você já sabe configurar coisas no Linux: Postgresql, HAproxy, etc., vá direto para a <a href="#phoenix-setup">seção específica do Phoenix</a>.</p>
<h3>IaaS (DigitalOcean) ao resgate!<span class="hx:absolute hx:-mt-20" id="iaas-digitalocean-ao-resgate"></span>
    <a href="#iaas-digitalocean-ao-resgate" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Você precisa de processos de longa duração em servidores alcançáveis pela rede (seja por rede privada, VPN, ou simplesmente - inseguro! - redes públicas).</p>
<p>Neste exemplo quero mostrar um deploy bem simples usando DigitalOcean (você pode escolher qualquer IaaS, como AWS, Google Cloud, Azure ou o que você se sentir mais confortável).</p>
<p>Criei 4 droplets (todos usando o menor tamanho de 512Mb de RAM):</p>
<ul>
<li>1 banco de dados Postgresql (ponto único de falha: não é o foco deste artigo construir um setup de banco de dados altamente disponível e replicado);</li>
<li>1 servidor HAProxy (ponto único de falha: novamente, não é o foco criar um esquema de balanceamento de carga altamente disponível);</li>
<li>2 servidores Phoenix - um no datacenter de NYC e outro no de Londres, para demonstrar como é fácil para o Erlang formar clusters mesmo com máquinas geograficamente separadas.</li>
</ul>
<h3>Configuração básica do Ubuntu 16.04<span class="hx:absolute hx:-mt-20" id="configuração-básica-do-ubuntu-1604"></span>
    <a href="#configura%c3%a7%c3%a3o-b%c3%a1sica-do-ubuntu-1604" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>Objetivos: configurar o locale, garantir que as atualizações automáticas estejam ativas, atualizar pacotes, instalar e configurar Elixir e Node.</li>
<li>Você também deveria: mudar o SSH para outra porta e instalar [fail2ban](<a href="https://www.digitalocean.com/community/tutorials/how-to-protect-ssh-with-fail2ban-on-ubuntu-14-04"target="_blank" rel="noopener">https://www.digitalocean.com/community/tutorials/how-to-protect-ssh-with-fail2ban-on-ubuntu-14-04</a>, desabilitar login por senha.</li>
</ul>
<p>Recomendo ler meu post sobre <a href="http://www.akitaonrails.com/2016/09/21/ubuntu-16-04-lts-xenial-no-vagrant-com-vmware-fusion"target="_blank" rel="noopener">configurar o Ubuntu 16.04</a>. Resumindo, comece configurando o UTF-8 corretamente:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo locale-gen &#34;en_US.UTF-8&#34;
sudo dpkg-reconfigure locales
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
echo &#39;LC_ALL=en_US.UTF-8&#39; | sudo tee -a /etc/environment
echo &#39;LANG=en_US.UTF-8&#39; | sudo tee -a /etc/environment</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Adicione um usuário adequado ao grupo sudo e, a partir de agora, não use mais o usuário root. Vou criar um usuário chamado <code>pusher</code> e vou explicar em outro post o porquê. Você deve criar um nome de usuário que faça sentido para a sua aplicação.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>adduser pusher
usermod -aG sudo pusher</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora saia e entre novamente com esse usuário. <code>ssh pusher@server-ip-address</code>. Se você estiver num Mac, copie a chave pública do seu certificado SSH assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>ssh-copy-id -i ~/.ssh/id_ed25519.pub pusher@server-ip-address</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso cria o <code>.ssh/authorized_keys</code> se não existir, define as permissões corretas e adiciona sua chave pública. Você pode fazer isso manualmente também, claro.</p>
<p>Os droplets do DigitalOcean não vêm com swap file e eu recomendaria adicionar um, especialmente se você quiser começar com as máquinas menores com menos de 1GB de RAM:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
sudo cp /etc/fstab /etc/fstab.bak
echo &#39;/swapfile none swap sw 0 0&#39; | sudo tee -a /etc/fstab
sudo sysctl vm.swappiness=10
sudo sysctl vm.vfs_cache_pressure=50
echo &#39;vm.swappiness=10&#39; | sudo tee -a /etc/sysctl.conf
echo &#39;vm.vfs_cache_pressure=50&#39; | sudo tee -a /etc/sysctl.conf</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Certifique-se de que as atualizações automáticas estejam configuradas. No mínimo você vai querer que as atualizações de segurança sejam instaladas automaticamente quando disponíveis.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo apt install unattended-upgrades</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora, vamos instalar Elixir e Node (Phoenix precisa de Node.js):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb &amp;&amp; sudo dpkg -i erlang-solutions_1.0_all.deb
curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install build-essential nodejs esl-erlang elixir erlang-eunit erlang-base-hipe
sudo npm install -g brunch
mix local.hex
mix archive.install https://github.com/phoenixframework/archives/raw/master/phoenix_new.ez # opcional, instale se quiser testar phoenix manualmente na sua máquina</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora você tem uma máquina pronta para Elixir. Crie um snapshot de imagem no DigitalOcean, mova para as regiões onde quer criar seus outros droplets e use essa imagem para criar quantos droplets precisar.</p>
<p>Para este exemplo, criei um segundo droplet na região de Londres, um terceiro para postgresql na região NYC1 e um quarto na região NYC3 para o HAProxy.</p>
<p>Vou me referir aos endereços IP públicos deles como <strong>&ldquo;nyc-ip-address&rdquo;</strong>, <strong>&ldquo;lon-ip-address&rdquo;</strong>, <strong>&ldquo;pg-ip-address&rdquo;</strong>, e <strong>&ldquo;ha-ip-address&rdquo;</strong>.</p>
<h3>Configuração básica do PostgreSQL<span class="hx:absolute hx:-mt-20" id="configuração-básica-do-postgresql"></span>
    <a href="#configura%c3%a7%c3%a3o-b%c3%a1sica-do-postgresql" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>Objetivo: configuração básica do Postgresql para permitir que os servidores Phoenix se conectem.</li>
<li>A fazer: criar uma role secundária só para conectar ao banco da aplicação e outra superusuário para criar o banco e migrar o schema. Também trancar a máquina e configurar SSH tunnels ou outro método seguro, pelo menos uma rede privada, em vez de permitir conexões TCP na porta 5432 abertas para a Internet.</li>
</ul>
<p>Agora conecte em <code>ssh pusher@pg-ip-address</code> e siga isso:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo apt-get install postgresql postgresql-contrib</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Crie uma nova role com o mesmo nome do usuário que você adicionou acima (&ldquo;pusher&rdquo; no nosso exemplo):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>$ sudo -u postgres createuser --interactive

Enter name of role to add: pusher
Shall the new role be a superuser? (y/n) y

$ sudo -u postgres createdb pusher</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O Postgresql espera encontrar um banco de dados com o mesmo nome da role e a role deve ter o mesmo nome do usuário Linux. Agora você pode usar <code>psql</code> para definir uma senha para essa nova role:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>$ sudo -u postgres psql
\password pusher</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Registre uma senha segura, anote-a e vamos em frente.</p>
<p>O Postgresql vem bloqueado para conexões externas. Uma forma de conectar de fora é configurar seus servidores para criar um <a href="https://www.postgresql.org/docs/9.1/static/ssh-tunnels.html"target="_blank" rel="noopener">SSH tunnel</a> ao servidor de banco de dados e manter as conexões TCP externas pela porta 5432 proibidas.</p>
<p>Mas neste exemplo, vamos simplesmente permitir conexões da Internet pública na porta TCP 5432. <strong>Atenção:</strong> isso é MUITO inseguro!</p>
<p>Edite o <code>/etc/postgresql/9.5/main/postgresql.conf</code> e encontre a linha de configuração <code>listen_addresses</code> e permita:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>listen_addresses = &#39;*&#39;    # what IP address(es) to listen on;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso deve vincular o servidor à porta TCP. Agora edite <code>/etc/postgresql/9.5/main/pg_hba.conf</code> e edite o final para ficar assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># IPv4 local connections:
host    all             all             127.0.0.1/32            trust
host    all             all             your-local-machine-ip-address/24        trust
host    all             all             nyc-ip-address/24       trust
host    all             all             lon-ip-address/24       trust
# IPv6 local connections:
host    all             all             ::1/128                 trust</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Salve o arquivo de configuração e reinicie o servidor:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo service postgresql restart</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Viu o que fiz aí? Só permiti conexões vindas dos IPs públicos dos servidores Phoenix. Isso não torna o servidor seguro, só um pouco menos vulnerável. Se você está atrás de uma rede DHCP/NAT, só pesquise &ldquo;what&rsquo;s my IP&rdquo; no Google para ver seu IP público - que provavelmente é compartilhado por muitos outros usuários; lembre-se que você está permitindo conexões de um IP inseguro ao seu servidor de banco de dados! Depois de fazer os testes iniciais e criar seu schema, remova a linha <code>your-local-machine-ip-address/24</code> da configuração.</p>
<p>Na sua aplicação Phoenix, edite o arquivo local <code>config/prod.secret.exs</code> para ficar assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># Configure your database</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span> <span class="ss">:your_app_name</span><span class="p">,</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Repo</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">adapter</span><span class="p">:</span> <span class="no">Ecto</span><span class="o">.</span><span class="n">Adapters</span><span class="o">.</span><span class="n">Postgres</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">username</span><span class="p">:</span> <span class="s2">&#34;pusher&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">password</span><span class="p">:</span> <span class="s2">&#34;your-super-secure-pg-password&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">database</span><span class="p">:</span> <span class="s2">&#34;your-app-database-name&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">hostname</span><span class="p">:</span> <span class="s2">&#34;pg-ip-address&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">pool_size</span><span class="p">:</span> <span class="mi">20</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Substitua as informações pelo seu servidor e banco de dados, e agora você pode testar assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>MIX_ENV=prod iex -S mix phoenix.server</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Se você ver uma mensagem <code>:econnrefused</code> do postgrex, tem algo errado. Revise sua configuração, reinicie o servidor e tente novamente. Se tudo conectar, rode <code>MIX_ENV=prod mix do ecto.create, ecto.migrate</code> para preparar seu banco de dados.</p>
<p>Por fim, você vai querer fechar o restante do seu servidor com UFW, pelo menos. UFW já vem instalado no Ubuntu 16, então basta fazer:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>ufw allow 5432
ufw allow ssh
ufw enable</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Pronto. E de novo, isso não torna seu servidor seguro, só o torna menos inseguro. Há uma diferença enorme!</p>
<p>E por falar nisso, se você é fã de Docker:</p>
<blockquote>
  <p>NÃO INSTALE UM BANCO DE DADOS DENTRO DE UM CONTAINER DOCKER!</p>

</blockquote>
<p><a href="http://patrobinson.github.io/2016/11/07/thou-shalt-not-run-a-database-inside-a-container/"target="_blank" rel="noopener">Você foi avisado</a>!</p>
<h3>Configuração básica do HAProxy<span class="hx:absolute hx:-mt-20" id="configuração-básica-do-haproxy"></span>
    <a href="#configura%c3%a7%c3%a3o-b%c3%a1sica-do-haproxy" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>Objetivos: fornecer uma solução simples de balanceamento de carga entre nossos 2 servidores Phoenix.</li>
<li>A fazer: há algo errado com a verificação de sessão ou algo assim, pois às vezes preciso atualizar o browser para não ser mandado de volta ao formulário de login na minha aplicação. Phoenix usa sessões baseadas em cookie, então não acho que esteja perdendo sessões.</li>
</ul>
<p>Agora vamos <code>ssh pusher@ha-ip-address</code>. Essa parte é fácil, basta instalar o HAProxy:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo apt-get install haproxy</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Edite <code>/etc/haproxy/haproxy.cfg</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>...
listen your-app-name
  bind 0.0.0.0:80
  mode http
  stats enable
  stats uri /haproxy?stats
  stats realm Strictly\ Private
  stats auth admin:some-secure-password-for-admin
  option forwardfor
  option http-server-close
  option httpclose
  balance roundrobin
  cookie SERVERID insert indirect nocache
  server us your_us_server_IP:8080 check cookie us1
  server uk your_uk_server_IP:8080 check cookie uk1</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Você pode omitir as linhas de <code>stats</code> se tiver outros meios de monitoramento, caso contrário defina uma senha segura para o usuário <code>admin</code>. Uma parte muito importante é o <code>option http-server-close</code>, conforme explicado <a href="http://blog.silverbucket.net/post/31927044856/3-ways-to-configure-haproxy-for-websockets"target="_blank" rel="noopener">neste outro post</a>, caso contrário você pode ter problemas com Websockets.</p>
<p>Por algum motivo estou tendo problemas com minha aplicação depois de fazer login e ela definir a sessão - às vezes preciso atualizar para ser redirecionado à página correta, ainda não sei o porquê e acredito que seja algo na configuração do HAProxy. Se alguém souber o que é, me avise nos comentários. Por ora, estou recorrendo a Sticky sessions (afinidade de servidor) fazendo o HAProxy escrever um cookie de volta com o servidor.</p>
<p>Agora reinicie o servidor e habilite o UFW também:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo service haproxy restart
sudo ufw allow http
sudo ufw allow https
sudo ufw allow ssh
sudo ufw enable</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Você pode facilmente adicionar suporte a SSL na sua aplicação configurando o HAProxy (e não os nodes Phoenix). A <a href="https://www.digitalocean.com/community/tutorials/how-to-secure-haproxy-with-let-s-encrypt-on-ubuntu-14-04"target="_blank" rel="noopener">documentação do DigitalOcean</a> é abrangente, é só seguir. No fim, meu <code>haproxy.cfg</code> ficou assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>global
   log /dev/log    local0
   log /dev/log    local1 notice
   chroot /var/lib/haproxy
   stats socket /run/haproxy/admin.sock mode 660 level admin
   stats timeout 30s
   user haproxy
   group haproxy
   daemon
   maxconn 2048
   tune.ssl.default-dh-param 2048

defaults
   log global
   mode http
   option httplog
   option dontlognull
   option redispatch
   option forwardfor
   option http-server-close
   timeout connect 5000
   timeout client  50000
   timeout server  50000

frontend www-http
   bind your_ha_proxy_IP:80
   reqadd X-Forwarded-Proto:\ http
   default_backend www-backend

frontend www-https
   bind your_ha_proxy_IP:443 ssl crt /etc/haproxy/certs/your_domain.pem
   reqadd X-Forwarded-Proto:\ https
   acl letsencrypt-acl path_beg /.well-known/acme-challenge/
   use_backend letsencrypt-backend if letsencrypt-acl
   default_backend www-backend

backend www-backend
   redirect scheme https if !{ ssl_fc }
   # setting session stickiness
   cookie SERVERID insert indirect nocache
   server us your_us_server_IP:8080 check cookie us1
   server uk your_uk_server_IP:8080 check cookie uk1

backend letsencrypt-backend
   server letsencrypt 127.0.0.1:54321</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Por fim, vou assumir que você tem um servidor/serviço de DNS em algum lugar onde pode registrar o IP deste servidor HAProxy como um registro A para acessá-lo por um nome completo como &ldquo;your-app-name.mydomain.com&rdquo;.</p>
<p><a name="phoenix-setup"></a></p>
<h3>Configuração básica do Phoenix<span class="hx:absolute hx:-mt-20" id="configuração-básica-do-phoenix"></span>
    <a href="#configura%c3%a7%c3%a3o-b%c3%a1sica-do-phoenix" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>Objetivo: configurar a app Phoenix para ser deployável. Configurar os servidores com os arquivos de configuração necessários.</li>
<li>A fazer: descobrir uma forma de reduzir os tempos absurdamente longos de deploy.</li>
</ul>
<p>Finalmente, temos quase tudo no lugar.</p>
<p>Vou assumir que você já tem uma aplicação Phoenix funcionando, caso contrário crie uma a partir de qualquer um dos muitos tutoriais disponíveis.</p>
<p>Montei estas informações a partir de <a href="https://medium.com/@diamondgfx/deploying-phoenix-applications-with-exrm-97a3867ebd04#.7qyuplncx"target="_blank" rel="noopener">posts</a> como <a href="http://engineering.pivotal.io/post/how-to-set-up-an-elixir-cluster-on-amazon-ec2/"target="_blank" rel="noopener">este muito útil</a> do Pivotal sobre deploy baseado em AWS. Em resumo, você precisa fazer uma série de alterações na sua configuração.</p>
<p>Quando você está desenvolvendo a aplicação, vai notar que toda vez que roda, ela compila incrementalmente o que mudou. Os binários ficam em <code>_build/dev</code> ou <code>_build/test</code> na forma de binários <code>.beam</code> (similar ao que <code>.class</code> são para Java).</p>
<p>Diferente de Ruby, Python ou PHP, você não está fazendo deploy de código-fonte para servidores de produção. É mais parecido com Java, onde você precisa ter tudo compilado em binários e empacotado no que chamamos de <strong>release</strong>. É como um &ldquo;.war&rdquo; ou &ldquo;.ear&rdquo; se você vem do mundo Java.</p>
<p>Para criar esse pacote as pessoas costumavam usar &ldquo;exrm&rdquo;, mas está sendo substituído por <a href="https://github.com/bitwalker/distillery"target="_blank" rel="noopener">&ldquo;distillery&rdquo;</a>, então vamos usá-lo.</p>
<p>Aí, se você vem do Ruby está familiarizado com Capistrano. Ou se vem do Python, conhece o clone do Capistrano, Fabric. Elixir tem uma ferramenta similar (bem mais simples por enquanto), chamada <a href="https://github.com/boldpoker/edeliver"target="_blank" rel="noopener">&ldquo;edeliver&rdquo;</a>. É basicamente uma ferramenta de automação SSH.</p>
<p>Você adiciona eles ao <code>mix.exs</code> como qualquer outra dependência:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">application</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="o">[</span><span class="ss">mod</span><span class="p">:</span> <span class="p">{</span><span class="no">ExPusherLite</span><span class="p">,</span> <span class="o">[]</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">   <span class="ss">applications</span><span class="p">:</span> <span class="o">[...</span><span class="p">,</span> <span class="ss">:edeliver</span><span class="o">]]</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">deps</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="o">[...</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="p">{</span><span class="ss">:edeliver</span><span class="p">,</span> <span class="s2">&#34;~&gt; 1.4.0&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">   <span class="p">{</span><span class="ss">:distillery</span><span class="p">,</span> <span class="s2">&#34;~&gt; 1.0&#34;</span><span class="p">}</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Do post do Pivotal, o detalhe importante para não esquecer é editar esta parte no arquivo <code>config/prod.exs</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="ss">http</span><span class="p">:</span> <span class="o">[</span><span class="ss">port</span><span class="p">:</span> <span class="mi">8080</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="ss">url</span><span class="p">:</span> <span class="o">[</span><span class="ss">host</span><span class="p">:</span> <span class="s2">&#34;your-app-name.yourdomain.com&#34;</span><span class="p">,</span> <span class="ss">port</span><span class="p">:</span> <span class="mi">80</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span> <span class="ss">:phoenix</span><span class="p">,</span> <span class="ss">:serve_endpoints</span><span class="p">,</span> <span class="kp">true</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Você PRECISA deixar fixo o PORT padrão do servidor web Phoenix e o domínio permitido (lembra do nome de domínio que você associou ao seu servidor HAProxy acima? Esse mesmo). E PRECISA descomentar a linha <code>:serve_endpoints, true</code>!</p>
<p>Para o edeliver funcionar, você precisa criar um arquivo <code>.deliver/config</code> assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>USING_DISTILLERY=true

# change this to your app name:
APP=&#34;your-app-name&#34;

# change this to your own servers IP and add as many as you want
US=&#34;nyc-ip-address&#34;
UK=&#34;lon-ip-address&#34;

# the user you created in your Ubuntu machines above
USER=&#34;pusher&#34;

# which server do you want to build the first release?
BUILD_HOST=$US
BUILD_USER=$USER
BUILD_AT=&#34;/tmp/edeliver/$APP/builds&#34;

# list the production servers declared above:
PRODUCTION_HOSTS=&#34;$US $UK&#34;
PRODUCTION_USER=$USER
DELIVER_TO=&#34;/home/$USER&#34;

# do not change here

LINK_VM_ARGS=&#34;/home/$USER/vm.args&#34;

# For *Phoenix* projects, symlink prod.secret.exs to our tmp source
pre_erlang_get_and_update_deps() {
  local _prod_secret_path=&#34;/home/$USER/prod.secret.exs&#34;
  if [ &#34;$TARGET_MIX_ENV&#34; = &#34;prod&#34; ]; then
    __sync_remote &#34;
      ln -sfn &#39;$_prod_secret_path&#39; &#39;$BUILD_AT/config/prod.secret.exs&#39;

      cd &#39;$BUILD_AT&#39;

      mkdir -p priv/static

      mix deps.get

      npm install

      brunch build --production

      APP=&#39;$APP&#39; MIX_ENV=&#39;$TARGET_MIX_ENV&#39; $MIX_CMD phoenix.digest $SILENCE
    &#34;
  fi
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Lembra de todas as informações que fomos juntando desde o início desta longa receita? São essas opções que você PRECISA mudar para as suas. Basta seguir os comentários no conteúdo do arquivo acima e adicionar ao seu repositório git. Aliás, seu projeto está num repositório GIT adequado, né??</p>
<p>Se você gosta de usar chaves SSH privadas protegidas por passphrase, vai ser uma dor de cabeça enorme para fazer deploy porque, para cada comando, o edeliver vai emitir um comando SSH que vai ficar pedindo sua passphrase, umas dez vezes ao longo de todo o processo. Você foi avisado! Se ainda assim não se importar, e estiver num Mac, vai ter um problema extra porque o Terminal não consegue criar um prompt para você digitar sua passphrase. Você precisa criar um <a href="https://github.com/markcarver/mac-ssh-askpass"target="_blank" rel="noopener">script</a> <code>/usr/local/bin/ssh-askpass</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>#!/bin/bash
# Script: ssh-askpass
# Author: Mark Carver
# Created: 2011-09-14
# Licensed under GPL 3.0

# A ssh-askpass command for Mac OS X
# Based from author: Joseph Mocker, Sun Microsystems
# http://blogs.oracle.com/mock/entry/and_now_chicken_of_the

# To use this script:
#   Install this script running INSTALL as root
#
# If you plan on manually installing this script, please note that you will have
# to set the following variable for SSH to recognize where the script is located:
#   export SSH_ASKPASS=&#34;/path/to/ssh-askpass&#34;

TITLE=&#34;${SSH_ASKPASS_TITLE:-SSH}&#34;;
TEXT=&#34;$(whoami)&#39;s password:&#34;;
IFS=$(printf &#34;\n&#34;);
CODE=(&#34;on GetCurrentApp()&#34;);
CODE=(${CODE[*]} &#34;tell application \&#34;System Events\&#34; to get short name of first process whose frontmost is true&#34;);
CODE=(${CODE[*]} &#34;end GetCurrentApp&#34;);
CODE=(${CODE[*]} &#34;tell application GetCurrentApp()&#34;);
CODE=(${CODE[*]} &#34;activate&#34;);
CODE=(${CODE[*]} &#34;display dialog \&#34;${@:-$TEXT}\&#34; default answer \&#34;\&#34; with title \&#34;${TITLE}\&#34; with icon caution with hidden answer&#34;);
CODE=(${CODE[*]} &#34;text returned of result&#34;);
CODE=(${CODE[*]} &#34;end tell&#34;);
SCRIPT=&#34;/usr/bin/osascript&#34;
for LINE in ${CODE[*]}; do
      SCRIPT=&#34;${SCRIPT} -e $(printf &#34;%q&#34; &#34;${LINE}&#34;)&#34;;
done;
eval &#34;${SCRIPT}&#34;;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora faça isso:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo chmod &#43;x /usr/local/bin/ssh-askpass
sudo ln -s /usr/local/bin/ssh-askpass /usr/X11R6/bin/ssh-askpass</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Lembre-se, apenas para Macs. E agora toda vez que tentar fazer deploy você vai receber vários prompts gráficos pedindo a passphrase da chave privada SSH. É de enlouquecer! E você precisa ter o <a href="https://www.xquartz.org/"target="_blank" rel="noopener">XQuartz</a> instalado, por sinal.</p>
<p>Agora você precisa criar manualmente 3 arquivos em todos os servidores Phoenix. Comece com <code>your-app-name/vm.args</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>-name us@nyc-ip-address
-setcookie @bCd&amp;fG
-kernel inet_dist_listen_min 9100 inet_dist_listen_max 9155
-config /home/pusher/your-app-name.config
-smp auto</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <code>/home/pusher/your-app-name</code> é o diretório onde a release vai ser descomprimida depois que o edeliver fizer o deploy do tarball.</p>
<p>Você precisa criar este arquivo em todas as máquinas Phoenix, mudando o bit <code>-name</code> pelo mesmo nome que declarou no arquivo <code>.deliver/config</code>. O <code>-setcookie</code> pode ser qualquer nome, desde que seja o mesmo em todos os servidores.</p>
<p>Viu o <code>-config /home/pusher/your-app-name.config</code>? Crie esse arquivo com o seguinte:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>[{kernel,
  [
    {sync_nodes_optional, [&#39;uk@lon-ip-address&#39;]},
    {sync_nodes_timeout, 30000}
  ]}
].</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso é código-fonte Erlang. Na máquina de NYC você deve declarar o nome de Londres, e vice-versa. Se você tiver várias máquinas, todas elas exceto a que você está agora. Entendeu?</p>
<p>Por fim, para a própria app Phoenix, você sempre tem um <code>config/prod.secret.exs</code> que nunca deve ser adicionado com <code>git add</code> ao repositório, lembra? É aqui que você coloca as informações do servidor Postgresql e a chave secreta aleatória para assinar os cookies de sessão:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>use Mix.Config

config :your_app_name, YourAppName.Endpoint,
  secret_key_base: &#34;...&#34;

# Configure your database
config :your_app_name, YourAppName.Repo,
  adapter: Ecto.Adapters.Postgres,
  username: &#34;pusher&#34;,
  password: &#34;your-super-secure-pg-password&#34;,
  database: &#34;your-app-database-name&#34;,
  hostname: &#34;pg-ip-address&#34;,
  pool_size: 20

# if you have Guardian, for example:
config :guardian, Guardian,
  secret_key: &#34;...&#34;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Como você cria uma nova chave secreta aleatória? Na sua máquina de desenvolvimento, basta rodar: <code>mix phoenix.gen.secret</code> e copiar a string gerada para o arquivo acima.</p>
<p>Então agora você deve ter esses 3 arquivos em cada servidor Phoenix, na pasta home <code>/home/pusher</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>~/your-app-name/vm.args
~/prod.secret.exs
~/your-app-name.config</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Conforme a <a href="https://hexdocs.pm/distillery/runtime-configuration.html#vm-args"target="_blank" rel="noopener">documentação do distillery</a>, você precisa definir uma variável de ambiente para informar a localização do arquivo <code>vm.args</code>, e isso é <strong>SUPER</strong> importante - caso contrário ele vai gerar um padrão que não define o nome e cookie corretos, então os nodes não vão se encontrar depois de subir.</p>
<p>Usando <code>sudo</code>, edite o <code>/etc/environment</code> e adicione a linha:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>RELEASE_CONFIG_DIR=/home/pusher/your-app-name
VMARGS_PATH=/home/pusher/your-app-name/vm.args</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Faça isso em todos os servidores phoenix.</p>
<p>No seu diretório de desenvolvimento local você ainda precisa rodar isso:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>mix release.init</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso vai gerar um arquivo padrão <code>rel/config.exs</code> que você precisa adicionar ao repositório git com as seguintes alterações no final:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="n">environment</span> <span class="ss">:prod</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">plugin</span> <span class="no">Releases</span><span class="o">.</span><span class="n">Plugin</span><span class="o">.</span><span class="n">LinkConfig</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">release</span> <span class="ss">:your_app_name</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">set</span> <span class="ss">version</span><span class="p">:</span> <span class="n">current_version</span><span class="p">(</span><span class="ss">:your_app_name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">set</span> <span class="ss">applications</span><span class="p">:</span> <span class="o">[</span>
</span></span><span class="line"><span class="cl">    <span class="ss">your_app_name</span><span class="p">:</span> <span class="ss">:permanent</span>
</span></span><span class="line"><span class="cl">  <span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O plugin deve habilitar a release a encontrar o arquivo <code>vm.args</code> local no servidor (isso é super importante, caso contrário os comandos remotos como start, stop, ping, etc. não vão funcionar e os nodes não vão carregar as informações corretas para subir e formar um cluster).</p>
<p>Finalmente, tudo pronto, você pode executar este comando:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>$ mix edeliver build release production --skip-mix-clean --verbose</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Se terminar sem erros você vai ter uma mensagem parecida com esta no final:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>...
==&gt; Release successfully built!
    You can run it in one of the following ways:
      Interactive: _build/prod/rel/your-app-name/bin/your-app-name console
      Foreground: _build/prod/rel/your-app-name/bin/your-app-name foreground
      Daemon: _build/prod/rel/your-app-name/bin/your-app-name start
--&gt; Copying release 0.0.1 to local release store
--&gt; Copying your-app-name.tar.gz to release store

RELEASE BUILD OF YOUR-APP-NAME WAS SUCCESSFUL!</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora, isso vai demorar um tempo absurdamente longo para fazer o deploy. Isso porque vai clonar o código-fonte da sua app com git, buscar todas as dependências Elixir (toda vez!), compilar tudo, depois rodar o lentíssimo <code>npm install</code> (toda vez!), processar os assets com brunch, criar a tal &ldquo;release&rdquo;, comprimir em tar e gzip, baixar e enviar via SCP para as outras máquinas configuradas.</p>
<p>No arquivo <code>.deliver/config</code> você definiu a opção <code>BUILD_HOST</code>. É a máquina onde todo esse processo acontece, então você vai querer que pelo menos essa máquina seja mais robusta que as outras. Como estou usando droplets pequenos de 512Mb, o processo leva uma eternidade.</p>
<p>O último comando vai baixar o tarball gerado. Ele precisa fazer isso para garantir que os <a href="http://erlang.org/doc/tutorial/nif.html"target="_blank" rel="noopener">NIFs</a> estejam compilados no ambiente nativo onde vai rodar, porque se você usa Mac, binários de Mac não vão rodar no Linux.</p>
<p>Agora precisamos fazer o upload e descompressão desse tarball em cada servidor com o seguinte comando:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>mix edeliver deploy release to production</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Quanto mais lenta sua rede, mais tempo vai levar, já que está enviando um tarfile grande pela internet pública, então certifique-se de estar numa conexão rápida. Quando terminar, podemos reiniciar os servidores (se você já tinha instâncias rodando):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>mix edeliver stop production
mix edeliver start production</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Se você fizer tudo certo, o processo do edeliver termina sem erros e deixa um daemon rodando no seu servidor, assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>/home/pusher/your-app-name/erts-8.2/bin/beam -- -root /home/pusher/your-app-name -progname home/pusher/your-app-name/releases/0.0.1/your-app-name.sh -- -home /home/pusher -- -boot /home/pusher/your-app-name/releases/0.0.1/your-app-name -config /home/pusher/your-app-name/running-config/sys.config -boot_var ERTS_LIB_DIR /home/pusher/your-app-name/erts-8.2/../lib -pa /home/pusher/your-app-name/lib/your-app-name-0.0.1/consolidated -name us@nyc-ip-address -setcookie ex-push&amp;r-l!te -kernel inet_dist_listen_min 9100 inet_dist_listen_max 9155 -config /home/pusher/your-app-name.config -mode embedded -user Elixir.IEx.CLI -extra --no-halt &#43;iex -- console</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Ainda vai demorar bastante, mas deve ser mais fácil. Então aqui vai uma dica pro para usuários Linux. Siga <a href="https://gist.github.com/mattweldon/2e8ecb953216438ad168"target="_blank" rel="noopener">este Gist</a> para mais detalhes; você precisa emular o que é executado na segunda metade do arquivo <code>.deliver/config</code>.</p>
<p>Note também que rodei as migrations manualmente, mas você pode fazer isso usando <code>mix edeliver migrate</code>.</p>
<p>Leia a documentação deles para mais comandos e configurações.</p>
<p>E não esqueça de habilitar o UFW:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo ufw allow ssh
sudo ufw allow 8080
sudo ufw allow 4369
sudo ufw allow proto tcp from any to any port 9100:9155
sudo ufw default allow outgoing
sudo ufw enable</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Depurando bugs em produção<span class="hx:absolute hx:-mt-20" id="depurando-bugs-em-produção"></span>
    <a href="#depurando-bugs-em-produ%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Logo depois que fiz o deploy, obviamente falhou. E o problema é que os arquivos <code>/home/pusher/your-app-name/log/erlang.log</code> (eles são rotacionados automaticamente, então você pode encontrar vários arquivos terminando em número) não vão mostrar muita coisa.</p>
<p>O que eu recomendo é alterar o arquivo <code>config/prod.exs</code> APENAS na sua máquina de desenvolvimento e mudar o nível de log para <code>config :logger, level: :debug</code>, usar o mesmo <code>prod.secret.exs</code> que você editou nos servidores e rodar localmente com <code>MIX_ENV=prod iex -S mix phoenix.server</code>.</p>
<p>Por exemplo, em modo de desenvolvimento eu tinha um código no controller que verificava a existência de um parâmetro de query string opcional assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">if</span> <span class="n">params</span><span class="o">[</span><span class="s2">&#34;some_parameter&#34;</span><span class="o">]</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl"> <span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Funcionava bem em desenvolvimento mas travava em produção, então tive que mudar para:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">if</span> <span class="no">Map</span><span class="o">.</span><span class="n">has_key?</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="s2">&#34;some_parameter&#34;</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl"> <span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Outro problema foi que Guardian funcionava normalmente em desenvolvimento, mas em produção precisei declarar sua aplicação no <code>mix.exs</code> assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">application</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="o">[</span><span class="ss">mod</span><span class="p">:</span> <span class="p">{</span><span class="no">ExPusherLite</span><span class="p">,</span> <span class="o">[]</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">   <span class="ss">applications</span><span class="p">:</span> <span class="o">[...</span><span class="p">,</span> <span class="ss">:guardian</span><span class="p">,</span> <span class="ss">:edeliver</span><span class="o">]]</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Estava recebendo erros <code>:econnrefused</code> porque esqueci de rodar <code>MIX_ENV=prod mix do ecto.create, ecto.migrate</code> como instruí acima. Depois de resolver essas questões, minha aplicação estava funcionando em <code>http://your-app-name.yourdomain.com</code>, o HAProxy estava encaminhando corretamente para a porta 8080 nos servidores e tudo roda bem, incluindo as conexões WebSocket.</p>
<h3>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Como mencionei acima, esse tipo de procedimento me faz sentir falta de uma solução de deploy fácil como o Heroku.</p>
<p>O único problema que estou enfrentando agora é que quando faço login pela página de sign in do Coherence, não sou redirecionado para o URI correto que estava tentando acessar (&quot;/admin&quot; no meu caso) - às vezes recarregar depois do login funciona, às vezes não. Às vezes estou dentro de uma página &ldquo;/admin&rdquo; mas quando clico em um dos links ele me manda de volta para a página de login mesmo estando logado. Não sei se é um bug no Coherence, ExAdmin, no próprio Phoenix ou uma configuração errada do HAProxy. Vou atualizar este post se descobrir.</p>
<p>O Edeliver também leva um tempo obsceno para fazer deploy. Até esperar os sprockets processarem num <code>git push heroku master</code> parece muito mais rápido em comparação. E isso para uma app Phoenix bem enxuta. Ter que buscar tudo (porque o Hex não mantém um cache global local, todas as dependências são vendorizadas estaticamente no diretório do projeto) e ter que rodar o lentíssimo npm não ajuda em nada.</p>
<p>Ainda preciso pesquisar se há opções mais rápidas, mas por ora o que tenho &ldquo;funciona&rdquo;.</p>
<p>E mais importante: agora tenho um cluster escalável para WebSockets bidirecionais em tempo real, que é a principal razão pela qual alguém pode querer usar Phoenix.</p>
<p>Se você quer construir um site &ldquo;normal&rdquo;, mantenha simples e faça em Rails, Django, Express ou qual for seu framework web preferido. Se você quer comunicações em tempo real de forma fácil, talvez eu tenha uma solução melhor. Fique de olho no blog para novidades que vêm por aí! ;-)</p>
]]></content:encoded><category>elixir-e-erlang</category><category>devops</category><category>cloud</category><category>sistemas-distribuidos</category></item><item><title>Moving away from Slack into Rocket.chat. Good Enough.</title><link>https://akitaonrails.github.io/2016/08/09/moving-away-from-slack-into-rocket-chat-good-enough/</link><guid isPermaLink="true">https://akitaonrails.github.io/2016/08/09/moving-away-from-slack-into-rocket-chat-good-enough/</guid><pubDate>Tue, 09 Aug 2016 16:21:00 -0300</pubDate><description>&lt;p&gt;In my quest to create my own private infrastructure I&amp;rsquo;ve already moved away from Pivotal Tracker (saving this for a future post), Bitbucket, Github, Travis and into GitLab and GitLab Runner. There are more services I will move away from in the future, so expect more posts like this.&lt;/p&gt;
&lt;p&gt;In &lt;a href="http://www.akitaonrails.com/2016/08/03/migrando-para-o-gitlab-sim-vale-a-pena"target="_blank" rel="noopener"&gt;my previous post&lt;/a&gt; I forgot to mention minimal firewall and SSL configuration, but you can check it here.&lt;/p&gt;
&lt;p&gt;One of the most difficult pieces to move away from is Slack. Don&amp;rsquo;t get me wrong, it&amp;rsquo;s a great tool and it gets the job done. The free-tier is really competent but I really want some of the payed features such as history of all conversations with search capabilities and uploads without limit.&lt;/p&gt;</description><content:encoded><![CDATA[<p>In my quest to create my own private infrastructure I&rsquo;ve already moved away from Pivotal Tracker (saving this for a future post), Bitbucket, Github, Travis and into GitLab and GitLab Runner. There are more services I will move away from in the future, so expect more posts like this.</p>
<p>In <a href="http://www.akitaonrails.com/2016/08/03/migrando-para-o-gitlab-sim-vale-a-pena"target="_blank" rel="noopener">my previous post</a> I forgot to mention minimal firewall and SSL configuration, but you can check it here.</p>
<p>One of the most difficult pieces to move away from is Slack. Don&rsquo;t get me wrong, it&rsquo;s a great tool and it gets the job done. The free-tier is really competent but I really want some of the payed features such as history of all conversations with search capabilities and uploads without limit.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/555/big_Franz.png" alt="Codeminer Rocket"  loading="lazy" /></p>
<h2>The Cost Conundrum<span class="hx:absolute hx:-mt-20" id="the-cost-conundrum"></span>
    <a href="#the-cost-conundrum" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>But then you have to commit to pay USD 8/month per user (USD 6.67 if you pay ahead of time). If you&rsquo;re a small team (around a dozen people) it&rsquo;s well worth it and you should definitely pay and get over with.</p>
<p>If you have more than 20 people then it starts to become a conundrum. On my GitLab cost calculations, some people argued that saving USD 15k yearly is not such a big deal, and they&rsquo;re not wrong.</p>
<p>If you&rsquo;re from outside of the US coastal cities, specially from countries in South America, Asia where currency can be more than 3 times devalued, than it quickly starts to become a big deal.</p>
<p>Apart from that, we are currently offloading a whole lot of out IPs and knowledge into the care of companies we don&rsquo;t really know and where we don&rsquo;t hold any real stakes.</p>
<p>If you&rsquo;re a medium to big company it really starts to make sense to want to have more control over your own property. Of course, it costs some work and maintenance, so it&rsquo;s not for everybody.</p>
<p>What I will argue is that if you have <strong>150 people</strong> or more, and you&rsquo;re still growing, it&rsquo;s not such a big burden to jump into your own infrastructure.</p>
<blockquote>
  <p>You avoid paying around <a href="https://codeminer42.slack.com/pricing"target="_blank" rel="noopener"><strong>USD 12,000 a year</strong></a> or more, and instead you will pay <strong>USD 960 a year</strong> in Digital Ocean boxes and MMS plus a few hours every month in basic maintenance. That&rsquo;s 12 times cheaper.</p>

</blockquote>
<p>Again, you shouldn&rsquo;t do it just for the cost savings as the maintenance burden can easily not be worth it if you or your team are not devops savvy and you don&rsquo;t have anyone to keep monitoring it. For most companies, the conversation history is not so important and even the free-tier will be more than enough.</p>
<p>Cost is less relevant depending on your context, and there are many different uses for group chats. For example, if you&rsquo;re an <a href="https://news.ycombinator.com/item?id=9754626"target="_blank" rel="noopener">open source community</a>, you would try <a href="https://gitter.im"target="_blank" rel="noopener">Gitter</a> instead.</p>
<p>There are also <a href="https://www.google.com/search?q=slack&#43;alternatives"target="_blank" rel="noopener">many other alternatives</a> that you may want to try.</p>
<p>For the sake of this article, let&rsquo;s assume you&rsquo;re a small to medium company, in need of private rooms, many external users (for example, your clients), with at least 50 people. Then this can start to make sense. Any other scenario and we have to analyze it differently.</p>
<h2>Why I chose Rocket.chat<span class="hx:absolute hx:-mt-20" id="why-i-chose-rocketchat"></span>
    <a href="#why-i-chose-rocketchat" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>With the cost reasoning out of the way, then it&rsquo;s a matter of choosing which alternative to use.</p>
<p>I tried <a href="https://www.mattermost.org/"target="_blank" rel="noopener">MatterMost</a> first and I really wanted to like it. It has a very similar feature set to Slack and most importantly, it&rsquo;s written in Go. So it&rsquo;s both lightweight, very fast, and not so difficult to contribute. It uses MySQL or PostgreSQL which I like.</p>
<p>Unfortunatelly, it has an important <strong>show-stopper</strong> for my needs. You can create both public channels or private groups, but anyone can delete a private group. If you want permission control for that you must use their <a href="https://about.mattermost.com/pricing/"target="_blank" rel="noopener">&ldquo;Enterprise&rdquo; offering</a> and pay <strong>USD 20/year per user</strong>.</p>
<blockquote>
  <p>It&rsquo;s still at least <strong>3 times cheaper</strong> than Slack, so it may be a good option for you.</p>

</blockquote>
<p>I really want to have full-control, not just make it cheaper. I could pay Slack already if I didn&rsquo;t care, so I would not just pay an alternative and not have full-control (including the ability to tweak and improve the entire code base).</p>
<p>Then, it took me back to <a href="https://rocket.chat/"target="_blank" rel="noopener">Rocket.chat</a>. It&rsquo;s feature set also rivals Slack, it&rsquo;s good looking enough. But, it&rsquo;s made in Meteor. Now, I don&rsquo;t have anything against Meteor and I really think a Slack-clone is exactly the kind of use case where you could use Meteor to its full potential.</p>
<p>Technically I really think it&rsquo;s a downside to be forced to use Mongodb (Meteor requires Mongo). For small installations I really prefer to have PostgreSQL. MongoDB is competent, but for medium to big installations, and I will show you why below.</p>
<p>I <strong>strongly dislike</strong> the <a href="https://medium.com/friendship-dot-js/i-peeked-into-my-node-modules-directory-and-you-wont-believe-what-happened-next-b89f63d21558#.8jd3z3n6u"target="_blank" rel="noopener">culture of writing software without <em>minimal care</em></a> such as having a reasonably complete test suite. You browse through the many packages that comprise Rocket.chat and several of them have no tests whatsoever. Some packages do have jasmine tests, but the majority is lacking.</p>
<p>This is <a href="https://github.com/RocketChat/Rocket.Chat/blob/a5cb22bb0017f4c39654bf1e2895ae64acb0339b/packages/rocketchat-katex/tests/jasmine/client/unit/katex.spec.coffee"target="_blank" rel="noopener">one example</a> I picked randomly, out of the few test files I found:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">describe</span> <span class="s1">&#39;rocketchat:katex Client&#39;</span><span class="p">,</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">it</span> <span class="s1">&#39;should exist&#39;</span><span class="p">,</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">expect</span><span class="p">(</span><span class="nx">RocketChat</span><span class="p">.</span><span class="nx">katex</span><span class="p">).</span><span class="nx">toBeDefined</span><span class="p">()</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This is sad, sorry to say that, but it is. Not surprisingly I&rsquo;ve seen katex issues while using the interface. And if you think this was just a poor choice, I picked <a href="https://github.com/RocketChat/Rocket.Chat/blob/a5cb22bb0017f4c39654bf1e2895ae64acb0339b/packages/rocketchat-markdown/tests/jasmine/client/unit/markdown.spec.coffee"target="_blank" rel="noopener">another test file</a>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">describe</span> <span class="s1">&#39;rocketchat:markdown Client&#39;</span><span class="p">,</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">it</span> <span class="s1">&#39;should exist&#39;</span><span class="p">,</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">expect</span><span class="p">(</span><span class="nx">RocketChat</span><span class="p">.</span><span class="nx">Markdown</span><span class="p">).</span><span class="nx">toBeDefined</span><span class="p">()</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Really?</p>
<p>So, the code itself is not so pretty, you have to keep this in mind. I&rsquo;d much rather use Mattermost (which is much better structured and with enough tests), but it&rsquo;s not entirely free, so it&rsquo;s not an option to me. For my scenario, I&rsquo;d rather have sloppy code (that minimally &ldquo;works&rdquo;) that I can tweak than code I can&rsquo;t see, for this particular venture. Many inexperienced developers may argue about the necessity of having automated tests, but not having them also impacts this:</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/552/big_Contributors_to_RocketChat_Rocket_Chat.png" alt="Rocket.chat contribution graph"  loading="lazy" /></p>
<p>If we see how the project is evolving over time, we can see that interest is diminishing. The lack of good automated tests also make it more difficult for new people to contribute without adding regressions, make contribution reviews more difficult and time consuming.</p>
<p>On the other hand the feature set is good enough as it is. There is only so much you can add to a Slack-clone. The upside is that if the code doesn&rsquo;t change too much, I can tweak it more without having many things breaking all the time. It&rsquo;s the complete opposite from the impressive contribution boost over at GitLab, for example.</p>
<p>Doing a few days of testing through the administration settings, user interface, having dozens of users doing real conversation, convinced me that even though the code quality is bad, the feature set is indeed there. I think I can live with that. Your mileage may vary though.</p>
<p>I am using <a href="http://meetfranz.com"target="_blank" rel="noopener">Franz</a> to open Slack, Rocket.chat, Hangouts, Messenger and any number of other communicators in one single app, which is great. And Rocket.chat does provide <a href="https://itunes.apple.com/us/app/rocket.chat/id1028869439?mt=8"target="_blank" rel="noopener">mobile</a> <a href="https://play.google.com/store/apps/details?id=com.konecty.rocket.chat&amp;hl=en"target="_blank" rel="noopener">apps</a> based off PhoneGap. Again, it&rsquo;s good, not great.</p>
<p>Many people have heavy bots or integrations into their Slack configuration. The alternatives such as Rocket.chat do support incoming and outgoing webhooks and most of the integrations should be possible (with some tweaking and hacking such as this <a href="https://github.com/FinndropStudios/GifRocket"target="_blank" rel="noopener">Giphy example</a>), so make a list of all important integrations and do a Google research on that before continuing.</p>
<h2>Planning the Infrastructure<span class="hx:absolute hx:-mt-20" id="planning-the-infrastructure"></span>
    <a href="#planning-the-infrastructure" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>All that having being said, I&rsquo;d recommend you try Rocket.chat&rsquo;s <a href="https://demo.rocket.chat"target="_blank" rel="noopener">demo server</a> first to see if it has the minimal features you need. And also that you install a dry-run in a small Digital Ocean box with the intention to blow it off later.</p>
<p>For a single-instance, stand-alone installation, temporary dry-run, I recommend you start by installing the <a href="https://www.digitalocean.com/community/tutorials/how-to-use-the-mongodb-one-click-application"target="_blank" rel="noopener">MongoDB One-Click Application</a> and follow <a href="https://www.digitalocean.com/community/tutorials/how-to-install-configure-and-deploy-rocket-chat-on-ubuntu-14-04"target="_blank" rel="noopener">this instruction</a> to install everything in one machine.</p>
<p>This is the worst possible production environment, which is why I am being so repetitive into saying that you should destroy this box after you do your testing.</p>
<p>As I&rsquo;ve said before, I am assuming a scenario where you have 50 or more people, with real clients, important communication going on. You don&rsquo;t want to keep a simple installation like this for production usage.</p>
<p>For a <strong>minimal production installation</strong> you will need at the very least 4 boxes with 1 GB of RAM each. Now, why 4?</p>
<p>This is an important section for most of the beginner Javascript programmers out there playing with the so called &ldquo;MEAN&rdquo; stack (MongoDB, Express, Angularjs, Node.js). I&rsquo;ve seen people deploy single-instance, stand-alone &ldquo;MEAN&rdquo; apps for production usage. And this is super bad.</p>
<blockquote>
  <p><strong>MongoDB is NOT MEANT to function with just one single instance!</strong></p>

</blockquote>
<p>More than that:</p>
<blockquote>
  <p><strong>DO NOT start an even number of MongoDB nodes, or at least install an Arbiter</strong></p>

</blockquote>
<p>So, the minimal setup for MongoDB is a <a href="https://docs.mongodb.com/manual/core/replica-set-architecture-three-members/"target="_blank" rel="noopener">Three-member Replica Set</a>, no less!</p>
<p>The article <a href="https://www.rainforestqa.com/blog/2012-11-05-mongodb-gotchas-and-how-to-avoid-them/"target="_blank" rel="noopener">&ldquo;MongoDB Gotchas &amp; How To Avoid Them&rdquo;</a> is a little bit old but you should be aware of several of the documented gotchas, including adding replica sets and avoid waiting too long to start sharding you data.</p>
<p>The most important part of making Rocket.chat reliable is to make your MongoDB minimally reliable, and this is a hidden cost you must consider.</p>
<h4>Installing a Three-Member Replica Set<span class="hx:absolute hx:-mt-20" id="installing-a-three-member-replica-set"></span>
    <a href="#installing-a-three-member-replica-set" class="subheading-anchor" aria-label="Permalink for this section"></a></h4><p>For the sake of this article I will assume that you created 3 (three) MongoDB One-Click Applications on Digital Ocean&rsquo;s at the very least 1 GB RAM boxes (2 GB RAM machines are a good choice if you&rsquo;re in doubt, but never the 512 Mb) and another bare Ubuntu 14.04 box to be the web server. Create all machines in the same sub-region, all of them with private networking and backup enabled.</p>
<p>Take note of the public IPs and private IPs, for the sake of this article let&rsquo;s say you have this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Public IP     Private IP    Hostname

192.160.10.1  10.0.0.1      mongo1.yourdomain.com
192.160.10.2  10.0.0.2      mongo2.yourdomain.com
192.160.10.3  10.0.0.3      mongo3.yourdomain.com
192.160.10.4  10.0.0.4      yourdomain.com</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In the commands and code snippets below, make sure to replace the fake IPs for your own private IPs.</p>
<p>As I said in <a href="http://www.akitaonrails.com/2016/08/03/migrando-para-o-gitlab-sim-vale-a-pena?"target="_blank" rel="noopener">my GitLab post</a>, create a swap file, configure the locale, set it up for automatic security updates, and do upgrade the Ubuntu packages.</p>
<p>The next first thing: configure the firewall in all machines (assuming <code>ufw</code> package is already installed):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo ufw reset
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow https
sudo ufw allow ssh
sudo ufw allow in on eth1 to any port 27017
sudo ufw enable</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>We are allowing MongoDB&rsquo;s 27017 port only for machines in the same private networking (through eth1, the public IP go through eth0). We&rsquo;re also allowing port 22 for SSH and although I am not listing it here in this article you should also consider installing and configuring <a href="https://www.digitalocean.com/community/tutorials/how-to-protect-ssh-with-fail2ban-on-ubuntu-14-04"target="_blank" rel="noopener">fail2ban</a>. Digital Ocean also has a <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-firewall-with-ufw-on-ubuntu-14-04"target="_blank" rel="noopener">good post on UFW</a> for more details.</p>
<p>Next thing to do on the MongoDB machines is to <a href="https://docs.mongodb.com/manual/tutorial/transparent-huge-pages/"target="_blank" rel="noopener">Disable Transparent Huge Pages (THP)</a>. For that create a file <code>/etc/init.d/disable-transparent-hugepages</code>, with this content:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/sh
</span></span></span><span class="line"><span class="cl"><span class="c1">### BEGIN INIT INFO</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Provides:          disable-transparent-hugepages</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Required-Start:    $local_fs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Required-Stop:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># X-Start-Before:    mongod mongodb-mms-automation-agent</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Default-Start:     2 3 4 5</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Default-Stop:      0 1 6</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Short-Description: Disable Linux transparent huge pages</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Description:       Disable Linux transparent huge pages, to improve</span>
</span></span><span class="line"><span class="cl"><span class="c1">#                    database performance.</span>
</span></span><span class="line"><span class="cl"><span class="c1">### END INIT INFO</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">case</span> <span class="nv">$1</span> in
</span></span><span class="line"><span class="cl">  start<span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">[</span> -d /sys/kernel/mm/transparent_hugepage <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">      <span class="nv">thp_path</span><span class="o">=</span>/sys/kernel/mm/transparent_hugepage
</span></span><span class="line"><span class="cl">    <span class="k">elif</span> <span class="o">[</span> -d /sys/kernel/mm/redhat_transparent_hugepage <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">      <span class="nv">thp_path</span><span class="o">=</span>/sys/kernel/mm/redhat_transparent_hugepage
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="m">0</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s1">&#39;never&#39;</span> &gt; <span class="si">${</span><span class="nv">thp_path</span><span class="si">}</span>/enabled
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s1">&#39;never&#39;</span> &gt; <span class="si">${</span><span class="nv">thp_path</span><span class="si">}</span>/defrag
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nb">unset</span> thp_path
</span></span><span class="line"><span class="cl">    <span class="p">;;</span>
</span></span><span class="line"><span class="cl"><span class="k">esac</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now you can register it as a service:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo chmod 755 /etc/init.d/disable-transparent-hugepages
sudo update-rc.d disable-transparent-hugepages defaults
sudo /etc/init.d/disable-transparent-hugepages start</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Stop MongoDB in all 3 machines:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo service mongod stop</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now comes a complicated procedure that you should not do wrong. It&rsquo;s only complicated because you have to do it in all 4 machines minding their correct private IP addresses. If you already have your own DNS server, you can skip this, otherwise follow through carefully.</p>
<p>You will have to edit both <code>/etc/hostname</code> and <code>/etc/hosts</code>. Each machine will have a particular name and they should be able to find the other machines by name. Let&rsquo;s start by editing the <code>/etc/hostname</code> on each machine. Use the table in the beginning of this section, for example, in the machine with public IP <code>192.160.10.1</code> you must edit it&rsquo;s hostname to be <code>mongo1.yourdomain.com</code>, and so on for all the machines.</p>
<p>Once you have it done, you have to replace the line <code>127.0.0.1 localhost</code> in the <code>/etc/hosts</code> in each of them for the following:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>127.0.0.1 localhost mongo1.yourdomain.com
10.0.0.1 mongo1.yourdomain.com
10.0.0.2 mongo2.yourdomain.com
10.0.0.3 mongo3.yourdomain.com
10.0.0.4 yourdomain.com</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This snippet assumes you&rsquo;re in the machine with private IP <code>10.0.0.1</code>, of course. Make sure you change <code>mongo1.yourdomain.com</code> in the first line for the hostname of the machine you&rsquo;re in. If this is too difficult for you, you definitely should not try to maintain your own infrastructure, so beware.</p>
<p>Finally, let&rsquo;s edit the <code>/etc/mongod.conf</code> with these changes:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>storage:
  dbPath: /mongodb-data
...
net:
  port: 27017
  bindIp: 0.0.0.0
...
replication:
  replSetName: rs0</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This should be exactly the same in all machines. And you must create this <code>/mongodb-data</code> directory like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo mkdir /mongodb-data
sudo chown -R mongodb:mongodb /mongodb-data</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now go ahead on machine <code>mongo1.yourdomain.com</code>, start up with <code>sudo service mongod start</code> and fire up the <code>mongo</code> command, and you should see something like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># mongo
MongoDB shell version: 3.2.8
connecting to: test
&gt;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Let&rsquo;s now configure the replica set by issuing this command in the MongoDB command-line interface:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>rs.initiate()
rs.add(&#39;mongo2.yourdomain.com&#39;)
rs.add(&#39;mongo3.yourdomain.com&#39;)</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And you can now log in to the secondary machines and <code>sudo service mongod start</code> in all of them. They should all start joining the replica set and syncing.</p>
<p>You can check the status of the replication like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>&gt; rs.conf()

{
  &#34;set&#34; : &#34;rs0&#34;,
  &#34;date&#34; : ISODate(&#34;2016-08-09T17:33:10.466Z&#34;),
  &#34;myState&#34; : 1,
  &#34;term&#34; : NumberLong(-1),
  &#34;heartbeatIntervalMillis&#34; : NumberLong(2000),
  &#34;members&#34; : [
    {
      &#34;_id&#34; : 1,
      &#34;name&#34; : &#34;mongo1.yourdomain.com:27017&#34;,
      &#34;health&#34; : 1,
      &#34;state&#34; : 1,
      &#34;stateStr&#34; : &#34;PRIMARY&#34;,
      &#34;uptime&#34; : 11727,
      &#34;optime&#34; : Timestamp(1470763990, 3),
      &#34;optimeDate&#34; : ISODate(&#34;2016-08-09T17:33:10Z&#34;),
      &#34;electionTime&#34; : Timestamp(1470752553, 1),
      &#34;electionDate&#34; : ISODate(&#34;2016-08-09T14:22:33Z&#34;),
      &#34;configVersion&#34; : 6,
      &#34;self&#34; : true
    },
    {
      &#34;_id&#34; : 2,
      &#34;name&#34; : &#34;mongo2.yourdomain.com:27017&#34;,
      &#34;health&#34; : 1,
      &#34;state&#34; : 2,
      &#34;stateStr&#34; : &#34;SECONDARY&#34;,
      &#34;uptime&#34; : 11693,
      &#34;optime&#34; : Timestamp(1470763988, 4),
      &#34;optimeDate&#34; : ISODate(&#34;2016-08-09T17:33:08Z&#34;),
      &#34;lastHeartbeat&#34; : ISODate(&#34;2016-08-09T17:33:10.026Z&#34;),
      &#34;lastHeartbeatRecv&#34; : ISODate(&#34;2016-08-09T17:33:08.887Z&#34;),
      &#34;pingMs&#34; : NumberLong(0),
      &#34;syncingTo&#34; : &#34;mongo3.yourdomain.com:27017&#34;,
      &#34;configVersion&#34; : 6
    },
    {
      &#34;_id&#34; : 3,
      &#34;name&#34; : &#34;mongo3.yourdomain.com:27017&#34;,
      &#34;health&#34; : 1,
      &#34;state&#34; : 2,
      &#34;stateStr&#34; : &#34;SECONDARY&#34;,
      &#34;uptime&#34; : 11693,
      &#34;optime&#34; : Timestamp(1470763988, 4),
      &#34;optimeDate&#34; : ISODate(&#34;2016-08-09T17:33:08Z&#34;),
      &#34;lastHeartbeat&#34; : ISODate(&#34;2016-08-09T17:33:09.994Z&#34;),
      &#34;lastHeartbeatRecv&#34; : ISODate(&#34;2016-08-09T17:33:08.655Z&#34;),
      &#34;pingMs&#34; : NumberLong(0),
      &#34;syncingTo&#34; : &#34;mongo1.yourdomain.com:27017&#34;,
      &#34;configVersion&#34; : 6
    }
  ],
  &#34;ok&#34; : 1
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Each box should be syncing data to another box, one of them will be marked &ldquo;PRIMARY&rdquo; and the others will be &ldquo;SECONDARY&rdquo;. If one of them &ldquo;dies&rdquo; for some reason, one of the others will be elected as PRIMARY until you add a replacement box. Read more details on this procedure in <a href="https://www.digitalocean.com/community/tutorials/how-to-implement-replication-sets-in-mongodb-on-an-ubuntu-vps"target="_blank" rel="noopener">Digital Ocean&rsquo;s post about implementing replication sets</a>.</p>
<h4>Adding your MongoDB environment to MMS<span class="hx:absolute hx:-mt-20" id="adding-your-mongodb-environment-to-mms"></span>
    <a href="#adding-your-mongodb-environment-to-mms" class="subheading-anchor" aria-label="Permalink for this section"></a></h4><p>Another thing beginners will not realise is that it&rsquo;s a good idea to monitor your services. At the very least you should install MongoDB Cloud Manager agents into your servers. Just set up an account at <a href="https://www.mongodb.com/cloud"target="_blank" rel="noopener">MongoDB Atlas</a>.</p>
<p>It&rsquo;s beyond the point of this article to explain Atlas, but you should have no trouble installing the agents with the Group ID and API Key provided. Then install the monitoring agent and have the Manager figure out your existing deployment and replica set.</p>
<p>The basic subscription for monitoring will cost you an extra USD 39 a month, but believe me when I say it&rsquo;s worth to add all your MongoDB deployments under MMS.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/553/big_Deployment___Cloud_Manager__MongoDB_Cloud_Manager.png" alt="MMS"  loading="lazy" /></p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/554/big_Host___Cloud_Manager__MongoDB_Cloud_Manager.png" alt="Replica Set Graphs"  loading="lazy" /></p>
<h4>Installing a multi-instance Node.js service<span class="hx:absolute hx:-mt-20" id="installing-a-multi-instance-nodejs-service"></span>
    <a href="#installing-a-multi-instance-nodejs-service" class="subheading-anchor" aria-label="Permalink for this section"></a></h4><p>Another mistake beginners in the MEAN stack do is to start up a single Node.js instance. Yes, Node.js asynchronous I/O nature makes it &ldquo;concurrent enough&rdquo; in 1 single thread. But you do want to maximize the rented machine so you should spin up at least one Node.js instance for each CPU core. A 1 GB RAM machine has just 1 core, but you can spin up at least 2 instances.</p>
<p>First of all, we must set up the <code>yourdomain.com</code> (<code>192.160.10.4</code> or <code>10.0.0.4</code> in the example) to support Node.js, let&rsquo;s do it:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo apt-get install -y npm curl graphicsmagick nginx git bc
sudo npm install -g n
sudo npm install -g forever
sudo npm install -g forever-service
sudo n 0.10.40</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>If you haven&rsquo;t already, <a href="https://www.digitalocean.com/community/tutorials/how-to-create-a-sudo-user-on-ubuntu-quickstart"target="_blank" rel="noopener">create a more restricted sudo user</a>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>adduser rocket # add a password when asked
usermod -aG sudo rocket
su - rocket # and always ssh in with `ssh rocket@yourdomain.com`</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Within rocket&rsquo;s home directory let&rsquo;s install the Rocket.chat codebase:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>cd /home/rocket
curl -L https://rocket.chat/releases/latest/download -o rocket.chat.tgz
tar zxvf rocket.chat.tgz
mv bundle Rocket.Chat
cd Rocket.Chat/programs/server
npm install</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And now let&rsquo;s add a way for the machine to start the Rocket.chat whenever it reboots:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>cd ~/Rocket.Chat
sudo forever-service install -s main.js -e &#34;ROOT_URL=https://rocketchat42.com/ MONGO_URL=mongodb://mongo1.yourdomain.com:27017/rocketchat MONGO_OPLOG_URL=mongodb://mongo1.yourdomain.com:27017/local PORT=3001&#34; rocketchat1
sudo forever-service install -s main.js -e &#34;ROOT_URL=https://rocketchat42.com/ MONGO_URL=mongodb://mongo1.yourdomain.com:27017/rocketchat MONGO_OPLOG_URL=mongodb://mongo1.yourdomain.com:27017/local PORT=3002&#34; rocketchat2

sudo start rocketchat1
sudo start rocketchat2</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Let&rsquo;s also create SSL certificates (and for that you must have a properly registered domain, of course). Again, <a href="https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-14-04"target="_blank" rel="noopener">Digital Ocean&rsquo;s documentation on Let&rsquo;s Encrypt</a> is very good. In summary, all you have to do is:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now edit the <code>/etc/nginx/sites-available/default</code> and add this inside the server block:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>server {
  ...
  location ~ /.well-known {
          allow all;
  }
  ...
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now you can:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo service nginx reload

sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048

cd /opt/letsencrypt
./letsencrypt-auto certonly -a webroot --webroot-path=/usr/share/nginx/html -d yourdomain.com -d www.yourdomain.com</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Again, replace <code>yourdomain.com</code> with your own registered domain, of course. The <code>letsencrypt</code> command will prompt you to enter your e-mail address and accept terms of service.</p>
<p><a href="https://letsencrypt.org/"target="_blank" rel="noopener">Let&rsquo;s Encrypt</a> is a fantastic free SSL provider. Their certificates are short lived and are meant to expire in 90 days, so you must set up auto-renewal. Start <code>sudo crontab -e</code> and add the following lines:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>30 2 * * 1 /opt/letsencrypt/letsencrypt-auto renew &gt;&gt; /var/log/le-renew.log
35 2 * * 1 /etc/init.d/nginx reload</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This should take care of SSL. Now you can edit your <code>/etc/nginx/sites-available/default</code> and replace it with this template:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># Upstreams
upstream backend {
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
}

server {
  listen 443 ssl;

  server_name yourdomain.com;

  ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  ssl_prefer_server_ciphers on;
  ssl_dhparam /etc/ssl/certs/dhparam.pem;
  ssl_ciphers &#39;ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH&#43;AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA&#39;;
  ssl_session_timeout 1d;
  ssl_session_cache shared:SSL:50m;
  ssl_stapling on;
  ssl_stapling_verify on;
  add_header Strict-Transport-Security max-age=15768000;

  location / {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection &#34;upgrade&#34;;
    proxy_set_header Host $http_host;

    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forward-Proto http;
    proxy_set_header X-Nginx-Proxy true;

    proxy_redirect off;
  }

}

server {
  listen 80;
  server_name yourdomain.com www.yourdomain.com;
  return 301 https://$host$request_uri;
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Just for the sake of completeness, again: replace <code>yourdomain.com</code> for your domain. Usually the <code>/etc/nginx/sites-enabled/default</code> is a symlink to <code>/etc/nginx/sites-available/default</code>, just check that as well. This configuration will load balance between the 2 node.js instances we configured and started before. And you can add as many as you want following the same <code>forever-install</code> procedure and adding the new instances to the <code>upstream</code> section in the nginx configuration above.</p>
<p>If everything is correct, you can restart nginx again: <code>sudo service nginx restart</code>.</p>
<p>And now you should have <code>https://yourdomain.com</code> already up and running. The first user that sign up will be the site administrator and from there you can customize Rocket.chat internally.</p>
<h2>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>As you can see, this is not a simple procedure to follow and I am assuming you have experience managing your own infrastructure. If you don&rsquo;t you definitely should NOT do this by yourself.</p>
<p>If you do it correctly, you should have a functional Slack-clone with minimal reliability (thanks in large part to Digital Ocean) and minimal cost (USD 80/month in the configuration I described that should be enough for more than a 100 users). Rocket.chat also offer more <a href="https://rocket.chat/docs/installation/"target="_blank" rel="noopener">documentation</a> for other environments including Docker configurations you might want to try. But the procedure above is sufficient for my needs.</p>
<p>I said it already but it&rsquo;s better to repeat it: don&rsquo;t fall for the trap of installing everything in a single box, with single instance MongoDB, and without proper monitoring. It&rsquo;s asking for trouble.</p>
<p>For now I am in the middle of the roll out. All developers in my company are already in the new deployment and soon half of the clients should also migrate (some will not be able to leave Slack just yet), but with Franz and the ability of full history and searchability this shoudn&rsquo;t be a concern even if a developer stays offline for some period of time.</p>
<p>I also don&rsquo;t advocate that everybody should be online and responding in real-time. It&rsquo;s unfeasible, unproductive, creates unnecessary tension. People should participate when they have free time, and they should be able to concentrate without worrying that they are missing something. That&rsquo;s why they should opt-out of being notified in the more busy channels and just enable notification on the private groups that matter.</p>
<p>Coincidentally Jason Fried just posted about concerns over group chats at <a href="https://m.signalvnoise.com/is-group-chat-making-you-sweat-744659addf7d#.3qxe09una"target="_blank" rel="noopener">Signal v. Noise</a>, but the gist is that group chat should be purposeful not yet another tool to create tension. People should definitelly get offline when they need to fully concentrate in their work and have the opportunity to catch up with interesting conversations later. And really important communication should go through e-mail or other tradicional ways, a simple <code>@all</code> don&rsquo;t cut it for company-wide announcements for example.</p>
<p>I hope this exercise gives you more perspective on what you can have and also raise awareness on the need for companies to regain more control over their own data, particularly knowledge. Erase your communication channels and you&rsquo;re losing years-worth of knowledge that can be invaluable.</p>
]]></content:encoded><category>reviews</category><category>devops</category><category>bancos-de-dados</category></item><item><title>Migrando para o GitLab! Sim, vale a pena!</title><link>https://akitaonrails.github.io/2016/08/03/migrando-para-o-gitlab-sim-vale-a-pena/</link><guid isPermaLink="true">https://akitaonrails.github.io/2016/08/03/migrando-para-o-gitlab-sim-vale-a-pena/</guid><pubDate>Wed, 03 Aug 2016 14:49:00 -0300</pubDate><description>&lt;p&gt;Eu &lt;a href="https://akitaonrails.github.io/2007/09/22/jogar-pedra-em-gato-morto-por-que-subversion-no-presta/"&gt;comecei a evangelizar Git em 2007&lt;/a&gt;. Foi uma venda dificílima de fazer naquela época.&lt;/p&gt;
&lt;p&gt;Fora do desenvolvimento do kernel quase ninguém queria aprender e tínhamos concorrentes bem dignos, de Subversion, Mercurial, Bazaar, Darcs, Perforce, e por aí vai. Mas quem cavou mais fundo sabia que o Git tinha a vantagem e era só uma questão de tempo.&lt;/p&gt;
&lt;p&gt;Aí o GitHub apareceu em 2008 e o resto é história. Por muitos anos era só &amp;ldquo;bacana&amp;rdquo; estar no GitHub. A comunidade Ruby impulsionou o GitHub lá pra cima. No fim virou o status quo e o único monopólio real em repositórios de informação, não só código-fonte, mas tudo.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Eu <a href="/2007/09/22/jogar-pedra-em-gato-morto-por-que-subversion-no-presta/">comecei a evangelizar Git em 2007</a>. Foi uma venda dificílima de fazer naquela época.</p>
<p>Fora do desenvolvimento do kernel quase ninguém queria aprender e tínhamos concorrentes bem dignos, de Subversion, Mercurial, Bazaar, Darcs, Perforce, e por aí vai. Mas quem cavou mais fundo sabia que o Git tinha a vantagem e era só uma questão de tempo.</p>
<p>Aí o GitHub apareceu em 2008 e o resto é história. Por muitos anos era só &ldquo;bacana&rdquo; estar no GitHub. A comunidade Ruby impulsionou o GitHub lá pra cima. No fim virou o status quo e o único monopólio real em repositórios de informação, não só código-fonte, mas tudo.</p>
<p>Sempre soube que deveríamos ter uma opção &ldquo;local&rdquo;, que é por isso que <a href="https://gitorious.org/gitorious/oboxodo-gitorious?p=gitorious:oboxodo-gitorious.git;a=search;h=9f6bdf5887c65a440bc3fdc43a14652f42ddf103;s=Fabio&#43;Akita;st=committer"target="_blank" rel="noopener">tentei contribuir com o Gitorious</a> lá em 2009. Outras opções apareceram, mas eventualmente o GitLab surgiu por volta de 2011 e pegou tração nos últimos dois anos.</p>
<p>O próprio GitHub levantou <a href="https://www.crunchbase.com/organization/github#/entity"target="_blank" rel="noopener">USD 350 milhões em financiamento</a> e uma das metas obrigatórias é arrasar na Enterprise Edition para grandes corporações que não querem seus dados fora dos jardins fechados. Embora o GitHub hospede cada projeto open source que existe, eles próprios são closed-source.</p>
<p>A <a href="http://gitlab.com"target="_blank" rel="noopener">GitLab Inc.</a> começou diferente, com uma abordagem open source-first com a Community Edition (CE) e tendo tanto uma opção hospedada estilo GitHub quanto uma Enterprise Edition suportada para as corporações temerosas. Já levantaram <a href="https://www.crunchbase.com/organization/gitlab-com#/entity"target="_blank" rel="noopener">USD 5.62 milhões em financiamento</a>, e são a alternativa mais promissora ao GitHub até agora.</p>
<p>Claro, existem outras plataformas como o Bitbucket da Atlassian. Mas acredito que a estratégia da Atlassian é mais lenta e eles têm uma suíte maior de produtos enterprise pra vender primeiro, como o Confluence e o Jira. Acho que eles nunca chegaram a representar muita competição contra o GitHub.</p>
<p>O GitLab realmente começou a acelerar em 2015 como mostra este <a href="https://github.com/gitlabhq/gitlabhq/graphs/contributors?from=2015-03-14&amp;to=2016-08-02&amp;type=c"target="_blank" rel="noopener">gráfico de commits</a>:</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/545/big_Contributors_to_gitlabhq_gitlabhq.png" alt="contributions"  loading="lazy" /></p>
<p>Vem crescendo de forma constante desde 2011, mas parece que cruzaram o primeiro ponto de virada lá por final de 2014, saindo dos early adopters pra early majority. Isso ficou mais importante quando o GitHub <a href="https://github.com/blog/2164-introducing-unlimited-private-repositories"target="_blank" rel="noopener">anunciou as mudanças de preço</a> em maio.</p>
<p>Disseram que não se comprometeram com um prazo pra forçar a mudança, então as organizações podem optar por ficar fora do novo formato por enquanto. Eles estão mudando de &ldquo;repositórios limitados e usuários ilimitados&rdquo; pra &ldquo;repositórios ilimitados e usuários limitados&rdquo;.</p>
<h2>O Dilema do Custo-Benefício<span class="hx:absolute hx:-mt-20" id="o-dilema-do-custo-benefício"></span>
    <a href="#o-dilema-do-custo-benef%c3%adcio" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Por exemplo, se você tem até 8 desenvolvedores no plano de USD 50/mês (20 repositórios privados), a mudança não vai te afetar, já que você vai pagar USD 25/mês por 5 usuários e USD 9 por usuários adicionais (total de USD 52/mês).</p>
<p>Agora, se você tem uma equipe grande de 100 desenvolvedores atualmente no Diamond Plan de USD 450/mês (300 repositórios privados), teria que pagar USD 25/mês + 95 vezes USD 9, o que totaliza impressionantes USD 880/mês! <strong>O dobro do valor!</strong></p>
<p>Isso são <strong>USD 10.560 a mais por ano</strong>!</p>
<p>E o que o GitLab te oferece em troca?</p>
<p>Você pode ter muito mais usuários e mais repositórios numa máquina virtual de <strong>USD 40/mês</strong> (4GB de RAM, 60GB SSD, 4TB de transferência).</p>
<p>E não para por aí. O GitLab também tem o funcionalíssimo <a href="https://gitlab.com/gitlab-org/gitlab-ci-multi-runner"target="_blank" rel="noopener">GitLab Multi Runner</a> que você pode instalar numa máquina separada (na verdade, pelo menos 3 máquinas, mais sobre isso abaixo).</p>
<p>Você pode facilmente conectar esse runner ao sistema de build pelo GitLab de modo que cada novo git push dispara o runner pra rodar a suíte de testes automatizados numa imagem Docker à sua escolha. Então é um sistema de Integração Contínua completo e totalmente funcional integrado na interface do projeto GitLab:</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/546/big_Pipelines___cm42-archived___PremiosOnline___GitLab.png" alt="Pipeline"  loading="lazy" /></p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/547/big_test___144____Builds___cm42-archived___PremiosOnline___GitLab.png" alt="CI Runner"  loading="lazy" /></p>
<p>Lembra de alguma coisa? Sim, é uma alternativa totalmente funcional ao Travis-CI, Semaphore, CircleCI ou qualquer outro CI que você esteja usando com um procedimento de instalação bem fácil. Digamos que você está pagando USD 489/mês ao Travis-CI pra ter 10 jobs concorrentes.</p>
<p>Você pode instalar o GitLab Runner em 3 máquinas de USD 10/mês (1GB RAM, 1 Core, 30GB SSD) e ter muito mais jobs concorrentes (20? 50? Auto-Scale!?) que rodam <strong>mais rápido</strong> (num teste simples, um build que demorou 15 minutos no Travis demorou menos de 8 minutos no Digital Ocean).</p>
<p>Então vamos fazer a conta pra um ano de serviço. Primeiro considerando nenhuma mudança no plano do GitHub:</p>
<blockquote>
  <p>USD 5.400 (GitHub) + USD 5.868 (Travis) = USD 11.268 por ano.</p>

</blockquote>
<p>Agora, GitLab + GitLab Runner + Digital Ocean pelas mesmas features e usuários ilimitados, repositórios ilimitados, builds concorrentes ilimitados:</p>
<blockquote>
  <p>USD 480 (GitLab) + USD 840 (máquina do Runner) = USD 1.320 por ano.</p>

</blockquote>
<p>Isso já é quase 8,5x mais barato com quase nenhuma mudança de qualidade.</p>
<p>No pior cenário, compare quando o GitHub decidir forçar os novos planos:</p>
<blockquote>
  <p>USD 10.560 (novos planos GitHub) + USD 5.868 (Travis) = USD 16.428</p>

</blockquote>
<p>Agora a opção GitLab é 11x mais barata! Você economiza quase USD 15.000 por ano! Isso é algo que você não pode ignorar na sua planilha de custos.</p>
<p>Como eu disse, os cálculos acima só são significativos num cenário de 100 desenvolvedores. Você tem que fazer sua própria conta considerando o tamanho do seu time e número de projetos ativos (você sempre pode arquivar projetos que não estão sendo usados).</p>
<p>Mesmo que você não tenha 100 desenvolvedores. Vamos considerar o cenário pra <strong>30 desenvolvedores</strong> nos novos planos por usuário do GitHub e uma configuração menor de Travis pra 5 jobs concorrentes:</p>
<blockquote>
  <p>USD 3.000 (novo plano GitHub) + USD 3.000 (Travis) = USD 6.000</p>

</blockquote>
<p>É 4,5x mais barato na opção suite Digital Ocean + GitLab.</p>
<p>Olha, vamos considerar o plano <strong>Atual</strong> do GitHub (o Platinum, pra até 125 repositórios):</p>
<blockquote>
  <p>USD 2.400 (plano atual GitHub) + USD 3.000 (Travis) = USD 5.400</p>

</blockquote>
<p>Ainda é pelo menos <strong>4x mais caro</strong> que uma solução baseada em GitLab!</p>
<p>E quanto tempo leva pra um único desenvolvedor descobrir o setup e migrar tudo do GitHub pra nova instalação do GitLab? Vou dizer que você pode reservar 1 semana de trabalho pra um programador mediano fazer isso seguindo a documentação oficial e minhas dicas e truques abaixo.</p>
<h2>Instalando o GitLab CE<span class="hx:absolute hx:-mt-20" id="instalando-o-gitlab-ce"></span>
    <a href="#instalando-o-gitlab-ce" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Não vou te encher com o que você encontra facilmente pela Web. Recomendo fortemente que você comece com a solução mais fácil primeiro: <a href="https://www.digitalocean.com/features/one-click-apps/gitlab/"target="_blank" rel="noopener">One-Click Automatic Install do Digital Ocean</a>. Instale numa máquina com pelo menos 4GB de RAM (você vai querer mantê-la se gostar).</p>
<p>Claro, existem várias opções diferentes de instalação, de imagens AMI da AWS a pacotes Ubuntu que você pode instalar manualmente. Estude a <a href="https://about.gitlab.com/installation/"target="_blank" rel="noopener">documentação</a>.</p>
<p>Vai te custar USD 40 por um mês de teste. Se você quiser economizar dezenas de milhares de dólares, isso é uma pechincha.</p>
<p>O GitLab tem muitas opções de customização. Você pode trancar seu GitLab privado pra permitir só usuários com e-mail oficial do seu domínio, por exemplo. Você pode configurar <a href="http://docs.gitlab.com/ee/integration/omniauth.html"target="_blank" rel="noopener">provedores OAuth2</a> pros seus usuários entrarem rapidamente usando contas do GitHub, Facebook, Google ou outras.</p>
<h4>Alguns Detalhes<span class="hx:absolute hx:-mt-20" id="alguns-detalhes"></span>
    <a href="#alguns-detalhes" class="subheading-anchor" aria-label="Permalink for this section"></a></h4><p>Tropecei em alguns caveats na configuração. É por isso que recomendo planejar com antecedência, estude este artigo inteiro antes! Faça uma instalação rápida que você pode jogar fora, pra você &ldquo;sentir&rdquo; o ambiente antes de tentar migrar todos os seus repositórios pro seu GitLab novinho. Como referência, esta é uma parte do meu <code>/etc/gitlab/gitlab.rb</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># registre um domínio pro seu servidor e coloque aqui:</span>
</span></span><span class="line"><span class="cl"><span class="n">external_url</span> <span class="s2">&#34;http://my-gitlab-server.com/&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># você vai querer habilitar [LFS](https://git-lfs.github.com)</span>
</span></span><span class="line"><span class="cl"><span class="n">gitlab_rails</span><span class="o">[</span><span class="s1">&#39;lfs_enabled&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="kp">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># registre seus emails</span>
</span></span><span class="line"><span class="cl"><span class="n">gitlab_rails</span><span class="o">[</span><span class="s1">&#39;gitlab_email_from&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="s2">&#34;no-reply@my-gitlab-server.com&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">gitlab_rails</span><span class="o">[</span><span class="s1">&#39;gitlab_support_email&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="s2">&#34;contact@my-gitlab-server.com&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># adicione sua configuração de email (template pra gmail)</span>
</span></span><span class="line"><span class="cl"><span class="n">gitlab_rails</span><span class="o">[</span><span class="s1">&#39;smtp_enable&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="kp">true</span>
</span></span><span class="line"><span class="cl"><span class="n">gitlab_rails</span><span class="o">[</span><span class="s1">&#39;smtp_address&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="s2">&#34;smtp.gmail.com&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">gitlab_rails</span><span class="o">[</span><span class="s1">&#39;smtp_port&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="mi">587</span>
</span></span><span class="line"><span class="cl"><span class="n">gitlab_rails</span><span class="o">[</span><span class="s1">&#39;smtp_user_name&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="s2">&#34;-- algum email no-reply ---&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">gitlab_rails</span><span class="o">[</span><span class="s1">&#39;smtp_password&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="s2">&#34;-- a senha ---&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">gitlab_rails</span><span class="o">[</span><span class="s1">&#39;smtp_domain&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="s2">&#34;my-gitlab-server.com&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">gitlab_rails</span><span class="o">[</span><span class="s1">&#39;smtp_authentication&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="s2">&#34;login&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">gitlab_rails</span><span class="o">[</span><span class="s1">&#39;smtp_enable_starttls_auto&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="kp">true</span>
</span></span><span class="line"><span class="cl"><span class="n">gitlab_rails</span><span class="o">[</span><span class="s1">&#39;smtp_openssl_verify_mode&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="s1">&#39;peer&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># aqui é onde você habilita a integração oauth2</span>
</span></span><span class="line"><span class="cl"><span class="n">gitlab_rails</span><span class="o">[</span><span class="s1">&#39;omniauth_enabled&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="kp">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># CUIDADO!</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Isso permite usuários logarem sem ter uma conta de usuário primeiro. Defina os providers permitidos</span>
</span></span><span class="line"><span class="cl"><span class="c1"># usando um array, ex. [&#34;saml&#34;, &#34;twitter&#34;], ou como true/false pra permitir todos os providers ou nenhum.</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Contas de usuário serão criadas automaticamente quando a autenticação for bem-sucedida.</span>
</span></span><span class="line"><span class="cl"><span class="n">gitlab_rails</span><span class="o">[</span><span class="s1">&#39;omniauth_allow_single_sign_on&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="o">[</span><span class="s1">&#39;github&#39;</span><span class="p">,</span> <span class="s1">&#39;google_oauth2&#39;</span><span class="p">,</span> <span class="s1">&#39;bitbucket&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="n">gitlab_rails</span><span class="o">[</span><span class="s1">&#39;omniauth_block_auto_created_users&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="kp">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">gitlab_rails</span><span class="o">[</span><span class="s1">&#39;omniauth_providers&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="o">[</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;name&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;github&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;app_id&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;-- github app id --&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;app_secret&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;-- github secret --&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;url&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;https://github.com/&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;args&#34;</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="s2">&#34;scope&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;user:email&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;name&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;google_oauth2&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;app_id&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;-- google app id --&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;app_secret&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;-- google secret --&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;args&#34;</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="s2">&#34;access_type&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;offline&#34;</span><span class="p">,</span> <span class="s2">&#34;approval_prompt&#34;</span> <span class="o">=&gt;</span> <span class="s1">&#39;&#39;</span><span class="p">,</span> <span class="n">hd</span> <span class="o">=&gt;</span> <span class="s1">&#39;codeminer42.com&#39;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;name&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;bitbucket&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;app_id&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;-- bitbucket app id --&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;app_secret&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;-- bitbucket secret id --&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;url&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;https://bitbucket.org/&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="o">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># se você está importando repos do GitHub, os workers do Sidekiq podem crescer até 2.5GB de RAM e a config padrão do [Sidekiq Killer](http://docs.gitlab.com/ee/operations/sidekiq_memory_killer.html) vai limitar em 1GB, então você vai querer ou desabilitar adicionando &#39;0&#39; ou adicionar um limite maior</span>
</span></span><span class="line"><span class="cl"><span class="n">gitlab_rails</span><span class="o">[</span><span class="s1">&#39;env&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="p">{</span> <span class="s1">&#39;SIDEKIQ_MEMORY_KILLER_MAX_RSS&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;3000000&#39;</span> <span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Existem <a href="https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/attributes/default.rb#L57"target="_blank" rel="noopener">dezenas de variáveis padrão</a> que você pode <a href="https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/environment-variables.md"target="_blank" rel="noopener">sobrescrever</a>, só tome cuidado nos seus testes.</p>
<p>Toda vez que você muda uma configuração, basta rodar os seguintes comandos:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo gitlab-ctl reconfigure
sudo gitlab-ctl restart</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Você pode abrir um console Rails pra inspecionar objetos de produção assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gitlab-rails console</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Tive muita dor de cabeça importando repos grandes do GitHub, mas depois de alguns dias debugando o problema com os desenvolvedores do GitLab Core Team <a href="https://gitlab.com/u/dbalexandre"target="_blank" rel="noopener">Douglas Alexandre</a>, <a href="https://gitlab.com/u/brodock"target="_blank" rel="noopener">Gabriel Mazetto</a>, alguns Merge Requests e algum patching local finalmente consegui importar projetos relativamente grandes (mais de 5.000 commits, mais de 1.000 issues, mais de 1.200 pull requests com várias discussões de comentários). Um projeto desse tamanho pode levar algumas horas pra completar, principalmente porque <strong>é lento demais usar as APIs públicas do GitHub</strong> (são lentas e têm rate limits e detecção de abuso, então você não consegue baixar tudo tão rápido quanto sua banda permitiria).</p>
<p>(A propósito, não perca o GitLab na <a href="http://www.rubyconf.com.br/pt-BR/speakers#Gabriel%20Gon%c3%a7alves%20Nunes%20Mazetto"target="_blank" rel="noopener">Rubyconf Brasil 2016</a>, nos dias 23 e 24 de setembro)</p>
<p>Migrar todos os meus projetos do GitHub levou alguns dias, mas todos passaram sem problemas e meu time não teve nenhuma dificuldade, só ajustar os git remote URLs e pronto.</p>
<p>O procedimento de importação do GitHub é bem completo, traz não só o repositório git em si, mas também todos os metadados, de labels a comentários e histórico de pull request, que é justamente o que normalmente leva mais tempo.</p>
<p>Mas eu recomendaria esperar pelo menos a versão 8.11 (atualmente está na 8.10.3) antes de tentar importar projetos grandes do GitHub.</p>
<p>Se você está no Bitbucket, infelizmente tem menos features no importador. Ele vai trazer principalmente só o código-fonte. Então fique atento se você depende bastante do sistema de pull request deles e quer preservar esse histórico. Mais features virão e você até pode ajudá-los, eles são bem desenvoltos e dispostos a melhorar o GitLab.</p>
<h3>Desvio: Customizações pra cada máquina Digital Ocean<span class="hx:absolute hx:-mt-20" id="desvio-customizações-pra-cada-máquina-digital-ocean"></span>
    <a href="#desvio-customiza%c3%a7%c3%b5es-pra-cada-m%c3%a1quina-digital-ocean" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Assuma que você deve rodar o que está nesta seção pra todas as novas máquinas que você criar no Digital Ocean.</p>
<p>Primeiro, elas vêm sem arquivo de swap. Não importa quanta RAM você tem, o Linux foi feito pra funcionar melhor combinado com um arquivo de swap. Você pode <a href="https://www.digitalocean.com/community/tutorials/how-to-add-swap-on-ubuntu-14-04"target="_blank" rel="noopener">ler mais sobre isso</a> depois, por enquanto só rode o seguinte como root:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>fallocate -l 4G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile

sysctl vm.swappiness=10
sysctl vm.vfs_cache_pressure=50</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Edite o arquivo <code>/etc/fstab</code> e adicione esta linha:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>/swapfile   none    swap    sw    0   0</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Finalmente, edite o arquivo <code>/etc/sysctl.conf</code> e adicione estas linhas:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>vm.swappiness=10
vm.vfs_cache_pressure = 50</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Não esqueça de configurar o <a href="http://askubuntu.com/questions/162391/how-do-i-fix-my-locale-issue"target="_blank" rel="noopener">locale padrão</a> da sua máquina. Comece editando o arquivo <code>/etc/environment</code> e adicionando:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>LC_ALL=en_US.UTF-8
LANG=en_US.UTF-8</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Então rode:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo locale-gen en_US en_US.UTF-8
sudo dpkg-reconfigure locales</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Finalmente, você deveria ter o Ubuntu instalando patches de segurança estáveis automaticamente pra você. Você não quer esquecer máquinas online sem as correções de segurança mais atuais, então só rode isto:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo dpkg-reconfigure --priority=low unattended-upgrades</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Escolha &ldquo;yes&rdquo; e pronto. E claro, pra toda instalação nova, é sempre bom rodar o velho e bom:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo apt-get update &amp;&amp; sudo apt-get upgrade</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso é o básico do básico, acho que é mais fácil ter uma imagem com tudo isso pronto, mas se você usa as imagens padrão do Digital Ocean, estas configurações devem dar conta por enquanto.</p>
<h2>Instalando o CI Runner<span class="hx:absolute hx:-mt-20" id="instalando-o-ci-runner"></span>
    <a href="#instalando-o-ci-runner" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Assim que você terminar sua instalação do GitLab, é <a href="https://about.gitlab.com/2016/04/19/how-to-set-up-gitlab-runner-on-digitalocean/"target="_blank" rel="noopener">super fácil</a> subir o GitLab Runner. Você pode usar a mesma máquina, mas recomendo instalar numa máquina separada.</p>
<p>Se você não sabe o que é um runner, imagina assim: basicamente é um servidor conectado à instalação do GitLab. Quando está disponível e online, sempre que alguém dá push num novo commit, merge request, num repositório que tem um arquivo <code>gitlab-ci-yml</code> presente, o GitLab vai empurrar um comando pro runner.</p>
<p>Dependendo de como você configurou o runner, ele vai receber esse comando e criar um novo container Docker. Dentro do container ele vai executar o que você definiu no arquivo <code>gitlab-ci.yml</code> do projeto. Normalmente é buscar arquivos em cache (dependências, por exemplo) e rodar sua suíte de testes.</p>
<p>No setup mais básico, você só vai ter um Runner e quaisquer builds subsequentes de outros usuários vão esperar na fila até terminarem. Se você já usou serviços externos de CI como Travis-CI ou CircleCI, sabe que eles cobram por um certo número de builds concorrentes. E é <strong>muito caro</strong>.</p>
<p>Quanto menos builds concorrentes disponíveis, mais seus usuários vão ter que esperar por feedback nas mudanças deles, e menos produtivos vocês ficam. As pessoas podem até começar a evitar adicionar novos testes, ou ignorar completamente os testes, o que vai machucar muito a qualidade do seu projeto com o tempo. Se tem uma coisa que você <strong>não pode</strong> deixar de fazer é ter boas suítes de testes automatizados.</p>
<p>O Gabriel Mazetto me apontou pra uma feature muito importante do GitLab CI Runner: <a href="https://about.gitlab.com/2016/03/29/gitlab-runner-1-1-released/"target="_blank" rel="noopener"><strong>auto-scaling</strong></a>. É isso que eles usam na oferta hospedada deles lá no GitLab.com.</p>
<p>Você pode <strong>facilmente</strong> configurar um runner que pode usar &ldquo;docker-machine&rdquo; e as APIs do seu provedor IaaS pra subir máquinas na hora pra rodar quantos builds concorrentes você quiser, e vai ser super barato!</p>
<p>Por exemplo, no Digital Ocean você pode ser cobrado USD 0,06 (6 centavos) por hora de uso numa máquina de 4GB. Na AWS EC2 você pode ser cobrado USD 0,041 por hora numa máquina m3.medium.</p>
<p>Tem documentação extensiva mas vou tentar resumir o que você tem que fazer. Pra mais detalhes recomendo fortemente que você estude a <a href="https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/install/autoscaling.md#prepare-the-docker-registry-and-cache-server"target="_blank" rel="noopener">documentação oficial</a>.</p>
<p>Comece criando 3 novas máquinas no Digital Ocean, todas na mesma Região com rede privada habilitada! Vou listar um endereço IP privado fake só pra avançar nos exemplos de configuração:</p>
<ul>
<li>uma máquina de 1GB chamada &ldquo;docker-registry-mirror&rdquo;, (ex 10.0.0.1)</li>
<li>uma máquina de 1GB chamada &ldquo;ci-cache&rdquo;, (ex 10.0.0.2)</li>
<li>uma máquina de 1GB chamada &ldquo;ci-runner&rdquo;, (ex 10.0.0.3)</li>
</ul>
<p>Sim, podem ser pequenas já que muito pouco vai rodar nelas. Você pode ser conservador e escolher as de 2GB RAM só pra ficar no seguro (e o preço ainda vai ser super barato).</p>
<p>Não esqueça de executar a configuração básica que mencionei acima pra habilitar swapfile, auto security update e regeneração de locale.</p>
<p>Faça SSH na &ldquo;docker-registry-mirror&rdquo; e rode:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>docker run -d -p 6000:5000 \
    -e REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io \
    --restart always \
    --name registry registry:2</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora você vai ter um registry proxy e cache local de imagens Docker em <code>10.0.0.1:6000</code> (anote o IP privado real).</p>
<p>Faça SSH na &ldquo;ci-cache&rdquo; e rode:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>mkdir -p /export/runner

docker run -it --restart always -p 9005:9000 \
        -v /.minio:/root/.minio -v /export:/export \
        --name minio \
        minio/minio:latest /export</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora você vai ter um clone do AWS S3 chamado <a href="https://github.com/minio/minio"target="_blank" rel="noopener">Minio</a> rodando. Eu nem sabia que esse projeto existia, mas é um serviçozinho legal escrito em Go pra clonar o comportamento e APIs do AWS S3. Então agora você pode ter seu próprio S3 dentro da sua infraestrutura!</p>
<p>Depois que o Docker sobe, ele vai imprimir a Access Key e Secret key, anote. E este serviço vai estar rodando em <code>10.0.0.2:9005</code>.</p>
<p>Você até pode abrir um browser e ver a interface web em <code>http://10.0.0.2:9005</code> e usar as access e secret keys pra logar. Certifique-se de ter um bucket chamado &ldquo;runner&rdquo;. Os arquivos serão armazenados no diretório <code>/export/runner</code>.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/549/big_Minio_Browser.png" alt="Minio Dashboard"  loading="lazy" /></p>
<p>Certifique-se de que o <a href="http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html"target="_blank" rel="noopener">nome do bucket é válido</a> (tem que ser um nome DNS válido, por exemplo, NÃO use underlines).</p>
<p>Abra esta URL do seu GitLab-CE recém instalado: <code>http://yourgitlab.com/admin/runners</code> e anote o Registration Token. Digamos que seja <code>1aaaa_Z1AbB2CdefGhij</code></p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/550/big_Admin_Area___GitLab.png" alt="Admin Area for Runner Registration Token"  loading="lazy" /></p>
<p>Finalmente, faça SSH na &ldquo;ci-runner&rdquo; e rode:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>curl -L https://github.com/docker/machine/releases/download/v0.7.0/docker-machine-`uname -s`-`uname -m` &gt; /usr/local/bin/docker-machine

chmod &#43;x /usr/local/bin/docker-machine

curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.deb.sh | sudo bash

sudo apt-get install gitlab-ci-multi-runner

rm -Rf ~/.docker # só pra garantir</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora você pode registrar este novo runner com sua instalação do GitLab, você vai precisar do Registration Token mencionado acima.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo gitlab-ci-multi-runner register</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Você vai ser perguntado algumas coisas, e isto é o que você pode responder:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com/ci )
https://yourgitlab.com/ci
Please enter the gitlab-ci token for this runner
1aaaa_Z1AbB2CdefGhij # como no exemplo acima
Please enter the gitlab-ci description for this runner
my-autoscale-runner
INFO[0034] fcf5c619 Registering runner... succeeded
Please enter the executor: shell, docker, docker-ssh, docker&#43;machine, docker-ssh&#43;machine, ssh?
docker&#43;machine
Please enter the Docker image (eg. ruby:2.1):
codeminer42/ci-ruby:2.3
INFO[0037] Runner registered successfully. Feel free to start it, but if it&#39;s
running already the config should be automatically reloaded!</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Vamos fazer uma cópia da configuração original, só pra garantir:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>cp /etc/gitlab-runner/config.toml /etc/gitlab-runner/config.bak</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Copie as primeiras linhas deste arquivo (você quer o token), vai parecer com isto:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>concurrent = 1
check_interval = 0

[[runners]]
  name = &#34;my-autoscale-runner&#34;
  url = &#34;http://yourgitlab.com/ci&#34;
  token = &#34;--- generated runner token ---&#34;
  executor = &#34;docker&#43;machine&#34;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A parte importante aqui é o &ldquo;token&rdquo;. Você vai querer anotar. E agora você também vai querer criar um <a href="https://cloud.digitalocean.com/settings/api/tokens"target="_blank" rel="noopener">novo API Token no Digital Ocean</a>. Só Gere um Novo Token e anote.</p>
<p>Agora você pode substituir o arquivo <code>config.toml</code> inteiro por isto:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>concurrent = 20
check_interval = 0

[[runners]]
  name = &#34;my-autoscale-runner&#34;
  url = &#34;http://yourgitlab.com/ci&#34;
  token = &#34;--- generated runner token ---&#34;
  executor = &#34;docker&#43;machine&#34;
  limit = 15
  [runners.docker]
    tls_verify = false
    image = &#34;codeminer42/ci-ruby:2.3&#34;
    privileged = false
  [runners.machine]
    IdleCount = 2                   # Tem que ter 2 máquinas em estado Idle
    IdleTime = 1800                 # Cada máquina pode ficar em estado Idle até 30 minutos (depois disso será removida)
    MaxBuilds = 100                 # Cada máquina pode lidar com até 100 builds em sequência (depois disso será removida)
    MachineName = &#34;ci-auto-scale-%s&#34;   # Cada máquina vai ter um nome único (&#39;%s&#39; é obrigatório)
    MachineDriver = &#34;digitalocean&#34;  # Docker Machine está usando o driver &#39;digitalocean&#39;
    MachineOptions = [
        &#34;digitalocean-image=coreos-beta&#34;,
        &#34;digitalocean-ssh-user=core&#34;,
        &#34;digitalocean-access-token=-- seu novo Digital Ocean API Token --&#34;,
        &#34;digitalocean-region=nyc1&#34;,
        &#34;digitalocean-size=4gb&#34;,
        &#34;digitalocean-private-networking&#34;,
        &#34;engine-registry-mirror=http://10.0.0.1:6000&#34;
    ]
  [runners.cache]
    Type = &#34;s3&#34;   # O Runner está usando um cache distribuído com o serviço Amazon S3
    ServerAddress = &#34;10.0.0.2:9005&#34;  # minio
    AccessKey = &#34;-- sua minio access key --&#34;
    SecretKey = &#34;-- sua minio secret key&#34;
    BucketName = &#34;runner&#34;
    Insecure = true # Use Insecure só quando usar com Minio, sem o certificado TLS habilitado</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E você pode reiniciar o runner pra pegar a nova configuração assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gitlab-ci-multi-runner restart</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Como eu disse antes, você vai querer ler a <a href="https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/autoscale.md"target="_blank" rel="noopener">documentação oficial</a> extensiva (e todos os links dentro).</p>
<p>Se você fez tudo certo, trocando os IPs privados corretos pro docker registry e cache, os tokens corretos, e assim por diante, pode logar no seu dashboard do Digital Ocean e vai ver algo assim:</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/548/big_DigitalOcean_-_Droplets.png" alt="Digital Ocean CI Setup"  loading="lazy" /></p>
<p>E da máquina <code>ci-runner</code>, você pode listá-las assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># docker-machine ls

NAME                                                ACTIVE   DRIVER         STATE     URL                         SWARM   DOCKER    ERRORS
runner-xxxx-ci-auto-scale-xxxx-xxxx   -        digitalocean   Running   tcp://191.168.0.1:2376            v1.10.3
runner-xxxx-ci-auto-scale-xxxx-xxxx   -        digitalocean   Running   tcp://192.169.0.2:2376           v1.10.3</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Elas não devem listar nenhum erro, significando que estão de pé e rodando, esperando novos builds começarem.</p>
<p>Vai ter 2 novas máquinas listadas no seu dashboard do Digital Ocean, chamadas &ldquo;runner-xxxxx-ci-auto-scale-xxxxx&rdquo;. Isso é o que o <code>IdleCount = 2</code> faz. Se ficarem paradas por mais de 30 minutos (<code>IdleTime = 1800</code>) serão desligadas pra você não ser cobrado.</p>
<p>Você pode ter várias definições de &ldquo;runner&rdquo;, cada uma com um <code>limit</code> de builds/máquinas que podem ser criadas no Digital Ocean. Você pode ter outras definições de runner pra outros provedores, por exemplo. Mas neste exemplo estamos limitados a no máximo 15 máquinas, então 15 builds concorrentes.</p>
<p>O limite <code>concurrent</code> é uma configuração global. Então se eu tivesse 3 definições de runner, cada uma com um <code>limit</code> de 15, elas ainda seriam globalmente limitadas a 20 como definido na variável global <code>concurrent</code>.</p>
<p>Você pode usar provedores diferentes pra necessidades específicas, por exemplo, pra rodar builds OS X ou builds Raspberry PI ou outros tipos exóticos de build. No exemplo estou mantendo simples e configurando vários builds no mesmo provedor (Digital Ocean).</p>
<p>E não se preocupe com a mensalidade de cada máquina. Quando usada dessa forma, você vai pagar por hora.</p>
<p>Além disso, certifique-se de ter subido todas as suas máquinas (docker-registry, minio cache, CI runner) todas com <strong>rede privada habilitada</strong> (pra que conversem pela VLAN interna em vez de ter que atravessar toda a internet pública) e que estão todas na mesma região/data center (NYC1 é New York 1, New York tem 3 sub-regiões, por exemplo). Não comece máquinas em regiões diferentes.</p>
<p>Porque temos proxy/cache Docker e cache Minio/S3, seus builds vão demorar mais na primeira vez (digamos, 5 minutos), e aí builds subsequentes vão buscar tudo do cache (levando, digamos, 1:30 minuto). É rápido e é conveniente.</p>
<h3>Configurando cada Projeto pro Runner<span class="hx:absolute hx:-mt-20" id="configurando-cada-projeto-pro-runner"></span>
    <a href="#configurando-cada-projeto-pro-runner" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O Runner é uma das peças mais novas do ecossistema GitLab então você pode ter algum trabalho no começo pra descobrir uma configuração decente. Mas uma vez que você tem a infraestrutura toda figurada como descrito na seção anterior, agora é tão fácil quanto adicionar um arquivo <code>.gitlab-ci.yml</code> no seu diretório raiz. Algo assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># Este arquivo é um template, e pode precisar de edição antes de funcionar no seu projeto.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">codeminer42/ci-ruby:2.3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Escolha zero ou mais serviços pra serem usados em todos os builds.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Só necessário quando usar um container docker pra rodar seus testes.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Confira: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">postgres:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">redis:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">cache</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">your-project-name</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">untracked</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">.ci_cache/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">variables</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">RAILS_ENV</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;test&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">DATABASE_URL</span><span class="p">:</span><span class="w"> </span><span class="l">postgresql://postgres:@postgres</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">CODECLIMATE_REPO_TOKEN</span><span class="p">:</span><span class="w"> </span>-- <span class="l">seu token de projeto codeclimate --</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">before_script</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">bundle install --without development production -j $(nproc) --path .ci_cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">cp .env.sample .env</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">cp config/database.yml.example config/database.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">bundle exec rake db:create db:migrate</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">test</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">script</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">xvfb-run bundle exec rspec</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Meu time na <a href="http://www.codeminer42.com"target="_blank" rel="noopener">Codeminer 42</a> preparou uma <a href="https://hub.docker.com/r/codeminer42/ci-ruby/"target="_blank" rel="noopener">imagem Docker simples</a> com coisas úteis pré-instaladas (como o phantomjs mais novo, xvfb, etc), então agora é super fácil habilitar builds automatizados dentro do GitLab só adicionando este arquivo aos repositórios. (Obrigado ao Carlos Lopes, Danilo Resende e Paulo Diovanni, que vai estar falando <a href="http://www.rubyconf.com.br/pt-BR/speakers#Paulo%20Diovani%20Gon%c3%a7alves"target="_blank" rel="noopener">sobre Docker na Rubyconf Brasil 2016</a>, a propósito)</p>
<p>O GitLab-CI até suporta buildar um Merge Request pendente, e você pode forçar o request pra que ele só possa ser merjado se os builds passarem, assim como no GitHub + Travis. E como o Code Climate é agnóstico a host de repositório ou CI runner, você pode integrá-lo facilmente também.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/551/big_Settings___Codeminer42___CM-Fulcrum___GitLab.png" alt="Project Force Successful Build to Merge"  loading="lazy" /></p>
<h2>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A matemática é difícil de contestar: o combo GitLab + GitLab-CI + Digital Ocean é uma grande vitória. A interface do GitLab é bem familiar, então usuários vindos do GitHub ou Bitbucket vão se sentir em casa rapidinho.</p>
<p>Podemos usar todos os <a href="https://about.gitlab.com/2014/09/29/gitlab-flow/"target="_blank" rel="noopener">Git flows</a> que estamos acostumados.</p>
<p>O GitLab-CE ainda é um trabalho em andamento, o time está aumentando o ritmo mas atualmente tem mais de <a href="https://gitlab.com/gitlab-org/gitlab-ce/issues"target="_blank" rel="noopener">4.200 issues abertas</a>. Mas como tudo isto é Ruby on Rails e tooling Ruby, você pode facilmente entrar e contribuir. Nenhuma contribuição é pequena demais. Só reportar como reproduzir um bug já é ajuda suficiente pra assistir os desenvolvedores a descobrirem como melhorar mais rápido.</p>
<p>Mas não se assuste com as issues abertas, está totalmente funcional agora e eu não achei nenhum bug que poderia ser considerado show stopper.</p>
<p>Eles têm muita coisa certa. Primeiro de tudo, é um projeto Ruby on Rails &ldquo;simples&rdquo;. É um front-end sem firulas com jQuery puro. A escolha de HAML pras views é questionável mas não atrapalha. Eles usam o bom e velho Sidekiq+Redis pra jobs assíncronos. Sem mágica negra aqui. Um monólito puro que não é difícil de entender e contribuir.</p>
<p>As APIs são todas escritas usando Grape. Eles têm o projeto <a href="https://gitlab.com/gitlab-org/gitlab-ce"target="_blank" rel="noopener">GitLab CE</a> separado de outros componentes, como o <a href="https://gitlab.com/gitlab-org/gitlab-shell"target="_blank" rel="noopener">GitLab-Shell</a> e o <a href="https://gitlab.com/gitlab-org/gitlab-ci-multi-runner"target="_blank" rel="noopener">GitLab-CI-Multi-Runner</a>.</p>
<p>Eles também forkaram o <a href="https://gitlab.com/gitlab-org/omnibus-gitlab"target="_blank" rel="noopener">Omnibus</a> pra conseguir empacotar o projeto Rails CE como um &ldquo;.deb&rdquo;. Tudo é orquestrado com Docker. E quando uma nova versão está disponível, você só precisa rodar <code>apt-get update &amp;&amp; apt-get upgrade</code> e ele vai fazer todo o trabalho de backup e migração do Postgresql, atualizar o código, incluir novas dependências, reiniciar os serviços e por aí vai. É super conveniente e você deveria dar uma olhada neste projeto se você tem deploys Rails complicados na sua própria infraestrutura (fora do Heroku, por exemplo).</p>
<p>Estou quase terminando de mover centenas de repositórios tanto do BitBucket quanto do GitHub pro GitLab agora e os desenvolvedores da minha empresa já estão usando no dia a dia sem nenhum problema. Estamos quase no ponto onde podemos nos desengajar do BitBucket, GitHub e CIs externos.</p>
<p>Você vai se surpreender com o quão fácil sua empresa também pode fazer isso e economizar alguns milhares de dólares no processo, enquanto se diverte fazendo!</p>
]]></content:encoded><category>controle-de-versao</category><category>devops</category></item><item><title>Sua aplicação Rails está pronta para Produção?</title><link>https://akitaonrails.github.io/2016/03/22/sua-aplicacao-rails-esta-pronta-para-producao/</link><guid isPermaLink="true">https://akitaonrails.github.io/2016/03/22/sua-aplicacao-rails-esta-pronta-para-producao/</guid><pubDate>Tue, 22 Mar 2016 15:11:00 -0300</pubDate><description>&lt;p&gt;Isto aqui é um checklist. Siga estas instruções se você quiser ter um pouco de paz de espírito depois de colocar uma aplicação em produção. Se você tem experiência, contribua nos comentários abaixo. Mas se você é iniciante, preste muita atenção.&lt;/p&gt;
&lt;h3&gt;Onde fazer o Deploy? TL;DR: Heroku&lt;span class="hx:absolute hx:-mt-20" id="onde-fazer-o-deploy-tldr-heroku"&gt;&lt;/span&gt;
&lt;a href="#onde-fazer-o-deploy-tldr-heroku" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Muita gente vai mirar primeiro nas opções baratas. Normalmente instâncias EC2 micro ou pequenos droplets do Digital Ocean. A menos que você tenha consciência de que &lt;strong&gt;precisa&lt;/strong&gt; monitorar e atualizar essas instâncias o tempo todo, &lt;strong&gt;NÃO&lt;/strong&gt; faça isso.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Isto aqui é um checklist. Siga estas instruções se você quiser ter um pouco de paz de espírito depois de colocar uma aplicação em produção. Se você tem experiência, contribua nos comentários abaixo. Mas se você é iniciante, preste muita atenção.</p>
<h3>Onde fazer o Deploy? TL;DR: Heroku<span class="hx:absolute hx:-mt-20" id="onde-fazer-o-deploy-tldr-heroku"></span>
    <a href="#onde-fazer-o-deploy-tldr-heroku" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Muita gente vai mirar primeiro nas opções baratas. Normalmente instâncias EC2 micro ou pequenos droplets do Digital Ocean. A menos que você tenha consciência de que <strong>precisa</strong> monitorar e atualizar essas instâncias o tempo todo, <strong>NÃO</strong> faça isso.</p>
<p>Em vez disso, escolha uma solução PaaS como o Heroku. É óbvio. Software em produção é coisa séria e contínua. Só pelo lado da segurança, você não deveria fazer isso sozinho a menos que saiba como hardenizar uma distribuição de SO. Por exemplo, a maioria das pessoas nem sabe o que é o <a href="http://www.fail2ban.org/wiki/index.php/Main_Page"target="_blank" rel="noopener">fail2ban</a>. Não assuma que, só porque você usa SSH com par de chaves, está seguro. Você não está.</p>
<p>Por exemplo, você sabia que a gente acabou de passar por uma <a href="https://blog.heroku.com/archives/2016/3/21/patching-glibc-security-hole"target="_blank" rel="noopener">crise de segurança da glibc</a>? Se você não sabia e há tempos não toca nos seus servidores de produção para atualizar todos os componentes, você está aberto, completamente aberto. Tive vários clientes que contrataram freelancers para implementar e fazer deploy das aplicações e depois ficaram sem ninguém olhando aquilo. Todos vocês estão inseguros.</p>
<p>O Heroku também não é garantia, porque as bibliotecas das aplicações ficam velhas, vulnerabilidades são descobertas, e você precisa atualizá-las. O Rails tem um <a href="https://groups.google.com/forum/#!forum/rubyonrails-security"target="_blank" rel="noopener">time de segurança ativo</a> lançando patches o tempo inteiro. Se você não anda atualizando, está inseguro.</p>
<p>Mas suporte 24x7 é caro. Se você tem infraestrutura própria ou usa uma plataforma IaaS como AWS EC2, você precisa de pessoal 24x7, ou seja, no mínimo 2 administradores de sistema/devops em tempo integral cuidando das coisas. A alternativa é usar um PaaS que pelo menos mantém essas higienes básicas em dia, mais um desenvolvedor part-time só para manter as suas aplicações atualizadas.</p>
<p>Se você não fizer isso, está expondo seu negócio a uma responsabilidade enorme. E se você é desenvolvedor, faça a coisa certa: avise seus clientes sobre esse fato e deixe-os tomar uma decisão informada. Eles podem optar por continuar no barato, mas pelo menos sabem o que isso realmente significa.</p>
<h3>Qual web server? TL;DR: Passenger ou Puma<span class="hx:absolute hx:-mt-20" id="qual-web-server-tldr-passenger-ou-puma"></span>
    <a href="#qual-web-server-tldr-passenger-ou-puma" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A maioria das aplicações Rails não tem nem as bibliotecas básicas instaladas para funcionar direito.</p>
<p>Já vi gente rodando aplicação em produção simplesmente subindo o <tt>rails server</tt> e mais nada! Isso roda o Webrick, o web server básico, em produção! Você tem que escolher entre Puma, Unicorn ou Passenger. O Unicorn, na verdade, não protege sua aplicação contra clientes lentos, então evite-o se puder.</p>
<p>Se estiver na dúvida, <a href="https://www.phusionpassenger.com/library/walkthroughs/deploy/ruby/heroku/standalone/oss/deploy_app_main.html"target="_blank" rel="noopener">instale o Passenger</a> e vá tomar um café.</p>
<p>A escolha mais comum hoje em dia é <a href="https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server"target="_blank" rel="noopener">usar Puma</a>. Mas se você está rodando no Heroku ou usando instâncias pequenas, tenha em mente o consumo de RAM. Uma aplicação Rails normal consome no mínimo 150Mb a 200Mb ou mais, dependendo do tamanho do seu app. O menor dyno do Heroku tem só 512Mb de RAM, o que significa que você não deve configurar Puma ou Unicorn com mais que 2 instâncias. Eu recomendaria ir de Dyno 2X (1GB) e ter no máximo 3 instâncias (ou seja, 3 requisições concorrentes possíveis sendo processadas ao mesmo tempo).</p>
<p>É comum sua aplicação ir aumentando o consumo de RAM ao longo do tempo, e a cada par de dias você ver um R14 acontecendo. Aí o dyno reinicia e começa a funcionar melhor por um tempo, até bater no teto de novo.</p>
<p>Se você não sabe por que isso acontece e é algo infrequente, dá para usar temporariamente o <a href="https://github.com/schneems/puma_worker_killer"target="_blank" rel="noopener">Puma Worker Killer</a> - se você estiver usando Puma, claro - para ficar monitorando suas instâncias até que cheguem a um certo teto mais baixo e reciclem automaticamente antes que seus usuários vejam os erros R14.</p>
<p>Você também já deve ter ouvido falar do <a href="http://tmm1.net/ruby21-oobgc"target="_blank" rel="noopener">Out-of-Band GC</a>. Mas <a href="https://github.com/puma/puma/issues/450"target="_blank" rel="noopener">fique avisado</a> que o Puma em modo threaded não funciona bem com isso. Se você usa Unicorn, então use sem medo.</p>
<h3>Meça, Meça, Meça. TL;DR: New Relic<span class="hx:absolute hx:-mt-20" id="meça-meça-meça-tldr-new-relic"></span>
    <a href="#me%c3%a7a-me%c3%a7a-me%c3%a7a-tldr-new-relic" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/535/big_heroku-metrics.png" alt="Heroku Metrics"  loading="lazy" /></p>
<p>Você deve <strong>sempre</strong> monitorar sua aplicação. Seja com software instalado por conta própria como Nagios, Munin, Zabbix, Zenoss, Ganglia etc, ou no mínimo com as métricas que o próprio Heroku fornece, como na imagem acima.</p>
<p>Se suas instâncias estão se comportando de forma estranha, dê uma olhada nos avisos. Um conjunto frequente de erros <a href="https://devcenter.heroku.com/articles/ruby-memory-use"target="_blank" rel="noopener">R14</a> significa que o consumo de memória da sua app está crescendo ao longo do tempo e estourando o limite de RAM disponível na instância (provável vazamento de memória). No mínimo, isso significa que você deve diminuir a concorrência/workers no seu Puma/Unicorn e também aumentar o número de dynos para compensar, se tiver tráfego suficiente para justificar. Mas essa é uma ação orientada a métricas que você pode tomar porque tem informação para uma decisão consciente.</p>
<p>Você também deve estar armazenando e indexando seus logs usando software instalado como Greylog2 ou Logstash, ou uma alternativa SaaS como o <a href="https://elements.heroku.com/addons/papertrail"target="_blank" rel="noopener">Papertrail</a>. Existem várias razões pelas quais uma aplicação pode estar vazando, ou pelas quais tudo parece estar bem nas métricas mas você está tendo erros aleatórios. Sempre consulte os logs para ter respostas.</p>
<p>Melhor ainda, <a href="https://docs.newrelic.com/docs/agents/ruby-agent/miscellaneous/ruby-agent-heroku"target="_blank" rel="noopener">instale o New Relic APM</a>. Você terá métricas, alertas automatizados, medição de erros e diagnósticos realmente profundos para ver exatamente que linha do seu código fonte é a culpada pela performance ruim, pelos erros frequentes ou pelo comportamento errático. Se estiver em dúvida, instale logo, o plano básico é grátis mesmo.</p>
<h3>Algumas Dependências do Rails<span class="hx:absolute hx:-mt-20" id="algumas-dependências-do-rails"></span>
    <a href="#algumas-depend%c3%aancias-do-rails" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Você <strong>NÃO</strong> deveria estar usando Rails abaixo da versão 4.0 a essa altura. O 3.2 ainda é bastante usado, mas o suporte vai acabar logo. Não use o Rails 5.0 agora a menos que tenha experiência suficiente para saber o que fazer. Qualquer coisa entre 4.0 e 4.2 deve estar ok, mas fique atento aos minor releases que corrigem vulnerabilidades de segurança.</p>
<p>Você <strong>NÃO</strong> deveria estar usando Ruby abaixo da versão 2.2 a essa altura. As atualizações de segurança para o 2.0 acabaram em fevereiro de 2016. O 2.1 não foi um release estável bom, mas o 2.2 e o 2.3 estão muito bons e você deveria estar usando eles.</p>
<p>Então a combinação Ruby 2.3.0 e Rails 4.2.5.2 (a partir de março de 2016) é a escolha ideal para a melhor estabilidade, segurança e suporte do ecossistema.</p>
<p>Você deveria ter, no mínimo, as seguintes dependências instaladas por padrão:</p>
<ul>
<li><a href="https://github.com/heroku/rails_12factor"target="_blank" rel="noopener">Rails 12 Factor gem</a></li>
<li><a href="http://rtomayko.github.io/rack-cache/"target="_blank" rel="noopener">Rack-Cache middleware</a>
<ul>
<li><a href="https://devcenter.heroku.com/articles/memcachier#rails-3-and-4"target="_blank" rel="noopener">Gem Dalli com Memcached ou Memcachier</a> - mesmo se você não estiver usando fragment caching ou AR caching, isso ainda é útil pelo menos para a <a href="https://devcenter.heroku.com/articles/rack-cache-memcached-rails31"target="_blank" rel="noopener">gem Rack-Cache</a>.</li>
</ul>
</li>
<li><a href="https://github.com/kickstarter/rack-attack"target="_blank" rel="noopener">Rack-Attack</a> - leia como configurar direito!</li>
<li><a href="http://www.sinatrarb.com/rack-protection/"target="_blank" rel="noopener">Rack-Protection</a> - leia como configurar direito e quais proteções você quer ativar, não tudo, mas pelo menos CSRF e XSS.</li>
<li><a href="http://www.schneems.com/blogs/2016-02-18-speeding-up-sprockets/"target="_blank" rel="noopener">Sprockets 3.3+</a> - garanta que está usando a versão do Sprockets que corrige a lentidão e deixa as coisas razoavelmente mais rápidas</li>
</ul>
<h3>Qual Banco de Dados? TL;DR: use Postgresql<span class="hx:absolute hx:-mt-20" id="qual-banco-de-dados-tldr-use-postgresql"></span>
    <a href="#qual-banco-de-dados-tldr-use-postgresql" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>NÃO</strong> escolha MongoDB ou similar a menos que tenha uma razão muito forte e esteja confiante de que é muito bom no que faz e vai ficar por perto para manter o que colocou em produção. Caso contrário, fique no Postgresql e na gem &ldquo;pg&rdquo;, você não vai se arrepender, nem o seu cliente.</p>
<p>Use instâncias pequenas de Redis (particularmente opções SaaS como o <a href="https://elements.heroku.com/addons/heroku-redis"target="_blank" rel="noopener">Heroku Redis</a>) com o <a href="https://github.com/mperham/sidekiq/wiki/Deployment"target="_blank" rel="noopener">Sidekiq</a>. Você pode optar pelo Sucker Punch, mais leve, mas saiba que ele vai aumentar bastante o consumo de RAM das suas instâncias dependendo do tamanho das filas e dos dados serializados de entrada para os workers. Nem sempre é trivial saber quantos workers Sidekiq você deveria ter, mas <a href="http://manuel.manuelles.nl/sidekiq-heroku-redis-calc/"target="_blank" rel="noopener">esta calculadora</a> pode ajudar.</p>
<p>Existem pelo menos 2 grandes erros que a maioria das pessoas comete usando Sidekiq:</p>
<ul>
<li>Ter workers pesados batendo no seu banco de dados master. Por exemplo, um grande procedimento de analytics buscando grandes conjuntos de dados enquanto seu site principal também tem uso intenso de usuários escrevendo dados o tempo inteiro. Definitivamente adicione um <a href="https://devcenter.heroku.com/articles/heroku-postgres-follower-databases"target="_blank" rel="noopener">banco Follower</a> e aponte os workers Sidekiq (read-only) para lá, para analytics, relatórios e cargas similares.</li>
<li>Buscar grandes datasets em memória e estourar seus dynos de worker. Se você fizer qualquer coisa parecida com &ldquo;YourModel.all.each { |r| &hellip; }&rdquo;, está dando um tiro no próprio pé. O Rails tem formas fáceis de <a href="http://api.rubyonrails.org/classes/ActiveRecord/Batches.html"target="_blank" rel="noopener">buscar pequenos conjuntos em batch</a> para você processar, use-as.</li>
</ul>
<h3>Você está usando um CDN? TL;DR: simplesmente use<span class="hx:absolute hx:-mt-20" id="você-está-usando-um-cdn-tldr-simplesmente-use"></span>
    <a href="#voc%c3%aa-est%c3%a1-usando-um-cdn-tldr-simplesmente-use" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Isso é fácil, use <a href="https://devcenter.heroku.com/articles/using-amazon-cloudfront-cdn"target="_blank" rel="noopener">AWS CloudFront</a> ou <a href="https://devcenter.heroku.com/articles/fastly"target="_blank" rel="noopener">Fastly</a> ou outra coisa, mas <strong>USE</strong> um CDN. É óbvio.</p>
<p>Caso contrário, seus assets vão ser servidos pelas suas instâncias de web dyno, que já são pequenas. E isso é um trabalho pesado que é melhor servido por um CDN muito mais rápido. Seus usuários vão perceber ordens de magnitude em usabilidade só mudando algumas linhas de configuração no seu código.</p>
<p>Se você implementou suas views, templates e stylesheets do Rails usando os helpers &ldquo;image_tag&rdquo; e &ldquo;asset_path&rdquo;, isso é automático. Faça logo.</p>
<p>E já que estamos falando de assets, se você por acaso tem upload de imagens e está usando o jeito simples de postar um formulário multipart direto para a sua aplicação Rails, vai ter problemas, porque o Heroku impõe - corretamente - um timeout máximo de 30 segundos. Se você tem muitos usuários subindo muitas imagens de alta resolução e tamanho pesado, eles vão ver cada vez mais timeouts.</p>
<p>Use a <a href="http://cloudinary.com/documentation/rails_additional_topics"target="_blank" rel="noopener">gem Attachinary</a> para fazer upload direto via Javascript, do navegador para o serviço Cloudinary, contornando completamente sua aplicação Rails, que vai apenas receber o ID quando o upload terminar. De novo, tem um plano grátis bem generoso e é tão simples de implementar que você precisa fazer isso já.</p>
<h3>Você demitiu seu desenvolvedor, e agora? TL;DR: revogue os acessos!<span class="hx:absolute hx:-mt-20" id="você-demitiu-seu-desenvolvedor-e-agora-tldr-revogue-os-acessos"></span>
    <a href="#voc%c3%aa-demitiu-seu-desenvolvedor-e-agora-tldr-revogue-os-acessos" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Isso acontece muito: uma empresa pequena precisa cortar custos e demitir desenvolvedores.</p>
<p>Garanta que você revogue os acessos. Na AWS isso é bem mais convoluto se você não conhece bem a ferramenta de autorização IAM deles. Ou pior: se eles têm as chaves privadas (arquivos .pem) para suas instâncias EC2!! Você vai ter que criar novos pares de chaves, desativar as instâncias EC2 antigas e criar novas. Uma puta dor de cabeça.</p>
<p>Você precisa trocar senhas do seu serviço de DNS, da organização no Github/Bitbucket, remover chaves SSH dos arquivos .ssh/authorized_keys em todos os seus servidores etc.</p>
<p>Garanta que sua empresa tem um bom suporte jurídico e faça com que cada colaborador, funcionário ou terceirizado, assine um acordo de confidencialidade, NDA ou non-compete.</p>
<p>Você <strong>PRECISA</strong> ter pelo menos uma pessoa-chave na empresa que tem controle sobre as chaves, senhas e acesso a todo serviço do qual sua empresa depende para funcionar. Caso contrário, não se surpreenda quando sua aplicação ficar fora do ar.</p>
<p>E não se iluda achando que, só porque uma aplicação está online e rodando, vai continuar assim por muito tempo. Vulnerabilidades de segurança estão soltas por aí, é só uma questão de tempo até algum problema derrubar seu serviço, e é melhor você ter alguém para colocá-lo de volta no ar se você mesmo não souber fazer isso.</p>
<h3>Site Grande? Parabéns, e agora?<span class="hx:absolute hx:-mt-20" id="site-grande-parabéns-e-agora"></span>
    <a href="#site-grande-parab%c3%a9ns-e-agora" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Se seu negócio está prosperando, florescendo, parabéns. Você está naquela porcentagem muito muito pequena que realmente conseguiu.</p>
<p>Em breve sua aplicação novinha em folha vai virar aquele sistema feio e temido chamado <strong>Legacy</strong>. Pior, provavelmente é um daqueles <strong>monolitos</strong> assustadores ou, pior ainda, você deixou um desenvolvedor hipster criar uma arquitetura espaguete cheia de <strong>microserviços</strong> sem controle. Você está condenado de qualquer jeito.</p>
<p>Essa situação exige que você fique com os pés no chão: bala de prata não existe, é o trabalho de sempre.</p>
<p>Você precisa se manter à frente do jogo, e a parte de monitoramento que mencionei tem um papel grande para guiar você pelas várias direções possíveis. Por exemplo, por um curto período de tempo dá para aumentar os planos dos seus serviços SaaS como bancos de dados, caches, filas, e também adicionar mais web dynos paralelos.</p>
<p>Isso vai segurar a onda por um tempo (suas métricas podem ajudar a estimar por quanto), mas agora você precisa montar um plano para otimizar o que tem. E não, reescrever tudo é a última coisa que você quer fazer, a menos que pretenda não ter vida pelos próximos dois anos.</p>
<p>Confie em Pareto: na vida real, 80% dos principais problemas podem ser resolvidos com 20% do esforço. A grande dica: abra o New Relic - agora carregado com dados reais de produção - e olhe os Top piores performers e comece por aí. O primeiro que você resolver (seja uma feia query N+1 no banco, uma integração lenta com web service), vai te dar muito espaço para respirar.</p>
<h3>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O que eu acabei de dizer é só a ponta do iceberg em termos de otimização de performance Rails. E posso te garantir que existe muito espaço para melhorar sem precisar recorrer a reescrever tudo na próxima linguagem da moda. Recomendo que você compre o <a href="https://www.railsspeed.com/"target="_blank" rel="noopener">&ldquo;The Complete Guide to Rails Performance&rdquo;</a> do Nate Berkopec para realmente aprender o terreno.</p>
<p>Esta é uma compilação muito rápida que despejei da cabeça, mas tem muito mais coisa que podemos considerar. Mande perguntas na seção de comentários e outros itens importantes que eu posso ter deixado passar.</p>
]]></content:encoded><category>tutoriais</category><category>devops</category><category>rails</category></item><item><title>Ex Pusher Lite - Parte 2 - O Primeiro Core Funcional!</title><link>https://akitaonrails.github.io/2015/12/14/ex-pusher-lite-parte-2-o-primeiro-core-funcional/</link><guid isPermaLink="true">https://akitaonrails.github.io/2015/12/14/ex-pusher-lite-parte-2-o-primeiro-core-funcional/</guid><pubDate>Mon, 14 Dec 2015 15:04:00 -0200</pubDate><description>&lt;p&gt;Na &lt;a href="http://www.akitaonrails.com/2015/12/09/ex-pusher-lite-parte-1-phoenix-channels-e-aplicacoes-rails"target="_blank" rel="noopener"&gt;Parte 1&lt;/a&gt; eu basicamente comecei a partir do tutorial do &lt;a href="https://labs.opendoor.com/phoenix-on-rails-for-client-push-notifications"target="_blank" rel="noopener"&gt;Daniel Neighman&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Nesta Parte 2 vou adicionar os mecanismos necessários para ter um core mínimo que seja realmente útil e fazer deploy no Heroku. Para isso preciso implementar o seguinte:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;uma autenticação simples de Administração (um admin_username e admin_password hardcoded já resolvem por enquanto)&lt;/li&gt;
&lt;li&gt;um endpoint &lt;strong&gt;&amp;quot;/api/admin/apps&amp;quot;&lt;/strong&gt; restrito ao admin para gerenciar novas Aplicações. Cada Aplicação deve ter uma key e um secret gerados aleatoriamente.&lt;/li&gt;
&lt;li&gt;o endpoint &amp;ldquo;/events&amp;rdquo; da Parte 1 deve ser movido para &lt;strong&gt;&amp;quot;/api/apps/:app_id/events&amp;quot;&lt;/strong&gt; e ter o acesso restrito pela autenticação da key e secret da Aplicação identificada como &amp;ldquo;app_id&amp;rdquo;.&lt;/li&gt;
&lt;li&gt;por enquanto, o Event vai apenas fazer broadcast no tópico apropriado. Queremos poder fazer broadcast pra todo mundo de uma Aplicação e também pra tópicos específicos dentro do escopo daquela Aplicação.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Como de costume, o código desta seção vai estar marcado com a tag &lt;strong&gt;v0.2&lt;/strong&gt; tanto no repositório do &lt;a href="https://github.com/akitaonrails/pusher_lite_demo/tree/v0.2"target="_blank" rel="noopener"&gt;client demo&lt;/a&gt; quanto no &lt;a href="https://github.com/akitaonrails/ex_pusher_lite/tree/v0.2"target="_blank" rel="noopener"&gt;server-side&lt;/a&gt; no Github.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Na <a href="http://www.akitaonrails.com/2015/12/09/ex-pusher-lite-parte-1-phoenix-channels-e-aplicacoes-rails"target="_blank" rel="noopener">Parte 1</a> eu basicamente comecei a partir do tutorial do <a href="https://labs.opendoor.com/phoenix-on-rails-for-client-push-notifications"target="_blank" rel="noopener">Daniel Neighman</a>.</p>
<p>Nesta Parte 2 vou adicionar os mecanismos necessários para ter um core mínimo que seja realmente útil e fazer deploy no Heroku. Para isso preciso implementar o seguinte:</p>
<ul>
<li>uma autenticação simples de Administração (um admin_username e admin_password hardcoded já resolvem por enquanto)</li>
<li>um endpoint <strong>&quot;/api/admin/apps&quot;</strong> restrito ao admin para gerenciar novas Aplicações. Cada Aplicação deve ter uma key e um secret gerados aleatoriamente.</li>
<li>o endpoint &ldquo;/events&rdquo; da Parte 1 deve ser movido para <strong>&quot;/api/apps/:app_id/events&quot;</strong> e ter o acesso restrito pela autenticação da key e secret da Aplicação identificada como &ldquo;app_id&rdquo;.</li>
<li>por enquanto, o Event vai apenas fazer broadcast no tópico apropriado. Queremos poder fazer broadcast pra todo mundo de uma Aplicação e também pra tópicos específicos dentro do escopo daquela Aplicação.</li>
</ul>
<p>Como de costume, o código desta seção vai estar marcado com a tag <strong>v0.2</strong> tanto no repositório do <a href="https://github.com/akitaonrails/pusher_lite_demo/tree/v0.2"target="_blank" rel="noopener">client demo</a> quanto no <a href="https://github.com/akitaonrails/ex_pusher_lite/tree/v0.2"target="_blank" rel="noopener">server-side</a> no Github.</p>
<h3>O Resource App<span class="hx:absolute hx:-mt-20" id="o-resource-app"></span>
    <a href="#o-resource-app" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Se este nosso projeto vai se comportar como o Pusher.com, precisamos de uma forma de criar novas &ldquo;Aplicações&rdquo;. Cada cliente que se conecta a esse serviço vai estar atrelado a essa Aplicação. Os Events ficam restritos à fronteira da Aplicação. É assim que vamos isolar os diferentes clientes que se conectam ao mesmo servidor. Então você pode ter um único core servindo várias aplicações web diferentes.</p>
<p>Quando uma nova aplicação é criada, o web app cliente/consumidor recebe o par de tokens key e secret que vai usar tanto para os triggers do lado servidor quanto para os listeners de Websocket no lado cliente.</p>
<p>Como aviso prévio, neste estágio do desenvolvimento eu não vou implementar nenhum sistema sofisticado de autenticação como OAuth2 ou JWT. Vou deixar isso pra posts futuros. Por enquanto vou usar a key e o secret da Aplicação como simples username e password num HTTP Basic Auth. Por ora isso já basta pros nossos propósitos.</p>
<p>Então, o primeiro passo é criar esse resource &ldquo;Application&rdquo; e podemos recorrer ao gerador de scaffold JSON que já vem com o Phoenix:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>mix phoenix.gen.json App apps name:string slug:string key:string secret:string active:boolean</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A maioria dos tutoriais mostra o gerador &ldquo;<tt>phoenix.gen.html</tt>&rdquo;, que se comporta como o &ldquo;scaffold&rdquo; do Rails, gerando templates HTML pra cada um dos verbos CRUD. Esse aqui é parecido mas pula o HTML e assume que vai ser uma API CRUD em JSON.</p>
<p>Precisamos atualizar manualmente o arquivo &ldquo;<tt>web/router.ex</tt>&rdquo; assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># web/router.ex</span>
</span></span><span class="line"><span class="cl"><span class="n">scope</span> <span class="s2">&#34;/api&#34;</span><span class="p">,</span> <span class="no">ExPusherLite</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">pipe_through</span> <span class="ss">:api</span>
</span></span><span class="line"><span class="cl">  <span class="n">post</span> <span class="s2">&#34;/apps/:app_slug/events&#34;</span><span class="p">,</span> <span class="no">EventsController</span><span class="p">,</span> <span class="ss">:create</span>
</span></span><span class="line"><span class="cl">  <span class="n">scope</span> <span class="s2">&#34;/admin&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">resources</span> <span class="s2">&#34;/apps&#34;</span><span class="p">,</span> <span class="no">AppController</span><span class="p">,</span> <span class="ss">except</span><span class="p">:</span> <span class="o">[</span><span class="ss">:new</span><span class="p">,</span> <span class="ss">:edit</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O &ldquo;<tt>EventsController</tt>&rdquo; é aquele que implementamos na Parte 1 e que vamos refazer durante esta Parte 2.</p>
<p>O gerador nos deu esse novo &ldquo;<tt>AppController</tt>&rdquo; e, assim como nas rotas do Rails, a DSL aqui é notavelmente parecida. Se você é um Railer aposto que reconhece de cara as rotas que essa DSL está gerando.</p>
<p>O gerador também criou uma migration apropriada pra gente:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># priv/repo/migrations/20151210131528_create_app.exs</span>
</span></span><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Repo</span><span class="o">.</span><span class="n">Migrations</span><span class="o">.</span><span class="n">CreateApp</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">Ecto</span><span class="o">.</span><span class="n">Migration</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">change</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">create</span> <span class="n">table</span><span class="p">(</span><span class="ss">:apps</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">add</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">:string</span>
</span></span><span class="line"><span class="cl">      <span class="n">add</span> <span class="ss">:slug</span><span class="p">,</span> <span class="ss">:string</span>
</span></span><span class="line"><span class="cl">      <span class="n">add</span> <span class="ss">:key</span><span class="p">,</span> <span class="ss">:string</span>
</span></span><span class="line"><span class="cl">      <span class="n">add</span> <span class="ss">:secret</span><span class="p">,</span> <span class="ss">:string</span>
</span></span><span class="line"><span class="cl">      <span class="n">add</span> <span class="ss">:active</span><span class="p">,</span> <span class="ss">:boolean</span><span class="p">,</span> <span class="ss">default</span><span class="p">:</span> <span class="kp">false</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="n">timestamps</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="n">create</span> <span class="n">index</span><span class="p">(</span><span class="ss">:apps</span><span class="p">,</span> <span class="o">[</span><span class="ss">:name</span><span class="o">]</span><span class="p">,</span> <span class="ss">unique</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">create</span> <span class="n">index</span><span class="p">(</span><span class="ss">:apps</span><span class="p">,</span> <span class="o">[</span><span class="ss">:slug</span><span class="o">]</span><span class="p">,</span> <span class="ss">unique</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>De novo, notavelmente parecido com a DSL de Migration do ActiveRecord. Migrations se comportam exatamente como você espera. Você precisa rodar:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>mix ecto.create # se ainda não tiver feito isso
mix ecto.migrate</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Este resource App vai precisar gerar slugs a partir dos nomes (que vamos usar como &ldquo;app_id&rdquo;) e também gerar valores aleatórios para key e secret. Então temos que adicionar essas dependências no arquivo &ldquo;<tt>mix.exs</tt>&rdquo;:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># mix.exs</span>
</span></span><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">deps</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="o">[...</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="p">{</span><span class="ss">:secure_random</span><span class="p">,</span> <span class="s2">&#34;~&gt; 0.2.0&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">   <span class="p">{</span><span class="ss">:slugger</span><span class="p">,</span> <span class="s2">&#34;~&gt; 0.0.1&#34;</span><span class="p">}</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <a href="https://github.com/akitaonrails/ex_pusher_lite/blob/v0.2/web/models/app.ex"target="_blank" rel="noopener">model App final</a> é bem grande, então vou destrinchar pra vocês:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># web/models/app.ex</span>
</span></span><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">App</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Web</span><span class="p">,</span> <span class="ss">:model</span>
</span></span><span class="line"><span class="cl">  <span class="k">alias</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Repo</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">schema</span> <span class="s2">&#34;apps&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">field</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">:string</span>
</span></span><span class="line"><span class="cl">    <span class="n">field</span> <span class="ss">:slug</span><span class="p">,</span> <span class="ss">:string</span>
</span></span><span class="line"><span class="cl">    <span class="n">field</span> <span class="ss">:key</span><span class="p">,</span> <span class="ss">:string</span>
</span></span><span class="line"><span class="cl">    <span class="n">field</span> <span class="ss">:secret</span><span class="p">,</span> <span class="ss">:string</span>
</span></span><span class="line"><span class="cl">    <span class="n">field</span> <span class="ss">:active</span><span class="p">,</span> <span class="ss">:boolean</span><span class="p">,</span> <span class="ss">default</span><span class="p">:</span> <span class="kp">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">timestamps</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="vi">@required_fields</span> <span class="o">~</span><span class="n">w</span><span class="p">(</span><span class="nb">name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="vi">@optional_fields</span> <span class="o">~</span><span class="n">w</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Esse bloco declara o Schema do model. Cuidado quando você gera uma migration e depois muda as configurações dos campos: você precisa lembrar de atualizar o schema no model. Na minha primeira tentativa eu não incluí o campo &ldquo;slug&rdquo;, então fiz rollback da migration (&quot;<tt>mix ecto.rollback</tt>&quot;), mudei a migration pra adicionar o campo &ldquo;slug&rdquo; e rodei a task &ldquo;<tt>ecto.migrate</tt>&rdquo; de novo.</p>
<p>Fiquei intrigado porque o model não estava pegando o novo campo; depois de um tempo lembrei que models do Ecto não tentam buscar o schema real do banco e gerar accessors dinamicamente, mas dependem do bloco de schema declarado explicitamente como mostrado acima. Depois que adicionei o campo &ldquo;slug&rdquo; no bloco de schema, o model passou a usar o novo campo direitinho.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># web/models/app.ex</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">get_by_slug</span><span class="p">(</span><span class="n">slug</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="no">Repo</span><span class="o">.</span><span class="n">get_by!</span><span class="p">(</span><span class="n">__MODULE__</span><span class="p">,</span> <span class="ss">slug</span><span class="p">:</span> <span class="n">slug</span><span class="p">,</span> <span class="ss">active</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">hashed_secret</span><span class="p">(</span><span class="n">model</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="no">Base</span><span class="o">.</span><span class="n">encode64</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">#{</span><span class="n">model</span><span class="o">.</span><span class="n">key</span><span class="si">}</span><span class="s2">:</span><span class="si">#{</span><span class="n">model</span><span class="o">.</span><span class="n">secret</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Essas são apenas funções helper pra usar no AppController. A parte estranha talvez seja &ldquo;<strong>MODULE</strong>&rdquo;, mas isso é só um atalho pra representação em atom do módulo atual, que é &ldquo;<tt>ExPusherLite.App</tt>&rdquo;. É assim que se faz uma query simples ao model, lembra muito o &ldquo;<tt>App.get_by(slug: slug, active: true)</tt>&rdquo; do Rails.</p>
<p>Por convenção do Elixir, o Ecto tem funções com e sem bang (&quot;<tt>get_by!</tt>&quot; e &ldquo;<tt>get_by</tt>&rdquo;). Se você quiser capturar um erro, usa a versão sem bang e ela retorna ou uma tupla &ldquo;<tt>{:ok, result}</tt>&rdquo; ou &ldquo;<tt>{:error, result}</tt>&rdquo; e você pode fazer pattern match. Ou usa a versão com bang e ela levanta uma exception. Depende do que você quer fazer.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># web/models/app.ex</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">changeset</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">params</span> <span class="p">\\</span> <span class="ss">:empty</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">model</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">cast</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="vi">@required_fields</span><span class="p">,</span> <span class="vi">@optional_fields</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">validate_length</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">min</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span> <span class="ss">max</span><span class="p">:</span> <span class="mi">255</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">unique_constraint</span><span class="p">(</span><span class="ss">:name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">generate_key</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">generate_secret</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">slugify</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">unique_constraint</span><span class="p">(</span><span class="ss">:slug</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Atrás apenas do bloco de Schema que mencionei acima, esta função &ldquo;<tt>changeset/2</tt>&rdquo; é a parte mais importante de um Model.</p>
<p>No Rails você tem só o conceito de &ldquo;Model&rdquo;, que é considerado &ldquo;Fat&rdquo; porque cuida de operações de banco, lógica de negócio e hooks do framework tudo no mesmo lugar. No Phoenix você lida com pelo menos 3 conceitos diferentes:</p>
<ul>
<li>Você tem um <strong>Repo</strong>, que recebe um Changeset e usa pra inserir ou atualizar as linhas designadas na tabela do banco. Você vai ver &ldquo;<tt>Repo.get</tt>&rdquo; ou &ldquo;<tt>Repo.insert</tt>&rdquo;, e não &ldquo;<tt>App.find</tt>&rdquo; ou &ldquo;<tt>App.save</tt>&rdquo;.</li>
<li>Aí você tem o <strong>Changeset</strong>, que são apenas Maps do Elixir (aqueles com a sintaxe &ldquo;%<tt>{key =&gt; value}</tt>&rdquo;). É só um Hash, um Dictionary, uma coleção de pares chave-valor. Um Repo aceita Maps pras suas operações.</li>
<li>Por fim, você tem o <strong>Model</strong>, que de fato dá significado e contexto aos Changesets crus. E a função &ldquo;<tt>changeset/2</tt>&rdquo; acima é a responsável por receber um Map cru, passá-lo por uma cadeia de validação e transformação e devolver um Changeset válido pro Repo usar.</li>
</ul>
<p>Então, num controller você normalmente vai encontrar código assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># web/controllers/app_controller.ex</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">create</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="sx">%{&#34;app&#34; =&gt; app_params}</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">changeset</span> <span class="o">=</span> <span class="no">App</span><span class="o">.</span><span class="n">changeset</span><span class="p">(</span><span class="o">%</span><span class="no">App</span><span class="p">{},</span> <span class="n">app_params</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="k">case</span> <span class="no">Repo</span><span class="o">.</span><span class="n">insert</span><span class="p">(</span><span class="n">changeset</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">app</span><span class="p">}</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="n">conn</span>
</span></span><span class="line"><span class="cl">      <span class="o">|&gt;</span> <span class="o">...</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="ss">:error</span><span class="p">,</span> <span class="n">changeset</span><span class="p">}</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="n">conn</span>
</span></span><span class="line"><span class="cl">      <span class="o">|&gt;</span> <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>É assim que você cria um changeset novo, validado, e passa pro Repo, tratando os resultados num bloco de pattern match. Compare a linha do changeset acima com o início da função &ldquo;changeset/2&rdquo;:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">changeset</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">params</span> <span class="p">\\</span> <span class="ss">:empty</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">model</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">cast</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="vi">@required_fields</span><span class="p">,</span> <span class="vi">@optional_fields</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">validate_length</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">min</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span> <span class="ss">max</span><span class="p">:</span> <span class="mi">255</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Ela mapeia o registro vazio &ldquo;<tt>%App{}</tt>&rdquo; pro argumento &ldquo;model&rdquo; e o &ldquo;app_params&rdquo; que vem do request (um map do formato &ldquo;<tt>%{name =&gt; &lsquo;foo&rsquo;, active: =&gt; &rsquo;true&rsquo;}</tt>&rdquo;) pro argumento &ldquo;params&rdquo;. Depois encanou o model e os params na função &ldquo;<tt>cast/4</tt>&rdquo;, que vai copiar os valores do map de params pro map/changeset do model. E vai passando o changeset resultante pras funções seguintes, como o &ldquo;<tt>validated_length/3</tt>&rdquo; abaixo, e assim por diante. Se a cadeia termina sem exceptions, você tem no final um changeset limpo, validado, que é só passar pro Repo enfiar no banco sem pensar.</p>
<p>Na implementação acima estamos encadeando filtros pra gerar key, secret e slug, e essa é a implementação como funções privadas:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># web/models/app.ex</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="n">defp</span> <span class="n">generate_key</span><span class="p">(</span><span class="n">model</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">get_field</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="ss">:key</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">model</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="n">model</span>
</span></span><span class="line"><span class="cl">      <span class="o">|&gt;</span> <span class="n">put_change</span><span class="p">(</span><span class="ss">:key</span><span class="p">,</span> <span class="no">SecureRandom</span><span class="o">.</span><span class="n">uuid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">defp</span> <span class="n">generate_secret</span><span class="p">(</span><span class="n">model</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">get_field</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="ss">:secret</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">model</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="n">model</span>
</span></span><span class="line"><span class="cl">      <span class="o">|&gt;</span> <span class="n">put_change</span><span class="p">(</span><span class="ss">:secret</span><span class="p">,</span> <span class="no">SecureRandom</span><span class="o">.</span><span class="n">uuid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">defp</span> <span class="n">slugify</span><span class="p">(</span><span class="n">model</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">name</span> <span class="o">=</span> <span class="n">get_change</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="ss">:name</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">model</span>
</span></span><span class="line"><span class="cl">      <span class="o">|&gt;</span> <span class="n">put_change</span><span class="p">(</span><span class="ss">:slug</span><span class="p">,</span> <span class="no">Slugger</span><span class="o">.</span><span class="n">slugify_downcase</span><span class="p">(</span><span class="nb">name</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="n">model</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A lógica é tal que key/secret novos são gerados apenas se os campos estiverem vazios e um slug novo é gerado apenas se o name tiver mudado. E é isso, eu disse que o código do model ia ser meio grande. Dá pra ver como usar as bibliotecas Slugger e SecureRandom que adicionamos no &ldquo;mix.exs&rdquo; antes.</p>
<p>Eu também quero adicionar o equivalente de um arquivo de seed do Rails pra criar uma aplicação de teste, assim quem está chegando agora sabe o que fazer. O Phoenix tem seeds e você pode implementar assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># priv/repo/seeds.exs</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">alias</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">App</span>
</span></span><span class="line"><span class="cl"><span class="k">alias</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Repo</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># não usar o App.changeset pula todas as validações e gerações</span>
</span></span><span class="line"><span class="cl"><span class="no">Repo</span><span class="o">.</span><span class="n">insert!</span> <span class="o">%</span><span class="no">App</span><span class="p">{</span> <span class="ss">slug</span><span class="p">:</span> <span class="s2">&#34;test-app&#34;</span><span class="p">,</span> <span class="nb">name</span><span class="p">:</span> <span class="s2">&#34;Test App&#34;</span><span class="p">,</span> <span class="ss">key</span><span class="p">:</span> <span class="s2">&#34;test-app-fake-key&#34;</span><span class="p">,</span> <span class="ss">secret</span><span class="p">:</span> <span class="s2">&#34;test-app-fake-secret&#34;</span><span class="p">,</span> <span class="ss">active</span><span class="p">:</span> <span class="kp">true</span> <span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Lembra como detalhei o papel da função &ldquo;changeset/2&rdquo; em criar um changeset limpo e validado, que é só um Map? Você pode pular essa função inteira, montar seu próprio Map final na mão e passar pro Repo. O Repo não liga se o Map é válido ou não, ele simplesmente tenta inserir no banco. E neste caso o Model do App nos impede de hardcodar keys e secrets, então é assim que fazemos num arquivo de seed.</p>
<p>Dá pra rodar diretamente assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>mix run priv/repo/seeds.exs</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O AppController só precisa de 2 mudanças. A primeira é buscar o App pelo campo slug em vez do campo padrão &lsquo;id&rsquo;. É bem simples, basta substituir todas as chamadas de &ldquo;<tt>app = Repo.get!(App, id)</tt>&rdquo; por &ldquo;<tt>app = App.get_by_slug(id)</tt>&rdquo;, que é justamente por isso que implementamos essa função no model acima.</p>
<p>A segunda coisa é Autenticação.</p>
<h3>Adicionando Autenticação<span class="hx:absolute hx:-mt-20" id="adicionando-autenticação"></span>
    <a href="#adicionando-autentica%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Agora que temos um model App que consegue gerar UUIDs aleatórios seguros pra key e secret, vou adicionar um segundo nível de autenticação pra administradores conseguirem criar esses Apps.</p>
<p>Pra isso vou simplesmente hardcodar um secret no arquivo de config da aplicação como default de desenvolvimento. Assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># config/config.exs</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span> <span class="ss">:ex_pusher_lite</span><span class="p">,</span> <span class="ss">:admin_authentication</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">username</span><span class="p">:</span> <span class="s2">&#34;pusher_admin_username&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">password</span><span class="p">:</span> <span class="s2">&#34;pusher_admin_password&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="n">import_config</span> <span class="s2">&#34;</span><span class="si">#{</span><span class="no">Mix</span><span class="o">.</span><span class="n">env</span><span class="si">}</span><span class="s2">.exs&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Você precisa adicionar esse bloco antes da função &ldquo;<tt>import_config</tt>&rdquo;. Aí você pode sobrescrever esses valores no arquivo &ldquo;config/prod.secret.exs&rdquo;, por exemplo, assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># config/prod.secret.exs</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span> <span class="ss">:ex_pusher_lite</span><span class="p">,</span> <span class="ss">:admin_authentication</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">username</span><span class="p">:</span> <span class="s2">&#34;14e86e5fee3335fa88b0&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">password</span><span class="p">:</span> <span class="s2">&#34;2b94ff0f07ce9769567f&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Claro, gere o seu próprio par seguro de username e password e substitua no ambiente de produção, se for usar isso pra valer. Pro Heroku ainda vamos ajustar isso um pouco mais, então fique atento.</p>
<p>Pra facilitar o processo, eu também adicionei o seguinte helper:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># lib/ex_pusher_lite.ex</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Retorna o hash de Basic HTTP Auth de administração desta aplicação</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">admin_secret</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">admin_username</span> <span class="o">=</span> <span class="no">Application</span><span class="o">.</span><span class="n">get_env</span><span class="p">(</span><span class="ss">:ex_pusher_lite</span><span class="p">,</span> <span class="ss">:admin_authentication</span><span class="p">)</span><span class="o">[</span><span class="ss">:username</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">    <span class="n">admin_password</span> <span class="o">=</span> <span class="no">Application</span><span class="o">.</span><span class="n">get_env</span><span class="p">(</span><span class="ss">:ex_pusher_lite</span><span class="p">,</span> <span class="ss">:admin_authentication</span><span class="p">)</span><span class="o">[</span><span class="ss">:password</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">    <span class="n">secret</span> <span class="o">=</span> <span class="no">Base</span><span class="o">.</span><span class="n">encode64</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">#{</span><span class="n">admin_username</span><span class="si">}</span><span class="s2">:</span><span class="si">#{</span><span class="n">admin_password</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>É assim que você busca os valores de configuração. Estou gerando uma string Base64 simples a partir do username concatenado com o password com uma vírgula, que é o que o Basic HTTP Auth requer. Vou usar esse hash de admin pro &ldquo;AppController&rdquo; e cada cliente vai precisar fornecer a key/secret da sua própria instância de App pra disparar o &ldquo;EventsController&rdquo;.</p>
<p>Pros dois controllers vou criar um único Plug de Autenticação, assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># lib/ex_pusher_lite/authentication.ex</span>
</span></span><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Authentication</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">import</span> <span class="no">Plug</span><span class="o">.</span><span class="n">Conn</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">alias</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">App</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">init</span><span class="p">(</span><span class="n">assigns</span> <span class="p">\\</span> <span class="o">[</span><span class="ss">admin</span><span class="p">:</span> <span class="kp">false</span><span class="o">]</span><span class="p">),</span> <span class="k">do</span><span class="p">:</span> <span class="n">assigns</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="n">assigns</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">token</span> <span class="o">=</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="n">assigns</span><span class="o">[</span><span class="ss">:admin</span><span class="o">]</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">        <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">admin_secret</span>
</span></span><span class="line"><span class="cl">      <span class="k">else</span>
</span></span><span class="line"><span class="cl">        <span class="n">params</span> <span class="o">=</span> <span class="n">fetch_query_params</span><span class="p">(</span><span class="n">conn</span><span class="p">)</span><span class="o">.</span><span class="n">params</span>
</span></span><span class="line"><span class="cl">        <span class="n">params</span><span class="o">[</span><span class="s2">&#34;app_slug&#34;</span><span class="o">]</span> <span class="o">|&gt;</span> <span class="no">App</span><span class="o">.</span><span class="n">get_by_slug</span> <span class="o">|&gt;</span> <span class="no">App</span><span class="o">.</span><span class="n">hashed_secret</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Basic &#34;</span> <span class="o">&lt;&gt;</span> <span class="n">auth_token</span> <span class="o">=</span> <span class="n">hd</span><span class="p">(</span><span class="n">get_req_header</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="s2">&#34;authorization&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="no">Plug</span><span class="o">.</span><span class="n">Crypto</span><span class="o">.</span><span class="n">secure_compare</span><span class="p">(</span><span class="n">auth_token</span><span class="p">,</span> <span class="n">token</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">conn</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="n">conn</span> <span class="o">|&gt;</span> <span class="n">send_resp</span><span class="p">(</span><span class="mi">401</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">)</span> <span class="o">|&gt;</span> <span class="n">halt</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span> </span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Como expliquei em artigos anteriores, um Plug é como um Middleware encadeável do Rails ou até uma aplicação Rack. Ele precisa ter uma única &ldquo;<tt>call/2</tt>&rdquo; que recebe uma estrutura Plug.Conn e devolve ela de volta, permitindo formar uma cadeia/pipeline de Plugs.</p>
<p>Verificamos se queremos comparar com o token de Admin ou o token do App e aí pegamos o token de autorização Basic HTTP que está na estrutura de conexão do request HTTP (recuperamos valores individuais do header pela função &ldquo;<tt>get_req_header/2</tt>&rdquo;). Por fim, fazemos uma comparação segura entre os tokens.</p>
<p>Pra habilitar esse plug nos controllers basta adicionar assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># web/controllers/app_controller.ex</span>
</span></span><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">AppController</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Web</span><span class="p">,</span> <span class="ss">:controller</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">alias</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">App</span>
</span></span><span class="line"><span class="cl">  <span class="n">plug</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Authentication</span><span class="p">,</span> <span class="o">[</span><span class="ss">admin</span><span class="p">:</span> <span class="kp">true</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"> <span class="n">defmodule</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">EventsController</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">   <span class="n">use</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Web</span><span class="p">,</span> <span class="ss">:controller</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">-</span>  <span class="n">plug</span> <span class="ss">:authenticate</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>  <span class="n">plug</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Authentication</span>
</span></span><span class="line"><span class="cl">   <span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Na Parte 1 tínhamos um &ldquo;<tt>plug :authenticate</tt>&rdquo; mais simples no EventsController. Podemos remover ele e também a função &ldquo;<tt>authenticate/2</tt>&rdquo;. Refatoramos tudo pra uma função melhor que agora também serve à autenticação de administração, mas a ideia é a mesma.</p>
<p>É isso: o básico de autenticação de API. De novo, esta não é a melhor solução porque o par username/password vai na URL e fica aberto a ataques de man-in-the-middle. SSL só criptografa o body do HTTP mas a URL fica aberta.</p>
<p>Por exemplo, se um administrador quiser criar uma nova aplicação, precisa fazer assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>curl --data &#34;app[name]=foo-app&#34; http://pusher_admin_username:pusher_admin_password@localhost:4000/api/admin/apps</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E este seria um exemplo da representação JSON resultante do novo app:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>{&#34;data&#34;:{&#34;slug&#34;:&#34;foo-app&#34;,&#34;secret&#34;:&#34;8ef69064-0d7e-c9ef-ac14-b6b1db303e7a&#34;,&#34;name&#34;:&#34;foo-app&#34;,&#34;key&#34;:&#34;9400ad21-eed8-117a-bce5-845262e0a09e&#34;,&#34;id&#34;:5,&#34;active&#34;:true}}%</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Com essa nova key e secret em mãos, podemos atualizar nosso client demo pra usar o novo app.</p>
<h3>Configurando o Client Demo<span class="hx:absolute hx:-mt-20" id="configurando-o-client-demo"></span>
    <a href="#configurando-o-client-demo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Vamos começar adicionando os detalhes apropriados da Aplicação no arquivo &ldquo;<tt>.env</tt>&rdquo;:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>PUSHER_URL: &#34;localhost:4000&#34;
PUSHER_APP_ID: &#34;foo-app&#34;
PUSHER_KEY: &#34;9400ad21-eed8-117a-bce5-845262e0a09e&#34;
PUSHER_SECRET: &#34;8ef69064-0d7e-c9ef-ac14-b6b1db303e7a&#34;
PUSHER_CHANNEL: &#34;foo-topic&#34;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Também temos que ajustar o &ldquo;<tt>config/secrets.yml</tt>&rdquo; pra refletir os novos metadados (development, test e production precisam seguir isso):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>development:
  secret_key_base: ded7c4a2a298c1b620e462b50c9ca6ccb60130e27968357e76cab73de9858f14556a26df885c8aa5004d0a7ca79c0438e618557275bdb28ba67a0ffb0c268056
  pusher_url: &lt;%= ENV[&#39;PUSHER_URL&#39;] %&gt;
  pusher_app_id: &lt;%= ENV[&#39;PUSHER_APP_ID&#39;] %&gt;
  pusher_key: &lt;%= ENV[&#39;PUSHER_KEY&#39;] %&gt;
  pusher_secret: &lt;%= ENV[&#39;PUSHER_SECRET&#39;] %&gt;
  pusher_channel: &lt;%= ENV[&#39;PUSHER_CHANNEL&#39;] %&gt;
  ...</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E podemos criar um initializer pra facilitar o uso desses metadados:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># config/initializers/pusher_lite.rb</span>
</span></span><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">PusherLite</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">uri</span>
</span></span><span class="line"><span class="cl">    <span class="n">key</span>    <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">secrets</span><span class="o">.</span><span class="n">pusher_key</span>
</span></span><span class="line"><span class="cl">    <span class="n">secret</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">secrets</span><span class="o">.</span><span class="n">pusher_secret</span>
</span></span><span class="line"><span class="cl">    <span class="n">app_id</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">secrets</span><span class="o">.</span><span class="n">pusher_app_id</span>
</span></span><span class="line"><span class="cl">    <span class="n">url</span>    <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">secrets</span><span class="o">.</span><span class="n">pusher_url</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">uri</span> <span class="o">=</span> <span class="s2">&#34;http://</span><span class="si">#{</span><span class="n">key</span><span class="si">}</span><span class="s2">:</span><span class="si">#{</span><span class="n">secret</span><span class="si">}</span><span class="s2">@</span><span class="si">#{</span><span class="n">url</span><span class="si">}</span><span class="s2">/api/apps/</span><span class="si">#{</span><span class="n">app_id</span><span class="si">}</span><span class="s2">/events&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="no">URI</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">uri</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span> </span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>De novo, o app Rails vai disparar o servidor ExPusherLite usando o Basic HTTP Auth. Não se iluda achando que isso é &ldquo;seguro&rdquo;, apenas &ldquo;parece um pouco seguro pela obscuridade&rdquo;. Você foi avisado, espere os próximos artigos sobre o assunto. Mas isso é utilizável em ambientes controlados.</p>
<p>Pra finalizar os upgrades, temos que mudar o acesso do lado cliente aos novos metadados, primeiro mudando o layout da aplicação:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="c">&lt;!-- app/views/layouts/application.html.erb --&gt;</span>
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">+  <span class="p">&lt;</span><span class="nt">meta</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;pusher_host&#34;</span> <span class="na">content</span><span class="o">=</span><span class="s">&#34;&lt;%= Rails.application.secrets.pusher_url %&gt;&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">-  <span class="p">&lt;</span><span class="nt">meta</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;pusher_key&#34;</span> <span class="na">content</span><span class="o">=</span><span class="s">&#34;&lt;%= Rails.application.secrets.pusher_key %&gt;&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">+  <span class="p">&lt;</span><span class="nt">meta</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;pusher_app_id&#34;</span> <span class="na">content</span><span class="o">=</span><span class="s">&#34;&lt;%= Rails.application.secrets.pusher_app_id %&gt;&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">   <span class="p">&lt;</span><span class="nt">meta</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;pusher_channel&#34;</span> <span class="na">content</span><span class="o">=</span><span class="s">&#34;&lt;%= Rails.application.secrets.pusher_channel %&gt;&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">...</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O javascript &ldquo;<tt>index.es6</tt>&rdquo; busca esses meta headers, então temos que mudar lá também:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="err">#</span> <span class="nx">app</span><span class="o">/</span><span class="nx">assets</span><span class="o">/</span><span class="nx">javascripts</span><span class="o">/</span><span class="nx">application</span><span class="o">/</span><span class="nx">pages</span><span class="o">/</span><span class="nx">home</span><span class="o">/</span><span class="nx">index</span><span class="p">.</span><span class="nx">es6</span>
</span></span><span class="line"><span class="cl"><span class="p">...</span>
</span></span><span class="line"><span class="cl">     <span class="kd">let</span> <span class="nx">guardianToken</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s2">&#34;meta[name=guardian-token]&#34;</span><span class="p">).</span><span class="nx">attr</span><span class="p">(</span><span class="s2">&#34;content&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">     <span class="kd">let</span> <span class="nx">csrfToken</span>     <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s2">&#34;meta[name=guardian-csrf]&#34;</span><span class="p">).</span><span class="nx">attr</span><span class="p">(</span><span class="s2">&#34;content&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> 
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="kd">let</span> <span class="nx">pusherHost</span>    <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s2">&#34;meta[name=pusher_host]&#34;</span><span class="p">).</span><span class="nx">attr</span><span class="p">(</span><span class="s2">&#34;content&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="o">-</span>    <span class="kd">let</span> <span class="nx">pusherKey</span>     <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s2">&#34;meta[name=pusher_key]&#34;</span><span class="p">).</span><span class="nx">attr</span><span class="p">(</span><span class="s2">&#34;content&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="kd">let</span> <span class="nx">pusherApp</span>     <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s2">&#34;meta[name=pusher_app_id]&#34;</span><span class="p">).</span><span class="nx">attr</span><span class="p">(</span><span class="s2">&#34;content&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">     <span class="kd">let</span> <span class="nx">pusherChannel</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s2">&#34;meta[name=pusher_channel]&#34;</span><span class="p">).</span><span class="nx">attr</span><span class="p">(</span><span class="s2">&#34;content&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> 
</span></span><span class="line"><span class="cl"><span class="o">-</span>    <span class="kd">let</span> <span class="nx">socket</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Socket</span><span class="p">(</span><span class="s2">&#34;ws://localhost:4000/socket&#34;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="kd">let</span> <span class="nx">socket</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Socket</span><span class="p">(</span><span class="sb">`ws://</span><span class="si">${</span><span class="nx">pusherHost</span><span class="si">}</span><span class="sb">/socket`</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">       <span class="nx">params</span><span class="o">:</span> <span class="p">{</span> <span class="nx">guardian_token</span><span class="o">:</span> <span class="nx">guardianToken</span><span class="p">,</span> <span class="nx">csrf_token</span><span class="o">:</span> <span class="nx">csrfToken</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">     <span class="p">})</span>
</span></span><span class="line"><span class="cl">     <span class="nx">socket</span><span class="p">.</span><span class="nx">connect</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"> 
</span></span><span class="line"><span class="cl">     <span class="c1">// Agora que está conectado, dá pra entrar em canais com um topic:
</span></span></span><span class="line"><span class="cl"><span class="o">-</span>    <span class="kd">let</span> <span class="nx">channel</span> <span class="o">=</span> <span class="nx">socket</span><span class="p">.</span><span class="nx">channel</span><span class="p">(</span><span class="nx">pusherChannel</span><span class="p">,</span> <span class="p">{})</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="kd">let</span> <span class="nx">channel</span> <span class="o">=</span> <span class="nx">socket</span><span class="p">.</span><span class="nx">channel</span><span class="p">(</span><span class="sb">`public:</span><span class="si">${</span><span class="nx">pusherApp</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span> <span class="p">{})</span>
</span></span><span class="line"><span class="cl">     <span class="nx">channel</span><span class="p">.</span><span class="nx">join</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">       <span class="p">.</span><span class="nx">receive</span><span class="p">(</span><span class="s2">&#34;ok&#34;</span><span class="p">,</span> <span class="nx">resp</span> <span class="p">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">&#34;Joined successfully&#34;</span><span class="p">,</span> <span class="nx">resp</span><span class="p">)</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">       <span class="p">.</span><span class="nx">receive</span><span class="p">(</span><span class="s2">&#34;error&#34;</span><span class="p">,</span> <span class="nx">resp</span> <span class="p">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">&#34;Unable to join&#34;</span><span class="p">,</span> <span class="nx">resp</span><span class="p">)</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl"> 
</span></span><span class="line"><span class="cl"><span class="o">-</span>    <span class="nx">channel</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s2">&#34;msg&#34;</span><span class="p">,</span> <span class="nx">data</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="nx">channel</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="sb">`</span><span class="si">${</span><span class="nx">pusherChannel</span><span class="si">}</span><span class="sb">:msg`</span><span class="p">,</span> <span class="nx">data</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">       <span class="kd">let</span> <span class="nx">new_line</span> <span class="o">=</span> <span class="sb">`&lt;p&gt;&lt;strong&gt;</span><span class="si">${</span><span class="nx">data</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span><span class="sb">&lt;strong&gt;: </span><span class="si">${</span><span class="nx">data</span><span class="p">.</span><span class="nx">message</span><span class="si">}</span><span class="sb">&lt;/p&gt;`</span>
</span></span><span class="line"><span class="cl">       <span class="nx">$</span><span class="p">(</span><span class="s2">&#34;.message-receiver&#34;</span><span class="p">).</span><span class="nx">append</span><span class="p">(</span><span class="nx">new_line</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">     <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="nx">channel</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s2">&#34;msg&#34;</span><span class="p">,</span> <span class="nx">data</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>      <span class="kd">let</span> <span class="nx">new_line</span> <span class="o">=</span> <span class="sb">`&lt;p&gt;&lt;strong&gt;Broadcast to all channels&lt;/strong&gt;: </span><span class="si">${</span><span class="nx">data</span><span class="p">.</span><span class="nx">message</span><span class="si">}</span><span class="sb">&lt;/p&gt;`</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>      <span class="nx">$</span><span class="p">(</span><span class="s2">&#34;.message-receiver&#34;</span><span class="p">).</span><span class="nx">append</span><span class="p">(</span><span class="nx">new_line</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="p">})</span>
</span></span><span class="line"><span class="cl">   <span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Uma modificação importante em relação à Parte 1 é que o host do WebSocket estava hardcoded em &ldquo;localhost&rdquo; e aqui estamos tornando ele configurável via meta tags. No momento, pros testes em localhost, estamos usando o protocolo &ldquo;ws://&rdquo; puro, mas quando fizermos deploy no Heroku vamos mudar pra &ldquo;wss://&rdquo; pra ter SSL. Mesma coisa pro initializer &ldquo;PusherLite&rdquo;. Tenha isso em mente.</p>
<p>Agora ele está se inscrevendo num formato diferente de topic/channel. Na Parte 1 era algo como: &ldquo;<tt>public:test_channel</tt>&rdquo;, e agora estamos escutando em &ldquo;<tt>public:foo-app</tt>&rdquo;, então a aplicação é o &ldquo;topic&rdquo; da inscrição no Websocket.</p>
<p>Aí estamos mudando o socket listener pra escutar 2 events diferentes. O primeiro está no formato &ldquo;<tt>test_channel:msg</tt>&rdquo;. É assim que agora temos que enviar mensagens pra um &ldquo;channel&rdquo; específico dentro de um &ldquo;app/topic&rdquo;.</p>
<p>E por último ainda escutamos o velho event &ldquo;msg&rdquo;, mas ele agora serve como um event de &ldquo;broadcast&rdquo; pra todos os clientes conectados inscritos nessa Aplicação &ldquo;foo-app&rdquo; em particular. Agora os clientes web podem escutar &ldquo;channels&rdquo; específicos dentro do &ldquo;app&rdquo; mas também receber mensagens de &ldquo;broadcast&rdquo; globais. Isso é uma melhoria grande e quase não exigiu coisa do lado do Javascript.</p>
<p>Mas o que mais é preciso pra fazer esse sistema <strong>&ldquo;channel-only and broadcast&rdquo;</strong> funcionar? Primeiro, começamos mudando o formulário web pra deixar o usuário escolher entre enviar uma mensagem só de canal ou um broadcast, assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="c">&lt;!-- app/views/home/index.html.erb --&gt;</span>
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">     <span class="err">&lt;</span>%= f.text_field :name, placeholder: &#34;Name&#34; %&gt;
</span></span><span class="line"><span class="cl">     <span class="err">&lt;</span>%= f.text_field :message, placeholder: &#34;Message&#34; %&gt;
</span></span><span class="line"><span class="cl">+    <span class="err">&lt;</span>%= f.check_box :broadcast %&gt;
</span></span><span class="line"><span class="cl">     <span class="err">&lt;</span>%= f.submit &#34;Send message&#34;, class: &#34;pure-button pure-button-primary&#34; %&gt;
</span></span><span class="line"><span class="cl">   <span class="p">&lt;/</span><span class="nt">fieldset</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">...</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora o EventsController precisa aceitar esse novo parâmetro:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># app/controllers/events_controller.rb</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">event_params</span>
</span></span><span class="line"><span class="cl">    <span class="n">params</span><span class="o">.</span><span class="n">require</span><span class="p">(</span><span class="ss">:pusher_event</span><span class="p">)</span><span class="o">.</span><span class="n">permit</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:message</span><span class="p">,</span> <span class="ss">:broadcast</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Por fim, o Model precisa usar essa nova informação antes de fazer o post pro servidor ExPusherLite:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># app/models/pusher_event.rb</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">PusherEvent</span>
</span></span><span class="line"><span class="cl">  <span class="kp">include</span> <span class="no">ActiveModel</span><span class="o">::</span><span class="no">Model</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kp">attr_accessor</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">:message</span><span class="p">,</span> <span class="ss">:broadcast</span>
</span></span><span class="line"><span class="cl">  <span class="n">validates</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">:message</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">save</span>
</span></span><span class="line"><span class="cl">    <span class="n">topic</span> <span class="o">=</span> <span class="k">if</span> <span class="n">broadcast</span> <span class="o">==</span> <span class="s2">&#34;1&#34;</span>
</span></span><span class="line"><span class="cl">              <span class="s2">&#34;#general&#34;</span>
</span></span><span class="line"><span class="cl">            <span class="k">else</span>
</span></span><span class="line"><span class="cl">              <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">secrets</span><span class="o">.</span><span class="n">pusher_channel</span>
</span></span><span class="line"><span class="cl">            <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="o">.</span><span class="n">post_form</span><span class="p">(</span><span class="no">PusherLite</span><span class="o">.</span><span class="n">uri</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;topic&#34;</span> <span class="o">=&gt;</span> <span class="n">topic</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;event&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;msg&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;scope&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;public&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;payload&#34;</span> <span class="o">=&gt;</span> <span class="p">{</span><span class="s2">&#34;name&#34;</span> <span class="o">=&gt;</span> <span class="nb">name</span><span class="p">,</span> <span class="s2">&#34;message&#34;</span> <span class="o">=&gt;</span> <span class="n">message</span><span class="p">}</span><span class="o">.</span><span class="n">to_json</span>
</span></span><span class="line"><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Estou apenas assumindo uma string hardcoded &ldquo;<tt>#general</tt>&rdquo; pra servir de gatilho de broadcast pro servidor. Agora temos que fazer o servidor aceitar esse novo schema de protocolo, então vamos voltar pro Elixir.</p>
<p>Primeiro temos que começar com a contraparte do trigger POST anterior, <tt>ExPusherLite.EventsController</tt>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># web/controllers/events_controller.ex</span>
</span></span><span class="line"><span class="cl"> <span class="n">defmodule</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">EventsController</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">   <span class="n">use</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Web</span><span class="p">,</span> <span class="ss">:controller</span>
</span></span><span class="line"><span class="cl"> 
</span></span><span class="line"><span class="cl"><span class="o">-</span>  <span class="n">plug</span> <span class="ss">:authenticate</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>  <span class="n">plug</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Authentication</span>
</span></span><span class="line"><span class="cl"> 
</span></span><span class="line"><span class="cl"><span class="o">-</span>  <span class="k">def</span> <span class="nf">create</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="n">params</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl"><span class="o">-</span>    <span class="n">topic</span> <span class="o">=</span> <span class="n">params</span><span class="o">[</span><span class="s2">&#34;topic&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="o">-</span>    <span class="n">event</span> <span class="o">=</span> <span class="n">params</span><span class="o">[</span><span class="s2">&#34;event&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>  <span class="k">def</span> <span class="nf">create</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="sx">%{&#34;app_slug&#34; =&gt; app_slug, &#34;event&#34; =&gt; event, &#34;topic&#34; =&gt; topic, &#34;scope&#34; =&gt; scope}</span> <span class="o">=</span> <span class="n">params</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">     <span class="n">message</span> <span class="o">=</span> <span class="p">(</span><span class="n">params</span><span class="o">[</span><span class="s2">&#34;payload&#34;</span><span class="o">]</span> <span class="o">||</span> <span class="s2">&#34;{}&#34;</span><span class="p">)</span> <span class="o">|&gt;</span> <span class="no">Poison</span><span class="o">.</span><span class="n">decode!</span>
</span></span><span class="line"><span class="cl"><span class="o">-</span>    <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Endpoint</span><span class="o">.</span><span class="n">broadcast!</span> <span class="n">topic</span><span class="p">,</span> <span class="n">event</span><span class="p">,</span> <span class="n">message</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="n">topic_event</span> <span class="o">=</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>      <span class="k">if</span> <span class="n">topic</span> <span class="o">==</span> <span class="s2">&#34;#general&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>        <span class="n">event</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>      <span class="k">else</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>        <span class="s2">&#34;</span><span class="si">#{</span><span class="n">topic</span><span class="si">}</span><span class="s2">:</span><span class="si">#{</span><span class="n">event</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>      <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Endpoint</span><span class="o">.</span><span class="n">broadcast!</span> <span class="s2">&#34;</span><span class="si">#{</span><span class="n">scope</span><span class="si">}</span><span class="s2">:</span><span class="si">#{</span><span class="n">app_slug</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">topic_event</span><span class="p">,</span> <span class="n">message</span>
</span></span><span class="line"><span class="cl">     <span class="n">json</span> <span class="n">conn</span><span class="p">,</span> <span class="sx">%{}</span>
</span></span><span class="line"><span class="cl">   <span class="k">end</span>
</span></span><span class="line"><span class="cl">   <span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A primeira diferença é que estou fazendo pattern match direto dos argumentos pras variáveis &ldquo;topic&rdquo; e &ldquo;event&rdquo;. Essa função também sabe da string &ldquo;<tt>#general</tt>&rdquo; que o cliente pode enviar pra indicar um broadcast global do app. E o novo topic é a concatenação de &ldquo;topic&rdquo; e &ldquo;event&rdquo; pra permitir mensagens &ldquo;channel-only&rdquo;.</p>
<p>Pra ligar tudo isso ao handler de WebSocket, temos que fazer as seguintes mudanças:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># web/channels/room_channel.ex</span>
</span></span><span class="line"><span class="cl"><span class="o">-</span>  <span class="k">def</span> <span class="nf">handle_in</span><span class="p">(</span><span class="s2">&#34;msg&#34;</span><span class="p">,</span> <span class="n">payload</span><span class="p">,</span> <span class="n">socket</span> <span class="o">=</span> <span class="sx">%{ topic: &#34;public:&#34; &lt;&gt; _ }</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl"><span class="o">-</span>    <span class="n">broadcast</span> <span class="n">socket</span><span class="p">,</span> <span class="s2">&#34;msg&#34;</span><span class="p">,</span> <span class="n">payload</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>  <span class="k">def</span> <span class="nf">handle_in</span><span class="p">(</span><span class="n">topic_event</span><span class="p">,</span> <span class="n">payload</span><span class="p">,</span> <span class="n">socket</span> <span class="o">=</span> <span class="sx">%{ topic: &#34;public:&#34; &lt;&gt; _ }</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="n">broadcast</span> <span class="n">socket</span><span class="p">,</span> <span class="n">topic_event</span><span class="p">,</span> <span class="n">payload</span>
</span></span><span class="line"><span class="cl">     <span class="p">{</span> <span class="ss">:noreply</span><span class="p">,</span> <span class="n">socket</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">   <span class="k">end</span>
</span></span><span class="line"><span class="cl"> 
</span></span><span class="line"><span class="cl"><span class="o">-</span>  <span class="k">def</span> <span class="nf">handle_in</span><span class="p">(</span><span class="s2">&#34;msg&#34;</span><span class="p">,</span> <span class="n">payload</span><span class="p">,</span> <span class="n">socket</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>  <span class="k">def</span> <span class="nf">handle_in</span><span class="p">(</span><span class="n">topic_event</span><span class="p">,</span> <span class="n">payload</span><span class="p">,</span> <span class="n">socket</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">     <span class="n">claims</span> <span class="o">=</span> <span class="no">Guardian</span><span class="o">.</span><span class="n">Channel</span><span class="o">.</span><span class="n">claims</span><span class="p">(</span><span class="n">socket</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">     <span class="k">if</span> <span class="n">permitted_topic?</span><span class="p">(</span><span class="n">claims</span><span class="o">[</span><span class="ss">:publish</span><span class="o">]</span><span class="p">,</span> <span class="n">socket</span><span class="o">.</span><span class="n">topic</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl"><span class="o">-</span>      <span class="n">broadcast</span> <span class="n">socket</span><span class="p">,</span> <span class="s2">&#34;msg&#34;</span><span class="p">,</span> <span class="n">payload</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>      <span class="n">broadcast</span> <span class="n">socket</span><span class="p">,</span> <span class="n">topic_event</span><span class="p">,</span> <span class="n">payload</span>
</span></span><span class="line"><span class="cl">       <span class="p">{</span> <span class="ss">:noreply</span><span class="p">,</span> <span class="n">socket</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">   <span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora o Channel já não faz pattern match num event específico, deixa passar sem mais validação, confiando que o EventsController está fazendo a coisa certa. Talvez eu volte nessa parte pra melhorias no futuro.</p>
<h3>Fazendo deploy do nosso primeiro app Phoenix no Heroku!<span class="hx:absolute hx:-mt-20" id="fazendo-deploy-do-nosso-primeiro-app-phoenix-no-heroku"></span>
    <a href="#fazendo-deploy-do-nosso-primeiro-app-phoenix-no-heroku" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Nesta seção vamos só seguir a <a href="http://www.phoenixframework.org/docs/heroku"target="_blank" rel="noopener">documentação oficial</a>, então leia lá se quiser mais detalhes.</p>
<p>Vamos começar:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>heroku apps:create your-expusherlite --buildpack &#34;https://github.com/HashNuke/heroku-buildpack-elixir.git&#34;
heroku buildpacks:add https://github.com/gjaldon/heroku-buildpack-phoenix-static.git</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Estou nomeando a aplicação como &ldquo;your-expusherlite&rdquo; mas você deve trocar pelo seu próprio nome, claro. E o resto dos dados de configuração são todos exemplos que você precisa mudar pras suas próprias necessidades.</p>
<p>O Heroku depende de variáveis de ambiente. Então começamos apagando &ldquo;<tt>config/prod.secret.exs</tt>&rdquo; e mudando o &ldquo;<tt>config/prod.exs</tt>&rdquo; pra ficar assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">config</span> <span class="ss">:ex_pusher_lite</span><span class="p">,</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Endpoint</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">http</span><span class="p">:</span> <span class="o">[</span><span class="ss">port</span><span class="p">:</span> <span class="p">{</span><span class="ss">:system</span><span class="p">,</span> <span class="s2">&#34;PORT&#34;</span><span class="p">}</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">url</span><span class="p">:</span> <span class="o">[</span><span class="ss">scheme</span><span class="p">:</span> <span class="s2">&#34;https&#34;</span><span class="p">,</span> <span class="ss">host</span><span class="p">:</span> <span class="s2">&#34;your-expusherlite.herokuapp.com&#34;</span><span class="p">,</span> <span class="ss">port</span><span class="p">:</span> <span class="mi">443</span><span class="o">]</span><span class="p">,</span> <span class="ss">force_ssl</span><span class="p">:</span> <span class="o">[</span><span class="ss">rewrite_on</span><span class="p">:</span> <span class="o">[</span><span class="ss">:x_forwarded_proto</span><span class="o">]]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">cache_static_manifest</span><span class="p">:</span> <span class="s2">&#34;priv/static/manifest.json&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">secret_key_base</span><span class="p">:</span> <span class="no">System</span><span class="o">.</span><span class="n">get_env</span><span class="p">(</span><span class="s2">&#34;SECRET_KEY_BASE&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Configure seu banco</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span> <span class="ss">:ex_pusher_lite</span><span class="p">,</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Repo</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">adapter</span><span class="p">:</span> <span class="no">Ecto</span><span class="o">.</span><span class="n">Adapters</span><span class="o">.</span><span class="n">Postgres</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">url</span><span class="p">:</span> <span class="no">System</span><span class="o">.</span><span class="n">get_env</span><span class="p">(</span><span class="s2">&#34;DATABASE_URL&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="ss">pool_size</span><span class="p">:</span> <span class="mi">20</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">config</span> <span class="ss">:ex_pusher_lite</span><span class="p">,</span> <span class="ss">:admin_authentication</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">username</span><span class="p">:</span> <span class="no">System</span><span class="o">.</span><span class="n">get_env</span><span class="p">(</span><span class="s2">&#34;PUSHER_ADMIN_USERNAME&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="ss">password</span><span class="p">:</span> <span class="no">System</span><span class="o">.</span><span class="n">get_env</span><span class="p">(</span><span class="s2">&#34;PUSHER_ADMIN_PASSWORD&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># remova esta linha:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># import_config &#34;prod.secret.exs&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora temos que configurar as variáveis de ambiente &ldquo;SECRET_KEY_BASE&rdquo;, &ldquo;PUSHER_ADMIN_USERNAME&rdquo; e &ldquo;PUSHER_ADMIN_PASSWORD&rdquo;. Use o &ldquo;<tt>mix phoenix.gen.secret</tt>&rdquo; que já vem incluído pra gerar elas.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>heroku config:set SECRET_KEY_BASE=&#34;`mix phoenix.gen.secret`&#34;
heroku config:set PUSHER_ADMIN_USERNAME=&#34;FPO0QUkqbAP6EGjElqBzDQuMs8bhFS3&#34;
heroku config:set PUSHER_ADMIN_PASSWORD=&#34;n78DPGmK3DBQy8YAVyshiGqcXjjSXSD&#34;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Aí é só esperar o velho e bom &ldquo;<tt>git push heroku master</tt>&rdquo; terminar de compilar tudo da primeira vez. E como é o primeiro deploy, não esqueça de rodar &ldquo;<tt>heroku run mix ecto.migrate</tt>&rdquo; pra criar a tabela do banco.</p>
<p>Agora, se eu fiz tudo direito, como Administrador que conhece os secrets hardcoded acima eu deveria conseguir criar uma nova Aplicação assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>curl --data &#34;app[name]=shiny-new-app&#34; https://FPO0QUkqbAP6EGjElqBzDQuMs8bhFS3:n78DPGmK3DBQy8YAVyshiGqcXjjSXSD@your-expusherlite.herokuapp.com/api/admin/apps</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E foi este o resultado que eu obtive!</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>{&#34;data&#34;:{&#34;slug&#34;:&#34;shiny-new-app&#34;,&#34;secret&#34;:&#34;42560373-0fe1-506e-28ca-35ab5221fb3d&#34;,&#34;name&#34;:&#34;shiny-new-app&#34;,&#34;key&#34;:&#34;958c16e7-ab93-dac0-0fc6-6cb864e26358&#34;,&#34;id&#34;:1,&#34;active&#34;:true}}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Beleza, agora que temos uma key e secret válidos da Aplicação podemos configurar nosso Rails Client Demo e fazer deploy dele no Heroku também.</p>
<h3>Fazendo deploy do Cliente Rails no Heroku<span class="hx:absolute hx:-mt-20" id="fazendo-deploy-do-cliente-rails-no-heroku"></span>
    <a href="#fazendo-deploy-do-cliente-rails-no-heroku" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Esta é uma aplicação Rails simples, podemos só criar o app e fazer deploy direto:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>heroku create your-expusherlite-client
heroku config:set PUSHER_URL=your-expusherlite.herokuapp.com
heroku config:set PUSHER_APP_ID=shiny-new-app
heroku config:set PUSHER_KEY=958c16e7-ab93-dac0-0fc6-6cb864e26358
heroku config:set PUSHER_SECRET=42560373-0fe1-506e-28ca-35ab5221fb3d
heroku config:set PUSHER_CHANNEL=shiny-new-topic
git push heroku master</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Estou assumindo que os leitores deste post já sabem como configurar uma app Rails apropriadamente pro Heroku. Só pra mencionar, eu configurei essa app com as gems 12 factor e Puma e adicionei um Procfile apropriado. Outra mudança bem pequena foi alterar o initializer &ldquo;pusher_lite.rb&rdquo; pra criar uma URI com &ldquo;https&rdquo; porque o ExPusherLite que fizemos deploy em produção exige SSL por padrão.</p>
<p>Tem mais um detalhe. Por ser conduzido por programadores web experientes, eles garantiram que, ao contrário deste exercício bem cru aqui, o framework Phoenix em si é seguro. Um exemplo é proibir conexões Websocket vindas de hosts diferentes.</p>
<p>Por padrão, o Socket do &ldquo;phoenix.js&rdquo; vai falhar a conexão quando tentarmos conectar do host do app Rails &ldquo;your-expusherlite-client.herokuapp.com&rdquo; pro app Phoenix em &ldquo;your-expusherlite.herokuapp.com&rdquo; com o seguinte erro:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>WebSocket connection to &#39;wss://your-expusherlite.herokuapp.com/socket/websocket?guardian_token=N_YCG6hGK7…iOlsicHVibGljOioiXX0._j6s2LiaKde9rBhnTMxDkm0XV5u89pNh1AdLFY6Rlt8&amp;vsn=1.0.0&#39; failed: Error during WebSocket handshake: Unexpected response code: 403</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E no log do Phoenix vamos ver esta mensagem bem útil:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>[error] Could not check origin for Phoenix.Socket transport.

This happens when you are attempting a socket connection to
a different host than the one configured in your config/
files. For example, in development the host is configured
to &#34;localhost&#34; but you may be trying to access it from
&#34;127.0.0.1&#34;. To fix this issue, you may either:

  1. update [url: [host: ...]] to your actual host in the
     config file for your current environment (recommended)

  2. pass the :check_origin option when configuring your
     endpoint or when configuring the transport in your
     UserSocket module, explicitly outlining which origins
     are allowed:

        check_origin: [&#34;https://example.com&#34;,
                       &#34;//another.com:888&#34;, &#34;//other.com&#34;]</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A menos que você saiba o que é um <a href="https://www.christian-schneider.net/CrossSiteWebSocketHijacking.html"target="_blank" rel="noopener">Cross-Site Web Socket Hijacking</a>, é melhor deixar as configurações padrão como estão. Numa app Phoenix novinha, a parte Web vai conectar no Web Socket dentro da mesma app e, portanto, no mesmo host, então isso não é problema.</p>
<p>No nosso caso eu estou fazendo um micro-serviço separado pra mimetizar o comportamento do Pusher.com, então ele precisa aceitar conexões de Web Socket de hosts diferentes.</p>
<p>Se você controla as aplicações que vão ser criadas, provavelmente vai preferir fazer a config &ldquo;<tt>check_origin</tt>&rdquo; ler do banco os hosts exatos. Como feature pra próxima vez eu poderia adicionar um campo &ldquo;host&rdquo; no model &ldquo;App&rdquo; e usar pra validar as conexões na configuração do transport. Por enquanto vou só fazer ele aceitar qualquer host:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># web/channels/user_socket.ex
defmodule ExPusherLite.UserSocket do
  use Phoenix.Socket

  ## Channels
  channel &#34;*&#34;, ExPusherLite.RoomChannel

  ## Transports
  transport :websocket, Phoenix.Transports.WebSocket, check_origin: false
  ...</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E é isso! Agora a app Rails deve conseguir conectar e enviar mensagens! E você deve conseguir criar quantos apps quiser e ligar todos eles a este mesmo serviço.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/534/big_Screenshot_from_2015-12-14_14_58_22.png" alt="Final Heroku Client"  loading="lazy" /></p>
<h3>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Neste momento, temos um clone funcional, embora bem cru, do Pusher.com que serve pra um monte de casos de uso onde o Pusher.com seria usado.</p>
<p>Como avisei várias vezes, a parte de segurança ainda está bamba e precisa de trabalho. Vou ainda estender o que o Daniel começou com o Guardian pra também autenticar usuários do Web Socket em canais privados. E o core também deve receber capacidades de auditoria e relatório (pra reportar uso, número de conexões ativas, throughput de eventos, manter pelo menos um histórico curto de eventos pras conexões novas conseguirem recuperar as últimas mensagens enviadas, e por aí vai).</p>
<p>Mas daqui pra frente é só uma questão de adicionar features a um core que já funciona. E isso é nada mais que Phoenix out-of-the-box sem grande coisa adicionada por cima! Diz muito sobre o atual estado de maturidade desse framework tão capaz.</p>
<p>Em termos de performance, pra este exemplo simplíssimo, eu subi 1 dyno free do Heroku pra cada app.</p>
<p>A app Rails consegue responder à interface front-end em torno de 2ms. E o job do Sucker Punch - que faz o HTTP POST pesado pro ExPusherLite - leva na ordem de 30ms ou menos.</p>
<p>O servidor Phoenix recebe o HTTP POST e faz o broadcast em torno de menos de 6ms. Também bem rápido. Os tempos vão variar bastante porque eu acredito que o dyno free é não só lento mas também fica em metal boxes muito compartilhadas, sofrendo impacto de outras apps vizinhas rodando na mesma máquina.</p>
<p>Como já temos uma API administrativa pra criar e gerenciar apps (criar novas, deletar, atualizar etc.) já dá pra criar uma aplicação separada em qualquer outro framework pra fazer um dashboard pra admins ou uma front-end self-service pros desenvolvedores registrarem novas apps e receberem o par key/secret pra adicionar nas suas próprias aplicações.</p>
<p>Tanto o servidor ExPusherLite quanto o cliente demo estão em deploy no Heroku e você pode testar o cliente agora mesmo <a href="https://expusherlite-client.herokuapp.com/"target="_blank" rel="noopener">clicando aqui</a>. As keys de admin são diferentes das que mostrei neste post, claro, então você não vai conseguir criar apps novas, mas pode fazer deploy você mesmo no seu próprio ambiente.</p>
]]></content:encoded><category>elixir-e-erlang</category><category>sistemas-distribuidos</category><category>devops</category></item><item><title>[Small Bites] Brincando com o Jelastic Cloud da Locaweb</title><link>https://akitaonrails.github.io/2015/04/28/small-bites-brincando-com-o-jelastic-cloud-da-locaweb/</link><guid isPermaLink="true">https://akitaonrails.github.io/2015/04/28/small-bites-brincando-com-o-jelastic-cloud-da-locaweb/</guid><pubDate>Tue, 28 Apr 2015 12:00:00 -0300</pubDate><description>&lt;p&gt;Se você participou da &lt;a href="https://www.eventials.com/locaweb/groups/rubyconf-brasil-2014-by-locaweb-2/"target="_blank" rel="noopener"&gt;Rubyconf Brasil em 2014&lt;/a&gt; teve a oportunidade de ver em primeira mão o lançamento do produto Jelastic Cloud, da Locaweb, na época com suporte beta a Ruby.&lt;/p&gt;
&lt;p&gt;Ontem brinquei com o Jelastic com a ajuda do camarada &lt;a href="https://twitter.com/kemelzaidan"target="_blank" rel="noopener"&gt;Kemel Zaidan&lt;/a&gt;, evangelista de tecnologia da Locaweb e colocamos o site do Call for Papers para a Rubyconf Brasil deste ano no ar por lá.&lt;/p&gt;
&lt;p&gt;Como há alguns truques, resolvi fazer um pequeno post para dar uma idéia do que é possível fazer. Diferente de VPS ou similares, o Jelastic é um Platform as a Service (PaaS) que oferece tanto escalabilidade horizontal (mais servidores embaixo de um balanceador de carga) ou também escalabilidade vertical (mais recursos como memória e CPU mais rápido na mesma máquina). Além disso ele pré-configura quase tudo para que você não tenha que lidar com coisas como configurar o NGINX como proxy reverso para balancear carga ou configurar o PostgreSQL.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Se você participou da <a href="https://www.eventials.com/locaweb/groups/rubyconf-brasil-2014-by-locaweb-2/"target="_blank" rel="noopener">Rubyconf Brasil em 2014</a> teve a oportunidade de ver em primeira mão o lançamento do produto Jelastic Cloud, da Locaweb, na época com suporte beta a Ruby.</p>
<p>Ontem brinquei com o Jelastic com a ajuda do camarada <a href="https://twitter.com/kemelzaidan"target="_blank" rel="noopener">Kemel Zaidan</a>, evangelista de tecnologia da Locaweb e colocamos o site do Call for Papers para a Rubyconf Brasil deste ano no ar por lá.</p>
<p>Como há alguns truques, resolvi fazer um pequeno post para dar uma idéia do que é possível fazer. Diferente de VPS ou similares, o Jelastic é um Platform as a Service (PaaS) que oferece tanto escalabilidade horizontal (mais servidores embaixo de um balanceador de carga) ou também escalabilidade vertical (mais recursos como memória e CPU mais rápido na mesma máquina). Além disso ele pré-configura quase tudo para que você não tenha que lidar com coisas como configurar o NGINX como proxy reverso para balancear carga ou configurar o PostgreSQL.</p>
<p>Criar uma conta trial é simples, você recebe sua senha por email e pode entrar no dashboard. De lá basta configurar a topologia da sua aplicação que te dá opção de escolher a tecnologia (Java, PHP, Ruby, Node.js, Python, .NET). No caso de Ruby você pode escolher direto a opção do <a href="http://www.akitaonrails.com/2014/10/19/o-novo-garoto-da-pesada-para-servidores-ruby-raptor/"target="_blank" rel="noopener">Raptor Beta</a>. Então pode configurar quantos cloudlets quer (pelo que entendi 1 cloudlet é uma unidade de processamento que representa 128Mb e 200Mhz e pode escalar verticalmente até 64 cloudlets ou 8GB e 12.80Ghz) por node. E horizontalmente você pode escolher ter até 18 nodes. Em termos de preço isso pode ir de até <strong>R$15</strong> por mês (lembrando que é pagamento por utilização e não preço fixo) até <strong>R$10.179</strong> na configuração máxima.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/488/Screen_Shot_2015-04-28_at_09.21.04.png" alt="Dashboard"  loading="lazy" /></p>
<p>Uma configuração média que deve servir pra grande maioria das aplicações pequenas/médias de 8 reserved cloudlet de 768Mb e 1.2GHZ, 2 nodes sob balanceamento, com 256MB de Memcached, 1Gb e 1.6Ghz de PostgreSQL (sempre deixe seu banco maior que sua máquina Web) dá em torno de R$ 280 por mês, ou o equivalente a USD 96, que eu achei até competitivo com alternativas internacionais (sim, é <a href="http://meiobit.com/301551/receita-cobranca-impostos-empresas-usam-servicos-datacenters-exterior/"target="_blank" rel="noopener">controverso</a> mas levar impostos em consideração).</p>
<h2>Dicas de Configuração<span class="hx:absolute hx:-mt-20" id="dicas-de-configuração"></span>
    <a href="#dicas-de-configura%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Você pode fazer o deployment diretamente de um repositório Github ou subir de um zip. Depois disso você pode entrar em qualquer das máquinas diretamente via SSH. Esse passo será necessário pois não há outra forma de configurar ambiente (especificamente variáveis de ambiente que é o correto para configurações). Entre via SSH e configure um arquivo <tt>.env</tt> com seus dados de produção usando a gem <a href="http://www.akitaonrails.com/2013/10/19/iniciante-configuracoes-de-ambiente-com-dotenv#.VT9_ZM5Hlp8"target="_blank" rel="noopener"><tt>dotenv-rails</tt></a>.</p>
<p>Uma coisa importante: se usar a configuração normal de banco de dados via <tt>config/database.yml</tt> vai ter problemas no caso do PostgreSQL pois parece que há diferença de versão entre o libpg nos nodes web com o PostgreSQL na máquina separada. Para bypassar isso, configure no arquivo <tt>.env</tt> uma variável chamada <tt>DATABASE_URL</tt> (que por padrão, bypassa o <tt>database.yml</tt> e conecta por TCP em vez de Unix Sockets). O formato dessa variável deve ser assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>DATABASE_URL: &#39;postgres://[usuario]:[senha]@[host]:5432/[nome do banco]&#39;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Ao criar o servidor PostgreSQL você também deve receber um email com um link para o PHPPGAdmin que é uma interface web para configurar o banco, você pode entrar por lá e criar um novo usuário (não use o usuário &lsquo;webadmin&rsquo;, obviamente). Outra opção é entrar via SSH direto no servidor de banco e usar comandos normais como <tt>createuser</tt> ou <tt>createdb</tt>. Não esqueça de adicionar sua chave pública de SSH na aba de configurações da aplicação, é simples.</p>
<p>O deployment automaticamente via colocar os arquivos da sua aplicação no lugar correto e já vai executar o <tt>bundle install</tt>. Mas depois do deployment você ainda vai precisar entrar via SSH no node NGINX para executar tasks como <tt>rake db:migrate</tt> e <tt>rake assets:precompile</tt>. Se estiver usando Memcache (com Rack::Cache, por exemplo), às vezes talvez precise entrar via SSH para acessar o <tt>rails console</tt> para poder zerar o cache via <tt>Rails.cache.clear</tt>. No meu caso, mesmo depois do deployment dizer que já rodou o Bundler ainda precisei entrar no diretório da aplicação e rodar <tt>bundle install</tt> manualmente. Nada de mais, mas vale lembrar disso.</p>
<p>Obs: O Kemel me avisou depois que publiquei o post que o Jelastic (que parece que usa a estrutura do OpenShift por trás) suporta um arquivo <tt>rake_deploy</tt> justamente para adicionar configurações pós-deployment que podem ser executados automaticamente sem precisar entrar por SSH toda vez. Veja <a href="http://docs.jelastic.com/ruby-post-deploy-configuration"target="_blank" rel="noopener">esta documentação</a>.</p>
<p>Finalmente, se fez tudo certo, você já pode acessar sua aplicação pelo endereço <tt>[nome da app].jelasticlw.com</tt>. Daí você pode configurar a aplicação para aceitar um Custom Domain (domínio que você registrou) e no seu serviço de DNS (seja na própria Locaweb ou serviços como Zerigo, SimpleDNS) configurar o CNAME para apontar para o endereço Jelastic.</p>
<p>Não cheguei a testar mas o Jelastic também oferece opções como adicionar SSL e também Auto Scaling (escalabilidade horizontal), de tal maneira que se sua aplicação ultrapassar certas barreiras de consumo de CPU e/ou de Memória, ele automaticamente pode adicionar e remover nodes (você configura números máximos e mínimos de nodes). Assim você pode ficar despreocupado se seu site tiver picos inesperados de acesso e vai gastar apenas o que realmente precisa.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/489/Screen_Shot_2015-04-28_at_09.48.23.png" alt="Auto Scale"  loading="lazy" /></p>
<p>De pontos negativos, comparado ao que um PaaS de Ruby poderia ser, infelizmente ele não tem o conceito de buildpacks abertos, não tem como configurar um pipeline de deployment que já execute corretamente as Rake Tasks do Assets Precompile, Db:Migrate, etc. Não tem suporte a Procfile para subir o servidor de aplicação correto (recomendo já usar o Raptor direto). A escalabilidade vertical não ajuda tanto porque mesmo aumentando a capacidade da máquina, precisaríamos entrar via SSH para configurar a quantidade de worker processes de um Unicorn, por exemplo. E no meu teste, adicionar o balanceador de carga depois que já tinha um node deu problema (acho que precisa já deixar adicionar logo que cria a aplicação e não adicionar depois). Um último ponto importante: não há uma máquina para workers (background jobs), somente nodes web. Mesmo se precisar rodar cronjobs, ele vai rodar em concorrência com seu processo Web, o que é ruim para o tempo de resposta da aplicação web. Ainda não parei para ver se há outra forma (se alguém souber como, não deixe de colocar nos comentários).</p>
<p>De ponto positivo, a aplicação que coloquei no ar <a href="http://rubyconfbrcfp.com.br"target="_blank" rel="noopener">funcionou</a> (Hurray!), teve alguns caveats que mencionei acima, mas o importante é que no final ele realmente funcionou. Ainda é cedo pra dizer o comportamento (foi meu primeiro teste), mas vamos ver o que teremos no futuro. O Jelastic continua em evolução: a própria adição do Raptor é um sinal disso, o que é muito positivo.</p>
<h2>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O Jelastic não é para completos iniciantes, para isso são planos de hospedagem compartilhada que já vem pré-configurados se você precisa subir um mero Wordpress ou Magento. Ele é para desenvolvedores com alguma experiência e que não deveriam estar responsabilizados pela infraestrutura do cliente. Configurar VPS não é difícil, mas a manutenção pode ser, especialmente se for necessário atualizar os recursos da máquina ou, principalmente, se for necessário configurar escalabilidade horizontal e recursos mais avançados como auto escalabilidade horizontal.</p>
<p>O custo do Jelastic é definitivamente maior que qualquer hospedagem compartilhada, porém é como comparar maçãs e bananas. Hospedagens compartilhadas são para coisas pequenas, de preferência sites completamente estáticos, que exigem quase nenhum processamento. Qualquer coisa maior que um pequeno blog vai sofrer. O único próximo passo que as hospedagens brasileiras ofereciam eram ou um Cloud Server (VPS) ou diretamente hardware bruto.</p>
<p>O Jelastic é o meio do caminho e da forma como está caminhando pode ser um ótimo meio do caminho que vale a pena ser explorado. O primeiro deployment pode ser meio doloroso até aprender o que é possível fazer, mas depois disso tudo fica menos complicado e definitivamente o custo-benefício entre manter um administrador de sistemas experiente full time na sua folha de pagamentos vai compensar algumas horas do seu desenvolvedor aprendendo a configurar a plataforma.</p>
<p>Num mundo onde ainda parece &ldquo;normal&rdquo; fazer deployment via FTP (protip: TOSCO!) e editar arquivos diretamente em produção, um produto como o Jelastic pode começar a ajudar a evangelizar conceitos mais corretos de deployment em produção.</p>
<p>Disclaimer: eu não sou da Locaweb e este post não é um marketing pago ou coisa do tipo. O Jelastic é um produto novo então muitos já devem ter enfrentado dores de cabeça. Se tiver experiências que possam ajudar outros desenvolvedores, não deixem de comentar abaixo.</p>
]]></content:encoded><category>cloud</category><category>rails</category><category>devops</category></item></channel></rss>