Desenvolvimento

19 jan, 2017

Elixir Phoenix App implantado em uma configuração Load Balanced DigitalOcean

Publicidade

Uma das principais vantagens de construir uma aplicação web habilitada para Websockets usando o Phoenix é o quanto é “fácil” para Erlang conectar-se em um cluster.

Para iniciantes, Erlang não precisa de vários processos como o Ruby (que é limitado a uma conexão por processo ou por thread, se você estiver usando um servidor threaded como o Puma). Um único processo Erlang assumirá toda a máquina, se você precisar. Internamente, ele manterá uma thread real por máquina principal. E cada thread terá seu próprio Scheduler para gerenciar tantos microprocessos quanto você precisar. Você pode ler tudo sobre isso neste meu artigo.

Além disso, o Erlang possui capacidades internas para formar um cluster, onde cada instância Erlang atua como um Node peer-to-peer, sem a necessidade de um coordenador centralizado. Você pode ler tudo sobre isso em meu texto sobre Nodes. O poder do Erlang está no quanto é “fácil” formar sistemas distribuídos confiáveis.

Você pode ativar muitas instâncias do Phoenix e, a partir de uma das instâncias, pode transmitir mensagens para Usuários inscritos em Canais mesmo que seus sockets estejam conectados a instâncias diferentes. É seamless, e você não precisa fazer nada de especial no seu código. Phoenix, Elixir e Erlang estão fazendo todo o trabalho pesado para você nos bastidores.

Nenhum Heroku para você 🙁

Como você deseja aproveitar essa escalabilidade e recurso de alta disponibilidade para sistemas distribuídos (no pequeno exemplo de um sistema de bate-papo em tempo real), você precisará ter mais controle sobre sua infraestrutura. Esse requisito exclui a maioria das ofertas de Plataforma como Serviço (PaaS) que existem por aí, como o Heroku. O modelo do Heroku gira em torno de processos únicos e voláteis em containers isolados. Esses processos presos (dynos) não estão cientes de outros processos ou da rede interna. Portanto, você não pode acionar Dynos e tê-los formando um cluster, porque eles não serão capazes de encontrar uns aos outros.

Se você já sabe como configurar coisas relacionadas ao Linux – Postgresql, HAproxy, etc. -, avance diretamente para a seção específica do Phoenix.

IaaS (DigitalOcean) ao salvamento!

Você quer processos de longa duração em servidores de rede acessíveis (seja através de rede privada, VPN ou simplesmente – não é seguro! – redes públicas).
Neste exemplo, quero orientá-lo através de uma implantação muito simples usando DigitalOcean (você pode escolher qualquer IaaS, como AWS, Google Cloud, Azure ou qualquer um com o qual você se sinta mais confortável).
Eu criei 4 droplets (todos usando o menor tamanho de 512Mb de RAM):

  • 1 banco de dados PostgreSQL (ponto único de falha: não é o foco deste artigo criar um banco de dados altamente replicável e disponível);
  • 1 servidor HAProxy (ponto único de falha: novamente, não é o foco criar um esquema de balanceamento de carga altamente disponível);
  • 2 servidores Phoenix – um no datacenter de Nova York e outro no de Londres, para demonstrar como é fácil para o Erlang formar clusters mesmo com caixas separadas geograficamente.

Configuração básica do Ubuntu 16.04

Você vai querer ler meu artigo sobre como configurar o Ubuntu 16.04. Para resumir, comece configurando o UTF-8 apropriado:

sudo locale-gen "en_US.UTF-8"
sudo dpkg-reconfigure locales
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
echo 'LC_ALL=en_US.UTF-8' | sudo tee -a /etc/environment
echo 'LANG=en_US.UTF-8' | sudo tee -a /etc/environment

Certifique-se de adicionar um usuário adequado para o grupo sudo e, a partir de agora, não use o usuário root. Vou criar um usuário chamado pusher e vou explicar em outro artigo por quê. Você deve criar um nome de usuário que se adapte ao seu aplicativo.

