Running Migrations, etc.

A very common task as part of deployment is the ability to run migrations, or other automated prep work prior to starting the new version. There are a number of approaches I’ve seen people take using the primitives Distillery provides, however my favored approach is one that I have not seen people use yet, and it surprises me because it is so easy and feels much more comfortable to use.

The approach is the following:

  • Define a module which will execute the migrations, this is a common requirement of all approaches to running migrations in a release.
  • Define a custom command which will execute this module for you without requiring that you type the module, function, and arguments yourself.

Migration Module

The following code is an example of a module which will run your Ecto migrations:

defmodule MyApp.ReleaseTasks do

  @start_apps [
    :crypto,
    :ssl,
    :postgrex,
    :ecto
  ]
  
  @myapps [
    :myapp
  ]

  @repos [
    MyApp.Repo
  ]

  def seed do
    IO.puts "Loading myapp.."
    # Load the code for myapp, but don't start it
    :ok = Application.load(:myapp)

    IO.puts "Starting dependencies.."
    # Start apps necessary for executing migrations
    Enum.each(@start_apps, &Application.ensure_all_started/1)

    # Start the Repo(s) for myapp
    IO.puts "Starting repos.."
    Enum.each(@repos, &(&1.start_link(pool_size: 1)))

    # Run migrations
    Enum.each(@myapps, &run_migrations_for/1)

    # Run the seed script if it exists
    seed_script = Path.join([priv_dir(:myapp), "repo", "seeds.exs"])
    if File.exists?(seed_script) do
      IO.puts "Running seed script.."
      Code.eval_file(seed_script)
    end

    # Signal shutdown
    IO.puts "Success!"
    :init.stop()
  end

  def priv_dir(app), do: "#{:code.priv_dir(app)}"

  defp run_migrations_for(app) do
    IO.puts "Running migrations for #{app}"
    Ecto.Migrator.run(MyApp.Repo, migrations_path(app), :up, all: true)
  end

  defp migrations_path(app), do: Path.join([priv_dir(app), "repo", "migrations"])
  defp seed_path(app), do: Path.join([priv_dir(app), "repo", "seeds.exs"])

end

Custom Command

Place the following shell script at rel/commands/migrate.sh:

#!/bin/sh

bin/myapp command Elixir.MyApp.ReleaseTasks seed

Tying it all together

Now that we have our custom command and migrator module defined, we just need to set up our config appropriately:

...

release :myapp do
  ...
  set commands: [
    "migrate": "rel/commands/migrate.sh"
  ]
end

...

Now, once you’ve deployed your application, you can run migrations with bin/myapp migrate. Easy as pie.

Thoughts

There are other approaches that may make more sense for your use case, for example, automatically running migrations by defining a pre-start hook which does basically the same thing as above, just in a hook instead of a command. You can even define the command, and execute the command as part of the hook, giving you the flexibility of both approaches.

Custom commands give you a lot of power to express potentially complex operations as a terse statement. I would encourage you to use them for these types of tasks rather than using the raw rpc and command constructs!