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/