adduser pusher
usermod -aG sudo pusher

Agora, saia e faça login novamente através deste usuário: ssh pusher@server-ip-address. Se você estiver em um Mac, copie a chave pública do seu certificado SSH desta forma:

ssh-copy-id -i ~/.ssh/id_ed25519.pub pusher@server-ip-address

Ele cria o .ssh/authorized_keys se ele não existir, define os bits de permissão corretos e anexa sua chave pública. Você pode fazê-lo manualmente, é claro.

As droplets do DigitalOcean começam sem um arquivo de troca e eu recomendaria adicionar um, especialmente se você quiser começar com as caixas menores com menos de 1GB de RAM:

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
sudo cp /etc/fstab /etc/fstab.bak
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
sudo sysctl vm.swappiness=10
sudo sysctl vm.vfs_cache_pressure=50
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
echo 'vm.vfs_cache_pressure=50' | sudo tee -a /etc/sysctl.conf

Verifique se você tem atualizações autônomas configuradas. Você vai querer pelo menos ter atualizações de segurança automaticamente instaladas quando disponíveis.

sudo apt install unattended-upgrades

Agora, vamos instalar Elixir e Node (Phoenix precisa de Node.js):

wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb && sudo dpkg -i erlang-solutions_1.0_all.deb
curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install build-essential nodejs esl-erlang elixir erlang-eunit erlang-base-hipe
sudo npm install -g brunch
mix local.hex
mix archive.install https://github.com/phoenixframework/archives/raw/master/phoenix_new.ez # this is optional, install if you want to manually test phoenix in your box

Agora você tem uma máquina capacitada de Elixir pronta. Crie um snapshot de imagem sobre o DigitalOcean, mova suas regiões em que deseja criar as outras droplets e use essa imagem para criar quantas droplets você precisar.

Para este exemplo, criei uma segunda droplet na região de Londres, uma terceira para PostgreSQL na região NYC1 e uma na região NYC3 para HAProxy.

Irei me referir a seus endereços IP públicos como “nyc-ip-address”, “lon-ip-address”, “pg-ip-address” e “ha-ip-address”.

Configuração básica do PostgreSQL

  • Objetivo: configuração básica do PostgreSQL para permitir que os servidores Phoenix se conectem.
  • Para fazer: crie um papel secundário apenas para se conectar ao banco de dados do aplicativo e outra função de superusuário para criar o banco de dados e migrar o esquema. Também bloqueie a máquina e configure túneis SSH ou outra maneira segura, pelo menos uma rede privada, em vez de permitir conexões de porta TCP 5432 da Internet pública.

Agora você pode se conectar ao ssh pusher@pg-ip-address e seguir isto:

sudo apt-get install postgresql postgresql-contrib

Você deve criar uma nova função com o mesmo nome do usuário que você adicionou acima (“pusher”, no nosso exemplo):

$ sudo -u postgres createuser --interactive

Enter name of role to add: pusher
Shall the new role be a superuser? (y/n) y

$ sudo -u postgres createdb pusher

O PostgreSQL espera encontrar um banco de dados com o mesmo nome da função, e a função deve ter o mesmo nome do usuário do Linux. Agora você pode usar psql para definir uma senha para essa nova função:

$ sudo -u postgres psql
\password pusher

Registre uma senha segura, tome nota e vamos seguir em frente.

O PostgreSQL vem bloqueado para conexões externas. Uma maneira de se conectar do lado de fora é configurar seus servidores para criar um túnel SSH para o servidor de banco de dados e manter as conexões TCP externas pela porta 5432 rejeitada.

Mas, para este exemplo, vamos apenas permitir conexões da Internet pública para a porta TCP 5432. Atenção: isso é MUITO inseguro!

Edite o /etc/postgresql/9.5/main/postgresql.conf e encontre a linha de configuração listen_addresses e permita:

listen_addresses = '*'    # what IP address(es) to listen on;

