Despliegue de Elixir/Phoenix con Travis y Gigalixir

Llevo ya cerca de un año jugando con Elixir pero hasta ahora no había puesto ningún proyecto serio en producción, sólo algunas pruebas de concepto con un despliegue más o menos manual. Y debo decir que en esas pruebas encontré el proceso de construcción y despliegue bastante farragoso.

Aprovechando unos días de vacaciones me he puesto a investigar si este proceso había mejorado desde la última vez que lo probé, y vaya si ha mejorado: en poco más de un día he puesto dos proyectos en producción, con pruebas y despliegue automatizado con Travis.

Vamos a verlo con una aplicación Phoenix de ejemplo, para lo que necesitaremos tener Elixir y Phoenix instalados en nuestra máquina si no los tenemos ya. En mi caso, utilizo OS X con homebrew:

$ brew update
$ brew install elixir
$ mix local.hex
$ mix archive.install https://github.com/phoenixframework/archives/raw/master/phx_new.ez

Este artículo se ha escrito utilizando Phoenix1.3 y Elixir 1.7.3. Si a pesar de las pruebas que he hecho encuentras alguna dificultad para seguirlo, déjame un comentario al pie del artículo para echarle un vistazo.

Podríamos utilizar una forma más “estándar” de instalación de Postgres o dockerizar la base datos, pero la forma más fácil de utilizar postgresql en OS X para desarrollo es instalar Postgres.app.

Una vez instalados los requisitos previos, ya podemos crear un nuevo proyecto de Phoenix.

Hemos creado una aplicación Phoenix a la que hemos llamado “gigatest” y hemos creado una base de datos para ella con Ecto, luego hemos pasado las pruebas de código y ejecutado la aplicación. Con esto ya tenemos una aplicación Phoenix corriendo en nuestra máquina en http://localhost:4000.

Phoenix Application in localhost

Queremos poner este proyecto en Github para integrarlo con Travis, para lo que crearemos un repositorio en Github y publicaremos esta versión inicial del proyecto.

De nada sirve el mejor de los proyectos si se queda en nuestra máquina, así que vamos a llevarlo a producción. Para eso vamos a utilizar Gigalixir, un PaaS construido específicamente para Elixir que, además, ofrece consultoría gratuita para proyectos de código abierto.

Gigalixir tiene una opción de prueba sin coste sin necesidad de introducir una tarjeta de crédito, aunque estaremos bastante limitados en la conectividad con las base de datos y la capacidad de nuestro servicio y no podremos utilizar algunas de las características más interesantes, como las actualizaciones en caliente o hot upgrades. Lo que sigue es una versión ampliada y revisada de la información que podéis encontrar en la guía de inicio de Gigalixir.

La herramienta de línea de comando de Gigalixir utiliza Python2 y Pip además de Git, por lo que necesitaremos tenerlos instalados. En OS X Python2 está ya instalado de serie, aunque podemos instalar la versión de homebrew (especificando la versión) para obtener también pip:

$ brew install python@2

Para instalar la utilidad de línea de comandos de Gigalixir utilizamos pip, excluyendo six puesto que OS X tiene una versión de six incompatible preinstalada.

sudo pip install gigalixir --ignore-installed six

Podemos crear una cuenta de usuario con la interfaz web de Gigalixir, pero vamos a utilizar la línea de comandos por comodidad. Durante el proceso de registro, nos pedirá una cuenta de correo electrónico que tendremos que verificar y una contraseña.

gigalixir signup

Una vez creada y verificada nuestra cuenta, ya podemos identificarnos en línea de comando y comenzar a trabajar con Gigalixir:

$ gigalixir login

La API key que nos proporciona es válida durante 365 días, el comando anterior nos ofrecerá almacenarla en ~/.netrc para autenticar todos los comandos posteriores.

Ahora ya podemos crear una aplicación en Gigalixir. Por defecto, Gigalixir generará un nombre aleatorio para nuestra aplicación y la configurará para despliegue en Google Cloud Platform en la región central de Estados Unidos. Usar Google Cloud o AWS puede ser algo más discutible, pero el nombre de nuestra aplicación -que, además, no podremos cambiar posteriormente- nos ayudará a relacionarla con un proyecto concreto. Además, si nuestra base de usuarios está en Europa probablemente querremos usar una región europea. Podemos especificar todo esto en la creación de la aplicación, ejecutando desde la raíz del proyecto el siguiente comando:

$ gigalixir create -n gigatest --cloud=gcp --region=europe-west1

Verifiquemos que se ha creado la aplicación y se ha establecido un git remoto para ella en Gigalixir además del que ya existía para Github:

