Por que mais um post sobre a gem AssetSync?

Eu resolvi escrever este post, pois esta tarefa aparentemente simples, acabou tomando quatro horas da minha preciosa vida.

Apesar de ter uma cacetada de tutoriais na Internet sobre o assunto (inclusive o excelente readme da gem), eu não consegui encontrar nenhum que mostrasse um pequeno detalhe que me fez perder todo esse tempo. Se você chegou até aqui, espero que não tenha perdido este tempo ainda, caso contrário, espero que este post lhe ajude. Mas, chega de papo furado e vamos colocar a mão na massa.

Mais um detalhe: eu não vou ficar aqui descrevendo tudo nos mínimos detalhes e com um monte de prints screen, ok? Imagino que você seja um programador e isso só faria você perder ainda mais tempo.

Amazon IAM & Amazon S3

Nós precisaremos usar estes dois serviços da Amazon:

  • Amazon S3: permite que você crie buckets de armazenamento de dados. É lá que nossos assets ficarão gravados.
  • Amazon IAM: com ele nós criaremos uma identidade e uma senha que permitirá a nossa API fazer o upload de nossos assets na S3.

Criando um bucket na Amazon S3

A primeira coisa que você precisa fazer é criar um novo bucket na S3.

1. Entre no console do AWS

2. Acesse o serviço S3 – Scalable Storage in the Cloud

3. Crie um novo bucket para o seu projeto. Em nosso exemplo nós criaremos um chamado myproject-bucket. Importante: não use ponto no nome do bucket. Isso lhe dará dores de cabeça mais tarde.

4. Você agora precisa dar acesso público aos arquivos hospedados. Para isso clique em Add bucket policy e adicione a política abaixo:

{
  "Version": "2008-10-17",
  "Statement": [
    {
      "Sid": "AllowPublicRead",
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::myproject-bucket/*"
    }
  ]
}

Cada arquivo poderá ser acessado por qualquer um publicamente, desde que o solicitante saiba o nome do arquivo. Note que nós não demos acesso a listagem de arquivos.

5. Se você usa webfonts, então você precisa configurar também o CORS para o seu bucket. Se você não fizer isso, um erro de Cross-Origin Resource Sharing pode ocorrer. Para isso clique em Add CORS Configuration e cole o script abaixo:

<CORSConfiguration>
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>GET</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>Authorization</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

6. Dê acesso total ao seu usuário. Em grantee selecione o seu usuário AWS e marque todas as opções. Veja a imagem acima.

Autorizando o acesso ao bucket via API com o IAM

1. Ainda no console do AWS acesse o serviço IAM – Identity & Access Manager.

2. Crie um novo access key. Não deixe de anotar o access key e um secret access key. Você precisará destes dois dados para que a gem Asset Sync possa ter acesso ao S3 via API.

3. Você agora precisa dar acesso a listar os buckets e a acessar o bucket do projeto. Para isso, anexe a seguinte Policy ao usuário criado:

{
  "Statement": [
    {
      "Action": [
        "s3:ListAllMyBuckets"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::*"
    },
    {
      "Action": "s3:*",
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::myproject-bucket"
    },
    {
      "Action": "s3:*",
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::myproject-bucket/*"
    }
  ]
}

Pronto! Os serviços da Amazon estão configurados. O próximo passo é mexer no seu aplicativo.

Modificações no seu projeto Rails

Por default o Rails gera os assets na pasta /assets. Em nosso exemplo nós faremos isso de forma ligeiramente diferente. Colocaremos em /production/assets. Assim, se for necessário um ambiente de staging, você pode manter os assets separados.

Vamos começar colocando tal pasta no .gitignore para não versioná-lo indevidamente. Aliás, você usa git, né?!

# arquivo: .gitignore

/public/production

Adicione a gem Asset Sync no Gemfile. Aqui eu travei na versão 1.1.0, mas você pode consultar a versão mais atual no Rubygems.

Gemfile

gem "asset_sync", "1.1.0"

Rode o bundler:

$ bundle install

Para o ambiente de produção, você precisa ensinar ao Rails aonde os assets estão hospedados. Você faz isso mexendo no parâmetro asset_host.

Lembra que eu falei acima que estamos usando /production/assets ao invés de /assets? Isso é feito através do parâmetro prefix.

# arquivo: config/environments/production.rb

# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.action_controller.asset_host = 'http://assets.example.com'
config.action_controller.asset_host = "//#{ENV['FOG_DIRECTORY']}.s3.amazonaws.com"
config.assets.prefix = "/production/assets"

Essa midificação explicarei mais abaixo. Ela é muito importante! Vale a pena a leitura.

# arquivo: config/initializers/assets.rb

Rails.application.config.assets.manifest = "#{Rails.root}/config/manifest.json"

E deu! Só isso! Nenhuma outra mudança no projeto é necessária.

Variáveis de ambiente para o projeto

Os parâmetros de acesso a Amazon serão todos abastecidos via variáveis de ambiente. As configurações acima são as recomendadas pela gem, mas você pode tentar outras modalidades. Segue abaixo as variáveis necessárias:

Ambiente de desenvolvimento:

AWS_ACCESS_KEY_ID=xxxxxxxxxxxx   # preencha aqui os dados gerados pelo IAM.
AWS_SECRET_ACCESS_KEY=yyyyyyyy   # preencha aqui os dados gerados pelo IAM.
FOG_DIRECTORY=myproject-bucket   # nome do bucket do nosso projeto.
FOG_PROVIDER=AWS                 # aqui você está ensinando ao fog, a gem que de fato faz o upload, que ele fará isso na Amazon.
FOG_REGION=sa-east-1             # nosso bucket de exemplo está em hospedado em São Paulo.
ASSET_SYNC_GZIP_COMPRESSION=true # indica que a versão compactada das assets deve ser servida.

No seu servidor:

FOG_DIRECTORY=myproject-bucket  # nome do bucket do nosso projeto.

Note que no servidor você não precisa colocar todas as variáveis.

Como o Rails coloca fingerprints nos arquivos de assets?

Você já percebeu que em produção os seus arquivos ganham um MD5 no nome do arquivo?

O que originalmente era:

/assets/application.css

Depois da compilação fica:

/assets/application-24b123b49a1e9a7d93fa032eba0850d8.css

Isto é a maneira engenhosa do Rails invalidar o cache dos browser. Ou seja, sempre que o conteúdo do seu arquivo mudar, o MD5 irá mudar, o browser pensará ser um novo arquivo e fará o download do mesmo recebendo a sua modificação. Bacana né?

Pois então… lembra que eu escrevi acima que eu perdi quatro horas da minha vida tentando configurar e não sabia o que estava acontecendo. Deixa eu explicar.

Quando eu colocava o meu aplicativo no ar, os assets não carregavam. Eles estavam todos lá na Amazon S3, porém, meu aplicativo simplesmente não os achavam. Ao inspecionar o HTML eu percebi que o Rails não estava gerando as URLs com fingerprint.

Veja abaixo um recorte do meu HTML:

<head>
  <link href="//myapp-mybucket.s3.amazonaws.com/production/assets/application.css" rel="stylesheet" media="all"/>
</head>

E deveria estar assim:

<head>
  <link href="//myapp-mybucket.s3.amazonaws.com/production/assets/application-5b9b60700d51c810a1920e5e5e0b1452.css" rel="stylesheet" media="all"/>
</head>

Onde o Rails pega esses fingerprints pra usar nos helpers que geram os links?

Esta foi a dura lição que eu aprendi. Em minha fantasia eu achava que o Rails fazia isso sempre que fosse necessário. Ou seja, eu achava que ao gerar o link ele lia o arquivo e gerava o MD5. Ainda bem que tem gente mais esperta do que eu no time do Sprockets, pois senão este processo tornaria o Rails uma carroça… hehehe.

A verdade é a seguinte, quando você executa o comando $ bundle exec rake assets:precompile RAILS_ENV=production, o Sprockets irá fazer tudo que necessita fazer (aprenda mais aqui) e também gerará um arquivo a mais chamado manifest-*.json.

Dentro deste arquivo há uma lista de todos os arquivos pré-compilados com os diversos dados de cada um, dentre eles, o MD5.

Sempre que um helper do Rails precisa saber o nome do arquivo com o MD5, ele pergunta para o Sprockets e o mesmo consulta o manifest.json.

Por padrão o Rails procura este arquivo em /public/assets/manifest-*.json. Quando você manda seus assets para a S3, logicamente o seu aplicativo não sabe mais onde está este arquivo manifesto e acaba não sabendo quais fingerprints usar.

O segredo aqui gerar este arquivo manifesto em outro diretório de tal forma que ele vá parar lá no seu servidor do aplicativo e não na S3. A configuração que faz isso é esta:

# arquivo: config/initializers/assets.rb

Rails.application.config.assets.manifest = "#{Rails.root}/config/manifest.json"

Assim, se você usa git pra fazer o deploy do seu aplicativo, basta versionar também este arquivo.

Eu aprendi isto lendo este post aqui. Vale a pena ler a discussão.

É isso… espero que isto poupe o seu tempo.

Links interessantes:



Gem AssetSync
Discussão sobre o arquivo manifest.json
Rails Guide – Asset pipeline in production
AkitaOnRails: Enciclopédia do Heroku