Isso deve ligar o servidor à porta TCP. Agora edite /etc/postgresql/9.5/main/pg_hba.conf no final para ficar assim:

# IPv4 local connections:
host    all             all             127.0.0.1/32            trust
host    all             all             your-local-machine-ip-address/24        trust
host    all             all             nyc-ip-address/24       trust
host    all             all             lon-ip-address/24       trust
# IPv6 local connections:
host    all             all             ::1/128                 trust

Salve o arquivo de configuração e reinicie o servidor:

sudo service postgresql restart

Viu o que eu fiz lá? Só permiti conexões provenientes dos IPs públicos dos servidores Phoenix. Isso não torna o servidor seguro, apenas um pouco menos vulnerável. Se você está atrás de uma rede baseada em DHCP/NAT, apenas procure no Google “qual é o meu IP” para ver o seu endereço IP público – que é provavelmente compartilhado por muitos outros usuários, lembrando que você está permitindo conexões de um IP inseguro para o servidor do seu banco de dados! Depois de fazer os testes iniciais, crie seu novo esquema, então você deve remover a linha your-local-machine-ip-address/24 da configuração.

A partir do seu aplicativo Phoenix, você pode editar o arquivo local config/prod.secret.exs para se parecer com isto:

# Configure your database
config :your_app_name, ExPusherLite.Repo,
  adapter: Ecto.Adapters.Postgres,
  username: "pusher",
  password: "your-super-secure-pg-password",
  database: "your-app-database-name",
  hostname: "pg-ip-address",
  pool_size: 20

Substitua as informações de seu servidor e banco de dados e agora você pode testá-lo:

MIX_ENV=prod iex -S mix phoenix.server

Se você vir uma mensagem :econnrefused do postgrex, então você está com problemas. Verifique novamente a configuração, reinicie o servidor e tente novamente. Se tudo se conectar, você pode executar MIX_ENV=prod mix do ecto.create, ecto.migrate para preparar seu banco de dados.

Finalmente, você vai querer bloquear o resto do seu servidor com UFW, no mínimo. UFW deve vir pré-instalado no Ubuntu 16; assim, você pode apenas fazer:

ufw allow 5432
ufw allow ssh
ufw enable

É isso aí. E, novamente, isso não torna seu servidor seguro, apenas o torna menos inseguro. Há uma enorme diferença!

E, a propósito, se você é um fã de Docket:

NÃO INSTALE UMA BASE DE DADOS DENTRO DE UM CONTAINER DOCKER!

Você foi avisado!

Configuração básica do HAProxy

  • Objetivos: Fornecer uma solução simples para equilibrar a carga entre nossos dois servidores Phoenix.
  • Para fazer: Há algo errado com a sessão de verificação ou algo parecido, já que às vezes eu tenho que atualizar o meu navegador para que eu não seja enviado de volta para o login no meu aplicativo. Phoenix usa sessões baseadas em cookie para que eu não pense que estejam faltando sessões.

Agora permita o ssh pusher@ha-ip-address. Isso é fácil, vamos apenas instalar HAProxy:

sudo apt-get install haproxy

Edite /etc/haproxy/haproxy.cfg:

...
listen your-app-name
  bind 0.0.0.0:80
  mode http
  stats enable
  stats uri /haproxy?stats
  stats realm Strictly\ Private
  stats auth admin:some-secure-password-for-admin
  option forwardfor
  option http-server-close
  option httpclose
  balance roundrobin
  cookie SERVERID insert indirect nocache
  server us your_us_server_IP:8080 check cookie us1
  server uk your_uk_server_IP:8080 check cookie uk1

Você pode evitar as linhas stats se tiver outros meios de monitoramento. Caso contrário, defina uma senha segura para o usuário admin. Uma parte muito importante é option http-server-close, como explicado neste artigo; caso contrário, você pode ter problemas com Websockets.