$ gigalixir apps
[
  {
    "cloud": "gcp",
    "region": "europe-west1",
    "replicas": 0,
    "size": 0.5,
    "unique_name": "gigatest"
  }
]
$ git remote -v
gigalixir	https://git.gigalixir.com/gigatest.git/ (fetch)
gigalixir	https://git.gigalixir.com/gigatest.git/ (push)
origin	git@github.com:gorkaio/gigatest.git (fetch)
origin	git@github.com:gorkaio/gigatest.git (push)

Para nuestra aplicación necesitamos una base de datos, y por ahora queremos que sea una instancia gratuita a pesar de las limitaciones que ya hemos mencionado:

$ gigalixir pg:create --free

A la hora de desplegar la aplicación, podemos utilizar mix o distillery. Nosotros vamos a utilizar Distillery para generar nuestras releases, entre otras cosas porque nos permite utilizar todas las características de Elixir y -aunque no lo vamos a utilizar aquí- ha añadido soporte para proveedores de configuración desde en su versión 2, solucionando otro de los mayores quebraderos de cabeza que hasta ahora me había encontrado con Elixir.

Para ello, tenemos que añadir a nuestro archivo mix.exs la dependencia de distillery:

  # ...
  defp deps do
    {:distillery, "~> 2.0"},
    # ...
  end
  # ...

Y a continuación obtener las dependencias y preparar la release de distillery:

$ mix deps.get
$ mix release.init

Por defecto, Phoenix genera un archivo de configuración config/prod.secret.exs para almacenar secretos de producción, pero nosotros vamos a utilizar variables de entorno por lo que este archivo no es necesario. Moveremos su contenido a config/prod.exs y modificaremos su contenido para usar variables de entorno.

Eliminamos la siguiente línea de config/prod.exs y en su lugar colocaremos el contenido de config/prod.secret.exs, que ya puede ser eliminado, sustituyendo los valores adecuados por referencias al entorno:

# import_config "prod.secret.exs"

config :gigatest, GigatestWeb.Endpoint,
  load_from_system_env: true,
  http: [port: {:system, "PORT"}],
  server: true, # Sin esta línea la aplicación no inicializa un servidor web
  secret_key_base: "${SECRET_KEY_BASE}",
  url: [host: "example.com", port: 80],
  cache_static_manifest: "priv/static/cache_manifest.json"

config :gigatest, Gigatest.Repo,
  adapter: Ecto.Adapters.Postgres,
  url: "${DATABASE_URL}",
  database: "",
  ssl: true,
  pool_size: 1 # La base de datos gratuita sólo permite 2 conexiones. Los despliegues requiren n+1

Gigalixir creará automáticamente las variables de entorno SECRET_KEY_BASE y DATABASE_URL con los valores adecuados. Si necesitásemos crear nuevas variables de entorno para nuestra aplicación, podríamos hacerlo con el siguiente comando:

$ gigalixir config:set MY_VAR="My value"

Vamos a comprobar que todo funciona correctamente, empezando por la instalación de dependencias y generación de los assets de producción:

$ mix deps.get
$ cd assets
$ npm install
$ node_modules/brunch/bin/brunch build --production
$ cd ..
$ mix phx.digest

Es posible que al hacer la instalación de npm veamos un aviso de auditoría de seguridad por una vulnerabilidad descubierta en una de las dependencias. Si es así, basta con ejecutar el siguiente comando para corregirlo:

$ npm audit fix 

Ahora vamos a probar a generar y ejecutar una release local. He utilizado las credenciales por defecto de Postgres.app y el catálogo generado por nuestras pruebas para el valor de DATABASE_URL, pero es posible que tengas que ajustarlo en tu caso:

$ MIX_ENV=prod mix release --env=prod
$ MIX_ENV=prod SECRET_KEY_BASE="$(mix phx.gen.secret)" DATABASE_URL="postgresql://postgres:postgres@localhost:5432/gigatest_test" MY_HOSTNAME=example.com MY_COOKIE=secret REPLACE_OS_VARS=true MY_NODE_NAME=foo@127.0.0.1 PORT=4000 _build/prod/rel/gigatest/bin/gigatest foreground

Esto debería haber arrancado la aplicación. Para construir y desplegar, Gigalixir utiliza buildpacks. Crearemos un archivo .buildpacks en la raíz del proyecto especificando los siguientes:

https://github.com/gigalixir/gigalixir-buildpack-clean-cache.git
https://github.com/HashNuke/heroku-buildpack-elixir
https://github.com/gjaldon/heroku-buildpack-phoenix-static
https://github.com/gigalixir/gigalixir-buildpack-distillery.git

Podemos especificar también la versión necesaria de Elixir y Erlang para nuestra aplicación en un archivo que crearemos con el nombre elixir_buildpack.config:

erlang_version=21.1
elixir_version=1.7.3

|Ya estamos preparados para desplegar a producción! Aún queda pendiente la integración con Travis, pero vamos a comprobar que podemos desplegar correctamente:

$ git push gigalixir master

