The more we work on an application, the more changes we introduce. Code changes are easily managed with version control, while database schema changes are handled through database migrations.
If you've been deploying your Ash application using releases, you probably want to run migrations in production without having to redeploy the entire application.
The challenge is that Elixir releases do not include Mix. As a result, you cannot run mix ash.migrate directly in production.
You need a way to execute migrations without depending on Mix. And that's exactly what I'm going to show you.
Let's walk through how to do it (assuming you're using PostgreSQL).
This approach is also documented in the ash_postgres package but it is not automatically generated for you, so you can find the official reference there as well.
Here's how to do it:
- Create
lib/my_app/release.exfile - Add below content and replace
:my_appwith the name of you appliation.
defmodule MyApp.Release do
@moduledoc """
Tasks that need to be executed in the released application (because mix is not present in releases).
"""
@app :my_app # <-- Replace this with the name of you application
def migrate do
load_app()
for repo <- repos() do
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
end
end
# only needed if you are using postgres multitenancy
def migrate_tenants do
load_app()
for repo <- repos() do
path = Ecto.Migrator.migrations_path(repo, "tenant_migrations")
# This may be different for you if you are not using the default tenant migrations
{:ok, _, _} =
Ecto.Migrator.with_repo(
repo,
fn repo ->
for tenant <- repo.all_tenants() do
Ecto.Migrator.run(repo, path, :up, all: true, prefix: tenant)
end
end
)
end
end
# only needed if you are using postgres multitenancy
def migrate_all do
load_app()
migrate()
migrate_tenants()
end
def rollback(repo, version) do
load_app()
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
end
# only needed if you are using postgres multitenancy
def rollback_tenants(repo, version) do
load_app()
path = Ecto.Migrator.migrations_path(repo, "tenant_migrations")
# This may be different for you if you are not using the default tenant migrations
for tenant <- repo.all_tenants() do
{:ok, _, _} =
Ecto.Migrator.with_repo(
repo,
&Ecto.Migrator.run(&1, path, :down,
to: version,
prefix: tenant
)
)
end
end
defp repos do
domains()
|> Enum.flat_map(fn domain ->
domain
|> Ash.Domain.Info.resources()
|> Enum.map(&AshPostgres.DataLayer.Info.repo/1)
|> Enum.reject(&is_nil/1)
end)
|> Enum.uniq()
end
defp domains do
Application.fetch_env!(@app, :ash_domains)
end
defp load_app do
Application.load(@app)
end
end3. Then you can run migrations in production release like the following:
- Basic Migrations (non-tenant): Run all pending migrations upward.
_build/prod/rel/my_app/bin/my_app eval "MyApp.Release.migrate()"- Tenant Migrations: If using multitenancy, run tenant-specific migrations.
_build/prod/rel/my_app/bin/my_app eval "MyApp.Release.migrate_tenants()"- All Migrations (tenant + non-tenant): Run everything.
_build/prod/rel/my_app/bin/my_app eval "MyApp.Release.migrate_all()"- Rollback: Roll back to a specific version (replace MyApp.Repo with your repo module and 1234567890 with the migration timestamp).
_build/prod/rel/my_app/bin/my_app eval "MyApp.Release.rollback(MyApp.Repo, 1234567890)"Since your application is already running, you can also expose this functionality on a settings or admin page. This way, you (or an authorized admin) can run migrations or roll back with just clicks with no SSH, no custom scripts, and no redeployment required.
Let me know in the comments how you've been handling migrations in production so far! I'm curious to hear about your setup.