<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Rails | AkitaOnRails.com</title><link>https://akitaonrails.github.io/tags/rails/</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:49 +0000</lastBuildDate><atom:link href="https://akitaonrails.github.io/tags/rails/index.xml" rel="self" type="application/rss+xml"/><item><title>Eu Fiz um Sistema de Data Mining pra Minha Namorada Influencer — Dicas e Truques</title><link>https://akitaonrails.github.io/2026/03/04/eu-fiz-um-sistema-de-data-mining-pra-minha-namorada-influencer-dicas-e-truques/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/03/04/eu-fiz-um-sistema-de-data-mining-pra-minha-namorada-influencer-dicas-e-truques/</guid><pubDate>Wed, 04 Mar 2026 10:00:00 -0300</pubDate><description>&lt;p&gt;Minha namorada é influencer de games, anime, cosplay e cultura pop. Faz entrevistas, cobre convenções, produz conteúdo pra múltiplas plataformas, negocia com patrocinadores. É uma operação profissional de uma pessoa com uma equipe enxuta. E como toda operação profissional, ela precisa de dados.&lt;/p&gt;
&lt;p&gt;O problema é que coletar dados de redes sociais é trabalho braçal. Abrir Instagram, YouTube, X, olhar números, comparar com concorrentes, checar calendário de eventos, monitorar patrocinadores, ler comentários, calcular engajamento, decidir preço de publi. Tudo manual, tudo repetitivo, tudo consumindo tempo que deveria ir pra criação de conteúdo.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Minha namorada é influencer de games, anime, cosplay e cultura pop. Faz entrevistas, cobre convenções, produz conteúdo pra múltiplas plataformas, negocia com patrocinadores. É uma operação profissional de uma pessoa com uma equipe enxuta. E como toda operação profissional, ela precisa de dados.</p>
<p>O problema é que coletar dados de redes sociais é trabalho braçal. Abrir Instagram, YouTube, X, olhar números, comparar com concorrentes, checar calendário de eventos, monitorar patrocinadores, ler comentários, calcular engajamento, decidir preço de publi. Tudo manual, tudo repetitivo, tudo consumindo tempo que deveria ir pra criação de conteúdo.</p>
<p>Fiz o que qualquer programador namorado faria: construí um sistema de data mining completo, com coleta automatizada, análise via LLM, e um chatbot no Discord onde ela conversa com os dados em linguagem natural.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/screenshot-2026-03-04_13-15-10.jpg" alt="mila-bot"  loading="lazy" /></p>
<p>Este artigo não é sobre o código em si (o projeto não será open source). É sobre o processo de construção, as decisões que só aparecem depois que você começa, os truques técnicos que salvaram o projeto, e por que um documento de desejos da sua usuária vale mais que qualquer spec funcional.</p>
<h2>Comece Pelo Desejo, Não Pela Arquitetura<span class="hx:absolute hx:-mt-20" id="comece-pelo-desejo-não-pela-arquitetura"></span>
    <a href="#comece-pelo-desejo-n%c3%a3o-pela-arquitetura" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A primeira coisa que fiz foi sentar com ela e pedir que falasse livremente o que queria. Sem linguagem técnica, sem formulário, sem user stories. As próprias palavras dela. Gravei num documento IDEA.md que virou o norte do projeto inteiro.</p>
<p>O que saiu dali:</p>
<blockquote>
  <p>&ldquo;Maior dificuldade hoje é saber qual tipo de conteúdo criar que pode dar muito certo e por que deram certo, o que fez dar o resultado que deu. Tem que ler comentários dos vídeos pra entender motivos gerais.&rdquo;</p>

</blockquote>
<blockquote>
  <p>&ldquo;Quanto deveria/poderia cobrar por uma publicidade, baseado na pesquisa de engajamento, concorrência, patrocinadores, eventos.&rdquo;</p>

</blockquote>
<blockquote>
  <p>&ldquo;Como fazer uma publicidade que não parece publicidade, conteúdo pago que traz engajamento sem parecer só propaganda de venda.&rdquo;</p>

</blockquote>
<p>Ela não pediu um dashboard. Não pediu gráficos. Não pediu relatórios em PDF. Pediu respostas pra problemas concretos do dia a dia dela. Isso mudou completamente a forma como eu abordei a arquitetura.</p>
<p>Se eu tivesse começado pela spec técnica, teria construído uma plataforma de analytics com charts bonitos e exports em CSV. O sistema que ela realmente precisava era algo mais parecido com uma assistente que conhece os dados e responde perguntas em português.</p>
<p>O IDEA.md também listava concorrentes e patrocinadores iniciais. Marcas com as quais ela poderia trabalhar (restaurantes temáticos, lojas de figures, Crunchyroll, Piticas, Fanlab). Perfis de referência no Instagram. Tudo isso virou dados de seed. O documento não era um spec — era uma conversa que virou requisito orgânico.</p>
<h2>58 Commits em 3 Dias<span class="hx:absolute hx:-mt-20" id="58-commits-em-3-dias"></span>
    <a href="#58-commits-em-3-dias" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O projeto foi construído em 3 dias. Não 3 semanas, não 3 meses. 58 commits, 3 fases distintas:</p>
<p><strong>Dia 1 — O Motor de Dados.</strong> 12 commits. Scaffold Rails 8, modelos, coletores pra YouTube/Instagram/X, integração com LLM. Pipeline de discovery pra encontrar novos perfis automaticamente. Scoring de performance por post. Análise de sentimento de comentários via Claude. No final do dia, o sistema coletava dados das 3 plataformas e classificava cada post como viral, acima da média, na média, abaixo ou flop.</p>
<p><strong>Dia 2 — O Cérebro e a Voz.</strong> 35 commits. O dia mais intenso. Dois sistemas grandes apareceram: o &ldquo;Oracle&rdquo; (rastreamento de eventos, convenções, lançamentos de jogos/filmes/anime, notícias) e o chatbot Discord com tool calling via RubyLLM. Também hardening pra produção, Docker, guia de deploy. E uma pivotada grande: troquei o relatório semanal por email por 5 digests diários via Discord.</p>
<p><strong>Dia 3 — Entretenimento e Resiliência.</strong> 11 commits. Steam games (Store API + SteamSpy), AniList pra tracking de animes da temporada, analytics de crescimento, geração de imagens via Gemini, sistema de auto-healing pra URLs quebradas.</p>
<p>Os números finais:</p>
<table>
  <thead>
      <tr>
          <th>Métrica</th>
          <th>Valor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Arquivos</td>
          <td>430</td>
      </tr>
      <tr>
          <td>Linhas totais</td>
          <td>37.088</td>
      </tr>
      <tr>
          <td>Linhas de Ruby</td>
          <td>25.562</td>
      </tr>
      <tr>
          <td>Testes</td>
          <td>916 (0 falhas)</td>
      </tr>
      <tr>
          <td>Modelos</td>
          <td>17</td>
      </tr>
      <tr>
          <td>Jobs agendados</td>
          <td>25+</td>
      </tr>
      <tr>
          <td>Tools do bot</td>
          <td>40+</td>
      </tr>
      <tr>
          <td>Prompts YAML</td>
          <td>23</td>
      </tr>
      <tr>
          <td>Integrações externas</td>
          <td>12+ APIs</td>
      </tr>
  </tbody>
</table>
<p>Esses números não são mérito meu. Claude fez boa parte do código. Mas a direção, as decisões arquiteturais, e principalmente a validação de cada step foram humanas. Claude não sabe o que uma influencer brasileira de cosplay precisa. Eu também não sabia — mas ela me disse.</p>
<h2>A Arquitetura que Emergiu<span class="hx:absolute hx:-mt-20" id="a-arquitetura-que-emergiu"></span>
    <a href="#a-arquitetura-que-emergiu" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O sistema final é um Rails 8 headless. Zero interface web. Sem views, sem controllers de verdade (só o <code>/up</code> pra health check). Toda funcionalidade é entregue via background jobs, rake tasks, e o chatbot Discord.</p>
<p>A stack:</p>
<ul>
<li><strong>Rails 8.1</strong> com Solid Queue (jobs) e Solid Cache</li>
<li><strong>SQLite3</strong> em produção (WAL mode, bind mount entre containers)</li>
<li><strong>RubyLLM</strong> pra integração com Claude Sonnet via OpenRouter e Grok</li>
<li><strong>Ferrum</strong> pra scraping com headless Chrome</li>
<li><strong>Discordrb</strong> pro chatbot</li>
<li><strong>Docker Compose</strong> com 4 serviços</li>
</ul>
<p>O <code>docker-compose.yml</code> ficou enxuto:</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">x-app</span><span class="p">:</span><span class="w"> </span><span class="cp">&amp;app</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">env_file</span><span class="p">:</span><span class="w"> </span><span class="l">.env</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">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/storage:/rails/storage</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">&lt;&lt;</span><span class="p">:</span><span class="w"> </span><span class="cp">*app</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;127.0.0.1:3000:80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;CMD&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;curl&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-f&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;http://localhost:80/up&#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">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">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">memory</span><span class="p">:</span><span class="w"> </span><span class="l">512M</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">jobs</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">*app</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="l">bundle exec rake solid_queue:start</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="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">condition</span><span class="p">:</span><span class="w"> </span><span class="l">service_healthy</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">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">memory</span><span class="p">:</span><span class="w"> </span><span class="l">1G</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">discord</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">*app</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="l">bundle exec rake discord:start</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="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">condition</span><span class="p">:</span><span class="w"> </span><span class="l">service_healthy</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">chrome</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">chromedp/headless-shell:stable</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">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">memory</span><span class="p">:</span><span class="w"> </span><span class="l">1G</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>4 containers: app (Puma, basicamente só o health check), jobs (Solid Queue rodando 25+ jobs scheduled), discord (o bot), e chrome (headless browser pra scraping). Todo o estado fica em SQLite via bind mount no host. Sem Redis, sem Postgres, sem infraestrutura extra.</p>
<h2>Idempotência Acima de Tudo<span class="hx:absolute hx:-mt-20" id="idempotência-acima-de-tudo"></span>
    <a href="#idempot%c3%aancia-acima-de-tudo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se eu tivesse que escolher um conceito pra resumir o projeto, seria <strong>idempotência</strong>. Todo job pode rodar duas vezes seguidas sem criar duplicatas, sem corromper dados, sem efeitos colaterais.</p>
<p>O pattern base:</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">module</span> <span class="nn">Collection</span>
</span></span><span class="line"><span class="cl">  <span class="k">class</span> <span class="nc">BaseCollectorJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
</span></span><span class="line"><span class="cl">    <span class="no">SNAPSHOT_DEDUP_WINDOW</span> <span class="o">=</span> <span class="mi">1</span><span class="o">.</span><span class="n">hour</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">retry_on</span> <span class="no">StandardError</span><span class="p">,</span> <span class="ss">wait</span><span class="p">:</span> <span class="ss">:polynomially_longer</span><span class="p">,</span> <span class="ss">attempts</span><span class="p">:</span> <span class="mi">3</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">perform</span><span class="p">(</span><span class="ss">profile_id</span><span class="p">:</span> <span class="kp">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">scope</span> <span class="o">=</span> <span class="n">profile_id</span> <span class="p">?</span> <span class="no">SocialProfile</span><span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="nb">id</span><span class="p">:</span> <span class="n">profile_id</span><span class="p">)</span> <span class="p">:</span> <span class="n">profiles_to_collect</span>
</span></span><span class="line"><span class="cl">      <span class="n">scope</span><span class="o">.</span><span class="n">find_each</span> <span class="k">do</span> <span class="o">|</span><span class="n">profile</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">        <span class="n">log</span> <span class="o">=</span> <span class="n">find_or_create_log</span><span class="p">(</span><span class="n">profile</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">begin</span>
</span></span><span class="line"><span class="cl">          <span class="n">items</span> <span class="o">=</span> <span class="n">collect_for_profile</span><span class="p">(</span><span class="n">profile</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="n">profile</span><span class="o">.</span><span class="n">touch</span><span class="p">(</span><span class="ss">:last_collected_at</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="n">log</span><span class="o">.</span><span class="n">update!</span><span class="p">(</span><span class="ss">status</span><span class="p">:</span> <span class="ss">:completed</span><span class="p">,</span> <span class="ss">items_collected</span><span class="p">:</span> <span class="n">items</span> <span class="o">||</span> <span class="mi">0</span><span class="p">)</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="n">log</span><span class="o">.</span><span class="n">update!</span><span class="p">(</span><span class="ss">status</span><span class="p">:</span> <span class="ss">:failed</span><span class="p">,</span> <span class="ss">error_message</span><span class="p">:</span> <span class="s2">&#34;</span><span class="si">#{</span><span class="n">e</span><span class="o">.</span><span class="n">class</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 class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="k">raise</span> <span class="k">if</span> <span class="n">should_raise?</span><span class="p">(</span><span class="n">e</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></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">upsert_post</span><span class="p">(</span><span class="n">profile</span><span class="p">,</span> <span class="n">attrs</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">post</span> <span class="o">=</span> <span class="n">profile</span><span class="o">.</span><span class="n">social_posts</span><span class="o">.</span><span class="n">find_or_initialize_by</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="ss">platform_post_id</span><span class="p">:</span> <span class="n">attrs</span><span class="o">[</span><span class="ss">:platform_post_id</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="n">post</span><span class="o">.</span><span class="n">assign_attributes</span><span class="p">(</span><span class="n">attrs</span><span class="o">.</span><span class="n">except</span><span class="p">(</span><span class="ss">:platform_post_id</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">      <span class="n">post</span><span class="o">.</span><span class="n">save!</span>
</span></span><span class="line"><span class="cl">      <span class="n">post</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="k">def</span> <span class="nf">record_snapshot</span><span class="p">(</span><span class="n">profile</span><span class="p">,</span> <span class="n">metrics</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="k">if</span> <span class="n">profile</span><span class="o">.</span><span class="n">profile_snapshots</span>
</span></span><span class="line"><span class="cl">        <span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="s2">&#34;captured_at &gt; ?&#34;</span><span class="p">,</span> <span class="no">SNAPSHOT_DEDUP_WINDOW</span><span class="o">.</span><span class="n">ago</span><span class="p">)</span><span class="o">.</span><span class="n">exists?</span>
</span></span><span class="line"><span class="cl">      <span class="n">profile</span><span class="o">.</span><span class="n">profile_snapshots</span><span class="o">.</span><span class="n">create!</span><span class="p">(</span><span class="ss">captured_at</span><span class="p">:</span> <span class="no">Time</span><span class="o">.</span><span class="n">current</span><span class="p">,</span> <span class="o">**</span><span class="n">metrics</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></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>Dois mecanismos de proteção: <code>find_or_initialize_by(platform_post_id)</code> pra posts (upsert, não insert), e <code>SNAPSHOT_DEDUP_WINDOW</code> pra snapshots (ignora se já coletou na última hora). Se o job crashear no meio e o Solid Queue reenfileirar, nada duplica.</p>
<p>Erros de scraping (<code>BlockedError</code>, <code>RateLimitError</code>) são engolidos silenciosamente — o retry não ajuda, o rate limit precisa esfriar, e ser bloqueado é esperado. Erros de verdade (banco, rede, bugs) sobem pro retry com backoff polinomial.</p>
<p>Essa decisão de &ldquo;engolir certos erros&rdquo; parece errada até você rodar o sistema por uma semana e ver que o Instagram bloqueia uma em cada 5 coletas. Se cada bloqueio gerasse 3 retries com backoff exponencial, o sistema ficaria perpetuamente atrasado.</p>
<h2>nil vs Zero<span class="hx:absolute hx:-mt-20" id="nil-vs-zero"></span>
    <a href="#nil-vs-zero" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Esse é um daqueles bugs conceituais que você só descobre com dados reais. Quando o Instagram não retorna o número de compartilhamentos de um post (porque a API simplesmente não fornece esse dado), o valor deveria ser <code>nil</code> ou <code>0</code>?</p>
<p>A diferença importa. <code>nil</code> significa <em>&ldquo;não temos esse dado&rdquo;</em>. Zero significa <em>&ldquo;temos o dado e ele é zero&rdquo;</em>. Se você trata <code>nil</code> como zero, a análise de LLM conclui que ninguém compartilha posts no Instagram — o que é falso. A API simplesmente não expõe essa métrica.</p>
<p>Criei um snippet de prompt reutilizável só pra isso:</p>
<blockquote>
  <p>Quando um campo é null: Não diga &ldquo;0 likes&rdquo; — diga &ldquo;dado não disponível para esta plataforma&rdquo;. Quando comparar plataformas, avise que certas métricas não são comparáveis. Quando é zero: Reporte normalmente — o dado é real e confirmado pela API.</p>

</blockquote>
<p>Esse snippet é incluído em todo prompt que lida com dados numéricos. Sem ele, o LLM mistura alegremente &ldquo;não coletado&rdquo; com &ldquo;zero de verdade&rdquo; e tira conclusões erradas.</p>
<h2>O Truque do Chrome Headless: Host Header Bypass<span class="hx:absolute hx:-mt-20" id="o-truque-do-chrome-headless-host-header-bypass"></span>
    <a href="#o-truque-do-chrome-headless-host-header-bypass" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O projeto usa <code>chromedp/headless-shell</code> num container separado pra scraping. Funciona perfeitamente. Até você tentar conectar o Ferrum (a gem Ruby de automação de Chrome) nele via Docker networking.</p>
<p>O problema: <code>chromedp/headless-shell</code> usa um proxy socat na porta 9222 que rejeita qualquer request cujo header <code>Host</code> não seja <code>localhost</code> ou um IP. Quando o Ferrum tenta conectar em <code>http://chrome:9222</code>, o Host header vai como <code>chrome:9222</code>, e o socat recusa.</p>
<p>A solução foi descobrir a URL do WebSocket manualmente, forjando o header:</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">discover_ws_url</span><span class="p">(</span><span class="n">chrome_url</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">uri</span> <span class="o">=</span> <span class="no">URI</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">#{</span><span class="n">chrome_url</span><span class="si">}</span><span class="s2">/json/version&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">req</span> <span class="o">=</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="o">::</span><span class="no">Get</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">uri</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">req</span><span class="o">[</span><span class="s2">&#34;Host&#34;</span><span class="o">]</span> <span class="o">=</span> <span class="s2">&#34;localhost&#34;</span>  <span class="c1"># bypass</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">response</span> <span class="o">=</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="o">.</span><span class="n">start</span><span class="p">(</span><span class="n">uri</span><span class="o">.</span><span class="n">hostname</span><span class="p">,</span> <span class="n">uri</span><span class="o">.</span><span class="n">port</span><span class="p">)</span> <span class="p">{</span> <span class="o">|</span><span class="n">http</span><span class="o">|</span> <span class="n">http</span><span class="o">.</span><span class="n">request</span><span class="p">(</span><span class="n">req</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="n">data</span> <span class="o">=</span> <span class="no">JSON</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">body</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">ws_url</span> <span class="o">=</span> <span class="n">data</span><span class="o">[</span><span class="s2">&#34;webSocketDebuggerUrl&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="kp">nil</span> <span class="k">unless</span> <span class="n">ws_url</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># WS URL aponta pra 127.0.0.1 dentro do container — troca pelo hostname acessível</span>
</span></span><span class="line"><span class="cl">  <span class="n">remote_uri</span> <span class="o">=</span> <span class="no">URI</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">chrome_url</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">ws_url</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sr">%r{://[^/]+}</span><span class="p">,</span> <span class="s2">&#34;://</span><span class="si">#{</span><span class="n">remote_uri</span><span class="o">.</span><span class="n">host</span><span class="si">}</span><span class="s2">:</span><span class="si">#{</span><span class="n">remote_uri</span><span class="o">.</span><span class="n">port</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></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>Faço o GET em <code>/json/version</code> com <code>Host: localhost</code> pra passar pelo socat. Recebo a URL de WebSocket (que aponta pra <code>127.0.0.1</code> — inútil fora do container). Reescrevo o hostname pra <code>chrome:9222</code> (o nome do serviço no Docker Compose). Passo a <code>ws_url</code> direto pro Ferrum, que abre o WebSocket sem passar pelo HTTP do socat.</p>
<p>Esse tipo de coisa não vem à cabeça no começo. Você descobre depois de 1 hora de <code>connection refused</code> e leitura do código fonte do <code>chromedp/headless-shell</code>.</p>
<h2>Tool Calling: O Bot que Consulta o Banco Sozinho<span class="hx:absolute hx:-mt-20" id="tool-calling-o-bot-que-consulta-o-banco-sozinho"></span>
    <a href="#tool-calling-o-bot-que-consulta-o-banco-sozinho" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A parte mais legal do projeto é o chatbot no Discord. Ela digita uma pergunta em português (&ldquo;como foram meus posts essa semana?&rdquo;) e o bot chama as ferramentas certas, consulta o banco, e responde com dados reais.</p>
<p>O segredo é o tool calling do RubyLLM. Cada ferramenta é uma classe Ruby com <code>description</code> e <code>params</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="k">class</span> <span class="nc">PostPerformanceTool</span> <span class="o">&lt;</span> <span class="no">RubyLLM</span><span class="o">::</span><span class="no">Tool</span>
</span></span><span class="line"><span class="cl">  <span class="n">description</span> <span class="s2">&#34;Retorna estatísticas de performance dos posts: baseline, &#34;</span> <span class="p">\</span>
</span></span><span class="line"><span class="cl">              <span class="s2">&#34;breakdown por tipo de conteúdo, melhores horários, melhores hashtags.&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">param</span> <span class="ss">:username</span><span class="p">,</span> <span class="ss">type</span><span class="p">:</span> <span class="ss">:string</span><span class="p">,</span> <span class="ss">desc</span><span class="p">:</span> <span class="s2">&#34;Username do perfil (ex: milaoliveira.png)&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">param</span> <span class="ss">:analysis</span><span class="p">,</span> <span class="ss">type</span><span class="p">:</span> <span class="ss">:string</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">desc</span><span class="p">:</span> <span class="s2">&#34;Tipo: baseline, content_types, timing, hashtags&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">required</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="k">def</span> <span class="nf">execute</span><span class="p">(</span><span class="ss">username</span><span class="p">:,</span> <span class="ss">analysis</span><span class="p">:</span> <span class="s2">&#34;baseline&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">profile</span> <span class="o">=</span> <span class="no">SocialProfile</span><span class="o">.</span><span class="n">find_by</span><span class="p">(</span><span class="ss">username</span><span class="p">:</span> <span class="n">username</span><span class="o">.</span><span class="n">to_s</span><span class="o">.</span><span class="n">strip</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="s2">&#34;Perfil &#39;</span><span class="si">#{</span><span class="n">username</span><span class="si">}</span><span class="s2">&#39; não encontrado.&#34;</span> <span class="k">unless</span> <span class="n">profile</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="n">analysis</span><span class="o">.</span><span class="n">to_s</span><span class="o">.</span><span class="n">strip</span><span class="o">.</span><span class="n">downcase</span>
</span></span><span class="line"><span class="cl">    <span class="k">when</span> <span class="s2">&#34;baseline&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="n">baseline</span> <span class="o">=</span> <span class="no">PostPerformance</span><span class="o">.</span><span class="n">compute_baseline</span><span class="p">(</span><span class="n">profile</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="ss">username</span><span class="p">:</span> <span class="n">profile</span><span class="o">.</span><span class="n">username</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="ss">post_count</span><span class="p">:</span> <span class="n">baseline</span><span class="o">[</span><span class="ss">:post_count</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="ss">mean_views</span><span class="p">:</span> <span class="n">baseline</span><span class="o">[</span><span class="ss">:mean_views</span><span class="o">].</span><span class="n">round</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="ss">stddev_views</span><span class="p">:</span> <span class="n">baseline</span><span class="o">[</span><span class="ss">:stddev_views</span><span class="o">].</span><span class="n">round</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="ss">mean_engagement</span><span class="p">:</span> <span class="n">baseline</span><span class="o">[</span><span class="ss">:mean_engagement</span><span class="o">].</span><span class="n">round</span><span class="p">(</span><span class="mi">2</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="k">when</span> <span class="s2">&#34;timing&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="no">PostPerformance</span><span class="o">.</span><span class="n">timing_breakdown</span><span class="p">(</span><span class="n">profile</span><span class="p">)</span><span class="o">.</span><span class="n">first</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span><span class="o">.</span><span class="n">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">entry</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span> <span class="ss">day</span><span class="p">:</span> <span class="n">days</span><span class="o">[</span><span class="n">entry</span><span class="o">[</span><span class="ss">:day</span><span class="o">]]</span><span class="p">,</span> <span class="ss">hour</span><span class="p">:</span> <span class="s2">&#34;%02d:00&#34;</span> <span class="o">%</span> <span class="n">entry</span><span class="o">[</span><span class="ss">:hour</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="ss">avg_percentile</span><span class="p">:</span> <span class="n">entry</span><span class="o">[</span><span class="ss">:avg_percentile</span><span class="o">].</span><span class="n">round</span><span class="p">(</span><span class="mi">1</span><span class="p">)</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="c1"># ...</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 LLM recebe a lista de 40+ ferramentas com suas descrições, decide quais chamar com quais parâmetros, recebe os resultados, e formula a resposta. O bot no Discord mostra status em tempo real enquanto processa: <em>&ldquo;Consultando métricas de perfis&hellip; Verificando crescimento&hellip; Analisando desempenho de posts&hellip; (3 consultas)&rdquo;</em>.</p>
<p>O truque foi fazer cada tool retornar dados estruturados (Hashes e Arrays), não texto formatado. O LLM é muito melhor formatando dados brutos em resposta conversacional do que tentando parsear texto semi-estruturado.</p>
<p>Outro detalhe que só apareceu no uso real: clamping de parâmetros. Quando o LLM pede <code>limit: 999</code> num parâmetro que aceita no máximo 50, ao invés de retornar erro, faço <code>[[val.to_i, 1].max, 50].min</code>. O LLM erra parâmetros mais do que você imagina. Cada erro gera um round-trip de correção que custa tokens e tempo.</p>
<h2>Composable Prompts: YAML &gt; Strings Hardcoded<span class="hx:absolute hx:-mt-20" id="composable-prompts-yaml--strings-hardcoded"></span>
    <a href="#composable-prompts-yaml--strings-hardcoded" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Todos os prompts do sistema vivem em YAML no <code>config/prompts/</code>. Cada um tem um <code>system</code> (instruções fixas), um <code>template</code> (com interpolação de dados), e opcionalmente <code>includes</code> (snippets reutilizáveis):</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">module</span> <span class="nn">Llm</span>
</span></span><span class="line"><span class="cl">  <span class="k">class</span> <span class="nc">PromptBuilder</span>
</span></span><span class="line"><span class="cl">    <span class="no">PROMPTS_DIR</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">root</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;config&#34;</span><span class="p">,</span> <span class="s2">&#34;prompts&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">class</span> <span class="o">&lt;&lt;</span> <span class="nb">self</span>
</span></span><span class="line"><span class="cl">      <span class="k">def</span> <span class="nf">build</span><span class="p">(</span><span class="nb">name</span><span class="p">,</span> <span class="n">data</span> <span class="o">=</span> <span class="p">{})</span>
</span></span><span class="line"><span class="cl">        <span class="n">prompt</span> <span class="o">=</span> <span class="n">load_prompt</span><span class="p">(</span><span class="nb">name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">base</span> <span class="o">=</span> <span class="n">load_prompt</span><span class="p">(</span><span class="ss">:_base_context</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">includes</span> <span class="o">=</span> <span class="n">resolve_includes</span><span class="p">(</span><span class="n">prompt</span><span class="o">[</span><span class="s2">&#34;includes&#34;</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">system_parts</span> <span class="o">=</span> <span class="o">[</span><span class="n">base</span><span class="o">[</span><span class="s2">&#34;system&#34;</span><span class="o">]</span><span class="p">,</span> <span class="n">includes</span><span class="p">,</span> <span class="n">prompt</span><span class="o">[</span><span class="s2">&#34;system&#34;</span><span class="o">]]</span>
</span></span><span class="line"><span class="cl">          <span class="o">.</span><span class="n">compact</span><span class="o">.</span><span class="n">reject</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:blank?</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">system_message</span> <span class="o">=</span> <span class="n">system_parts</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">user_message</span> <span class="o">=</span> <span class="n">interpolate</span><span class="p">(</span><span class="n">prompt</span><span class="o">[</span><span class="s2">&#34;template&#34;</span><span class="o">]</span><span class="p">,</span> <span class="n">data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="p">{</span> <span class="nb">system</span><span class="p">:</span> <span class="n">system_message</span><span class="p">,</span> <span class="ss">user</span><span class="p">:</span> <span class="n">user_message</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></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>_base_context.yml</code> tem informações que todo prompt precisa (quem é a influencer, nichos, público, diretrizes gerais). Os snippets em <code>config/prompts/snippets/</code> resolvem problemas recorrentes:</p>
<ul>
<li><code>null_vs_zero.yml</code> — a distinção nil/zero explicada acima</li>
<li><code>never_invent.yml</code> — &ldquo;NUNCA invente dados, analise só o que está presente&rdquo;</li>
<li><code>json_only.yml</code> — &ldquo;responda SOMENTE em JSON válido&rdquo;</li>
</ul>
<p>Quando um prompt precisa de qualquer combinação desses, basta listar nos <code>includes</code>. Sem duplicação, sem risco de instruções conflitantes entre prompts.</p>
<p>Essa decisão veio de um bug real: dois prompts diferentes davam instruções contraditórias sobre como tratar valores nulos. Um dizia &ldquo;use 0&rdquo;, outro dizia &ldquo;use null&rdquo;. O LLM obedecia um ou outro dependendo do dia. Centralizar em snippets eliminou a classe inteira de inconsistência.</p>
<h2>Discovery Pipeline: Mineração Autônoma de Perfis<span class="hx:absolute hx:-mt-20" id="discovery-pipeline-mineração-autônoma-de-perfis"></span>
    <a href="#discovery-pipeline-minera%c3%a7%c3%a3o-aut%c3%b4noma-de-perfis" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O sistema inicial partiu de uma lista manual de concorrentes e patrocinadores. Mas a influencer vive num ecossistema dinâmico — novos creators aparecem toda semana, marcas surgem e somem. O discovery pipeline automatiza isso.</p>
<p>Roda toda sexta-feira às 2 da manhã. Começa minerando dados que já temos no banco: quem tá sendo @mencionado nos posts, quem comenta com frequência e alto engagement, que perfis aparecem em links de bio, quem usa hashtags de marca (#publi, #parceria), que links de Linktree apontam pra outros criadores. Tudo puro SQL, sem API calls, sem risco de rate limit.</p>
<p>Os candidatos que saem daí são validados contra as APIs das plataformas (o Instagram existe? O canal do YouTube é real?), depois enriquecidos com conexões cross-platform (se validamos um Instagram, procuramos o YouTube e X do mesmo criador).</p>
<p>Uma chamada de LLM em batch avalia todos os candidatos, classifica como concorrente, patrocinador potencial ou irrelevante, e dá scores de relevância e fit com o nicho. Os top 3 viram perfis rastreados de verdade — o sistema começa a coletar dados deles automaticamente.</p>
<p>Depois, agrupa tudo em entidades <code>Creator</code>. A influencer é profundidade 0. Concorrentes diretos, profundidade 1. Menções de concorrentes, profundidade 2. Limite máximo de 3 graus de separação, pra evitar cascata infinita.</p>
<h2>De Relatório Semanal pra Digests Diários: Ouvindo a Usuária<span class="hx:absolute hx:-mt-20" id="de-relatório-semanal-pra-digests-diários-ouvindo-a-usuária"></span>
    <a href="#de-relat%c3%b3rio-semanal-pra-digests-di%c3%a1rios-ouvindo-a-usu%c3%a1ria" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O plano original era um relatório semanal por email com 9 seções. Mas no meio do caminho decidi que seria mais dinâmico não sair do Discord mesmo e ter essa informação sempre disponível.</p>
<p>Pivotei pra 5 digests diários no Discord:</p>
<ul>
<li><strong>Segunda</strong> — Recap de Performance (seguidores, crescimento, top posts)</li>
<li><strong>Terça</strong> — Radar de Concorrentes (snapshot, notícias, estratégias)</li>
<li><strong>Quarta</strong> — Playbook de Conteúdo (agenda, tendências, hashtags)</li>
<li><strong>Quinta</strong> — Oportunidades e Preços (marcas, pricing, pacotes)</li>
<li><strong>Sexta</strong> — Semana que Vem (eventos, ações prioritárias)</li>
</ul>
<p>Cada digest tem itens numerados com botões de feedback. Ela marca o que achou útil e o que não achou. Esse feedback alimenta as análises futuras — o sistema aprende quais tipos de insight ela valoriza.</p>
<p>Essa mudança, que não estava em nenhum plano, provavelmente foi a que mais melhorou a experiência dela. Toda manhã às 9h ela abre o Discord e tem um resumo fresco, digerível, acionável. O email semanal ficou como backup, comentado no <code>recurring.yml</code>.</p>
<h2>O Oracle: Contexto que o Algoritmo Não Vê<span class="hx:absolute hx:-mt-20" id="o-oracle-contexto-que-o-algoritmo-não-vê"></span>
    <a href="#o-oracle-contexto-que-o-algoritmo-n%c3%a3o-v%c3%aa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/screenshot-2026-03-04_13-24-24.jpg" alt="playbook"  loading="lazy" /></p>
<p>Dados de redes sociais sem contexto externo são meio inúteis. <em>&ldquo;Esse post teve 3x mais views que a média&rdquo;</em> é um fato. <em>&ldquo;Esse post teve 3x mais views porque saiu no dia do trailer do novo Sonic&rdquo;</em> é uma insight.</p>
<p>O Oracle é o subsistema que fornece esse contexto.</p>
<p>A parte mais crítica é o rastreamento de convenções: CCXP, Anime Friends, BGS, e dezenas de outros eventos do calendário geek. Datas, preços, venues, links de ingresso, contas de Instagram/X do evento. Monitora diariamente por mudanças (data alterada, cancelamento, anúncio de convidado). A influencer planeja o calendário inteiro em torno de convenções. Se a CCXP muda de data, ela precisa saber no dia, não na semana seguinte.</p>
<p>Também rastreia lançamentos de filmes/séries via TMDB, jogos via IGDB (com OAuth do Twitch), animes via AniList — tudo filtrado pra conteúdo geek, janela de 90 dias pra frente. Aniversários de franquias (usando TMDB, IGDB, AniList, Wikimedia) e feriados brasileiros com cálculo de datas móveis. Sim, tem um calculador de Páscoa no código baseado no algoritmo computus. E notícias via scraping de RSS e X/Twitter a cada 6 horas, classificadas por relevância ao nicho.</p>
<p>O evento tracker scrapa sites de convenção com Chrome headless e usa LLM com temperatura 0.1 pra extrair metadados estruturados (data, preço, venue). Tem fallback com regex pra datas no formato brasileiro (&ldquo;15 de março de 2026&rdquo;). Deduplicação por fuzzy matching de nome + proximidade de data — porque &ldquo;CCXP 2026&rdquo;, &ldquo;CCXP São Paulo 2026&rdquo; e &ldquo;CCXP SP&rdquo; são o mesmo evento.</p>
<h2>SQLite em Produção: Sim, Funciona<span class="hx:absolute hx:-mt-20" id="sqlite-em-produção-sim-funciona"></span>
    <a href="#sqlite-em-produ%c3%a7%c3%a3o-sim-funciona" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>SQLite3 em produção. Com WAL mode, um único writer por vez é suficiente porque o sistema tem um padrão previsível de escrita (jobs sequenciais, nunca concorrentes no mesmo segundo). Os dados ficam num bind mount do Docker (<code>./data/storage:/rails/storage</code>) e backup é <code>cp data/storage/*.db backups/</code>.</p>
<p>Rails 8 trata SQLite como cidadão de primeira classe. Solid Queue e Solid Cache rodam em SQLite sem problemas. O overhead de um Postgres pra um sistema que serve uma pessoa não se justifica.</p>
<h2>25+ Jobs Agendados<span class="hx:absolute hx:-mt-20" id="25-jobs-agendados"></span>
    <a href="#25-jobs-agendados" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O <code>config/recurring.yml</code> tem 25+ jobs com horários distribuídos pra não sobrecarregar a máquina:</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">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">youtube_collection</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">class</span><span class="p">:</span><span class="w"> </span><span class="l">Collection::YoutubeCollectorJob</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="l">every day at 3am</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">instagram_collection</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">class</span><span class="p">:</span><span class="w"> </span><span class="l">Collection::InstagramCollectorJob</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="l">every day at 4am</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">x_collection</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">class</span><span class="p">:</span><span class="w"> </span><span class="l">Collection::XCollectorJob</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="l">every 2 days at 5am</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">oracle_events</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">class</span><span class="p">:</span><span class="w"> </span><span class="l">Oracle::EventTrackerJob</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="l">every Monday at 2am</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">convention_monitor</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">class</span><span class="p">:</span><span class="w"> </span><span class="l">Oracle::ConventionMonitorJob</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="l">every day at 5am</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">discovery_pipeline</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">class</span><span class="p">:</span><span class="w"> </span><span class="l">Discovery::OrchestratorJob</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="l">every Friday at 2am</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">comment_sentiment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">class</span><span class="p">:</span><span class="w"> </span><span class="l">Analysis::CommentSentimentJob</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="l">every Sunday at 5am</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">weekly_analysis</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">class</span><span class="p">:</span><span class="w"> </span><span class="l">Analysis::WeeklyAnalysisJob</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="l">every Sunday at 6am</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>Coleta roda de madrugada (3am-5am). Oracle de inteligência começa na segunda às 2am. Discovery na sexta às 2am. Análise pesada (sentimento, estratégia) no domingo quando não tem digest pra entregar. Digests de segunda a sexta às 9am, cada dia com tema diferente. Manutenção (limpeza de jobs terminados, health check de URLs, agregação de dados antigos) nos intervalos.</p>
<p>O padrão é intencional: nenhum job pesado compete com os digests da manhã. Se a análise semanal atrasar, o digest de segunda ainda entrega porque usa dados da coleta de sábado, não da análise de domingo.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/screenshot-2026-03-04_13-29-08.jpg" alt="nano banana 2"  loading="lazy" /></p>
<h2>O Que Não Dá Pra Prever Sem Rodar<span class="hx:absolute hx:-mt-20" id="o-que-não-dá-pra-prever-sem-rodar"></span>
    <a href="#o-que-n%c3%a3o-d%c3%a1-pra-prever-sem-rodar" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Algumas coisas que só descobri depois que o sistema tava rodando em produção.</p>
<p>O Instagram bloqueia scraping aleatoriamente. Não importa quão stealth seu browser é. A configuração inicial era só scraping via Ferrum. De cara, resolvi adicionar o <strong>Apify</strong> como fonte primária (API paga, mais confiável) e o scraping virou fallback.</p>
<p>LLMs alucinam timestamps. Pedi pro bot criar um lembrete pra &ldquo;amanhã às 15h&rdquo; e ele calculou uma data em 2024. A solução foi injetar <code>current_datetime</code> em todo prompt que lida com tempo e instruir explicitamente: <em>&ldquo;calcule baseado em current_datetime, converta pra ISO 8601 com UTC-3&rdquo;</em>.</p>
<p>O Discord tem limite de 2000 caracteres por mensagem, o que parece óbvio até seu bot mandar uma análise de 4000 chars e o Discord simplesmente truncar. Implementei split automático que quebra no último <code>\n</code> antes do limite.</p>
<p>Os tool calls falham com frequência em parâmetros, o que me surpreendeu. O LLM pede <code>limit: 100</code> quando o máximo é 50, ou manda o username sem o @ quando deveria ter. Clamping silencioso (<code>[[val.to_i, 1].max, 50].min</code>) e normalização de input (<code>.strip</code>, <code>.delete(&quot;@&quot;)</code>) em toda tool eliminaram uma categoria inteira de erros que estavam consumindo tokens em loops de retry.</p>
<p>Convenções mudam de data mais do que eu imaginava. Adicionei um sistema de announcements que acompanha cada mudança (data, venue, preço, cancelamento) com timestamp. O evento tracker roda toda segunda, mas o convention monitor roda DIÁRIO porque a última coisa que a influencer quer é descobrir que uma convenção mudou de data depois de já ter reservado hotel.</p>
<h2>O Bot Como Interface<span class="hx:absolute hx:-mt-20" id="o-bot-como-interface"></span>
    <a href="#o-bot-como-interface" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/screenshot-2026-03-04_13-28-05.jpg" alt="eventos"  loading="lazy" /></p>
<p>A decisão mais acertada do projeto foi não fazer interface web. O Discord já é onde a influencer passa o dia. O bot vive ali, disponível a qualquer momento. Ela digita <em>&ldquo;o que eu deveria postar essa semana?&rdquo;</em> e recebe uma resposta contextualizada com dados dos seus posts, dos concorrentes, do calendário de eventos, das tendências de hashtag, e dos animes/games da temporada.</p>
<p>O chatbot combina múltiplas tools numa resposta. Se ela pergunta sobre estratégia de conteúdo, o LLM chama: digest de conteúdo + calendário de eventos + tendências de hashtag + animes da temporada + jogos da Steam. Cinco consultas ao banco, tudo costurado numa resposta conversacional em português.</p>
<p>Tem até um modo de <strong>&ldquo;pensamento profundo&rdquo;</strong> que ativa quando detecta palavras como &ldquo;pesquise&rdquo;, &ldquo;analise&rdquo;, &ldquo;investigue&rdquo;. Nesse modo, o bot usa TODAS as ferramentas relevantes, faz múltiplas consultas, cruza dados entre fontes.</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 projeto inteiro saiu em 3 dias, de zero a produção, coletando dados reais, mandando digests toda manhã. Sem Jira, sem sprint planning, sem 6 meses de discovery phase. Um IDEA.md com os desejos da usuária nas palavras dela, desenvolvimento iterativo commit a commit, e validação contínua com quem vai usar de verdade.</p>
<p>Se eu tivesse começado pela arquitetura, teria passado semanas projetando um dashboard que ela não ia abrir. Começando pelo desejo, o sistema acabou sendo um chatbot no Discord que ela usa todo dia. Jobs assíncronos que não são idempotentes são bombas-relógio — o rate limit vai estourar, o scraping vai falhar, o container vai reiniciar, e se o job não sobrevive a isso sem duplicar dados, o sistema não funciona.</p>
<p>No final das contas, a medida de sucesso do projeto não é a quantidade de linhas de código, nem o número de testes passando, nem a arquitetura elegante. É a influencer abrindo o Discord segunda de manhã e tendo o que precisa pra planejar a semana.</p>
]]></content:encoded><category>rails</category><category>backend</category><category>llms</category></item><item><title>Vibe Code: Fiz um clone do Mega em Rails em 1 dia pro meu Home Server</title><link>https://akitaonrails.github.io/2026/02/21/vibe-code-fiz-um-clone-do-mega-em-rails-em-1-dia-pro-meu-home-server/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/02/21/vibe-code-fiz-um-clone-do-mega-em-rails-em-1-dia-pro-meu-home-server/</guid><pubDate>Sat, 21 Feb 2026 18:48:25 +0000</pubDate><description>&lt;p&gt;Faz tempo que eu queria ter um serviço de file sharing privado no meu home server. Sabe quando precisa mandar um arquivo grande pra alguém? &amp;ldquo;Ah, usa o Google Drive&amp;rdquo;. Não. &amp;ldquo;WeTransfer?&amp;rdquo; Também não. Eu quero controle total. Quero saber onde meus arquivos estão, por quanto tempo existem, quem baixou, e não quero depender de serviço de terceiro pra algo tão simples.&lt;/p&gt;
&lt;p&gt;Então fiz o que qualquer programador faria: sentei, escrevi um documento de especificação, e com ajuda do Claude Code (Claude Opus 4.6), construí o &lt;a href="https://github.com/akitaonrails/FrankMega"target="_blank" rel="noopener"&gt;&lt;strong&gt;FrankMega&lt;/strong&gt;&lt;/a&gt;, um clone simplificado do Mega, self-hosted, em Rails 8, em um único dia de trabalho. 21 commits, 3 horas de desenvolvimento ativo, 210 testes, zero dependência externa além do SQLite.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Faz tempo que eu queria ter um serviço de file sharing privado no meu home server. Sabe quando precisa mandar um arquivo grande pra alguém? &ldquo;Ah, usa o Google Drive&rdquo;. Não. &ldquo;WeTransfer?&rdquo; Também não. Eu quero controle total. Quero saber onde meus arquivos estão, por quanto tempo existem, quem baixou, e não quero depender de serviço de terceiro pra algo tão simples.</p>
<p>Então fiz o que qualquer programador faria: sentei, escrevi um documento de especificação, e com ajuda do Claude Code (Claude Opus 4.6), construí o <a href="https://github.com/akitaonrails/FrankMega"target="_blank" rel="noopener"><strong>FrankMega</strong></a>, um clone simplificado do Mega, self-hosted, em Rails 8, em um único dia de trabalho. 21 commits, 3 horas de desenvolvimento ativo, 210 testes, zero dependência externa além do SQLite.</p>
<p><img src="https://github.com/akitaonrails/FrankMega/raw/master/docs/upload_screen.png" alt="frankmega"  loading="lazy" /></p>
<p>E antes que apareça o esperto nos comentários: &ldquo;ah, mas Vibe Coding é só prompt engineering, qualquer um faz&rdquo; &ndash; calma. Esse post é justamente pra mostrar que <strong>não é</strong>.</p>
<h2>Da Ideia ao Código: o IDEA.md<span class="hx:absolute hx:-mt-20" id="da-ideia-ao-código-o-ideamd"></span>
    <a href="#da-ideia-ao-c%c3%b3digo-o-ideamd" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Todo projeto começa com um documento. O meu foi o <a href="https://github.com/akitaonrails/FrankMega/blob/master/docs/IDEA.md"target="_blank" rel="noopener"><code>docs/IDEA.md</code></a> &ndash; 56 linhas descrevendo o que eu queria:</p>
<ul>
<li>Upload de arquivo único, com link compartilhável</li>
<li>Contador de downloads (padrão 5, configurável)</li>
<li>Expiração automática em até 24 horas</li>
<li>Autenticação completa: senha + 2FA + passkeys</li>
<li>Sem página de registro público &ndash; convites only</li>
<li>Admin cria convites, gerencia usuários, bane abusadores</li>
<li>Rate limiting agressivo, IP banning automático</li>
<li>Deploy via Docker + Cloudflare Tunnel no meu home server</li>
<li>Stack: Rails 8, SQLite, Tailwind CSS, Hotwire/Stimulus</li>
</ul>
<p>Eu deliberadamente incluí no final: <em>&ldquo;Sugira features importantes que possam ser relevantes pra um serviço assim.&rdquo;</em> Porque eu sei que um documento de spec nunca cobre tudo. E é aí que começa a parte interessante.</p>
<h2>O Big Bang: Commit #1<span class="hx:absolute hx:-mt-20" id="o-big-bang-commit-1"></span>
    <a href="#o-big-bang-commit-1" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O primeiro commit (<code>e277226</code>) às 12:17 entregou <strong>207 arquivos, 6.901 linhas de código</strong>. Num único commit. Sim, eu sei que é controverso, mas quando se está construindo do zero com AI, o primeiro commit é necessariamente grande.</p>
<p>O que veio nesse commit inicial:</p>
<ul>
<li>Autenticação completa com <code>has_secure_password</code> (bcrypt), TOTP via <code>rotp</code>, passkeys via <code>webauthn</code></li>
<li>Sistema de convites com códigos únicos e expiração</li>
<li>Admin panel completo (Users, Invitations, Files, MIME Types)</li>
<li>Upload com Active Storage + drag-and-drop via Stimulus</li>
<li>Links de download com hashes de 24 bytes (<code>SecureRandom.urlsafe_base64(24)</code>)</li>
<li>Rate limiting via Rack::Attack em todos os endpoints públicos</li>
<li>IP banning automático com modelo <code>Ban</code> e job assíncrono</li>
<li>Tema dark/light com Tailwind CSS inspirado no Mega.nz</li>
<li>73 testes (Minitest + FactoryBot)</li>
<li>Docker configuration com Puma + Thruster</li>
<li>Solid Queue/Cache/Cable &ndash; zero Redis</li>
</ul>
<p>Entenderam? <strong>Tudo isso no commit #1</strong>. O IDEA.md virou código funcional numa tacada. Mas o ponto é esse: <strong>o primeiro commit não era production-ready</strong>. Nem de longe.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-21_15-32-29.png" alt="tema light"  loading="lazy" /></p>
<h2>A Realidade: 20 Commits de Iteração<span class="hx:absolute hx:-mt-20" id="a-realidade-20-commits-de-iteração"></span>
    <a href="#a-realidade-20-commits-de-itera%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Depois do Big Bang vieram 20 commits ao longo de 3 horas. E é aqui que mora a verdade sobre desenvolvimento de software que nenhum tutorial de &ldquo;Vibe Coding&rdquo; vai te contar.</p>
<h3>Fase 1: &ldquo;Funciona na minha máquina&rdquo; (12:40 - 12:46)<span class="hx:absolute hx:-mt-20" id="fase-1-funciona-na-minha-máquina-1240---1246"></span>
    <a href="#fase-1-funciona-na-minha-m%c3%a1quina-1240---1246" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Quatro commits em 6 minutos. Sabe o que aconteceu? Fui buildar o Docker e <strong>quebrou</strong>. Faltava <code>libssl-dev</code>. Consertei. Daí o CSP (Content Security Policy) do <code>secure_headers</code> conflitava com os inline scripts do importmap do Rails. Consertei. Mudei a porta de 3000 pra 3100 pra não conflitar com outros serviços no meu server.</p>
<p>Olha o commit <code>389ebe8</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"># CSP agora é feito pelo Rails built-in, não pelo secure_headers</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Porque secure_headers não suporta nonces pra importmap inline scripts</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">csp</span> <span class="o">=</span> <span class="no">SecureHeaders</span><span class="o">::</span><span class="no">OPT_OUT</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 tipo de incompatibilidade entre gems você só descobre na hora. Nenhum LLM vai te avisar disso de antemão porque é uma combinação específica de versões.</p>
<h3>Fase 2: O Commit de Segurança &ndash; 22 Falhas Corrigidas (13:10)<span class="hx:absolute hx:-mt-20" id="fase-2-o-commit-de-segurança--22-falhas-corrigidas-1310"></span>
    <a href="#fase-2-o-commit-de-seguran%c3%a7a--22-falhas-corrigidas-1310" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Esse é o commit mais importante do projeto inteiro: <code>4a854a6</code>. 35 arquivos, 612 linhas, 22 issues de segurança corrigidas. Vou detalhar porque é aqui que se separa código de brinquedo de código de produção.</p>
<p>O que o commit inicial fazia de errado:</p>
<p><strong>CRÍTICO &ndash; Chaves de criptografia com fallback hardcoded:</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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># ANTES (perigoso):</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">active_record</span><span class="o">.</span><span class="n">encryption</span><span class="o">.</span><span class="n">primary_key</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">.</span><span class="n">fetch</span><span class="p">(</span><span class="s2">&#34;KEY&#34;</span><span class="p">,</span> <span class="s2">&#34;test-primary-key&#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"># DEPOIS (fail-fast):</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="no">Rails</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">production?</span>
</span></span><span class="line"><span class="cl">  <span class="n">config</span><span class="o">.</span><span class="n">active_record</span><span class="o">.</span><span class="n">encryption</span><span class="o">.</span><span class="n">primary_key</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">.</span><span class="n">fetch</span><span class="p">(</span><span class="s2">&#34;ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY&#34;</span><span class="p">)</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>Se o ENV var não existisse em produção, a app rodava feliz com a chave hardcoded. Qualquer pessoa que lesse o código-fonte conseguiria decriptar os segredos OTP de todos os usuários. Agora em produção, se faltar a variável, a app <strong>crasheia no boot</strong>. É o comportamento correto.</p>
<p><strong>CRÍTICO &ndash; Replay Attack no OTP:</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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># ANTES:</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">verify_otp</span><span class="p">(</span><span class="n">code</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">totp</span> <span class="o">=</span> <span class="no">ROTP</span><span class="o">::</span><span class="no">TOTP</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">otp_secret</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">totp</span><span class="o">.</span><span class="n">verify</span><span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="ss">drift_behind</span><span class="p">:</span> <span class="mi">30</span><span class="p">,</span> <span class="ss">drift_ahead</span><span class="p">:</span> <span class="mi">30</span><span class="p">)</span><span class="o">.</span><span class="n">present?</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"># DEPOIS:</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">verify_otp</span><span class="p">(</span><span class="n">code</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">totp</span> <span class="o">=</span> <span class="no">ROTP</span><span class="o">::</span><span class="no">TOTP</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">otp_secret</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">timestamp</span> <span class="o">=</span> <span class="n">totp</span><span class="o">.</span><span class="n">verify</span><span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="ss">drift_behind</span><span class="p">:</span> <span class="mi">30</span><span class="p">,</span> <span class="ss">drift_ahead</span><span class="p">:</span> <span class="mi">30</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="kp">false</span> <span class="k">unless</span> <span class="n">timestamp</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Previne replay: rejeita se o timestamp desse código já foi usado</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="n">last_otp_at</span><span class="o">.</span><span class="n">present?</span> <span class="o">&amp;&amp;</span> <span class="no">Time</span><span class="o">.</span><span class="n">at</span><span class="p">(</span><span class="n">timestamp</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="n">last_otp_at</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="kp">false</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">update_column</span><span class="p">(</span><span class="ss">:last_otp_at</span><span class="p">,</span> <span class="no">Time</span><span class="o">.</span><span class="n">at</span><span class="p">(</span><span class="n">timestamp</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">  <span class="kp">true</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>Todo tutorial de TOTP que você encontra na internet mostra o <code>verify(code).present?</code> e para aí. O problema: um código TOTP é válido por 30 segundos. Se eu interceptar seu código (shoulder surfing, câmera, clipboard), posso usá-lo múltiplas vezes nessa janela. O <code>last_otp_at</code> garante one-time use real.</p>
<p><strong>ALTO &ndash; Race Condition no Download Counter:</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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># ANTES (race condition):</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">increment_download!</span>
</span></span><span class="line"><span class="cl">  <span class="n">increment!</span><span class="p">(</span><span class="ss">:download_count</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="c1"># DEPOIS (atômico):</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">increment_download!</span>
</span></span><span class="line"><span class="cl">  <span class="nb">self</span><span class="o">.</span><span class="n">class</span><span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="nb">id</span><span class="p">:</span> <span class="nb">id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="s2">&#34;download_count &lt; max_downloads&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="s2">&#34;expires_at &gt; ?&#34;</span><span class="p">,</span> <span class="no">Time</span><span class="o">.</span><span class="n">current</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="o">.</span><span class="n">update_all</span><span class="p">(</span><span class="s2">&#34;download_count = download_count + 1&#34;</span><span class="p">)</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></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>increment!</code> faz: (1) lê o valor, (2) soma 1 em Ruby, (3) escreve de volta. Duas requisições simultâneas podem ler <code>4</code>, ambas escreverem <code>5</code>, e o usuário ganha dois downloads quando só tinha um. O <code>UPDATE ... WHERE</code> atômico no SQL garante que só uma requisição vence. E o <code>== 1</code> retorna sucesso/falha numa única operação, sem lock.</p>
<p>Isso é o clássico bug TOCTOU (Time of Check, Time of Use). Você nunca vai ver isso no desenvolvimento local com um browser. Só aparece em produção com requisições concorrentes.</p>
<p><strong>ALTO &ndash; Open Redirect após login:</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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># DEPOIS:</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">safe_redirect_url?</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">uri</span> <span class="o">=</span> <span class="no">URI</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">uri</span><span class="o">.</span><span class="n">host</span><span class="o">.</span><span class="n">nil?</span> <span class="o">||</span> <span class="n">uri</span><span class="o">.</span><span class="n">host</span> <span class="o">==</span> <span class="n">request</span><span class="o">.</span><span class="n">host</span>
</span></span><span class="line"><span class="cl"><span class="k">rescue</span> <span class="no">URI</span><span class="o">::</span><span class="no">InvalidURIError</span>
</span></span><span class="line"><span class="cl">  <span class="kp">false</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>Sem isso, um atacante podia enviar um link tipo <code>frankmega.com/session?return_to=https://evil.com</code> e após login o usuário seria redirecionado pro site malicioso.</p>
<p><strong>MÉDIO &ndash; Nonce do CSP previsível:</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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># ANTES:</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">content_security_policy_nonce_generator</span> <span class="o">=</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">request</span><span class="p">)</span> <span class="p">{</span> <span class="n">request</span><span class="o">.</span><span class="n">session</span><span class="o">.</span><span class="n">id</span><span class="o">.</span><span class="n">to_s</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># DEPOIS:</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">content_security_policy_nonce_generator</span> <span class="o">=</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">_request</span><span class="p">)</span> <span class="p">{</span> <span class="no">SecureRandom</span><span class="o">.</span><span class="n">base64</span><span class="p">(</span><span class="mi">16</span><span class="p">)</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>Session ID não muda entre requests. Se o nonce é previsível, um atacante pode injetar scripts com o nonce correto. Tem que ser aleatório por request.</p>
<p>E mais: IPv6 do Cloudflare adicionado aos trusted proxies, mínimo de 12 caracteres pra senha, MIME detection server-side via Marcel, verificação de file size pelo tempfile (não pelo header do client que pode ser forjado), proteção contra deletar/banir/rebaixar o último admin.</p>
<p>O total de testes pulou de 73 pra 109 com esse único commit.</p>
<h2>Como Rails Facilita Segurança<span class="hx:absolute hx:-mt-20" id="como-rails-facilita-segurança"></span>
    <a href="#como-rails-facilita-seguran%c3%a7a" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Rails torna MUITO mais fácil implementar segurança do que fazer do zero. Olha a comparação:</p>
<table>
  <thead>
      <tr>
          <th>Feature</th>
          <th>Rails</th>
          <th>Do zero</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CSRF</td>
          <td>Automático no <code>ActionController::Base</code></td>
          <td>Implementar tokens por sessão em todos os forms</td>
      </tr>
      <tr>
          <td>Hash de senha</td>
          <td><code>has_secure_password</code> (1 linha)</td>
          <td>Escolher algoritmo, salt, implementar verificação</td>
      </tr>
      <tr>
          <td>Parâmetros filtrados</td>
          <td><code>config.filter_parameters</code></td>
          <td>Hook no sistema de logging</td>
      </tr>
      <tr>
          <td>Strong params</td>
          <td><code>params.require().permit()</code></td>
          <td>Middleware de whitelist manual</td>
      </tr>
      <tr>
          <td>Criptografia de campos</td>
          <td><code>encrypts :otp_secret</code> (1 linha)</td>
          <td>Envelope encryption, key rotation, decrypt transparente</td>
      </tr>
      <tr>
          <td>CSP com nonces</td>
          <td>Config de 1 bloco</td>
          <td>Gerar nonces, injetar nos tags HTML, setar headers</td>
      </tr>
      <tr>
          <td>Rate limiting (Rails 8)</td>
          <td><code>rate_limit to: 10, within: 3.minutes</code></td>
          <td>Storage de contadores, lógica de sliding window</td>
      </tr>
      <tr>
          <td>SQL injection</td>
          <td>Parameterized queries by default</td>
          <td>Parametrizar manualmente cada query</td>
      </tr>
  </tbody>
</table>
<p>O Rails 8 em particular trouxe o <code>rate_limit</code> built-in no controller:</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">SessionsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
</span></span><span class="line"><span class="cl">  <span class="n">rate_limit</span> <span class="ss">to</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span> <span class="ss">within</span><span class="p">:</span> <span class="mi">3</span><span class="o">.</span><span class="n">minutes</span><span class="p">,</span> <span class="ss">only</span><span class="p">:</span> <span class="ss">:create</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">with</span><span class="p">:</span> <span class="o">-&gt;</span> <span class="p">{</span> <span class="n">redirect_to</span> <span class="n">new_session_path</span><span class="p">,</span> <span class="ss">alert</span><span class="p">:</span> <span class="n">t</span><span class="p">(</span><span class="s2">&#34;flash.sessions.create.rate_limit&#34;</span><span class="p">)</span> <span class="p">}</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>E a autenticação? Rails 8 agora vem com um <strong>authentication generator</strong> built-in. Não precisa mais do Devise pra coisas básicas. O <code>app/controllers/concerns/authentication.rb</code> do FrankMega é baseado nisso e estende com 2FA, passkeys e invitation-only registration.</p>
<h2>File Sharing Não É Só Download<span class="hx:absolute hx:-mt-20" id="file-sharing-não-é-só-download"></span>
    <a href="#file-sharing-n%c3%a3o-%c3%a9-s%c3%b3-download" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Uma coisa que muita gente não percebe: um serviço de file sharing tem uma superfície de ataque enorme. Não é só &ldquo;salvar arquivo, gerar link&rdquo;. Olha tudo que precisa ser considerado:</p>
<h3>Sanitização de Filename<span class="hx:absolute hx:-mt-20" id="sanitização-de-filename"></span>
    <a href="#sanitiza%c3%a7%c3%a3o-de-filename" 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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">sanitize_filename</span><span class="p">(</span><span class="nb">name</span><span class="p">,</span> <span class="n">content_type</span> <span class="o">=</span> <span class="kp">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">sanitized</span> <span class="o">=</span> <span class="no">File</span><span class="o">.</span><span class="n">basename</span><span class="p">(</span><span class="nb">name</span><span class="o">.</span><span class="n">to_s</span><span class="p">)</span>                          <span class="c1"># Strip path traversal</span>
</span></span><span class="line"><span class="cl">  <span class="n">sanitized</span> <span class="o">=</span> <span class="n">sanitized</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="s2">&#34;UTF-8&#34;</span><span class="p">,</span> <span class="ss">invalid</span><span class="p">:</span> <span class="ss">:replace</span><span class="p">,</span>      <span class="c1"># Handle invalid UTF-8</span>
</span></span><span class="line"><span class="cl">                                <span class="k">undef</span><span class="p">:</span> <span class="ss">:replace</span><span class="p">,</span> <span class="ss">replace</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="n">sanitized</span> <span class="o">=</span> <span class="n">sanitized</span><span class="o">.</span><span class="n">gsub</span><span class="p">(</span><span class="sr">/[\x00-\x1f\x7f\/\\:*?&#34;&lt;&gt;|]/</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">)</span> <span class="c1"># Control chars + unsafe chars</span>
</span></span><span class="line"><span class="cl">  <span class="n">sanitized</span> <span class="o">=</span> <span class="n">sanitized</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sr">/\A\.+/</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">)</span>                        <span class="c1"># Leading dots (hidden files)</span>
</span></span><span class="line"><span class="cl">  <span class="n">sanitized</span> <span class="o">=</span> <span class="n">sanitized</span><span class="o">.</span><span class="n">gsub</span><span class="p">(</span><span class="sr">/\s+/</span><span class="p">,</span> <span class="s2">&#34; &#34;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span>                  <span class="c1"># Collapse whitespace</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Windows reserved device names</span>
</span></span><span class="line"><span class="cl">  <span class="n">base_without_ext</span> <span class="o">=</span> <span class="n">sanitized</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sr">/\.[^.]*\z/</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="k">if</span> <span class="n">base_without_ext</span><span class="o">.</span><span class="n">match?</span><span class="p">(</span><span class="sr">/\A(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])\z/i</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">sanitized</span> <span class="o">=</span> <span class="s2">&#34;_</span><span class="si">#{</span><span class="n">sanitized</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></span><span class="line"><span class="cl">  <span class="n">sanitized</span> <span class="o">=</span> <span class="n">strip_extension_junk</span><span class="p">(</span><span class="n">sanitized</span><span class="p">,</span> <span class="n">content_type</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">sanitized</span> <span class="o">=</span> <span class="n">truncate_filename</span><span class="p">(</span><span class="n">sanitized</span><span class="p">,</span> <span class="mi">255</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">sanitized</span><span class="o">.</span><span class="n">presence</span> <span class="o">||</span> <span class="s2">&#34;unnamed_file&#34;</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>Cada linha existe por um motivo:</p>
<ul>
<li><code>File.basename</code> impede path traversal (<code>../../etc/passwd</code> vira <code>passwd</code>)</li>
<li>Strip de control chars impede null byte injection e terminal escape injection</li>
<li>Nomes reservados do Windows (CON, NUL, AUX, LPT1-9) causam problemas se o storage for acessado de um Windows</li>
<li>Truncamento pra 255 bytes (não caracteres!) preservando a extensão &ndash; um emoji de 4 bytes conta como 4 pro limite do ext4/NTFS</li>
<li><code>strip_extension_junk</code> trata nomes que vêm de URLs com parâmetros grudados: <code>photo.jpg_1280x720+quality=80</code> vira <code>photo.jpg</code></li>
</ul>
<p>E isso é server-side. Tem validação client-side também no Stimulus controller pra rejeitar antes do upload:</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">isInvalidFilename</span><span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="k">new</span> <span class="nx">Blob</span><span class="p">([</span><span class="nx">name</span><span class="p">]).</span><span class="nx">size</span> <span class="o">&gt;</span> <span class="mi">255</span><span class="p">)</span> <span class="k">return</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="sr">/[\x00-\x1f\x7f/:*?&#34;&lt;&gt;|\\]/</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">name</span><span class="p">))</span> <span class="k">return</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="kc">false</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>
<h3>MIME Type Detection Server-Side<span class="hx:absolute hx:-mt-20" id="mime-type-detection-server-side"></span>
    <a href="#mime-type-detection-server-side" 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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="vi">@shared_file</span><span class="o">.</span><span class="n">content_type</span> <span class="o">=</span> <span class="no">Marcel</span><span class="o">::</span><span class="no">MimeType</span><span class="o">.</span><span class="n">for</span><span class="p">(</span><span class="n">uploaded</span><span class="o">.</span><span class="n">tempfile</span><span class="p">,</span> <span class="nb">name</span><span class="p">:</span> <span class="n">uploaded</span><span class="o">.</span><span class="n">original_filename</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="vi">@shared_file</span><span class="o">.</span><span class="n">file_size</span> <span class="o">=</span> <span class="n">uploaded</span><span class="o">.</span><span class="n">tempfile</span><span class="o">.</span><span class="n">size</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 confia no <code>Content-Type</code> que o browser envia. O Marcel inspeciona os magic bytes do arquivo pra determinar o tipo real. E o file size vem do tempfile no disco, não do header <code>Content-Length</code> que pode ser forjado.</p>
<h3>Quotas e Disk Usage<span class="hx:absolute hx:-mt-20" id="quotas-e-disk-usage"></span>
    <a href="#quotas-e-disk-usage" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-21_16-00-52.png" alt="quota"  loading="lazy" /></p>
<p>Cada usuário tem uma quota de disco (5 GB default, admin pode customizar):</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">can_upload?</span><span class="p">(</span><span class="n">file_size</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">grace</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">config</span><span class="o">.</span><span class="n">x</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">disk_quota_grace_bytes</span>
</span></span><span class="line"><span class="cl">  <span class="p">(</span><span class="n">storage_used</span> <span class="o">+</span> <span class="n">file_size</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="p">(</span><span class="n">disk_quota</span> <span class="o">+</span> <span class="n">grace</span><span class="p">)</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>Sem quota, um único usuário malicioso enche o disco do servidor. Com o grace buffer de 10%, um upload que passa um pouco do limite é aceito pra não frustrar o usuário no edge case.</p>
<h3>Cleanup Automático<span class="hx:absolute hx:-mt-20" id="cleanup-automático"></span>
    <a href="#cleanup-autom%c3%a1tico" 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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">CleanupExpiredFilesJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
</span></span><span class="line"><span class="cl">  <span class="n">queue_as</span> <span class="ss">:default</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">perform</span>
</span></span><span class="line"><span class="cl">    <span class="no">SharedFile</span><span class="o">.</span><span class="n">inactive</span><span class="o">.</span><span class="n">find_each</span> <span class="k">do</span> <span class="o">|</span><span class="n">shared_file</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="n">shared_file</span><span class="o">.</span><span class="n">file</span><span class="o">.</span><span class="n">purge</span> <span class="k">if</span> <span class="n">shared_file</span><span class="o">.</span><span class="n">file</span><span class="o">.</span><span class="n">attached?</span>
</span></span><span class="line"><span class="cl">      <span class="n">shared_file</span><span class="o">.</span><span class="n">destroy</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>Roda a cada 15 minutos via Solid Queue. Se ninguém baixou em 24 horas, sumiu. Se bateu o limite de downloads, sumiu. Sem isso, disco do servidor enche e o serviço morre.</p>
<h3>IP Banning Automático<span class="hx:absolute hx:-mt-20" id="ip-banning-automático"></span>
    <a href="#ip-banning-autom%c3%a1tico" 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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">InvalidHashAccessJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
</span></span><span class="line"><span class="cl">  <span class="n">queue_as</span> <span class="ss">:default</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">perform</span><span class="p">(</span><span class="n">ip_address</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">security</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">config</span><span class="o">.</span><span class="n">x</span><span class="o">.</span><span class="n">security</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="k">unless</span> <span class="n">security</span><span class="o">.</span><span class="n">enable_banning</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">cache_key</span> <span class="o">=</span> <span class="s2">&#34;invalid_hash:</span><span class="si">#{</span><span class="n">ip_address</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">count</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">increment</span><span class="p">(</span><span class="n">cache_key</span><span class="p">,</span> <span class="mi">1</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">hour</span><span class="p">)</span> <span class="o">||</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">count</span> <span class="o">&gt;=</span> <span class="n">security</span><span class="o">.</span><span class="n">max_invalid_hash_attempts</span>
</span></span><span class="line"><span class="cl">      <span class="no">Ban</span><span class="o">.</span><span class="n">ban!</span><span class="p">(</span><span class="n">ip_address</span><span class="p">,</span> <span class="ss">reason</span><span class="p">:</span> <span class="s2">&#34;Repeated invalid download hash access&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">               <span class="ss">duration</span><span class="p">:</span> <span class="n">security</span><span class="o">.</span><span class="n">ban_duration</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="no">Rails</span><span class="o">.</span><span class="n">cache</span><span class="o">.</span><span class="n">delete</span><span class="p">(</span><span class="n">cache_key</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></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>Tentou 3 hashes inválidos? Banido por 1 hora. Isso impede enumeração de hashes. Com 24 bytes de entropia (192 bits, 2^192 possibilidades), brute force é computacionalmente inviável, mas a proteção extra não custa nada.</p>
<h3>Rate Limiting em Camadas<span class="hx:absolute hx:-mt-20" id="rate-limiting-em-camadas"></span>
    <a href="#rate-limiting-em-camadas" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O Rack::Attack opera na camada de middleware (antes do Rails processar):</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"># Login: 5 tentativas por minuto por IP</span>
</span></span><span class="line"><span class="cl"><span class="no">Rack</span><span class="o">::</span><span class="no">Attack</span><span class="o">.</span><span class="n">throttle</span><span class="p">(</span><span class="s2">&#34;logins/ip&#34;</span><span class="p">,</span> <span class="ss">limit</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span> <span class="ss">period</span><span class="p">:</span> <span class="mi">1</span><span class="o">.</span><span class="n">minute</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">req</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">req</span><span class="o">.</span><span class="n">ip</span> <span class="k">if</span> <span class="n">req</span><span class="o">.</span><span class="n">path</span> <span class="o">==</span> <span class="s2">&#34;/session&#34;</span> <span class="o">&amp;&amp;</span> <span class="n">req</span><span class="o">.</span><span class="n">post?</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"># Login: 5 tentativas por minuto por email</span>
</span></span><span class="line"><span class="cl"><span class="no">Rack</span><span class="o">::</span><span class="no">Attack</span><span class="o">.</span><span class="n">throttle</span><span class="p">(</span><span class="s2">&#34;logins/email&#34;</span><span class="p">,</span> <span class="ss">limit</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span> <span class="ss">period</span><span class="p">:</span> <span class="mi">1</span><span class="o">.</span><span class="n">minute</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">req</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="n">req</span><span class="o">.</span><span class="n">path</span> <span class="o">==</span> <span class="s2">&#34;/session&#34;</span> <span class="o">&amp;&amp;</span> <span class="n">req</span><span class="o">.</span><span class="n">post?</span>
</span></span><span class="line"><span class="cl">    <span class="n">req</span><span class="o">.</span><span class="n">params</span><span class="o">.</span><span class="n">dig</span><span class="p">(</span><span class="s2">&#34;email_address&#34;</span><span class="p">)</span><span class="o">&amp;.</span><span class="n">to_s</span><span class="o">&amp;.</span><span class="n">downcase</span><span class="o">&amp;.</span><span class="n">strip</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"># Downloads: 60 views e 30 downloads por minuto</span>
</span></span><span class="line"><span class="cl"><span class="no">Rack</span><span class="o">::</span><span class="no">Attack</span><span class="o">.</span><span class="n">throttle</span><span class="p">(</span><span class="s2">&#34;downloads_get/ip&#34;</span><span class="p">,</span> <span class="ss">limit</span><span class="p">:</span> <span class="mi">60</span><span class="p">,</span> <span class="ss">period</span><span class="p">:</span> <span class="mi">1</span><span class="o">.</span><span class="n">minute</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">req</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">req</span><span class="o">.</span><span class="n">ip</span> <span class="k">if</span> <span class="n">req</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">start_with?</span><span class="p">(</span><span class="s2">&#34;/d/&#34;</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">req</span><span class="o">.</span><span class="n">get?</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"># Geral: 300 requests por 5 minutos (exceto assets)</span>
</span></span><span class="line"><span class="cl"><span class="no">Rack</span><span class="o">::</span><span class="no">Attack</span><span class="o">.</span><span class="n">throttle</span><span class="p">(</span><span class="s2">&#34;requests/ip&#34;</span><span class="p">,</span> <span class="ss">limit</span><span class="p">:</span> <span class="mi">300</span><span class="p">,</span> <span class="ss">period</span><span class="p">:</span> <span class="mi">5</span><span class="o">.</span><span class="n">minutes</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">req</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">req</span><span class="o">.</span><span class="n">ip</span> <span class="k">unless</span> <span class="n">req</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">start_with?</span><span class="p">(</span><span class="s2">&#34;/assets&#34;</span><span class="p">)</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>Sete throttles diferentes pra cenários específicos. Mais o <code>rate_limit</code> do Rails 8 nos controllers. IPs banidos nem chegam ao Rails &ndash; são rejeitados na blocklist do middleware com status 403.</p>
<p>E os limites são configuráveis: em produção é 1x, em desenvolvimento é 10x pra não se trancar durante testes. Tudo centralizado no <code>config/initializers/security.rb</code>.</p>
<h2>A Saga do Download: 5 Commits em 21 Minutos<span class="hx:absolute hx:-mt-20" id="a-saga-do-download-5-commits-em-21-minutos"></span>
    <a href="#a-saga-do-download-5-commits-em-21-minutos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-21_15-31-25.png" alt="download"  loading="lazy" /></p>
<p>Essa parte é divertida porque mostra debugging real. Entre 14:44 e 15:05, eu fiz <strong>5 commits</strong> tentando fazer o download funcionar com Turbo Drive:</p>
<ol>
<li><code>cc3c23d</code> &ndash; Adicionei <code>data-turbo=false</code> no botão de download. <strong>Não funcionou.</strong></li>
<li><code>5224ed2</code> &ndash; Movi <code>data-turbo=false</code> pro <code>&lt;form&gt;</code>. <strong>Não funcionou.</strong></li>
<li><code>98b0d4f</code> &ndash; Abandonei o <code>redirect_to rails_blob_path</code> e usei <code>send_file</code> direto. <strong>Parcialmente funcionou.</strong></li>
<li><code>1fdf5f9</code> &ndash; Troquei de POST pra GET link. Funcionou mas&hellip; bots podem consumir downloads com GET.</li>
<li><code>4c74b27</code> &ndash; Voltei pra POST. Adicionei cleanup de URL artifacts nos filenames.</li>
</ol>
<p>O root cause: o Turbo Drive intercepta navegação agressivamente, e download de arquivo via <code>redirect_to</code> (que gera uma cadeia de 302 redirects do Active Storage) confunde o Turbo. A solução final: <code>send_file</code> direto do disco com <code>disposition: &quot;attachment&quot;</code> via POST com <code>data: { turbo: false }</code> no form.</p>
<p>Esse tipo de problema um LLM não resolve sozinho. Precisa testar no browser real, ver o que acontece, tentar, falhar, ajustar. É desenvolvimento iterativo puro.</p>
<h2>O Gap entre a Ideia e o Produto Final<span class="hx:absolute hx:-mt-20" id="o-gap-entre-a-ideia-e-o-produto-final"></span>
    <a href="#o-gap-entre-a-ideia-e-o-produto-final" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olha o que o IDEA.md não mencionava e que emergiu durante o desenvolvimento:</p>
<ol>
<li>Per-user disk quotas &ndash; sem isso um usuário enche o disco do server</li>
<li>I18n completo (EN + PT-BR) &ndash; 66 arquivos, ~250 strings extraídas</li>
<li>Terms of Service com aceite obrigatório</li>
<li>User self-deletion &ndash; requisito de privacidade</li>
<li>Blocked downloads de usuários banidos &ndash; seus links retornam 410 Gone</li>
<li>Client-side upload validation &ndash; verifica size, quota e filename antes do upload</li>
<li>Inline file previews &ndash; imagens, vídeo e áudio no download page sem consumir downloads</li>
<li>Styled error pages &ndash; em vez de plain text &ldquo;Not Found&rdquo;, páginas branded com i18n</li>
</ol>
<p>O IDEA.md dizia &ldquo;use activeadmin ou administrate&rdquo;. Na prática construí um admin panel custom com Tailwind porque o <code>administrate</code> tinha problemas de compatibilidade com Rails 8.1. O IDEA.md falava em &ldquo;progress bar se possível&rdquo;. Na prática usei drag-and-drop com preview de arquivo via Stimulus, que é melhor que progress bar.</p>
<p>Ninguém consegue prever todas as features no dia 1. Você descobre requisitos conforme constrói. E cada feature nova traz edge cases que precisam ser tratados.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-21_15-32-50.png" alt="pt"  loading="lazy" /></p>
<h2>Os Números<span class="hx:absolute hx:-mt-20" id="os-números"></span>
    <a href="#os-n%c3%bameros" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Ao final do projeto:</p>
<ul>
<li>21 commits em uma sessão de ~3 horas</li>
<li>210 testes (Minitest + FactoryBot), 513 assertions, zero failures</li>
<li>~3.100 linhas de código de aplicação (models, controllers, views, JS, CSS, configs)</li>
<li>~1.965 linhas de testes (incluindo testes de segurança dedicados)</li>
<li>9 models, 20 controllers, 36 views, 9 Stimulus controllers</li>
<li>24 medidas de segurança distintas em 7 camadas</li>
<li>CI: SimpleCov, RuboCop (zero offenses), Brakeman (zero warnings), bundler-audit (zero vulnerabilities)</li>
<li>Zero dependências externas além do SQLite. Sem Redis, sem PostgreSQL, sem serviço de fila externo</li>
</ul>
<h2>O Invitation-Only Server<span class="hx:absolute hx:-mt-20" id="o-invitation-only-server"></span>
    <a href="#o-invitation-only-server" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-21_15-31-54.png" alt="invite"  loading="lazy" /></p>
<p>O FrankMega não tem cadastro público. O fluxo é:</p>
<ol>
<li>Na primeira execução, sem nenhum usuário, aparece a tela de setup pra criar o admin</li>
<li>A rota <code>/setup</code> só existe enquanto <code>User.count.zero?</code> &ndash; depois some completamente:</li>
</ol>
<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">constraints</span><span class="p">(</span><span class="o">-&gt;</span><span class="p">(</span><span class="n">request</span><span class="p">)</span> <span class="p">{</span> <span class="no">User</span><span class="o">.</span><span class="n">count</span><span class="o">.</span><span class="n">zero?</span> <span class="p">})</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">get</span> <span class="s2">&#34;setup&#34;</span><span class="p">,</span> <span class="ss">to</span><span class="p">:</span> <span class="s2">&#34;setup#new&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">post</span> <span class="s2">&#34;setup&#34;</span><span class="p">,</span> <span class="ss">to</span><span class="p">:</span> <span class="s2">&#34;setup#create&#34;</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>
<ol start="3">
<li>O admin cria convites no painel admin (com expiração)</li>
<li>Cada convite gera um código único de 16 bytes</li>
<li>Cada código só pode ser usado uma vez (<code>with_lock</code> + transaction pra impedir race condition)</li>
<li>Após cadastrar, o convite é marcado como &ldquo;used&rdquo;</li>
</ol>
<p>Isso é perfeito pra um serviço pessoal/familiar. Controle total de quem acessa.</p>
<h2>Deploy: Docker + Cloudflare Tunnel<span class="hx:absolute hx:-mt-20" id="deploy-docker--cloudflare-tunnel"></span>
    <a href="#deploy-docker--cloudflare-tunnel" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-21_15-33-40.png" alt="share"  loading="lazy" /></p>
<p>Pra fechar, o tutorial de deploy, caso queira rodar no seu próprio Home Server Docker. Não é complicado mas tem detalhes que importam.</p>
<h3>O Dockerfile<span class="hx:absolute hx:-mt-20" id="o-dockerfile"></span>
    <a href="#o-dockerfile" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Multi-stage build em 3 estágios:</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"># Stage 1: Base (runtime)</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.4.8-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="k">RUN</span> apt-get install -y curl libjemalloc2 libvips sqlite3<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># jemalloc pra melhor gerenciamento de memória</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"># Stage 2: Build (throwaway)</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="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="k">RUN</span> apt-get install -y build-essential git libssl-dev<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Bundle install + asset precompilation com dummy env vars</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"># Stage 3: Final</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Copia gems e app, roda como user não-root (rails:rails, UID 1000)</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;./bin/thrust&#34;</span><span class="p">,</span> <span class="s2">&#34;./bin/rails&#34;</span><span class="p">,</span> <span class="s2">&#34;server&#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>O <code>bin/thrust</code> é o Thruster (do Basecamp) &ndash; um proxy HTTP na frente do Puma que faz: gzip/brotli compression, asset caching, X-Sendfile acceleration. Escuta na porta 80, proxeia pro Puma na 3000.</p>
<p>O entrypoint roda <code>db:prepare</code> e <code>db:seed</code> automaticamente no boot:</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 -e
</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="si">${</span><span class="p">@: -2:</span><span class="nv">1</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;./bin/rails&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="si">${</span><span class="p">@: -1:</span><span class="nv">1</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;server&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  ./bin/rails db:prepare
</span></span><span class="line"><span class="cl">  ./bin/rails db:seed
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="nb">exec</span> <span class="s2">&#34;</span><span class="si">${</span><span class="p">@</span><span class="si">}</span><span class="s2">&#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>
<h3>docker-compose.yml<span class="hx:absolute hx:-mt-20" id="docker-composeyml"></span>
    <a href="#docker-composeyml" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Dois serviços:</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">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">image</span><span class="p">:</span><span class="w"> </span><span class="l">akitaonrails/frankmega: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;3100: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">/home/seuuser/frankmega/uploads:/rails/storage/uploads</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/home/seuuser/frankmega/db:/rails/storage</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">SECRET_KEY_BASE</span><span class="p">:</span><span class="w"> </span><span class="l">${SECRET_KEY_BASE}</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">${HOST}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># ... mais ~15 variáveis</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></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tunnel</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">cloudflare/cloudflared: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 class="l">tunnel run</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">TUNNEL_TOKEN=${TUNNEL_TOKEN}</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">web</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></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 sidecar <code>cloudflare/cloudflared</code> cria um túnel outbound pra edge do Cloudflare. Não precisa abrir portas no firewall do home server. O Cloudflare faz SSL termination, DDoS protection, e roteia o tráfego pro container.</p>
<h3>Passo a Passo<span class="hx:absolute hx:-mt-20" id="passo-a-passo"></span>
    <a href="#passo-a-passo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>1. Crie o Tunnel no Cloudflare:</strong></p>
<ul>
<li>Cloudflare Zero Trust Dashboard &gt; Networks &gt; Tunnels &gt; Create</li>
<li>Tipo: Cloudflared</li>
<li>Copie o <code>TUNNEL_TOKEN</code></li>
<li>Configure o hostname: <code>frankmega.seudominio.com</code> apontando pra <code>http://web:80</code></li>
</ul>
<p><strong>2. Gere os segredos:</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-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># SECRET_KEY_BASE</span>
</span></span><span class="line"><span class="cl">openssl rand -hex <span class="m">64</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Chaves de criptografia (3 valores separados)</span>
</span></span><span class="line"><span class="cl">openssl rand -hex <span class="m">32</span>  <span class="c1"># PRIMARY_KEY</span>
</span></span><span class="line"><span class="cl">openssl rand -hex <span class="m">32</span>  <span class="c1"># DETERMINISTIC_KEY</span>
</span></span><span class="line"><span class="cl">openssl rand -hex <span class="m">32</span>  <span class="c1"># KEY_DERIVATION_SALT</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><strong>3. Configure o <code>.env</code>:</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-env" data-lang="env"><span class="line"><span class="cl"><span class="nv">SECRET_KEY_BASE</span><span class="o">=</span>&lt;gerado acima&gt;
</span></span><span class="line"><span class="cl"><span class="nv">RAILS_MASTER_KEY</span><span class="o">=</span>&lt;<span class="k">do</span> config/master.key&gt;
</span></span><span class="line"><span class="cl"><span class="nv">HOST</span><span class="o">=</span>frankmega.seudominio.com
</span></span><span class="line"><span class="cl"><span class="nv">WEBAUTHN_ORIGIN</span><span class="o">=</span>https://frankmega.seudominio.com
</span></span><span class="line"><span class="cl"><span class="nv">WEBAUTHN_RP_ID</span><span class="o">=</span>frankmega.seudominio.com
</span></span><span class="line"><span class="cl"><span class="nv">ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY</span><span class="o">=</span>&lt;gerado&gt;
</span></span><span class="line"><span class="cl"><span class="nv">ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY</span><span class="o">=</span>&lt;gerado&gt;
</span></span><span class="line"><span class="cl"><span class="nv">ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT</span><span class="o">=</span>&lt;gerado&gt;
</span></span><span class="line"><span class="cl"><span class="nv">TUNNEL_TOKEN</span><span class="o">=</span>&lt;<span class="k">do</span> Cloudflare&gt;
</span></span><span class="line"><span class="cl"><span class="nv">FORCE_SSL</span><span class="o">=</span><span class="nb">true</span>
</span></span><span class="line"><span class="cl"><span class="nv">APP_LOCALE</span><span class="o">=</span>pt-BR</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><strong>4. Suba:</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-bash" data-lang="bash"><span class="line"><span class="cl">docker compose pull
</span></span><span class="line"><span class="cl">docker compose 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><strong>5. Acesse</strong> <code>https://frankmega.seudominio.com</code>, crie a conta admin, comece a usar.</p>
<h3>Cuidados Importantes<span class="hx:absolute hx:-mt-20" id="cuidados-importantes"></span>
    <a href="#cuidados-importantes" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><code>FORCE_SSL=false</code> pra testar localmente em <code>http://localhost:3100</code> sem Cloudflare na frente, senão entra em redirect loop</li>
<li>WebAuthn origin precisa bater exatamente, incluindo o <code>https://</code>. Se errar, passkeys silenciosamente não funcionam</li>
<li>IPs do Cloudflare são hardcoded no initializer (porque o gem <code>cloudflare-rails</code> não é compatível com Rails 8.1). Se o Cloudflare mudar os ranges, precisa rebuildar a imagem</li>
<li>Chaves de criptografia são permanentes. Se trocar depois que usuários configuraram 2FA, os segredos OTP ficam ilegíveis e eles perdem acesso</li>
<li>Cloudflare free tem limite de 100 MB de upload. Pro plano grátis, arquivos acima disso não passam pelo tunnel. A app permite até 1 GB mas o Cloudflare é o gargalo</li>
<li>O container roda como UID 1000. Se usar bind mounts ao invés de named volumes, o diretório precisa ser owned pelo UID 1000</li>
<li>São 4 bancos SQLite separados em produção (app, cache, queue, cable), todos no volume <code>db_data</code>. Backup é copiar o volume inteiro</li>
</ul>
<h2>O Verdadeiro Ponto Sobre &ldquo;Vibe Coding&rdquo;<span class="hx:absolute hx:-mt-20" id="o-verdadeiro-ponto-sobre-vibe-coding"></span>
    <a href="#o-verdadeiro-ponto-sobre-vibe-coding" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O Claude Code é absurdamente produtivo. Eu não teria feito tudo isso em 3 horas sem ele. Lembra de antigamente que você dava murro em ponta de faca por 1 semana só pra fazer um login funcionar? Pois é.</p>
<p>O commit de I18n sozinho (66 arquivos, ~250 strings extraídas pra dois idiomas) levaria um dia inteiro manualmente.</p>
<p>Mas o ponto que ninguém quer ouvir: <strong>o LLM não tomou as decisões de segurança</strong>. Eu pedi pra ele fazer um audit e ele encontrou os problemas, mas eu precisei saber pedir. E precisei saber quais perguntas fazer. <em>&ldquo;Revise a segurança&rdquo;</em> é uma instrução vaga. <em>&ldquo;Verifique se tem race condition no increment do download counter&rdquo;</em> é uma instrução que só quem sabe o que é TOCTOU consegue dar.</p>
<p>Aquelas 56 linhas do IDEA.md carregam décadas de experiência em desenvolvimento web. Eu sabia pedir rate limiting porque já vi serviços derrubados por bots. Sabia pedir IP banning porque já lidei com abuse. Sabia pedir download counter atômico porque entendo concorrência.</p>
<p>Se um iniciante escrevesse o mesmo IDEA.md, provavelmente não teria metade dessas preocupações. E o LLM não iria sugerir espontaneamente. O resultado seria um serviço funcional, bonito, e completamente inseguro.</p>
<p>O valor do programador experiente no Vibe Coding não é escrever código. É saber o que pedir e revisar o que foi gerado. A experiência é o filtro entre &ldquo;funciona&rdquo; e &ldquo;funciona em produção&rdquo;.</p>
<p>Os 210 testes e 24 medidas de segurança não vieram de prompt engineering. Vieram de saber o que testar e por quê.</p>
<p>O código é open source sob AGPL-3.0: <a href="https://github.com/akitaonrails/frankmega"target="_blank" rel="noopener">github.com/akitaonrails/frank_mega</a>. Deploy no seu server, fucem o código, aprendam com os commits. E se acharem mais falhas de segurança, me avisem.</p>
<p>Lembrem-se:</p>
<blockquote>
  <p>A IA é o espelho da sua própria competência.</p>

</blockquote>
]]></content:encoded><category>vibe-coding</category><category>rails</category><category>homelab</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>Jobs Assíncronos que sobrevivem ao Caos | Bastidores do The M.Akita Chronicles</title><link>https://akitaonrails.github.io/2026/02/19/jobs-asssincronos-que-sobrevivem-ao-caos-bastidores-do-the-m-akita-chronicles/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/02/19/jobs-asssincronos-que-sobrevivem-ao-caos-bastidores-do-the-m-akita-chronicles/</guid><pubDate>Thu, 19 Feb 2026 01:10:08 +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 5.&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;Vou ser direto: a maioria dos desenvolvedores trata background jobs como se fossem scripts que rodam uma vez e pronto. &amp;ldquo;Ah, coloca num Sidekiq e tá bom.&amp;rdquo; Não, &lt;strong&gt;não tá bom&lt;/strong&gt;. Jobs que processam coisas importantes — enviar emails, publicar conteúdo, chamar APIs externas — precisam ser tratados como cidadãos de primeira classe na sua arquitetura.&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 5.</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>Vou ser direto: a maioria dos desenvolvedores trata background jobs como se fossem scripts que rodam uma vez e pronto. &ldquo;Ah, coloca num Sidekiq e tá bom.&rdquo; Não, <strong>não tá bom</strong>. Jobs que processam coisas importantes — enviar emails, publicar conteúdo, chamar APIs externas — precisam ser tratados como cidadãos de primeira classe na sua arquitetura.</p>
<p>Neste post vou mostrar como o Rails 8 com ActiveJob e SolidQueue mudou minha perspectiva sobre processamento assíncrono. Não é teoria — são padrões que emergiram de um sistema real rodando em produção toda semana.</p>
<h2>O Problema: Jobs São Frágeis por Natureza<span class="hx:absolute hx:-mt-20" id="o-problema-jobs-são-frágeis-por-natureza"></span>
    <a href="#o-problema-jobs-s%c3%a3o-fr%c3%a1geis-por-natureza" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pense no cenário: você tem um job que monta uma newsletter, publica num blog via API do GitHub, espera um podcast ficar pronto, e depois dispara emails para centenas de assinantes. Qualquer passo pode falhar. A API pode dar timeout. O podcast pode demorar mais que o esperado. O servidor pode reiniciar no meio do envio.</p>
<p>Se você tratar isso como um script linear, vai sofrer. A pergunta certa não é &ldquo;como faço esse job rodar?&rdquo; — é <strong>&ldquo;como faço esse job se recuperar quando algo der errado?&rdquo;</strong></p>
<h2>Padrão 1: retry_on Com Exceções Específicas<span class="hx:absolute hx:-mt-20" id="padrão-1-retry_on-com-exceções-específicas"></span>
    <a href="#padr%c3%a3o-1-retry_on-com-exce%c3%a7%c3%b5es-espec%c3%adficas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O <code>retry_on</code> do ActiveJob é absurdamente poderoso, mas a maioria usa errado. Olha o anti-padrão clássico:</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">MeuJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
</span></span><span class="line"><span class="cl">  <span class="n">retry_on</span> <span class="no">StandardError</span><span class="p">,</span> <span class="ss">wait</span><span class="p">:</span> <span class="mi">5</span><span class="o">.</span><span class="n">seconds</span><span class="p">,</span> <span class="ss">attempts</span><span class="p">:</span> <span class="mi">3</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>Isso re-tenta <strong>qualquer</strong> erro 3 vezes e desiste. Serve pra quase nada no mundo real. O padrão que funciona é criar exceções específicas para estados transitórios:</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">PodcastNotReady</span> <span class="o">&lt;</span> <span class="no">StandardError</span><span class="p">;</span> <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">PublishAndSendJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
</span></span><span class="line"><span class="cl">  <span class="n">retry_on</span> <span class="no">PodcastNotReady</span><span class="p">,</span> <span class="ss">wait</span><span class="p">:</span> <span class="mi">15</span><span class="o">.</span><span class="n">minutes</span><span class="p">,</span> <span class="ss">attempts</span><span class="p">:</span> <span class="mi">16</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">perform</span><span class="p">(</span><span class="n">newsletter_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">metadata</span> <span class="o">=</span> <span class="n">check_podcast_metadata</span><span class="p">(</span><span class="n">newsletter_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">raise</span> <span class="no">PodcastNotReady</span><span class="p">,</span> <span class="s2">&#34;Aguardando podcast&#34;</span> <span class="k">unless</span> <span class="n">metadata</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># continua o trabalho...</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>Percebe o que está acontecendo? O job espera até <strong>4 horas</strong> (15min × 16) pelo podcast ficar pronto, fazendo polling a cada 15 minutos. Não é um loop infinito — tem um limite claro. E quando estoura esse limite, você trata o timeout explicitamente:</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">rescue</span> <span class="no">PodcastNotReady</span> <span class="o">=&gt;</span> <span class="n">e</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="n">executions</span> <span class="o">&gt;=</span> <span class="mi">16</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Publica sem podcast e avisa a equipe</span>
</span></span><span class="line"><span class="cl">    <span class="n">publish_without_podcast</span><span class="p">(</span><span class="n">newsletter</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">notify_team</span><span class="p">(</span><span class="s2">&#34;Podcast não ficou pronto a tempo&#34;</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="k">raise</span> <span class="c1"># re-raise pra SolidQueue agendar o próximo retry</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>Isso é <strong>degradação graciosa</strong>. O sistema não para porque um componente falhou — ele se adapta e continua.</p>
<h2>Padrão 2: Locks Distribuídos<span class="hx:absolute hx:-mt-20" id="padrão-2-locks-distribuídos"></span>
    <a href="#padr%c3%a3o-2-locks-distribu%c3%addos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Quando você tem múltiplos caminhos que podem disparar o mesmo job (um cron de segurança, um trigger manual, uma API), precisa garantir que só uma instância roda por vez.</p>
<p>O conceito é simples: um lock baseado em arquivo (ou banco) que expira automaticamente:</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">DeployLock</span>
</span></span><span class="line"><span class="cl">  <span class="no">LOCK_DIR</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">root</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;tmp/locks&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="no">DEFAULT_TTL</span> <span class="o">=</span> <span class="mi">30</span><span class="o">.</span><span class="n">minutes</span>
</span></span><span class="line"><span class="cl">
</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">with_lock</span><span class="p">(</span><span class="nb">name</span><span class="p">,</span> <span class="ss">ttl</span><span class="p">:</span> <span class="no">DEFAULT_TTL</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">acquire!</span><span class="p">(</span><span class="nb">name</span><span class="p">,</span> <span class="ss">ttl</span><span class="p">:</span> <span class="n">ttl</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">yield</span>
</span></span><span class="line"><span class="cl">  <span class="k">ensure</span>
</span></span><span class="line"><span class="cl">    <span class="n">release</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">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>Com SQLite, isso é trivial — sem Redis, sem coordenação externa. O lock tem TTL pra não travar se o processo morrer. E o <code>ensure</code> garante que libera mesmo se o bloco levantar exceção.</p>
<h2>Padrão 3: Envio Atômico de Emails<span class="hx:absolute hx:-mt-20" id="padrão-3-envio-atômico-de-emails"></span>
    <a href="#padr%c3%a3o-3-envio-at%c3%b4mico-de-emails" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Aqui é onde a maioria dos sistemas de newsletter falha espetacularmente. O cenário: você está enviando 500 emails e o servidor reinicia no email 247. O que acontece quando o job re-executa?</p>
<p>Se você só iterou uma lista, vai reenviar os primeiros 247. Seus assinantes vão adorar receber a newsletter duas vezes.</p>
<p>A solução é <strong>claiming atômico</strong> por destinatário:</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"># Antes de enviar, cria um registro por assinante</span>
</span></span><span class="line"><span class="cl"><span class="n">subscribers</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="nb">sub</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="no">EmailDelivery</span><span class="o">.</span><span class="n">create!</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="ss">newsletter</span><span class="p">:</span> <span class="n">newsletter</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">subscriber</span><span class="p">:</span> <span class="nb">sub</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">status</span><span class="p">:</span> <span class="s2">&#34;pending&#34;</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></span><span class="line"><span class="cl"><span class="c1"># Na hora de enviar, faz claim atômico</span>
</span></span><span class="line"><span class="cl"><span class="n">delivery</span> <span class="o">=</span> <span class="no">EmailDelivery</span>
</span></span><span class="line"><span class="cl">  <span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="ss">status</span><span class="p">:</span> <span class="o">[</span><span class="s2">&#34;pending&#34;</span><span class="p">,</span> <span class="s2">&#34;failed&#34;</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">.</span><span class="n">lock</span>
</span></span><span class="line"><span class="cl">  <span class="o">.</span><span class="n">first</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">delivery</span><span class="o">.</span><span class="n">update!</span><span class="p">(</span><span class="ss">status</span><span class="p">:</span> <span class="s2">&#34;sending&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">send_email</span><span class="p">(</span><span class="n">delivery</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">delivery</span><span class="o">.</span><span class="n">update!</span><span class="p">(</span><span class="ss">status</span><span class="p">:</span> <span class="s2">&#34;sent&#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>Se o servidor morrer entre <code>sending</code> e <code>sent</code>, o registro fica como &ldquo;sending&rdquo; — e um job de recuperação (<code>RecoverStaleDeliveriesJob</code>) periodicamente move esses registros para &ldquo;unknown&rdquo; depois de um timeout, pra que nunca sejam automaticamente reenviados. Emails ambíguos <strong>nunca</strong> são reenviados automaticamente. Esse é o tipo de detalhe que separa um sistema hobby de um sistema de produção.</p>
<h2>Padrão 4: Jobs Orquestradores<span class="hx:absolute hx:-mt-20" id="padrão-4-jobs-orquestradores"></span>
    <a href="#padr%c3%a3o-4-jobs-orquestradores" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Um erro comum é colocar lógica demais num único job. O padrão que funciona é ter <strong>jobs orquestradores</strong> que delegam para jobs especializados:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>PublishAndSendJob (orquestrador)
  ├── Espera podcast (retry_on PodcastNotReady)
  ├── PublishToBlogJob.publish(newsletter)
  └── SendNewsletterJob.perform_now(newsletter.id)
        └── SendSingleEmailJob (por assinante)</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 orquestrador coordena a sequência. Cada job especializado sabe fazer exatamente uma coisa e pode ser re-executado independentemente. Se o envio de emails falhar, você pode re-disparar só o <code>SendNewsletterJob</code> sem republicar o blog.</p>
<h2>Padrão 5: Safety Nets com Cron<span class="hx:absolute hx:-mt-20" id="padrão-5-safety-nets-com-cron"></span>
    <a href="#padr%c3%a3o-5-safety-nets-com-cron" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Não confie em uma única cadeia de execução. O cron do SolidQueue (<code>config/recurring.yml</code>) serve como rede de seguranç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-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">send_newsletter</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">class</span><span class="p">:</span><span class="w"> </span><span class="l">SendNewsletterJob</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 12 * * 1&#34;</span><span class="w">  </span><span class="c"># Segunda 9h BRT (UTC-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>
<p>Se o <code>PublishAndSendJob</code> das 7h falhou catastroficamente, o cron das 9h dispara o <code>SendNewsletterJob</code> como fallback. O job verifica se a newsletter já foi enviada e faz no-op se sim. <strong>Idempotência</strong> é a palavra-chave aqui — o job pode rodar quantas vezes quiser e o resultado é sempre o mesmo.</p>
<p>Mas cuidado com o inverso: um job que se reagenda infinitamente quando não encontra trabalho é uma bomba-relógio. Se o job não encontra newsletter pronta, ele simplesmente retorna. O cron cuida de tentar de novo na próxima semana.</p>
<h2>Padrão 6: Notificações de Status<span class="hx:absolute hx:-mt-20" id="padrão-6-notificações-de-status"></span>
    <a href="#padr%c3%a3o-6-notifica%c3%a7%c3%b5es-de-status" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Todo job de longa duração deve comunicar seu progresso. Um concern 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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">DiscordStatus</span>
</span></span><span class="line"><span class="cl">  <span class="kp">extend</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Concern</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">notify_start</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="no">DiscordNotifier</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="ss">channel</span><span class="p">:</span> <span class="n">status_channel</span><span class="p">,</span> <span class="ss">text</span><span class="p">:</span> <span class="s2">&#34;▶️ </span><span class="si">#{</span><span class="n">message</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></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">notify_done</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="no">DiscordNotifier</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="ss">channel</span><span class="p">:</span> <span class="n">status_channel</span><span class="p">,</span> <span class="ss">text</span><span class="p">:</span> <span class="s2">&#34;✅ </span><span class="si">#{</span><span class="n">message</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></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">notify_error</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="no">DiscordNotifier</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="ss">channel</span><span class="p">:</span> <span class="n">status_channel</span><span class="p">,</span> <span class="ss">text</span><span class="p">:</span> <span class="s2">&#34;❌ </span><span class="si">#{</span><span class="n">message</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>Cada job inclui o concern e chama <code>notify_start</code> no início, <code>notify_done</code> no sucesso, <code>notify_error</code> no rescue. Quando algo dá errado às 3 da manhã, você acorda e vê exatamente onde parou — sem precisar cavar logs.</p>
<h2>SolidQueue: O Fim da Dependência de Redis<span class="hx:absolute hx:-mt-20" id="solidqueue-o-fim-da-dependência-de-redis"></span>
    <a href="#solidqueue-o-fim-da-depend%c3%aancia-de-redis" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Uma nota sobre SolidQueue, que veio como padrão no Rails 8: usar o mesmo banco SQLite para jobs e dados da aplicação simplifica <strong>drasticamente</strong> a operação. Não precisa de Redis rodando separado. Não precisa se preocupar com o Redis reiniciar e perder jobs que estavam na memória.</p>
<p>Os jobs ficam persistidos no banco. Se o servidor reiniciar, eles estão lá esperando. O retry state é preservado. É absurdamente mais simples que a alternativa, e pra 99% dos casos, a performance é mais que suficiente.</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 Rails 8 com ActiveJob e SolidQueue não inventou nada revolucionário. O que ele fez foi deixar ridiculamente fácil de implementar padrões que antes precisavam de infraestrutura pesada:</p>
<ul>
<li><strong>retry_on</strong> com exceções específicas e limites claros</li>
<li><strong>Locks distribuídos</strong> com TTL automático</li>
<li><strong>Claiming atômico</strong> pra operações que não podem duplicar</li>
<li><strong>Jobs orquestradores</strong> que delegam e se recuperam</li>
<li><strong>Crons de segurança</strong> como fallback</li>
<li><strong>Notificações</strong> pra visibilidade operacional</li>
</ul>
<p>Nenhum desses padrões é complicado sozinho. Mas juntos, é o que faz a diferença entre um sistema que quebra na primeira falha e um que aguenta o tranco em produção — e te deixa dormir tranquilo na segunda-feira.</p>
]]></content:encoded><category>the-m-akita-chronicles</category><category>rails</category><category>sistemas-distribuidos</category></item><item><title>[Akitando] #59 - A História de Ruby on Rails | Por que deu certo?</title><link>https://akitaonrails.github.io/2019/08/21/akitando-59-a-historia-de-ruby-on-rails-por-que-deu-certo/</link><guid isPermaLink="true">https://akitaonrails.github.io/2019/08/21/akitando-59-a-historia-de-ruby-on-rails-por-que-deu-certo/</guid><pubDate>Wed, 21 Aug 2019 17:00:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/oEorhw5r2Do"
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;15 anos atrás Ruby on Rails é lançado, e todo mundo tinha certeza que logo desapareceria.&lt;/p&gt;
&lt;p&gt;Ruby on Rails 6.0 acabou de ser lançado.&lt;/p&gt;
&lt;p&gt;Why, The Lucky Stiff sumiu faz 10 anos.&lt;/p&gt;
&lt;p&gt;Estou iniciando o 2o ano do meu canal e acho que agora é uma boa hora para contar essa história.&lt;/p&gt;
&lt;p&gt;Esta é a história que eu queria ter contado logo que comecei o canal, 1 ano atrás. Mas eu precisava explicar muitas outras histórias antes para dar contexto. Minha própria carreira inicial. A história da computação e da Web nos anos 90 a 2000. Conceitos como Agilidade e Startups.&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/oEorhw5r2Do"
    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>15 anos atrás Ruby on Rails é lançado, e todo mundo tinha certeza que logo desapareceria.</p>
<p>Ruby on Rails 6.0 acabou de ser lançado.</p>
<p>Why, The Lucky Stiff sumiu faz 10 anos.</p>
<p>Estou iniciando o 2o ano do meu canal e acho que agora é uma boa hora para contar essa história.</p>
<p>Esta é a história que eu queria ter contado logo que comecei o canal, 1 ano atrás. Mas eu precisava explicar muitas outras histórias antes para dar contexto. Minha própria carreira inicial. A história da computação e da Web nos anos 90 a 2000. Conceitos como Agilidade e Startups.</p>
<p>Agora que contei boa parte do que queria, se vocês assistiram os vídeos do último ano, devem estar equipados pra entender esta parte da história. Neste episódio vou compilar 8 anos de história, de onde veio Ruby on Rails, e porque ele estava destinado a dar certo mesmo que todos os &ldquo;experts&rdquo; achassem que não daria.</p>
<p>Se quiser aprender mais, entre no grupo de Rails do Facebook: <a href="https://www.facebook.com/groups/rubyonrailsbrasil/"target="_blank" rel="noopener">https://www.facebook.com/groups/rubyonrailsbrasil/</a></p>
<p>É o maior e melhor grupo que eu conheço e eles estão sempre compartilhando dicas, materiais e cursos, fora ofertas de emprego.</p>
<p>Disclaimer:</p>
<p>Esta história é contada sob meu ponto de vista pessoal tendo participado ativamente de muitos eventos. Posso ter esquecido alguns momentos e nomes importantes, então mande nos comentários se eu perdi alguma coisa.</p>
<p>Logo de cara, só depois que terminei de subir o video notei que esqueci de mencionar o pessoal do Rio Grande do Sul, do antigo Mailee e que fez o Catarse, o Kickstarter brasileiro!! Perdoem-me por ter esquecido.</p>
<p>Links:</p>
<ul>
<li>
<p>15 Minute Blog Demo 2005 (<a href="https://www.youtube.com/watch?v=Gzj723LkRJY"target="_blank" rel="noopener">https://www.youtube.com/watch?v=Gzj723LkRJY</a>)</p>
</li>
<li>
<p>Ten Years Of Test Driven Development (<a href="http://wiki.c2.com/?TenYearsOfTestDrivenDevelopment"target="_blank" rel="noopener">http://wiki.c2.com/?TenYearsOfTestDrivenDevelopment</a>)</p>
</li>
<li>
<p>The history of testing framework in Ruby (<a href="http://rubykaigi.org/2015/presentations/kou/"target="_blank" rel="noopener">http://rubykaigi.org/2015/presentations/kou/</a>)</p>
</li>
<li>
<p>Why’s Poignant Guide to Ruby (<a href="https://poignant.guide"target="_blank" rel="noopener">https://poignant.guide</a>)</p>
</li>
<li>
<p>Why The Lucky Stiff Documentary (<a href="https://www.youtube.com/watch?v=64anPPVUw5U"target="_blank" rel="noopener">https://www.youtube.com/watch?v=64anPPVUw5U</a>)</p>
</li>
<li>
<p>Ruby Koans (<a href="http://rubykoans.com"target="_blank" rel="noopener">http://rubykoans.com</a>)</p>
</li>
<li>
<p>Ruby Quiz (<a href="http://rubyquiz.com"target="_blank" rel="noopener">http://rubyquiz.com</a>)</p>
</li>
<li>
<p>RailsCasts (<a href="http://railscasts.com"target="_blank" rel="noopener">http://railscasts.com</a>)</p>
</li>
<li>
<p>RubyTapas (<a href="https://www.rubytapas.com/"target="_blank" rel="noopener">www.rubytapas.com/</a>)</p>
</li>
<li>
<p>CodeSchool: Rails for Zombies (<a href="https://www.pluralsight.com/courses/code-school-rails-for-zombies"target="_blank" rel="noopener">https://www.pluralsight.com/courses/code-school-rails-for-zombies</a>)</p>
</li>
<li>
<p>Blog Igvita (<a href="https://www.igvita.com"target="_blank" rel="noopener">https://www.igvita.com</a>)</p>
</li>
<li>
<p>Rails and Merb Merge (<a href="https://yehudakatz.com/2008/12/23/rails-and-merb-merge/"target="_blank" rel="noopener">https://yehudakatz.com/2008/12/23/rails-and-merb-merge/</a>)</p>
</li>
<li>
<p>RailsEnvy videos (<a href="https://www.youtube.com/watch?v=YZeZsZEEpno&amp;list=PLfBoZfyUGYQZ--h9B4e_tmyob8tbqOTKd"target="_blank" rel="noopener">https://www.youtube.com/watch?v=YZeZsZEEpno&list=PLfBoZfyUGYQZ--h9B4e_tmyob8tbqOTKd</a>)</p>
</li>
<li>
<p>The Decision to Leave Code School (<a href="https://www.greggpollack.com/the-decision-to-leave-code-school/"target="_blank" rel="noopener">https://www.greggpollack.com/the-decision-to-leave-code-school/</a>)</p>
</li>
<li>
<p>Travis CI joins the Idera family (<a href="https://blog.travis-ci.com/2019-01-23-travis-ci-joins-idera-inc"target="_blank" rel="noopener">https://blog.travis-ci.com/2019-01-23-travis-ci-joins-idera-inc</a>)</p>
</li>
<li>
<p>Reia (<a href="https://github.com/tarcieri/reia"target="_blank" rel="noopener">https://github.com/tarcieri/reia</a>)</p>
</li>
<li>
<p>11 types of apps you can build with Ruby on Rails (<a href="https://naturaily.com/blog/ruby-on-rails-apps"target="_blank" rel="noopener">https://naturaily.com/blog/ruby-on-rails-apps</a>)</p>
</li>
<li>
<p>Who gives a F*** about Rails in 2019? (<a href="https://naturaily.com/blog/who-gives-f-about-rails?utm_source=linkedin&amp;utm_medium=post"target="_blank" rel="noopener">https://naturaily.com/blog/who-gives-f-about-rails?utm_source=linkedin&utm_medium=post</a>)</p>
</li>
<li>
<p>Why should you choose Ruby on Rails for your MVP? (<a href="https://prograils.com/posts/why-should-you-choose-ruby-on-rails-for-your-mvp?utm_source=rubydaily&amp;utm_medium=referral&amp;utm_campaign=rails-mvp-1"target="_blank" rel="noopener">https://prograils.com/posts/why-should-you-choose-ruby-on-rails-for-your-mvp?utm_source=rubydaily&utm_medium=referral&utm_campaign=rails-mvp-1</a>)</p>
</li>
<li>
<p>AkitaOnRails: Retrospectiva on Rails - 10 Anos e Muito Mais! (<a href="http://www.akitaonrails.com/2014/08/26/retrospectiva-on-rails-10-anos-e-muito-mais"target="_blank" rel="noopener">http://www.akitaonrails.com/2014/08/26/retrospectiva-on-rails-10-anos-e-muito-mais</a>)</p>
</li>
<li>
<p>AkitaOnRails: Entrevistas (<a href="https://www.akitaonrails.com/interview"target="_blank" rel="noopener">https://www.akitaonrails.com/interview</a>)</p>
</li>
<li>
<p>AkitaOnRails: _Why, Ruby Dramas, and Dynamiting Courtlandt (<a href="http://www.akitaonrails.com/2016/09/26/os-proximos-10-anos"target="_blank" rel="noopener">http://www.akitaonrails.com/2016/09/26/os-proximos-10-anos</a>)</p>
</li>
<li>
<p>AkitaOnRails: The Next 10 Years (<a href="http://www.akitaonrails.com/2016/09/26/os-proximos-10-anos"target="_blank" rel="noopener">http://www.akitaonrails.com/2016/09/26/os-proximos-10-anos</a>)</p>
</li>
</ul>
<p>R.I.P.:</p>
<ul>
<li>Remembering Jason Seifer (<a href="https://www.greggpollack.com/remembering-jason-seifer/"target="_blank" rel="noopener">https://www.greggpollack.com/remembering-jason-seifer/</a>)</li>
<li>EZRA ZYGMUNTOWICZ: IN MEMORIAM (<a href="https://www.engineyard.com/blog/ezra-zygmuntowicz-in-memorium"target="_blank" rel="noopener">https://www.engineyard.com/blog/ezra-zygmuntowicz-in-memorium</a>)</li>
<li>REMEMBERING JIM WEIRICH (<a href="https://lithespeed.com/remembering-jim-weirich/"target="_blank" rel="noopener">https://lithespeed.com/remembering-jim-weirich/</a>)</li>
<li>Remembering James Golick (<a href="https://medium.com/@benkaufman/remembering-james-golick-23c1dc3ab920"target="_blank" rel="noopener">https://medium.com/@benkaufman/remembering-james-golick-23c1dc3ab920</a>)</li>
<li>GIVING OPEN-SOURCE PROJECTS LIFE AFTER A DEVELOPER&rsquo;S DEATH (<a href="https://www.wired.com/story/giving-open-source-projects-life-after-a-developers-death/"target="_blank" rel="noopener">https://www.wired.com/story/giving-open-source-projects-life-after-a-developers-death/</a>)</li>
</ul>
<p>=== Script</p>
<p>Olá pessoal, Fabio Akita</p>
<p>Finalmente! Finalmente, decidi fazer um episódio sobre Ruby on Rails. Muita gente que me conhece nas interwebs como AkitaOnRails começou a me acompanhar quando eu comecei a evangelizar Rails mais de 12 anos atrás. Quando comecei o canal talvez muitos pensariam que a coisa mais óbvia é que eu ia continuar falando de Rails, mas até hoje ainda não fiz nenhum video a respeito. Esse é o episódio que eu queria ter feito 1 ano atrás mas que precisei publicar 58 vídeos antes pra dar o contexto. Exagerando um pouco eu diria que todos os videos que eu fiz até agora foram um prólogo pra esse. Também é apropriado que estou oficialmente iniciando o 2o ano do canal exatamente com esse tema. Quem acompanha o canal já me viu mencionando alguns eventos históricos de Ruby on Rails como nesse episódio da série Começando aos 40 (episodio 1 de backend)</p>
<p>Se alguém me perguntar hoje: é essencial usar Rails? Honestamente não, embora eu diria que você se daria melhor usando Rails, sem dúvida. Em 2019 temos muitas outras opções se, e somente se, você souber o que está fazendo. Eu ainda considero Rails o melhor framework de ponta a ponta e o melhor ecossistema pra maioria dos casos, o pacote completo. Os princípios e fundamentos que tornaram Rails especial em 2004 continuam valendo hoje. Mesmo que você não use Ruby, você provavelmente está &ldquo;On Rails&rdquo; e não sabe disso. A história do Rails é um caso único e eu não vejo ele se repetindo de novo tão cedo.</p>
<p>A idéia do video de hoje não é ser a história totalmente completa, mas sim a história pelo meu ponto de vista enquanto personagem nessa história, então vai ter alguns eventos talvez importantes que eu talvez pule. Enquanto escrevia eu via as páginas aumentando, 10 páginas, 20 páginas, 30 … e resolvi que não vou cortar em múltiplos video, pra variar. Vai tudo numa tacada só (&hellip; respira …), então vão pegar a pipoca, a água, vão no banheiro antes, e senta aí que lá vem história.</p>
<p>(&hellip;)</p>
<p>Como é o jeito &ldquo;certo&rdquo; de desenvolver um software médio web hoje em dia? Simples, começa criando um novo projeto no GitHub ou GitLab.
Daí roda o generator do seu framework pra criar o esqueleto. Faz o primeiro commit e dá push pro repositório e agora abre acesso pros seus colegas de trabalho.
Vamos organizar o trabalho da equipe num backlog e cada um vai fazendo uma pequena feature. Cada feature você abre um novo Pull Request ou Merge Request, dependendo se for GitHub ou GitLab.</p>
<p>Agora vamos criar os specs e fazer o código pra passar. Ou se você não é adepto de test-first development ou TDD como Eu também não sou sempre, pode ser o bom e velho test-after, importante é subir seu código com um mínimo de testes. Alguém insistiu em tentar subir código sem testes? git reset &ndash;hard nele!
Junta os commits dos testes, do seu código, manda pro branch do pull request. Finalizou? Pede pros colegas darem aquela revisada marota.
Agora que tem algum código já seria bom automatizar a execução dos testes, então vamos configurar um Travis-CI ou Circle-CI ou o CI integrado do próprio GitLab.</p>
<p>Agora a gente vai subindo novos códigos, vendo os testes de todo mundo da equipe rodando e passando. E depois de alguns pull requests mergeados, temos uma primeira versão que já dá pra pro usuário brincar. Hora de subir num ambiente de staging pra mandar pros usuários testarem. Então bora criar um projeto no Heroku, definir os recursos extras como banco de dados que vamos precisar e git push heroku.</p>
<p>Sucesso! Aplicativo funcionando! E pra facilitar a vida, vamos criar um continuous deployment ligado ao CI pra toda vez que subir código novo e os testes passarem, ele já subir a versão nova pro ar. Ou então pra cada pull request ainda não aprovado subir um ambiente de testes no Heroku em paralelo também. Assim a vida dos testers e QA vai ficar mais fácil.</p>
<p>Além disso podemos conectar serviços como CodeClimate pra constantemente avaliar a saúde do nosso código, ver se não estamos acumulando muito débito técnico. Avaliar se não estamos deixando de atualizar bibliotecas com bugs de segurança.
Se quisermos adicionar recursos mais avançados como WebSockets, pra criar um chat ou fazer push notifications, devemos usar serviços como o bom e velho Pusher. Pra maioria de nós não é necessário usar Node.js ou Go ou algo assim. E podemos habilitar o New Relic RPM no Heroku pra monitorar o uso da nossa aplicação e avaliar possíveis gargalos que precisamos ir otimizando. Ah, é bom viver nessa era de Software as a Service.</p>
<p>Tudo isso que eu acabei de explicar em 2 minutos é o dia a dia de qualquer projeto web comum, ou pelo menos deveria ser caso você ainda esteja numa empresa que não segue esse tipo de fluxo de trabalho. Eu diria que mesmo quem não segue, sente uma certa inveja e gostaria de estar trabalhando assim. Porém lembre-se do que eu venho falando em todos os meus vídeos: essa é uma realidade hoje pra muitos, em 2019. Isso não era verdade em 2004. E esta é a história de como a comunidade Ruby on Rails criou essa realidade.</p>
<p>Eu já contei parte disso no episódio do Diário de Henry Jones, mas em 2004 eu ainda era um consultor Java e SAP, lidando com o peso e burocracia de projetos enterprise desenhados em centenas de páginas de casos de uso e diagramas UML, usando coisas como IBM WebSphere ou BEA WebLogic. Mundo open source não era algo que programadores enterprise discutiam muito, porque não parecia muito &ldquo;profissional&rdquo;, era coisa de &ldquo;agência de php pra sitezinho de sobrinho&rdquo;. Profissionais usavam JSP, com algum framework proprietário da IBM ou da Oracle.</p>
<p>Pensem em um ambiente onde gerenciar banco de dados era um troço difícil, nenhum programador gostava de fazer ou mesmo podia fazer se quisesse. Pra isso existiam DBAs. Subir aplicativo web em produção era um saco, ninguém queria fazer, por isso existia gente de infra só pra subir versão nova em produção. Gerenciar versionamento de software era um saco! Ninguém queria fazer! Eu juro que existia emprego pra uma posição chamada &ldquo;Gerente de Configuração&rdquo; pra ferramentas da Rational tipo Clearcase, que é literalmente alguém cujo job description era fazer commit do código! (mindblown!)</p>
<p>O que havia de mais avançado no mundo open source naquela época era poder desenvolver apps em Apache Struts ou o novíssimo Spring na era do Java 1.4 pra 1.5, subir num tomcat ou glassfish local pra testar e num JBoss pra subir em produção. Usar Subversion era um troço meio subversivo ainda. Povo preferia usar Clearcase ou Microsoft SourceSafe. E mesmo usar Subversion era sofrido, tenta fazer merge de branches pra subir no trunk. E desenvolver fora da rede offline não dava porque Subversion precisava estar online já que commit é o equivalente a commit mais push num Git de hoje. Windows XP ainda era mais do que normal, com um tanto de antivirus tipo McAfee (video) pesando e mesmo assim com aquela sensação que ligar o XP na internet era que nem fazer sexo sem proteção. Acho que eu formatava e reinstalava meu sistema com certa frequência só pra garantir. Eram tempos difíceis &hellip;</p>
<p>Então em 2005 a gente  esbarra num video com o chamativo título de &ldquo;Blog em 15 minutos&rdquo; de um moleque desconhecido com cara de cagador de regra … não, não, não era eu, era esse dinamarquês chamado David Heinemeir Hansson que a gente se acostumou a chamar só de DHH. Pff &hellip; em 15 minutos meu Eclipse com os plugins de Websphere nem carregou tudo ainda. Esse video aliás, tem quase 15 anos, e eu acho que vale a pena incorporar ele aqui. Lembrem-se, eu sei que 15 anos atrás muitos de vocês assistindo tinham uns 10, talvez 15 anos de idade, mas tente assistir assumindo que Nada que vocês usam hoje existia naquela época.</p>
<p>(&hellip; blog 15 minutos)</p>
<p>A gente chamava esse video também de &ldquo;video do oops&rdquo;. O David foi convidado pra vir pro FISL de 2005, o Forum Internacional de Software Livre que acontece em Porto Alegre, todo ano. Um dos organizadores naquele ano foi meu camarada Rodrigo Kochenburger que hoje mora em São Francisco e é um dos melhores programadores que eu já conheci. Antes de mim, pessoas como ele já estavam engajados nessa comunidade de nicho que era Ruby. Agora, entenda que literalmente ninguém usava Ruby fora do Japão e mesmo lá era só um pequeno grupo. Quando o DHH veio apresentar o Rails no FISL a platéia eram basicamente pythonistas e outros que tavam nos corredores. Se tinha meia dúzia de rubistas, no FISL inteiro, era muito.</p>
<p>Pra dar uma noção, tirando pessoas como o Rodrigo não tinha quase nenhum rubista na ativa em 2005. Tinha o Caffo que é outro Rodrigo, o Rodrigo Franco que tinha começado a primeira mailing list brasileira de Rails, a Rails-BR. Tinha o Adriano Dadario e o Ronie Uliana que mantinham o antigo site/forum RubyBR. Tinha o mestre Eustáquio Rangel que publicou o primeiro livro de Ruby no Brasil, e o Ronaldo Ferraz que publicou o primeiro tutorial de Rails em português. Ainda ia levar algum tempo pra aparecer outros blogs que viraram referência aqui no Brasil como o Simples Ideias do Nando Vieira. Aliás, eu provavelmente vou esquecer muitos nomes do começo da história, me perdoem e quem se lembrar da galera das antigas pode mandar nos comentários abaixo.</p>
<p>Naquela época o Vinicius Teles do Rio de Janeiro já tinha publicado seu livro sobre Extreme Programming e estava começando a aplicar em projetos Rails. E outros nomes começaram a surgir como o Manoel Lemos, que hoje virou venture capitalist na Redpoint mas na época desenvolveu o BlogBlogs em 2006, antes de entrar pra Editora Abril. A ótima equipe que de alguma forma o Marco Gomes conseguiu juntar foi quem construiu o famoso Boo-Box em 2007. Na época existia os e-mail do UOL e tinha um produto paralelo que era o BOL ou Brasil Online, que foi feito em Rails lá por 2006 ou 2007 também.</p>
<p>Como falei em outros episódios eu acredito em ciclos de mercado. É muito difícil identificar o começo de um ciclo, normalmente você se dá conta que tá dentro de um quando já passou da fase de early adopters. Mas em 2005 eu sabia que Ruby on Rails, principalmente no Brasil, estava ainda começando o dia 1 desse ciclo. É muito raro conseguir enxergar o dia 1 de alguma coisa que vai conseguir completar o ciclo de 10 anos e embarcar nele no começo. Em 2005 eu tinha já uns 10 anos de carreira, e eu lembro que senti esse frio na espinha quando relembrei de 1995, ano que tivemos alguns dos lançamentos mais históricos até então, o Java 1.0, o Delphi 1.0, o Netscape 1.0 com Javascript 1.0, o Windows 95 inaugurando a era 32-bits. E ali estava eu, 10 anos depois, sentindo que um novo ciclo de 10 anos estava iniciando no mundo de programação. Eu não podia imaginar o que viria pela frente, só agora em retrospectiva dá pra ligar os pontos.</p>
<p>Eu achei o David Hansson, o DHH, um cara muito inteligente, diferente da maioria puramente técnica. Ele criou um produto que ninguém tinha feito igual até então. E não estou dizendo só em criatividade técnica. Estou falando em produto como um todo mesmo. Aliás, ninguém se referia a sites como “produtos” nessa época. Algumas coisas eram geniais só de bater o olho e algumas coisas a gente sabia que seria genial mas faltava ainda tempo pra se provar.</p>
<p>Vamos começar do básico. Naquela época dentre algumas das linguagens mais usadas estava Basic, tanto do Visual Basic, quanto VBScript de ASP e plugins de Office, e no novo .NET, daí C#, Java, PHP, ColdFusion, lembram de ColdFusion?? Lembrando que estamos falando de C# acho que 2, Java 1.5, PHP 3 ou 4. Javascript ainda era não mais que um brinquedo pra soltar box de alert nas páginas. ActionScript que era baseado em Javascript só existia no mundo Flash. Nenhuma das novas linguagens que você conhece hoje tinham sido inventados ainda como Scala, Elixir, Go, Rust, Clojure, Swift, Kotlin. Objective-C ou Erlang já existiam mas ninguém nunca tinha ouvido falar neles no mercado em geral.</p>
<p>Falando em Objective-C a gente tava entrando no final do ciclo de hype de orientação a objetos. Nos anos 70 principalmente a discussão sobre o que era a melhor forma de descrever e programar objetos foi tema de inúmeras discussões, livros, eventos e eu argumentaria que a melhor linguagem orientada a objetos de todos os tempos foi Smalltalk. Porém as empresas que mantinham as versões comerciais de Smalltalk como ParcPlace-Digitalk, ObjectShare, acho que tiveram ingerências ou azar mesmo, e começaram a desaparecer nos anos 80. E o aparecimento de coisas como Visual Basic e depois Java meio que foi o tiro de misericórdia. Smalltalk desapareceu quase totalmente nos anos 90, ninguém mais ouvia falar. Hoje se você procurar ainda acha a Cincom ou a Gemstone e versões open source como o Squeak e Pharo. Só alguns nichos ainda carregavam a herança de Smalltalk em linguagens como Objective-C que a NEXT de Steve Jobs estava usando e essa linguagem obscura do Japão chamada Ruby, que é uma das únicas que se assemelha ao estilo do Smalltalk e Objective-C, mesmo hoje.</p>
<p>Escolher Ruby foi um acerto improvável. Muitos poderiam criticar o fato do DHH não ter escolhido algo como Python que também é uma linguagem interpretada, orientada a objetos e com tipagem dinâmica, mas teria sido a escolha errada. Eu nem sei se o DHH chegou a pensar tão longe assim, eu acho que foi mais sorte mesmo, mas ainda bem que ele escolheu Ruby no lugar. Apesar da linguagem em si ter características excepcionais de ser super flexível, expressiva e agradável de trabalhar comparado com Java, ele tinha Uma coisa que ninguém mais tinha: … ninguém usava, não tinha um ecossistema estabelecido. É como você encontrar ouro e acumular, anos antes do mundo determinar que ele tinha valor.</p>
<p>O DHH começou como um freelancer, trabalhando da Dinamarca que é o país dele, pra agência chamada 37signals, de Chicago, que foi fundado por Jason Fried. Ele já tinha trabalhado um pouco com Java, um pouco com PHP, e assim como todos nós naquela época, já estava de saco cheio da burocracia do Java ou dos bugs e cultura de &ldquo;quick and dirty&rdquo; do PHP e Perl do fim dos anos 90. Então por 2003 ou 2004 a agência começou a migrar pra virar uma empresa de produtos, e o DHH resolveu dar uma chance pra esse tal de Ruby. Assim ele desenvolveu o lendário Basecamp que é uma ferramenta colaborativa pra organizar pequenos projetos, e em 2004 ele extraiu o framework Ruby on Rails. Aliás, primeira dica, nunca acredite num framework que não foi extraído de um aplicativo de verdade.</p>
<p>(musica de suspense)
Agora aqui entra a minha análise pessoal. O fato do DHH ter se aliado a Jason Fried e o DNA da 37signals ser de design, especialmente numa época onde o design da Apple estava começando a despontar, foi extremamente importante. Muito da linguagem minimalista do design da escola alemã de Bauhaus se misturou ao estilo de código, produto e filosofia do DHH. Naquela época o normal pra um arquiteto enterprise era criar frameworks o mais flexível possível, tudo explícito, tudo configurável, com o maior número de abstrações e indireções pra suportar projetos corporativos de qualquer tamanho e qualquer escala, e de preferência via comitê, com consenso de gente que nem de fato vai usar. Era a filosofia de “e se amanhã eu precisar?”. O DHH preferiu fazer o caminho inverso, que na época era muito inédito: criar um produto de verdade primeiro, que realmente funciona com usuários de verdade, e depois extrair as peças da fundação pra montar um framework. Ironicamente era assim que antes empresas como IBM e Oracle extraíam seus produtos: depois de implementar primeiro num grande cliente, como o governo ou os militares.</p>
<p>(comercial java)</p>
<p>Pouca gente fazia isso nos anos 2000. Arquitetos corporativos eram esnobes demais pra ir pra ralé e, deus me livre, fazer alguma coisa que funciona de verdade. (excerpt) No mundo PHP o pessoal fazia produtos inteiros, vide Joomla, vide Drupal, vide Wordpress. Mas eles raramente extraíam as partes comuns pra reusar em outro projeto, e a comunidade ficava fragmentada em ilhas. O estilo de código quick and dirty macarrônico, muita otimização prematura, porque o PHP não era exatamente a coisa mais performática do mundo também, e diferente de Ruby o PHP de fato tomou muita porrada de verdade. Em vez de &ldquo;perder tempo&rdquo; fazendo frameworks, eles ficavam cada um na sua ilha de produto e não se preocupavam muito em reuso. O máximo eram as tentativas de imitar o mundo Java, e imitar justamente a parte ruim: fazendo frameworks gigantes demais como o Zend e criando monstrinhos como o primeiro Magento.</p>
<p>“Vamos ficar masturbando diagramas abstratos como se fosse arte barroca o dia inteiro”.</p>
<p>(comercial php)</p>
<p>Comparativamente o framework Ruby on Rails era muito pequeno. Quando eu escrevi o primeiro livro de Rails eu também não sabia tudo de Rails ainda, então eu escrevi meu livro, capítulo a capítulo lendo o código fonte de cada parte. O código naquela época era pequeno o suficiente pra dar pra ler de ponta a ponta. Mais importante: o código que eu como desenvolvedor de uma app usando Rails precisava escrever era surpreendentemente pouco. Vindo de J2EE a gente tinha que fazer um trilhão de boilerplates; exatamente porque o framework exigia tudo explícito.</p>
<p>Por exemplo, quero fazer um Entity Bean em 2000. Precisava fazer um arquivo que é a interface, outro que é a implementação em si, outro que é um stub pra acesso remoto e por aí vai. Cada coisa que você fazia num J2EE precisava de uns 3 ou 4 arquivos de boilerplate pra ligar as coisas no framework. Não seria exagero dizer que se o projeto tinha 100 arquivos, só uns 20 era código de verdade com regra de negócio e tudo. Por isso a gente precisava usar Eclipse com plugins: pra ele gerar e atualizar essas classes extras pra gente. Era que nem os JavaBean antigos, se eu precisasse programar 10 propriedades, precisava implementar 20 métodos: 10 getters e 10 setters e, de novo, o Eclipse gerava isso automaticamente pra gente. Sério, era um saco programar com as primeiras versões de J2EE. (abaixa a cabeça)</p>
<p>Mas aí eu vi que no Rails não precisava fazer nada disso. Tinha quase zero boilerplates, era uma heresia! Ele apresentou esse conceito de Convention over Configuration e DRY ou Don&rsquo;t Repeat Yourself, que é um pensamento meio óbvio hoje  - ou pelo menos deveria ser - mas não era em 2004. Toda entidade vai ter os mesmos 3 ou 4 arquivos de boilerplate que não serve pra nada, então pra que gerar? Faça o framework inferir o que precisa gerar em tempo de execução e não gere nenhum arquivo que não seja absolutamente necessário. E, mais do que isso, o pouco que eu preciso gerar, não precisa de um plugin numa IDE: roda um script na linha de comando e boom, gera tudo pra mim! Isso era revolucionário! Daí é facil ver como um projeto que deveria ter 100 arquivos, em Rails tinha só uns 20 ou menos.</p>
<p>Justiça seja feita, o J2EE foi quem introduziu o modelo de MVC ou Model View Controller ou mais corretamente o Model 2 da Sun para o mundo em geral. Só que era tão complicado de entender que fazia sentido você precisar de certificação de arquiteto Java só pra conseguir entender esse tal de MVC. O Rails foi quem tornou MVC e conceitos como thin-controllers algo finalmente acessível pro público em geral.</p>
<p>O gerador do Rails sempre gera o esqueleto do projeto. Você sempre sabe que controllers vai nessa pasta, models vão nessa pasta, e views vão nessa outra pasta. Sem XMLs complicados pra configurar e duas dúzias de boilerplates que você nunca sabe onde colocar. Daí todo projeto Rails tem mais ou menos a mesma estrutura e era fácil pular de projeto e saber onde ficam as coisas. E a idéia se pagou porque 15 anos depois, a estrutura do Rails 5 de hoje é muito parecida com a do Rails 1. Mais do que Convention over Configuration, desde o começo Rails era o que gente chamava de opinionated, ou seja, seguia as opiniões do DHH. Isso é controverso até hoje, mas ele bateu o pé e insistiu em integridade conceitual.</p>
<p>Isso Não é uma democracia e Rails era um framework sem consenso e sem comitê. Já não era sem tempo, consenso em mundo técnico é só o mínimo denominador comum dos membros. Em vez de tentar abraçar o mundo e criar um framework que poderia ser pra qualquer coisa ele focou pra ser bom pra fazer apps similares ao Basecamp. Tem gente que odeia isso, mas haters gonna hate. Você pode trocar alguns componentes com alguma facilidade, mas quanto mais diferente você tentar ficar do Rails original, mais complicado vai ficar pra manter o projeto no futuro.</p>
<p>E aqui vemos a influência da Apple. Macbooks bem que poderiam ter mais portas de expansão, ser mais fácil de trocar peças e assim por diante. Mas aí ele seria igual um Dell ou Lenovo. O que torna o Macbook um Macbook é a forma. Sempre vai existir a discussão de forma contra função mas qualquer coisa diferente só pra conseguir consenso com algum nicho destrói sua integridade conceitual. Minimalismo parte do princípio que quem faz o design tem uma opinião muito forte sobre o que é esse mínimo. DHH não é nenhum Jobs, mas ambos certamente tinham muita opinião e não seriam dobrados com facilidade, e ambos foram provados corretos ao longo da década. Não é difícil de argumentar que se hoje Macs são dominantes no mundo de tech startups, foi por causa de Ruby on Rails. Sim, a culpa é nossa. Vejam este vídeo de 2007 que foi feito em parceria com a Apple Education:</p>
<p>(apple video)</p>
<p>Um dos aspectos que vinham ligados por padrão era segurança. Numa época onde era super comum sites sendo lançados em produção com erros primários como cross site request forgery, cross site scripting, sql injection, session hijacking, tudo isso já era travado por padrão no Rails. De novo um efeito de Convention over Configuration, ou seja, porque ter que ligar coisas manualmente que já deveriam estar ligadas por padrão? Você precisava manualmente desligar as proteções se quisesse, e aí fica por sua conta. Por isso apps Rails por muitos anos sempre estiveram entre alguns dos mais seguros por padrão. Todo framework que te vende “flexibilidade” porque você mesmo pode escolher quais componentes por adicionar, sempre vai ser pior porque a grande maioria vai sempre escolher errado.</p>
<p>E se isso não fosse suficiente, nesse esqueleto que ele gera igual pra todo projeto ainda havia outra coisa Completamente diferente: um diretório chamado test. E todo gerador de models, ou controllers ou views também gerava arquivos correspondentes de testes vazios nesse diretório. Lembrem-se era 2004, o manifesto ágil de software tinha saído só 3 anos antes. Vamos lembrar uma coisa, Gerry Weinberg fez testes pela primeira vez em cartões perfurados em 1989. Kent Beck, um dos criadores de Extreme Programming começa a experimentar essa ideia e cria SUnit em 1994 inaugurando a idéia da gramática de testes como test cases, asserts,  e vai fazendo pesquisa e escrevendo a respeito até finalmente consolidar a idéia de test driven development e lançar o JUnit no ano 2000. Ou seja, a idéia de TDD não tinha ainda 4 anos desde que o mercado começou a ouvir falar.</p>
<p>Os mundos de desenvolvimento tradicionais, sejam .NET, Java, PHP, não estavam assim muuuuito empolgados em adotar, só alguns pequenos nichos. Então nesse ponto era muito frustrante querer que os programadores fizessem testes, se você tentasse provavelmente todo mundo ia tirar sarro de você na melhor das hipóteses ou você podia ser demitido mesmo, por ser improdutivo fazendo código irrelevante pro projeto! Éee eu sei! De novo, aqui temos outro insight: é muito difícil implementar mudanças radicais como um TDD num ecossistema gigante que já existe e é resistente a mudanças. Não vai acontecer tão fácil, mas existem estratégias. Lembram que eu falei antes que um dos acertos improváveis do DHH foi ter escolhido Ruby, uma linguagem que ninguém usava? Eis o motivo de porque deu certo: porque ele precisava introduzir um novo paradigma mas sem ter que gastar tempo convencendo antigos usuários de Ruby e migrando projetos inteiros pro novo paradigma, por isso dava pra se mover muito rápido.</p>
<p>No ano 2001 o mundo Ruby começou a se interessar com a idéia de testes no estilo de xUnit, surgiram bibliotecas como testsup, depois Lapidary, finalmente RubyUnit e Test::Unit que seguia o estilo xUnit do Kent Beck. E isso foi adotado no Rails do DHH, como padrão! Não algo que você tinha que adicionar depois, mas algo que o framework obrigava você a manualmente tirar se não quisesse. Junte a isso que alguns agilistas como Ward Cunningham e acho que Martin Fowler já estavam com um olho no Ruby porque ele lembra o Smalltalk que eles já gostavam como a verdadeira orientação a objetos, um estilo que ficou perdido nos anos 80.</p>
<p>Aliás, o primeiro livro de Ruby em inglês saiu em 2001, apelidado de Pickaxe por causa da capa, foi escrito por dois signatários do Manifesto Ágil, Dave Thomas e Andy Hunt. Portanto você imagina que além de tudo essa comunidade ocidental de Ruby, começando do zero já iniciaria com Extreme Programming e The Pragmatic Programmer como filosofias de fundação. Finalmente uma comunidade de software que estava começando do zero do jeito certo, ou seja, o jeito verdadeiramente Ágil de desenvolvimento.</p>
<p>Mais do que isso, o tipo de desenvolvedor que virava rubista era muito estranho. O exemplo mais notável foi o lendário Why, the Lucky Stiff, que escreveu naquela mesma época o lendário livro que muitos usaram pra aprender Ruby: The Poignant Guide to Ruby. Sério, olha algumas páginas desse livro (video).</p>
<p>Why era um pseudônimo, e ele nunca quis revelar seu nome ou qualquer detalhe da sua vida, e quando algumas pessoas começaram a fuçar e começar expor sua vida pessoal, inclusive revelando seu nome verdadeiro como sendo Jonathan Gilette, ele fez uma coisa que admiro até hoje. Ele resolveu sumir do mapa em 2009, sem deixar vestígios. Aliás, em 18 de agosto de 2019 faz 10 anos desde que Why apagou todos os seus repositórios e sites e desapareceu.</p>
<p>Ele era um artista que usava código pra se expressar, e ao contrário de um Python ou Java com seu estilo de engenharia de ter só um jeito certo pra se fazer as coisas, e tudo deve ter consenso em comitê, o Ruby era o oposto: manipule a linguagem como você quiser. Ele tinha a liberdade do Lisp com a expressividade do Smalltalk. Nenhuma outra linguagem da época permitiria algo como o que Why fazia. Why the Lucky Stiff que inclusive é um nome que aparece citado uma única vez no romance The Fountainhead, de Ayn Rand. Outra coincidência que eu achei muito providencial dado que Why fez a mesma coisa que o fim do livro A Nascente.</p>
<p>(video acabando com chunky bacon)</p>
<p>Se você nunca ouviu a expressão “Chunky Bacon”, você ainda não aprendeu Ruby do jeito certo (risos). Ruby começou sendo ferramenta de artistas. A primeira geração de rubistas é um grupo que não se via igual talvez desde os anos 60 ou 70. E o que eu falei que Ruby atraía desenvolvedores estranhos é porque eram pessoas que gostavam de expressar suas décadas de experiência com código. Entrar num grupo gigante é fácil, é só ser um seguidor. Mas pra entrar num grupo com zero pessoas, você precisa ser um criador. Por isso a primeira geração de rubistas trouxe gente como Chad Fowler, Rich Kilmer, David Black, o falecido e adorado Jim Weirich, depois agilistas como Dave Thomas ou Martin Fowler ou Uncle Bob. Não deixem de assistir o documentário sobre o Why que eu co-apresentei na Rubyconf Brasil de 2014 com Kevin Triplett e meus artigos sobre ele que vou deixar nas descrições abaixo.</p>
<p>Então vamos recapitular nossa sopa primordial: uma linguagem obscura, que resgata o espírito de Smalltalk e Lisp, a orientação a objetos original, que implementa corretamente os princípios ágeis, e até mesmo outras noções como yagni ou you ain’t gonna need it, que é o jeito minimalista que o DHH aprendeu na renascença da nova Apple de Steve Jobs, numa comunidade de misfits e artistas wannabe, de desenvolvedores experientes dos anos 70 a 90 que queriam voltar a se expressar com código. Claro que isso atraiu a atenção de muitos  agilistas da época, incluindo metade dos signatários do manifesto. E mais do que isso: chegamos ao fim da depressão do crash das ponto coms em 2001, estava na hora de recomeçar a criar produtos e o Basecamp e o estilo Getting Real da 37signals se tornou o exemplo de como fazer isso. Aliás, o Getting Real que aqui no Brasil chegamos a fazer uma tradução oficial em português também e que se tornou o manifesto da nova geração de empreendedores de produtos de tech startups, muito antes do surgimento de Lean Startup.</p>
<p>E claro, todo mundo deu risada de Ruby on Rails, um framework não-enterprise, que não era infinitamente configurável, que vinha com defaults estranhos como MySQL em vez de suportar Oracle, que não tinha suporte pra Windows, que te obrigava a apagar os arquivos de testes, porque ninguém queria fazer testes no mundo Java, e ainda usando Macs??? Profissionais usam Windows XP, lógico! E ainda usando essa linguagem bizarra chamada Ruby que diz que é orientada a objetos mas não tem tipos estáticos como toda linguagem profissional como Java ou C#. Lógico que ia ser um fracasso. (gandhi)</p>
<p>Ah, e você não gosta do Jira hoje? Imagina como o Jira era mil vezes pior em 2004, e como todo mundo ria que algo no estilo Basecamp nunca seria usado no mundo enterprise. Se ainda não entendeu a gravidade da situação da época foi em 2003, 1 a 2 anos antes que o Java Server Faces, o JSF tinha acabado de ser lançado e todo mundo estava adotando aquele lixo, que era um ASP.NET WebForms piorado pra Java! Ah sim, pra quem gosta de falar em frameworks baseados em componentes e orientados a eventos, ASP.NET e JSF. (joinha)</p>
<p>A partir de agora, apertem os cintos, vamos fazer uma longa timeline, de uns 7 anos! Eu não vou por temas e sim em ordem cronológica. Vocês assistiram os demais videos do meu canal né? Porque agora muita coisa que eu disse neles vai se encaixar.</p>
<p>2004 foi um ano especial. Em 1o de Abril o Gmail foi lançado, demonstrando um dos primeiros single page applications e o poder de tecnologias como Ajax. Em 24 de julho saiu o primeiro beta público do Ruby on Rails versão 0.5. Em 2 de Outubro o DHH apresenta o Rails pela primeira vez numa RubyConf, que na época estima-se que tinha umas 60 pessoas na platéia. Três dias depois a O’Reilly promove a conferência Web 2.0 onde o termo foi cunhado pela primeira vez. Em 10 de dezembro o Google lança aquela funcionalidade que você usa todo dia e nem pensa muito, o Google Suggest, que vai te dando sugestões à medida que você vai digitando os termos de pesquisa, usando XMLHTTP e de novo demonstrando o poder do AJAX e comunicação assíncrona em tempo real. Todo mundo queria fazer a mesma coisa.</p>
<p>2005 não ia ficar pra trás. Logo no 1o dia do ano, a Robot Co-op lança o 43 Things; o site é um early adopter de Ruby on Rails. Em 8 de fevereiro o Google Maps lança pra Internet Explorer e Firefox, redefinindo o nível de interação possível em um web browser. Essa era a época que o Google tava literalmente on-fire lançando um app atrás do outro. E alguns dias depois, em 15 de Fevereiro, Jesse James Garrett cunha o termo Ajax para descrever as novas aplicações web ricas como Flickr, Google Suggest, ou Google Maps. No dia seguinte o Rodrigo Caffo cria a primeira mailing list brasileira de Rails, a Rails-BR.</p>
<p>Em março o britânico Peter Cooper começaria o blog Ruby Inside, o melhor blog pra se manter atualizado no mundo Ruby e Rails e que daria origem anos depois ao conjunto de mailing lists como JavaScript Weekly, Ruby Weekly, RubyFlow, Frontend Focus, Node Weekly, Golang Weekly e vários outros que centenas de milhares de desenvolvedores usam até hoje e se mantém com um dos melhores canais pra se manter atualizado. Em 7 de Abril tivemos outra revolução, o Lançamento inicial do sistema de controle de versão distribuída git. E na sequência, em 2 de Julho, o DHH grava o demo do blog de 15 minutos, demonstrando a velocidade do desenvolvimento rápido com Rails. Esse video se torna viral e todos os sites de todas as linguagens começam a notar a presença desse alien.</p>
<p>E não demorou muito, também por volta de julho tivemos o lançamento inicial do Groovy on Grails, um framework inspirado no Rails pra recém lançada linguagem Groovy que roda na JVM. Foi o primeiro de muitos clones de Rails que apareceriam nos próximos anos. Daí ainda em julho, no dia 7, é lançada a biblioteca javascript script.aculo.us 1.0.0 que possibilita um novo nível de interação em páginas web, tornando fácil adicionar os efeitos de estilo Web 2.0 nas suas aplicações. No dia  21 de Julho, o framework Django é lançado depois de ficar em desenvolvimento por mais de 2 anos. Em 4 de Agosto o livro Agile Web Development with Rails, primeira edição foi publicado pela PragProg, novamente escrito pelos agilitas Dave Thomas e Andy Hunt. E foi em outubro de 2005 que o indiano Satish Talim inaugurou o curso online pra iniciantes RubyLearning que está ativo até hoje e foi importante naquela época.</p>
<p>De curiosidade, em 3 de Dezembro teve a Conferência Snakes and Rubies que levou os criadores do Django e do Rails (video) pra discutir sobre frameworks web e os caminhos diferentes que eles tomaram. Naquela época ainda não estava claro quem ia ganhar, mas os motivos que eu já citei antes devem deixar claro porque um foi muito diferente do outro apesar de terem começado parecido. 10 dias depois o Rails 1.0 é lançado. (&hellip;) Nós tínhamos alguns dos melhores escritores de blog daquela geração, um dos melhores sempre foi o Ilya Grigorik do blog Igvita e que hoje é engenheiro de web performance do Google. Seus artigos sobre performance no Ruby e Web em geral influenciaram toda a nossa geração e vale a pena reler ainda hoje. Seu primeiro blog post foi ao ar em 28 de dezembro. E assim acaba 2005!</p>
<p>As coisas começam a esquentar em 2006. Primeiro, em março, Jamis Buck que entrou pra equipe Core do Rails renomeia seu projeto SwitchTower pra se tornar o que conhecemos como Capistrano e a forma inédita de automatizar deployment de projetos em máquinas de produção orquestrando via SSH, e essa ferramenta inspiraria diversas outras como o Fabric e Ansible de Python que vieram depois dele. Ainda em março, no dia 21 o Jack Dorsey posta o primeiro tweet: “just setting up my twttr”. 2 dias depois o Eustáquio Rangel lança o primeiro livro de Ruby no Brasil e um mês depois em 5 de Abril eu escrevo meu primeiro post no que viria depois a se tornar o blog AkitaOnRails.com.</p>
<p>Em 22 de Junho acontece a primeira RailsConf em Chicago. E já em 30 de Junho o rubista Hamptom Caitlin sobe o primeiro commit do SASS e inicia uma nova era de meta-linguagens pra tornar possível ser capaz de codar decentemente em linguagens chatas como CSS. Também já em 5 de Julho surge o JRuby 0.9, uma versão de Ruby que roda em cima da JVM e já consegue rodar Rails. Charles Nutter e Tom Enebo continuam firmes e fortes mantendo a linguagem até hoje.</p>
<p>Muito antes de termos a onda de cursos online que temos hoje, a comunidade Ruby on Rails foi pioneira em mostrar como cursos de qualidade poderiam ser feitos e foi Geoffrey Grosenbach que iniciou isso com seu Peepcode, que anos depois seria adquirida pela Pluralsight. Muita gente, incluindo eu, aprendemos muito nos cursos do Geoffrey.</p>
<p>Pra continuar os lançamentos revolucionários, em 25 de Agosto a Amazon Web Services, ou AWS, lança o Elastic Compute Cloud (EC2) e inicia de fato a Era do &ldquo;Cloud Computing&rdquo; como conhecemos hoje. No dia seguinte, em 26 de Agosto o John Resig lança o então adorado e hoje denegrido jQuery. Pouco mais de um mês depois em 6 de Outubro meu primeiro livro &ldquo;Repensando a Web com Rails&rdquo; saiu da gráfica, o primeiro de Rails publicado no Brasil. Três dias depois em 9 de Outubro, o Ronaldo Ferraz lança o primeiro tutorial de Rails no Brasil. E ainda em Outubro surge o primeiro framework concorrente de Rails escrito em Ruby, o Merb 0.03 que significa (&ldquo;Mongrel+Erb&rdquo;).</p>
<p>(video jobs iphone)</p>
<p>E falando em 2007, logo no começo do ano em 9 de Janeiro o mundo da tecnologia recebe um novo marco: o lançamento do primeiro iPhone. Poucos dias depois em 18 de Janeiro é lançado o Rails 1.2 lançado com o lendário RESTful resources e o mundo finalmente aprendeu que HTTP não é só GET e POST e finalmente os outros verbos ganharam uso. Foi neste ponto que mundo Web começou a proliferar a idéia de APIs REST como Roy Fielding publicou no ano 2000 definindo REST. Ruby on Rails se tornou o gold standard em APIs. Em 20 de Fevereiro o Christian Neukirchen define a interface Rack, inspirado pelo Web Service Gateway Interface ou WSGI de Python que foi definido na famosa PEP 333 de 7 de dezembro de 2003.</p>
<p>Em 4 de março o saudoso Ryan Bates lançou o primeiro episódio do videocast chamado Railscasts. Assim como Geoffrey da Peepcode ele criou dezenas de videos curtos sensacionais e muito bem produzidos e editados. Novamente mostrando como videos de programação poderiam ser bem feitos e ensinando toda uma geração de Railers, incluindo eu mesmo. Infelizmente ele parou por volta de 2013. Eu diria que o sucessor espiritual do Ryan hoje é o Avdi Grimm com seu também excelente RubyTapas, cujo primeiro episódio foi ao ar em 24 de setembro de 2012, então ele realmente é quase uma continuação do Railscasts só que voltado ao Ruby em si. Eu recomendo que vocês assinem.</p>
<p>Na renascença das tech startups, começaram a surgir as famigeradas redes sociais, a versão moderna do muro das lamentações. Na melhor fase tivemos Orkut, Friendster e MySpace que já estavam saturados mas o público pedia por mais. Facebook estava só começando a aparecer, não lembro se nesse ano já tinha aberto no Brasil. De repente um grupo que tinha crescido no mundo de blogs com a Odeo resolveu criar um modelo experimental de micro-blogging, que você podia enviar posts via SMS, e por isso tinha o limite de 140 caracteres. E eles resolveram batizar esses micro-posts de tweets, inaugurando também a era de nomes bizarros pra startups. E, mais importante, escolheram Ruby on Rails pra implementar. Claro, ser inspirado pelo framework do blog de 15 minutos pra fazer uma plataforma de micro-blogging fazia todo sentido. Eu abri minha conta em 2007 se não me engano.</p>
<p>Agora, em Abril de 2007 inicia-se a maior infâmia da história até então, a famigerada controvérsia do &ldquo;Rails não Escala&rdquo; iniciada por uma comunicação mal feita do Alex Payne, do próprio Twitter. Lembram que eu expliquei que Twitter foi criado como uma plataforma de micro-blogging? Pensa se o Twitter tivesse sido programado em cima do Wordpress. Só que 2 anos depois ele deu certo e milhões de pessoas começaram a usar, só que o uso mudou. Em vez de ser micro blog posts, a plataforma começou a ser usada como um sistema de comunicação, ou seja, de broadcast de mensagens. Claro que nem Ruby, nem PHP, nem Python, aguentariam isso. O problema nunca foi a linguagem, foi a arquitetura.</p>
<p>Só existia uma arquitetura conhecida na época que suportaria o broadcast massivo de mensagens, e se chamava Erlang. Mas ninguém queria ter que usar a sintaxe do Erlang, e por sorte o Odersky tinha acabado de lançar o Scala, com o framework Akka, que copia a arquitetura de atores do Erlang e possibilita usar esse sistema em cima da JVM. Ou seja, o Scala era a coisa mais próxima do Erlang que se tinha e, claro, fazia sentido mudar essa parte do back-end. Não é que o Rails não escala, nenhum outro framework em nenhuma outra linguagem ia escalar. Hoje em dia, pra esse cenário seria Elixir. Essa controvérsia seguiria o Rails por anos (e finalmente nos livramos disso, afinal se você acha que o Rails não escala, olhe pro GitHub, até hoje feito em Rails e usando a última versão)</p>
<p>Em 18 de Maio de 2007 o mundo de testes nunca mais seria o mesmo. Já expliquei sobre a importância de desenvolvimento com testes e como isso foi um gancho importante pra ganhar o apoio dos agilistas ao Rails, mas foi neste dia que o lendário RSpec foi lançado, baseado nas experiências de Steven Baker desde 2005 e com a ajuda de Dave Astels, Aslak Hellesøy que depois criaria o Cucumber, iniciando a era de Behavior Driven Development, e claro o simpático David Chelimsky que a comunidade brasileira adorou conhecer quando eu trouxe nas duas primeiras Rails Summit Latin America em 2008 e 2009. Todos os frameworks de teste na maioria das linguagens modernas que você conhece hoje como o Chai do Javascript é descendente direto do RSpec.</p>
<p>Seguindo na linha dos clones de Rails, ainda em maio de 2007 surge o Framework Play para Java. Em 16 de Maio eu iniciei meu primeiro emprego oficial como Railer full-time na Surgeworks, uma pequena consultoria de Utah. Mais pro fim vou falar da pessoa que se arriscou em contratar um brasileiro numa época em que isso era muito raro. Eu trabalhei remoto por um ano mais ou menos, onde eu finalmente comecei a programar em Rails profissionalmente, aos 30 anos. Só pra relembrar: eu deixei de ser sênior de Java e SAP e voltei a ser júnior e ganhar como júnior.</p>
<p>Por volta de Julho outro nome importante apareceu, o grupo do RailsEnvy com Gregg Pollack e Jason Seifer que contribuíram muito no estilo diferentão dos hipsters de Rails dessa época. Eles aproveitaram a campanha Get a Mac que a Apple agressivamente publicou como comerciais de TV entre 2006 e 2009, no auge da dominância da Apple do Zeitgeist, os famosos comerciais “Hello I’m a Mac ….</p>
<p>(comercial mac vs pc)</p>
<p>Então em 2007 Gregg e Jason fizeram diversos clones desses videos com o mesmo princípio com Rails pra tornar mais divertido a forma como a gente via as demais tecnologias diferentes de Rails que até aquele momento ainda estavam vivendo no passado. E eles fizeram vários videos começando com o famoso Hi, I’m Ruby on Rails … (comercial)</p>
<p>Vocês podem ver os videos no canal de YouTube do Gregg Pollack que vou deixar linkado abaixo. Com o tempo o Gregg e Jason acabaram se separando, o Gregg montou a consultoria EnvyLabs e em paralelo, seguindo nos passos de Geoffrey Grosenbach e Ryan Bates montou o famoso Code School, com uma grade mais estruturada pra ensinar Ruby on Rails e outras novas tecnologias. E foram eles que criaram o conceito de Rails for Zombies que depois outras comunidades, como Python, copiaram também. Não existia comunidade que gerava mais conteúdo técnico do que a de Rails naquela época. O Rails for Zombies foi o primeiro curso do Code School que foi lançado em 2010. E o negócio foi vendido em 2015 também pra PluralSight onde vocês ainda podem achar os cursos.</p>
<p>Em 4 de Outubro tivemos o lançamento inicial do microframework Sinatra, também escrito em Ruby mas que não tinha o objetivo de competir com o Rails, ele era mais focado em micro-aplicações ou mesmo pra ser micro-serviço de APIs; ele é largamente copiado em outras linguagens até hoje. Ainda em outubro, no dia 19 finalmente os rubistas Chris Wanstrath e Tom Preston-Werner começam a trabalhar no GitHub, literalmente de um Starbucks. O Chris tinha o blog Err the Blog e começou criando bibliotecas de Ruby que conseguiam interfacear com Git.</p>
<p>Também em outubro, no dia 22 foi a primeira vez que eu organizei um evento, em São Paulo, o RejectConf SP que teve muitos dos nomes do Ruby brasileiro que você reconhece até hoje como Carlos Brando (CTO do Enjoei), Danilo Sato (ThoughtWorks), Fabio Kung (que foi do Heroku por muitos anos e agora está no Netflix), Vinicius Teles (da HE:Labs), Carlos Villela (que foi da ThoughtWorks por muitos anos e foi pra DigitalOcean), George Guimarães (Plataformatec) e mais. A Caelum do Paulo Silveira estava lá também ajudando com o coffee-break da galera.    Vocês estão vendo como as coisas estão acontecendo muito rápido, todo mês, às vezes toda semana estava saindo alguma coisa que influenciaria o mercado pelos próximos anos. Como eu disse no começo, 2004 marcou o início de uma nova era pra Web.</p>
<p>(2008)
E pra não quebrar a tradição do ano iniciar com alguma coisa, já no primeiro dia de 2008 tivemos nosso primeiro grande RubyDrama. Era engraçado que antigamente não existia dramas em nenhum lugar, com frequência. Era tão raro na real que quando acontecia a gente chamava de RubyDrama. Notem que dramas só começaram a acontecer com frequência com o surgimento de plataformas como o Twitter. No caso do Ruby em 2008, Zed Shaw, o criador do Mongrel que foi o primeiro servidor de aplicações pra Rails, fez uma declaração bombástica de sua saída, chamando a comunidade Ruby de ghetto. Isso reverberou por um algum tempo. E ele não estava totalmente errado, só que ele não sabia jogar o jogo, o Zed é o tipo de cara tecnicamente excepcional mas de cabeça quente. Mas, todo drama que parece tão importante pra você hoje, alguns anos depois, ninguém mais nem se lembra, e o Mongrel foi rapidamente substituído por outros servidores de aplicação como o Thin, Passenger, Unicorn e depois o Puma que usamos hoje.</p>
<p>Mais importante do que isso, nesse mesmo dia 1o de Janeiro de 2008 foi quando o Heroku recebeu seu primeiro investimento seed de USD 20 mil da Y/Combinator. Esse foi o mesmo ano que eu participei da minha primeira Railsconf nos Estados Unidos. Lá eu conheci o James Lindenbaum um dos fundadores do Heroku. Eles estavam com um estande no evento e ninguém mais se lembra disso mas a primeira versão do Heroku era pra ser uma IDE Web pra desenvolvimento de software. Só que eles viram que isso não ia rolar naquela época. Pense que 10 anos atrás ainda era muito difícil imaginar coisas como o Electron que usamos hoje, o Google ainda não tinha começado a otimizar o Javascript, acho que nem tinha o V8 ainda. Então eles tiraram essa idéia mas ficaram com a segunda parte que era onde os apps iam rodar, a infraestrutura de containers que surgiria muito antes do Docker e que inaugura o primeiro PaaS ou Platform as a Service. Aliás, é disso que nasce as boas práticas do Doze Fatores ou 12 factors pra escalabilidade em 2007, escrito por Adam Wiggins que foi co-fundador do Heroku também. E, claro, Ruby on Rails foi o primeiro framework que estava compatível com os Doze Fatores.</p>
<p>Se você é de Elixir, pra efeitos históricos vale relembrar o projeto Reia de Tony Arcieri, que eu recomendo que vocês sigam porque ele manja muito sobre segurança e hoje em dia Rust. Pra dar perspectiva, o primeiro commit do Elixir do José Valim foi em 9 de Janeiro de 2011. O primeiro commit do Reia é de 21 de março de 2008 e ele é basicamente o Elixir uns três anos antes: uma nova linguagem muito mais próxima de Ruby que compila em bytecodes que executam na VM Beam do Erlang. No fim o Tony perdeu o interesse no projeto e foi fazer outras coisas como a biblioteca Celluloid pra Ruby que hoje é usado pra abstração de concorrência, seja de threads ou I/O assíncrono, inclusive hoje é dependência no Rails.</p>
<p>Já em 2 de Abril o desenvolvimento do Rails sai de Subversion e move seus repositórios pro GitHub, dando credibilidade pra essa ferramenta chamada Git que quase ninguém tinha ouvido falar fora dos grupos de Linux e desenvolvimento do kernel. Por muito tempo 90% dos repositórios que tinha no GitHub eram projetos Ruby e Rails, e todo mundo de fora começou a ficar interessado em testar. Alguns dias depois, em 10 de Abril o GitHub sai do beta e lança publicamente. Hoje em dia vocês nem pensam e basicamente adotam Git, mas nessa época além de Subversion, no mundo open source ainda tinha gente que usava CVS mesmo, e a nova geração ainda estava entre coisas como Mercurial ou Bazaar. Em 14 de Abril foi quando José Valim mandou seu primeiro commit pro Rails, foi o começo de 2 anos de trabalho que o tornou membro do grupo Rails Core em 2010.</p>
<p>Nesse ano eu estava tendo que me virar. Em Junho eu tinha saído da Surgeworks e entrei na Locaweb. Embarquei pra Portland, no Oregon, pra Railsconf com a missão de conseguir integrar com a comunidade de lá e organizar o primeiro grande evento de Rails no Brasil, na América Latina na real. Com cerca de 3 meses só pra organizar tudo do zero, em 15 de Outubro tivemos o Rails Summit Latin America 2008, no Auditório Elis Regina do Anhembi em São Paulo. Deu bastante trabalho mas os nomes que eu consegui trazer muitos vão reconhecer até hoje como Chad Fowler, um dos rubistas originais no Ocidente, fundador da Rubyconf, meus amigos Ninh Bui e Hongli Lai da Phusion que desenvolveram o Passenger, um dos servidores de aplicação tipo o Puma de hoje, o David Chelimsky que contribuía no projeto RSpec e Cucumber, o Chris Wanstrath, fundador do GitHub, o Dr. Nic, o Obie Fernandez e mais. Ninguém achava a) que um evento desses fosse possível, nem b) que ele seria consistente e duraria quase 10 anos! Nessa primeira edição acho que já conseguimos juntar mais de 400 pessoas.</p>
<p>Outro acontecimento que vale lembrar nesse ano foi em 2 de setembro quando o Google finalmente lança o venerado e hoje temido navegador Chrome junto com a engine de Javascript mais importante de todos os tempos, o controverso V8, que vai dar início ao atual ecossistema Javascript. Pare um instante pra considerar isso se você for iniciante. A Apple se inspirou no Konqueror do KDE, fez fork e transformou o componente KHTML no WebKit. Daí o Google tinha contratado engenheiros do Firefox alguns anos antes mas eles nunca se acharam na posição de empurrar um novo navegador, mas chegaram num ponto que fez sentido começar com o WebKit como fundação e, no melhor estilo Google, fazer um deles parecido a partir de um fork em 2013.</p>
<p>Pra fechar bem o ano, em 23 de Dezembro tivemos o merge dos projetos Rails e Merb e a  equipe do Merb se junta ao Rails core team. Lembram o RubyDrama do começo do ano do Zed Shaw? Ele tinha meia razão, sempre existiu um grupo fechado pra discutir o desenvolvimento do Ruby on Rails, e isso não é nenhum problema, o DHH sempre quis ser como um Steve Jobs, ele controla o Rails com mãos de ferro, e não só o código mas tudo relacionado ao “produto” Ruby on Rails, como a marca, o nome, os eventos e tudo mais. Certo ou errado, ele owna as decisões em vez de se esconder atrás de um comitê. Isso é integridade conceitual que todo projeto precisa: um ditador benevolente. Gostem ou odeiem, foi o que manteve o Rails intacto até hoje.</p>
<p>Mas deixa eu explicar esse episódio, só pra ter registrado. Durante essa bolha de interesse explosivo em cima de Ruby on Rails, alguma empresas foram fundadas pra suportar a infraestrutura de startups que cresciam do nada como Twitter ou GitHub, especialmente depois da famigerada controvérsia do Rails não escala do Alex Payne. Uma dessas foi a Engine Yard, que estava competindo com a AWS e Rackspace. Pense que em 2008 a AWS só existia fazia 2 anos e nem todo mundo ainda tinha entendido essa idéia de máquinas virtuais voláteis cobradas por uso. Ainda dava pra pensar em competir com a AWS. A Engine Yard foi fundada por pessoas excepcionais, em particular o EZRA ZYGMUNTOWICZ, super gente boa que eu conheci em 2008 também. Ele era genial, tanto como programador, como quanto infra.</p>
<p>Por exemplo, na época ele criou um sistema de deployment em máquinas físicas usando o protocolo Jabber que se usa em instant messager. Pensa você abrir faz de conta, um whatsapp de hoje em dia, e falar com sua infraestrutura e mandar ordens como atualizar as máquinas ou deployar apps, em 2008! Além disso ele foi o único que enxergou como fazer um concorrente de verdade pro Ruby on Rails, dado sua experiência com hardware e otimização de Linux, ele começou fazendo patches no Rails e no Mongrel e eventualmente criou o Mongrel ERB, um software que tira o Rails da equação. Em vez de fazer só mais um micro-framework, ele resolveu competir pau a pau e fez um Rails que muitos argumentariam que era melhor, esse foi o Merb.</p>
<p>A equipe que a Engine Yard montou contou com programadores como o Yehuda Katz e Carl Lerche pra acelerar o desenvolvimento do Merb. Tudo isso enquanto ele estava montando um data center, carregando servidores nas costas, montando rede, e ajudando outras tech startups a escalarem seus projetos em Rails. Então eles rapidamente criaram uma excelente reputação. Eles eram um dos maiores patrocinadores da Railsconf. Só que o erro foi que eles começaram a evangelizar o Merb agressivamente demais. Tipo, eles iam nas Railsconf falar mal do Rails e ofereciam o Merb como opção. Eu disse que o DHH queria ser o Steve Jobs? Saibam que a Railsconf só pode acontecer com o aval dele, porque a marca Ruby on Rails é trademark do DHH e o evento também. Imagina se na Macworld aparecesse um palestrante falando que Mac é um lixo, na frente do Steve Jobs. …. Pois é, o DHH ficou furioso com isso e eu soube de uma conversa dele com o Tom Mornini, um dos co-fundadores e na época CTO da Engine Yard. Eu estava online pouco depois que isso aconteceu falando com o Matt Aimonetti, outro core developer do Merb na época e o autor do post de reconciliação anunciando que o Merb seria mergeado no Rails.</p>
<p>Eu não estava fisicamente na sala ouvindo, então não me citem, mas do que eu ouvi foi um ultimato do DHH pro Tom Mornini. Ou vocês páram com essa palhaçada de falar mal do Rails, na Railsconf, que inclusive é o seu sustento já que vocês atendem clientes de Rails, ou vamos “ter problemas”. Isso seria péssimo problema de relações públicas num estágio onde eles provavelmente ainda estavam no vermelho, procurando mais investidores e mais clientes. Então no final a Engine Yard cedeu à pressão e anunciou parar o Merb. Foi assim que o Yehuda Katz, Carl Lerche e Matt Aimonetti entraram pro grupo Rails Core Team e foi assim que o Merb morreu e nasceu o Rails 3.0. ufffff</p>
<p>(2009)
Com toda essa controvérsia “resolvida” por enquanto, podemos começar o próximo ano, 2009 em 16 de Março com o lançamento do Rails 2.3; agora sendo uma aplicação Rack. Finalmente, só agora, 7 anos depois do lançamento do framework baseado em I/O assíncrono Twisted do Python, em 27 de Maio temos o lançamento inicial do Node.js graças ao V8 lançado um ano antes. Ela permite que programadores escrevam Javascript client side e server side, pela primeira vez desde o falecido Netscape Livescript nos anos 90. E só pra colocar um pé do Rails até nisso, Ryan Dahl mesmo disse que foi inspirado pelo parser do Mongrel de Zed Shaw pra fazer o Node.js.</p>
<p>Em 13 de Dezembro, Jeremy Ashkenas sobe o primeiro commit do Coffeescript, um dos primeiros transpilers pra tentar tornar programar em Javascript algo mais agradável. Lembrando que nesta época ainda não tínhamos nem sinais do ES6 e Javascript pré-ES6 era um pesadelo de se usar. Na época o Coffeescript era excelente pra se trabalhar, ele inventou o conceito de fat arrows pra funções anônimas, interpolação de strings e muito mais que hoje você tem no ES6, então é muito fácil odiar o Coffee hoje sem saber como era em 2009. O Coffeescript serviu seu propósito em ajudar a evoluir o Javascript.</p>
<p>Virando mais um ano, por volta de abril de 2010 Steve Jobs publica o prego no caixão do Adobe Flash com o artigo Thoughts on Flash e sedimenta o caminho para HTML 5 + CSS 3 + Javascript não só em dispositivos móveis mas também em desktops. O Flash se tornou um morto-vivo desde então. A intenção original de Jobs era empurrar o então melhor navegador com o então melhor engine Javascript que era o Safari no Mac e agora no iPhone como uma plataforma para criar aplicações web. Eu diria que foi Jobs que sem querer tornou mainstream o conceito de Single Page Apps e o primeiro framework javascript a oferecer isso foi o hoje defunto SproutCore que deixou descendente na forma do atual Ember, que foi criado pelo mesmo Yehuda Katz que trabalhou no Merb da Engine Yard e também no Rails 3.0 e resolveu o problema de dependências do Ruby com a introdução do Bundler, que se tornaria referência pra todas as ferramentas de gerenciamento que vieram depois dele como Hex do Elixir, Cargo do Rust, Npm e Yarn no Javascript e assim por diante.</p>
<p>Esse negócio de dois frameworks se unificarem e só sobrar um, como o próprio Yehuda lembrou na época, já tinha acontecido outras vezes. No mundo Java o Struts 2 foi um merge com outro framework, o WebWork que se vendia como “Struts feito direito”. Em 1o de agosto de 2010 é lançado o Rails 3.0 com contribuições do Merb core team; um ano desde o lançamento do Rails 2.3. O que sobrou da comunidade Merb viu as promessas de um caminho de migração e a idéia de que o Rails se tornaria mais próximo das idéias do Merb se esvair. E esse foi o fim dessa história.</p>
<p>2011 foi outro ano interessante, em 12 de Julho o Criador do Ruby, Yukihiro Matsumoto (Matz) e outros membros do Ruby Core, são contratados pelo Heroku. Em 31 de Agosto o Rails 3.1 é lançado e o jQuery substitui o antigo Prototype como padrão. E em 19 de Setembro eu anunciei a criação da minha primeira empresa de desenvolvimento de software própria, a Codeminer 42. Aqui se iniciaria uns 4 anos de pesadelo na minha vida, vocês não tem idéia de como doeu criar uma empresa do zero, bootstrapped, sem investimento externo, pra crescer de forma sustentável e ser lucrativa. Felizmente já faz uns 3 ou 4 anos que as coisas tão super bem, graças a de … graças aos esforços incessantes de quem trabalhou nesses últimos anos na empresa.</p>
<p>Finalmente, em Novembro de 2011 outro produto importante foi lançado, o Travis-CI, fundado por rubistas como Josh Kalderimis, Konstantin Haase, Sven Fuchs e outros, em Berlin, na Alemanha. Eles conseguiram finalmente produtificar o conceito de integração contínua. Não só isso como ofereceram um serviço gratuito pra projetos open source que se integra diretamente no GitHub e pela primeira vez projetos de software livre tinham a opção de demonstrar seu nível de testes diretamente na primeira página dos repositórios. Vocês podem ver que nós rubistas originais levávamos testes realmente a sério.</p>
<p>Só de bônus, foi também em 2011 mas não achei a data que Bryan Helmkamp criou o CodeClimate pra automatizar ferramentas de análise de código como Rubocop, Flay, Brakeman e outros, se integrando ao GitHub pra demonstrar como anda a saúde de nosso código. A partir desse ponto fomos refinando mais e mais nosso cinto de utilidades de qualidade de código Ruby e demos o exemplo pra todas as outras comunidades. Nenhum até hoje conseguiu chegar no mesmo nível obsessivo de qualidade que nós das primeiras gerações de Railers quisemos alcançar.</p>
<p>Vou parar a cronologia por aqui, veja que estou parando 8 anos atrás, mas acho que vocês podem ver como os acontecimentos do mercado em geral e do Ruby on Rails a esta altura já tinham mudado o mundo de desenvolvimento de software. Novas linguagens começaram a surgir, muitos influenciados por Ruby. Novos frameworks começaram a surgir, muitos influenciados por Ruby on Rails. O fluxo de trabalho havia sido já fundamentalmente mudado graças à adoção das práticas ágeis de extreme programming, Os conceitos de produtos Web 2.0, adoção de SaaS, PaaS, mundo cloud e tudo mais derivou dessa geração de Ruby on Rails entre 2004 e 2011. E em 2011 ainda estamos 2 anos antes do Docker ser lançado pela primeira vez, essa história é longa.</p>
<p>Lá no começo, em 2008 eu comecei a entrevistar diversos rubistas e outros desenvolvedores e publiquei no meu blog. Se você é de Microsoft vai saber quem é Scott Hanselman, leia a entrevista no blog. Adrian Holovaty o criador do Django de Python? Tá no blog também. Criadores de conteúdo como Geoffrey Grosenbach que na época tocava o Peepcode e o Peter Cooper que na época tocava o Ruby Inside? Tão no blog também. Depois da controvérsia que o Alex Payne do Twitter causou com o Rails não Escala eu entrevistei um dos primeiros engenheiros do Twitter, o Blaine Cook, confira no blog. Membros fundadores do Ruby Central e os primeiros rubistas, Chad Fowler, David Black depois Evan Phoenix do projeto Rubinius e Ruby Specs, tão no blog. Rails Core Team Alumni como Jamis Buck e Josh Peek, também tão lá no blog. Enfim, vou deixar a página com as entrevistas linkadas nas descrições abaixo.</p>
<p>Durante esse período entre 2008 e 2016 eu tive a oportunidade de trazer muitos dos principais nomes pra cá incluindo os fundadores do Ruby Central como Chad Fowler, David Black e Rich Kilmer, depois Evan Phoenix. Trouxe gente do Rails Core da época como Matt Aimonetti, Aaron Petterson que é o Tenderlove. Fundador do GitHub Chris Wanstrath, depois Scott Chacon. O Konstantin Haase do Sinatra e Travis-CI. E na última tivemos gente do Ruby Core Team do Japão com a presença de Akira Matsuda. E falando em última, foi no dia 25 de setembro de 2016, que depois de 10 anos organizando eventos de Ruby, eu decidi pendurar as chuteiras.</p>
<p>Como tudo que eu faço fora do meu trabalho principal, eu nunca lucrei nada com os eventos. Vamos dizer que como gerador de conteúdo eu dou um péssimo empresário, porque meu primeiro livro de Rails eu escrevi sem ganhar um centavo porque abri mão dos royalties pra baratear o custo, o objetivo era fomentar o mercado. O blog eu até experimentei colocar ads por um tempo mas era muito feio então desisti e nunca monetizei. Os podcasts que eu fazia com Carlos Brando ou o Rosa nunca monetizei. A Rubyconf eu nunca tive nenhuma compensação financeira. E agora fazendo videos, eu também não monetizo.</p>
<p>Faz mais de 10 anos que eu gero conteúdo de graça e espero poder continuar fazendo isso. E eu não faço isso porque eu sou um cara legal, pelo bem disso ou pelo bem daquilo. Eu só faço assim porque eu quero. O dia que eu não puder, eu páro. Na minha cabeça eu sempre tive uma noção que se meu conteúdo fosse atrelado a um incentivo financeiro, isso ia me comprometer demais. Isso já aconteceu antes, quando tinha patrocinador ou grupos externos envolvidos. E não tem nada que me irrita mais do que eu não poder falar alguma coisa porque certos grupos não iam gostar. Por isso eu prefiro pagar as coisas do meu bolso pra nunca ser usado como plataforma pros outros.</p>
<p>Quando anunciei minha aposentadoria da Rubyconf Brasil, eu achava que finalmente ia conseguir passar um ano sem ter que pensar em eventos… ledo engano. Em dezembro de 2016 uma idéia apareceu do nada na minha cabeça, eu juro que tentei tirar essa idéia da cabeça, mas aí eu e meu sócio começamos a montar a conferência experimental The Conf, mas isso é história pra outro dia. Agora em 2019 estamos na 3a edição da The Conf, crescendo um passo de cada vez, como aconteceu com a Rubyconf Brasil. Um dia eu juro que páro de fazer eventos. E, de novo, eu quero que o evento seja o mais independente possível, basicamente eu e meus sócios pagamos praticamente tudo.</p>
<p>Essa história pra mim começou numa quarta-feita a noite em abril de 2006 quando eu me dei conta que quase ninguém no Brasil inteiro tava dando a mínima pra Ruby on Rails ou sequer sabia o que era isso. Eu nunca tinha visto algo assim antes: a tecnologia certa, com os conceitos certos, tudo pronto, e todo mundo ignorando. Eu tinha duas escolhas, me mudar pra São Francisco e tentar a vida em tech startup de Rails, ou tentar começar do absoluto zero no Brasil. Obviamente escolhi a segunda opção e foi assim que eu comecei, com zero seguidores, zero views, zero empresas e zero emprego.</p>
<p>Preciso agradecer muito meu primeiro chefe de Rails, o Carl Youngblood que achou meu blog lá no começo e me mandou uma oferta de trabalho remoto pra Surgeworks. A coincidência é que o Carl foi missionário no Brasil. Ele é mórmom de Utah. E ele fala português brasileiro quase perfeito, com sotaque brasileiro e tudo, foi muito bizarro ver um gringo falar melhor português do que eu falo inglês. E foi assim que me tornei o primeiro brasileiro da equipe e pra onde eu levei diversos nomes que você deve conhecer hoje como Carlos Brando, o Rodrigo Kochenburger que mencionei no começo, o Marcos Tapajós do Rio de Janeiro, o Renato Carvalho de Brasília que depois fundou a Startaê e vários outros.</p>
<p>Com a missão de evangelizar Ruby e Rails no Brasil eu embarquei numa jornada de palestras por todo o Brasil, fui pra Natal, Teresina, Brasilia, Fortaleza, Campos no Rio, Xanxerê, Londrina, Porto Alegre, Curitiba, Salvador, Rio de Janeiro, São Carlos, Campo Grande, além de ter tido a oportunidade de fazer amigos como o Nihn e Hongli da Phusion na Railsconf de Portland em 2008, ter ido pra Las Vegas pela primeira vez na RailsConf 2009, ter conseguido palestrar pela primeira vez num evento internacional na RailsConf 2010, onde eu conversei pela primeira vez com o DHH, com o Bob Martin e muitos outros, e de lá fui palestrar em Buenos Aires na Locos x Rails, em Moscow, em Amsterdam, em Tel Aviv em Israel que certamente foi um dos países mais fascinantes que eu já conheci, e finalmente em Tokyo em 2011 onde eu pude conhecer o grupo por trás do desenvolvimento do Ruby, incluindo seu criador, o Matz, que assistiu minha palestra sobre como a comunidade de Ruby cresceu no Brasil. Ao longo de 10 anos eu devo ter dado umas 200 palestras no Brasil e no mundo, foi uma jornada absurda que eu não imaginava que pudesse acontecer naquela quarta feira a noite em 2006.</p>
<p>Não podia deixar de relembrar os rubistas que nós perdemos. O Jason Seifer, que foi parceiro de Gregg Pollack no RailsEnvy nos deixou em abril de 2017. O excelente blogueiro de Ruby e criador de bibliotecas como o resource controller original, anterior ao inherited resources nos deixou 3 dias antes de acabar 2014. Ezra Zygmuntowicz nos deixou em Dezembro de 2014. O simpático e saudoso Jim Weirich que inspirou toda uma geração a se importar com testes nos deixou também em 2014, em fevereiro. 2014 foi um ano ruim onde, por diversas razões, perdemos excelente pessoas na nossa comunidade.</p>
<p>Falando em Jim Weinrich se você gosta da linguagem Ruby devia se exercitar num de seus legados, o site Ruby Koans com diversos exercícios diferentes do que vocês está acostumado e que vai te fazer pensar bastante. E na mesma linha temos o Ruby Quiz do James Edward Grey II com mais dezenas de exercícios. São excelentes opções pra você que quer juntar seus amigos pra aprender Ruby, como desenvolver com TDD, no formato de Dojo.</p>
<p>Ruby on Rails já completou 15 anos. Foi uma década e meia de mercado e até hoje ainda temos um mercado enorme, onde rubistas ainda estão entre os desenvolvedores mais bem pagos do mercado e onde o framework ainda é o pacote mais completo, tanto do ponto de vista do framework em si e do ecossistema que se formou ao redor. É o pacote mais pragmático pra montar uma tech startup, salvo raras exceções em componentes especiais que podem ser feitos em Elixir ou Go. Mas se você não quer ter dores de cabeça, você ainda escolhe Ruby on Rails. E se precisa de exemplos, não precisa ir muito longe, o GitHub que você talvez já use todo dia é Ruby on Rails, o gigante Shopify foi um dos primeiros a usar Rails e usa até hoje, o AirBnb é Rails, Kickstater é Rails, a Bloomberg ainda usa Rails, o Twitch usa Rails, Zendesk é Rails, Hulu que foi adquirida primeiro pela Fox e agora pela Disney é Rails.</p>
<p>Uma das coisas que muita gente critica é que Ruby on Rails é um framework que se tornou quase um monopólio dentro da comunidade Ruby, temos alguns micro-frameworks como Sinatra e outros menores que tem seu lugar, mas depois do incidente do Merb, nunca mais outro framework chegou perto de competir. Muita gente usa isso pra dizer como em Java, PHP, Javascript tem muito mais opção. Só que isso é um problema na verdade, porque todo mundo pega uma escolha diferente e isso fragmenta a comunidade. Agora você como desenvolvedor muda de empresa, vai encontrar um projeto com bibliotecas e estruturas e estilos completamente diferentes. Toda vez você tem uma curva de aprendizado maior e você nunca realmente domina, é o caso de virar um Jack of all trades, master of none. Como em Ruby, todo projeto em Rails é parecido, quando você muda de empresa, encontra o mesmo ambiente e já é produtivo imediatamente. Por isso andamos tão rápido em tão pouco tempo, porque além disso todo o ecossistema de bibliotecas, produtos e soluções só precisava focar num único framework e com isso nasceu Heroku, Travis, CodeClimate, New Relic.</p>
<p>Levou anos pras outras comunidades começarem a entender um pouco disso e agora no Python meio que Django é a escolha padrão, em PHP o Laravel é a escolha padrão, em Elixir o Phoenix é o padrão, em Crystal o Lucky ou Kemal são padrão, no Java não tenho certeza se Play ou Spring é mais usado, no Go não tenho idéia de qual é padrão mas acho que não tem, são várias opções, em Javascript nem me pergunta, não quero nem pensar nessa zona.</p>
<p>A única outra comunidade na época que pôde usufruir desse fator, pelo bem ou pelo mal, foi o ASP.NET da Microsoft. A diferença é que o ASP.NET original era baseado nos paradigmas que tínhamos no J2EE do fim dos anos 90 e que foi justamente o que o Rails veio combater em 2004. E em 2004 nenhuma outra linguagem tinha opção parecida. Levou anos e anos até finalmente chegarmos no estágio de hoje, onde todo mundo finalmente está trabalhando mais ou menos “on Rails” que quer dizer com foco em qualidade, em entregar valor, em usar as práticas ágeis, usando essa geração de SaaS e IaaS com bons repositórios de código como GitHub ou GitLab, bons gerenciadores de projeto especialmente os derivados do Pivotal Tracker, embora os derivados de Trello também quebrem o galho, boas soluções de deployment de Heroku a soluções da AWS. E até a Microsoft cedeu e com o ASP.NET MVC ele se aproximou dos paradigmas do Ruby on Rails também.</p>
<p>O modelo de desenvolvimento de software se divide em antes e depois de 2004. E tudo que construímos nos 10 anos seguintes até 2014 é o que usamos até hoje em 2019 como o estado da arte. Eu queria falar dos últimos 8 anos desde 2011 mas vou parar por aqui e quem sabe retomar num próximo episódio, mas o objetivo de hoje era registrar o que eu lembro que foi importante pra mim no começo da história e qual foi a importância do surgimento do Ruby on Rails e porque vai ser muito difícil algo parecido acontecer de novo: o DHH teve a sorte de lançar o produto certo na hora certa, com diversos acontecimentos por todo o mercado convergindo no tipo de solução que ele apresentou. O produto certo, feito com os princípios corretos, escolhendo as tecnologias e paradigmas que mais tinham chance, e conseguindo convencer o grupo certo de pessoas, inspiradas pela vontade de se expressar com código em vez de só seguir patterns chatos corporativos e fazer produtos enterprise que ninguém ligava.</p>
<p>Mas sendo muito honesto eu acredito que a influência do Ruby on Rails foi até mais ou menos 2012. Depois disso finalmente as demais comunidades adotaram práticas similares e apareceu uma nova classe de problemas pra resolver. O tipo de coisa que você precisa pra ter um Whatsapp, Discord, Hangouts, Facebook Messenger. Eu diria que é onde o problema original do Twitter finalmente atingiu o mainstream e realmente passamos a resgatar o modelo do Erlang em outras linguagens como Clojure, Go, Elixir, e o meio tapa buraco que foi Node.js pra esses casos. Depois de 2012 que as linguagens mobile se modernizaram com Swift e Kotlin. E também foi depois de 2012 que o Docker apareceu e a conversa sobre devops ganhou um level up culminando com o surgimento de ferramentas e plataformas como Terraform e Kubernetes.</p>
<p>Com tudo isso dito, missão cumprida! Por hoje eu fico por aqui. Se ficaram com dúvidas mande nos comentários abaixo, se curtiram mandem um joinha, não deixem de assinar o canal e 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>rails</category><category>ruby</category><category>historia-da-computacao</category><category>akitando</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>Iniciante: Longa Vida ao PhantomJS - Vamos Usar Chrome Headless Agora</title><link>https://akitaonrails.github.io/2017/10/31/iniciante-longa-vida-ao-phantomjs-vamos-usar-chrome-headless-agora/</link><guid isPermaLink="true">https://akitaonrails.github.io/2017/10/31/iniciante-longa-vida-ao-phantomjs-vamos-usar-chrome-headless-agora/</guid><pubDate>Tue, 31 Oct 2017 19:48:00 -0200</pubDate><description>&lt;p&gt;Se você faz Feature Specs — o processo de subir um servidor real da aplicação e um browser headless real para testar funcionalidades do ponto de vista do usuário — então você conhece o &lt;a href="https://github.com/teamcapybara/capybara/issues/1860"target="_blank" rel="noopener"&gt;Capybara&lt;/a&gt; e um dos seus drivers mais famosos, o &lt;a href="https://github.com/teampoltergeist/poltergeist/issues/882"target="_blank" rel="noopener"&gt;Poltergeist&lt;/a&gt;. O Poltergeist encapsula o PhantomJS, que é um browser headless baseado em WebKit bastante conhecido.&lt;/p&gt;
&lt;p&gt;Só que o WebKit é famoso por ser &lt;strong&gt;muito&lt;/strong&gt; complicado de lidar. Então dá pra imaginar o pesadelo que é manter o PhantomJS, que é basicamente manter um browser completo como Chrome ou Safari.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Se você faz Feature Specs — o processo de subir um servidor real da aplicação e um browser headless real para testar funcionalidades do ponto de vista do usuário — então você conhece o <a href="https://github.com/teamcapybara/capybara/issues/1860"target="_blank" rel="noopener">Capybara</a> e um dos seus drivers mais famosos, o <a href="https://github.com/teampoltergeist/poltergeist/issues/882"target="_blank" rel="noopener">Poltergeist</a>. O Poltergeist encapsula o PhantomJS, que é um browser headless baseado em WebKit bastante conhecido.</p>
<p>Só que o WebKit é famoso por ser <strong>muito</strong> complicado de lidar. Então dá pra imaginar o pesadelo que é manter o PhantomJS, que é basicamente manter um browser completo como Chrome ou Safari.</p>
<p>Não é de espantar que, quando o time do Chrome anunciou a disponibilidade do <a href="https://developers.google.com/web/updates/2017/04/headless-chrome"target="_blank" rel="noopener">Chrome Driver</a>, o mantenedor do PhantomJS <a href="https://github.com/teampoltergeist/poltergeist/issues/882"target="_blank" rel="noopener">decidiu se afastar do projeto</a>.</p>
<p>Se você conhece os contribuidores do PhantomJS, agradeça a eles — o projeto nos ajudou a construir funcionalidades de usuário mais sólidas por anos.</p>
<p>Dito isso, não precisa se preocupar. É perfeitamente possível substituir Poltergeist/PhantomJS por Selenium WebDriver/Chrome Driver no seu setup de RSpec/Capybara.</p>
<p>Meu amigo <a href="https://www.lucascaton.com.br/2017/06/22/how-to-run-your-feature-specs-using-capybara-and-headless-chrome/"target="_blank" rel="noopener">Lucas Caton</a> escreveu sobre isso em junho deste ano. Vale acompanhar o blog dele também.</p>
<p>Se você usa Linux com o browser Chromium, não precisa instalar nada — o Chrome Driver já vem com o Chromium. Caso contrário, instale os pacotes adequados para o seu sistema operacional. Por exemplo: <code>brew install chromedriver</code> no OS X, ou <code>pacaur -S chromedriver</code> no Arch Linux se preferir não ter o Chromium instalado. No Ubuntu, talvez seja necessário <a href="https://askubuntu.com/questions/539498/where-does-chromedriver-install-to"target="_blank" rel="noopener">ajustar seu PATH</a>.</p>
<p>Regra geral: instale o Chromium.</p>
<p>No meu caso, as mudanças foram as seguintes:</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-diff" data-lang="diff"><span class="line"><span class="cl"># Gemfile
</span></span><span class="line"><span class="cl"><span class="gd">- gem &#34;poltergeist&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+ gem &#34;selenium-webdriver&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+ gem &#34;rspec-retry&#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>Depois, na configuração do Capybara:</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-diff" data-lang="diff"><span class="line"><span class="cl">Capybara.server = :puma # Até o setup funcionar
</span></span><span class="line"><span class="cl">Capybara.server = :puma, { Silent: true } # Para limpar o output dos testes
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gd">- Capybara.register_driver :poltergeist do |app|
</span></span></span><span class="line"><span class="cl"><span class="gd">-   options = {
</span></span></span><span class="line"><span class="cl"><span class="gd">-     timeout: 3.minutes,
</span></span></span><span class="line"><span class="cl"><span class="gd">-     phantomjs_options: [
</span></span></span><span class="line"><span class="cl"><span class="gd">-       &#39;--proxy-type=none&#39;,
</span></span></span><span class="line"><span class="cl"><span class="gd">-       &#39;--load-images=no&#39;,
</span></span></span><span class="line"><span class="cl"><span class="gd">-       &#39;--ignore-ssl-errors=yes&#39;,
</span></span></span><span class="line"><span class="cl"><span class="gd">-       &#39;--ssl-protocol=any&#39;,
</span></span></span><span class="line"><span class="cl"><span class="gd">-       &#39;--web-security=false&#39;
</span></span></span><span class="line"><span class="cl"><span class="gd">-     ]
</span></span></span><span class="line"><span class="cl"><span class="gd">-   }
</span></span></span><span class="line"><span class="cl"><span class="gd">-   Capybara::Poltergeist::Driver.new(app, options)
</span></span></span><span class="line"><span class="cl"><span class="gd">- end
</span></span></span><span class="line"><span class="cl"><span class="gd">- Capybara.javascript_driver = :poltergeist
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gi">+ Capybara.register_driver :chrome do |app|
</span></span></span><span class="line"><span class="cl"><span class="gi">+   capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
</span></span></span><span class="line"><span class="cl"><span class="gi">+     chromeOptions: {
</span></span></span><span class="line"><span class="cl"><span class="gi">+       args: %w[ no-sandbox headless disable-popup-blocking disable-gpu window-size=1280,1024]
</span></span></span><span class="line"><span class="cl"><span class="gi">+     }
</span></span></span><span class="line"><span class="cl"><span class="gi">+   )
</span></span></span><span class="line"><span class="cl"><span class="gi">+ 
</span></span></span><span class="line"><span class="cl"><span class="gi">+   Capybara::Selenium::Driver.new(app, browser: :chrome, desired_capabilities: capabilities)
</span></span></span><span class="line"><span class="cl"><span class="gi">+ end
</span></span></span><span class="line"><span class="cl"><span class="gi">+ 
</span></span></span><span class="line"><span class="cl"><span class="gi">+ Capybara::Screenshot.register_driver :chrome do |driver, path|
</span></span></span><span class="line"><span class="cl"><span class="gi">+   driver.save_screenshot(path)
</span></span></span><span class="line"><span class="cl"><span class="gi">+ end
</span></span></span><span class="line"><span class="cl"><span class="gi">+ 
</span></span></span><span class="line"><span class="cl"><span class="gi">+ Capybara.javascript_driver = :chrome
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Capybara.default_max_wait_time = 5 # aumente esse timeout se sua aplicação for pesada para carregar
</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 feature specs, às vezes o próprio Rails demora um pouco para subir, compilar assets e tal, e o primeiro spec pode dar timeout. Para evitar falha na execução dos testes, recomendo adicionar a gem <code>rspec-retry</code> como fiz acima, e incluir o seguinte no seu <code>spec/rails_helper.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;rspec/retry&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="no">RSpec</span><span class="o">.</span><span class="n">configure</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># mostra o status de retry no processo do spec</span>
</span></span><span class="line"><span class="cl">  <span class="n">config</span><span class="o">.</span><span class="n">verbose_retry</span> <span class="o">=</span> <span class="kp">true</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Tenta duas vezes (repete uma vez)</span>
</span></span><span class="line"><span class="cl">  <span class="n">config</span><span class="o">.</span><span class="n">default_retry_count</span> <span class="o">=</span> <span class="mi">2</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Faz retry apenas quando o Selenium lança Net::ReadTimeout</span>
</span></span><span class="line"><span class="cl">  <span class="n">config</span><span class="o">.</span><span class="n">exceptions_to_retry</span> <span class="o">=</span> <span class="o">[</span><span class="no">Net</span><span class="o">::</span><span class="no">ReadTimeout</span><span class="o">]</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>E é isso. Não precisei alterar nenhum dos meus feature specs e todos rodaram perfeitamente. Parabéns às equipes que mantêm o Capybara e o Selenium-WebDriver por suportarem essa mudança.</p>
<p>Se você também desenvolve em Node.js, provavelmente já usou algo como o Casper, que dizem suportar Chrome Headless também. Mas já que estamos no assunto, vale muito conferir o <a href="https://github.com/GoogleChrome/puppeteer"target="_blank" rel="noopener">Puppeteer</a>, da própria equipe do Google. É uma biblioteca baseada em Promises onde você escreve 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-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">puppeteer</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;puppeteer&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span><span class="kr">async</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">browser</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">puppeteer</span><span class="p">.</span><span class="nx">launch</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">page</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">browser</span><span class="p">.</span><span class="nx">newPage</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">&#39;https://example.com&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">screenshot</span><span class="p">({</span><span class="nx">path</span><span class="o">:</span> <span class="s1">&#39;example.png&#39;</span><span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kr">await</span> <span class="nx">browser</span><span class="p">.</span><span class="nx">close</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>Então sim, o Chrome Headless parece uma ótima opção — afinal, a maioria dos usuários já usa o Chrome, o que significa que teremos feature specs mais confiáveis e ferramentas de web crawling mais robustas.</p>
]]></content:encoded><category>rails</category><category>testes</category><category>tutoriais</category></item><item><title>Como Não Zerar sua Cota de Desenvolvimento no Pusher</title><link>https://akitaonrails.github.io/2017/10/27/como-nao-zerar-sua-cota-de-desenvolvimento-no-pusher/</link><guid isPermaLink="true">https://akitaonrails.github.io/2017/10/27/como-nao-zerar-sua-cota-de-desenvolvimento-no-pusher/</guid><pubDate>Fri, 27 Oct 2017 13:43:00 -0200</pubDate><description>&lt;p&gt;Se você está desenvolvendo com WebSockets (notificações em tempo real, chat em tempo real, etc), uma das melhores opções SaaS disponíveis ainda é o &lt;a href="pusher.com"&gt;&lt;strong&gt;Pusher&lt;/strong&gt;&lt;/a&gt;. Sempre foi confiável.&lt;/p&gt;
&lt;p&gt;Para cada aplicação que você cria, ele oferece ambientes separados de desenvolvimento, staging e produção, com tokens de chave/segredo independentes.&lt;/p&gt;
&lt;p&gt;Um problema que encontrei esses dias foi que eu estava zerando rapidamente a cota gratuita de mensagens do ambiente de desenvolvimento (200.000 mensagens por dia). O motivo era que toda a minha equipe usava a mesma chave, e o servidor de Integração Contínua fazia conexões reais sempre que rodava no mesmo ambiente. Isso consome tudo rapidamente e bloqueia tanto o desenvolvimento quanto o CI.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Se você está desenvolvendo com WebSockets (notificações em tempo real, chat em tempo real, etc), uma das melhores opções SaaS disponíveis ainda é o <a href="pusher.com"><strong>Pusher</strong></a>. Sempre foi confiável.</p>
<p>Para cada aplicação que você cria, ele oferece ambientes separados de desenvolvimento, staging e produção, com tokens de chave/segredo independentes.</p>
<p>Um problema que encontrei esses dias foi que eu estava zerando rapidamente a cota gratuita de mensagens do ambiente de desenvolvimento (200.000 mensagens por dia). O motivo era que toda a minha equipe usava a mesma chave, e o servidor de Integração Contínua fazia conexões reais sempre que rodava no mesmo ambiente. Isso consome tudo rapidamente e bloqueia tanto o desenvolvimento quanto o CI.</p>
<p>Na verdade, fazer conexões reais a sistemas externos em situações de teste não é boa prática. Os testes podem falhar aleatoriamente por vários motivos, então sempre devemos mockar essas dependências. Mas mockar um sistema complexo (WebSockets e HTTP) como o Pusher não é trivial.</p>
<p>Felizmente, encontrei o <a href="https://github.com/tristandunn/pusher-fake"target="_blank" rel="noopener">Pusher Fake</a>. Ele basicamente implementa todas as APIs e endpoints WebSocket necessários para imitar o Pusher e enganar tanto o servidor quanto o cliente JS para se comunicar com ele.</p>
<p>A ideia é que sua aplicação Rails, em modo de desenvolvimento, faça um fork de um processo separado de servidor para que o cliente Pusher se conecte. Essa gem é ao mesmo tempo um servidor clone do Pusher e uma série de wrappers para carregá-lo no seu setup.</p>
<p>Começando pelo básico.</p>
<p>Na sua aplicação, você terá tanto uma configuração de conexão do cliente Pusher no lado Ruby (para enviar mensagens a um canal no servidor Pusher) quanto uma instância Pusher do lado Javascript, no cliente, principalmente para assinar um canal em uma conexão WebSocket e receber mensagens.</p>
<p>Primeiro, precisamos configurar o lado Ruby. Geralmente fica em um <code>config/initializers/pusher.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="no">Pusher</span><span class="o">.</span><span class="n">app_id</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_APP&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="no">Pusher</span><span class="o">.</span><span class="n">key</span>    <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_KEY&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="no">Pusher</span><span class="o">.</span><span class="n">secret</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_SECRET&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="no">Pusher</span><span class="o">.</span><span class="n">cluster</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_CLUSTER&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># nunca defina Pusher.host ou Pusher.port aqui</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>Note que estou usando variáveis de ambiente para guardar a configuração. Você deve usar algo como a <a href="https://github.com/laserlemon/figaro"target="_blank" rel="noopener">gem figaro</a> ou a <a href="https://github.com/bkeepers/dotenv"target="_blank" rel="noopener">gem dotenv-rails</a>. Por exemplo:</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">PUSHER_APP</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;xpto&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">PUSHER_KEY</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;abcd1234&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">PUSHER_SECRET</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;abcd1234&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">PUSHER_CLUSTER</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;us2&#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>No mínimo você precisa de um ID de aplicação, uma chave, um segredo e um nome de cluster. Tudo isso é fornecido pelo Pusher quando você registra uma nova aplicação lá.</p>
<p>Segundo, precisamos configurar a instância Javascript. Geralmente você tem algo no diretório <code>assets/javascripts</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-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="c1">// exemplo .js.erb
</span></span></span><span class="line"><span class="cl"><span class="nb">window</span><span class="p">.</span><span class="nx">pusher</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Pusher</span><span class="p">(</span><span class="o">&lt;%=</span> <span class="nx">ENV</span><span class="p">[</span><span class="s1">&#39;PUSHER_APP] %&gt;, {
</span></span></span><span class="line"><span class="cl"><span class="s1">  cluster: &lt;%= ENV[&#39;</span><span class="nx">PUSHER_KEY</span><span class="s1">&#39;] %&gt;,
</span></span></span><span class="line"><span class="cl"><span class="s1">  encrypted: &lt;%= ENV[&#39;</span><span class="nx">PUSHER_ENCRYPTED</span><span class="err">&#39;</span><span class="p">]</span> <span class="o">%&gt;</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>No Console de Desenvolvimento do Chrome, você pode inspecionar essa instância digitando:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Pusher.instances[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>Assim você consegue verificar se está pegando as configurações corretas para a conexão e depurar problemas.</p>
<p>As dependências são a gem pusher no seu <code>Gemfile</code> e o cliente javascript.</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"># Gemfile</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;pusher&#39;</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>No caso do arquivo javascript, você pode adicioná-lo ao projeto e depender do <a href="http://www.akitaonrails.com/2017/10/24/iniciante-experimentando-o-rails-5-1-x"target="_blank" rel="noopener">Webpacker</a>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>yarn add 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>Então, no seu arquivo javascript ES6, você faz:</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="kr">const</span> <span class="nx">Pusher</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;pusher-js&#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>Ou pode linkar diretamente no seu layout:</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="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;https://js.pusher.com/4.2/pusher.min.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</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 mais informações sobre o cliente pusher-js, leia o <a href="https://github.com/pusher/pusher-js"target="_blank" rel="noopener">arquivo README</a>.</p>
<h2>Adicionando o Pusher Fake<span class="hx:absolute hx:-mt-20" id="adicionando-o-pusher-fake"></span>
    <a href="#adicionando-o-pusher-fake" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Nesse ponto, você já deve conseguir se conectar à conta real do Pusher e ver a mágica do tempo real acontecendo.</p>
<p>E você já está consumindo a cota gratuita disponível no seu ambiente de desenvolvimento no Pusher. Para a maioria das pessoas, isso é suficiente.</p>
<p>Mas o objetivo é NÃO se conectar ao Pusher pela internet e manter tudo local para desenvolvimento e testes. Vamos começar adicionando o <a href="https://github.com/tristandunn/pusher-fake"target="_blank" rel="noopener">Pusher Fake</a> ao nosso <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">:development</span><span class="p">,</span> <span class="ss">:test</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;pusher-fake&#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 é onde a configuração do Pusher Fake pode ficar complicada se você não entender o que está acontecendo. Como disse antes, o PusherFake precisa fazer um fork de um novo processo para carregar um servidor local que imita o Pusher.</p>
<p>Para carregá-lo, você precisa apontar para o servidor local. Lembra do nosso <code>config/initializers/pusher.rb</code>? Basta fazer um require de um arquivo simples 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="no">Pusher</span><span class="o">.</span><span class="n">app_id</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_APP&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="no">Pusher</span><span class="o">.</span><span class="n">key</span>    <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_KEY&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="no">Pusher</span><span class="o">.</span><span class="n">secret</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_SECRET&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="no">Pusher</span><span class="o">.</span><span class="n">cluster</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_CLUSTER&#39;</span><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="no">Rails</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">development?</span>
</span></span><span class="line"><span class="cl">  <span class="nb">require</span> <span class="s2">&#34;pusher-fake/support/base&#34;</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>Só isso já apresenta MUITOS problemas se você não tiver cuidado. Esse <code>require</code> vai <strong>fazer um fork</strong> de um novo processo. Se você estiver usando um servidor web de processo único como Webrick ou Thin, tudo bem. Se estiver usando Puma, Unicorn ou Passenger com no máximo um processo, também deve funcionar. Mas se você carregar um servidor web que por si mesmo faz fork de novos processos, vai ter problema.</p>
<p>Na prática, prefiro carregar o servidor Pusher Fake separadamente, em modo standalone. Felizmente ele fornece um comando de linha de comando para iniciá-lo. E é boa prática configurar isso em um arquivo <code>Procfile.dev</code> e usar o <a href="https://github.com/ddollar/foreman"target="_blank" rel="noopener">foreman</a> para iniciar tudo. O <code>Procfile.dev</code> fica assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>web: bundle exec rails s -p 3000
db: postgres -D /usr/local/var/postgres
redis: redis-server /usr/local/etc/redis.conf
mailcatcher: mailcatcher -f
pusherfake: pusher-fake -i ${PUSHER_APP:-xpto} --socket-host 0.0.0.0 --socket-port ${PUSHER_WS_PORT:-45449} --web-host 0.0.0.0 --web-port ${PUSHER_PORT:-8888} -k ${PUSHER_KEY:-abcd1234} -s ${PUSHER_SECRET:-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>De bônus, veja como configuro outros serviços como PostgreSQL, Redis, etc.</p>
<p>Se você não sabia, pode usar <code>${VARIABLE_NAME:-default_value}</code> para usar uma variável de ambiente ou ter um valor padrão caso ela não exista. Isso significa que suas variáveis de ambiente configuradas com Figaro ou Dotenv precisam ter os mesmos valores.</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">PUSHER_APP</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;xpto&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">PUSHER_KEY</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;abcd1234&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">PUSHER_SECRET</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;abcd1234&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">PUSHER_CLUSTER</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;us2&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">PUSHER_HOST</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;127.0.0.1&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">PUSHER_PORT</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;8888&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">PUSHER_WS_HOST</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;127.0.0.1&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">PUSHER_WS_PORT</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;45449&#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 seu <code>config/initializers/pusher.rb</code> deve 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="no">Pusher</span><span class="o">.</span><span class="n">app_id</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_APP&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="no">Pusher</span><span class="o">.</span><span class="n">key</span>    <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_KEY&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="no">Pusher</span><span class="o">.</span><span class="n">secret</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_SECRET&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="no">Pusher</span><span class="o">.</span><span class="n">cluster</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_CLUSTER&#39;</span><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="no">Rails</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">development?</span>
</span></span><span class="line"><span class="cl">  <span class="no">Pusher</span><span class="o">.</span><span class="n">host</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_HOST&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="no">Pusher</span><span class="o">.</span><span class="n">port</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_PORT&#39;</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>E a configuração do Pusher-js em algum lugar do seu diretório <code>app/assets/javascripts/</code> vai ficar 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-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="o">&lt;%</span> <span class="k">if</span> <span class="nx">defined</span><span class="o">?</span><span class="p">(</span><span class="nx">PusherFake</span><span class="p">)</span> <span class="o">%&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="o">&lt;%</span> <span class="k">if</span> <span class="nx">Rails</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">test</span><span class="o">?</span> <span class="o">%&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nx">pusher</span> <span class="o">=</span> <span class="o">&lt;%=</span> <span class="nx">PusherFake</span><span class="p">.</span><span class="nx">javascript</span><span class="p">(</span><span class="nx">cluster</span><span class="o">:</span> <span class="nx">ENV</span><span class="p">[</span><span class="s2">&#34;PUSHER_CLUSTER&#34;</span><span class="p">])</span> <span class="o">%&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="o">&lt;%</span> <span class="k">else</span> <span class="o">%&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">window</span><span class="p">.</span><span class="nx">pusher</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Pusher</span><span class="p">(</span><span class="o">&lt;%=</span> <span class="nx">ENV</span><span class="p">[</span><span class="s1">&#39;PUSHER_KEY&#39;</span><span class="p">]</span> <span class="o">%&gt;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">cluster</span><span class="o">:</span> <span class="o">&lt;%=</span> <span class="nx">ENV</span><span class="p">[</span><span class="s1">&#39;PUSHER_CLUSTER&#39;</span><span class="p">]</span> <span class="o">%&gt;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">wsHost</span><span class="o">:</span> <span class="o">&lt;%=</span> <span class="nx">ENV</span><span class="p">[</span><span class="s1">&#39;PUSHER_WS_HOST&#39;</span><span class="p">]</span> <span class="o">%&gt;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">wsPort</span><span class="o">:</span> <span class="o">&lt;%=</span> <span class="nx">ENV</span><span class="p">[</span><span class="s1">&#39;PUSHER_WS_PORT&#39;</span><span class="p">]</span> <span class="o">%&gt;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">encrypted</span><span class="o">:</span> <span class="o">&lt;%=</span> <span class="nx">ENV</span><span class="p">[</span><span class="s1">&#39;PUSHER_ENCRYPTED&#39;</span><span class="p">]</span> <span class="o">%&gt;</span><span class="p">})</span>    
</span></span><span class="line"><span class="cl">    <span class="o">&lt;%</span> <span class="nx">end</span> <span class="o">%&gt;</span>
</span></span><span class="line"><span class="cl"><span class="o">&lt;%</span> <span class="k">else</span> <span class="o">%&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">window</span><span class="p">.</span><span class="nx">pusher</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Pusher</span><span class="p">(</span><span class="o">&lt;%=</span> <span class="nx">ENV</span><span class="p">[</span><span class="s1">&#39;PUSHER_KEY&#39;</span><span class="p">]</span> <span class="o">%&gt;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">cluster</span><span class="o">:</span> <span class="o">&lt;%=</span> <span class="nx">ENV</span><span class="p">[</span><span class="s1">&#39;PUSHER_CLUSTER&#39;</span><span class="p">]</span> <span class="o">%&gt;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">encrypted</span><span class="o">:</span> <span class="o">&lt;%=</span> <span class="nx">ENV</span><span class="p">[</span><span class="s1">&#39;PUSHER_ENCRYPTED&#39;</span><span class="p">]</span> <span class="o">%&gt;</span><span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="o">&lt;%</span> <span class="nx">end</span> <span class="o">%&gt;</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>Lembre que este é um arquivo Javascript+ERB, então podemos buscar as mesmas variáveis de ambiente para a configuração.</p>
<p>Agora, sempre que você executar <code>foreman start -f Procfile.dev -p 3000</code>, ele vai carregar o servidor Pusher Fake com as configurações de desenvolvimento corretas, e tanto o seu servidor ruby quanto o cliente javascript devem se conectar a ele sem nenhum problema.</p>
<p>Note também o trecho <code>if Rails.env.test?</code>. Isso é para a sua suite de testes RSpec. No caso do ambiente de testes, não vamos carregar o servidor fake manualmente; em vez disso, vamos criar algo como <code>spec/support/pusher-fake.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="nb">require</span> <span class="s2">&#34;pusher-fake/support/rspec&#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>E é isso. O <code>PusherFake.javascript</code> vai definir a configuração padrão de conexão WebSocket, e o <code>require</code> acima vai tanto fazer o fork do servidor fake quanto configurar o RSpec para limpar os canais a cada execução de teste (via <code>PusherFake::Channel.reset</code>).</p>
<p>Desta forma, seu ambiente de testes também evita se conectar ao servidor Pusher real, externo.</p>
<p>A chave de tudo isso são as variáveis de ambiente. Você precisa garantir que cada peça esteja carregando a mesma configuração — caso contrário, você terá o servidor fake fazendo bind em uma porta diferente da que o Pusher-js está tentando se conectar, e vai ter erros. Depure com cuidado.</p>
<p>O mais importante: se você fez tudo corretamente, agora é independente do servidor Pusher real para os ambientes de desenvolvimento e testes, nunca vai atingir nenhum limite de cota, e sua equipe e seu CI poderão trabalhar de forma ininterrupta, com comportamento determinístico.</p>
]]></content:encoded><category>rails</category><category>testes</category><category>tutoriais</category></item><item><title>Iniciante Experimentando o Rails 5.1.x</title><link>https://akitaonrails.github.io/2017/10/24/iniciante-experimentando-o-rails-5-1-x/</link><guid isPermaLink="true">https://akitaonrails.github.io/2017/10/24/iniciante-experimentando-o-rails-5-1-x/</guid><pubDate>Tue, 24 Oct 2017 16:29:00 -0200</pubDate><description>&lt;p&gt;Uma coisa chata de lidar com projetos de clientes é que a maioria deles demora para fazer um upgrade completo de framework. E não dá para culpá-los, já que boa parte das dependências do ecossistema também leva um tempão para atualizar.&lt;/p&gt;
&lt;p&gt;O &lt;a href="http://weblog.rubyonrails.org/2017/9/29/this-week-in-rails-getting-closer-to-rails-5-2-beta/"target="_blank" rel="noopener"&gt;Rails 5.2 Beta&lt;/a&gt; está chegando, e eu apostaria que a maioria dos projetos Rails lá fora ainda está na versão 4.2, ainda cogitando o salto para o 5.0 (que até não é um upgrade tão complicado).&lt;/p&gt;</description><content:encoded><![CDATA[<p>Uma coisa chata de lidar com projetos de clientes é que a maioria deles demora para fazer um upgrade completo de framework. E não dá para culpá-los, já que boa parte das dependências do ecossistema também leva um tempão para atualizar.</p>
<p>O <a href="http://weblog.rubyonrails.org/2017/9/29/this-week-in-rails-getting-closer-to-rails-5-2-beta/"target="_blank" rel="noopener">Rails 5.2 Beta</a> está chegando, e eu apostaria que a maioria dos projetos Rails lá fora ainda está na versão 4.2, ainda cogitando o salto para o 5.0 (que até não é um upgrade tão complicado).</p>
<p>A propósito, caso você não saiba, o site oficial do Rails Guides tem uma seção dedicada a upgrades. É o <a href="http://guides.rubyonrails.org/upgrading_ruby_on_rails.html"target="_blank" rel="noopener">A Guide for Upgrading Ruby on Rails</a>. O processo de upgrade não é tão difícil <strong>se, e somente se</strong>:</p>
<ul>
<li>
<p>Você tiver uma cobertura de testes razoável. No mínimo, feature specs para as partes mais críticas do sistema.</p>
</li>
<li>
<p>Você subir apenas uma versão de cada vez, nunca pular direto para a mais recente. Por exemplo: se está no Rails 4.1, primeiro sobe para o 4.2, confirma que tudo funciona. Depois vai para o 5.0. E só então vai para o 5.1. Cada versão tem <strong>muitas</strong> deprecations, mudanças de comportamento, alterações na configuração padrão e recursos novos. Cada ponto merece atenção cuidadosa.</p>
</li>
</ul>
<p>Então, o que você está perdendo se ainda está no Rails 4.2?</p>
<p>De novo, caso não saiba, o site oficial do Rails Guides também compila Release Notes detalhadas e fáceis de ler para cada nova versão. As mais recentes são as <a href="http://guides.rubyonrails.org/5_1_release_notes.html"target="_blank" rel="noopener">Ruby on Rails 5.1 Release Notes</a>.</p>
<h2>Começando um novo projeto<span class="hx:absolute hx:-mt-20" id="começando-um-novo-projeto"></span>
    <a href="#come%c3%a7ando-um-novo-projeto" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O comando <code>rails new</code> ganhou várias flags novas. Acho que as que a maioria vai usar são:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>rails new --skip-action-mailer --skip-coffee --webpack=react my_fancy_new_project</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 desabilitar recursos que não precisa, como o ActionMailer. Pode desabilitar o CoffeeScript. Foi legal por um tempo, mas agora que temos ES6 é melhor usar direto.</p>
<p>O Rails 5.1 vem com suporte a webpack. Mas até o Rails 5.2 chegar, precisamos rodar o <code>webpack-dev-server</code> manualmente em modo de desenvolvimento para ter hot reload dos assets. Para isso, recomendo instalar o bom e velho <a href="https://github.com/ddollar/foreman"target="_blank" rel="noopener">Foreman</a>. Instale assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gem install foreman</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 adicione um arquivo <code>Procfile.dev</code> no seu projeto com o seguinte conteúdo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>web: ./bin/rails s -b 0.0.0.0 -p 3000
webpack: ./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>Agora você pode subir seu servidor de desenvolvimento assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>foreman start -f Procfile.dev</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>Em produção, não precisa do <code>webpack-dev-server</code>, pois a task <code>bin/rails assets:precompile</code> consegue compilar todos os assets como arquivos estáticos com timestamps em <code>public/assets</code>.</p>
<h2>Mudanças no Front-end<span class="hx:absolute hx:-mt-20" id="mudanças-no-front-end"></span>
    <a href="#mudan%c3%a7as-no-front-end" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Agora você pode comemorar: Yarn, Webpack, e sem mais jQuery pré-instalado. Samuel Muller tem um <a href="http://samuelmullen.com/articles/embracing-change-rails51-adopts-yarn-webpack-and-the-js-ecosystem/"target="_blank" rel="noopener">artigo bem bacana</a> que resume a maioria dessas mudanças em mais detalhes.</p>
<p>O Ruby on Rails agora suporta oficialmente tudo o que o ecossistema Javascript tem a oferecer. Não falta mais nada.</p>
<p>Acabou a era de fazer vendor manual de assets JS no diretório <code>vendor/assets/javascripts</code>. Não precisa mais do <a href="https://rails-assets.org/"target="_blank" rel="noopener">Rails Assets</a>, que era uma fonte secundária do RubyGems específica para empacotar bibliotecas JS como gems.</p>
<p>Digamos que, por algum motivo, você quer o jQuery de volta. Agora é só fazer:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>yarn add jquery</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, como de costume, adicionar no seu manifest <code>app/assets/javascripts/application.js</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>// app/assets/javascripts/application.js
...
//= require jquery
...
//= require_tree .</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 tal <a href="https://github.com/rails/rails/pull/26836"target="_blank" rel="noopener">&ldquo;suporte ao Yarn no Rails 5.1&rdquo;</a> se resume a alguns wrappers e boilerplate. Você pode rodar <code>rails yarn:install</code> para instalar as dependências, mas pode igualmente digitar <code>yarn</code> que ele instala tudo o que está declarado no <code>package.json</code>, como em qualquer projeto puramente Javascript.</p>
<p>Toda a mágica da integração vem com a inclusão da gem <a href="https://github.com/rails/webpacker"target="_blank" rel="noopener">Webpacker</a>. Ela adiciona um monte de configuração de boilerplate para o webpack. E, <strong>dado bônus</strong>: você pode incluí-la hoje mesmo nos seus projetos Rails 4.2+. Comece adicionando a gem ao <code>Gemfile</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># Gemfile
gem &#39;webpacker&#39;, &#39;~&gt; 3.0&#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>Depois <code>bundle install</code> normalmente e rode:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>bundle exec rake webpacker: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>Se não usou a flag <code>--webpack=react</code> no <code>rails new</code>, pode adicionar depois. Ou adicionar Angular também:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>bundle exec rake webpacker:install:angular
bundle exec rake webpacker:install:react </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 estiver no Rails 5.1, substitua <code>bundle exec rake</code> por <code>bin/rails</code>.</p>
<p>Adicionar componentes React de verdade exige um pouco mais do que os comandos acima, mas os detalhes fogem ao escopo deste post.</p>
<p>Andy Barnov tem um <a href="https://x-team.com/blog/get-in-full-stack-shape-with-rails-5-1-webpacker-and-reactjs/"target="_blank" rel="noopener">bom post</a> sobre como seguir a convenção de packs do Rails 5.1 para adicionar seus componentes React no pipeline. Como ele recomenda: não briga com as convenções e você fica tranquilo.</p>
<p>Curiosamente, mesmo tendo usado a flag <code>--webpack-react</code> no <code>rails new</code>, ainda precisei rodar o seguinte comando para inicializar o suporte a webpack e react:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>./bin/rails webpacker:install:react</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>Mas uma vez feito isso, você tem um novo lugar para adicionar componentes React: o diretório <code>app/javascripts/packs</code>. E nas views, em vez do habitual <code>javascript_include_tag</code>, você usa o novíssimo <code>javascript_pack_tag</code>. O resto você consegue descobrir a partir do componente de exemplo <code>hello_react.jsx</code> que é gerado automaticamente.</p>
<h2>Considerações Finais<span class="hx:absolute hx:-mt-20" id="considerações-finais"></span>
    <a href="#considera%c3%a7%c3%b5es-finais" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Agora você tem muito mais opções para Javascript, já que o Rails 5.1 abraça de vez os padrões atuais.</p>
<p>Houve uma época em que ninguém sabia direito como agilizar um <a href="http://guides.rubyonrails.org/asset_pipeline.html"target="_blank" rel="noopener">Asset Pipeline</a>. Era uma bagunça compilar todos os assets num único arquivo com timestamps para cache busting — mas a gem <strong>Sprockets</strong> resolveu isso.</p>
<p>Houve uma época em que o Javascript 5 era uma bagunça monumental. Bibliotecas como o jQuery &ldquo;consertaram&rdquo; de fato a situação do DOM nos browsers, e o CoffeeScript normalizou a linguagem de um jeito que a tornava prazerosa. O ES6 veio tomar as rédeas, mas é muito injusto criticar o jQuery e o CoffeeScript: nem o ES6 nem o HTML5 existiam para resolver o problema quase 10 anos atrás. O jQuery e o CoffeeScript estavam lá, e resolveram.</p>
<p>Quando Angular, Ember e React começaram a surgir, já tínhamos uma solução boa o suficiente para sites pesados em Javascript com o <a href="https://github.com/turbolinks/turbolinks"target="_blank" rel="noopener">Turbolinks</a>. Ainda é uma ótima solução que você deveria considerar antes de adicionar uma solução React/Redux completa (e às vezes desnecessária) de uma tacada só.</p>
<p>CSS é outro assunto. A comunidade Rails também criou o Sass (que o Twitter depois invejou e copiou como Less para o framework Bootstrap).</p>
<p>É fácil olhar para o cenário atual e simplesmente repetir que CoffeeScript é ruim ou que Sprockets é ruim. Mas nos últimos 10 anos essas ferramentas eram o estado da arte — numa época em que as pessoas não tinham Yarn, Webpack, React e tudo o que surgiu nos últimos 3 anos ou assim.</p>
<p>O Rails 5.1 é um release muito bom para casar o melhor do Rails com o melhor que se estabilizou no cenário caótico do Javascript. Recomendo fortemente que qualquer projeto novo, em campo aberto, comece com o Rails 5.1 em mente.</p>
<p>O que me motivou a escrever este post foi destacar que muita gente faz perguntas que já estão respondidas no <a href="http://guides.rubyonrails.org/"target="_blank" rel="noopener">Rails Guides</a> oficial. Não só sobre como instalar e usar os vários componentes do framework, mas também como fazer upgrade e quais são as novidades de cada release. Vale a pena dar uma olhada mais cuidadosa por lá.</p>
]]></content:encoded><category>rails</category><category>javascript</category><category>frontend</category></item><item><title>Por que Falar Mal do Ruby on Rails é Simplesmente Preguiça</title><link>https://akitaonrails.github.io/2017/08/03/por-que-falar-mal-do-ruby-on-rails-e-simplesmente-preguica/</link><guid isPermaLink="true">https://akitaonrails.github.io/2017/08/03/por-que-falar-mal-do-ruby-on-rails-e-simplesmente-preguica/</guid><pubDate>Thu, 03 Aug 2017 18:01:00 -0300</pubDate><description>&lt;p&gt;É inevitável hoje em dia: de tempos em tempos vai aparecer um artigo proclamando a morte do Ruby on Rails. É o click-bait mais fácil do mundo, como &lt;a href="https://thenextweb.com/dd/2017/07/26/ruby-rails-major-coding-bootcamp-ditches-due-waning-interest/#.tnw_kWT917pu"target="_blank" rel="noopener"&gt;esse aqui do TNW&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Você pode responder &lt;em&gt;&amp;ldquo;mais um fanboy de Ruby.&amp;rdquo;&lt;/em&gt; Tudo bem, é um argumento justo de fazer, mas terrível de sustentar, porque é o clássico e raso &lt;a href="https://en.wikipedia.org/wiki/Ad_hominem"target="_blank" rel="noopener"&gt;argumentum ad hominem&lt;/a&gt;. E já que estamos no assunto de falácias, o artigo click-bait acima está errado exatamente porque cai numa &lt;a href="https://en.wikipedia.org/wiki/Post_hoc_ergo_propter_hoc"target="_blank" rel="noopener"&gt;Post hoc ergo propter hoc&lt;/a&gt; descarada, temperada com um bom tanto de &lt;a href="https://en.wikipedia.org/wiki/Confirmation_bias"target="_blank" rel="noopener"&gt;viés de confirmação&lt;/a&gt; — do qual todos nós somos culpados o tempo todo.&lt;/p&gt;</description><content:encoded><![CDATA[<p>É inevitável hoje em dia: de tempos em tempos vai aparecer um artigo proclamando a morte do Ruby on Rails. É o click-bait mais fácil do mundo, como <a href="https://thenextweb.com/dd/2017/07/26/ruby-rails-major-coding-bootcamp-ditches-due-waning-interest/#.tnw_kWT917pu"target="_blank" rel="noopener">esse aqui do TNW</a>.</p>
<p>Você pode responder <em>&ldquo;mais um fanboy de Ruby.&rdquo;</em> Tudo bem, é um argumento justo de fazer, mas terrível de sustentar, porque é o clássico e raso <a href="https://en.wikipedia.org/wiki/Ad_hominem"target="_blank" rel="noopener">argumentum ad hominem</a>. E já que estamos no assunto de falácias, o artigo click-bait acima está errado exatamente porque cai numa <a href="https://en.wikipedia.org/wiki/Post_hoc_ergo_propter_hoc"target="_blank" rel="noopener">Post hoc ergo propter hoc</a> descarada, temperada com um bom tanto de <a href="https://en.wikipedia.org/wiki/Confirmation_bias"target="_blank" rel="noopener">viés de confirmação</a> — do qual todos nós somos culpados o tempo todo.</p>
<p>Não estou dizendo que o autor escreveu essas falácias de propósito. Infelizmente, é fácil demais cair nelas. Especialmente quando todo mundo tem um desejo intrínseco de confirmar suas próprias crenças. Mesmo tentando ser cuidadoso, eu mesmo acabo fazendo isso.</p>
<p>Sobre o assunto específico de Rails, comece por aqui:</p>
<ul>
<li><a href="http://www.akitaonrails.com/2016/05/23/rails-venceu-o-elefante-na-sala"target="_blank" rel="noopener">Rails has won: The Elephant in the Room</a></li>
<li><a href="http://www.akitaonrails.com/2016/08/19/a-comunidade-ruby-e-a-reputacao"target="_blank" rel="noopener">The Ruby Community and Reputation</a></li>
<li><a href="http://www.akitaonrails.com/2017/05/14/guilda-de-programadores-religiao-e-esportes"target="_blank" rel="noopener">Programmers Guild - Religion and Sports</a></li>
</ul>
<p>Como regra geral, qualquer pessoa anunciando a morte futura de qualquer coisa está provavelmente errada. É divertido fazer apostas, mas artigos em sites de alto perfil como o TNW deveriam ter mais cuidado, já que muitos iniciantes acabam acreditando piamente nessas opiniões.</p>
<p>É por isso que eu sempre digo às pessoas: parem de confiar em opiniões. Todo mundo tem uma opinião, e é exatamente por isso que elas são tão baratas. Aí as pessoas artificialmente elevam &ldquo;celebridades&rdquo; ou &ldquo;gurus&rdquo; ao status de quem tem opiniões de maior valor. Eu não compro isso, e você também não deveria.</p>
<p><a href="https://en.wikipedia.org/wiki/Black_swan_theory"target="_blank" rel="noopener">Cisnes Negros</a> acontecem. Mas tentar prevê-los é exercício de futilidade. Você pode ter sorte uma ou duas vezes. Mas tentativas sistemáticas são inúteis. Por isso sou um grande defensor da <a href="https://en.wikipedia.org/wiki/Antifragility"target="_blank" rel="noopener">Antifrabilidade</a> — e vou te dar as regras gerais para entender o que fazer.</p>
<p>Estou dizendo que Ruby on Rails vai durar para sempre? Não seja ridículo, claro que não. Nada dura para sempre. Mais de 2 décadas trabalhando ativamente na área me ensinaram que apostar tudo num único cavalo é um erro. Vale no mercado financeiro, vale no mercado de tecnologia.</p>
<p>Em vez de ações ou títulos, o seu ativo é o seu conhecimento e a sua prática. Diversificação de portfólio costuma ser a aposta mais inteligente.</p>
<p>Você colocou todo o seu dinheiro em Bitcoins. Agora vai fazer de tudo para confirmar esse viés, assinando todo noticiário e opinião que confirma sua aposta, e vai atacar qualquer artigo que apresente um argumento contrário. Estou dizendo para não investir em Bitcoins? De forma alguma. Só não seja burro a ponto de colocar tudo num único ativo.</p>
<p>Você gastou todo o seu tempo aprendendo <a href="https://en.wikipedia.org/wiki/Google_Web_Toolkit"target="_blank" rel="noopener">Google Web Toolkit</a> em 2006. Boa sorte encontrando artigo que confirme esse viés agora. Esse é um daqueles casos post-factum em que só nos resta concordar consensualmente sobre a morte da coisa.</p>
<blockquote>
  <p>O trabalho de um programador profissional de verdade não é dominar uma única ferramenta ou mesmo um único stack tecnológico. Seu trabalho é ser o melhor possível em aprender e praticar na área de tecnologia.</p>

</blockquote>
<p>Dito isso, e mesmo que a estrutura do artigo seja imprecisa, o sentimento não está de todo errado. Ruby on Rails não representa mais o que representava há 10 anos. Mas antes de entrar em pânico, vamos explorar essa história juntos.</p>
<h3>2003 - Um Novo Renascimento<span class="hx:absolute hx:-mt-20" id="2003---um-novo-renascimento"></span>
    <a href="#2003---um-novo-renascimento" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Num exercício retrospectivo, é preciso reconhecer que uma série de eventos ajudou a desdobrar o fenômeno Rails entre 2003 e 2011. Não foi por design. Não foi planejado. Poderia ter sido outra coisa qualquer.</p>
<ul>
<li>Estávamos nos recuperando do crash da bolha dot-com de 2001.</li>
<li>Redes sociais começavam a emergir: Orkut, MySpace, Friendster. Facebook ainda nem estava no radar.</li>
<li>Blogs ganhavam força e competiam com os veículos de notícia tradicionais.</li>
<li>Os grandes fornecedores focavam exclusivamente no mercado Enterprise e o resto era considerado amadorismo.</li>
<li>No mainstream, ou você fazia .NET ou Java no Enterprise, ou fazia hotsites em Flash e PHP para agências.</li>
<li>Até ferramentas open source como o Eclipse eram usadas principalmente dentro de umbrellas enterpriseiras como WebSphere ou WebLogic.</li>
<li>O Manifesto Ágil tinha acabado de ser publicado em 2001 e o mercado mainstream não ligou muito. PMI e RUP eram a ordem do dia.</li>
</ul>
<p>Então chegamos a 2004.</p>
<ul>
<li>O Google lançou o Gmail com grande repercussão. HTML dinâmico e Ajax demonstraram como era possível criar aplicações de nível Desktop na Web. É aí também que o Javascript ganha um novo papel, além de mini jogos burros ou anúncios.</li>
<li>O Facebook explodiu e tivemos os primeiros vislumbres do que redes sociais granulares pareciam.</li>
<li>A Apple consolidou seu retorno como gigante tech relevante com a ressurreição bem-sucedida do OS X, provando que Unix no desktop do consumidor era viável.</li>
<li>A Microsoft entrou no século XXI depois de um pesadíssimo processo antitruste que quase a destruiu. Bill Gates saiu, Steve Ballmer assumiu as rédeas e eles viveram sua pior década.</li>
<li>Anos de lock-in de fornecedor finalmente começaram a cansar os desenvolvedores; ninguém de verdade queria mais usar IBM, BEA, Oracle — e a promessa da revolução open source iniciada nos anos 90 finalmente começava a se solidificar.</li>
<li>O crash dot-com deixou uma depressão no mercado tech, mas também deixou para trás uma infraestrutura que finalmente estava sendo bem aproveitada.</li>
</ul>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/654/s-l225.jpg" alt="How Bill Blew it"  loading="lazy" /></p>
<p>O Ruby on Rails aparece como uma pequena curiosidade, numa conferência tech obscura no Brasil, criado por uma agência pequena mas promissora chamada 37signals. Sinalizando algo que a maioria dos desenvolvedores nunca tinha visto antes: uma linguagem flexível — diferente de Java, C# ou PHP — que podia ser usada para implementar aplicações web estilo Gmail com a estética da Apple, se afastando de tudo que Microsoft e IBM representavam, e incorporando técnicas ágeis como Test-Driven Development de forma prática e já inclusa.</p>
<p><a href="https://www.youtube.com/watch?v=Gzj723LkRJY"target="_blank" rel="noopener"><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/655/75879992_fa890a9a07.jpg" alt="15 minute blog"  loading="lazy" /></a></p>
<p>O que mais você poderia pedir em 2005?</p>
<p>Então chega <strong>2006</strong>: a Amazon AWS lança S3 e EC2 e o termo &ldquo;Elastic Cloud&rdquo; vira o novo hype.</p>
<p>Até aquele ponto, &ldquo;desenvolvimento web&rdquo; significava ou departamentos de TI caros e burocráticos em grandes corporações, ou hospedagem compartilhada barata mas fraca em serviços como Dreamhost, 1&amp;1, Media Temple.</p>
<p>A Elastic Cloud tinha a chave para superar o Enterprise e tornar possível que desenvolvedores independentes construíssem a tão sonhada &ldquo;escala web&rdquo;.</p>
<p>O Ruby on Rails estava exatamente onde precisava estar: começando a ser adotado por uma nova safra de startups do Vale do Silício tentando fazer a diferença nessa nova era Ajax + Cloud. Lembre que o Rails foi lançado com RJS, uma forma de escrever Javascript via Ruby usando Prototype e Scriptaculous. O jQuery nem era mainstream ainda.</p>
<p>O Twitter aparece. O Groupon aparece. A Engine Yard aparece. E muito mais vem na sequência.</p>
<p>Tudo isso acontecendo numa velocidade absurda — e estou apenas arranhando a superfície aqui.</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/4XpnKHJAok8"
    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><strong>2007</strong> chega e pelo menos 3 coisas grandes acontecem:</p>
<ul>
<li>Nasce outra startup baseada em Rails: Heroku. Se o EC2 da AWS cunhou o termo &ldquo;Infrastructure as a Service&rdquo; (IaaS), o Heroku definiu o que &ldquo;Platform as a Service&rdquo; (PaaS) significa. Conseguiu aproveitar o EC2 e tornar seu poder acessível.</li>
<li>Ao mesmo tempo, desenvolvedores tinham ficado frustrados com sistemas de controle de versão como CVS, e as opções dos grandes fornecedores como Rational ClearCase, Microsoft SourceSafe eram simplesmente terríveis. O Subversion tinha surgido, mas suas muitas limitações seriam sua perdição. Linus Torvalds decide resolver o problema e lança o Git. Desenvolvedores mainstream finalmente aprendem termos básicos de ciência da computação como DAGs (&ldquo;Directed Acyclic Graphs&rdquo;).</li>
<li>O iPhone é lançado e uma nova revolução mobile começa. Players dominantes como Nokia e Blackberry foram abalados até o núcleo. Desenvolvedores queriam uma fatia disso. O Mobile Safari vira destaque da noite para o dia. E o WebKit sobe ao estrelato, eclipsando veteranos como Firefox/Gecko e Opera.</li>
</ul>
<p><strong>2008</strong>: o nascimento do GitHub e o alvorecer do DevOps.</p>
<p>O Git era ótimo, mas baixo nível demais. Vários outros concorrentes tentaram se tornar o novo padrão — Mercurial, Bazaar. Mas o nascimento do GitHub mudou tudo isso. E era mais uma startup baseada em Rails nas manchetes.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/653/Theres-An-App-for-That-Tanglewood-Moms.jpg" alt="There’s an App for that"  loading="lazy" /></p>
<p>Mas a lua de mel mostrava sinais de esgotamento: o proeminente desenvolvedor Ruby Zed Shaw <a href="https://techcrunch.com/2008/01/01/zed-shaw-puts-the-smack-down-on-the-rails-community/"target="_blank" rel="noopener">saiu da comunidade em alto e bom som</a> em janeiro.</p>
<p>Nesse mesmo ano tivemos a guerra fria entre o framework Merb da Engine Yard e o Rails da 37signals, que resultou no <a href="http://yehudakatz.com/2008/12/23/rails-and-merb-merge/"target="_blank" rel="noopener">controverso merge Rails-Merb</a>, abrindo mais uma rachadura na comunidade.</p>
<p>Mas ainda tínhamos muito trabalho pela frente, então seguimos em frente.</p>
<p><strong>2009</strong>: &ldquo;tem um app pra isso!&rdquo;</p>
<p>A Apple finalmente lança um SDK nativo de verdade. Desenvolvedores começam a construir uma nova geração de apps de produtividade e jogos. O WebKit cresce em participação de mercado. A Apple solidifica o conceito de estética no desenvolvimento de software e o Ruby on Rails está no seu auge nesse ponto.</p>
<p>Começa a era NoSQL. Quando armazenamento e busca de dados realmente viram um gargalo, rapidamente aprendemos que bancos de dados enterpriseiros tradicionais não são &ldquo;web scale&rdquo; o suficiente. O MongoDB lidera o bando. O desenvolvimento web entra numa era de frenesi com NoSQL.</p>
<p>Em paralelo, o Twitter, que foi o cartão-postal da geração Rails, abre uma rachadura na comunidade Ruby ao declarar que começou a usar mais uma linguagem obscura: <a href="http://www.artima.com/scalazine/articles/twitter_on_scala.html"target="_blank" rel="noopener">Scala</a>!</p>
<p>Isso dá início à era da obsessão sem fim com &ldquo;concorrência&rdquo; — mais um conceito escorregadio sobre o qual muita gente fala bonito mas poucos de fato entendem o que significa. É o início da janela de oportunidade para Node.js, Scala, Erlang, Clojure, Go.</p>
<p>A lua de mel acabou.</p>
<p><strong>2010</strong>:</p>
<ul>
<li>A morte da Sun e a Oracle assumindo o Java. Se as coisas já não estavam boas para o Java, isso foi um prego pesado no caixão.</li>
<li>A Apple declara guerra aberta ao Flash. Mais um grande empurrão para Javascript e apps web front-end pesados.</li>
</ul>
<p><a href="https://www.apple.com/hotnews/thoughts-on-flash/"target="_blank" rel="noopener"><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/656/apple-no-flash-250x376.jpg" alt="Thoughts on Flash"  loading="lazy" /></a></p>
<p>Na minha opinião pessoal, esse é o ano em que o Rails atingiu seu pico de influência. Era o stack obrigatório para desenvolvedores em Macs que queriam criar sua nova Lean Startup do zero.</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/icMA9rYNmgU"
    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>

<h3>2011: Uma Nova Era<span class="hx:absolute hx:-mt-20" id="2011-uma-nova-era"></span>
    <a href="#2011-uma-nova-era" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Em cerca de 6 anos, o mercado de desenvolvimento de software fez mudanças titânicas:</p>
<ul>
<li>O desenvolvimento distribuído derivado de anos de workflow Open Source se torna o novo padrão. Git e GitHub foram peças-chave. Os grandes fornecedores precisaram correr atrás com suas alternativas enterprise mal cheirosas como o TFS.</li>
<li>O Ágil passa a ser levado a sério por uma nova safra de startups tech que evangelizavam desenvolvimento de software de qualidade. Os testes deixam de ser coisas improdutivas e chatas para se tornarem técnica central e inestimável. Ferramentas como Hudson/Jenkins combinadas com o workflow do Git pavimentam o caminho para o que a integração contínua e depois o deploy contínuo se tornariam.</li>
<li>A AWS e o exemplo do Heroku pavimentam o caminho para tornar a TI antiga obsoleta e criar uma nova geração de infraestrutura como software, o &ldquo;DevOps&rdquo;. Surge a urgência de fazer deploy em escala web com zero atrito. O time-to-market vira fator-chave: release cedo, release com frequência.</li>
<li>A Apple lança a AppStore e ajuda a impulsionar o mercado de Software como Serviço, com apps precisando de internet ao vivo para funcionar. A &ldquo;Cloud&rdquo; chega às massas.</li>
<li>Ao fazer com que todo ser humano no planeta carregasse um dispositivo poderoso e sempre conectado — um smartphone —, nunca estivemos tão perto do futuro orwelliano de 1984. Tudo que uma pessoa faz vira ponto de dados. Big Data se torna uma coisa. Ciência de Dados se torna uma profissão viável no mainstream. Disciplinas relacionadas à IA se tornam necessidade para raciocinar sobre tanto dado.</li>
</ul>
<p>O Android se aproxima do iPhone. A Microsoft ressurge das cinzas. Em outubro, Steve Jobs morre, marcando o fim da Era de forte influência da Apple.</p>
<p>Mas por volta de 2011 outra coisa acontece: nós vencemos.</p>
<p>Desenvolvedores independentes superaram o status quo dos desenvolvedores enterprise. Houve uma virada de poder. As convenções para o campo do desenvolvimento de software não vêm mais dos velhos fornecedores de ferramentas. Não estamos mais ouvindo fabricantes de IDEs como Microsoft, BEA, Oracle. Estamos ouvindo os vencedores dessa geração de startups: Apple, Google, Facebook.</p>
<p>O conceito de <em>&ldquo;uma linguagem para governar todas&rdquo;</em> foi destroçado no campo web pelo Ruby on Rails e no espaço de aplicações nativas pela Apple. Duas linguagens muito incomuns e estranhas, Ruby e Objective-C, atingiram o mainstream. Elas provaram que desenvolvedores podem aprender e dominar linguagens exóticas transformando-as em produtos lucrativos.</p>
<p>Com essas bases estabelecidas, uma nova <strong>corrida do ouro</strong> emerge: linguagens novas para todo lado, livres da noção de que para ter sucesso era preciso ficar em Java, C#, C++ — as pessoas finalmente começaram a olhar para outros lados para se diferenciar, e assim uma nova geração de linguagens exóticas chega: Scala, Haskell, Ocaml, F#, Erlang, Clojure, Rust, Elixir, etc. O céu é o limite.</p>
<p>Ao matar o Flash e acelerar HTML 5, ES6, e com toda a controvérsia em torno de concorrência, o Javascript ganhou nova vida e uma adoção rápida como nada antes.</p>
<h3>Epílogo<span class="hx:absolute hx:-mt-20" id="epílogo"></span>
    <a href="#ep%c3%adlogo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O Ruby on Rails está morto? Não.</p>
<p>O Ruby on Rails ainda é o líder do mercado? Não.</p>
<p>Qual é o legado da Geração Ruby on Rails? Ele teve um papel importante em definir o que hoje consideramos o novo status quo do desenvolvimento web: Git, CI, CD, Cloud, Métricas. Trouxe à mesa, com força, o pensamento estético da Apple.</p>
<p>Essa nova safra de startups tech teve um efeito colateral sério, porém: colocou novos engenheiros nos holofotes, elevando-os ao nível de rock stars e transformando-os em pretendentes a Steve Jobs. Eles simplesmente começaram a tomar tudo que conquistamos como garantido. Receita para o desastre.</p>
<p>E embora a influência dos grandes fornecedores tenha ficado rala comparada aos anos 90, eles não estão mortos. Depois de 2011 entramos numa nova era de caos, uma era de desenvolvedores insatisfeitos. Onde a cada dois anos surge uma nova derivação e temos aquela sensação insatisfatória de precisar reescrever tudo o tempo todo.</p>
<p>O pool do Ruby on Rails se diluiu em linguagens de nicho menores como o próprio Javascript, Go, Elixir, Scala.</p>
<p>O mainstream Java/Microsoft está ganhando nova tração. A estética também se diluiu. A engenharia cuidadosa está se diluindo. A ubiquidade das redes sociais e o caminho fácil do deploy rápido na nuvem estão tornando as pessoas cada vez menos pacientes. Artesanato requer paciência. As pessoas anseiam por Likes, uma nova geração de métricas de vaidade surgiu.</p>
<p>Temos agora um número maior de desenvolvedores independentes não-enterprise com pouca ou nenhuma paciência, com um senso exagerado de confiança, síndrome de <a href="https://en.wikipedia.org/wiki/Not_invented_here"target="_blank" rel="noopener">not-invented-here</a>, tomando tudo como garantido e se importando cada vez menos com valor de verdade.</p>
<p>Concluindo: a morte do Ruby on Rails está sim muito exagerada, ele vai continuar por muitos anos ainda — especialmente se o Ruby Core Team conseguir entregar de fato a <a href="https://blog.heroku.com/ruby-3-by-3"target="_blank" rel="noopener">meta 3x3</a>. As pessoas adoram compartilhar artigos de doom enquanto relatórios otimistas e mais realistas como <a href="https://reinteractive.com/posts/320-why-ruby-on-rails-is-still-the-best-choice"target="_blank" rel="noopener">Por que Ruby on Rails ainda é a melhor escolha?</a> são amplamente ignorados. É assim que funciona a vaidade das redes sociais.</p>
<p>Mas não vai ser o que foi, porque os tempos mudaram. Hoje podemos viver pelo workflow &ldquo;On Rails&rdquo; sem o Ruby on Rails. E ele não pode continuar quebrando o status quo, porque ele se tornou o status quo.</p>
<p>Fico curioso sobre para onde isso vai nos levar. Tenho algumas pistas, mas como disse, não estou no negócio de publicar manchetes click-bait.</p>
]]></content:encoded><category>rails</category><category>desenvolvimento-web</category><category>engenharia-de-software</category></item><item><title>A Solução de Upload Nativa do Rails 5.2: ActiveStorage</title><link>https://akitaonrails.github.io/2017/07/07/a-solucao-de-upload-nativa-do-rails-5-2-activestorage/</link><guid isPermaLink="true">https://akitaonrails.github.io/2017/07/07/a-solucao-de-upload-nativa-do-rails-5-2-activestorage/</guid><pubDate>Fri, 07 Jul 2017 14:40:00 -0300</pubDate><description>&lt;p&gt;&lt;strong&gt;Atualização 07/11/2017&lt;/strong&gt;: Logo depois do nosso brainstorm no Twitter, DHH e @gauravtiwari se mobilizaram e iniciaram um &lt;a href="https://github.com/rails/activestorage/pull/35"target="_blank" rel="noopener"&gt;novo branch&lt;/a&gt; para adicionar suporte a Direct Upload para a nuvem imediatamente. Claro, ainda é trabalho em andamento, mas vai ser ótimo ter esse suporte nativo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Atualização 07/20/2017:&lt;/strong&gt; Preciso esclarecer alguns pontos. Primeiro, estou recomendando a solução proprietária do Cloudinary, mas há ressalvas. Você está trocando facilidade de uso por vendor lock-in. E só recomendo para fotos e imagens. Se precisar fazer upload de arquivos gigantes (vídeos, ou binários arbitrários como tarballs pesados), você precisa pesquisar mais e talvez construir uma solução customizada com Shrine ou similar. Outro ponto que faço é que se você usa Heroku, sua única opção é Direct Upload para serviços de nuvem como o AWS S3. Já entreguei vários apps que bateram no &lt;a href="https://devcenter.heroku.com/articles/request-timeout#uploading-large-files"target="_blank" rel="noopener"&gt;H12 Routing Timeout&lt;/a&gt; — está na documentação oficial. Mas há pouco deployei um Rails 5.1 simples com Carrierwave padrão e um formulário HTML multipart. Consegui fazer upload de um vídeo de mais de 2GB e ele passou pela camada de roteamento do Heroku. Parece que o roteador absorve o arquivo, deixa o upload terminar e só então passa para a aplicação Rails. Agora, se você bloqueia a requisição (por exemplo, fazendo upload do Rails para o S3 ou qualquer operação demorada), a camada de roteamento vai dar H12/timeout após 30 segundos. Então, se você permitir uploads de arquivos grandes, vai acabar tendo desconexões aleatórias. Mesmo que você consiga receber o arquivo, não pode mantê-lo no sistema de arquivos (que é volátil num Dyno do Heroku), então ainda precisa enviá-lo para algum lugar — e essa é a parte que pode dar timeout. Portanto, o Direct Upload continua sendo a abordagem correta no Heroku.&lt;/p&gt;</description><content:encoded><![CDATA[<p><strong>Atualização 07/11/2017</strong>: Logo depois do nosso brainstorm no Twitter, DHH e @gauravtiwari se mobilizaram e iniciaram um <a href="https://github.com/rails/activestorage/pull/35"target="_blank" rel="noopener">novo branch</a> para adicionar suporte a Direct Upload para a nuvem imediatamente. Claro, ainda é trabalho em andamento, mas vai ser ótimo ter esse suporte nativo.</p>
<p><strong>Atualização 07/20/2017:</strong> Preciso esclarecer alguns pontos. Primeiro, estou recomendando a solução proprietária do Cloudinary, mas há ressalvas. Você está trocando facilidade de uso por vendor lock-in. E só recomendo para fotos e imagens. Se precisar fazer upload de arquivos gigantes (vídeos, ou binários arbitrários como tarballs pesados), você precisa pesquisar mais e talvez construir uma solução customizada com Shrine ou similar. Outro ponto que faço é que se você usa Heroku, sua única opção é Direct Upload para serviços de nuvem como o AWS S3. Já entreguei vários apps que bateram no <a href="https://devcenter.heroku.com/articles/request-timeout#uploading-large-files"target="_blank" rel="noopener">H12 Routing Timeout</a> — está na documentação oficial. Mas há pouco deployei um Rails 5.1 simples com Carrierwave padrão e um formulário HTML multipart. Consegui fazer upload de um vídeo de mais de 2GB e ele passou pela camada de roteamento do Heroku. Parece que o roteador absorve o arquivo, deixa o upload terminar e só então passa para a aplicação Rails. Agora, se você bloqueia a requisição (por exemplo, fazendo upload do Rails para o S3 ou qualquer operação demorada), a camada de roteamento vai dar H12/timeout após 30 segundos. Então, se você permitir uploads de arquivos grandes, vai acabar tendo desconexões aleatórias. Mesmo que você consiga receber o arquivo, não pode mantê-lo no sistema de arquivos (que é volátil num Dyno do Heroku), então ainda precisa enviá-lo para algum lugar — e essa é a parte que pode dar timeout. Portanto, o Direct Upload continua sendo a abordagem correta no Heroku.</p>
<p>O DHH acabou de anunciar uma funcionalidade nova para o Rails 5.2 que está por vir. É o <a href="https://github.com/rails/activestorage"target="_blank" rel="noopener">ActiveStorage</a>.</p>
<p>Ele deve se tornar a solução padrão para suporte a uploads de arquivos, basicamente substituindo o Paperclip, o Carrierwave, e algumas funcionalidades do Dragonfly e do Shrine (que fazem bem mais do que isso).</p>
<p>Escrevo este post não para apresentar a solução, mas para esclarecer algumas críticas que fiz no Twitter. Acontece que o Twitter é uma plataforma péssima para discussões mais aprofundadas, daí este texto.</p>
<p>A thread original no Twitter pode ser encontrada <a href="https://twitter.com/AkitaOnRails/status/882998977754537984"target="_blank" rel="noopener">aqui</a>.</p>
<p>O processo tem pelo menos 3 pontos importantes a considerar:</p>
<ol>
<li>O upload do arquivo, do usuário final até o seu controller Rails.</li>
<li>Uma etapa opcional de transformação (que você deveria fazer, para redimensionar imagens e servi-las para diferentes dispositivos e resoluções de tela).</li>
<li>Servir o blob do arquivo de volta ao usuário final como uma URL de imagem, por exemplo.</li>
</ol>
<h3>A Etapa de Upload<span class="hx:absolute hx:-mt-20" id="a-etapa-de-upload"></span>
    <a href="#a-etapa-de-upload" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A maioria das soluções simples de upload — como o ActiveStorage e as antigas como o Paperclip original e o Carrierwave numa instalação padrão — basicamente configuram o formulário HTML como multipart e adicionam um campo de arquivo HTML simples. Isso vai enviar o formulário diretamente para uma action do controller Rails, que receberá o arquivo no hash <code>params</code> e você pode lidar com o blob binário a partir daí.</p>
<p>Numa implantação ingênua, isso vai <strong>bloquear</strong> o MRI durante todo o upload. Se o arquivo for muito grande, pode bloquear qualquer outra requisição entrante pela duração desse upload. (Tecnicamente, como o Rails suporta threads MRI e threads MRI são teoricamente não-bloqueantes para operações de IO, não deveria ser tão grave quanto parece.)</p>
<p>Felizmente, acredito que ninguém em sã consciência expõe um processo MRI diretamente para a internet. Normalmente ficamos atrás de um <a href="https://www.digitalocean.com/community/tutorials/how-to-deploy-a-rails-app-with-puma-and-nginx-on-ubuntu-14-04"target="_blank" rel="noopener">proxy reverso</a>, como Haproxy, NGINX, Apache HTTPD ou algo similar.</p>
<p>E geralmente esses proxies reversos são os que recebem o upload e só fazem proxy da requisição quando o upload está concluído. Assim, a aplicação MRI/Rails pode continuar respondendo às outras requisições enquanto isso.</p>
<p>Então, se você tem uma infraestrutura customizada, está bem.</p>
<p>Se você usa algo como Heroku, está basicamente ferrado. A camada de roteamento do Heroku tem um timeout fixo de 30 segundos. Acho isso uma coisa boa, porque você não deveria ter requisições que demoram tanto para processar. Ora, você não deveria ter uma requisição respondendo em 1 segundo, quanto mais 30 segundos.</p>
<p>Mas upload de arquivos é a exceção. Um arquivo grande demora para transferir pela internet. E aí o timeout do Heroku entra em ação e interrompe a transmissão. O usuário tenta de novo, e se você tiver gente suficiente acessando, começa a encher a fila HTTP até ter timeouts em cascata.</p>
<p>É por isso que a única solução disponível para Heroku é fazer &ldquo;Direct Upload&rdquo; para um serviço de nuvem como o AWS S3. Você pode adicionar o add-on <a href="https://github.com/dwilkie/carrierwave_direct"target="_blank" rel="noopener">Carrierwave Direct</a>, ou usar uma solução completa de terceiros como o <a href="http://www.akitaonrails.com/2016/07/28/atualizando-meus-posts-antigos-sobre-uploads"target="_blank" rel="noopener">Cloudinary</a>, com sua biblioteca cliente Attachinary para facilitar as coisas. E pronto!</p>
<p>O Active Storage, como está agora, vai funcionar bem para qualquer deploy customizado razoável (NGINX + Rails/Puma) <del>mas não vai funcionar de jeito nenhum no Heroku</del> e quando o <a href="https://github.com/rails/activestorage/pull/35"target="_blank" rel="noopener">novo branch</a> estiver pronto, será uma boa opção para usar no Heroku também.</p>
<h3>A Etapa de Transformação<span class="hx:absolute hx:-mt-20" id="a-etapa-de-transformação"></span>
    <a href="#a-etapa-de-transforma%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Isso pode ser feito logo após o upload ou logo antes de servir o arquivo de volta ao usuário.</p>
<p>A primeira opção pode ser feita de forma síncrona ou assíncrona.</p>
<p>Síncrona é &ldquo;ruim&rdquo; (quer dizer, na própria action do controller, porque essa etapa é CPU-intensiva e leva tempo). É basicamente transformar a imagem (usando algo como Rmagick ou MiniMagick) em outras versões de tamanhos diferentes (thumbnail, versão mobile, versão high-dpi, etc.) e armazenar os caminhos para as diferentes versões no storage.</p>
<p>Assíncrona é delegar essa transformação custosa para o ActiveJob, para que um worker do Sidekiq pegue a tarefa mais tarde e faça o processamento. Enquanto isso, você pode servir um placeholder se a versão específica ainda não estiver pronta.</p>
<p>Uma ressalva é que se você tem cloud storage e transformação assíncrona em jobs, vai ter muito tráfego, porque você vai gastar tempo fazendo upload para o cloud storage, depois o job vai ter que baixar de lá, fazer a transformação e fazer novos uploads.</p>
<p>A outra solução é não fazer nenhum processamento e delegar o processamento para ser feito sob demanda. É isso que o <a href="http://cloudinary.com/documentation/image_transformations"target="_blank" rel="noopener">Cloudinary/Attachinary</a> de terceiros faz — você pode fazer transformações customizadas usando parâmetros URI. Ele faz a transformação uma vez e faz cache dos resultados para uso futuro. Um exemplo de URL de transformação de imagem do Cloudinary:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>http://res.cloudinary.com/demo/image/upload/w_400,h_400,c_crop,g_face,r_max/w_200/lady.jpg</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 também é o que você obtém se implementar o <a href="https://github.com/refile/refile"target="_blank" rel="noopener">Refile</a> ou o <a href="https://github.com/janko-m/shrine"target="_blank" rel="noopener">Shrine</a>. Eles adicionam um endpoint Rack à sua aplicação Rails que vai buscar o binário da imagem, executar a transformação de acordo com os parâmetros recebidos na URI, fazer cache da transformação e então enviar o binário.</p>
<h3>A Etapa de Servir os Arquivos<span class="hx:absolute hx:-mt-20" id="a-etapa-de-servir-os-arquivos"></span>
    <a href="#a-etapa-de-servir-os-arquivos" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Aqui é fazer sua aplicação Rails servir o blob armazenado. O arquivo pode ser armazenado localmente (ou em um mount NFS remoto) ou na internet a partir de qualquer cloud storage como o AWS S3 (caso em que você simplesmente linka diretamente para o endpoint HTTP deles).</p>
<p>Quando sua aplicação Rails serve um arquivo local, pode apenas enviar um Header especial para o proxy reverso (NGINX ou Apache — <code>X-SendFile</code> ou <code>X-Accel-Redirect</code>, que é a diferença entre <code>send_file</code> e <code>send_data</code> no <code>ActionController::DataStreaming</code>, por sinal) e eles vão servir o arquivo diretamente, evitando bloquear o MRI durante a transferência.</p>
<p>Se está em cloud storage, é ainda mais fácil porque você simplesmente imprime a URL do arquivo diretamente no template HTML e não há processamento nenhum.</p>
<p>Haverá algum processamento se você fizer o Rails ler o blob e fazer streaming diretamente (às vezes isso é necessário porque você tem acesso restrito aos arquivos e não quer arriscar usar uma URL aleatória para o arquivo).</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 DHH está certo: o Basecamp serve muitos arquivos e o ActiveStorage (assim como o Paperclip e o Carrierwave) funciona bem, desde que você tenha um proxy reverso NGINX adequado na frente e tenha adicionado um CDN para fazer cache dos arquivos.</p>
<p>Se você não quer ter que gerenciar seu próprio storage, use um Cloud Storage (AWS S3, Google Cloud, Azure, etc.). O ActiveStorage ou outras soluções vão receber o arquivo no nível do controller Rails e você DEVE usar o ActiveJob para fazer POST do blob para o serviço de nuvem em background — sem bloquear sua aplicação no processo. Mas o trade-off é que se você adicionar transformação assíncrona em jobs, vai acabar tendo que buscar a imagem original do cloud storage para fazer as transformações.</p>
<p>Você deveria fazer transformações nas suas imagens para enviar a imagem no tamanho ideal de volta aos seus usuários. Novamente, se fizer isso na sua aplicação, considere um worker do ActiveJob ou as soluções Rack de transformação em tempo real com cache disponíveis no Refile ou Shrine. Pelo menos como está agora, o ActiveStorage não fornece uma solução para as transformações de imagem.</p>
<p><a href="http://www.akitaonrails.com/2016/07/28/atualizando-meus-posts-antigos-sobre-uploads"target="_blank" rel="noopener">Minha recomendação</a> dos posts anteriores sobre o assunto continua valendo: se é sua primeira vez ou seu negócio está começando, não se estresse. Use uma solução completa como Cloudinary/Attachinary. Ela vai cuidar de tudo da forma mais otimizada possível.</p>
<p>Mas isso NÃO é uma recomendação definitiva. Se você tem deploys customizados e conhece seus requisitos e restrições, uma solução como ActiveStorage, Carrierwave vanilla, etc. funciona bem. Sempre há trade-offs, e ter muitas partes móveis sempre soma complexidade. A recomendação do Cloudinary é apenas para que você possa começar com o mínimo de partes móveis possível e depois migrar para cenários mais complexos se precisar.</p>
<p>Lidar com todas as combinações possíveis de gerenciamento de upload de arquivos não é tarefa simples. E você provavelmente tem preocupações mais urgentes na sua lógica de negócios do que lidar com arquivos.</p>
<p>Ah, e repetindo o mesmo de sempre: use um CDN, pelo amor de Deus! Independentemente da solução, você sempre deveria <a href="https://devcenter.heroku.com/articles/using-amazon-cloudfront-cdn"target="_blank" rel="noopener">adicionar um CDN</a> para servir seus assets (javascripts, stylesheets, imagens, etc.).</p>
]]></content:encoded><category>rails</category><category>backend</category><category>cloud</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>Implementação em Ruby on Rails de um sistema de Ranking/Popularidade (do jeito certo)</title><link>https://akitaonrails.github.io/2016/10/31/implementacao-ruby-on-rails-sistema-ranking-popularidade-correto/</link><guid isPermaLink="true">https://akitaonrails.github.io/2016/10/31/implementacao-ruby-on-rails-sistema-ranking-popularidade-correto/</guid><pubDate>Mon, 31 Oct 2016 16:51:00 -0200</pubDate><description>&lt;p&gt;Estava lendo um post publicado recentemente, intitulado &lt;a href="http://naturaily.com/blog/post/ruby-on-rails-implementation-of-a-ranking-system-using-postgresql-window-functions"target="_blank" rel="noopener"&gt;&amp;ldquo;Ruby on Rails implementation of a ranking system using PostgreSQL window functions&amp;rdquo;&lt;/a&gt; e, para ser justo, o objetivo do post era apresentar a &lt;a href="https://www.postgresql.org/docs/8.4/static/functions-window.html"target="_blank" rel="noopener"&gt;função de janela &amp;ldquo;ntile&amp;rdquo; do PostgreSQL&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Acontece que, no caminho, o autor cometeu o mesmo erro que eu vejo se repetir o tempo todo.&lt;/p&gt;
&lt;p&gt;Vamos supor que você tem um projeto com recursos que precisa listar por &amp;ldquo;popularidade&amp;rdquo;. Pode ser um site estilo Reddit, onde as pessoas curtem ou descurtem posts e comentários. Pode ser um e-commerce onde as pessoas curtem ou descurtem produtos.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Estava lendo um post publicado recentemente, intitulado <a href="http://naturaily.com/blog/post/ruby-on-rails-implementation-of-a-ranking-system-using-postgresql-window-functions"target="_blank" rel="noopener">&ldquo;Ruby on Rails implementation of a ranking system using PostgreSQL window functions&rdquo;</a> e, para ser justo, o objetivo do post era apresentar a <a href="https://www.postgresql.org/docs/8.4/static/functions-window.html"target="_blank" rel="noopener">função de janela &ldquo;ntile&rdquo; do PostgreSQL</a>.</p>
<p>Acontece que, no caminho, o autor cometeu o mesmo erro que eu vejo se repetir o tempo todo.</p>
<p>Vamos supor que você tem um projeto com recursos que precisa listar por &ldquo;popularidade&rdquo;. Pode ser um site estilo Reddit, onde as pessoas curtem ou descurtem posts e comentários. Pode ser um e-commerce onde as pessoas curtem ou descurtem produtos.</p>
<p>Pode ser qualquer coisa em que as pessoas curtem ou descurtem algo.</p>
<p>O maior erro que se comete é considerar um score simplório 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">popularity</span> <span class="o">=</span> <span class="n">positive_votes</span> <span class="o">-</span> <span class="n">negative_votes</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>Existe um artigo antigo chamado <a href="http://www.evanmiller.org/how-not-to-sort-by-average-rating.html"target="_blank" rel="noopener">&ldquo;How Not To Sort By Average Rating&rdquo;</a> e cito:</p>
<blockquote>
  <p>&ldquo;<em>Por que está errado:</em> Suponha que um item tenha 600 votos positivos e 400 negativos: 60% positivo. Suponha que o item dois tenha 5.500 votos positivos e 4.500 negativos: 55% positivo. Esse algoritmo coloca o item dois (score = 1000, mas só 55% positivo) acima do item um (score = 200, com 60% positivo). ERRADO.&rdquo;</p>

</blockquote>
<p>Aí você pode pensar: já sei como consertar:</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="no">Score</span> <span class="o">=</span> <span class="n">average_rating</span> <span class="o">=</span> <span class="n">positive_votes</span> <span class="o">/</span> <span class="n">total_votes</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, está errado, e cito mais uma vez:</p>
<blockquote>
  <p>&ldquo;<em>Por que está errado:</em> A média funciona bem quando sempre se tem uma tonelada de votos, mas suponha que o item 1 tenha 2 votos positivos e 0 negativos. Suponha que o item 2 tenha 100 votos positivos e 1 negativo. Esse algoritmo coloca o item dois (toneladas de votos positivos) abaixo do item um (pouquíssimos votos positivos). ERRADO.&rdquo;</p>

</blockquote>
<h3>Solução Correta: Limite Inferior do Intervalo de Confiança de Wilson para uma Bernoulli<span class="hx:absolute hx:-mt-20" id="solução-correta-limite-inferior-do-intervalo-de-confiança-de-wilson-para-uma-bernoulli"></span>
    <a href="#solu%c3%a7%c3%a3o-correta-limite-inferior-do-intervalo-de-confian%c3%a7a-de-wilson-para-uma-bernoulli" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>E cito de novo:</p>
<blockquote>
  <p>&ldquo;<em>Como assim:</em> Precisamos balancear a proporção de votos positivos com a incerteza de um número pequeno de observações. Felizmente, a matemática para isso já tinha sido resolvida em 1927 por Edwin B. Wilson. O que queremos perguntar é: dadas as avaliações que tenho, existe 95% de chance de que a fração &lsquo;real&rsquo; de avaliações positivas seja, no mínimo, quanto? Wilson dá a resposta. Considerando apenas votos positivos e negativos (ou seja, não uma escala de 5 estrelas), o limite inferior da proporção de avaliações positivas é dado por:&rdquo;</p>

</blockquote>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/565/rating-equation.png" alt="Equação do Limite Inferior de Bernoulli"  loading="lazy" /></p>
<p>Recomendo que você leia o <a href="http://www.evanmiller.org/how-not-to-sort-by-average-rating.html"target="_blank" rel="noopener">artigo original</a>, mas vamos ao que interessa. Seguindo o post original que linkei no começo, eu tenho uma app Rails simples de blog posts, só que em vez de um campo <code>visits_count</code> preciso adicionar campos <code>positive:integer</code> e <code>negative:integer</code>, além da interface para registrar os votos.</p>
<p>E vou substituir a classe <code>PostWithPopularityQuery</code> pelo 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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">PostWithPopularityQuery</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">call</span>
</span></span><span class="line"><span class="cl">    <span class="no">Post</span><span class="o">.</span><span class="n">find_by_sql</span> <span class="o">[</span><span class="s1">&#39;SELECT id, title, body, positive, negative,
</span></span></span><span class="line"><span class="cl"><span class="s1">        ((positive + 1.9208) / (positive + negative) -
</span></span></span><span class="line"><span class="cl"><span class="s1">        1.96 * SQRT((positive * negative) / (positive + negative) + 0.9604) /
</span></span></span><span class="line"><span class="cl"><span class="s1">        (positive + negative)) / (1 + 3.8416 / (positive + negative))
</span></span></span><span class="line"><span class="cl"><span class="s1">        AS ci_lower_bound
</span></span></span><span class="line"><span class="cl"><span class="s1">      FROM posts 
</span></span></span><span class="line"><span class="cl"><span class="s1">      WHERE positive + negative &gt; 0
</span></span></span><span class="line"><span class="cl"><span class="s1">      ORDER BY ci_lower_bound DESC&#39;</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>E isto é o que eu espero ver num scaffold simples de <code>index.html.erb</code>:</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/564/Screen_Shot_2016-10-31_at_16.43.50.png" alt="Página index de Posts"  loading="lazy" /></p>
<p>Eu iria além e recomendaria que <code>ci_lower_bound</code> fosse um campo float na tabela, com um ActiveJob assíncrono atualizando esse valor em intervalos maiores (a cada 5 minutos, por exemplo). Aí a action <code>PostsController#index</code> faria um <code>SELECT</code> direto, ordenando por um campo real e indexado <code>ci_lower_bound DESC</code>, sem refazer as contas a <strong>cada</strong> query.</p>
<p>Agora <strong>ESTE</strong> é o jeito correto de implementar um sistema simples e ingênuo de ranking de popularidade que realmente funciona como deveria.</p>
<p>E não é a única forma. Há dezenas de boas discussões sobre algoritmos online. Todo serviço que depende de popularidade de conteúdo vem refinando algoritmos desse tipo há anos. O Facebook tinha um algoritmo chamado &ldquo;EdgeRank&rdquo;, que dependia de variáveis como Afinidade, Peso, Decaimento Temporal, e parece que evoluiu tanto que hoje calcula popularidade contra mais de <a href="http://marketingland.com/edgerank-is-dead-facebooks-news-feed-algorithm-now-has-close-to-100k-weight-factors-55908"target="_blank" rel="noopener">100 mil variáveis</a>!!</p>
<p>Mas, independente do serviço online, eu garanto que <strong>nenhum</strong> ordena por contagem simples de visitas ou pela média simples dos votos. Isso seria simplesmente errado.</p>
]]></content:encoded><category>algoritmos-e-estruturas-de-dados</category><category>rails</category><category>ruby</category></item><item><title>Geração de Linguagem Natural em Ruby (com JRuby + SimpleNLG)</title><link>https://akitaonrails.github.io/2016/10/28/geracao-de-linguagem-natural-em-ruby-com-jruby-simplenlg/</link><guid isPermaLink="true">https://akitaonrails.github.io/2016/10/28/geracao-de-linguagem-natural-em-ruby-com-jruby-simplenlg/</guid><pubDate>Fri, 28 Oct 2016 17:53:00 -0200</pubDate><description>&lt;p&gt;Estou tocando um projeto que precisa gerar frases em inglês decentes. A primeira versão que fiz usava o jeito mais ingênuo possível: criar um template de string e ir fazendo substituições e concatenações simples.&lt;/p&gt;
&lt;p&gt;Mas dá pra imaginar que isso vira uma bagunça rapidamente quando você precisa lidar com pluralização, flexão verbal, e o código começa a ficar parecido com isso:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ruby" data-lang="ruby"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;There &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@users&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;is&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;are&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@users&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; user&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;s&amp;#39;&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="vi"&gt;@users&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;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"&gt;
&lt;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"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Ou usar o suporte de I18n do Rails desse jeito:&lt;/p&gt;</description><content:encoded><![CDATA[<p>Estou tocando um projeto que precisa gerar frases em inglês decentes. A primeira versão que fiz usava o jeito mais ingênuo possível: criar um template de string e ir fazendo substituições e concatenações simples.</p>
<p>Mas dá pra imaginar que isso vira uma bagunça rapidamente quando você precisa lidar com pluralização, flexão verbal, e o código começa a ficar parecido com 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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="s2">&#34;There </span><span class="si">#{</span><span class="vi">@users</span><span class="o">.</span><span class="n">size</span> <span class="o">==</span> <span class="mi">1</span> <span class="o">?</span> <span class="s1">&#39;is&#39;</span> <span class="p">:</span> <span class="s1">&#39;are&#39;</span><span class="si">}</span><span class="s2"> </span><span class="si">#{</span><span class="vi">@users</span><span class="o">.</span><span class="n">size</span><span class="si">}</span><span class="s2"> user</span><span class="si">#{</span><span class="s1">&#39;s&#39;</span> <span class="k">unless</span> <span class="vi">@users</span><span class="o">.</span><span class="n">size</span> <span class="o">==</span> <span class="mi">1</span><span class="si">}</span><span class="s2">.&#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>Ou usar o suporte de I18n do Rails desse jeito:</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="no">I18n</span><span class="o">.</span><span class="n">backend</span><span class="o">.</span><span class="n">store_translations</span> <span class="ss">:en</span><span class="p">,</span> <span class="ss">:user_msg</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="ss">:one</span> <span class="o">=&gt;</span> <span class="s1">&#39;There is 1 user&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">:other</span> <span class="o">=&gt;</span> <span class="s1">&#39;There are %{count} users&#39;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="no">I18n</span><span class="o">.</span><span class="n">translate</span> <span class="ss">:user_msg</span><span class="p">,</span> <span class="ss">:count</span> <span class="o">=&gt;</span> <span class="mi">2</span>
</span></span><span class="line"><span class="cl"><span class="c1"># =&gt; &#39;There are 2 users&#39;</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 frases transacionais simples (tipo flash messages), isso resolve de sobra.</p>
<p>Mas se você quer gerar um artigo inteiro em inglês a partir de estruturas de dados, a lógica fica convoluta muito rápido.</p>
<p>Dei uma olhada em alguns projetos Ruby que poderiam ajudar, por exemplo:</p>
<ul>
<li><a href="https://github.com/chorn/nameable"target="_blank" rel="noopener">&ldquo;nameable&rdquo;</a> que faz coisas úteis assim:</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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">Nameable</span><span class="o">::</span><span class="no">Latin</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s1">&#39;Chris&#39;</span><span class="p">)</span><span class="o">.</span><span class="n">gender</span>
</span></span><span class="line"><span class="cl"><span class="c1">#=&gt; :male</span>
</span></span><span class="line"><span class="cl"><span class="no">Nameable</span><span class="o">::</span><span class="no">Latin</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s1">&#39;Janine&#39;</span><span class="p">)</span><span class="o">.</span><span class="n">female?</span>
</span></span><span class="line"><span class="cl"><span class="c1">#=&gt; 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>
<ul>
<li><a href="https://github.com/maetl/calyx"target="_blank" rel="noopener">&ldquo;calyx&rdquo;</a> que dá pra usar para gerar frases simples desse jeito:</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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">GreenBottle</span> <span class="o">&lt;</span> <span class="no">Calyx</span><span class="o">::</span><span class="no">Grammar</span>
</span></span><span class="line"><span class="cl">  <span class="n">mapping</span> <span class="ss">:pluralize</span><span class="p">,</span> <span class="sr">/(.+)/</span> <span class="o">=&gt;</span> <span class="s1">&#39;\\1s&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="n">start</span> <span class="s1">&#39;One green {bottle}.&#39;</span><span class="p">,</span> <span class="s1">&#39;Two green {bottle.pluralize}.&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="n">bottle</span> <span class="s1">&#39;bottle&#39;</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"># =&gt; &#34;One green bottle.&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># =&gt; &#34;Two green bottles.&#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>Bonitinho, mas ainda inútil para as necessidades mais complexas que tenho em mente.</p>
<p>Então resolvi cavar mais fundo, no mundo obscuro do NLG, ou <strong>Natural Language Generation</strong> (Geração de Linguagem Natural). Cuidado para não confundir com NLP, que é Natural Language Processing, justamente o oposto do que eu quero. NLP recebe um texto em inglês e devolve uma estrutura de dados parseada.</p>
<p>Para <strong>NLP</strong> (parsing, tokenização, etc), recomendo demais o <a href="http://stanfordnlp.github.io/CoreNLP/"target="_blank" rel="noopener">&ldquo;Stanford CoreNLP&rdquo;</a>. Parece ser um dos mais robustos e completos por aí (vai, é de Stanford). Também é um projeto Java, e um download enorme (mais de 300MB!). Esses projetos de linguística são pesados pra caramba porque precisam baixar dicionários inteiros e bases de léxico.</p>
<p>Mas focando no meu problema atual, <strong>NLG</strong>, existem <a href="https://aclweb.org/aclwiki/index.php?title=Downloadable_NLG_systems"target="_blank" rel="noopener">várias opções</a> disponíveis. Sendo bem honesto, não fiz uma pesquisa muito extensa, então se você conhece qual é o mais robusto, bem mantido e com uma interface fácil de usar, deixa nos comentários aí embaixo.</p>
<p>Minha escolha foi o <a href="https://github.com/simplenlg/simplenlg"target="_blank" rel="noopener">SimpleNLG</a>. Pela página dele no GitHub dá pra ver que continua sendo bem mantido até hoje, é uma biblioteca Java simples e é uma das alternativas mais &ldquo;simples&rdquo;. Já o <a href="http://www.fb10.uni-bremen.de/anglistik/langpro/kpml/README.html"target="_blank" rel="noopener">KPML</a> está no extremo oposto: parece ser um dos mais antigos (desde os anos 80!) e robustos. Mas falando sério, você precisa praticamente de um Ph.D. só pra começar.</p>
<p>Ler o código-fonte Java do SimpleNLG foi chato, mas tranquilo o suficiente. Reserva um dia inteiro de estudo para se acostumar com o código e você está pronto.</p>
<p>O problema principal é que ele é escrito em Java, e eu não pretendo escrever nada em Java (ou derivados) por enquanto. Por um instante cheguei a considerar a empreitada de reescrever o troço todo em algo mais portátil, tipo Rust, que eu poderia carregar de qualquer lugar via FFI.</p>
<p>Mas mesmo o SimpleNLG tendo &ldquo;Simple&rdquo; no nome, ele tem algumas dependências cabeludas para carregar a base de léxico. E a base em si é um dump de HSQLDB, que é um banco escrito em Java. E ainda haveria a questão de manter um fork.</p>
<p>Desisti rápido dessa ideia e contornei o problema embrulhando a biblioteca dentro de um endpoint <a href="https://github.com/rails-api/rails-api"target="_blank" rel="noopener">Rails-API</a> simples. Tive uns problemas porque eu tinha o Git LFS rastreando os arquivos jar no meu sistema e o Heroku não suporta isso, e acabei com um deploy corrompido (cuidado com essas pegadinhas, aliás!).</p>
<p>No fim consegui colocar no ar um projeto JRuby + Rails-API funcionando com SimpleNLG embutido no Heroku. Você pode subir sua própria cópia clonando meu <a href="https://github.com/Codeminer42/nlg_service"target="_blank" rel="noopener">nlg_service</a>. Funciona bem com o JRuby 9.1.5.0 mais recente. Você precisa pagar pelo menos um plano Hobby no Heroku. Java leva um tempo ridículo para subir e mais tempo ainda para esquentar. O free tier do Heroku desliga seu dyno se ele ficar parado, e a próxima requisição web vai dar timeout ou levar uma eternidade para responder.</p>
<p>Uma vez no ar, ele sobe o Rails e carrega <a href="https://github.com/Codeminer42/nlg_service/blob/master/config/initializers/simple_nlg.rb"target="_blank" rel="noopener">este initializer</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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;java&#39;</span>
</span></span><span class="line"><span class="cl"><span class="no">Java</span><span class="o">::</span><span class="no">JavaLang</span><span class="o">::</span><span class="no">System</span><span class="o">.</span><span class="n">set_property</span> <span class="s2">&#34;file.encoding&#34;</span><span class="p">,</span><span class="s2">&#34;UTF-8&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="no">SIMPLE_NLG_DEFAULT_LEXICON_PATH</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">root</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;lib/SimpleNLG/resources/default-lexicon.xml&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">to_s</span><span class="o">.</span><span class="n">freeze</span>
</span></span><span class="line"><span class="cl"><span class="no">SIMPLE_NLG_PATH</span>                 <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">root</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;lib/SimpleNLG&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">to_s</span><span class="o">.</span><span class="n">freeze</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="no">Dir</span><span class="o">[</span><span class="s2">&#34;</span><span class="si">#{</span><span class="no">SIMPLE_NLG_PATH</span><span class="si">}</span><span class="s2">/*.jar&#34;</span><span class="o">].</span><span class="n">each</span> <span class="p">{</span> <span class="o">|</span><span class="n">jar</span><span class="o">|</span> <span class="nb">require</span> <span class="n">jar</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>E depois mapeio as classes <a href="https://github.com/Codeminer42/nlg_service/blob/master/app/models/simple_nlg.rb"target="_blank" rel="noopener">assim</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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">SimpleNLG</span>
</span></span><span class="line"><span class="cl">  <span class="sx">%w(
</span></span></span><span class="line"><span class="cl"><span class="sx">    simplenlg.aggregation
</span></span></span><span class="line"><span class="cl"><span class="sx">    simplenlg.features
</span></span></span><span class="line"><span class="cl"><span class="sx">    simplenlg.format.english
</span></span></span><span class="line"><span class="cl"><span class="sx">    simplenlg.framework
</span></span></span><span class="line"><span class="cl"><span class="sx">    simplenlg.lexicon
</span></span></span><span class="line"><span class="cl"><span class="sx">    simplenlg.morphology.english
</span></span></span><span class="line"><span class="cl"><span class="sx">    simplenlg.orthography.english
</span></span></span><span class="line"><span class="cl"><span class="sx">    simplenlg.phrasespec
</span></span></span><span class="line"><span class="cl"><span class="sx">    simplenlg.realiser.english
</span></span></span><span class="line"><span class="cl"><span class="sx">    simplenlg.syntax.english
</span></span></span><span class="line"><span class="cl"><span class="sx">    simplenlg.xmlrealiser
</span></span></span><span class="line"><span class="cl"><span class="sx">    simplenlg.xmlrealiser.wrapper
</span></span></span><span class="line"><span class="cl"><span class="sx">  )</span><span class="o">.</span><span class="n">each</span> <span class="p">{</span> <span class="o">|</span><span class="n">package</span><span class="o">|</span> <span class="n">include_package</span> <span class="n">package</span> <span class="p">}</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, tenho um endpoint simples mapeado para uma action de <a href="https://github.com/Codeminer42/nlg_service/blob/master/app/controllers/api/realisers_controller.rb"target="_blank" rel="noopener">controller</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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Api</span><span class="o">::</span><span class="no">RealisersController</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">create</span>
</span></span><span class="line"><span class="cl">    <span class="n">reader</span> <span class="o">=</span> <span class="n">java</span><span class="o">::</span><span class="n">io</span><span class="o">::</span><span class="no">StringReader</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">params</span><span class="o">[</span><span class="ss">:xml</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">begin</span>
</span></span><span class="line"><span class="cl">      <span class="n">records</span> <span class="o">=</span> <span class="no">SimpleNLG</span><span class="o">::</span><span class="no">XMLRealiser</span><span class="o">.</span><span class="n">getRecording</span><span class="p">(</span><span class="n">reader</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">output</span> <span class="o">=</span> <span class="n">records</span><span class="o">.</span><span class="n">getRecord</span><span class="o">.</span><span class="n">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">record</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">        <span class="no">SimpleNLG</span><span class="o">::</span><span class="no">XMLRealiser</span><span class="o">.</span><span class="n">realise</span><span class="p">(</span><span class="n">record</span><span class="o">&amp;.</span><span class="n">getDocument</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="vi">@realisation</span> <span class="o">=</span> <span class="n">output</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span>
</span></span><span class="line"><span class="cl">      <span class="n">render</span> <span class="ss">plain</span><span class="p">:</span> <span class="vi">@realisation</span>
</span></span><span class="line"><span class="cl">    <span class="k">ensure</span>
</span></span><span class="line"><span class="cl">      <span class="n">reader</span><span class="o">.</span><span class="n">close</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 processo de gerar o texto final em inglês se chama <strong>&ldquo;realisation&rdquo;</strong> (realização). O SimpleNLG tem uma API Java bem completa, mas também expõe tudo num formato XML mais simples. O <a href="https://github.com/simplenlg/simplenlg/blob/master/src/main/resources/xml/RealizerSchema.xsd"target="_blank" rel="noopener">XML Realiser Schema</a> completo está disponível como XSD.</p>
<p>Se eu quiser escrever esta frase:</p>
<blockquote>
  <p>&ldquo;There are some finished and delivered stories that may not have been tested.&rdquo;</p>

</blockquote>
<p>Este é o XML que preciso montar:</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-xml" data-lang="xml"><span class="line"><span class="cl"><span class="cp">&lt;?xml version=&#34;1.0&#34;?&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;NLGSpec</span> <span class="na">xmlns=</span><span class="s">&#34;http://simplenlg.googlecode.com/svn/trunk/res/xml&#34;</span> <span class="na">xmlns:xsd=</span><span class="s">&#34;http://www.w3.org/2001/XMLSchema&#34;</span> <span class="na">xmlns:xsi=</span><span class="s">&#34;http://www.w3.org/2001/XMLSchema-instance&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;Recording&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;Record&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&lt;Document</span> <span class="na">cat=</span><span class="s">&#34;PARAGRAPH&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&lt;child</span> <span class="na">xsi:type=</span><span class="s">&#34;SPhraseSpec&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&lt;subj</span> <span class="na">xsi:type=</span><span class="s">&#34;NPPhraseSpec&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&lt;head</span> <span class="na">cat=</span><span class="s">&#34;ADVERB&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl">              <span class="nt">&lt;base&gt;</span>there<span class="nt">&lt;/base&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&lt;/head&gt;</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&lt;/subj&gt;</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&lt;vp</span> <span class="na">xsi:type=</span><span class="s">&#34;VPPhraseSpec&#34;</span> <span class="na">PERSON=</span><span class="s">&#34;THIRD&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&lt;head</span> <span class="na">cat=</span><span class="s">&#34;VERB&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl">              <span class="nt">&lt;base&gt;</span>be<span class="nt">&lt;/base&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&lt;/head&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&lt;compl</span> <span class="na">xsi:type=</span><span class="s">&#34;NPPhraseSpec&#34;</span> <span class="na">NUMBER=</span><span class="s">&#34;PLURAL&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl">              <span class="nt">&lt;head</span> <span class="na">cat=</span><span class="s">&#34;NOUN&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl">                <span class="nt">&lt;base&gt;</span>story<span class="nt">&lt;/base&gt;</span>
</span></span><span class="line"><span class="cl">              <span class="nt">&lt;/head&gt;</span>
</span></span><span class="line"><span class="cl">              <span class="nt">&lt;spec</span> <span class="na">xsi:type=</span><span class="s">&#34;WordElement&#34;</span> <span class="na">cat=</span><span class="s">&#34;DETERMINER&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl">                <span class="nt">&lt;base&gt;</span>a<span class="nt">&lt;/base&gt;</span>
</span></span><span class="line"><span class="cl">              <span class="nt">&lt;/spec&gt;</span>
</span></span><span class="line"><span class="cl">              <span class="nt">&lt;preMod</span> <span class="na">xsi:type=</span><span class="s">&#34;CoordinatedPhraseElement&#34;</span> <span class="na">conj=</span><span class="s">&#34;and&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl">                <span class="nt">&lt;coord</span> <span class="na">xsi:type=</span><span class="s">&#34;VPPhraseSpec&#34;</span> <span class="na">TENSE=</span><span class="s">&#34;PAST&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl">                  <span class="nt">&lt;head</span> <span class="na">cat=</span><span class="s">&#34;VERB&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl">                    <span class="nt">&lt;base&gt;</span>finish<span class="nt">&lt;/base&gt;</span>
</span></span><span class="line"><span class="cl">                  <span class="nt">&lt;/head&gt;</span>
</span></span><span class="line"><span class="cl">                <span class="nt">&lt;/coord&gt;</span>
</span></span><span class="line"><span class="cl">                <span class="nt">&lt;coord</span> <span class="na">xsi:type=</span><span class="s">&#34;VPPhraseSpec&#34;</span> <span class="na">TENSE=</span><span class="s">&#34;PAST&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl">                  <span class="nt">&lt;head</span> <span class="na">cat=</span><span class="s">&#34;VERB&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl">                    <span class="nt">&lt;base&gt;</span>deliver<span class="nt">&lt;/base&gt;</span>
</span></span><span class="line"><span class="cl">                  <span class="nt">&lt;/head&gt;</span>
</span></span><span class="line"><span class="cl">                <span class="nt">&lt;/coord&gt;</span>
</span></span><span class="line"><span class="cl">              <span class="nt">&lt;/preMod&gt;</span>
</span></span><span class="line"><span class="cl">              <span class="nt">&lt;compl</span> <span class="na">xsi:type=</span><span class="s">&#34;SPhraseSpec&#34;</span> <span class="na">MODAL=</span><span class="s">&#34;may&#34;</span> <span class="na">PASSIVE=</span><span class="s">&#34;true&#34;</span> <span class="na">TENSE=</span><span class="s">&#34;PAST&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl">                <span class="nt">&lt;vp</span> <span class="na">xsi:type=</span><span class="s">&#34;VPPhraseSpec&#34;</span> <span class="na">TENSE=</span><span class="s">&#34;PAST&#34;</span> <span class="na">NEGATED=</span><span class="s">&#34;true&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl">                  <span class="nt">&lt;head</span> <span class="na">cat=</span><span class="s">&#34;VERB&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl">                    <span class="nt">&lt;base&gt;</span>test<span class="nt">&lt;/base&gt;</span>
</span></span><span class="line"><span class="cl">                  <span class="nt">&lt;/head&gt;</span>
</span></span><span class="line"><span class="cl">                <span class="nt">&lt;/vp&gt;</span>
</span></span><span class="line"><span class="cl">              <span class="nt">&lt;/compl&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&lt;/compl&gt;</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&lt;/vp&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&lt;/child&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&lt;/Document&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;/Record&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;/Recording&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/NLGSpec&gt;</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>Ok, eu sei, isso é absurdo.</p>
<p>Por isso resolvi seguir adiante e usar uma das forças mais reconhecidas do Ruby: criar <strong>DSLs</strong>, ou Domain Specific Languages.</p>
<p>O resultado da minha tentativa inicial de simplificar esse processo é a gem <a href="https://github.com/Codeminer42/nlg_xml_realiser_builder"target="_blank" rel="noopener">nlg_xml_realiser_builder</a>. Basta adicionar isso ao seu <code>Gemfile</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gem &#39;nlg_xml_realiser_builder&#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>E o XML monstruoso ali em cima vira algo bem mais gerenciável, 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">dsl</span> <span class="o">=</span> <span class="no">NlgXmlRealiserBuilder</span><span class="o">::</span><span class="no">DSL</span><span class="o">.</span><span class="n">new</span>
</span></span><span class="line"><span class="cl"><span class="n">dsl</span><span class="o">.</span><span class="n">builder</span><span class="p">(</span><span class="kp">true</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">sp</span> <span class="ss">:child</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">subj</span> <span class="ss">:np</span><span class="p">,</span> <span class="s1">&#39;there&#39;</span><span class="p">,</span> <span class="ss">cat</span><span class="p">:</span> <span class="s1">&#39;ADVERB&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="n">verb</span> <span class="s1">&#39;be&#39;</span><span class="p">,</span> <span class="ss">PERSON</span><span class="p">:</span> <span class="s1">&#39;THIRD&#39;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">compl</span> <span class="ss">:np</span><span class="p">,</span> <span class="o">[</span><span class="s1">&#39;a&#39;</span><span class="p">,</span> <span class="s1">&#39;story&#39;</span><span class="o">]</span><span class="p">,</span> <span class="ss">NUMBER</span><span class="p">:</span> <span class="s1">&#39;PLURAL&#39;</span>  <span class="k">do</span>
</span></span><span class="line"><span class="cl">        <span class="n">preMod</span> <span class="ss">:cp</span><span class="p">,</span> <span class="ss">conj</span><span class="p">:</span> <span class="s1">&#39;and&#39;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">          <span class="n">coord</span> <span class="ss">:vp</span><span class="p">,</span> <span class="s1">&#39;finish&#39;</span><span class="p">,</span> <span class="ss">TENSE</span><span class="p">:</span> <span class="s1">&#39;PAST&#39;</span>
</span></span><span class="line"><span class="cl">          <span class="n">coord</span> <span class="ss">:vp</span><span class="p">,</span> <span class="s1">&#39;deliver&#39;</span><span class="p">,</span> <span class="ss">TENSE</span><span class="p">:</span> <span class="s1">&#39;PAST&#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="n">compl</span> <span class="ss">:sp</span><span class="p">,</span> <span class="ss">MODAL</span><span class="p">:</span> <span class="s1">&#39;may&#39;</span><span class="p">,</span> <span class="ss">PASSIVE</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">TENSE</span><span class="p">:</span> <span class="s1">&#39;PAST&#39;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">          <span class="n">verb</span> <span class="s1">&#39;test&#39;</span><span class="p">,</span> <span class="ss">TENSE</span><span class="p">:</span> <span class="s1">&#39;PAST&#39;</span><span class="p">,</span> <span class="ss">NEGATED</span><span class="p">:</span> <span class="kp">true</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">end</span><span class="o">.</span><span class="n">to_xml</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>Entender as nuances de um <code>NPPhraseSpec</code> versus um <code>VPPhraseSpec</code>, ou a diferença entre um <code>WordElement</code> e um <code>StringElement</code>, está fora do escopo deste post. Mas a maior parte do XSD original foi mapeada <a href="https://github.com/Codeminer42/nlg_xml_realiser_builder/blob/master/lib/nlg_xml_realiser_builder/consts.rb"target="_blank" rel="noopener">neste arquivo de constantes</a>.</p>
<p>Tenho <a href="https://github.com/Codeminer42/nlg_xml_realiser_builder/blob/master/spec/nlg_xml_realiser_builder_spec.rb"target="_blank" rel="noopener">algumas specs de aceitação</a> que geram XMLs como o de cima, postam no meu NLG Web Service que está no ar e pegam de volta as frases em inglês resultantes. Vou mudar esse processo no futuro, mas você já pode testar por conta própria.</p>
<p>As vantagens começam aqui. Vamos olhar o exemplo anterior mais de perto. De novo, ele renderiza esta frase:</p>
<blockquote>
  <p>&ldquo;There are some finished and delivered stories that may not have been tested.&rdquo;</p>

</blockquote>
<p>Repare que está no plural porque estou falando de &lsquo;stories&rsquo;, mas e se eu quiser uma versão no singular?</p>
<p>Abaixo está a nova versão, onde eu envolvo o código num método e faço o atributo &lsquo;NUMBER&rsquo; aceitar tanto &lsquo;PLURAL&rsquo; quanto &lsquo;SINGULAR&rsquo;:</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">example</span><span class="p">(</span><span class="n">plural</span> <span class="o">=</span> <span class="s1">&#39;PLURAL&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">dsl</span> <span class="o">=</span> <span class="no">NlgXmlRealiserBuilder</span><span class="o">::</span><span class="no">DSL</span><span class="o">.</span><span class="n">new</span>
</span></span><span class="line"><span class="cl">  <span class="n">dsl</span><span class="o">.</span><span class="n">builder</span><span class="p">(</span><span class="kp">true</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">sp</span> <span class="ss">:child</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">subj</span> <span class="ss">:np</span><span class="p">,</span> <span class="s1">&#39;there&#39;</span><span class="p">,</span> <span class="ss">cat</span><span class="p">:</span> <span class="s1">&#39;ADVERB&#39;</span>
</span></span><span class="line"><span class="cl">      <span class="n">verb</span> <span class="s1">&#39;be&#39;</span><span class="p">,</span> <span class="ss">PERSON</span><span class="p">:</span> <span class="s1">&#39;THIRD&#39;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">        <span class="n">compl</span> <span class="ss">:np</span><span class="p">,</span> <span class="o">[</span><span class="s1">&#39;a&#39;</span><span class="p">,</span> <span class="s1">&#39;story&#39;</span><span class="o">]</span><span class="p">,</span> <span class="ss">NUMBER</span><span class="p">:</span> <span class="n">plural</span>  <span class="k">do</span>
</span></span><span class="line"><span class="cl">          <span class="n">preMod</span> <span class="ss">:cp</span><span class="p">,</span> <span class="ss">conj</span><span class="p">:</span> <span class="s1">&#39;and&#39;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">            <span class="n">coord</span> <span class="ss">:vp</span><span class="p">,</span> <span class="s1">&#39;finish&#39;</span><span class="p">,</span> <span class="ss">TENSE</span><span class="p">:</span> <span class="s1">&#39;PAST&#39;</span>
</span></span><span class="line"><span class="cl">            <span class="n">coord</span> <span class="ss">:vp</span><span class="p">,</span> <span class="s1">&#39;deliver&#39;</span><span class="p">,</span> <span class="ss">TENSE</span><span class="p">:</span> <span class="s1">&#39;PAST&#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="n">compl</span> <span class="ss">:sp</span><span class="p">,</span> <span class="ss">MODAL</span><span class="p">:</span> <span class="s1">&#39;may&#39;</span><span class="p">,</span> <span class="ss">PASSIVE</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">TENSE</span><span class="p">:</span> <span class="s1">&#39;PAST&#39;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">            <span class="n">verb</span> <span class="s1">&#39;test&#39;</span><span class="p">,</span> <span class="ss">TENSE</span><span class="p">:</span> <span class="s1">&#39;PAST&#39;</span><span class="p">,</span> <span class="ss">NEGATED</span><span class="p">:</span> <span class="kp">true</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">end</span><span class="o">.</span><span class="n">to_xml</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>E posso rodar a versão singular assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>puts example(&#39;SINGULAR&#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>Esta é a frase resultante:</p>
<blockquote>
  <p>&ldquo;There is a finished and delivered story that may not have been tested.&rdquo;</p>

</blockquote>
<p>Olha como ele mudou o verbo de &ldquo;are&rdquo; para &ldquo;is&rdquo; e o determinante do substantivo de &ldquo;some&rdquo; para &ldquo;a&rdquo; sozinho! E claro, este é um exemplo bobo. Agora imagine um artigo inteiro customizável, cheio de parágrafos e frases que eu posso variar dependendo de várias variáveis que tenho.</p>
<p>Enquanto eu estudava e escrevia essa DSL, peguei uma compreensão razoável da estrutura do SimpleNLG, mas se você tem mais exemplos para estruturas de frase mais complexas, deixa nos comentários aí embaixo.</p>
<p>A maior parte das specs foi copiada dos testes do XML Realiser do projeto Java original para garantir que estou cobrindo a maioria dos casos.</p>
<p>Vai ser interessante ver se essa DSL facilita a vida de mais gente que queira experimentar com NLG. Como sempre, mandem seus Pull Requests, ideias e sugestões nos meus repositórios públicos do GitHub:</p>
<ul>
<li><a href="https://github.com/Codeminer42/nlg_service"target="_blank" rel="noopener">nlg_service</a></li>
<li><a href="https://github.com/Codeminer42/nlg_xml_realiser_builder"target="_blank" rel="noopener">nlg_xml_realiser_builder</a></li>
</ul>
<p>E se você se interessa pelo assunto de NLP e NLG, encontrei <a href="https://github.com/diasks2/ruby-nlp"target="_blank" rel="noopener">esta lista</a> de projetos open source relacionados em Ruby também.</p>
]]></content:encoded><category>ruby</category><category>java</category><category>rails</category></item><item><title>Compartilhando models entre apps Rails - Parte 2</title><link>https://akitaonrails.github.io/2016/10/03/compartilhando-models-entre-apps-rails-parte-2/</link><guid isPermaLink="true">https://akitaonrails.github.io/2016/10/03/compartilhando-models-entre-apps-rails-parte-2/</guid><pubDate>Mon, 03 Oct 2016 15:57:00 -0300</pubDate><description>&lt;p&gt;Vamos continuar de onde parei na &lt;a href="http://www.akitaonrails.com/2016/10/03/compartilhando-models-entre-aplicacoes-rails-parte-1"target="_blank" rel="noopener"&gt;Parte 1&lt;/a&gt;, onde descrevi rapidamente como extrair lógica reusável de models de uma app Rails para um Rubygem testável.&lt;/p&gt;
&lt;p&gt;Se eu fosse construir uma segunda app Rails conectando direto ao mesmo banco da primeira, bastaria adicionar a dependência do gem extraído:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ruby" data-lang="ruby"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;central-support&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;github&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Codeminer42/cm42-central-support&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;master&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;central/support&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;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"&gt;
&lt;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"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Aí recriar os models incluindo os mesmos Concerns, e remover a capacidade de rodar &lt;code&gt;bin/rails db:migrate&lt;/code&gt; na app secundária (criando tasks vazias com o mesmo nome, por exemplo).&lt;/p&gt;</description><content:encoded><![CDATA[<p>Vamos continuar de onde parei na <a href="http://www.akitaonrails.com/2016/10/03/compartilhando-models-entre-aplicacoes-rails-parte-1"target="_blank" rel="noopener">Parte 1</a>, onde descrevi rapidamente como extrair lógica reusável de models de uma app Rails para um Rubygem testável.</p>
<p>Se eu fosse construir uma segunda app Rails conectando direto ao mesmo banco da primeira, bastaria adicionar a dependência do gem extraí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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;central-support&#39;</span><span class="p">,</span> <span class="ss">github</span><span class="p">:</span> <span class="s1">&#39;Codeminer42/cm42-central-support&#39;</span><span class="p">,</span> <span class="ss">branch</span><span class="p">:</span> <span class="s1">&#39;master&#39;</span><span class="p">,</span> <span class="nb">require</span><span class="p">:</span> <span class="s1">&#39;central/support&#39;</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í recriar os models incluindo os mesmos Concerns, e remover a capacidade de rodar <code>bin/rails db:migrate</code> na app secundária (criando tasks vazias com o mesmo nome, por exemplo).</p>
<p>Aliás, esse é um grande detalhe que deixei de fora na Parte 1: até aqui, o schema estava <strong>congelado</strong> no <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/spec/support/rails_app/db/schema.rb"target="_blank" rel="noopener">gem central-support</a>.</p>
<p>Daqui pra frente, você precisa controlar a evolução das tabelas mapeadas no gem a partir do próprio gem. A melhor abordagem é usar o <code>spec/support/rails_app</code> e criar normalmente migrations novas com <code>bin/rails g migration</code> por lá. Depois você move a migration para a pasta <code>lib/generators/central/templates/migrations</code>.</p>
<p>O <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/lib/generators/central/install_generator.rb"target="_blank" rel="noopener"><code>lib/generators/central/install_generator.rb</code></a> cuida de disponibilizar uma task <code>central:install</code> que joga as migrations novas na pasta <code>db/migrate</code> da sua aplicação como de costume. Basta rodar <code>bundle update central-support</code> para pegar as últimas mudanças, rodar <code>bin/rails g central:install</code> para criar as migrations novas (ele pula automaticamente as existentes) e rodar o <code>bin/rails db:migrate</code> normal. O código de um gerador de migration é bem simples, dá pra 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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;securerandom&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;rails/generators&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;rails/generators/base&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;rails/generators/active_record&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">Central</span>
</span></span><span class="line"><span class="cl">  <span class="k">module</span> <span class="nn">Generators</span>
</span></span><span class="line"><span class="cl">    <span class="k">class</span> <span class="nc">InstallGenerator</span> <span class="o">&lt;</span> <span class="no">Rails</span><span class="o">::</span><span class="no">Generators</span><span class="o">::</span><span class="no">Base</span>
</span></span><span class="line"><span class="cl">      <span class="kp">include</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Generators</span><span class="o">::</span><span class="no">Migration</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="n">source_root</span> <span class="no">File</span><span class="o">.</span><span class="n">expand_path</span><span class="p">(</span><span class="s2">&#34;../templates&#34;</span><span class="p">,</span> <span class="bp">__FILE__</span><span class="p">)</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">create_migrations</span>
</span></span><span class="line"><span class="cl">        <span class="no">Dir</span><span class="o">[</span><span class="s2">&#34;</span><span class="si">#{</span><span class="nb">self</span><span class="o">.</span><span class="n">class</span><span class="o">.</span><span class="n">source_root</span><span class="si">}</span><span class="s2">/migrations/*.rb&#34;</span><span class="o">].</span><span class="n">sort</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">filepath</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">          <span class="nb">name</span> <span class="o">=</span> <span class="no">File</span><span class="o">.</span><span class="n">basename</span><span class="p">(</span><span class="n">filepath</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="n">migration_template</span> <span class="s2">&#34;migrations/</span><span class="si">#{</span><span class="nb">name</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="s2">&#34;db/migrate/</span><span class="si">#{</span><span class="nb">name</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="ss">skip</span><span class="p">:</span> <span class="kp">true</span>
</span></span><span class="line"><span class="cl">          <span class="nb">sleep</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">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></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>migration_template</code> cuida de adicionar o timestamp correto no arquivo de migration, então você não precisa colocar manualmente, e o nome do arquivo template pode ser algo simples como <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/lib/generators/central/templates/migrations/add_role_field_to_users.rb"target="_blank" rel="noopener"><code>migrations/add_role_field_to_user.rb</code></a>.</p>
<p>Dito isso, tem um segundo desafio que adicionei na minha app secundária interna: eu queria que ela tivesse o seu próprio banco principal e usasse o banco do Central como fonte secundária somente leitura.</p>
<p>Então as migrations da app secundária (vamos chamar de Central-2) vão rodar contra o seu próprio banco principal, e não contra o banco principal do Central. Isso traz o seguinte problema: a suíte de testes precisa conseguir criar e migrar os dois bancos de teste, isolados do Central. Só em produção é que o Central-2 deve conectar no banco do Central.</p>
<p>Toda aplicação Rails tem um <code>config/database.yml.sample</code> e um <code>db/schema.rb</code>, então comecei criando um <code>config/database_central.yml.sample</code> e um <code>db_central/schema.rb</code>.</p>
<p>O <code>config/database_central.yml.sample</code> já fica interessante:</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">development</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">adapter</span><span class="p">:</span><span class="w"> </span><span class="l">postgresql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">encoding</span><span class="p">:</span><span class="w"> </span><span class="l">unicode</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="m">5000</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">fulcrum_development</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pool</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></span><span class="line"><span class="cl"><span class="c"># Warning: The database defined as &#34;test&#34; will be erased and</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># re-generated from your development database when you run &#34;rake&#34;.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Do not set this db to the same as development or production.</span><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">adapter</span><span class="p">:</span><span class="w"> </span><span class="l">postgresql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">encoding</span><span class="p">:</span><span class="w"> </span><span class="l">unicode</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="m">5000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="l">&lt;% if ENV[&#39;DATABASE_CENTRAL_URL&#39;] %&gt;</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">&lt;%= ENV[&#39;DATABASE_CENTRAL_URL&#39;] %&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="l">&lt;% else %&gt;</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">central_test</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="l">&lt;% end %&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pool</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></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">adapter</span><span class="p">:</span><span class="w"> </span><span class="l">postgresql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">encoding</span><span class="p">:</span><span class="w"> </span><span class="l">unicode</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="m">5000</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">&lt;%= ENV[&#39;DATABASE_CENTRAL_URL&#39;] %&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pool</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;%= ENV[&#39;DB_POOL&#39;] || 5 %&gt;</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>Em produção, ele vai usar a variável de ambiente <code>DATABASE_CENTRAL_URL</code> para conectar no banco principal do Central.</p>
<p>Rodando os testes localmente, ele simplesmente conecta num banco local chamado <code>central_test</code>.</p>
<p>Agora, ao rodar testes no Gitlab-CI (ou qualquer outro CI), eu preciso configurar o <code>DATABASE_CENTRAL_URL</code> para apontar para um banco Postgresql secundário de testes.</p>
<p>No Gitlab, eu configuro o script de build 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">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="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></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">central-bot</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="l">test</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">DATABASE_CENTRAL_URL</span><span class="p">:</span><span class="w"> </span><span class="l">postgresql://postgres:@postgres/central_test</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 config/database.yml.sample config/database.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">cp config/database_central.yml.sample config/database_central.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">bin/rails --trace central:db:create central:db:schema:load</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">bin/rails --trace db:create db:schema:load</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">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>Repare como eu copio os arquivos &ldquo;.sample&rdquo; para garantir que existam. E como eu rodo tasks que você conhece como <code>db:create db:schema:load</code> para criar o banco de teste normal, mas também tasks que você não conhece como <code>central:db:create central:db:schema:load</code>.</p>
<p>Eu defini essas tasks em <code>lib/tasks/db_central.rake</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">task</span> <span class="ss">spec</span><span class="p">:</span> <span class="o">[</span><span class="s2">&#34;central:db:test:prepare&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">namespace</span> <span class="ss">:central</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">namespace</span> <span class="ss">:db</span> <span class="k">do</span> <span class="o">|</span><span class="n">ns</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">task</span> <span class="s2">&#34;environment:set&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:environment:set&#34;</span><span class="o">].</span><span class="n">invoke</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">task</span> <span class="ss">:drop</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:drop&#34;</span><span class="o">].</span><span class="n">invoke</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">task</span> <span class="ss">:create</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:create&#34;</span><span class="o">].</span><span class="n">invoke</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">task</span> <span class="ss">:setup</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:setup&#34;</span><span class="o">].</span><span class="n">invoke</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">task</span> <span class="ss">:reset</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:reset&#34;</span><span class="o">].</span><span class="n">invoke</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">task</span> <span class="ss">:migrate</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:migrate&#34;</span><span class="o">].</span><span class="n">invoke</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">task</span> <span class="ss">:rollback</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:rollback&#34;</span><span class="o">].</span><span class="n">invoke</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">task</span> <span class="ss">:seed</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:seed&#34;</span><span class="o">].</span><span class="n">invoke</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">task</span> <span class="ss">:version</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:version&#34;</span><span class="o">].</span><span class="n">invoke</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">namespace</span> <span class="ss">:schema</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">task</span> <span class="ss">:load</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">        <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:schema:load&#34;</span><span class="o">].</span><span class="n">invoke</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">task</span> <span class="ss">:dump</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">        <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:schema:dump&#34;</span><span class="o">].</span><span class="n">invoke</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">namespace</span> <span class="ss">:test</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">task</span> <span class="ss">:prepare</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">        <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;db:test:prepare&#34;</span><span class="o">].</span><span class="n">invoke</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"># append and prepend proper tasks to all the tasks defined here above</span>
</span></span><span class="line"><span class="cl">    <span class="n">ns</span><span class="o">.</span><span class="n">tasks</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">task</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="n">task</span><span class="o">.</span><span class="n">enhance</span> <span class="o">[</span><span class="s2">&#34;central:set_custom_config&#34;</span><span class="o">]</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">        <span class="no">Rake</span><span class="o">::</span><span class="no">Task</span><span class="o">[</span><span class="s2">&#34;central:revert_to_original_config&#34;</span><span class="o">].</span><span class="n">invoke</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></span><span class="line"><span class="cl">  <span class="n">task</span> <span class="ss">:set_custom_config</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># save current vars</span>
</span></span><span class="line"><span class="cl">    <span class="vi">@original_config</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="ss">env_schema</span><span class="p">:</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;SCHEMA&#39;</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">config</span><span class="p">:</span> <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">dup</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># set config variables for custom database</span>
</span></span><span class="line"><span class="cl">    <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;SCHEMA&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="s2">&#34;db_central/schema.rb&#34;</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">config</span><span class="o">.</span><span class="n">paths</span><span class="o">[</span><span class="s1">&#39;db&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="o">[</span><span class="s2">&#34;db_central&#34;</span><span class="o">]</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">config</span><span class="o">.</span><span class="n">paths</span><span class="o">[</span><span class="s1">&#39;db/migrate&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="o">[</span><span class="s2">&#34;db_central/migrate&#34;</span><span class="o">]</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">config</span><span class="o">.</span><span class="n">paths</span><span class="o">[</span><span class="s1">&#39;db/seeds&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="o">[</span><span class="s2">&#34;db_central/seeds.rb&#34;</span><span class="o">]</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">config</span><span class="o">.</span><span class="n">paths</span><span class="o">[</span><span class="s1">&#39;config/database&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="o">[</span><span class="s2">&#34;config/database_central.yml&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Tasks</span><span class="o">::</span><span class="no">DatabaseTasks</span><span class="o">.</span><span class="n">database_configuration</span> <span class="o">=</span> <span class="no">CM</span><span class="p">(</span><span class="no">Rails</span><span class="o">.</span><span class="n">root</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;config&#34;</span><span class="p">,</span> <span class="s2">&#34;database_central.yml&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="o">.</span><span class="n">File</span><span class="o">.</span><span class="n">read</span>
</span></span><span class="line"><span class="cl">      <span class="o">.</span><span class="n">ERB</span><span class="o">.</span><span class="n">new</span>
</span></span><span class="line"><span class="cl">      <span class="o">.</span><span class="n">result</span>
</span></span><span class="line"><span class="cl">      <span class="o">.</span><span class="n">YAML</span><span class="o">.</span><span class="n">load</span>
</span></span><span class="line"><span class="cl">      <span class="o">.</span><span class="n">unwrap</span><span class="o">.</span><span class="n">freeze</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">task</span> <span class="ss">:revert_to_original_config</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># reset config variables to original values</span>
</span></span><span class="line"><span class="cl">    <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;SCHEMA&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="vi">@original_config</span><span class="o">[</span><span class="ss">:env_schema</span><span class="o">]</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">config</span> <span class="o">=</span> <span class="vi">@original_config</span><span class="o">[</span><span class="ss">:config</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>É assim que defino um namespace de tasks Rake que começam com <code>central:</code>, e cada uma delas conecta no banco secundário descrito no <code>database_central.yml</code>. A sintaxe estranha vem do meu <a href="https://github.com/akitaonrails/chainable_methods"target="_blank" rel="noopener"><code>chainable_methods</code></a>, não dê muita atenção.</p>
<p>O <code>db_central/schema.rb</code> é basicamente uma cópia do <code>spec/support/rails_app/db/schema.rb</code> do gem central-support, com as mesmas tabelas. O runner de specs tanto do gem quanto dessa app secundária só carrega o schema no banco de teste.</p>
<p>Agora que temos os fundamentos básicos para os specs no lugar, podemos focar em como a aplicação em si consome esses models externos.</p>
<p>Começamos adicionando um initializer como <code>config/initializer/db_central.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="no">DB_CENTRAL</span> <span class="o">=</span> <span class="no">CM</span><span class="p">(</span><span class="no">Rails</span><span class="o">.</span><span class="n">root</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;config&#34;</span><span class="p">,</span> <span class="s2">&#34;database_central.yml.sample&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">.</span><span class="n">File</span><span class="o">.</span><span class="n">read</span>
</span></span><span class="line"><span class="cl">  <span class="o">.</span><span class="n">ERB</span><span class="o">.</span><span class="n">new</span>
</span></span><span class="line"><span class="cl">  <span class="o">.</span><span class="n">result</span>
</span></span><span class="line"><span class="cl">  <span class="o">.</span><span class="n">YAML</span><span class="o">.</span><span class="n">load</span>
</span></span><span class="line"><span class="cl">  <span class="o">.</span><span class="n">unwrap</span><span class="o">.</span><span class="n">freeze</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>Nesse caso eu leio do arquivo sample porque, diferente do build de CI, quando eu faço deploy no Heroku eu não tenho um script para copiar o sample para o yaml final. Isso popula a constante <code>DB_CENTRAL</code> com a URL do banco armazenada na variável de ambiente <code>DATABASE_CENTRAL_URL</code> que eu preciso configurar.</p>
<p>Aí crio um arquivo novo chamado <code>app/models/remote_application_record.rb</code> mais ou menos 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">RemoteApplicationRecord</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
</span></span><span class="line"><span class="cl">  <span class="n">establish_connection</span> <span class="no">DB_CENTRAL</span><span class="o">[</span><span class="no">Rails</span><span class="o">.</span><span class="n">env</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="nb">self</span><span class="o">.</span><span class="n">abstract_class</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="k">unless</span> <span class="no">Rails</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">test?</span>
</span></span><span class="line"><span class="cl">    <span class="n">default_scope</span> <span class="o">-&gt;</span> <span class="p">{</span> <span class="n">readonly</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ê cria um novo connection pool para a configuração do banco secundário. Você deve ter esse <code>establish_connection</code> em um único lugar e fazer os models herdarem daqui. O <code>abstract_class = true</code> faz com que o ActiveRecord deixe de tentar carregar de uma tabela com o mesmo nome dessa classe.</p>
<p>Em seguida temos um <code>default_scope</code> travando o model como <code>readonly</code>. A gente quer abrir mão disso no ambiente de teste, porque eu ainda quero deixar o Factory Girl popular o banco de teste com dados falsos para os specs. Mas é uma boa ideia ter isso em produção só para garantir.</p>
<p>Por fim, posso criar todos os models que preciso, como <code>app/models/central/team.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="k">module</span> <span class="nn">Central</span>
</span></span><span class="line"><span class="cl">  <span class="k">class</span> <span class="nc">Team</span> <span class="o">&lt;</span> <span class="no">RemoteApplicationRecord</span>
</span></span><span class="line"><span class="cl">    <span class="nb">self</span><span class="o">.</span><span class="n">table_name</span> <span class="o">=</span> <span class="s1">&#39;teams&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kp">include</span> <span class="no">Central</span><span class="o">::</span><span class="no">Support</span><span class="o">::</span><span class="no">TeamConcern</span><span class="o">::</span><span class="no">Associations</span>
</span></span><span class="line"><span class="cl">    <span class="kp">include</span> <span class="no">Central</span><span class="o">::</span><span class="no">Support</span><span class="o">::</span><span class="no">TeamConcern</span><span class="o">::</span><span class="no">Validations</span>
</span></span><span class="line"><span class="cl">    <span class="kp">include</span> <span class="no">Central</span><span class="o">::</span><span class="no">Support</span><span class="o">::</span><span class="no">TeamConcern</span><span class="o">::</span><span class="no">Scopes</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 partir daqui posso fazer queries Arel normais como <code>Central::Team.not_archived.limit(5)</code>.</p>
<h3>Conclusões<span class="hx:absolute hx:-mt-20" id="conclusões"></span>
    <a href="#conclus%c3%b5es" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Se ainda não viu, dá uma olhada na <a href="http://www.akitaonrails.com/2016/10/03/compartilhando-models-entre-aplicacoes-rails-parte-1"target="_blank" rel="noopener">Parte 1</a> para mais detalhes.</p>
<p>Essa é uma receita simples para compartilhar lógica de model entre uma app Rails principal de leitura e escrita e uma app Rails secundária de só leitura. Elas compartilham a maioria (não todos) dos mesmos models, compartilham a mesma lógica (através de alguns Concerns) e compartilham o mesmo banco.</p>
<p>Nesse caso particular, a abordagem recomendada é criar um <a href="https://devcenter.heroku.com/articles/heroku-postgres-follower-databases"target="_blank" rel="noopener">Follower database</a>, que é como o Heroku chama um banco secundário replicado, e fazer minha aplicação secundária conectar nele (já que ela só precisa de uma fonte somente leitura).</p>
<p>Para cenários mais complicados, você vai precisar de uma solução mais elaborada como uma camada de API HTTP para garantir que apenas uma App gerencie as migrations dos models. Mas a abordagem do Rubygem deve ser &ldquo;boa o suficiente&rdquo; para muitos casos.</p>
<p>Se eu realmente precisar ir por esse caminho, não vai ser difícil transformar esse pequeno gem numa app Rails API completa. Se você nem consegue separar a lógica em Concern, também não vai conseguir separar como APIs, então encare isso como um exercício rápido, um primeiro passo para criar uma <a href="http://programmers.stackexchange.com/questions/184464/what-is-an-anti-corruption-layer-and-how-is-it-used"target="_blank" rel="noopener">Anti Corruption Layer</a>.</p>
<p>E como bônus, considere contribuir com os projetos open source <a href="https://github.com/Codeminer42/cm42-central"target="_blank" rel="noopener">Central</a> e <a href="https://github.com/Codeminer42/cm42-central-support"target="_blank" rel="noopener">central-support</a>. Pretendo construir uma alternativa competitiva ao Pivotal Tracker/Trello e estamos chegando lá!</p>
]]></content:encoded><category>rails</category><category>bancos-de-dados</category><category>arquitetura-de-software</category></item><item><title>Compartilhando models entre aplicações Rails - Parte 1</title><link>https://akitaonrails.github.io/2016/10/03/compartilhando-models-entre-aplicacoes-rails-parte-1/</link><guid isPermaLink="true">https://akitaonrails.github.io/2016/10/03/compartilhando-models-entre-aplicacoes-rails-parte-1/</guid><pubDate>Mon, 03 Oct 2016 15:00:00 -0300</pubDate><description>&lt;p&gt;&lt;em&gt;&amp;ldquo;Cria um microservice e expõe uma API.&amp;rdquo;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Essa seria a resposta rápida se você perguntar para qualquer desenvolvedor como compartilhar lógica de negócio entre aplicações.&lt;/p&gt;
&lt;p&gt;Apesar de fazer sentido em muitas situações, essa resposta deixa a desejar em vários outros casos.&lt;/p&gt;
&lt;p&gt;Meu TL;DR para algumas situações é o seguinte: você pode organizar a lógica dos seus models como &lt;a href="http://api.rubyonrails.org/classes/ActiveSupport/Concern.html"target="_blank" rel="noopener"&gt;ActiveSupport::Concerns&lt;/a&gt; (ou Modules Ruby puros, se preferir) e movê-los para uma Rubygem que suas aplicações podem consumir.&lt;/p&gt;</description><content:encoded><![CDATA[<p><em>&ldquo;Cria um microservice e expõe uma API.&rdquo;</em></p>
<p>Essa seria a resposta rápida se você perguntar para qualquer desenvolvedor como compartilhar lógica de negócio entre aplicações.</p>
<p>Apesar de fazer sentido em muitas situações, essa resposta deixa a desejar em vários outros casos.</p>
<p>Meu TL;DR para algumas situações é o seguinte: você pode organizar a lógica dos seus models como <a href="http://api.rubyonrails.org/classes/ActiveSupport/Concern.html"target="_blank" rel="noopener">ActiveSupport::Concerns</a> (ou Modules Ruby puros, se preferir) e movê-los para uma Rubygem que suas aplicações podem consumir.</p>
<p>Repare que estou falando apenas de Models, não de Controllers ou Views. Para compartilhar essas outras coisas você precisaria de uma Rails Engine completa. Mas em muitos casos que vi a vontade era apenas compartilhar a lógica de negócio entre aplicações, mantendo o front-end separado.</p>
<p>Um pequeno exemplo desse cenário é o projeto open source em que venho trabalhando nas últimas semanas. <a href="https://github.com/Codeminer42/cm42-central"target="_blank" rel="noopener">Central</a>, uma alternativa ao Pivotal Tracker/Trello - caso você se interesse.</p>
<p>Há alguns dias comecei um novo projeto (apenas para uso interno) que precisava consultar os mesmos models do Central. Eu não queria implementar APIs HTTP nesse momento, e a nova aplicação teria seus próprios models com relacionamentos com os models do Central (tratando-os como read-only).</p>
<p>Depois de alguns refactorings, a maioria dos models do Central ficou parecida com este:</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">Team</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
</span></span><span class="line"><span class="cl">  <span class="kp">include</span> <span class="no">Central</span><span class="o">::</span><span class="no">Support</span><span class="o">::</span><span class="no">TeamConcern</span><span class="o">::</span><span class="no">Associations</span>
</span></span><span class="line"><span class="cl">  <span class="kp">include</span> <span class="no">Central</span><span class="o">::</span><span class="no">Support</span><span class="o">::</span><span class="no">TeamConcern</span><span class="o">::</span><span class="no">Validations</span>
</span></span><span class="line"><span class="cl">  <span class="kp">include</span> <span class="no">Central</span><span class="o">::</span><span class="no">Support</span><span class="o">::</span><span class="no">TeamConcern</span><span class="o">::</span><span class="no">Scopes</span>
</span></span><span class="line"><span class="cl">  <span class="kp">include</span> <span class="no">Central</span><span class="o">::</span><span class="no">Support</span><span class="o">::</span><span class="no">TeamConcern</span><span class="o">::</span><span class="no">DomainValidator</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>E tenho essa dependência no <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">gem</span> <span class="s1">&#39;central-support&#39;</span><span class="p">,</span> <span class="ss">github</span><span class="p">:</span> <span class="s1">&#39;Codeminer42/cm42-central-support&#39;</span><span class="p">,</span> <span class="ss">branch</span><span class="p">:</span> <span class="s1">&#39;master&#39;</span><span class="p">,</span> <span class="nb">require</span><span class="p">:</span> <span class="s1">&#39;central/support&#39;</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>Sempre que mudo as concerns, rodo um <code>bundle update central-support</code> nos projetos (esse é o cuidado que você precisa ter para evitar lidar com models desatualizados).</p>
<p>Isso foi possível porque a maioria desses models já estava madura e estável e eu não vou mexer neles com frequência. Eu não recomendo expor dependências instáveis (como gems ou APIs, tanto faz), porque essa é a receita para uma dor de cabeça enorme com breaking changes em cascata por causa de dependências desatualizadas que mudam com frequência demais.</p>
<blockquote>
  <p>Você deve APENAS expor lógica de negócio que esteja razoavelmente estável (mudanças apenas de semana em semana, mais ou menos).</p>

</blockquote>
<p>A jornada toda foi montar uma certa estrutura de Rubygems, organizar os models originais em Concerns (o que não quebra nenhum comportamento), garantir que os specs continuassem passando, e então mover o conteúdo (models e specs) para a nova Rubygem e garantir que os specs passassem por lá.</p>
<p>Foi assim que construí uma dependência open source secundária do Central, chamada <a href="https://github.com/Codeminer42/cm42-central-support"target="_blank" rel="noopener">Central Support</a>. Como muitas gems, o arquivo principal <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/lib/central/support.rb"target="_blank" rel="noopener">lib/central/support.rb</a> é só um monte de &lsquo;requires&rsquo; para carregar todas as dependências.</p>
<p>Então organizei metodicamente a lógica em concerns, como <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/lib/central/support/concerns/team_concern/associations.rb"target="_blank" rel="noopener">lib/central/support/concerns/team_concern/association.rb</a>, que é só a extração das associações de Active Record do model &lsquo;Team&rsquo;.</p>
<p>Recortar do Central, colar no Support. Quando toda a lógica relevante já tinha sido movida, consegui mover o spec inteiro do model Team, basicamente sem alterações, e fazê-lo rodar. Toda vez que eu movia um pedacinho, dava <code>bundle update</code> na gem e rodava a suíte principal de specs para garantir que nada tinha quebrado.</p>
<p>E essa é a parte difícil: montar um sandbox onde essas concerns possam rodar e ser testadas.</p>
<p>Para começar, precisei construir uma aplicação Rails mínima dentro da pasta de specs, em <code>spec/support/rails_app</code>. E ali eu pude colocar models falsos que incluem as concerns que acabei de extrair do Central.</p>
<p>Existe pouca documentação sobre como fazer isso, mas acho que dá para fazer um <code>rails new</code> e começar dali, ou copiar a minha pasta <code>rails_app</code> para ter o mínimo necessário. Meu caso é mais simples porque essa gem não é de propósito geral, então não preciso rodá-la contra versões diferentes do Rails, por exemplo.</p>
<p>Essa app de teste interna precisa ter um <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/spec/support/rails_app/Gemfile"target="_blank" rel="noopener"><code>Gemfile</code></a> cuidadosamente montado:</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">gem</span> <span class="s1">&#39;central-support&#39;</span><span class="p">,</span> <span class="ss">path</span><span class="p">:</span> <span class="no">File</span><span class="o">.</span><span class="n">expand_path</span><span class="p">(</span><span class="s2">&#34;../../../..&#34;</span><span class="p">,</span> <span class="bp">__FILE__</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;devise&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;pg&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;friendly_id&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;foreigner&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">group</span> <span class="ss">:test</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;test-unit&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;rspec-rails&#39;</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>Você não precisa adicionar as gems do <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/central-support.gemspec"target="_blank" rel="noopener">gemspec</a> principal. Mas você pode remover as dependências de desenvolvimento que iriam para o gemspec e mantê-las no Gemfile da app de teste.</p>
<p>Agora, a partir do <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/Gemfile"target="_blank" rel="noopener"><code>Gemfile</code> principal</a> você pode 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="n">source</span> <span class="s1">&#39;https://rubygems.org&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">eval_gemfile</span> <span class="no">File</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="no">File</span><span class="o">.</span><span class="n">dirname</span><span class="p">(</span><span class="bp">__FILE__</span><span class="p">),</span> <span class="s2">&#34;spec/support/rails_app/Gemfile&#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>A maioria dos tutoriais de como construir uma Rubygem coloca uma linha para carregar dependências do gemspec, mas aqui estamos substituindo isso pelo Gemfile da app de teste. Esse é o manifest que será carregado quando rodarmos <code>bundle exec rspec</code>, por exemplo.</p>
<p>Por falar nisso, esse é o <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/spec/rails_helper.rb"target="_blank" rel="noopener"><code>spec/rails_helper.rb</code></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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;RAILS_ENV&#39;</span><span class="o">]</span> <span class="o">||=</span> <span class="s1">&#39;test&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;rails/all&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;factory_girl&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;factory_girl_rails&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;rspec/rails&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;shoulda/matchers&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="sb">`cd spec/support/rails_app ; bin/rails db:drop db:create db:schema:load RAILS_ENV=test`</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;support/rails_app/config/environment&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;support/database_cleaner&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;support/factory_girl&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;support/factories&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;spec_helper&#39;</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 visualizar como tudo se encaixa:</p>
<ul>
<li><code>bundle exec rspec</code> carrega o <code>Gemfile</code> principal</li>
<li>o <code>Gemfile</code> principal carrega a partir do <code>Gemfile</code> da app de teste interna</li>
<li>esse <code>Gemfile</code> da app de teste interna requer o gemspec de <code>../../../..</code> e os grupos de gems de development e test (incluindo Rspec, Factory Girl, etc)</li>
<li>o gemspec requer as dependências de runtime como &ldquo;activesupport&rdquo;, &ldquo;enumerize&rdquo;, etc</li>
<li>finalmente, o <code>rails_helper.rb</code> listado acima é carregado.</li>
</ul>
<p>Ali na linha 11, o runner executa um comando para fazer <code>cd</code> na pasta raiz da app de teste interna e rodar <code>db:schema:load</code>, então você precisa de um <code>db/schema.rb</code> pronto para ser carregado, assim como um <code>config/database.yml</code>.</p>
<p>O <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/spec/spec_helper.rb"target="_blank" rel="noopener"><code>spec/spec_helper.rb</code></a> é mais padrão, com configurações opcionais para test coverage, etc.</p>
<p>Os models dentro da app de teste interna são as partes importantes, porque são o meio para incluir as concerns extraídas em um formato executável. O <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/spec/support/rails_app/app/models/team.rb"target="_blank" rel="noopener">&lsquo;spec/support/rails_app/app/models/team.rb&rsquo;</a> é um exemplo:</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">Team</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
</span></span><span class="line"><span class="cl">  <span class="kp">include</span> <span class="no">Central</span><span class="o">::</span><span class="no">Support</span><span class="o">::</span><span class="no">TeamConcern</span><span class="o">::</span><span class="no">Associations</span>
</span></span><span class="line"><span class="cl">  <span class="kp">include</span> <span class="no">Central</span><span class="o">::</span><span class="no">Support</span><span class="o">::</span><span class="no">TeamConcern</span><span class="o">::</span><span class="no">Validations</span>
</span></span><span class="line"><span class="cl">  <span class="kp">include</span> <span class="no">Central</span><span class="o">::</span><span class="no">Support</span><span class="o">::</span><span class="no">TeamConcern</span><span class="o">::</span><span class="no">Scopes</span>
</span></span><span class="line"><span class="cl">  <span class="kp">include</span> <span class="no">Central</span><span class="o">::</span><span class="no">Support</span><span class="o">::</span><span class="no">TeamConcern</span><span class="o">::</span><span class="no">DomainValidator</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>E com isso, consegui mover os specs sem modificações diretamente do projeto principal (Central), como <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/spec/central/support/team_spec.rb"target="_blank" rel="noopener"><code>spec/central/support/team_spec.rb</code></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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;rails_helper&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">describe</span> <span class="no">Team</span><span class="p">,</span> <span class="ss">type</span><span class="p">:</span> <span class="ss">:model</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">it</span> <span class="p">{</span> <span class="n">is_expected</span><span class="o">.</span><span class="n">to</span> <span class="n">validate_presence_of</span> <span class="ss">:name</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="n">it</span> <span class="p">{</span> <span class="n">is_expected</span><span class="o">.</span><span class="n">to</span> <span class="n">have_many</span> <span class="ss">:enrollments</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="n">it</span> <span class="p">{</span> <span class="n">is_expected</span><span class="o">.</span><span class="n">to</span> <span class="n">have_many</span> <span class="ss">:users</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="n">it</span> <span class="p">{</span> <span class="n">is_expected</span><span class="o">.</span><span class="n">to</span> <span class="n">have_many</span> <span class="ss">:ownerships</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="n">it</span> <span class="p">{</span> <span class="n">is_expected</span><span class="o">.</span><span class="n">to</span> <span class="n">have_many</span> <span class="ss">:projects</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="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>Se você voltar no projeto Central, alguns commits atrás, vai encontrar exatamente esse mesmo arquivo como <a href="https://github.com/Codeminer42/cm42-central/blob/a80eefadf233f4a8c5f88829836c872b199798cd/spec/models/team_spec.rb"target="_blank" rel="noopener"><code>spec/models/team_spec.rb</code></a>. E a principal vantagem dessa abordagem é justamente conseguir mover a maior parte do código para fora do projeto principal, junto com os specs, para uma gem de dependência, sem precisar &ldquo;reescrever&rdquo; nada.</p>
<p>Se eu tivesse que reescrever todo ou um pedaço grande do código, teria sido uma escolha bem mais cara e eu provavelmente teria adiado isso para outro momento e focado em features mais valiosas primeiro.</p>
<p>Essa abordagem está longe de perfeita, mas saiu super barata. Consegui mover toda a lógica de negócio relevante para fora do projeto principal sem precisar reescrever nada além de algum código de &ldquo;fiação&rdquo;. A nova gem de dependência recebeu todas as partes relevantes e os specs, e tudo simplesmente roda.</p>
<p>Então, se você tem 2 ou mais aplicações Rails que poderiam compartilhar os mesmos models, é assim que você pode começar. Claro, sempre tem um monte de ressalvas para manter em mente.</p>
<p>No meu caso, o projeto Central é o que pode ler e escrever no banco. Minha aplicação secundária interna usa os models apenas como read-only. Quando 2 aplicações diferentes escrevem no mesmo banco, você pode ter vários conflitos para lidar.</p>
<p>Essa abordagem é útil quando sua aplicação secundária se parece com um dashboard de Administração, por exemplo. Você precisa ter algumas das mesmas associações, scopes, e até validações para edição eventual, mas isso fica limitado a poucos usuários controlados.</p>
<p>Também é útil quando você está fazendo análise de dados, e novamente pode usar as mesmas associações, scopes, para construir relatórios e dashboards. Essencialmente, quando você precisa de acesso read-only, é decisão fácil.</p>
<p>No <a href="http://www.akitaonrails.com/2016/10/03/compartilhando-models-entre-apps-rails-parte-2"target="_blank" rel="noopener">próximo artigo</a> eu vou explicar como liguei uma aplicação secundária, usando a gem central-support e lidando com 2 bancos de dados diferentes ao mesmo tempo.</p>
]]></content:encoded><category>rails</category><category>arquitetura-de-software</category><category>testes</category></item><item><title>A Comunidade Ruby e a Reputação</title><link>https://akitaonrails.github.io/2016/08/19/a-comunidade-ruby-e-a-reputacao/</link><guid isPermaLink="true">https://akitaonrails.github.io/2016/08/19/a-comunidade-ruby-e-a-reputacao/</guid><pubDate>Fri, 19 Aug 2016 11:02:00 -0300</pubDate><description>&lt;p&gt;Acabei de ler os posts do &lt;a href="http://hawkins.io/2015/05/the-ruby-community-the-next-version/"target="_blank" rel="noopener"&gt;Adam Hawkins&lt;/a&gt; e o apoio do &lt;a href="http://www.alanbradburne.com/Rubys-reputation"target="_blank" rel="noopener"&gt;Alan Bradburne&lt;/a&gt; na Ruby Weekly.&lt;/p&gt;
&lt;p&gt;Como disclaimer, eu não conheço esses caras pessoalmente e respeito o ponto de vista deles. A intenção aqui não é começar um flame war, é só pintar um ponto de vista alternativo.&lt;/p&gt;
&lt;p&gt;Ambos os artigos representam o ponto de vista de muitos desenvolvedores Ruby veteranos dessa comunidade. Vários deles já partiram para outras tecnologias ou pararam de aparecer publicamente.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Acabei de ler os posts do <a href="http://hawkins.io/2015/05/the-ruby-community-the-next-version/"target="_blank" rel="noopener">Adam Hawkins</a> e o apoio do <a href="http://www.alanbradburne.com/Rubys-reputation"target="_blank" rel="noopener">Alan Bradburne</a> na Ruby Weekly.</p>
<p>Como disclaimer, eu não conheço esses caras pessoalmente e respeito o ponto de vista deles. A intenção aqui não é começar um flame war, é só pintar um ponto de vista alternativo.</p>
<p>Ambos os artigos representam o ponto de vista de muitos desenvolvedores Ruby veteranos dessa comunidade. Vários deles já partiram para outras tecnologias ou pararam de aparecer publicamente.</p>
<p>Eu já deixei minha posição bem clara em <a href="http://www.akitaonrails.com/2016/05/23/rails-venceu-o-elefante-na-sala"target="_blank" rel="noopener">&ldquo;Rails has Won: The Elephant in the Room&rdquo;</a>. Mas deixa eu simplificar aqui.</p>
<p>Tem claramente uma facção se formando. Tirando ótimos desenvolvedores como Solnic (dry-rb), Nick (Trailblazer) e Luca (Hanami), a maioria das pessoas está reclamando que o Rails e algumas das peças mais próximas do ecossistema ao redor dele não se encaixam na visão recém-descoberta deles do que seria o &ldquo;bom&rdquo;.</p>
<p>Na visão de mundo deles, um Rails &ldquo;melhor&rdquo; seria muito mais simples, muito mais explícito, composto de bibliotecas super pequenas, com APIs externas super explícitas, e super componibilidade pra desmontar e remontar pra todo lado.</p>
<p>E é aí que mora meu problema: se o DHH cedesse e tomasse esse caminho, isso seria &ldquo;Ruby OFF the Rails&rdquo;. O ponto inteiro de &ldquo;Ruby <strong>ON</strong> Rails&rdquo; existe justamente porque ele é definido como um full stack coerente, opinativo, com bibliotecas grossas (coarse-grained) feitas pra trabalhar juntas em sintonia.</p>
<p>Deixa eu apresentar o verdadeiro Elephant in the Room:</p>
<ul>
<li>
<p>Discussões como essa são só link baits inflamatórios. Não servem pra nada além de criar uma nuvem de animosidade contra alguma coisa, sem apresentar de fato uma alternativa boa e objetiva. Escrever bullet points é fácil; escrever soluções completas e mantê-las já é outro departamento.</p>
</li>
<li>
<p>Existem sim boas alternativas pipocando, como o já mencionado Trailblazer, o Hanami e a coleção de bibliotecas dry-rb, só pra citar algumas. A reclamação é que elas não têm tanta tração agora. Qual a solução? Falar mal do Rails? Escrever artigos chorosos sobre como a vida é injusta? Ou, melhor, escrever mais tutoriais sobre como usar Trailblazer? Gravar screencasts sobre como usar Hanami? Ir a uma conferência e fazer mais palestras sobre como importar dry-rb nos seus projetos?</p>
</li>
<li>
<p>O Ruby on Rails foi feito pra programadores cansados de super componibilidade, super configuração, super explicitação, super bibliotecas fine-grained. Foi feito pra programadores Java saindo direto do pesadelo que foi o J2EE 1.x em 2004, lembram disso? E agora a &ldquo;proposta&rdquo; é voltar pra aquilo? Desperdiçar quantidades enormes de tempo afinando seu mini-stack feito sob medida? A gente já tem isso. Chama Javascript. E deixa eu te falar: configurar a geração atual de <code>package.json</code> me traz memórias muito ruins de <code>pom.xml</code>, <code>hibernate.cfg.xml</code> e <code>struts-config.xml</code>.</p>
</li>
<li>
<p>Iniciantes não estão interessados em soluções fine-grained. A maioria das pessoas esquece como era ser iniciante. Na verdade, eu argumentaria que 80% dos desenvolvedores do mundo se beneficiam muito de uma abordagem como a do Rails. Não é coincidência que muitas plataformas se conformaram a uma abordagem opinativa, convention-over-configuration, depois que ela provou seu valor.</p>
</li>
</ul>
<p>O fato das pessoas estarem reclamando contra o Rails dá a sensação de que a culpa, de algum jeito, é do Rails. Na real, isso é só um reflexo da frustração das próprias pessoas que <strong>falharam</strong> (e estão falhando) em vender as alternativas. Todo mundo quer almoço grátis.</p>
<p>Deixa eu contar uma historinha.</p>
<p>Lá em 2005, eu era o único desenvolvedor Rails que eu conhecia no meu país (Brasil). Eu googlei por aí e achei uns meia dúzia de outros hobbyists fazendo Ruby por diversão, e um par já fazia profissionalmente.</p>
<p>As pessoas zoavam a gente em todo lugar que eu olhava porque achavam que a gente era doido. <em>&ldquo;Lógico que J2EE é o caminho.&rdquo; &ldquo;Lógico que todo projeto deveria seguir a abordagem DDD do Eric Evans.&rdquo; &ldquo;Lógico que todo projeto deveria seguir todos os Design Patterns do Gang of Four, quanto mais melhor.&rdquo; &ldquo;Lógico que a gente deveria ter pacotes de deploy bem isolados, com fronteiras de API bem explícitas entre eles, não importa o quanto isso derrube a produtividade.&rdquo;</em></p>
<p>Já passei por isso antes. Em 2004, eu já tinha mais de dez anos de experiência em programação profissional.</p>
<p>Levei mais dez anos de peregrinação. 9 anos organizando minha própria conferência. Mais de 1.000 posts no blog. Quase 200 palestras, várias delas que paguei do meu próprio bolso pra comprar passagens de ônibus e avião. Botei o dinheiro onde minha boca estava.</p>
<p>A promessa era substituir toda a complexidade e burocracia por uma stack opinativa, na qual a maioria das decisões básicas já estavam tomadas. Ela ficaria fora do nosso caminho pra que pudéssemos focar na parte mais importante: o negócio.</p>
<div class="video-container">
    <video controls>
        <source src="https://s3.us-east-2.amazonaws.com/blip.tv/Java_is_not_evil.mp4" type="video/mp4">
        Seu navegador não suporta a tag video. [Link Direto](https://s3.us-east-2.amazonaws.com/blip.tv/Java_is_not_evil.mp4)
    </video>
</div>
<p>Toda palestra de Rails que eu fiz desde 2006 tinha a seção acima. E com cada pessoa que assistiu qualquer uma das minhas palestras como testemunha, eu sempre disse:</p>
<blockquote>
  <p>&ldquo;Muitos de vocês ouviram que Java é ruim porque é muito complexo e burocrático. E muitos vão tentar vender suas alternativas às custas do Java. Nós não. Nós abraçamos o que Java tem a oferecer. Solr, Elasticsearch e Hadoop são todos feitos em Java. O que vamos fazer? Reescrever tudo em Ruby? Claro que não. Pra Rails Vencer, Java não precisa Perder.&rdquo; (E aí eu colocava fogo na imagem da &ldquo;edição evil&rdquo;, no palco).</p>

</blockquote>
<p>Eu escrevi um artigo em outubro de 2007 chamado <a href="http://www.akitaonrails.com/2007/10/23/para-eu-ganhar-o-outro-precisa-perder"target="_blank" rel="noopener">&ldquo;Para eu ganhar, o outro precisa perder &hellip;&rdquo;</a>, onde expliquei por que esse jeito de pensar é tão tosco. Também me inspirei na famosa palestra de Steve Jobs na Macworld de 1998, onde ele chamou o Bill Gates pro palco, na tela gigante, pra anunciar a colaboração entre eles. Os fanboys da Apple surtaram, ficaram horrorizados com aquela cena, foi como dormir com Satã.</p>
<p>Quase 20 anos depois, a Apple é uma das empresas mais valiosas do mundo. Porque não importava. Jobs deixou a ideologia pura pra trás e virou pragmático. Eles precisavam do endosso da Microsoft, conseguiram, e provaram seus pontos pelos resultados: passaram os anos 90 se masturbando sobre por que o clássico OS 7 a 9 era melhor que o Windows 3.1 até o NT 4. Mas levou dez anos pra provar o valor do OS X ao longo da primeira década do século XXI. Um passo de cada vez, um release atrás do outro, entregando com frequência, em vez de passar anos trancados nos laboratórios até a solução &ldquo;perfeita&rdquo; surgir. Baby steps. Culminou quando eles espremeram o OS X dentro do iOS em 2007.</p>
<p>É assim que se prova um ponto: através de suor e dor, colocando aquilo na rua, vendendo, evangelizando e criando valor real, um pequeno release de cada vez.</p>
<p>E aí aconteceu isto:</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/559/big_mac-school.jpg" alt="Macbooks por todo lado"  loading="lazy" /></p>
<p>Lembram quando a Apple dominou o mundo? Uma década inteira pra voltar de <a href="http://money.cnn.com/1998/10/14/technology/apple/"target="_blank" rel="noopener">sangrar 1 bilhão por ano</a> até virar a <a href="http://fortune.com/2016/02/03/apple-facebook-amazon-google/"target="_blank" rel="noopener">empresa mais valiosa do mundo</a>, cujos efeitos ainda continuam 5 anos depois da morte do Jobs.</p>
<p>Alguns de nós entendemos que engenheiros gostam de se masturbar sobre coisas irrelevantes.</p>
<blockquote>
  <p>&ldquo;- Meu algoritmo roda 0.1 milissegundos mais rápido que o seu&rdquo;.</p>

</blockquote>
<blockquote>
  <p>&ldquo;- Ha, mas meu design tem menos pontos de dependência que o seu!&rdquo;</p>

</blockquote>
<p>O Rails fez pelo Ruby o que a Apple fez pelo Unix e o que a Canonical fez pelas distros baseadas em Linux. As pessoas reclamando do Rails me lembram as pessoas tentando argumentar por que o Archlinux é muito superior ao OS X ou ao Ubuntu, mas é só que as pessoas em geral são burras demais pra perceber esse valor.</p>
<p>E pra alguns, talvez não seja exatamente um elogio estar na mesma liga da Apple ou da Canonical, mas os resultados são inegáveis.</p>
<p>Lembram do lendário &ldquo;blog em 15 minutos&rdquo;? Por que aquilo nos surpreendeu tanto em 2005? Não era o que o Rails podia fazer; era todo o trabalho que a gente não precisava fazer. E o Rails permaneceu fiel a isso até hoje.</p>
<p>Se você é um programador iniciante, o Rails ainda tem muito o que ensinar, e você pode aprender os detalhes depois.</p>
<p>Se você é o programador mediano, o ecossistema do Rails vai te levar do ponto A ao ponto B muito mais rápido, com produtividade e manutenibilidade melhores.</p>
<p>Por que você está reclamando se você é o programador estrela do tech, financiado por um unicórnio?</p>
<p>Programadores reclamões estão esquecendo um dos nossos próprios ditados favoritos: &ldquo;Otimização Prematura é a Raiz de Todo Mal&rdquo;. E como o &ldquo;UNIX way&rdquo; já dizia:</p>
<blockquote>
  <p>A estratégia é, definitivamente:</p>

</blockquote>
<blockquote>
  <p>Faça funcionar,</p>

</blockquote>
<blockquote>
  <p>Depois faça do jeito certo,</p>

</blockquote>
<blockquote>
  <p>e, por fim, faça rápido.</p>

</blockquote>
<p>Não tem nada de errado em ter alternativas ao Rails, mas o bashing contra Rails está ficando muito velho muito rápido. Vender alternativas é difícil, eu sei. Gastei muita sola de sapato nos últimos 10 anos evangelizando Ruby, então eu sei mesmo.</p>
<p>Eu nunca acreditei em almoço grátis. Eu acredito em trabalho duro e direcionado, e essa tem sido minha filosofia nos últimos 25 anos de programação.</p>
]]></content:encoded><category>rails</category><category>ruby</category></item><item><title>Saindo do Pivotal Tracker para o Fulcrum. Bom o Suficiente.</title><link>https://akitaonrails.github.io/2016/08/10/saindo-do-pivotal-tracker-para-o-fulcrum-bom-o-suficiente/</link><guid isPermaLink="true">https://akitaonrails.github.io/2016/08/10/saindo-do-pivotal-tracker-para-o-fulcrum-bom-o-suficiente/</guid><pubDate>Wed, 10 Aug 2016 18:30:00 -0300</pubDate><description>&lt;p&gt;Venho escrevendo aqui no blog sobre como estou montando minha própria infraestrutura interna. Já contei como saí do &lt;a href="http://www.akitaonrails.com/2016/08/03/migrando-para-o-gitlab-sim-vale-a-pena"target="_blank" rel="noopener"&gt;GitHub e Travis para o GitLab&lt;/a&gt;, depois como migramos do &lt;a href="http://www.akitaonrails.com/2016/08/09/moving-away-from-slack-into-rocket-chat-good-enough"target="_blank" rel="noopener"&gt;Slack para o Rocket.chat&lt;/a&gt;, mas a primeira migração de todas é o tema deste artigo.&lt;/p&gt;
&lt;p&gt;Uso o Pivotal Tracker desde 2010 até 2015, e isso porque eu acredito que ele é muito superior ao Trello e outras &lt;a href="http://www.akitaonrails.com/2009/12/16/off-topic-cuidado-com-o-kanban-butt"target="_blank" rel="noopener"&gt;listas de tarefas glorificadas&lt;/a&gt; por aí.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/Codeminer42/cm42-central"target="_blank" rel="noopener"&gt;&lt;img src="https://raw.githubusercontent.com/Codeminer42/cm42-central/master/doc/cm42-central-screenshot.png" alt="Cm42-Central" loading="lazy" /&gt;&lt;/a&gt;&lt;/p&gt;</description><content:encoded><![CDATA[<p>Venho escrevendo aqui no blog sobre como estou montando minha própria infraestrutura interna. Já contei como saí do <a href="http://www.akitaonrails.com/2016/08/03/migrando-para-o-gitlab-sim-vale-a-pena"target="_blank" rel="noopener">GitHub e Travis para o GitLab</a>, depois como migramos do <a href="http://www.akitaonrails.com/2016/08/09/moving-away-from-slack-into-rocket-chat-good-enough"target="_blank" rel="noopener">Slack para o Rocket.chat</a>, mas a primeira migração de todas é o tema deste artigo.</p>
<p>Uso o Pivotal Tracker desde 2010 até 2015, e isso porque eu acredito que ele é muito superior ao Trello e outras <a href="http://www.akitaonrails.com/2009/12/16/off-topic-cuidado-com-o-kanban-butt"target="_blank" rel="noopener">listas de tarefas glorificadas</a> por aí.</p>
<p><a href="https://github.com/Codeminer42/cm42-central"target="_blank" rel="noopener"><img src="https://raw.githubusercontent.com/Codeminer42/cm42-central/master/doc/cm42-central-screenshot.png" alt="Cm42-Central"  loading="lazy" /></a></p>
<h3>O Dilema do Custo<span class="hx:absolute hx:-mt-20" id="o-dilema-do-custo"></span>
    <a href="#o-dilema-do-custo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Como em todos os outros relatos, vou começar pelo argumento do custo. O Pivotal Tracker tem <a href="http://www.pivotaltracker.com/why-tracker/pricing/"target="_blank" rel="noopener">preço</a> baseado na quantidade de usuários.</p>
<p>Se você tem um time pequeno e poucos clientes externos, um plano básico de 10 usuários já resolve, e você vai pagar apenas USD 420 por ano. Simplesmente pague e pronto.</p>
<p>Se você tem até 25 usuários, ainda continua barato, e sai por USD 1.800 por ano. Vale muito a pena.</p>
<p>Mesmo se você tiver 50 usuários, o plano máximo, você vai pagar USD 3.600 por ano. Acima disso, precisa entrar em contato direto com eles.</p>
<p>O custo está longe de ser proibitivo e, se você puder pagar, meu conselho é que pague.</p>
<p>No meu caso, a estratégia é ter controle total sobre nossos dados e conhecimento, e por isso prefiro ter a propriedade do serviço.</p>
<p>E como o Fulcrum estava &ldquo;quase&rdquo; bom o suficiente, resolvi testar e ver se conseguíamos evoluí-lo com o tempo. Acredito que, mesmo estando ainda muito longe do Tracker completo, o que temos hoje já atende à maior parte dos nossos projetos.</p>
<p>Deixa eu repetir: provavelmente você não vai ganhar nada com isso se seu time for pequeno. O Pivotal Tracker continua sendo, até hoje, a melhor ferramenta de gestão de projetos para times de desenvolvimento de software, e você deveria testá-lo primeiro, se acostumar com o fluxo dele (como vou explicar abaixo), e só depois decidir se quer assumir o controle e instalar a alternativa open source.</p>
<p>Tem o fator custo, claro, mas ele não é o incentivo principal para essa mudança.</p>
<h3>As Funcionalidades Importantes<span class="hx:absolute hx:-mt-20" id="as-funcionalidades-importantes"></span>
    <a href="#as-funcionalidades-importantes" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Pesquisando um pouco, encontrei o projeto Fulcrum. É um projeto Rails muito simples, muito bare-bone, que imita as funcionalidades mais importantes do Tracker.</p>
<p>Qualquer ferramenta competente de gestão de projetos <strong>precisa</strong> ter, no mínimo, 4 pilares:</p>
<ul>
<li>Stories (com threads de discussão, upload de arquivos, tags, organização em Epics e busca)</li>
<li>Estimativa numérica adequada (story points, tamanho proporcional para as stories)</li>
<li>Sprints com time-box (períodos curtos e fixos de tempo)</li>
<li>Velocity (a produtividade real de um determinado time em um determinado projeto)</li>
</ul>
<p>Qualquer ferramenta que tenha todos os itens acima já é útil.</p>
<p>O próprio Fulcrum tem quase tudo. Fizemos um fork interno do projeto e começamos a adicionar algumas das funcionalidades que faltavam. Já estamos usando ele há mais ou menos 1 ano e tem sido bom o suficiente para nossa rotina. Ainda há alguns bugs e funcionalidades faltando que pretendemos incluir.</p>
<p>Se você se interessar, estou disponibilizando esse fork <a href="https://github.com/Codeminer42/cm42-central"target="_blank" rel="noopener">publicamente</a> para cumprir a licença Affero GPL.</p>
<p>Qualquer contribuição é bem-vinda.</p>
<h3>Sobre Gestão Ágil Adequada<span class="hx:absolute hx:-mt-20" id="sobre-gestão-ágil-adequada"></span>
    <a href="#sobre-gest%c3%a3o-%c3%a1gil-adequada" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Desviando um pouco do assunto, quero explicar por que esse tipo de ferramenta (no estilo Tracker) é a melhor opção quando comparada ao que chamei de &ldquo;listas de tarefas glorificadas&rdquo;.</p>
<p>Os princípios são muito simples:</p>
<ul>
<li>Todo projeto precisa começar com o backlog mais completo possível (a incerteza está no seu pico no começo, e vão faltar detalhes, mas é importante ter uma visão do que significa &ldquo;minimamente completo&rdquo; e a ordem de importância de cada story, uma &ldquo;prioridade&rdquo; adequada).
<ul>
<li>O nível de detalhe em si não importa. Uma story precisa ter informação suficiente para que qualquer desenvolvedor do time consiga implementar as tarefas necessárias. Caso contrário, ela deve ser marcada como &ldquo;bloqueada&rdquo;, para que o Product Owner saiba que precisa adicionar mais detalhes, e nenhum desenvolvedor encosta nela até que a tag seja removida.</li>
</ul>
</li>
<li>Todas as stories <strong>precisam ser estimadas</strong> (falo mais sobre isso na próxima seção). A unidade realmente não importa. Você pode pensar em horas, em frações de um dia ou no que ela realmente é: uma proporção. Uma story de tamanho &ldquo;2&rdquo; deveria ter aproximadamente metade da complexidade de uma story de tamanho &ldquo;4&rdquo;. É só isso. Até stories &ldquo;bloqueadas&rdquo; devem receber tamanho.</li>
<li>Projetos precisam ser divididos em Sprints com tempo fixo e duração fixa, para que o time tenha a oportunidade de reavaliar frequentemente. É aqui que a estimativa pode ser refinada, onde mais níveis de detalhe nas stories devem ser exigidos.</li>
<li>Bugs não podem ter estimativa numérica. Eles servem justamente para puxar a velocity do time para baixo, e são um indicativo de entregas mal feitas.</li>
<li>Velocity é a quantidade real de story points entregues até o fim de uma Sprint.
<ul>
<li>O total de story points de todas as stories do backlog do projeto dividido pela velocity do time dá uma &ldquo;Data de Término&rdquo; estimada por alto. Isso é importante, porque é assim que você sabe quando determinados grupos de funcionalidades serão entregues ao longo do tempo, e é isso que o Product Owner precisa saber para priorizar stories, simplificar stories ou até cortar funcionalidades desnecessárias.</li>
<li>A única responsabilidade do Product Owner é prestar contas pelo Retorno sobre o Investimento das funcionalidades implementadas. E a única forma de medir custo-benefício é saber &ldquo;quanto&rdquo; uma funcionalidade vai &ldquo;custar&rdquo; em termos de story points divididos pela velocity.</li>
</ul>
</li>
</ul>
<p>O que a ferramenta vai expor:</p>
<ul>
<li>Se um backlog tem muitas stories &ldquo;bloqueadas&rdquo; ou muitas stories entregues sem aceitação, isso é sintoma de um Product Owner que não está fazendo o trabalho dele.</li>
<li>Se a velocity varia demais entre sprints, isso é sintoma ou de stories mal escritas (e portanto desenvolvedores com dificuldade de entender o que fazer), ou de desenvolvedores sem concentração no trabalho e entregando resultados inconsistentes.</li>
<li>Velocities inconsistentes com muitos bugs a cada sprint são sintoma de desenvolvedores que não estão fazendo código com qualidade adequada (testes automatizados para evitar regressões, por exemplo).</li>
<li>Um &ldquo;bug&rdquo; não é a mesma coisa que o Product Owner mudar de ideia sobre uma story e rejeitá-la. O time não deve implementar mudanças não documentadas. A aceitação está amarrada ao que a story diz. O Product Owner precisa mudar de ideia antes da story ser implementada ou criar uma nova story para adicionar a mudança (e, portanto, adicionar mais custo ao projeto; não existe almoço grátis).</li>
<li>Stories no &ldquo;Chilly Bin&rdquo; (no Fulcrum, ou &ldquo;Ice Box&rdquo; no Tracker) são &ldquo;lista de desejos&rdquo; e nunca serão implementadas até o Product Owner movê-las para o backlog.</li>
<li>A velocity deveria ser aproximadamente a mesma entre sprints. Picos ou vales na velocity são sintoma de um problema que precisa ser atacado na próxima sprint. É assim que um projeto evolui para melhor ao longo do tempo.</li>
</ul>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/557/big_Cm-Fulcrum_-_CM42_Central.png" alt="Velocity"  loading="lazy" /></p>
<h3>Sobre Estimativa<span class="hx:absolute hx:-mt-20" id="sobre-estimativa"></span>
    <a href="#sobre-estimativa" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Muita gente se preocupa demais com &ldquo;estimativa correta&rdquo; a ponto de criar tanta tensão que os desenvolvedores começam a gritar por <a href="http://www.akitaonrails.com/en/2013/10/07/off-topic-desmontando-o-noestimates/"target="_blank" rel="noopener">&ldquo;no estimates&rdquo;</a>. Isso é loucura. Por isso &ldquo;estimativa&rdquo; é uma palavra diferente de &ldquo;previsão&rdquo;: uma é incerta, um chute de grosso calibre, a outra é exata.</p>
<blockquote>
  <p>Não existe estimativa &ldquo;correta&rdquo;.</p>

</blockquote>
<p>Eu diria que em muitos projetos, se as stories estiverem minimamente descritas (ou seja, um desenvolvedor consegue implementar o que está escrito), poderíamos estimar todas como tamanho &ldquo;1&rdquo; e esquecer disso.</p>
<p>Rodamos 2 ou 3 sprints (normalmente uma sprint deveria ser uma semana de trabalho - 5 dias de no máximo 8 horas, nem mais, nem menos) e medimos quantas stories foram entregues. E essa é a velocity.</p>
<p>Agora podemos ver quantas stories temos até o fim do projeto e dividir por essa velocity. E esse é o número de semanas que vamos levar para terminar o projeto.</p>
<blockquote>
  <p>Não há necessidade de estimativa por consenso.</p>

</blockquote>
<p>Consenso em estimativas normalmente é desperdício de tempo. Estimativa é apenas a constatação de que é difícil escrever cada story no exatamente mesmo tamanho, e por isso adicionamos proporções a elas (uma story tamanho 2 é metade do tamanho de uma story tamanho 4).</p>
<p>Mas se um projeto tem 3 meses ou mais, menos as proporções importam (a menos que você caia na anti-prática de ter stories muito desproporcionais, algumas tamanho 1 e muitas tamanho 5, 8 ou até 10, por exemplo). Mas se você ficar abaixo de tamanho 5 para toda story, então quanto maior o projeto, menos as estimativas individuais importam. A velocity vai diluir as diferenças ao longo do tempo.</p>
<p>A tarefa do Project Manager, do Product Owner, do time em geral, é ficar de olho no total de pontos dividido por essa velocity, de onde sai a data estimada de término do projeto. E todos podem negociar, com antecedência, quais stories podem ser feitas, quais não podem, mudar prioridades, remover stories ou simplificar stories.</p>
<blockquote>
  <p>Não perca tempo tentando criar fórmulas para derivar consenso de estimativa.</p>

</blockquote>
<p>Se você administra a Velocity, então você administra o projeto inteiro. Esse é o cerne de toda metodologia Ágil adequada. Todo o resto é acessório a essa finalidade.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/556/big_Heroleads_-_CM42_Central.png" alt="Velocity"  loading="lazy" /></p>
<p>Tentar aumentar a velocity artificialmente é um dos vários pecados. Você não consegue. É melhor investir tempo adicionando mais detalhes a uma story, ou simplificando uma story, ou decidindo que algumas stories simplesmente não são necessárias para entregar um produto decente no final.</p>
<p>Simulações de Monte Carlo, técnicas Six Sigma, conceitos de Kanban, nada disso importa. Todas as técnicas da indústria tradicional foram pensadas para linhas de produção de fábricas, onde o tempo de cada tarefa é bem definido e você administra a variância, a exceção. É um mundo Gaussiano, Normal.</p>
<p>Em projetos de software, ou qualquer trabalho artesanal e de pesquisa, você administra expectativas sobre resultados com base em marcos e cortando excessos e incertezas. É um mundo Pareto, onde não existe &ldquo;média&rdquo;, não existe meio, não existe variância.</p>
<p>Em um mundo <strong>Pareto</strong>, tudo está interconectado, e nada é estatisticamente independente (um requisito para Gaussianas). É portanto 80/20: onde 20% do projeto rende 80% dos resultados, onde 20% do escopo pode ser cortado.</p>
<p>Qualquer coisa que não seja <strong>&ldquo;estatisticamente independente&rdquo;</strong> (como jogar uma moeda) não pode ser tirada a média, não cabe em uma distribuição Normal. Provavelmente é uma Pareto, Power Law, Zipf, Exponencial, ou até uma Poisson, mas não uma Gaussiana. As partes de um software estão todas entrelaçadas em uma rede, elas não são eventos independentes, não podem ser reorganizadas aleatoriamente, elas não são facilmente intercambiáveis.</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>Este artigo é bem curto e eu posso expandir cada item acima em seu próprio artigo, bem longo. Me diz nos comentários se eu devo, e quais pontos levantam mais dúvidas.</p>
<p>Venho refinando essas técnicas ao longo dos <a href="http://www.akitaonrails.com/agile"target="_blank" rel="noopener">últimos 10 anos</a>, e pelo menos 5 deles dentro das restrições de uma ferramenta como o Tracker. E acredito que consegui chegar ao conjunto mínimo de princípios que levam a uma gestão adequada, longe de superstição, fantasia ou ilusão que você vê por aí hoje em dia.</p>
<p>Projetos sofrem quando as recomendações acima não são seguidas:</p>
<ul>
<li>Product Owners não cumprem suas responsabilidades: falham em adicionar detalhes às stories, negligenciam funcionalidades entregues e não as testam direito, não participam.</li>
<li>Backlogs estão incompletos, com falta de detalhe, falta de estimativa, falta de priorização.</li>
<li>Desenvolvedores não começam nem terminam suas tarefas direito, com qualidade de código adequada.</li>
<li>Tempo é desperdiçado em bikeshedding em vez de entrega.</li>
</ul>
<p>Nosso fork do Fulcrum, chamado <a href="https://github.com/Codeminer42/cm42-central"target="_blank" rel="noopener">&ldquo;Central&rdquo;</a>, está longe de ser perfeito, mas funciona bem o suficiente e nos dá controle para continuar adicionando funcionalidades que tornam nossa rotina mais confortável, e estamos apenas começando.</p>
<p>Espero que essa técnica e essa ferramenta sejam úteis para mais times, e adoraria ouvir o feedback de quem se interessar.</p>
]]></content:encoded><category>agile</category><category>rails</category><category>reviews</category></item><item><title>Pequeno Experimento com Rails sobre JRuby 9.x no Heroku</title><link>https://akitaonrails.github.io/2016/06/03/pequeno-experimento-com-rails-sobre-jruby-9-x-no-heroku/</link><guid isPermaLink="true">https://akitaonrails.github.io/2016/06/03/pequeno-experimento-com-rails-sobre-jruby-9-x-no-heroku/</guid><pubDate>Fri, 03 Jun 2016 18:12:00 -0300</pubDate><description>&lt;p&gt;Depois de brincar com linguagens diferentes de Ruby (como &lt;a href="https://akitaonrails.github.io/elixir"&gt;Elixir&lt;/a&gt; ou &lt;a href="https://akitaonrails.github.io/crystal"&gt;Crystal&lt;/a&gt;), chegou a hora de voltar para o Ruby e ver como o JRuby está se saindo nos dias de hoje.&lt;/p&gt;
&lt;p&gt;O JRuby sempre perseguiu o objetivo de ser um substituto competente para o MRI. Se você não acompanhou o que andou rolando, o JRuby mudou o esquema de versões quando saiu da série 1.7 para a série 9.x. Vale a pena ler [este post](blog post) que explica isso.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Depois de brincar com linguagens diferentes de Ruby (como <a href="/elixir">Elixir</a> ou <a href="/crystal">Crystal</a>), chegou a hora de voltar para o Ruby e ver como o JRuby está se saindo nos dias de hoje.</p>
<p>O JRuby sempre perseguiu o objetivo de ser um substituto competente para o MRI. Se você não acompanhou o que andou rolando, o JRuby mudou o esquema de versões quando saiu da série 1.7 para a série 9.x. Vale a pena ler [este post](blog post) que explica isso.</p>
<p>Em resumo, se você quer compatibilidade com o MRI 2.2, precisa usar o JRuby 9.0.x; e se quer compatibilidade com o MRI 2.3, precisa usar a série 9.1.x. A versão mais atual é a 9.1.2.0. Para qualquer coisa anterior, dá pra consultar esta <a href="https://devcenter.heroku.com/articles/ruby-support#ruby-versions"target="_blank" rel="noopener">tabela de versões</a> da documentação do Heroku.</p>
<p>Existem algumas recomendações importantes também:</p>
<ul>
<li>
<p>O ideal é usar Rails 4.2. Tente ficar pelo menos acima de 4.0, e você pode ligar o <code>config.threadsafe!</code> por padrão no arquivo &ldquo;config/environments/production.rb&rdquo;. Para entender o assunto, dá uma olhada na <a href="http://tenderlovemaking.com/2012/06/18/removing-config-threadsafe.html"target="_blank" rel="noopener">excelente explicação do Tenderlove</a>.</p>
</li>
<li>
<p>Se você está fazendo deploy no Heroku, esquece o dyno gratuito ou o 1X, que só te dá 512Mb de RAM. Apesar disso ser suficiente para a maioria das aplicações Rails pequenas (mesmo com 2 ou 3 workers concorrentes do Puma), eu percebi que até as menores apps facilmente passam disso. Então o mínimo que você deve considerar é o dyno 2X. Em qualquer configuração de servidor, pense sempre em mais de 1Gb de RAM.</p>
</li>
</ul>
<h3>Gems<span class="hx:absolute hx:-mt-20" id="gems"></span>
    <a href="#gems" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Existem várias gems com extensões em C que simplesmente não funcionam. Algumas têm substitutas drop-in, outras não. Dá uma olhada no <a href="https://github.com/jruby/jruby/wiki/C-Extension-Alternatives"target="_blank" rel="noopener">Wiki deles</a> para ver a lista de casos.</p>
<p>No meu pequeno aplicativo de exemplo - que nada mais é do que um site de conteúdo apoiado em um banco Postgresql, com ActiveAdmin para gerenciar o conteúdo, RMagick + Paperclip (sim, é uma app antiga) para upload e redimensionamento de imagens - não havia muito o que mudar. As partes importantes do meu &ldquo;Gemfile&rdquo; acabaram ficando 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">source</span> <span class="s1">&#39;https://rubygems.org&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">ruby</span> <span class="s1">&#39;2.3.0&#39;</span><span class="p">,</span> <span class="ss">:engine</span> <span class="o">=&gt;</span> <span class="s1">&#39;jruby&#39;</span><span class="p">,</span> <span class="ss">:engine_version</span> <span class="o">=&gt;</span> <span class="s1">&#39;9.1.1.0&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ruby &#39;2.3.1&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;rails&#39;</span><span class="p">,</span> <span class="s1">&#39;~&gt; 4.2.6&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;devise&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;haml&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;puma&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;rack-attack&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;rack-timeout&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;rakismet&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Database</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;pg&#39;</span><span class="p">,</span> <span class="ss">platforms</span><span class="p">:</span> <span class="ss">:ruby</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;activerecord-jdbcpostgresql-adapter&#39;</span><span class="p">,</span> <span class="ss">platforms</span><span class="p">:</span> <span class="ss">:jruby</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Cache</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="s2">&#34;actionpack-action_caching&#34;</span><span class="p">,</span> <span class="ss">github</span><span class="p">:</span> <span class="s2">&#34;rails/actionpack-action_caching&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Admin</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;activeadmin&#39;</span><span class="p">,</span> <span class="ss">github</span><span class="p">:</span> <span class="s1">&#39;activeadmin&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;active_skin&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Assets</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;therubyracer&#39;</span><span class="p">,</span> <span class="ss">platforms</span><span class="p">:</span> <span class="ss">:ruby</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;therubyrhino&#39;</span><span class="p">,</span> <span class="ss">platforms</span><span class="p">:</span> <span class="ss">:jruby</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;asset_sync&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;jquery-ui-rails&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;sass-rails&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;uglifier&#39;</span><span class="p">,</span>     <span class="s1">&#39;&gt;= 1.3.0&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;coffee-rails&#39;</span><span class="p">,</span> <span class="s1">&#39;~&gt; 4.0.0&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;compass-rails&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;sprockets&#39;</span><span class="p">,</span> <span class="s1">&#39;~&gt;2.11.0&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Image Processing</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;paperclip&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;fog&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;rmagick&#39;</span><span class="p">,</span> <span class="ss">platforms</span><span class="p">:</span> <span class="ss">:ruby</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;rmagick4j&#39;</span><span class="p">,</span> <span class="ss">platforms</span><span class="p">:</span> <span class="ss">:jruby</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">group</span> <span class="ss">:test</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;shoulda-matchers&#39;</span><span class="p">,</span> <span class="nb">require</span><span class="p">:</span> <span class="kp">false</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">group</span> <span class="ss">:test</span><span class="p">,</span> <span class="ss">:development</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s2">&#34;sqlite3&#34;</span><span class="p">,</span> <span class="ss">platforms</span><span class="p">:</span> <span class="ss">:ruby</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s2">&#34;activerecord-jdbcsqlite3-adapter&#34;</span><span class="p">,</span> <span class="ss">platforms</span><span class="p">:</span> <span class="ss">:jruby</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Pretty printed test output</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;turn&#39;</span><span class="p">,</span> <span class="nb">require</span><span class="p">:</span> <span class="kp">false</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;jasmine&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;pry-rails&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;rspec&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;rspec-rails&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;capybara&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;poltergeist&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;database_cleaner&#39;</span><span class="p">,</span> <span class="s1">&#39;&lt; 1.1.0&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;letter_opener&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;dotenv-rails&#39;</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">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;rails_12factor&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;rack-cache&#39;</span><span class="p">,</span> <span class="nb">require</span><span class="p">:</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;memcachier&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;newrelic_rpm&#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>Repare como pareei as gems para as plataformas <code>:ruby</code> e <code>:jruby</code>. Depois dessa mudança e de rodar <code>bundle install</code>, executei minha suíte de Rspec e - felizmente - todos os testes passaram de primeira sem precisar de nenhuma alteração adicional! O resultado vai variar dependendo da complexidade da sua aplicação, então tenha seus testes prontos.</p>
<h3>Puma<span class="hx:absolute hx:-mt-20" id="puma"></span>
    <a href="#puma" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>No caso do Puma, a configuração é um pouco mais delicada, a minha ficou 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">web_concurrency</span> <span class="o">=</span> <span class="nb">Integer</span><span class="p">(</span><span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;WEB_CONCURRENCY&#39;</span><span class="o">]</span> <span class="o">||</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">web_concurrency</span> <span class="o">&gt;</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">  <span class="n">workers</span> <span class="n">web_concurrency</span>
</span></span><span class="line"><span class="cl">  <span class="n">preload_app!</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">threads_count</span> <span class="o">=</span> <span class="nb">Integer</span><span class="p">(</span><span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;RAILS_MAX_THREADS&#39;</span><span class="o">]</span> <span class="o">||</span> <span class="mi">5</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">threads</span> <span class="n">threads_count</span><span class="p">,</span> <span class="n">threads_count</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">rackup</span>      <span class="no">DefaultRackup</span>
</span></span><span class="line"><span class="cl"><span class="n">port</span>        <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PORT&#39;</span><span class="o">]</span>     <span class="o">||</span> <span class="mi">3000</span>
</span></span><span class="line"><span class="cl"><span class="n">environment</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;RACK_ENV&#39;</span><span class="o">]</span> <span class="o">||</span> <span class="s1">&#39;development&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">on_worker_boot</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Worker specific setup for Rails 4.1+</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># See: https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#on-worker-boot</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">establish_connection</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>É um pouco diferente do que você vai encontrar em outras documentações. A parte importante é desligar workers e preload_app quando estiver carregando sobre JRuby. Caso contrário ele reclama e quebra. No meu deploy original com MRI estou usando um dyno 1X pequeno e posso deixar <code>WEB_CONCURRENCY=2</code> e <code>RAILS_MAX_THREADS=5</code>, mas no deploy com JRuby coloquei <code>WEB_CONCURRENCY=1</code> (para desligar os workers) e <code>RAILS_MAX_THREADS=16</code> (porque parto do princípio que o JRuby aguenta mais multithreading do que o MRI).</p>
<p>Outro ponto importante: muita gente ainda assume que o MRI não consegue tirar proveito de threads paralelas nativas por causa do tal GIL (Global Interpreter Lock), mas isso é uma meia verdade. O MRI Ruby consegue paralelizar threads em esperas de I/O. Então, se uma parte da sua app está esperando o banco processar e devolver linhas, por exemplo, outra thread pode assumir e fazer outra coisa, em paralelo. O modelo tem alguma concorrência, ainda que limitada, então configurar uma quantidade pequena de threads para o Puma pode ajudar um pouco.</p>
<p>Não esqueça de configurar o Pool size para pelo menos o mesmo número de <code>RAILS_MAX_THREADS</code>. Você pode usar o <code>config/database.yml</code> para Rails 4.1+ ou adicionar um initializer. Siga a <a href="https://devcenter.heroku.com/articles/concurrency-and-database-connections#threaded-servers"target="_blank" rel="noopener">documentação do Heroku</a> para ver como fazer.</p>
<h3>Benchmarks Iniciais<span class="hx:absolute hx:-mt-20" id="benchmarks-iniciais"></span>
    <a href="#benchmarks-iniciais" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Bom, consegui fazer o deploy de uma versão secundária em JRuby da minha app Rails original baseada em MRI.</p>
<p>A app original ainda está em um Hobby Dyno, com 512Mb de RAM. A app secundária precisou de um Standard 1X Dyno, com 1Gb de RAM.</p>
<p>A app em si é super simples e como eu uso cache - <a href="http://www.akitaonrails.com/2015/05/20/dynamic-site-as-fast-as-a-static-generated-one-with-raptor"target="_blank" rel="noopener">como você sempre deveria fazer!</a> -, o tempo de resposta é bem baixo, na casa das dezenas de milissegundos.</p>
<p>Tentei usar a ferramenta <a href="https://github.com/rakyll/boom"target="_blank" rel="noopener">Boom</a> para fazer benchmark das requisições. Aqueci bastante a versão JRuby, rodei os benchmarks várias vezes e até usei o Loader.io para colocar mais pressão.</p>
<p>Estou rodando este teste:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>$ boom -n 200 -c 50 http://foo-my-site/</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 versão MRI se comporta assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Summary:
  Total:    16.4254 secs
  Slowest:  9.0785 secs
  Fastest:  0.8362 secs
  Average:  2.6551 secs
  Requests/sec: 12.1763
  Total data:   28837306 bytes
  Size/request: 144186 bytes

Status code distribution:
  [200] 200 responses

Response time histogram:
  0.836 [1] |
  1.660 [57]    |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  2.485 [57]    |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  3.309 [33]    |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  4.133 [22]    |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  4.957 [16]    |∎∎∎∎∎∎∎∎∎∎∎
  5.782 [6] |∎∎∎∎
  6.606 [3] |∎∎
  7.430 [1] |
  8.254 [3] |∎∎
  9.079 [1] |

Latency distribution:
  10% in 1.2391 secs
  25% in 1.5910 secs
  50% in 2.1974 secs
  75% in 3.4327 secs
  90% in 4.5580 secs
  95% in 5.6727 secs
  99% in 8.1567 secs</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 a versão JRuby se comporta assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Summary:
  Total:    15.5784 secs
  Slowest:  7.4106 secs
  Fastest:  0.5770 secs
  Average:  2.3224 secs
  Requests/sec: 12.8383
  Total data:   28848475 bytes
  Size/request: 144242 bytes

Status code distribution:
  [200] 200 responses

Response time histogram:
  0.577 [1] |
  1.260 [23]    |∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  1.944 [62]    |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  2.627 [51]    |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  3.310 [24]    |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  3.994 [26]    |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  4.677 [8] |∎∎∎∎∎
  5.361 [1] |
  6.044 [0] |
  6.727 [1] |
  7.411 [3] |∎

Latency distribution:
  10% in 1.1599 secs
  25% in 1.5154 secs
  50% in 2.0781 secs
  75% in 2.8909 secs
  90% in 3.7409 secs
  95% in 4.2556 secs
  99% in 6.7685 secs</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 geral, eu diria que estão por aí, mais ou menos no mesmo patamar. Como esse não é um processamento particularmente intensivo de CPU, e a maior parte do tempo é gasta atravessando a stack do Rails e batendo no Memcachier para puxar de volta sempre o mesmo conteúdo, talvez seja justo esperar resultados parecidos.</p>
<p>Por outro lado, não tenho certeza se estou usando a ferramenta da melhor maneira possível. O log fala algo assim para cada requisição:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>source=rack-timeout id=c8ad5f0c-b5c1-47ec-b88b-3fc597ab01dc wait=29ms timeout=20000ms state=ready
Started GET &#34;/&#34; for XXX.35.10.XXX at 2016-06-03 18:54:34 &#43;0000
Processing by HomeController#home as HTML
Read fragment views/radiant-XXXX-XXXXX.herokuapp.com/en (6.0ms)
Completed 200 OK in 8ms (ActiveRecord: 0.0ms)
source=rack-timeout id=c8ad5f0c-b5c1-47ec-b88b-3fc597ab01dc wait=29ms timeout=20000ms service=19ms state=completed
source=rack-timeout id=a5389dc4-9a1a-46b7-a1e5-53f334ca0941 wait=35ms timeout=20000ms state=ready

Started GET &#34;/&#34; for XXX.35.10.XXX at 2016-06-03 18:54:36 &#43;0000
Processing by HomeController#home as HTML
Read fragment views/radiant-XXXX-XXXXX.herokuapp.com/en (6.0ms)
Completed 200 OK in 9ms (ActiveRecord: 0.0ms)
source=rack-timeout id=a5389dc4-9a1a-46b7-a1e5-53f334ca0941 wait=35ms timeout=20000ms service=21ms state=completed
at=info method=GET path=&#34;/&#34; host=radiant-XXXX-XXXXX.herokuapp.com request_id=a5389dc4-9a1a-46b7-a1e5-53f334ca0941 fwd=&#34;XXX.35.10.XXX&#34; dyno=web.1 connect=1ms service=38ms status=200 bytes=144608</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>Os tempos reportados pelo Boom são bem maiores (2 segundos?) do que os tempos de processamento nos logs (10ms?). Mesmo considerando algum overhead do roteador e por aí vai, ainda é uma diferença grande, fico me perguntando se as requisições estão ficando tempo demais na fila porque a app não está conseguindo responder mais das requisições concorrentes.</p>
<p>A quantidade de requisições dividida pelo número de conexões concorrentes vai puxar a performance e o throughput geral para baixo se você aumentar demais, e eu não consegui passar muito de 14 rpm com essa configuração.</p>
<p>Se você tem mais experiência com benchmark de http e consegue identificar algo errado que estou fazendo aqui, por favor, me avise nos comentários abaixo.</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 JRuby continua evoluindo, e você pode se beneficiar se já tiver um conjunto grande de Dynos ou de servidores robustos. Para aplicações pequenas ou médias, eu não recomendaria.</p>
<p>Já vi melhorias de várias ordens de magnitude em casos de uso específicos (acho que era um endpoint de API com tráfego muito alto). Esse caso particular que testei provavelmente não é o ponto forte dele, e mudar do MRI para o JRuby não me deu muita vantagem, então nesse caso eu recomendaria continuar com o MRI.</p>
<p>Tempo de inicialização ainda é um problema. Existe uma entrada no <a href="https://github.com/jruby/jruby/wiki/Improving-startup-time"target="_blank" rel="noopener">Wiki deles</a> com algumas recomendações, mas mesmo no deploy do Heroku eu acabei tendo erros R10 (Boot Timeout) de vez em quando para essa app pequena.</p>
<p>Não cheguei a tentar aumentar os dynos para a <a href="https://blog.heroku.com/archives/2015/8/20/introducing-improved-performance-dynos"target="_blank" rel="noopener">tier Performance</a> lançada no ano passado. Apostaria que o JRuby se sairia melhor nesses e seria mais capaz de aproveitar o poder extra de ter de 2.5GB até 14GB se você tem tráfego realmente grande (na ordem de milhares ou dezenas de milhares de requisições por minuto). Com MRI, a recomendação seria usar dynos pequenos (2X ou no máximo Performance-M) e escalar horizontalmente. Com JRuby você poderia ter menos dynos com tamanhos maiores (Performance-L, por exemplo). De novo, depende do seu caso.</p>
<p>Não encare os benchmarks acima como &ldquo;fatos&rdquo; para generalizar para qualquer lugar, eles estão ali só para te dar uma noção de um caso de uso específico meu. O resultado vai variar, então você precisa testar por si mesmo.</p>
<p>Outro caso de uso (que não testei) é, ao invés de simplesmente &ldquo;portar&rdquo; uma app MRI para JRuby, alavancar as forças únicas do JRuby como <a href="https://blog.heroku.com/archives/2016/5/24/reactive_ruby_building_real_time_apps_with_jruby_and_ratpack"target="_blank" rel="noopener">este post</a> do Heroku explica, no caso de usar JRuby com Ratpack, por exemplo.</p>
<p>Resumindo, o JRuby continua sendo um ótimo projeto para experimentar. O próprio MRI também avançou bastante e o 2.3.1 é bem decente. Na maior parte do tempo, tudo depende das suas escolhas de arquitetura como um todo, e a linguagem é só uma peça do quebra-cabeça. Se você ainda não experimentou, com certeza deveria. Ele &ldquo;simplesmente funciona&rdquo;.</p>
]]></content:encoded><category>rails</category><category>performance</category><category>ruby</category></item><item><title>Rails Venceu: O Elefante na Sala</title><link>https://akitaonrails.github.io/2016/05/23/rails-venceu-o-elefante-na-sala/</link><guid isPermaLink="true">https://akitaonrails.github.io/2016/05/23/rails-venceu-o-elefante-na-sala/</guid><pubDate>Mon, 23 May 2016 17:21:00 -0300</pubDate><description>&lt;p&gt;Acabei de ler um &lt;a href="http://web.archive.org/web/20160523160801/http://solnic.eu/2016/05/22/my-time-with-rails-is-up.html"target="_blank" rel="noopener"&gt;artigo muito bem escrito e importante&lt;/a&gt; do contribuidor OSS Solnic e tenho que concordar com ele em quase todos os pontos técnicos.&lt;/p&gt;
&lt;p&gt;Antes de mais nada, é inevitável, mas ainda odeio títulos dramáticos, mesmo que eu mesmo escreva assim às vezes. &amp;ldquo;Meu tempo com Rails acabou&amp;rdquo; é como dizer &amp;ldquo;E você também deveria largar Rails se for inteligente&amp;rdquo;. Descartei o artigo por completo por causa disso. Estava prestes a escrever uma contra-rant sem ler direito, mas agora que li, escrevi este artigo novo do zero.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Acabei de ler um <a href="http://web.archive.org/web/20160523160801/http://solnic.eu/2016/05/22/my-time-with-rails-is-up.html"target="_blank" rel="noopener">artigo muito bem escrito e importante</a> do contribuidor OSS Solnic e tenho que concordar com ele em quase todos os pontos técnicos.</p>
<p>Antes de mais nada, é inevitável, mas ainda odeio títulos dramáticos, mesmo que eu mesmo escreva assim às vezes. &ldquo;Meu tempo com Rails acabou&rdquo; é como dizer &ldquo;E você também deveria largar Rails se for inteligente&rdquo;. Descartei o artigo por completo por causa disso. Estava prestes a escrever uma contra-rant sem ler direito, mas agora que li, escrevi este artigo novo do zero.</p>
<p>Solnic está certo: Ruby, por si só, tem pouco ou nenhum futuro sozinho. Rails é um monopólio real nesta comunidade e a maioria dos projetos OSS mira no Rails. E sim, Rails encoraja algumas práticas ruins e anti-padrões. Isso pode ser muito desanimador para muitos contribuidores OSS, especialmente porque mudar a direção de um Elefante enorme exige um esforço descomunal.</p>
<p>Deixando claro: os argumentos técnicos dialéticos que ele apresenta são todos verdadeiros. Mas metade do artigo, como ele mesmo disse, é pura rant, retórica. E acho que seria bom equilibrar isso, que é o que vou tentar fazer neste post.</p>
<h3>Aceitando a Realidade<span class="hx:absolute hx:-mt-20" id="aceitando-a-realidade"></span>
    <a href="#aceitando-a-realidade" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A realidade é esta: Rails foi feito sob medida para o Basecamp.</p>
<p>Todos sabemos disso, ou deveríamos. Apps no estilo Basecamp não são muito difíceis de fazer, pelo menos em termos de arquitetura. Não precisa de linguagens sofisticadas super performáticas com primitivas de concorrência e paralelismo. E a realidade é que 80% das aplicações web são estilo Basecamp (com base na minha experiência em consultoria). É por isso que Rails resistiu até agora.</p>
<p>É como sistemas de gerenciamento de conteúdo, ou CMS. A maioria deles são sistemas de blog. E para isso você pode ir em frente e instalar o Wordpress. Os mesmos argumentos feitos contra o Rails podem ser feitos contra o Wordpress. E você nunca deveria tunar o Wordpress para ser qualquer coisa além de um sistema de blog. Tente transformá-lo num e-commerce de alto tráfego e vai sofrer.</p>
<p>Para deixar claro: Wordpress tem um dos códigos-fonte mais ofensivos que já vi. Odiaria ter que manter aquela base de código. Me desculpo se algum contribuidor do Wordpress estiver lendo isso, digo sem malícia. E sabe o quê? Possivelmente metade de todos os CMSs do mundo é Wordpress. Mais de um milhão de sites.</p>
<p>Depois tem o caso do Magento2. Grande reescrita sobre o Magento original, escrito usando toda aquela parafernália do Zend que todo mundo detesta. Mas é enorme. Se você precisa de uma solução rápida e pronta para e-commerce, não precisa procurar mais.</p>
<p>Plugins do Wordpress funcionam no Magento? Não. São 2 comunidades fragmentadas, independentes e isoladas. Mas ambas geram muito dinheiro, o que cobre o custo da redundância entre elas. E isso sem contar Drupal, Joomla. PHP é um grande oceano de ilhas desconectadas.</p>
<p>A fragmentação não é estranha ao mundo Javascript. Mas é um tipo diferente de geração de valor. Facebook, Google, Microsoft, todos querem ser os líderes de pensamento na geração Millennial em rápida evolução. É uma estratégia de longo prazo. E um dos elementos desse jogo é o Browser. Não só em termos de Chrome vs Firefox vs IE, mas também em como as aplicações são implementadas.</p>
<p>Facebook criou o React. Google criou o Polymer e o Angular. O pessoal do Node passou por uma luta de poder com a Joyent que quase resultou em mais fragmentação, mas chegaram a um acordo com a <a href="http://readwrite.com/2015/02/10/joyent-node-js-foundation/"target="_blank" rel="noopener">Node Foundation</a>.</p>
<p>A Apple foi à guerra total contra o Flash da Adobe e só agora o Google está <a href="http://web.archive.org/web/20160520233349/http://www.theinquirer.net/inquirer/news/2458329/googles-chrome-browser-will-switch-off-flash-content-by-default"target="_blank" rel="noopener">desligando-os no Chrome</a>. A Apple quer que o nativo prospere e que o Swift seja a linguagem que lidere tudo. O Google tem estratégias conflitantes porque quer que os <a href="http://techcrunch.com/2016/05/18/google-takes-a-new-approach-to-native-apps-with-instant-apps-for-android/"target="_blank" rel="noopener">Instant Apps</a> nativos triunfem, mas se falharem, o plano B continua sendo dominar os apps web baseados em HTML5/CSS3 com Angular. O Facebook não quer ter seu destino decidido pela luta de poder entre Apple e Google.</p>
<p>É uma luta de poder complexa se desenrolando, e o motivo está longe de competência técnica ou geração de valor. É sobre ego, influência e poder. E as tecnologias web estão sendo mantidas como <em>reféns</em> neste cerco.</p>
<p>Depois tem a questão de que o futuro do Ruby está agora fortemente acoplado ao Rails. Isso é uma realidade e se você é um Rubyista que não gosta de Rails, sinto muito. Mas não tanto. Por exemplo, se Hanami é interessante, acredito que pelo menos uma empresa investiu nele. Se ninguém está usando, então não importa o quão tecnicamente superior ele seja. Se Rom.rb é ótimo, alguém deveria estar usando; caso contrário, qual é o ponto? Por que criar uma maravilha técnica que ninguém quer? Mas se há pelo menos uma empresa usando, é <strong>razão suficiente</strong> para continuar, independentemente do que aconteça com Rails ou do que DHH diga ou faça.</p>
<blockquote>
  <p>As pessoas pensam que porque algo é &ldquo;tecnicamente superior&rdquo; todos os outros deveriam adotar cegamente. Mas não é assim que o mercado funciona.</p>

</blockquote>
<p>De todos os eventos de tamanho cósmico que acontecem por aí, realmente não me preocupo tanto se Ruby continua amarrado ao Rails. O que ele faria sem ele?</p>
<p>Todas as comunidades enfrentam fragmentação em algum momento. É muito difícil e caro manter coesão por muito tempo. A única comunidade que acho que conseguiu isso por pura força de regulação é a stack .NET da Microsoft. O próprio Rails desempenhou um papel importante em influenciar a mudança do antigo ASP.NET para o ASP.NET MVC. Agora eles finalmente <a href="http://blogs.microsoft.com/blog/2016/02/24/microsoft-to-acquire-xamarin-and-empower-more-developers-to-build-apps-on-any-device/"target="_blank" rel="noopener">adquiriram o Xamarin</a> antes que o .NET pudesse escapar do controle deles em plataformas open source.</p>
<p>Ruby on Rails é a única outra comunidade &ldquo;coesa&rdquo; que já vi. Com a vantagem de que o Basecamp não precisa de centenas de milhares de desenvolvedores para existir. Um nicho de mercado seria suficiente para o framework evoluir gradualmente através de processos OSS. Por isso sempre questiono a história e as origens de ferramentas e tecnologias para tomar minhas decisões sobre onde usá-las, não só a competência técnica.</p>
<p>Rails funciona porque não precisa fazer política com Apple, Facebook, Microsoft, Google ou qualquer outro comitê (por padrão, nunca confio em comitês). Quem depende do Rails fará a manutenção diretamente. Heroku, Github, New Relic, Shopify e muitos desenvolvedores talentosos.</p>
<h3>3 Leis da Realidade de Mercado<span class="hx:absolute hx:-mt-20" id="3-leis-da-realidade-de-mercado"></span>
    <a href="#3-leis-da-realidade-de-mercado" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ol>
<li>
<p>É fácil analisar demais algo após o fato. 10 anos depois, consigo traçar facilmente um caminho ótimo, evitando todas as armadilhas. Não me torna gênio nenhum, apenas mostra que consigo conectar os pontos, agora claramente visíveis.</p>
</li>
<li>
<p>Nenhuma implementação de solução é perfeita. Se ela resolve um problema real, está fadada a ser imperfeita. Se resolve num mercado em rápida mudança, mais imperfeita ainda.</p>
</li>
<li>
<p>Ou você constrói ferramentas porque suas aplicações de negócio dependem disso, ou constrói ferramentas para vender. As primeiras geralmente serão melhores em termos de ajuste ao mercado. Então se tiver que escolher às cegas, vá com as primeiras.</p>
</li>
</ol>
<p>Portanto, sempre preferirei ferramentas que resolvem um problema real feitas por quem realmente depende delas. Caso contrário, o filho do sapateiro ficará descalço. Por exemplo: vou usar Angular se precisar. Mas nunca começaria um novo negócio que dependesse exclusivamente do Angular para sobreviver. Por quê? Porque o Google não precisa dele. Não piscou para abrir mão do GWT, não precisou pensar duas vezes para reescrever o Angular 2 de forma incompatível com o Angular 1.</p>
<p>Sempre vejo quais fatores externos vão influenciar o destino da tecnologia. Imagine que passei muito tempo escrevendo scripts para o Grunt. As pessoas decidem que é ruim. Agora o Gulp é a melhor escolha. Você investe tudo migrando para Gulp. Então as pessoas decidem que Webpack é a melhor escolha. Síndrome NIH (Not-invented-here) em abundância.</p>
<p>Às vezes isso faz o monopólio do ASP.NET no campo Microsoft e o monopólio do Rails no campo Ruby parecerem inócuos. E sim, comparei Rails com .NET. São as 2 stacks mais comparáveis. Comparável à facção Spring no campo Java. O Spring se levantou contra a complexidade enorme do J2EE oficial em 2002 e então ele mesmo se tornou o novo gigante estilo J2EE a ser combatido.</p>
<p>Este é um dilema milenar:</p>
<blockquote>
  <p>&ldquo;Ou você morre como herói, ou vive tempo suficiente para se ver se tornar o vilão.&rdquo;</p>

</blockquote>
<h3>Por que Rails é um problema agora?<span class="hx:absolute hx:-mt-20" id="por-que-rails-é-um-problema-agora"></span>
    <a href="#por-que-rails-%c3%a9-um-problema-agora" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Como disse antes, concordo com quase todo problema técnico que Solnic apontou.</p>
<p>Rails é de fato a criação de David Hansson (DHH). DHH é uma pessoa, com um ego muito grande e um negócio para administrar. Não dá para esperar que qualquer pessoa seja razoável o tempo todo, especialmente uma que empurrou algo do zero, tanto um negócio quanto uma plataforma tecnológica.</p>
<p>Quando começou, as pessoas desertaram de Java, .NET, PHP e até Python em massa. Todos reconheceram como Rails era interessante comparado a J2EE, ASP.NET, Plone. Oferecia não só produtividade, mas prazer técnico. Estávamos discutindo o maravilhoso mundo das linguagens dinâmicas, classes abertas, injetando comportamento em tempo real (aka <a href="https://m.signalvnoise.com/provide-sharp-knives-cc0a22bf7934"target="_blank" rel="noopener">monkey patching</a>), aplaudindo o abandono de todas as abstrações desnecessárias.</p>
<p>A Idade de Ouro de 2004 a 2006 viu um fluxo interminável de celebração da magia Ruby. Aprendemos Ruby através da magia negra mais obscura do <a href="http://poignant.guide/"target="_blank" rel="noopener">Why, the Lucky Stiff</a>. Era tudo menos arquiteturas limpas e modulares.</p>
<p>Então entramos na Idade de Prata, de 2007 até por volta de 2011. Rails foi longe demais, rápido demais. De repente vimos grandes empresas surgindo de todos os lados! Twitter, Github, Engine Yard, Heroku, Zendesk, Airbnb, todo mundo bebeu o Kool-Aid do Rails. O Merb estava à frente do seu tempo e foi apresentado da maneira errada. Confrontar o todo-poderoso Rails de frente naquele momento não foi inteligente. Fiquei muito apreensivo em 2009 e 2010 para ver se a pseudo-reescrita do Rails 3 realmente se concretizaria.</p>
<p>2011 a 2016 foi a Idade do Bronze, um período agridoce. Muitas linguagens novas surgiram e atingiram o estado &ldquo;utilizável&rdquo;: V8 e Node.js, Rust, Clojure, Elixir, Scala, Haskell. A mudança mais importante: 2010 viu o advento das App Stores, com aplicativos nativos para smartphones e tablets. Esqueça desenvolvimento web, o mobile estava ganhando tudo.</p>
<p>Foi aí que as grandes empresas mostraram seus bolsos fundos. Apple lançou Swift. Google lançou Dart, Angular, Go. Microsoft lançou TypeScript e ASP.NET MVC. Facebook entrou tarde com React e depois React Native.</p>
<p>Rails agora pode ser considerado um &ldquo;problema&rdquo; pelas mesmas razões que o tornaram popular. Isso está fadado a acontecer com qualquer tecnologia.</p>
<p>Mas não dá para mudar muito a arquitetura do Rails, sob risco de quebrar partes enormes dos projetos em produção. E quando alguém precisa reescrever partes grandes de um projeto, é melhor considerar reescrever em outra coisa completamente.</p>
<p>Muitas pessoas estão fazendo exatamente isso, especialmente para APIs e SPAs. As APIs podem ser escritas em Go, Elixir, Scala, evitando Ruby. Você perde o giro rápido do ecossistema Rails, mas se puder pagar (Unicórnio com bolsos fundos), por que não?</p>
<p>Mas para os 90% dos projetos pequenos e médios por aí, ainda dá para conseguir o melhor custo-benefício com Rails. É como dizer: para um blog, use Wordpress. Não tente escrever um blog SPA com APIs em Go e React do zero. Factível, mas não vale a pena.</p>
<p>Se você já usa Rails há algum tempo, siga boas práticas básicas. Adicione testes. Refatore o código, remova duplicações, atualize gems. Considere adicionar uma camada de abstração como o <a href="http://trailblazer.to/"target="_blank" rel="noopener">Trailblazer</a> e componentizar partes da aplicação como <a href="https://leanpub.com/cbra"target="_blank" rel="noopener">Rails Engines</a> ou separar em APIs <a href="https://github.com/rails-api/rails-api"target="_blank" rel="noopener">Rails-API</a>. Um passo de cada vez.</p>
<blockquote>
  <p>Raramente se beneficia de grandes rewrites do zero.</p>

</blockquote>
<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>Para desenvolvedores como Solnic a comunidade Rails provavelmente é um lugar frustrante. Mas também é um vício difícil de largar porque Rails é muito maior do que qualquer concorrente, você sempre se sente mal por ser o azarão.</p>
<p>Rails foi de azarão ao mainstream em 5 anos. Possivelmente o crescimento mais rápido que qualquer framework web já alcançou. A vida de um desenvolvedor web de 1995 a 2003 não era particularmente interessante. Rails fez muito para melhorá-la.</p>
<p>A arquitetura do Active Record vai machucar de verdade talvez 10% dos casos. O Active Support não tem alternativa melhor ainda. Substituir o Active Record por DataMapper ou Rom.rb como padrão não vai trazer tanto valor para as centenas de aplicações existentes. Se eu tivesse que reescrever, faria uma nova aplicação usando Rails + Trailblazer ou iria direto para Hanami. Mas a maioria das pessoas decidiria a favor de abandonar Ruby completamente.</p>
<p>O Active Record poderia ser melhor? Claro! Temos Data Mapper, Sequel e ROM.rb para provar. Mas poderia ter sido feito melhor em 2004? Não acho. Em 2004 &ldquo;NoSQL&rdquo; nem era uma coisa. O melhor que tínhamos era o Hibernate, muito antes do JPA. E para todos os efeitos práticos, o Active Record ainda faz muito melhor do que a média. Mas se você é grande, deve ter cuidado.</p>
<p>As outras comunidades vão enfrentar os mesmos problemas. É inevitável. Tudo é muito mais fácil quando você tem uma comunidade pequena que pode quebrar coisas sem grande impacto. É muito mais difícil manter algo que ficou grande além de todos os cenários otimistas.</p>
<p>Entendo a abordagem conservadora do DHH ao não fazer grandes perturbações. É um movimento válido. Vai alienar desenvolvedores avançados como Solnic, mas ainda vai permitir que iniciantes mergulhem nele sem se preocupar com abstrações demais.</p>
<p>Atualização: DHH comentou nesta seção depois:</p>
<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="en" dir="ltr"><a href="https://twitter.com/AkitaOnRails">@akitaonrails</a> It's not out of conservatism. But a difference of opinion and values. I love Active Record. Love callbacks.</p>&mdash; DHH (@dhh) <a href="https://twitter.com/dhh/status/735051111141396480">May 24, 2016</a></blockquote>
<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="en" dir="ltr"><a href="https://twitter.com/AkitaOnRails">@akitaonrails</a> Will everyone love the same techniques and methods as me? Of course not (see RSpec for just one popular example!). That's fine</p>&mdash; DHH (@dhh) <a href="https://twitter.com/dhh/status/735051237071171584">May 24, 2016</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
<p>As pessoas esquecem que abstrações são muito boas para desenvolvedores avançados que sofreram com a falta delas. Mas iniciantes sempre vão sofrer se apresentados com muitas escolhas arquiteturais logo de cara. Pessoas experientes frequentemente esquecem a curva de aprendizado de quando eram iniciantes, e naquela época Rails era tão atraente. Lembra aquela sensação de realização?</p>
<p><strong>Rails venceu</strong> pela sua simplicidade para iniciantes, orientação clara para pessoas experientes e flexibilidade razoável para desenvolvedores mais avançados. Conseguirá manter mais 10 anos? O tempo dirá.</p>
<p>Para terminar, com Bob Dylan:</p>
<blockquote>
  <p>Reúnam-se ao redor, pessoas</p>

</blockquote>
<blockquote>
  <p>Onde quer que vagueiem</p>

</blockquote>
<blockquote>
  <p>E admitam que as águas</p>

</blockquote>
<blockquote>
  <p>Ao redor de vocês cresceram</p>

</blockquote>
<blockquote>
  <p>E aceitem que em breve</p>

</blockquote>
<blockquote>
  <p>Vocês estarão encharcados até os ossos.</p>

</blockquote>
<blockquote>
  <p>Se o seu tempo para você</p>

</blockquote>
<blockquote>
  <p>Vale ser salvo</p>

</blockquote>
<blockquote>
  <p>É melhor começar a nadar</p>

</blockquote>
<blockquote>
  <p>Ou vai afundar como uma pedra</p>

</blockquote>
<blockquote>
  <p>Pois os tempos estão mudando.</p>

</blockquote>
<blockquote>
  <p>&hellip;</p>

</blockquote>
]]></content:encoded><category>rails</category><category>ruby</category><category>off-topic</category></item></channel></rss>