Desenvolvimento

8 ago, 2017

Como configurar e implantar um aplicativo Elixir em um VPS

Publicidade

Neste guia, iremos passar por um processo de preparação do nosso Servidor Privado Virtual para um pacote de produção de nosso aplicativo web, criando um pacote de lançamento usando Distillery, implantando e expondo-o ao mundo através do servidor web nginx. Algumas soluções de problemas comuns também estão incluídas.

Vindo de um contexto Rails, a implantação de um aplicativo Elixir é significativamente diferente em comparação com a implantação de um aplicativo Rails. A diferença mais importante é que não precisamos instalar o Elixir no nosso VPS, graças ao fato de que o aplicativo Elixir pode ser compilado em pacotes de lançamento Erlang/OTP executáveis, com o Erlang já incorporado. Para o propósito deste guia, eu estou assumindo que o VPS é o Ubuntu 14.04.2 LTS.

 

Configurando o PostgreSQL

Se você estiver usando o Ecto e o PostgreSQL em seu projeto (e você provavelmente está), você deve certificar-se de que a versão do PostgreSQL seja pelo menos 9.5 para evitar possíveis problemas. Enquanto o Ecto suporta versões antigas do PostgreSQL, alguns de seus recursos falharão no tempo de execução – por exemplo, se você usar o map de tipo do Ecto, que é um tipo de jsonb no PostgreSQL, que foi adicionado na 9.4.

Então, vamos nos conectar ao nosso servidor através do ssh e instalar o PostgreSQL 9.6:

$ sudo apt-get update
$ sudo apt-get install postgresql-9.6 postgresql-contrib libpq-dev

Isso falhará para algumas versões antigas do Ubuntu, como 14.04 LTS, mas ainda é possível instalar o PostgreSQL 9.6 nele, siga as instruções aqui.

Se já existe uma versão anterior do PostgreSQL instalada em seu VPS, você pode instalar a nova versão para executar ao mesmo tempo (embora em uma porta diferente) ou transferir todos os dados do banco de dados postgres anterior para o novo, despejando os dados do antigo e os ler no novo – siga as instruções aqui.

Uma vez que finalmente tenhamos uma configuração adequada do PostgreSQL, vamos criar o usuário e o banco de dados que o nosso aplicativo Elixir estará usando. Primeiro, faça o login no console do PostgreSQL

$ sudo -u postgres psql

Em seguida, crie um banco de dados

postgres=# CREATE DATABASE elixir_app_production;

E um usuário com uma senha

postgres=# CREATE USER elixir_app_user WITH PASSWORD 'some_password';

E, finalmente, conceda todos os privilégios ao usuário para esse banco de dados

postgres=# GRANT ALL PRIVILEGES ON DATABASE elixir_app_production to elixir_app_user;

Saia do console entrando

postgres=# \q

Agora, o banco de dados está configurado e pronto para o nosso aplicativo. Volte à nossa máquina local agora e prepare o pacote.

 

Construindo um pacote de lançamento usando Distillery

Em um tempo distante, a ferramenta go-to para gerar lançamentos de projetos Elixir era a Exrm, até mesmo recomendada nos guias de lançamento de Phoenix. Exrm não está mais sendo mantido e, em vez disso, o autor nos incita a usar sua substituição – Distillery, que vamos usar aqui. Claro que existem alternativas, como por exemplo o Relx.

Vamos começar adicionando Distillery como uma dependência em nosso arquivo mix.exs:

{:distillery, "~> 1.4"}

Ou seja lá qual for a versão mais recente. Em seguida, obtenha as novas dependências:

$ mix deps.get

e crie arquivos de configuração inicial para o nosso lançamento:

$ mix release.init

Vamos dar uma olhada no arquivo de configuração que acabou de ser adicionado em rel/config.exs. Nosso ambiente de produção deve ser, por padrão, configurado para incluir binários Erlang e não incluir o código fonte do projeto:

environment :prod do
  set include_erts: true
  set include_src: false
  set cookie: :"A*CesyJa[$IYwrq*FZCno8Nnv,mqiyA$MhGH/:EK$)es//~*@EcUDVWCp}0607A;"
end

Agora, provavelmente precisaremos mudar nossa configuração de lançamento na parte inferior do arquivo. Um padrão deve funcionar para aplicativos básicos e não abrangentes:

release :myapp do
  set version: current_version(:myapp)
  set applications: [
    :runtime_tools
  ]
end

Aqui especificamos quais aplicativos devem ser empacotados em conjunto com nosso projeto. Neste exemplo, Distillery  só adicionou runtime_tools por padrão, mas provavelmente haverá mais necessidade.

Não sei quais aplicativos seu projeto precisará aqui. Você sabe? Provavelmente não. Vamos perguntar a Elixir tentando construir um lançamento (no ambiente de desenvolvimento por enquanto):