Después de unos minutos, nuestra aplicación estará ya disponible en producción, en un subdominio de Gigalixir:

Gigatest in production

Para un proyecto real, probablemente querremos utilizar nuestro propio dominio. Para ello, podemos registrar una asociación de dominio para esta aplicación con el siguiente comando:

gigalixir domains:add WWW.EXAMPLE.COM

Gigalixir creará la asociación y nos proporcionará un valor que tendremos que añadir a un registro CNAME de nuestro dominio, y al cabo de un tiempo ya podremos acceder a la aplicación con él.

Ahora que ya tenemos la aplicación preparada para desplegar en producción, y sabemos que el despliegue funciona correctamente, podemos pasar a configurar Travis. Nuestro objetivo es que un push a la rama master del repositorio en Github lance una construcción en Travis que ejecute las pruebas de código y, si estas pasan, despliegue a producción en Gigalixir. Siendo absolutamente estrictos, el artefacto generado para las pruebas debería ser el mismo que se pone en producción, sin una reconstrucción posterior en Gigalixir. Pero, por ahora, relajaremos esa exigencia sabiendo que las versiones de las dependencias instaladas deben ser las mismas gracias al bloqueo de dependencias y la construcción de los assets de producción tampoco debería variar.

En el panel de control de Travis enlazaremos el repositorio de nuestro proyecto en Github para que se lance una tarea con cada cambio en master, y en la sección settings, bajo Environment variables, definiremos las siguientes variables:

  • GIGALIXIR_EMAIL: nuestro email de registro en Gigalixir. Recuerda codificarlo como URI (foo%40example.com)
  • GIGALIXIR_API_KEY: la API key que nos proporciona Gigalixir en el login, que podemos consultar en ~/.netrc si elegimos esta opción al hacer login. En caso contrario, podemos hacer login de nuevo y Gigalixir nos retornará una nueva API key.
  • GIGALIXIR_APP_NAME: el nombre de nuestra aplicación, tal como aparece al ejecutar gigalixir apps

Sólo nos queda configurar nuestro proyecto para Travis, de modo que utilice Elixir para la construcción. Crearemos un archivo .travis.yml en la raíz del proyecto con el siguiente contenido:

language: elixir
elixir: 1.7.3
otp_release: '21.1'
script:
  - mix test && ./deploy.sh
services:
  - postgresql
before_script:
  - PGPASSWORD=postgres psql -c 'create database gigatest_test;' -U postgres

En este script, extraído del artículo de Sayo Egunlegan, estamos indicando a Travis que utilice un script específico para el deploy (deploy.sh) que crearemos también en la raíz del proyecto. Este script permite que Travis únicamente actualice el repositorio de Gigalixir, y lance por tanto el deploy, con los cambios a master.

#!/usr/bin/env bash

git remote add gigalixir https://$GIGALIXIR_EMAIL:$GIGALIXIR_API_KEY@git.gigalixir.com/$GIGALIXIR_APP_NAME.git

BRANCH=$(if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then echo $TRAVIS_BRANCH; else echo $TRAVIS_PULL_REQUEST_BRANCH; fi)
echo "TRAVIS_BRANCH=$TRAVIS_BRANCH, PR=$PR"
echo "------------------------------------"
echo "BRANCH=$BRANCH"

if [ "$BRANCH" == "master" ]; then
  echo "Pushing HEAD to master branch on Gigalixir."
  git push gigalixir HEAD:master --verbose
  echo "Deploy completed."
fi

echo "Exiting."

Es necesario también dar permisos de ejecución a este script para que Travis pueda ejecutarlo:

chmod +x deploy.sh

Si añadimos estos cambios al repositorio en la rama master, al cabo de unos segundos veremos cómo Travis comienza a montar el entorno y a pasar las pruebas del proyecto.

Sin embargo, las pruebas fallarán. ¿Por qué? Echando un vistazo al log de Travis, al intentar ejecutar las migraciones de la base de datos no encuentra el directorio en el que normalmente se alojan estas. Sin embargo, este directorio sí existe en local. Como no tenemos ninguna migración en nuestra base de datos, el directorio estaba vacío y no se ha incluido en el repositorio. Para solucionarlo, podemos crear un archivo .gitkeep en ese directorio para garantizar que exista en el repositorio.

Travis se lanzará de nuevo con este cambio y, finalmente, desplegará a producción nuestra aplicación con los cambios realizados.

Si bien el camino para configurar este proceso es largo, a partir de ahora cualquier cambio que realicemos en el proyecto pasará automáticamente las pruebas de código y desplegará a producción nuestra aplicación, haciendo tremendamente sencillo el ciclo de vida del proyecto.

Puedes echar un vistazo al repositorio del proyecto en GitHub y, si tienes cualquier duda, déjame un comentario e intentaré echarle un vistazo.

Happy coding!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.