@Version : 0.1.0
@Build : abcd
By using this site, you acknowledge that you have read and understand the Cookie Policy, Privacy Policy, and the Terms. Close


Elixir Mix Releases By Example - Powerful and Inbuilt

Posted Friday, August 27th, 2021

ElixirOTP and BEAMErLangElixir MixElixir Mix Releases
Elixir Mix Releases By Example - Powerful and Inbuilt

Prerequisites

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.

Introductions

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.

Creating an Elixir Application

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]}

Creating the first release.

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.

Interacting with the deployment

Once you deploy your release, you can interact with the release like getting a remote console and executing functions while it is running.

Connect to a running daemon deployment.

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>

Call a function in the remote console.

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>

Umbrella Apps

In umbrella apps, the release works the same with a few added requirements and functionalities.

  • You must define the release.
  • You can release subsets of the umbrella apps.

Dummy Umbrella App

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

App 1
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.
App 2
 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 the release.

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]}

Release Tasks

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

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!