Running Ecto migrations in production releases with Distillery custom commands
By DevOps on Fri 24 May 2019
inIn a dev or test environment, we execute the mix ecto.migrate
command to run
database migrations. When running from a release, however, the mix
command is not available. Instead, we need to execute Ecto.Migrator.run/4
from code.
We do this by adding a Distillery custom command called
migrate
which we call from the release script, e.g.:
/srv/foo/current/bin/foo migrate
The fundamentals of defining and running a migration are covered in this article. In a more complex app, however, we need a bit more in our script to initialize the environment.
First, in rel/config.exs
, define the migrate
command:
set commands: [
migrate: "rel/commands/migrate.sh"
]
Next, create the script rel/commands/migrate.sh
, changing Foo
to match your project module name:
release_ctl eval --mfa "Foo.Tasks.Migrate.run/1" --argv -- "$@"
Finally, add the Elixir code which runs the database migrations.
The main point is that we need to initialize the database connection
information the same way as your application normally does, e.g. by loading
a runtime file like /etc/foo/config.toml
or setting environment variables.
Following is a working example. Where you see CHANGEME
, use the name of your
project.
defmodule Foo.Tasks.Migrate do
@moduledoc "Mix task to run Ecto database migrations"
# CHANGEME: Name of app as used by Application.get_env
@app :foo
# CHANGEME: Name of app repo module
@repo_module Foo.Repo
def run(_args) do
ext_name = @app |> to_string |> String.replace("_", "-")
config_dir = Path.join("/etc", ext_name)
config_exs = Path.join(config_dir, "config.exs")
if File.exists?(config_exs) do
IO.puts "==> Loading config file #{config_exs}"
Mix.Releases.Config.Providers.Elixir.init([config_exs])
end
config_toml = Path.join(config_dir, "config.toml")
if File.exists?(config_toml) do
IO.puts "==> Loading config file #{config_toml}"
Toml.Provider.init([path: config_toml])
end
repo_config = Application.get_env(@app, @repo_module)
repo_config = Keyword.put(repo_config, :adapter, Ecto.Adapters.Postgres)
Application.put_env(@app, @repo_module, repo_config)
# Start requisite apps
IO.puts "==> Starting applications.."
for app <- [:crypto, :ssl, :postgrex, :ecto, :ecto_sql] do
{:ok, res} = Application.ensure_all_started(app)
IO.puts "==> Started #{app}: #{inspect res}"
end
# Start repo
IO.puts "==> Starting repo"
{:ok, _pid} = apply(@repo_module, :start_link, [[pool_size: 2, log: :info, log_sql: true]])
# Run migrations for the repo
IO.puts "==> Running migrations"
priv_dir = Application.app_dir(@app, "priv")
migrations_dir = Path.join([priv_dir, "repo", "migrations"])
opts = [all: true]
config = apply(@repo_module, :config, [])
pool = config[:pool]
if function_exported?(pool, :unboxed_run, 2) do
pool.unboxed_run(@repo_module, fn -> Ecto.Migrator.run(@repo_module, migrations_dir, :up, opts) end)
else
Ecto.Migrator.run(@repo_module, migrations_dir, :up, opts)
end
# Shut down
:init.stop()
end
end