Running tenant migrations without Mix for apps deployed via mix release
Scenario
We deploy using releases, thus Mix is not available and we cannot migrate in production (or other server environments) using the Triplex Mix tasks. We run our public schema migrations via a similar Release module to the one recommended in the Phoenix documentation. To that basic structure, we added an additional migrate_tenants function:
defmodule MyApp.Release do
@app :my_app
def migrate do
load_app()
for repo <- repos() do
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
end
end
def migrate_tenants do
load_app()
for repo <- repos() do
for tenant <- Triplex.all(repo) do
Triplex.migrate(tenant, repo)
end
end
end
defp repos do
Application.fetch_env!(@app, :ecto_repos)
end
defp load_app do
Application.load(@app)
end
end
Problem
The migrate_tenants function succeeds when run via a remote IEx session or rpc, but fails when run via eval, ie bin/my_app eval "MyApp.Release.migrate_tenants()". This is in contrast to the migrate function which succeeds when run via either remote IEx, rpc, or eval. The error observed is the following:
** (RuntimeError) could not lookup Ecto repo MyApp.Repo because it was not started or it does not exist
lib/ecto/repo/registry.ex:19: Ecto.Repo.Registry.lookup/1
lib/ecto/adapter.ex:127: Ecto.Adapter.lookup_meta/1
lib/ecto/adapters/sql.ex:404: Ecto.Adapters.SQL.query/4
lib/ecto/adapters/sql.ex:362: Ecto.Adapters.SQL.query!/4
lib/triplex.ex:289: Triplex.all/1
(myapp 0.1.0) lib/myapp/release.ex:21: anonymous fn/2 in MyApp.Release.migrate_tenants/0
(elixir 1.11.4) lib/enum.ex:2193: Enum."-reduce/3-lists^foldl/2-0-"/3
(myapp 0.1.0) lib/myapp/release.ex:20: MyApp.Release.migrate_tenants/0
Questions
- Is it possible to migrate tenants using Triplex via eval on a release?
- Is there a known root cause for this issue?
- Is there a recommended approach for using Triplex with Mix releases in general?
Notes:
- This issue seems somewhat similar to https://github.com/ateliware/triplex/issues/66, but I have different questions so I opened a new issue.
- Posted same scenario on Elixir forum: https://elixirforum.com/t/how-to-run-triplex-tenant-migrations-when-deploying-via-mix-release/42135
Thanks for the report @haizop! Here are your answers:
Is it possible to migrate tenants using Triplex via eval on a release?
Should be possible, as it is basically doing pretty much the same that is done on your migrate function, with some extra options to run the migrations on the tenant prefixes.
Is there a known root cause for this issue?
By looking at the error, looks like the Repo is not started by the time Triplex.all is called, so it might be something to do with that. My guess is that your migrate function works because all the code that actually do something to DB is inside the Ecto.Migrator.with_repo, and by looking at that function you can see that they ensure the repo is started there. My suggestion is that you do the same manually like it's done there: https://github.com/elixir-ecto/ecto_sql/blob/master/lib/ecto/migrator.ex#L125
Is there a recommended approach for using Triplex with Mix releases in general?
There is nothing really specific to Triplex, the thing is that you kinda need to know some of the inner workings of ecto to actually make it work correctly, and that's an Ecto thing, not just specific to Triplex.
@kelvinst . Thank you for the reply.
I hear you that this is not a problem specific to Triplex, but given that Mix releases are a pretty standard method of deployment, do you think that some deployment guidelines should be added to Triplex documentation?
This is what I have working now:
defmodule MyApp.Release do
@moduledoc """
Used for executing DB release tasks when run in production without Mix
installed.
"""
alias Ecto.Migrator
alias MyApp.Repo
alias MyApp.Tenants
@app :my_app
def migrate_public_schema do
load_app()
{:ok, _, _} = Migrator.with_repo(Repo, &Migrator.run(&1, :up, all: true))
end
def migrate_tenant_schemas do
load_app()
Migrator.with_repo(Repo, fn repo ->
Tenants.list_tenants()
|> Enum.each(
&Migrator.run(repo, tenant_migrations_path(), :up, all: true, prefix: &1.schema_name)
)
end)
end
def rollback_public_schema(version) do
load_app()
{:ok, _, _} = Migrator.with_repo(Repo, &Migrator.run(&1, :down, to: version))
end
def rollback_tenant_schemas(version) do
load_app()
Migrator.with_repo(Repo, fn repo ->
Tenants.list_tenants()
|> Enum.each(
&Migrator.run(repo, tenant_migrations_path(), :down, to: version, prefix: &1.schema_name)
)
end)
end
defp tenant_migrations_path() do
Triplex.migrations_path(Repo)
end
defp load_app do
Application.load(@app)
end
end
@haizop definitely! A PR with a guide for that is totally welcome. Will leave this issue open to remember to do exactly that.
@kelvinst Will get you a PR in the next couple of weeks.