Por algum motivo, estou tendo alguns problemas com meu aplicativo após o login e ele define a sessão. Às vezes, eu tenho que atualizar para ser enviado para a página correta, não sei por que ainda, e eu acredito que é algo na configuração HAProxy. Se alguém sabe o que é, deixe-me saber na seção de comentários abaixo. Por agora, estou confiando apenas em sessões de Sticky (afinidade de servidor), fazendo o HAProxy gravar um cookie com o servidor.

Agora você pode reiniciar o servidor e habilitar o UFW também:

sudo service haproxy restart
sudo ufw allow http
sudo ufw allow https
sudo ufw allow ssh
sudo ufw enable

Você pode facilmente adicionar suporte para SSL ao seu aplicativo configurando HAProxy (e não os nodes do Phoenix). A documentação do DigitalOcean é abrangente, então basta segui-la. No final, meu haproxy.cfg se parece com isto:

global
   log /dev/log    local0
   log /dev/log    local1 notice
   chroot /var/lib/haproxy
   stats socket /run/haproxy/admin.sock mode 660 level admin
   stats timeout 30s
   user haproxy
   group haproxy
   daemon
   maxconn 2048
   tune.ssl.default-dh-param 2048

defaults
   log global
   mode http
   option httplog
   option dontlognull
   option redispatch
   option forwardfor
   option http-server-close
   timeout connect 5000
   timeout client  50000
   timeout server  50000

frontend www-http
   bind your_ha_proxy_IP:80
   reqadd X-Forwarded-Proto:\ http
   default_backend www-backend

frontend www-https
   bind your_ha_proxy_IP:443 ssl crt /etc/haproxy/certs/your_domain.pem
   reqadd X-Forwarded-Proto:\ https
   acl letsencrypt-acl path_beg /.well-known/acme-challenge/
   use_backend letsencrypt-backend if letsencrypt-acl
   default_backend www-backend

backend www-backend
   redirect scheme https if !{ ssl_fc }
   # setting session stickiness
   cookie SERVERID insert indirect nocache
   server us your_us_server_IP:8080 check cookie us1
   server uk your_uk_server_IP:8080 check cookie uk1

backend letsencrypt-backend
   server letsencrypt 127.0.0.1:54321

Finalmente, assumirei que você tem um servidor/serviço DNS em algum lugar onde você pode registrar o IP desse servidor HAproxy como um registro A para que você possa acessá-lo por um nome completo como “your-app-name.mydomain.com”.

Configuração básica do Phoenix

  • Objetivo: configure o aplicativo Phoenix para ser implementável. Configure os servidores para que tenham os arquivos de configuração necessários.
  • Para fazer: descubra uma maneira de reduzir os tempos de implantação super lentos.

Finalmente, temos quase tudo no lugar.

Vou assumir que você tem uma aplicação Phoenix de trabalho já no lugar. Se não, crie uma a partir dos muitos tutoriais para aí afora.

Eu reuni essas informações de textos como este muito útil do Pivotal sobre uma implantação baseada em AWS. Em resumo, você deve fazer um número de mudanças em sua configuração.

Quando você está desenvolvendo sua aplicação, vai notar que sempre que executá-la, ela vai fazer delta-compile no que mudou. Os bits binários estão em _build/dev ou _build/test na forma de binários .beam (semelhante ao que .class são para Java).

Diferente de Ruby, Python ou PHP, você não está implantando o código-fonte nos servidores de produção. É mais parecido com o Java, onde você deve ter tudo compilado em bits binários e empacotado no que é chamado de release. É como um “.war” ou “.ear”, se você é de Java.

Para criar esse pacote, as pessoas costumam usar “exrm”, mas está sendo substituído por “distillery“, então vamos usá-lo.

Então, se você é de Ruby, está familiarizado com Capistrano. Ou se você é de Python, conhece o clone de Capistrano, Fabric. Elixir tem uma ferramenta similar (muito mais simples nesse ponto), chamada de “edeliver“. É sua ferramenta básica de automação SSH.

