You need to run you're migrations with the production release of your Elixir app, in your production environment. You can't use mix! You can use Ecto Migrator. Read on to find out how to run your Ecto migrations in production using Distillery's Boot Hooks.
The Problem
Coming from a Rails background, I just sort of expected to be able to execute my migrations in the production environment exactly like I would in dev.
mix ecto.migrate
I was confused when my attempts to do so in production were met with:
** (Mix) The task "ecto.migrate" could not be found
Turns out, mix tasks aren't compiled into the project BEAMs, and therefore they aren't deployed with the release. Womp womp.
Lucky for us, we can leverage the easy-to-use Ecto Migrator API to define a custom migration task. Then, we can run this task with Distillery's Boot Hooks.
A Note on Distillery
This problem (no mix in prod) and the solution of using Ecto Migrator was revealed to me by this excellent post by Flatiron Labs engineer Kate Travers. Sadly for me, Kate's project used eDeliver for deployments, which made it even easier to leverage Ecto Migrator. I had to take a slightly different approach to get my migration script working with Distillery.
Writing a Custom Migration Task with Ecto Migrator
Our custom migration task is pretty simple. We'll define a module MyApp.ReleaseTasks
that looks like this:
defmodule MyApp.ReleaseTasks do
def migrate do
{:ok, _} = Application.ensure_all_started(:my_app)
path = Application.app_dir(:my_app, "priv/repo/migrations")
Ecto.Migrator.run(MyApp.Repo, path, :up, all: true)
end
end
Our task does a few things:
- Make sure the app is started
- Grab the path to the migrations directory
- Use
Ecto.Migrator
to run the migrations against our app'sRepo
.
Now that we have a nice tidy function that wraps up our Ecto.Migrator
work, let's write a shell script that Distillery can execute for us when it starts running a production release.
Writing a Migration Shell Script
Leveraging Erlang's rpc
Module
In order to execute our ReleaseTasks.migration/0
function against our production release, we'll use Erlang's rpc
module. The rpc
module provides Remote Produce Call services
A remote procedure call is a method to call a function on a remote node and collect the answer. It is used for collecting information on a remote node, or for running a function with some specific side effects on the remote node.
rpc
calls are executed like this:
:rpc.call(node, module, fun, args)
We can send an rpc
call to our production release like this:
bin/my_app rpc "Elixir.MyApp.ReleaseTasks.migrate"
Now that we know how to call on our task, let's write that shell script!
The Migration Shell Script
set +e
echo "Preparing to run migrations"
while true; do
nodetool ping
EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ]; then
echo "Application is up!"
break
fi
done
set -e
echo "Running migrations"
bin/my_app rpc "Elixir.MyApp.ReleaseTasks.migrate"
echo "Migrations run successfully"
Our script is pretty simple, it loops until our application is up and running (with the help of the nodetool command line utility). Then, once the app is up, it uses rpc
to execute our migration function.
Now that our shell script is ready to go, we need to teach Distillery when to execute it.
Executing the Migration Script with Distillery's Boot Hooks
Distillery allows us to execute shell scripts at points in time via Boot Hooks.
Boot hooks are scripts which execute pre/post to events that occur in the run control script.
Whatever scripts we specify as a pre/post hook will get sourced into Distillery's own boot script at runtime.
We want our migration task to run as a post_start
script, since it requires the app to be running.
In order to leverage boot hooks, we need to do two things:
- Tell Distillery that we have a post start script for it to run
- Place the post start script in the correct file path
# rel/config.exs
use Mix.Releases.Config,
default_release: :default,
default_environment: Mix.env()
...
environment :prod do
set include_erts: true
set include_src: false
set post_start_hooks: "rel/hooks/post_start"
end
...
This will tell Distilerry to execute any scripts in the rel/hooks/post_start
directory after the app starts up. So, we need to place our migration script in a file:
rel/hooks/post_start/migrate.sh
And that's it! When we build our production release and run it, our migrations will run.