$ mix release --env=dev

Embora o lançamento tenha sido provavelmente construído com sucesso, um aviso pode ter sido exibido, por exemplo:

==> One or more direct or transitive dependencies are missing from
    :applications or :included_applications, they will not be included
    in the release:

    :ex_aws
    :uuid

    This can cause your application to fail at runtime. If you are sure
    that this is not an issue, you may ignore this warning.

Estes são os aplicativos que estão faltando em seu projeto agora. Basta adicioná-los à lista em rel/config.exs:

release :myapp do
  set version: current_version(:myapp)
  set applications: [
    :runtime_tools,
    :ex_aws,
    :uuid
  ]
end

E tente construir o lançamento novamente, que deve estar bem agora:

$ mix release --env=dev

Se você obtiver o mesmo aviso novamente, mas com diferentes aplicativos, adicione estes para rel/config.exs também. Repita até que não sejam mais exibidos avisos.

Agora, como sugerem as mensagens de sucesso, podemos executar nossa compilação para verificar se ela realmente funciona. Por exemplo, tente abrir o console interativo, um equivalente ao comando local iex -S mix:

$ _build/dev/rel/myapp/bin/myapp console

Isso irá falhar se você estiver usando o plugin Phoenix Code Reloader no seu ambiente de desenvolvimento. Simplesmente altere temporariamente code_reloader: true para code_reloader: false em config/dev.exs e reconstrua o lançamento novamente para se livrar do erro.

Depois de confirmar que um aplicativo pode ser construído no ambiente de desenvolvimento, vamos configurar nosso ambiente de produção básico em config/prod.exs.

Primeiro o ponto de extremidade do nosso aplicativo, que escutará os pedidos HTTP:

config :myapp, Myapp.Endpoint,
  http: [port: 8888],
  url: [host: "127.0.0.1", port: 8888],
  cache_static_manifest: "priv/static/manifest.json",
  secret_key_base: "rkb5NLnoB1jXI5hDYnpG9Q",
  server: true,
  root: "."

E, em seguida, o acesso ao banco de dados que configuramos anteriormente, usando qualquer nome de username, password e database  que foi utilizado:

config :myapp, Myapp.Repo,
  adapter: Ecto.Adapters.Postgres,
  username: "elixir_app_user",
  password: "some_password",
  database: "elixir_app_production",
  hostname: "localhost",
  pool_size: 10

Você também pode precisar adicionar um número de porta que não seja padrão se você tiver várias instâncias do PostgreSQL em execução no seu servidor:

port: 5434

Agora, podemos finalmente construir nosso lançamento de produção:

$ MIX_ENV=prod mix release --env=prod

 

Vamos testá-lo um pouco mais

Para testes locais mais extensos, podemos copiar temporariamente nossas credenciais de banco de dados locais de config/dev.exs para config/prod.exs, e reconstruir o lançamento novamente:

$ MIX_ENV=prod mix release --env=prod

Inicie o servidor usando o executável que acabamos de criar:

$ _build/prod/rel/myapp/bin/myapp start

Verifique se ele responde:

$ _build/prod/rel/myapp/bin/myapp ping

E, finalmente, usando um navegador, visite o endereço configurado no nosso Ponto Final config/prod.exs, que para este guia é 127.0.0.1.18888.

 

Mas e quanto às migrações de banco de dados em nosso servidor de produção?

Não podemos mais executar tarefas de mixagem, pois mix não está incluído (e não deve ser incluído) em nosso pacote de lançamento. Teremos que adicionar um comando personalizado para isso, que executaremos de forma semelhante aos comandos que já utilizamos no nosso executável, como, por exemplo, _build/prod/rel/myapp/bin/myapp console.

Vamos seguir o método recomendado pelo Destillery de adicionar um módulo de migração executável ao nosso projeto e criar um módulo que executará migrações para nós em qualquer lugar em nosso projeto, por exemplo, em lib/myapp/release_tasks.ex:

defmodule MyApp.ReleaseTasks do

  @start_apps [
    :postgrex,
    :ecto
  ]
  
  @myapps [
    :myapp
  ]

  @repos [
    MyApp.Repo
  ]

  def seed do
    IO.puts "Loading myapp.."
    # Load the code for myapp, but don't start it
    :ok = Application.load(:myapp)

    IO.puts "Starting dependencies.."
    # Start apps necessary for executing migrations
    Enum.each(@start_apps, &Application.ensure_all_started/1)

    # Start the Repo(s) for myapp
    IO.puts "Starting repos.."
    Enum.each(@repos, &(&1.start_link(pool_size: 1)))

    # Run migrations
    Enum.each(@myapps, &run_migrations_for/1)

    # Run the seed script if it exists
    seed_script = Path.join([priv_dir(:myapp), "repo", "seeds.exs"])
    if File.exists?(seed_script) do
      IO.puts "Running seed script.."
      Code.eval_file(seed_script)
    end

    # Signal shutdown
    IO.puts "Success!"
    :init.stop()
  end

  def priv_dir(app), do: "#{:code.priv_dir(app)}"

  defp run_migrations_for(app) do
    IO.puts "Running migrations for #{app}"
    Ecto.Migrator.run(MyApp.Repo, migrations_path(app), :up, all: true)
  end

  defp migrations_path(app), do: Path.join([priv_dir(app), "repo", "migrations"])
  defp seed_path(app), do: Path.join([priv_dir(app), "repo", "seeds.exs"])