Adicione-os ao mix.exs como qualquer outra dependência:

...
def application do
  [mod: {ExPusherLite, []},
   applications: [..., :edeliver]]
end

defp deps do
  [...,
   {:edeliver, "~> 1.4.0"},
   {:distillery, "~> 1.0"}]
end
...

A partir do artigo do Pivotal, a coisa importante a não esquecer é editar esta parte no arquivo config/prod.exs:

http: [port: 8080],
url: [host: "your-app-name.yourdomain.com", port: 80],
...
config :phoenix, :serve_endpoints, true

Você DEVE codar na mão o PORT padrão do servidor web Phoenix e o domínio permitido (você se lembra do nome de domínio que você associou ao seu servidor HAProxy acima? Esse mesmo). E você DEVE descomentar a linha :serve_endpoints, true!

Para o edeliver funcionar, você tem que criar um arquivo .deliver/config como este:

USING_DISTILLERY=true

# change this to your app name:
APP="your-app-name"

# change this to your own servers IP and add as many as you want
US="nyc-ip-address"
UK="lon-ip-address"

# the user you created in your Ubuntu machines above
USER="pusher"

# which server do you want to build the first release?
BUILD_HOST=$US
BUILD_USER=$USER
BUILD_AT="/tmp/edeliver/$APP/builds"

# list the production servers declared above:
PRODUCTION_HOSTS="$US $UK"
PRODUCTION_USER=$USER
DELIVER_TO="/home/$USER"

# do not change here

LINK_VM_ARGS="/home/$USER/vm.args"

# For *Phoenix* projects, symlink prod.secret.exs to our tmp source
pre_erlang_get_and_update_deps() {
  local _prod_secret_path="/home/$USER/prod.secret.exs"
  if [ "$TARGET_MIX_ENV" = "prod" ]; then
    __sync_remote "
      ln -sfn '$_prod_secret_path' '$BUILD_AT/config/prod.secret.exs'

      cd '$BUILD_AT'

      mkdir -p priv/static

      mix deps.get

      npm install

      brunch build --production

      APP='$APP' MIX_ENV='$TARGET_MIX_ENV' $MIX_CMD phoenix.digest $SILENCE
    "
  fi
}

Lembra-se das informações que estamos reunindo desde o início desta longa receita? Estas são as opções que você DEVE mudar para o seu próprio bem. Basta seguir os comentários no conteúdo do arquivo acima e adicioná-los ao seu repositório git. A propósito, seu projeto está em um repositório GIT apropriado, NÃO ESTÁ??

Se você gosta de usar as chaves confidenciais SSH protegidas por senhas, então vai ser uma dor enorme para implantar, porque, para cada comando, edeliver irá emitir um comando SSH que vai continuar pedindo para você as senhas, uma dúzia de vezes através de tudo. Você foi avisado! Se você ainda não se importa com isso e estiver em um Mac, você terá um problema extra porque o Terminal não será capaz de criar um prompt para você inserir sua senha. Você deve criar um script /usr/local/bin/ssh-askpass:

#!/bin/bash
# Script: ssh-askpass
# Author: Mark Carver
# Created: 2011-09-14
# Licensed under GPL 3.0

# A ssh-askpass command for Mac OS X
# Based from author: Joseph Mocker, Sun Microsystems
# http://blogs.oracle.com/mock/entry/and_now_chicken_of_the

# To use this script:
#   Install this script running INSTALL as root
#
# If you plan on manually installing this script, please note that you will have
# to set the following variable for SSH to recognize where the script is located:
#   export SSH_ASKPASS="/path/to/ssh-askpass"

