Back-End

24 jul, 2017

Lista de verificação de Implantação Rails 5.1 para Heroku

Publicidade

Eu lancei THE CONF ontem. Espero que você aproveite o programa da conferência e aproveite o desconto limitado da madrugada.

De qualquer forma, o site em si é super simples. Um site de página única. Eu escolhi usar o Rails 5.1 como a estrutura do site porque ele cuida de todas as coisas que eu teria que adicionar manualmente em qualquer outro framework. Especialmente agora que ele traz suporte nativo Yarn e Webpack, é muito fácil de usar por qualquer desenvolvedor front-end competente.

Mas mesmo com as muitas minúcias internas, uma configuração de produção completa ainda requer passos extras que a maioria dos iniciantes não saberá. Então, eu decidi compilar uma pequena lista de verificação de coisas das quais você deve cuidar antes da implantação na produção. Não é uma lista extensa e completa para todos os casos de uso, mas ela deve ser suficiente para a maioria dos casos pequenos.

  • Configurando o projeto;
  • Adicionando um CDN e configurando suporte CORS (para poder carregar fontes, por exemplo);
  • Adicionando Memcached.

 

Configurando o projeto

Uma coisa que a maioria das pessoas esquece, mesmo os desenvolvedores experientes, é que o comando rails new usado para inicializar a estrutura de projeto inicial aceita muitas bandeiras de opção. Em vez de ter que ajustar manualmente os arquivos mais tarde, você pode começar assim:

rails new your_project \
--database=postgresql \
--webpack=react \
--skip-action-cable \
--skip-coffee
--skip-turbolinks

Se você estiver construindo com o React ou outro framework de javascript completo, você provavelmente vai querer ignorar o Turbolinks. Caso contrário, se é um site simples, use Turbolinks.

Comece a usar o Postgresql desde o início, não use o Sqlite3.

Pule Action Cable. Prefira uma solução real, como Pusher.com. Se você realmente precisa de algo feito em casa, considere algo como minha solução com Elixir, o Ex Pusher Lite. Tome esta recomendação com cautela, é claro, para pequenas coisas, o Action Cable é mais do que suficiente. Posso escrever outra publicação apenas elaborando sobre este ponto se as pessoas indicarem este desejo na seção de comentários.

De qualquer forma, eu divago. Certifique-se de ter 2 arquivos de inicialização, primeiro o Procfile canônico a ser usado pela Heroku na produção:

web: bin/rails server -p $PORT -b 0.0.0.0

Em segundo lugar, um Procfile.dev para ser usado apenas no seu ambiente de desenvolvimento:

web: ./bin/rails server
webpacker: ./bin/webpack-dev-server

É assim que você aciona o servidor do webpack que irá compilar seus recursos em tempo real durante o desenvolvimento. Você também precisa se lembrar de executar estes dois comandos de dependência agora:

yarn install
bundle install

Instale dependências de javascript com yarn add [package] e é isso aí! Na produção, você não usa o servidor webpack (é por isso que não o adicionamos ao Procfile de produção). Em vez disso, o Heroku detecta automaticamente a gema webpacker, então instala o buildpack nodejs, executa yarn install para você e quando rails assets:precompile é executado,  também executará yarn run que pré-compilará todos os recursos (javascript, stylesheets, imagens) com as impressões digitais adequadas para a recuperação do  cache e tudo o mais que estamos habituados no Rails Asset Pipeline normal.

Então, para Heroku, você basicamente não precisa fazer nada. E em um servidor de implantação personalizado, você apenas precisa se lembrar de executar a tarefa assets:precompile e deixá-la fazer tudo para você.

 

Adicionando um CDN e configurando CORS

Isso é realmente super fácil. Não há nenhum motivo para que alguém não adicione uma CDN a qualquer site. Por favor, faça isso.