end

Observe que este módulo também executará o script de semente em toda migração, o que pode ser potencialmente destrutivo ou simplesmente adicione inúmeros registros ao nosso banco de dados. Se for esse o caso no seu projeto, você deve criar funções separadas para executar migrações e executar o script de semente no módulo acima, ou simplesmente exclua o código que executa o script de semente.

Agora, depois de reconstruir nosso pacote de lançamento, podemos executar migrações com o seguinte comando:

$ _build/prod/rel/myapp/bin/myapp command Elixir.MyApp.ReleaseTasks seed

Nós podemos encurtar isso adicionando um comando personalizado em rel/config.exs

release :myapp do
  set version: current_version(:myapp)
  set applications: [
    :runtime_tools,
    :ex_aws,
    :uuid
  ]
  set commands: [
    "migrate": "rel/commands/migrate.sh"
  ]
end

E criando um arquivo de script em rel/commands/migrate.sh:

#!/bin/sh

bin/myapp command Elixir.MyApp.ReleaseTasks seed

Da mesma forma, comandos mais complexos podem ser adicionados ao nosso pacote por conveniência. Vamos reconstruir nosso pacote mais uma vez e talvez já estejamos prontos para implantação:

$ MIX_ENV=prod mix release --env=prod

 

Lançamentos entre plataformas

Nosso pacote atualmente inclui binários Erlang compilados do nosso sistema local. Isso significa que nosso pacote não será executado em uma plataforma diferente.

Se você estiver na mesma distro que seu servidor de destino você já está pronto, pode ignorar este capítulo.

Caso contrário, existem algumas maneiras de corrigir isso. Aqui estão apenas 3 maneiras comuns e simples entre muitas outras:

 

1) Instale o Erlang no seu VPS e não inclua binários Erlang no seu pacote de lançamento

No nosso exemplo, o Ubuntu VPS é apenas um revestimento único:

$ sudo apt-get install erlang

De volta à nossa máquina local, edite rel/config.exs para que include_erts seja false

environment :prod do
  set include_erts: false
  set include_src: false
  set cookie: :"A*CesyJa[$IYwrq*FZCno8Nnv,mqiyA$MhGH/:EK$)es//~*@EcUDVWCp}0607A;"
end

E reconstrua o lançamento:

$ MIX_ENV=prod mix release --env=prod

 

2) Copie os binários Erlang do seu VPS para sua máquina local e ligue-os ao criar um lançamento.

Novamente, primeiro instale o Erlang no seu VPS:

$ sudo apt-get install erlang

Navegue até o diretório que contém bibliotecas Erlang, que deve ser /usr/lib/erlang, e copie tudo de volta para sua máquina local, por exemplo usando scp:

$ scp -r root@some.address:/usr/lib/erlang path/to/compiled

Apesar da Distillery alegar que só precisa de erts – * / bin e de erts – * / lib, copiar o diretório erts-9.0 sozinho não funciona.

Agora edite rel/config.exs para apontar para o diretório que acabamos de copiar:

environment :prod do
  set include_erts: "path/to/compiled/erlang"
  set include_src: false
  set cookie: :"A*CesyJa[$IYwrq*FZCno8Nnv,mqiyA$MhGH/:EK$)es//~*@EcUDVWCp}0607A;"
end

Recrie o lançamento, e está pronto para a implantação.

$ MIX_ENV=prod mix release --env=prod

E, finalmente, você pode remover Erlang do seu VPS se quiser:

$ sudo apt-get purge erlang

 

3) Use Docker para construir o pacote dentro dele.

Embora esta seja provavelmente a solução mais elegante, flexível e expansível, ela requer algumas pesquisas iniciais para se familiarizar com o Docker e configurar um contêiner Docker apropriado. Enquanto o Docker é geralmente um serviço comercial, ele também oferece uma Edição Comunitária gratuita, que é suficiente para nossas necessidades. O ponto aqui é criar um contêiner, configurado tão próximo ao nosso VPS quanto possível, e construir nossos lançamentos nela. Confira nosso screencast no Docker. Mais informações no Docker e Docker com Elixir.

 