TITLE="${SSH_ASKPASS_TITLE:-SSH}";
TEXT="$(whoami)'s password:";
IFS=$(printf "\n");
CODE=("on GetCurrentApp()");
CODE=(${CODE[*]} "tell application \"System Events\" to get short name of first process whose frontmost is true");
CODE=(${CODE[*]} "end GetCurrentApp");
CODE=(${CODE[*]} "tell application GetCurrentApp()");
CODE=(${CODE[*]} "activate");
CODE=(${CODE[*]} "display dialog \"${@:-$TEXT}\" default answer \"\" with title \"${TITLE}\" with icon caution with hidden answer");
CODE=(${CODE[*]} "text returned of result");
CODE=(${CODE[*]} "end tell");
SCRIPT="/usr/bin/osascript"
for LINE in ${CODE[*]}; do
      SCRIPT="${SCRIPT} -e $(printf "%q" "${LINE}")";
done;
eval "${SCRIPT}";

Agora faça isto:

sudo chmod +x /usr/local/bin/ssh-askpass
sudo ln -s /usr/local/bin/ssh-askpass /usr/X11R6/bin/ssh-askpass

Lembre-se, apenas para Macs. E agora, toda vez que você tenta implantar, você receberá um número de janelas de prompt gráfico pedindo a senha privada SSH. É assustadoramente irritante! E você deve ter XQuartz instalado, a propósito.

Agora, você deve criar manualmente 3 arquivos em todos os servidores Phoenix. Comece com your-app-name/vm.args:

-name us@nyc-ip-address
-setcookie @bCd&fG
-kernel inet_dist_listen_min 9100 inet_dist_listen_max 9155
-config /home/pusher/your-app-name.config
-smp auto

O /home/pusher/your-app-name é o diretório onde a versão será descompactada após o edeliver implementar o tarball de lançamento.

Você deve criar esse arquivo em todas as máquinas Phoenix, a propósito, mudando o bit -name para o mesmo nome que você declarou no arquivo .deliver/config. O -setcookie deve ser qualquer nome, desde que seja o mesmo em todos os servidores.

Viu o -config /home/pusher/your-app-name.config? Crie esse arquivo com o seguinte:

[{kernel,
  [
    {sync_nodes_optional, ['uk@lon-ip-address']},
    {sync_nodes_timeout, 30000}
  ]}
].

Esse é um código-fonte Erlang. Na máquina de NYC, você deve declarar o nome de Londres, e vice-versa. Se você tem várias máquinas, todas elas, exceto aquela em que você está agora. Entendeu?

Finalmente, para o aplicativo Phoenix em si, você sempre tem um config/prod.secret.exs que nunca deve ser git added ao repositório, lembra? É aqui que você coloca as informações do servidor PostgreSQL e a chave secreta aleatória para assinar os cookies de sessão:

use Mix.Config

config :your_app_name, YourAppName.Endpoint,
  secret_key_base: "..."

# Configure your database
config :your_app_name, YourAppName.Repo,
  adapter: Ecto.Adapters.Postgres,
  username: "pusher",
  password: "your-super-secure-pg-password",
  database: "your-app-database-name",
  hostname: "pg-ip-address",
  pool_size: 20

# if you have Guardian, for example:
config :guardian, Guardian,
  secret_key: "..."

Como você cria uma nova chave secreta aleatória? De sua máquina de desenvolvimento, basta executar: mix phoenix.gen.secret e copiar a string gerada para o arquivo acima.

Portanto, agora você deve ter esses 3 arquivos em cada servidor Phoenix, na pasta /home/pusher:

~/your-app-name/vm.args
~/prod.secret.exs
~/your-app-name.config

Como pela documentação de distillery, você tem que definir uma variável de ambiente para deixá-la saber sobre o arquivo vm.args, e isso é SUPER importante. Caso contrário, ela irá gerar um padrão que não define o nome próprio e cookie, para que os nodes não se encontrem após o booting up.

Usando o sudo, edite o /etc/environment e adicione a linha:

RELEASE_CONFIG_DIR=/home/pusher/your-app-name
VMARGS_PATH=/home/pusher/your-app-name/vm.args

Você tem que fazê-lo em todos os servidores phoenix.

No diretório de desenvolvimento local, você ainda precisa executar isto:

mix release.init

Ele irá gerar um arquivo rel/config.exs padrão que você deve adicionar ao seu repositório git com as seguintes mudanças no fundo:

...
environment :prod do
  plugin Releases.Plugin.LinkConfig
  ...
end

release :your_app_name do
  set version: current_version(:your_app_name)

  set applications: [
    your_app_name: :permanent
  ]
end

O plugin deve permitir que a versão encontre o arquivo vm.args local no servidor (isso é super importante; caso contrário, os comandos remotos, como start, stop, ping etc. não funcionarão, e os nodes não carregarão as informações corretas para boot up e formar um cluster).

Finalmente, tudo pronto, você pode emitir este comando:

$ mix edeliver build release production --skip-mix-clean --verbose

Se ele terminar sem erros, você terá uma mensagem como a seguinte no final:

...
==> Release successfully built!
    You can run it in one of the following ways:
      Interactive: _build/prod/rel/your-app-name/bin/your-app-name console
      Foreground: _build/prod/rel/your-app-name/bin/your-app-name foreground
      Daemon: _build/prod/rel/your-app-name/bin/your-app-name start
--> Copying release 0.0.1 to local release store
--> Copying your-app-name.tar.gz to release store

RELEASE BUILD OF YOUR-APP-NAME WAS SUCCESSFUL!

Agora, isso levará um tempo absurdamente longo para ser implantado. Isso porque ele vai versionar o clone e o código-fonte do seu aplicativo, buscar todas as dependências Elixir (toda vez!), ele terá que compilar tudo, então ele irá executar o super lento npm install (toda vez!), brunch seus ativos, criar o chamado “release”, tar e gzip-lo, baixá-lo e SCP-lo para as outras máquinas que você configurou.

No arquivo .deliver/config, você define uma opção BUILD_HOST. Esta é a máquina onde todo esse processo ocorre, então você vai querer ter pelo menos essa máquina sendo mais parruda do que as outras. Como eu estou usando pequenas droplets de 512Mb, o processo leva muito tempo.

O último comando irá baixar o tarball gerado. Ele tem que fazer isso para se certificar de que você tem os NIFs compilados no ambiente nativo onde é executado, porque se você usar um Mac, binários de Macs não serão executados no Linux.

Agora, devemos carregar e descomprimir esse tarball em cada servidor com o seguinte comando:

mix edeliver deploy release to production

Quanto mais lenta for a sua rede, mais tempo demorará, uma vez que ela está carregando um tarfile grande na Internet pública. Por isso, certifique-se de que está numa conexão rápida. E, quando terminar, podemos reiniciar os servidores (se você já teve instâncias em execução):

mix edeliver stop production
mix edeliver start production

Se você fizer tudo certo, o processo edeliver termina sem qualquer erro, e ele deixa um daemon em execução no seu servidor, como este:

/home/pusher/your-app-name/erts-8.2/bin/beam -- -root /home/pusher/your-app-name -progname home/pusher/your-app-name/releases/0.0.1/your-app-name.sh -- -home /home/pusher -- -boot /home/pusher/your-app-name/releases/0.0.1/your-app-name -config /home/pusher/your-app-name/running-config/sys.config -boot_var ERTS_LIB_DIR /home/pusher/your-app-name/erts-8.2/../lib -pa /home/pusher/your-app-name/lib/your-app-name-0.0.1/consolidated -name us@nyc-ip-address -setcookie ex-push&r-l!te -kernel inet_dist_listen_min 9100 inet_dist_listen_max 9155 -config /home/pusher/your-app-name.config -mode embedded -user Elixir.IEx.CLI -extra --no-halt +iex -- console

Ainda vai levar muito tempo, mas deve se tornar mais fácil. Portanto, este é um pro-tip para vocês, usuários do Linux. Siga este Gist para obter mais detalhes, você deve emular o que é executado a partir da metade inferior do arquivo .deliver/config.

