Deploy de App Ruby on Rails em VPS

PaaS ou VPS? Ultimamente tenho preferido serviços PaaS como o Heroku, mas muitos clientes preferem uma VPS na maioria das vezes pelo preço fixo, mas já até mesmo vi um cliente escolher Amazon por que ganhou bônus lá.

Convenções usadas neste post

  • Terminal na máquina de desenvolvimento:
    % **cd ~
    **%
  • Terminal no servidor:
    ubuntu@ip-172–30–0–58:~$ **sudo su -**
    root@ip-172-30-0-58:~#

Ferramentas usadas

Neste post nós vamos usar o Passenger, sua escolha foi feita por causa do zero downtime, i. e., o Passenger vai parar sua app e iniciar novamente, porém ele vai coordenar isso de forma que nenhum usuário perceba essa mudança

O servidor usado foi um Ubuntu Server 16.04 LTS (HVM), SSD Volume Type — ami-e13739f6 na Amazon.

Como o propósito desse post é apenas a instalação de uma app Ruby On Rails eu vou usar o banco de dados RDS da Amazon.

O Projeto

Eu vou usar um projeto Rails gerado usando esses comandos:

    % **rails new MyBlog --database postgresql**
    % **rails generate scaffold Post title content:text**

Máquina de desenvolvimento

Tudo o que você precisa na máquina de desenvolvedor é instalar o ruby 🙂

Mas primeiro: Atualizar o servidor

Antes de tudo é recomendado atualizar o servidor, faça ssh para o seu servidor com:

    % **ssh -i my-key-pair.pem ubuntu@ec2-54-85-37-40.compute-1.amazonaws.com**

Note que como estou usando a Amazon para escrever esse post eu preciso usar o usuário ubuntu, na Linode você tem que se logar diretamente como root.

E depois rode estes comandos:

    ubuntu@ip-172-30-0-58:~$ **sudo su -**
    root@ip-172-30-0-58:~# **apt-get update && apt-get dist-upgrade --yes && reboot**

Como o último comando mostra depois de atualizar o servidor vai reiniciar.

Instalando e Configurando o Servidor