Implantando o pacote para um VPS

Isso é bastante direto. Além de algumas tarefas únicas, o que fazemos em um processo de implantação normal é apenas copiar um arquivo .tar.gz para o nosso servidor e depois descompactá-lo lá. Apesar de sua simplicidade, o processo pode ser propenso a erros e estar em falta de escalabilidade no caso de o nosso projeto se transformar em múltiplas aplicações ou ambientes distintos. Vamos demorar alguns minutos para configurar um sistema de implantação automática usando o edeliver.

Como de costume, adicione a dependência no nosso arquivo mix.exs:

{:edeliver, "~> 1.4.3"}

Obtenha a nova dependência:

$ mix deps.get

E adicione: edeliver ao nosso arquivo de configuração Destillery em rel/config.exs, até o final da lista de aplications:

release :myapp do
  set version: current_version(:myapp)
  set applications: [
    # ....
    :edeliver
  ]
end

Agora, vamos configurar o próprio edelivery. Embora ofereça muitos outros recursos, vamos intrsuí-lo a criar nosso aplicativo localmente (ele detecta automaticamente Destillery sendo configurada) e o implantaremos na produção. Crie um arquivo .deliver/config:

APP="myapp"

BUILD_HOST="localhost"
BUILD_USER="$USER"
BUILD_AT="~/my-build-dir"

PRODUCTION_HOSTS="my.vps.address"
PRODUCTION_USER="user"
DELIVER_TO="/home/web"

PRODUCTION_USER e PRODUCTION_HOSTS serão os mesmos que os nomes de usuário e host no comando ssh que você usou para se conectar ao seu VPS antes.

Embora seja muito redundante, o edeliver também usará o ssh para construir o lançamento em sua própria máquina local. Então, talvez seja necessário instalar openssh-server para lidar com isso:

sudo apt-get install openssh-server

Para manter nosso repositório limpo, vamos adicionar o diretório de lançamento do edelivery para .gitignore:

echo ".deliver/releases/" >> .gitignore

Agora, basta construir, implantar, extrair e iniciar o aplicativo. Com o edeliver é apenas um comando:

mix edeliver update production

E então execute migrações:

mix edeliver migrate production

Se essa não fosse a nossa primeira implantação, seria tudo o que teríamos que fazer graças às atualizações de hot code de Elixir (em alguns casos incomuns que falham, você simplesmente para/inicia o aplicativo novamente). Estamos implantando pela primeira vez, portanto, há mais algumas coisas para verificar.

Conecte-se ao VPS via ssh e navegue até o diretório do aplicativo. Nós instruímos o edeliver para o diretório DELIVER_TO /home/web, então teremos que navegar para /home/web/myapp

Vamos verificar se o aplicativo responde:

$ bin/myapp ping

E você também pode verificar se ele realmente serve alguns dados com uma curl simples:

$ curl 127.0.0.1:8888

Em caso de problemas, você deve verificar logs no diretório var/log/ (do diretório raiz do seu pacote).

A última coisa a fazer aqui é certificar-se de que o aplicativo será iniciado novamente no caso de o nosso VPS reiniciar ou encerrar temporariamente. Como o nosso exemplo o VPS é o Ubuntu, podemos simplesmente adicionar um script de inicialização upstart:

$ sudo nano /etc/init/myapp.conf
description "myapp"

start on startup
stop on shutdown

respawn

env MIX_ENV=prod
env PORT=8888
export MIX_ENV
export PORT

exec /bin/sh /some/path/to/myapp/bin/myapp start

É isso aí. A única coisa que restou para fazer é tornar nosso aplicativo acessível do lado de fora.

 

Configurando o nginx

Primeiro vamos instalar o pacote:

$ sudo apt-get install nginx

Em seguida, edite o arquivo de configuração do site padrão usando qualquer editor que você goste:

$ sudo nano /etc/nginx/sites-available/default

Agora, vamos configurar um upstream para o nosso aplicativo em execução e um bloco de servidor básico com o seu endereço DNS do site que o usa:

upstream my_app {
  server localhost:8888;
}

server {
        listen 80;
        server_name your_website_address.com;

        location / {
          try_files $uri @proxy;
        }

        location @proxy {
          include proxy_params;
          proxy_redirect off;
          proxy_pass http://my_app;
        }
}

É uma boa prática testar a configuração antes de reiniciar o nginx:

$ sudo nginx -t

Se estiver tudo bem, vamos reiniciá-lo para que as mudanças entrem em vigor:

$ sudo service nginx restart

Parabéns, o aplicativo agora está acessível pela Internet.

 

***

Hubert Lepicki faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela Redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: https://www.amberbit.com/blog/2017/7/17/deploy-elixir-app-to-a-vps/