Observe também que executei as migrações manualmente, mas você pode fazer isso usando o mix edeliver migrate.

Leia a documentação para obter mais comandos e configurações.

Além disso, não se esqueça de habilitar o UFW:

sudo ufw allow ssh
sudo ufw allow 8080
sudo ufw allow 4369
sudo ufw allow proto tcp from any to any port 9100:9155
sudo ufw default allow outgoing
sudo ufw enable

Depurando erros de produção

Logo após eu ter feito a implementação, obviamente ela falhou. E o problema é que os arquivos /home/pusher/your-app-name/log/erlang.log (eles são girados automaticamente para que você possa encontrar vários arquivos terminados em um número), você não verá muitos.

O que eu recomendo que você faça é mudar o arquivo config/prod.exs SOMENTE em sua máquina de desenvolvimento e alterar a configuração de log para config: logger, level:: debug, usar o mesmo prod.secret.exs que você editou nos servidores acima e executá-lo localmente com
MIX_ENV = prod iex -S mix phoenix.server.

Por exemplo, no modo de desenvolvimento, eu tinha um código no controlador que estava verificando a existência de um parâmetro de string de consulta opcional como este:

if params["some_parameter"] do
 ...

Isso estava funcionando bem no desenvolvimento, mas falhando na produção, então eu tive que mudá-lo para:

if Map.has_key?(params, "some_parameter") do
 ...

Outra coisa foi que o Guardian estava trabalhando normalmente no desenvolvimento, mas na produção eu tive que declarar sua aplicação no mix.exs desta forma:

def application do
  [mod: {ExPusherLite, []},
   applications: [..., :guardian, :edeliver]]
end

Eu estava encontrando erros :econnrefused porque me esqueci de executar MIX_ENV=prod mix do ecto.create, ecto.migrate como eu instruí acima. Uma vez que eu percebi isso, meu aplicativo estava instalado e funcionando através do http://your-app-name.yourdomain.com, HAProxy foi corretamente encaminhado para a porta 8080 nos servidores e tudo corre bem, incluindo as conexões WebSocket.

Conclusão

Como eu mencionei acima, esse tipo de procedimento me faz realmente sentir falta de uma solução fácil de implantar, como Heroku.

O único problema que estou enfrentando agora é que quando eu logo através da página de login do Coherence, eu não sou redirecionado para o URI correto que estou tentando (“/ admin” no meu caso). Às vezes, recarregar após o login funciona, às vezes não. Às vezes, estou dentro de uma página “/ admin”, mas quando eu clico em um dos links, ele me envia de volta para a página de login, embora eu já esteja conectada. Não tenho certeza se é um “mas” em Coherence, ExAdmin, Phoenix em si ou uma má configuração HAProxy. Vou atualizar este artigo se eu descobrir.

Edeliver também leva uma quantidade obscena de tempo para implantar. Mesmo esperando por sprockets para processar em um git push heroku master, implantar parece ser mais rápido em comparação. E isso é para um aplicativo Phoenix muito cru. Tendo que buscar tudo (porque Hex não mantém um cache global local, todas as dependências são estaticamente vendorizadas no diretório do projeto). Ter que executar o npm super lento também não ajuda.

Eu ainda preciso pesquisar se há opções mais rápidas, mas por agora o que eu tenho “funciona”.

E, mais importante, agora eu tenho um cluster escalável para WebSockets bidirecionais em tempo real, que é a principal razão pela qual alguém pode querer usar Phoenix em primeiro lugar.

Se você quer construir um website “normal”, mantenha-o simples e faça-o no Rails, no Django, no Express ou no framework web de sua escolha. Se você quiser comunicações em tempo real da maneira mais fácil, eu posso ter uma solução melhor. Fique atento para que as notícias venham logo! 😉

***

Artigo traduzido com autorização do autor. Publicado originalmente em  http://www.akitaonrails.com/2016/12/23/elixir-phoenix-app-deployed-into-a-load-balanced-digitalocean-setup.