Abra o seu AWS Management Console abra o CloudFront. Daí, clique em “Create Distribution” e apenas siga os padrões no Assistente. O requisito é que você DEVE conhecer esse domínio e subdomínio para o qual deseja apontar (por exemplo, “www.theconf.club”) no campo “Origin Domain Name”.

A única personalização que você DEVE fazer é mudar a opção Forward Headers para “Whitelist” nas “Default Cache Behavior Settings”, você precisa adicionar “Origin”, “Access-Control-Request-Headers” e “Access-Control-Request-Method” para a whitelist. E é isso, agora sua distribuição está habilitada para CORS.

Vai demorar algum tempo para criar (tem que configurar muitos centros de dados em todo o mundo), mas você acabará com uma URL que represente sua distribuição, algo como doz7rtw2u3wg4.cloudfront.net. Eu recomendo que você adicione isso como uma variável de ambiente Heroku como esta:

heroku config:set CDN_URL=doz7rtw2u3wg4.cloudfront.net

Agora, edite seu config/environments/production.rb e adicione o seguinte:

config.action_controller.asset_host = ENV['CDN_URL'] if ENV['CDN_URL']

Para realmente usar o CDN, você deve declarar todos os recursos que você usa em seus modelos de exibição usando o Rails View Helpers, como image_tag, asset_path, javascript_pack_tag, stylesheed_pack_tag, stylesheet_link_tag, etc. A inicialização do Rails já criará o modelo de layout com esses helpers, você só precisará segui-los.

Quando o webpack é executado, ele gerará todos os recursos estáticos, otimizados e pré-compilados no public/packs com um arquivo de manifesto declarando a URL completa apontando para a CDN. Por exemplo, se eu buscar o /app/public/packs/manifest.json diretamente do dyno Heroku, receberei algo como isto:

  "Roboto-Bold.woff": "//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Bold-eed9aab5449cc9c8430d7d258108f602.woff",
  "Roboto-Bold.woff2": "//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Bold-c0f1e4a4fdfb8048c72e86aadb2a247d.woff2",
  "Roboto-Light.woff": "//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Light-ea36cd9a0e9eee97012a67b8a4570d7b.woff",
  "Roboto-Light.woff2": "//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Light-3c37aa69cd77e6a53a067170fa8fe2e9.woff2",
  "Roboto-Medium.woff": "//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Medium-cf4d60bc0b1d4b2314085919a00e1724.woff",
  "Roboto-Medium.woff2": "//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Medium-1561b424aaef2f704bbd89155b3ce514.woff2",
  "Roboto-Regular.woff": "//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Regular-3cf6adf61054c328b1b0ddcd8f9ce24d.woff",
  "Roboto-Regular.woff2": "//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Regular-5136cbe62a63604402f2fedb97f246f8.woff2",
  "Roboto-Thin.woff": "//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Thin-44b78f142603eb69f593ed4002ed7a4a.woff",
  "Roboto-Thin.woff2": "//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Thin-1f35e6a11d27d2e10d28946d42332dc5.woff2",
  "application.css": "//doz7rtw2u3wg4.cloudfront.net/packs/application-09b53ce9ca3dd595ee99.css",
  "application.css.map": "//doz7rtw2u3wg4.cloudfront.net/packs/application-09b53ce9ca3dd595ee99.css.map",
  "application.js": "//doz7rtw2u3wg4.cloudfront.net/packs/application-799300612ff6d6595198.js",
  "application.js.map": "//doz7rtw2u3wg4.cloudfront.net/packs/application-799300612ff6d6595198.js.map",
  "home_page.js": "//doz7rtw2u3wg4.cloudfront.net/packs/home_page-ff3b49407a1d01592ad5.js",
  "home_page.js.map": "//doz7rtw2u3wg4.cloudfront.net/packs/home_page-ff3b49407a1d01592ad5.js.map"
}