Vamos usar o Ruby da Hellobits, faça ssh novamente na sua máquina e:

    ubuntu@ip-172-30-0-58:~$ **wget -q -O - http://apt.hellobits.com/hellobits.key | sudo apt-key add -
    **OK
    ubuntu@ip-172-30-0-58:~$ **echo 'deb [arch=amd64] http://apt.hellobits.com/ trusty main' | sudo tee /etc/apt/sources.list.d/hellobits.list
    **deb [arch=amd64] [http://apt.hellobits.com/](http://apt.hellobits.com/) trusty main
    ubuntu@ip-172-30-0-58:~$ **sudo apt-get update**
    ubuntu@ip-172-30-0-58:~$ **sudo apt-get install --yes ruby-2.3 nodejs build-essential libcurl4-openssl-dev libssl-dev zlib1g-dev git-core libpq-dev**

Instale o passenger usando:

    ubuntu@ip-172-30-0-58:~$ **sudo gem install passenger bundler**

Por questões de segurança eu sempre recomendo adicionar um usuário para a app:

    ubuntu@ip-172-30-0-58:~$ **sudo useradd -m deploy**

Adicione agora as variáveis de ambiente, edite o arquivo /etc/environment, adicionando essas linhas:

    RAILS_ENV=production
    SECRET_KEY_BASE=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
    DATABASE_URL=postgres://myuser:mypass123@myblog.cc189gx1rqes.us-east-1.rds.amazonaws.com:5432/somedatabase

Leia o artigo How To Read and Set Environmental and Shell Variables on a Linux VPS | DigitalOcean para saber outras formas de adicionar as variáveis de ambiente.

Para gerar o seu SECRET_KEY_BASE rode esse comando no projeto:

    % **bundle exec rake secret**
    0b4c32051205fa77a9035dc31ed7d1769252f0977d4c1bee2befcdee56d4a01f87e41223a9ddf44327e21f81aaa9d8e23979fb65dfdb9b29de1d216809b9b6fb

Instale o passenger com nginx usando esse comando:

    ubuntu@ip-172-30-0-58:~$ **sudo passenger-install-nginx-module --auto --auto-download --languages ruby**

Crie um script de inicialização do nginx em /etc/systemd/system/nginx.service:

[Unit]
Description=A high performance web server and a reverse proxy server
After=network.target

[Service]
Type=forking
PIDFile=/run/nginx.pid
PrivateDevices=yes
SyslogLevel=err
EnvironmentFile=/etc/environment

ExecStart=/opt/nginx/sbin/nginx -g 'pid /run/nginx.pid; error_log stderr;'
ExecReload=/usr/bin/kill -HUP $MAINPID
KillSignal=SIGQUIT
KillMode=mixed

[Install]
WantedBy=multi-user.target

Atualize o arquivo de configuração do nginx, /opt/nginx/conf/nginx.conf, com essas configurações:

user deploy;
worker_processes 1;
events {
  worker_connections 1024;
}
http {
  passenger_root /usr/lib/ruby/gems/2.3.0/gems/passenger-5.1.1/;
  passenger_ruby /usr/bin/ruby;
  include mime.types;
  default_type application/octet-stream;
  sendfile on;
  keepalive_timeout 65;
  server {
    listen 80;
    passenger_enabled on;
    root /home/deploy/myapp/current/public;
  }
}

Configure o nginx para iniciar junto com a máquina:

    root@ip-172-30-0-58:~# **systemctl daemon-reload**
    root@ip-172-30-0-58:~# **systemctl enable nginx**
    Created symlink from /etc/systemd/system/multi-user.target.wants/nginx.service to /etc/systemd/system/nginx.service.

Configurando o Deploy na App

Primeiro é preciso instalar a gem mina, a melhor forma de fazer isso é adicionar essa linha no Gemfile:

    gem "mina", group: :development

E rodar:

    % **bundle install**

Agora vamos adicionar os arquivos do mina no projeto:

    % **mina init**
    -----> Created ./config/deploy.rb
    Edit this file, then run `mina setup` after.

Se formos ler os comentários do config/deploy.rb e a documentação da gem mina, para nossa app, nós vamos acabar gerando um arquivo semelhante a este:

require "mina/bundler"
require "mina/rails"
require "mina/git"

set :domain, "ec2-54-91-72-135.compute-1.amazonaws.com"
set :deploy_to, "/home/deploy/myapp"
set :repository, "https://github.com/dmitryrck/MyBlog.git"
set :branch, "master"

set :shared_dirs, fetch(:shared_dirs, []).push("log")

set :user, "deploy"
set :forward_agent, true

desc "Deploys the current version to the server."
task :deploy do
  deploy do
    invoke :"git:clone"
    invoke :"deploy:link_shared_paths"
    invoke :"bundle:install"
    invoke :"rails:db_migrate"
    invoke :"rails:assets_precompile"
    invoke :"deploy:cleanup"

    on :launch do
      in_path(fetch(:current_path)) do
        command %{mkdir -p tmp/}
        command %{touch tmp/restart.txt}
      end
    end
  end
end

Como estamos usando variáveis de ambiente edite o config/database.yml para que o ambiente de production fique semelhante a isso:

    production:
      url: <%= ENV['DATABASE_URL'] %>

Copie a sua chave privada para o usuário deploy do servidor, i. e., você tem que copiar o conteúdo do arquivo ~/.ssh/id_rsa.pub da sua máquina para o /home/deploy/.ssh/authorized_keys do servidor.

Com a sua app configurada vamos fazer um “setup” inicial do servidor:

    % **mina setup**
    -----> Setting up /home/deploy/myapp
           total 16
           drwxrwxr-x 4 deploy deploy 4096 Dec  4 15:40 .
           drwxr-xr-x 5 deploy deploy 4096 Dec  4 15:40 ..
           drwxrwxr-x 2 deploy deploy 4096 Dec  4 15:40 releases
           drwxrwxr-x 6 deploy deploy 4096 Dec  4 15:40 shared
           # github.com:22 SSH-2.0-libssh-0.7.0
           Connection to ec2-54-91-72-135.compute-1.amazonaws.com closed.

    Elapsed time: 2.82 seconds

Para finalmente fazer o deploy você precisa executar esse comando:

    % **mina deploy**

Criando os dados iniciais

Eu sempre prefiro adicionar os dados no db/seed.rb e chamar uma tarefa rake para popular o banco de dados, nesse caso chamar a tarefa db:seed seria:

    % **mina "rake[db:seed]"**

Caso precise acessar o rails console do servidor:

    % **mina console**
    Loading production environment (Rails 5.0.1)
    irb(main):001:0>

Agora sim, com seus dados todos prontos só falta iniciar o nginx, eu recomendo que você reinicie a sua máquina, até por que você vai precisar dessa “feature” da app iniciar junto com a máquina.

Caso queira iniciar o nginx sem reiniciar a máquina:

    ubuntu@ip-172-30-0-58:~$ **sudo systemctl start nginx**

Agora basta acessar sua app, se você seguiu meu exemplo você tem que entrar em /posts: http://ec2-54-91-72-135.compute-1.amazonaws.com/posts .

Resolução de problemas

Tudo ok, mas minha aplicação não abre no navegador

Verifique o seu Security Group no Console do AWS.

Tudo ok, mas recebo erro 500 ou 404.

Veja o log do nginx com o seguinte comando:

    root@ip-172-30-0-126:~# **systemctl status nginx**

Se estiver tudo ok, veja o log da sua própria aplicação:

    deploy@ip-172-30-0-126:~$ **tail -F /home/deploy/myapp/shared/log/production.log**

Servidor não tem acesso ao repositório

Se ao executar mina deploy você receber esse erro:

    $ mina deploy
    -----> Creating a temporary build path
    -----> Cloning the Git repository
           Cloning into bare repository '/home/deploy/myapp/scm'...
           Warning: Permanently added the RSA host key for IP address '192.30.253.113' to the list of known hosts.
           Permission denied (publickey).
           fatal: Could not read from remote repository.

    Please make sure you have the correct access rights
           and the repository exists.
     !     ERROR: Deploy failed.
    -----> Cleaning up build
           Unlinking current
           OK
           Connection to ec2-54-91-72-135.compute-1.amazonaws.com closed.

    !     Run Error

Significa que o servidor não tem acesso ao repositório, faça um teste com o usuário deploy:

    ubuntu@ip-172-30-0-58:~$ **sudo su - deploy
    **deploy@ip-172-30-0-58:~$ **git clone git@github.com:dmitryrck/MyBlog.git /tmp/clone**
    Cloning into '/tmp/clone'...
    Permission denied (publickey).
    fatal: Could not read from remote repository.

    Please make sure you have the correct access rights
    and the repository exists.

A solução para isso é permitir que seu usuário deploy tenha acesso ao seu repositório.

Primeiro crie a chave ssh:

    ubuntu@ip-172-30-0-58:~$ **sudo su - deploy**
    deploy@ip-172-30-0-58:~$ **ssh-keygen -t rsa -N '' -f ~/.ssh/id_rsa**
    deploy@ip-172-30-0-58:~$ **exit**
    ubuntu@ip-172-30-0-58:~$

Copie o conteúdo do arquivo /home/deploy/.ssh/id_rsa.pub e:

  • Se estiver usando o github coloque sua chave em “Settings > Deploy keys”;

  • No bitbucket seria em “Settings > Deployment Keys”;

Erro “Incomplete response received from application”

Esse é causado por causa da falta de algumas das variáveis de ambiente que sua app precisa, para corrigi-lo basta editar o arquivo /etc/environment corretamente.

Erro “PG::ConnectionBad: could not connect to server: Connection timed out”

Esse erro se deve ao fato de o banco de dados não estar acessível da máquina ruby.

Para testar sua conexão você pode instalar o cliente padrão do PostgreSQL:

    root@ip-172-30-0-126:~# **apt-get install postgresql-client**

E tentar se conectar:

    deploy@ip-172-30-0-126:~$ **psql -h myblog.cc189gx1rqes.us-east-1.rds.amazonaws.com -p 5432 -U myuser -W somedatabase**
    Password for user myuser:
    psql (9.5.5, server 9.5.4)
    SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
    Type "help" for help.

    somedatabase=>

No exemplo acima a conexão está certa, apenas precisava atualizar a variável em /etc/environment.

Outro problema bastante comum é nas configurações do Security Group do RDS, no meu caso eu apenas permiti o acesso ao Security Group da app Ruby no Secutiry Group do RDS.

Mais informações

Conclusão

Esse post visa cobrir o deploy apenas de uma app Ruby on Rails numa VPS, seja ela AWS, Linode ou qualquer outra.

Se você tiver algum problema comente abaixo o problema, tentarei manter o post o mais atualizado possível, pelo menos na parte de resolução de problemas.