Como ingenieros de infraestructura cada vez es más común participar en proyectos de entrega de software en donde el
desarrollo se basa en el lenguaje de programación Elixir
, el cual se basa en la máquina virtual de Erlang
,
es decir, la BEAM
.
En esta guía mostramos cómo construir proyectos basados en Elixir y Phoenix. Iremos desde lo más básico que es
instalar las dependencias del sistema para poder crear nuestros proyectos, usaremos el manejador de paquetes asdf
para simplificar el manejo de las versiones de erlang
y elixir
que vamos a necesitar.
En los ejercicios que realicemos usaremos Elixir 12
con Erlang/OTP 23
.
El principal interés es que los ingenieros de infraestructura aprendan los principios para construir proyectos basados en Elixir y que sea más sencillo participar en un proyecto de desarrollo basado en Elixir, al final esperamos:
- Aprender a instalar Erlang y Elixir en modo binario
- Aprender a crear proyectos basados en Elixir y el framework Phoenix
- Aprender a modificar las configuraciones de un proyecto elixir
- Aprender a hacer liberaciones de proyectos basados en elixir
Para poder realizar estos ejercicios es necesario tener una instancia del servidor de bases de datos Postgres,
en MacOS puede ser instalado a través de la aplicación Postgres.app
, con esta aplicación podrá crear diferentes
instancias de bases de datos en diferentes versiones, lo cual facilita el desarrollo.
Otra forma práctica de levantar un servidor de base de datos es a través de contenedores docker.
Para estos ejercicios usaremos Postgres versión 12
.
Instalaremos algunas dependencias de herramientas sistema para hacer nuestro trabajo:
$ brew install coreutils git curl unzip
Y también instalamos dependenciasd desarrollo:
$ brew install autoconf automake readline fop libyaml libxslt libtool unixodbc wxmac
Ahora debemos instalar versión 1.1
de openssl
:
$ brew install openssl@1.1
Arreglamos los links:
$ brew unlink openssl@2
$ brew unlink openssl@3
$ brew link openssl@1.1
Agregamos el path de openssl:
$ vim $HOME/.zshrc
Al final del archivo agregamos:
# openssl@1.1 custom path
export PATH="/opt/homebrew/opt/openssl@1.1/bin:$PATH"
Recargamos zsh:
$ source $HOME/.zshrc
Ahora si instalamos asdf con brew:
$ brew install asdf
Agregamos asdf
al arranque del shell:
$ vim $HOME/.zshrc
Al final agregamos:
# load asdf
. /opt/homebrew/opt/asdf/libexec/asdf.sh
Recargamos el shell:
$ source $HOME/.zshrc
Verificamos la instalación de asdf
:
$ asdf --version
v0.10.2
Agregamos los plugins para erlang
y elixir
:
$ asdf plugin-add erlang https://github.com/asdf-vm/asdf-erlang.git
$ asdf plugin-add elixir https://github.com/asdf-vm/asdf-elixir.git
Instalamos erlang
en su versión mayor 23
desde brew:
$ brew install erlang@23
Agregamos el path de erlang al shell:
$ vim $HOME/.zshrc
Al final del archivo agregamos:
# erlang@23 custom path
export PATH="/opt/homebrew/opt/erlang@23/bin:$PATH"
export LDFLAGS="-L/opt/homebrew/opt/erlang@23/lib"
Recargamos el shell:
$ source $HOME/.zshrc
$ mkdir ~/.asdf/installs
$ cp -r /opt/homebrew/opt/erlang@23/lib/erlang ~/.asdf/installs/erlang/23.3.4.18
$ asdf reshim erlang 23.3.4.18
$ asdf global erlang 23.3.4.18
Instalamos los plugins para `elixir` en versión mayor `1`, versión menor `12` y compatibilidad con erlang/otp `23`:
```shell
$ asdf install elixir 1.12-otp-23
Verificamos la instalación de elixir:
$ elixir --version
Erlang/OTP 23 [erts-11.2.2.17] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [dtrace]
Elixir 1.12.3 (compiled with Erlang/OTP 23)
Como se puede ver en la salida, nos muestra que la versión de elixir coincide con la versión de OTP 23.
Ahora que tenemos todos los requisitos listos en nuestra máquina, creamos un directorio de trabajo para el nuevo proyecto:
$ mkdir -p $HOME/code/elixir-dev
$ cd $HOME/code/elixir-dev
Ahora, los siguientes comandos se deben realizar dentro del directorio del nuevo proyecto. Configuramos la versión de erlang
para el proyecto local:
$ asdf local erlang 23.3
Configuramos la versión de elixir
para el proyecto local:
$ asdf local elixir 1.12-otp-23
Esto nos crea el archivo .tool-versions
:
$ cat .tool-versions
erlang 23.3
elixir 1.12-otp-23
Ahora instalamos hex
:
$ mix local.hex
Are you sure you want to install "https://repo.hex.pm/installs/1.12.0/hex-1.0.1.ez"? [Yn] y
* creating .asdf/installs/elixir/1.12-otp-23/.mix/archives/hex-1.0.1
También instalamos rebar
:
$ mix local.rebar
* creating .asdf/installs/elixir/1.12-otp-23/.mix/rebar
* creating .asdf/installs/elixir/1.12-otp-23/.mix/rebar3
Instalamos el archive installer de Phoenix, el cual es usado para generar la estructura base del proyecto:
$ mix archive.install hex phx_new
Resolving Hex dependencies...
Dependency resolution completed:
New:
phx_new 1.6.10
* Getting phx_new (Hex package)
All dependencies are up to date
Compiling 11 files (.ex)
Generated phx_new app
Generated archive "phx_new-1.6.10.ez" with MIX_ENV=prod
Generamos un proyecto Phoenix en el directorio src
, la aplicación del proyecto se llama hello
y desactivamos
el uso de html
ya que crearemos solo una API:
$ mix phx.new src --app hello --no-html --no-assets
* creating src/config/config.exs
* creating src/config/dev.exs
* creating src/config/prod.exs
* creating src/config/runtime.exs
* creating src/config/test.exs
* creating src/lib/hello/application.ex
* creating src/lib/hello.ex
* creating src/lib/hello_web/views/error_helpers.ex
* creating src/lib/hello_web/views/error_view.ex
* creating src/lib/hello_web/endpoint.ex
* creating src/lib/hello_web/router.ex
* creating src/lib/hello_web/telemetry.ex
* creating src/lib/hello_web.ex
* creating src/mix.exs
* creating src/README.md
* creating src/.formatter.exs
* creating src/.gitignore
* creating src/test/support/conn_case.ex
* creating src/test/test_helper.exs
* creating src/test/hello_web/views/error_view_test.exs
* creating src/lib/hello/repo.ex
* creating src/priv/repo/migrations/.formatter.exs
* creating src/priv/repo/seeds.exs
* creating src/test/support/data_case.ex
* creating src/lib/hello/mailer.ex
* creating src/lib/hello_web/gettext.ex
* creating src/priv/gettext/en/LC_MESSAGES/errors.po
* creating src/priv/gettext/errors.pot
Fetch and install dependencies? [Yn] Y
* running mix deps.get
* running mix deps.compile
We are almost there! The following steps are missing:
$ cd src
Then configure your database in config/dev.exs and run:
$ mix ecto.create
Start your Phoenix app with:
$ mix phx.server
You can also run your app inside IEx (Interactive Elixir) as:
$ iex -S mix phx.server
Y listo!!!, ahora el código base se encuentra en el directorio src
, echemos un vistazo.
Nos cambiamos al directorio src
donde realizaremos las tareas:
$ cd src
Ya que nuestra aplicación será una API, debemos desactivar la visualización de errores en modo debug en formato HTML,
para esto editamos config/dev.exs
y en el Endpoint
de HelloWeb
cambiamos la llave debug_errors
a false
,
por ejemplo:
config :hello, HelloWeb.Endpoint,
# Binding to loopback ipv4 address prevents access from other machines.
# Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
http: [ip: {127, 0, 0, 1}, port: 4000],
check_origin: false,
code_reloader: true,
debug_errors: false,
secret_key_base: "eyLT2BnWB8FgjAdvqdx6OJ7UFU3ahZQdY8EmJvajhnmUhy2/TCGRjrpoJsvLaJNk",
watchers: []
Ahora editamos el archivo config/dev.exs
para revisar la configuración a la base de datos, esta es definida en el Repo
, por ejemplo:
# Configure your database
config :hello, Hello.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "hello_dev",
stacktrace: true,
show_sensitive_data_on_connection_error: true,
pool_size: 10
Aquí por default se define la configuración para el repositorio, esto es la base de datos en donde se almacenan los datos de forma persistente y estructurada. Note que cada parámetro de conexión es una llave dedicada.
Ahora si podemos inicializar la base de datos:
$ mix ecto.create
Compiling 11 files (.ex)
Generated hello app
The database for Hello.Repo has already been created
Esto nos crea una base de datos en la instancia Postgres local, nos conectamos a Postgres y con la opción \l
listamos las bases de datos:
$ psql "host=localhost dbname=postgres"
postgres=# \l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
-----------+----------+----------+-------------+-------------+-----------------------
hello_dev | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 |
foo.lano | foo.lano | UTF8 | en_US.UTF-8 | en_US.UTF-8 |
postgres | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 |
template0 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/postgres +
| | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/postgres +
| | | | | postgres=CTc/postgres
(5 rows)
postgres=# \c hello_dev
psql (14.3, server 12.11)
You are now connected to database "hello_dev" as user "jmedina".
hello_dev=# \dt
Did not find any relations.
Note que en el comando de arriba se listan las bases de datos default: postgres
, template0
, template1
,
foo.lano
y la recién creada hello_dev
.
Arriba también podemos notar que con el comando \c hello_dev
nos podemos cambiar a esa base de datos,
y el comando \dt
se pueden listar las tablas.
Ahora sí estamos listos para correr el servidor Phoenix en local:
$ mix phx.server
mix phx.server
[info] Running HelloWeb.Endpoint with cowboy 2.9.0 at 127.0.0.1:4000 (http)
[debug] Downloading esbuild from https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.29.tgz
[info] Access HelloWeb.Endpoint at http://localhost:4000
[watch] build finished, watching for changes...
Ahora puedes apuntar tu navegador a https://localhost:4000
.
Deberás de visualizar un error como el siguiente:
{"errors": {"detail":"Not Found"}}
Esto es normal porque no se ha definido una ruta, en la siguiente sección se creará una ruta simple.
También puedes probar usando la línea de comandos con el comando curl
, por ejemplo:
$ curl -H "Content-Type: application/json" -X GET http://localhost:4000/
{"errors":{"detail":"Not Found"}}
Ahora que tenemos nuestro primer proyecto corriendo, es aconsejable dejar el servidor corriendo en primer plano
para estar viendo como llegan las peticiones al servidor web que se levanta en el puerto 4000
.
Ahora vamos a crear un ruta simple para el index del scope /
, editamos el archivo lib/hello_web/router.ex
:
$ vim lib/hello_web/router.ex
Después de el pipeline :api
y el scope /api
definimos el scope /
:
scope "/", HelloWeb do
pipe_through :api
get "/", IndexController, :index
end
Ahora creamos el controlador para el índice en lib/hello_web/controllers
:
$ vim lib/hello_web/controllers/index_controller.ex
El contenido sería algo así:
defmodule HelloWeb.IndexController do
use HelloWeb, :controller
def index(conn, _params) do
text conn, "Hello World!"
end
end
Ahora recargamos el navegador para volver a lanzar la petición, deberíamos de recibir el siguiente mensaje en respuesta:
Hello World!
También podemos hacer la petición en tipo json con curl, por ejemplo:
$ curl -H "Content-Type: application/json" -X GET http://localhost:4000/
Hello World!
Verifiquemos que en la salida estándar del servidor Phoenix se registran las peticiones realizadas.
[info] GET /
[debug] Processing with HelloWeb.IndexController.index/2
Parameters: %{}
Pipelines: [:api]
[info] Sent 200 in 617µs
Felicidades acabas de crear tu primer proyecto en elixir.
Si quieres ver un dashboard de la operación de Elixir nos dirigimos a la url: http://localhost:4000/dashboard/home
.
Preparamos ambiente test:
$ export MIX_ENV=test
$ export DATABASE_URL=ecto://postgres:postgres@localhost/hello_test
$ mix ecto.create
Cuando se crea un proyecto de Phoenix por default se incluyen unas pruebas, estas se pueden ejecutar así:
$ mix test
..
Finished in 0.04 seconds (0.04s async, 0.00s sync)
2 tests, 0 failures
NOTA: En otro momento hablaremos más de las pruebas.
Antes de hacer commit a git con estos cambios recomiendo hacer una revisión de format usando mix format
,
este comando ejecuta una tarea de revisar la sintaxis de los archivos del proyecto, y también hace correcciones
para mejorar la lectura y cumplir con las diferentes recomendaciones.
$ mix format
Se aconseja que siempre que se edite un archivo, al guardar los cambios se haga mix format para validar y corregir, es bueno hacerse de este habito, también se pueden apoyar de plugins en los editores de texto ó en su IDE de preferencia.
En otro momento hablaremos más de las pruebas.
Antes de terminar preparamos el proyecto para subir los cambios a git:
$ cd ..
$ git checkout foo.lano/initial_api
$ git add .tool-versions
$ git commit -m "Add asdf elixir and erlang plugin versions" .tool-versions
$ mv src/.gitignore .
$ vim .gitignore
$ # Agrega prefijo /src a directorios _build, cover, deps, doc y .fetch
$ git add .gitignore
$ git commit -m "Update gitignore file with one created by phoenix." .gitignore
$ mv src/README.md .
$ git commit -m "Update README with one created by phoenix." README.md
$ git add src
$ git commit -m "Initial commit for elixir phoenix api." src
$ git push --set-upstream origin $(git_current_branch)
Ahora vamos a cambiar la configuración para el ambiente test, en especial la configuración del repo, por default la configuración del repo está así:
# Configure your database
#
# The MIX_TEST_PARTITION environment variable can be used
# to provide built-in test partitioning in CI environment.
# Run `mix help test` for more information.
config :hello, Hello.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "hello_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Ecto.Adapters.SQL.Sandbox,
pool_size: 10
En esta configuración del repo se deben de definir los parámetros de conexión a la base de datos de forma independiente, es decir, un parámetro para el username, otro para el password, para el hostname, el nombre de la base de datos y el tamaño del pool de conexiones.
Cambiamos ese bloque por algo así:
# Configure your database
#
config :hello, Hello.Repo, pool: Ecto.Adapters.SQL.Sandbox
Aquí se puede ver que se quitan todos los parámetros, solo se deja el del pool, esto es porque dichos parámetros los definiremos en tiempo de ejecución.
En el archivo de config/runtime.ex
eliminamos todas las configuraciones actuales y solo dejamos esto:
import Config
config :hello, Hello.Repo,
url: System.fetch_env!("DATABASE_URL"),
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "5")
Con esto definimos los parámetros de conexión a la base de datos en formato de url, es decir, en un format en donde en un solo string se define el usuario, la contraseña, el servidor y la base de datos, esto facilita la configuración tanto para el ambiente de pruebas como el de producción.
En este caso, el valor de url
lo obtendremos desde una variable de ambiente llamada DATABASE_URL
, su valor
sería algo así:
ecto://postgres:postgres@localhost/hello_test
Adicionalmente, definimos el tamaño del pool por medio de una variable de ambiente llamada POOL_SIZE
, a la cual
se le asigna un valor default de 5
.
Vamos a crear un endpoint en donde podemos registrar y consultar una lista de tareas.
$ mix phx.gen.context Todo Task tasks name:string:unique description:string completed:boolean
* creating lib/hello/todo/task.ex
* creating priv/repo/migrations/20220711024839_create_tasks.exs
* creating lib/hello/todo.ex
* injecting lib/hello/todo.ex
* creating test/hello/todo_test.exs
* injecting test/hello/todo_test.exs
* creating test/support/fixtures/todo_fixtures.ex
* injecting test/support/fixtures/todo_fixtures.ex
Remember to update your repository by running migrations:
$ mix ecto.migrate
Ejecutamos las migraciones de la base de datos:
$ mix ecto.migrate
Compiling 2 files (.ex)
Generated hello app
21:49:45.215 [info] == Running 20220711024839 Hello.Repo.Migrations.CreateTasks.change/0 forward
21:49:45.218 [info] create table tasks
21:49:45.231 [info] create index tasks_name_index
21:49:45.235 [info] == Migrated 20220711024839 in 0.0s
Ejecutamos nuevamente los tests:
$ MIX_ENV=test mix test
Compiling 3 files (.ex)
Generated hello app
..........
Finished in 0.1 seconds (0.07s async, 0.04s sync)
10 tests, 0 failures
Randomized with seed 805128
Ahora creamos el modelo:
$ mix phx.gen.json Todo Task tasks name:string:unique description:string completed:boolean --no-schema --no-context
* creating lib/hello_web/controllers/task_controller.ex
* creating lib/hello_web/views/task_view.ex
* creating test/hello_web/controllers/task_controller_test.exs
* creating lib/hello_web/views/changeset_view.ex
* creating lib/hello_web/controllers/fallback_controller.ex
Add the resource to your :api scope in lib/hello_web/router.ex:
resources "/tasks", TaskController, except: [:new, :edit]
Editamos el archivo del router y definimos la ruta:
$ vim lib/hello_web/router.ex
En el scope de /api
agregamos el resource /tasks
:
scope "/api", HelloWeb do
pipe_through :api
resources "/tasks", TaskController, except: [:new, :edit]
end
Hacemos una petición para crear un registro:
$ curl -H "Content-Type: application/json" -X POST http://localhost:4000/api/tasks -d '{"task":{"name": "diviértete jugando con Phoenix", "description": "crear una API Elixir", "completed": false}}'
{"data":{"completed":false,"description":"crear una API Elixir","id":1,"name":"diviértete jugando con Phoenix"}}
Consultamos los registros:
$ curl -H "Content-Type: application/json" -X GET http://localhost:4000/api/tasks
{"data":[{"completed":false,"description":"crear una API Elixir","id":1,"name":"diviértete jugando con Phoenix"}]}
TODO.
Estando en el directorio con el código fuente ejecutamos:
$ mix release.init
* creating rel/vm.args.eex
* creating rel/remote.vm.args.eex
* creating rel/env.sh.eex
* creating rel/env.bat.eex
Estos archivos son usado para crear inicializar los releases de las aplicaciones, eliminamos el archivo bat
ya
que no lo usaremos en la plataforma unix:
$ rm rel/env.bat.eex
Leer las siguientes referencias adicionales para complementar la guía:
- Brew - The Missing Package Manager for macOS (or Linux)
- asdf - Manage multiple runtime versions with a single CLI tool
- Install Elixir using asdf
- Phoenix Framework
- Phoenix Up and running
- Elixir mix new task
- Elixir - Compatibility and Deprecations
- Staknine - Elixir Config and Environment Variables
- Elixir - Introduction to Deployments
- Elixir - Deploying with Releases
- Mix releases
- Postgres.app