Então, se por algum motivo eu tivesse que criar uma nova distribuição CDN, você deve se lembrar de atualizar a variável CDN_URL no Heroku e redistribuir seu aplicativo para que ele regenere os recursos e este arquivo de manifesto. Serão apenas esses URLs ao renderizar os HTMLs finais.

Quando um usuário abre seu site, ele receberá o HTML com URLs apontando para a CDN. Na primeira vez não terá nenhum recurso em cache, então ele pedirá por eles no site. Seu site deve retornar os recursos COM os cabeçalhos CORSs corretos para que o CDN os armazene com os cabeçalhos e encaminhe esses cabeçalhos para o navegador. O navegador precisa desses cabeçalhos porque ele irá abrir do domínio www.theconf.club, por exemplo, mas as fontes estão sendo carregadas a partir de doz7rtw2u3wg4.cloudfront.net, por isso subiria um aviso de segurança e não carregaria as fontes porque elas estão em domínios diferentes. Daí os cabeçalhos CORS que a fonte fornece indicando que são seguros de serem carregados.

Para o seu aplicativo Rails retornar os cabeçalhos apropriados, você precisa adicionar a gem rack-cors ao seu Gemfile. Então, você deve adicionar um novo arquivo de configuração em config/initializers/rack-cors.rb com:

if defined? Rack::Cors
  Rails.configuration.middleware.insert_before 0, Rack::Cors do
    allow do
      origins %w[
        https://theconf.club
        http://theconf.club
        https://www.theconf.club
        http://www.theconf.club
        https://theconf.herokuapp.com
        http://theconf.herokuapp.com
      ]
      resource '/assets/*'
    end
  end
end

E, finalmente, edite config/application.rb para inserir o middleware corretamente:

if Rails.env.production?
  config.middleware.insert_before 0, Rack::Cors do
    allow do
      origins '*'
      resource '*', :headers => :any, :methods => [:get, :post, :options]
    end
  end
end

Quando você implanta, você sabe que está funcionando corretamente quando você da um Curl e ele retorna os cabeçalhos Access-* como este:

$ curl -I -s -X GET -H "Origin: www.theconf.club" 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

E se tudo acima já estiver no lugar, você poderá ver os cabeçalhos sendo encaminhados através da CDN, assim:

$ curl -I -s -X GET -H "Origin: www.theconf.club" 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==

Uma CDN é o molho secreto que permite que qualquer site baseado em conteúdo escale muito além do que seu servidor pode fornecer. É uma enorme economia de custos e também proporciona uma experiência do usuário mais suave.

Uma última ressalva. Os muitos Rails View Helpers, como image_tag, permitem que você adicione o nome do arquivo de imagem sem uma extensão no desenvolvimento e ele encontrará corretamente a imagem. Mas no servidor, não conseguirá renderizar o modelo dessa maneira. Como regra geral, SEMPRE preencha todo o nome do arquivo e extensão, por exemplo, image_tag(“logo.png”) em vez de apenas image_tag(“logo”).

Você pode ver como isso falha se você abrir um console no Heroku e verificar como ele não consegue derivar o URL completo da imagem:

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

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

irb(main):002:0> ActionController::Base.helpers.asset_path("icon-goals.png")
=> "//d134ipy19a646x.cloudfront.net/assets/icon-goals-b969b3b7325d33ad85a88dbb5b894832909ed738eea9964b9cf535646b93674b.png"

 

Adicionando Memcached

Falar sobre cache é sempre uma coisa complicada. Razão pela qual muitas pessoas evitam até mesmo tentar. Mesmo que você possa ficar realmente louco com configurações super-granulares, como usar o Russian Doll Caching, apenas adicionar cache em alguns pontos pode beneficiá-lo grandemente. E é super fácil inicializar.

A primeira coisa a fazer é adicionar Memcachier ao seu aplicativo Heroku. Ele tem um nível livre e para aplicativos menores deve ser suficiente.

A configuração também é trivial. Comece adicionando as seguintes gems ao seu Gemfile:

group :production do
  gem 'rack-cors', require: 'rack/cors'
  gem 'rack-cache'
  gem 'dalli'
  gem 'kgio'
  gem 'memcachier'
end

Agora você deve editar seu config/environments/production.rb como este:

config.cache_store = :dalli_store

client = Dalli::Client.new((ENV["MEMCACHIER_SERVERS"] || "").split(","),
                           :username => ENV["MEMCACHIER_USERNAME"],
                           :password => ENV["MEMCACHIER_PASSWORD"],
                           :failover => true,
                           :socket_timeout => 1.5,
                           :socket_failure_delay => 0.2,
                           :value_max_bytes => 10485760)
config.action_dispatch.rack_cache = {
  :metastore    => client,
  :entitystore  => client
}

Agora, digamos que você tem um bloco em seu modelo que requer um monte de registros de seu banco de dados. Mas você sabe que esses registros mal mudam. O que você pode fazer? Uma alternativa é armazenar em cache a consulta ActiveRecord completamente assim:

class HomePageController < ApplicationController
  def index
    @selected_proposals = Rails.cache.fetch('selected_proposals', expires_in: 1.day) do
      Proposal.includes(:authors).where(selected: true).to_a
    end
  end
end

O #to_a é necessário porque as consultas ActiveRecord são preguiçosas. O #to_a força-o a buscar e ele será armazenado em cache. Da próxima vez, não irá tocar o banco de dados por um dia inteiro!

Eu também poderia ter adicionado um bloco cache do … end no próprio modelo. Existem muitas opções. O ponto é que não é tão difícil como a maioria das pessoas pensaria.

O que torna difícil o armazenamento em cache é adicionar a lógica de expiração. E a regra é: nunca tente expirar manualmente qualquer cache. Basta alterar a chave de pesquisa para outra coisa e deixar os dados antigos simplesmente morrerem (o memcached cuidará de se livrar dos dados antigos não utilizados).

Você realmente quer ler o Rails Guides no Caching. Realmente não é tão difícil como você pensa e você pode armazenar em cache apenas os poucos fragmentos que você sabe que são sensíveis ao desempenho e deixe as outras partes que são altamente dinâmicas e que você não tem certeza de como armazenar em cache corretamente.

Mas, como é super barato, use-o agora mesmo.

 

Adicionando suporte SSL

Se você tiver alguma transação sensível à segurança em andamento (por exemplo, compras), você deseja usar o SSL. Novamente, muitas pessoas evitam isso, porque geralmente é difícil entender como obter corretamente um certificado.

Let´s Encypt torna o processo super trivial. Parabéns para eles! E ainda melhor: é grátis! Então, você não tem desculpas para não ter SSL.

E sobre o Heroku, é ainda mais fácil!

O ACM (Automated Certificate Management) é habilitado por padrão para todos os aplicativos criados após 21 de março de 2017 que estão sendo executados em dynos Hobby ou Professional. As seguintes etapas são para aplicativos atualmente em execução no dynos Gratuito e para aplicativos criados antes dessa data.

No Dynos de Nível Gratuito, é isso que você faz:

 

heroku certs:auto:enable

Verifique o status com heroku certs: auto.

Feito, não há passo 2.

Nós costumávamos usar a gem complicada letsencrypt-rails-heroku, mas agora é apenas muito fácil.

 

Conclusão

Eu acredito que isso cobre os princípios básicos de coisas que você deveria fazer antes de implementar seu pequeno aplicativo para o Heroku. Para aplicativos maiores, há muitas outras preocupações que estão além do escopo desta postagem.

Se você se lembra de mais dicas e truques, compartilhe na seção de comentários abaixo.

***

Artigo traduzido com autorização do autor. Publicado originalmente em http://www.akitaonrails.com/2017/06/28/rails-5-1-heroku-deployment-checklist-for-heroku