Posted Friday, August 27th, 2021
This post assumes familiarity with Elixir and OTP/Beam. If you are new to Elixir, you can still continue with this tutorial as it is very basic and I provide straight forward commands that you just need to execute. The full code used here is also available on this GitHub respository.
Since Elixir version 1.9.0, Elixir mix supports releases. Mix releases are a way to package your application into an executable binary and archive that you can use to deploy the application. Mix release supports several operations that you can use to interact with your application much like Unix service management tasks like start
, stop
, restart
and ping
among others.
Lets imagine you have an app that tracks the current time and updates every second. The app will have a process that runs each second and updates its own state value which stores the current date.
To create a new application using mix
mix new sample_app_releases
Locate the file lib/sample_app_releases.ex
and replace with the code below. This is a GenServer process that sends a message to itself each second to update its state of UTC DateTime value.
defmodule SampleAppReleases do
use GenServer
def start_link(_) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@impl true
def init(_) do
Process.send_after(self(), :update, 1000)
{:ok, DateTime.utc_now()}
end
@impl true
def handle_call(:get, _from, current_time) do
{:reply, current_time, current_time}
end
@impl true
def handle_info(:update, _state) do
Process.send_after(self(), :update, 1000)
{:noreply, DateTime.utc_now()} |> IO.inspect()
end
def ge_current_date() do
GenServer.call(__MODULE__, :get)
end
end
Create a new file lib/application.ex
and add the code below. This adds the GenServer process to a supervision tree.
defmodule SampleAppReleases.Application do
use Application
def start(_type, _args) do
children = [ {SampleAppReleases, []}]
Supervisor.start_link(children, strategy: :one_for_one)
end
end
Locate the mix file mix.exs
and add the application to the list of mix applications. To do this add mod: {SampleAppReleases.Application, []}
to the list of application
function returned in the mix file.
defmodule SampleAppReleases.MixProject do
...
def application do
[
extra_applications: [:logger],
mod: {SampleAppReleases.Application, []}
]
end
...
end
Now test if your application setup is working fine by executing iex -S mix run
iex -S mix run
Erlang/OTP 23 [erts-11.0] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]
Interactive Elixir (1.11.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
{:noreply, ~U[2021-08-17 17:23:07.815318Z]}
{:noreply, ~U[2021-08-17 17:23:08.816313Z]}
{:noreply, ~U[2021-08-17 17:23:09.817352Z]}
If you are running your app using an elixir version 1.9 and above, you can just run the release command and you will have a release of your app. To build your first release, issue mix release
and you will get the result below.
mix release
Compiling 2 files (.ex)
Generated sample_app_releases app
* assembling sample_app_releases-0.1.0 on MIX_ENV=dev
* skipping runtime configuration (config/runtime.exs not found)
Release created at _build/dev/rel/sample_app_releases!
# To start your system
_build/dev/rel/sample_app_releases/bin/sample_app_releases start
Once the release is running:
# To connect to it remotely
_build/dev/rel/sample_app_releases/bin/sample_app_releases remote
# To stop it gracefully (you may also send SIGINT/SIGTERM)
_build/dev/rel/sample_app_releases/bin/sample_app_releases stop
To list all commands:
_build/dev/rel/sample_app_releases/bin/sample_app_releases
The above command create a binary that you can execute. Here is a list of the available management operations.
_build/dev/rel/sample_app_releases/bin/sample_app_releases help
Usage: sample_app_releases COMMAND [ARGS]
The known commands are:
start Starts the system
start_iex Starts the system with IEx attached
daemon Starts the system as a daemon
daemon_iex Starts the system as a daemon with IEx attached
eval "EXPR" Executes the given expression on a new, non-booted system
rpc "EXPR" Executes the given expression remotely on the running system
remote Connects to the running system via a remote shell
restart Restarts the running system via a remote command
stop Stops the running system via a remote command
pid Prints the operating system PID of the running system via a remote command
version Prints the release name and version to be booted
ERROR: Unknown command help 🤭 🤭 🤭
Try some of them.
Once you deploy your release, you can interact with the release like getting a remote console and executing functions while it is running.
Lets start with a remote console. You can see below, now I have access to the remote console of the app
_build/dev/rel/sample_app_releases/bin/sample_app_releases remote ─╯
Erlang/OTP 23 [erts-11.0] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]
Interactive Elixir (1.11.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(sample_app_releases@balakr-surface)1>
Now lets call the function named ge_current_date
to see the current state of the GenServer.
iex(sample_app_releases@balakr-surface)1> SampleAppReleases.ge_current_date
~U[2021-08-27 16:46:51.219157Z]
iex(sample_app_releases@balakr-surface)2> SampleAppReleases.ge_current_date
~U[2021-08-27 16:47:00.227865Z]
iex(sample_app_releases@balakr-surface)3>
You can also do basic system checks like:
iex(sample_app_releases@balakr-surface)3> :application.which_applications
[
{:iex, 'iex', '1.11.0'},
{:sample_app_releases, 'sample_app_releases', '0.1.0'},
{:logger, 'logger', '1.11.0'},
{:sasl, 'SASL CXC 138 11', '4.0'},
{:elixir, 'elixir', '1.11.0'},
{:compiler, 'ERTS CXC 138 10', '7.6'},
{:stdlib, 'ERTS CXC 138 10', '3.13'},
{:kernel, 'ERTS CXC 138 10', '7.0'}
]
iex(sample_app_releases@balakr-surface)4> :erlang.memory
[
total: 47892848,
processes: 17069280,
processes_used: 17069280,
system: 30823568,
atom: 688353,
atom_used: 665510,
binary: 38656,
code: 14953250,
ets: 1416768
]
iex(sample_app_releases@balakr-surface)5>
In umbrella apps, the release works the same with a few added requirements and functionalities.
The code for this umbrella section is available on this GitHub Repo. First generate a dummy umbrella app using
mix new sample_umbrella_releases --umbrella
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating apps
* creating config
* creating config/config.exs
Your umbrella project was created successfully.
Inside your project, you will find an apps/ directory
where you can create and host many apps:
cd sample_umbrella_releases
cd apps
mix new my_app
Commands like "mix compile" and "mix test" when executed
in the umbrella project root will automatically run
for each application in the apps/ directory.
Then cd apps
to enter the apps directory then generate two apps: app_1 and app_2
mix new app_1 ─╯
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/app1.ex
* creating test
* creating test/test_helper.exs
* creating test/app1_test.exs
Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
cd app_1
mix test
Run "mix help" for more commands.
mix new app_2 ─╯
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/app2.ex
* creating test
* creating test/test_helper.exs
* creating test/app2_test.exs
Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
cd app_2
mix test
Run "mix help" for more commands.
Now add the code from the time tracker above make the apps have same functionality.
Define three releases where one for each umbrella app and one release for both apps by adding the following to the mix project function in mix.exs
def project do
[
...
releases: [
app_1_only: [
applications: [
app_1: :permanent
]
],
app_2_only: [
applications: [
app_2: :permanent
]
],
all_apps: [
applications: [
app_1: :permanent,
app_2: :permanent
]
]
]
]
end
Now to create the releases:
mix release app_1_only
mix release app_2_only
mix release all_apps
Example after running mix release all_apps
you can run both apps like:
_build/dev/rel/all_apps/bin/all_apps start ─╯
App 1: {:noreply, ~U[2021-08-27 19:17:56.926861Z]}
App 2: {:noreply, ~U[2021-08-27 19:17:56.927195Z]}
App 1: {:noreply, ~U[2021-08-27 19:17:57.927787Z]}
App 2: {:noreply, ~U[2021-08-27 19:17:57.927957Z]}
You can do tasks while packaging the release. At the time of writing this, Elixir releases support overlays for copying files to the root of the directory of the release. To do this, you place the files in rel/overlays
For example create a file rel/overlays/timezones.json
then run the release command and see the file created at the root of the release.
mix release all_apps ─╯
* assembling all_apps-0.1.0 on MIX_ENV=dev
Release created at _build/dev/rel/all_apps!
# To start your system
_build/dev/rel/all_apps/bin/all_apps start
Once the release is running:
# To connect to it remotely
_build/dev/rel/all_apps/bin/all_apps remote
# To stop it gracefully (you may also send SIGINT/SIGTERM)
_build/dev/rel/all_apps/bin/all_apps stop
To list all commands:
_build/dev/rel/all_apps/bin/all_apps
ls _build/dev/rel/all_apps/
bin erts-11.0 lib releases timezones.json
Distillery is a hex package that has been used before mix release and as of now still offers more features like hot code reloads and advanced release tasks.
To get started just follow the steps in the getting started link then you are good to go.
Create an initial configuration
mix distillery.init
An example config file has been placed in rel/config.exs, review it,
make edits as needed/desired, and then run `mix distillery.release` to build the release
By default this will create an elixir script rel/config.exs
which has the definitions for your release. You can see the umbrella apps are included automatically in the release.
# Import all plugins from `rel/plugins`
# They can then be used by adding `plugin MyPlugin` to
# either an environment, or release definition, where
# `MyPlugin` is the name of the plugin module.
~w(rel plugins *.exs)
|> Path.join()
|> Path.wildcard()
|> Enum.map(&Code.eval_file(&1))
use Distillery.Releases.Config,
# This sets the default release built by `mix distillery.release`
default_release: :default,
# This sets the default environment used by `mix distillery.release`
default_environment: Mix.env()
# For a full list of config options for both releases
# and environments, visit https://hexdocs.pm/distillery/config/distillery.html
# You may define one or more environments in this file,
# an environment's settings will override those of a release
# when building in that environment, this combination of release
# and environment configuration is called a profile
environment :dev do
# If you are running Phoenix, you should make sure that
# server: true is set and the code reloader is disabled,
# even in dev mode.
# It is recommended that you build with MIX_ENV=prod and pass
# the --env flag to Distillery explicitly if you want to use
# dev mode.
set dev_mode: true
set include_erts: false
set cookie: :"^8:fT[va|G3V/r8~dUa]nX{.9jCGN_R*T7*yO{d{[Yn.b=AoCs9V.f5VSH@?wS>B"
end
environment :prod do
set include_erts: true
set include_src: false
set cookie: :"CGv:Yu1gb/t84h;?cuMQdS076!;&YlvJiZRA[hR&ZR822H|HV9(wIUyiC%{Nf85P"
set vm_args: "rel/vm.args"
end
# You may define one or more releases in this file.
# If you have not set a default release, or selected one
# when running `mix distillery.release`, the first release in the file
# will be used by default
release :sample_umbrella_releases do
set version: "0.1.0"
set applications: [
:runtime_tools,
app_1: :permanent,
app_2: :permanent
]
end
The available command in distillery releases differ slightly from those provided on the mix releases. Lets create a distillery release and see the available commands.
mix distillery.release
==> Assembling release..
==> Building release sample_umbrella_releases:0.1.0 using environment dev
Here is the list of commands.
_build/dev/rel/sample_umbrella_releases/bin/sample_umbrella_releases help ─╯
USAGE
sample_umbrella_releases <task> [options] [args..]
COMMANDS
start Start sample_umbrella_releases as a daemon
start_boot <file> Start sample_umbrella_releases as a daemon, but supply a custom .boot file
foreground Start sample_umbrella_releases in the foreground
console Start sample_umbrella_releases with a console attached
console_clean Start a console with code paths set but no apps loaded/started
console_boot <file> Start sample_umbrella_releases with a console attached, but supply a custom .boot file
stop Stop the sample_umbrella_releases daemon
restart Restart the sample_umbrella_releases daemon without shutting down the VM
reboot Restart the sample_umbrella_releases daemon
upgrade <version> Upgrade sample_umbrella_releases to <version>
downgrade <version> Downgrade sample_umbrella_releases to <version>
attach Attach the current TTY to sample_umbrella_releases's console
remote_console Remote shell to sample_umbrella_releases's console
reload_config Reload the current system's configuration from disk
pid Get the pid of the running sample_umbrella_releases instance
ping Checks if sample_umbrella_releases is running, pong is returned if successful
pingpeer <peer> Check if a peer node is running, pong is returned if successful
escript Execute an escript
rpc Execute Elixir code on the running node
eval Execute Elixir code locally
describe Print useful information about the sample_umbrella_releases release
No custom commands found. again 🤭 🤭 🤭
I am putting together a guide on distillery that I will link here to detail all the more features it has over mix releases.
Thank you for finding time to read my post. I hope you found this helpful and it was insightful to you. I enjoy creating content like this for knowledge sharing, my own mastery and reference.
If you want to contribute, you can do any or all of the following 😉. It will go along way! Thanks again and Cheers!