Import runtime config from apps in umbrella app root

Our umbrella application had two releases with no overlapping apps: Our “main release” and our “importer release”. By providing runtime_config_path in the releases configuration, we created one runtime.exs file for each of these releases and everything worked perfectly.

Then, we added one common app to both releases, which seems to be a valid use case: mix release — Mix v1.11.3. The problem we are facing now is how to configure the same app in the 2 releases without duplicating configuration across the 2 runtime.exs files, since that could very easily lead to bugs.

Our initial idea was to create a runtime.exs config file inside the common app’s config directory and import it in the 2 root runtime configuration files. However, the mix release documentation says: "It MUST NOT import any other configuration file via import_config". Trying to understand why is that, we found this comment from @josevalim, explaining that this is discouraged because the other config files are not copied to the release by default. Having understood the rule, we thought we were ready to break it, so we copied the files manually by adding an extra step in the release process and imported it in our runtime.exs.

However, since v1.11, the “MUST NOT” rule is enforced by code: elixir/config.ex at master · elixir-lang/elixir · GitHub, so it seems that the only way to configure this common app is by duplicating its configuration across both root runtime.exs files. Is that really the recommended approach?

Has anyone faced this problem and solved it in a different way? I do believe that there should be a way to solve this problem without duplication.

1 Like

Keep all of the shared configuration in config/runtime.exs. Then for each node, add their own specific configuration via config providers:

https://hexdocs.pm/elixir/Config.Reader.html#module-as-a-provider

You can often keep the extra configurations in the overlay directory and then point to them accordingly.

3 Likes

@gabrielpra how did you end up solving the solution?

I have a problem which I believe is similar to yours. I have an umbrella app with multiple releases, for microservices. Some service apps in the umbrella are included in multiple releases. We want to have one runtime.exs config for each app in the umbrella (which does a case config_env() to do different stuff between prod and dev). But this doesn’t work because you can only have one runtime.exs file for a release.

The config providers sound tedious because each release will have to figure out it’s dependencies and then manually copy all the runtime.exs files to the release, then write a config reader to load the elixir configs.

@josevalim, why does the runtime config system not handle runtime configs of dependencies?

Elixir has long moved away from configuration per application because it is confusing and error prone.

Regarding the original inquiry in this thread, note it is also possible to set up a base configuration and then have each individual release override only the parts it needs. This is preferred to having each configuration scattered around per application:

https://hexdocs.pm/elixir/1.12/Config.Provider.html#module-multiple-config-files

3 Likes

Sorry! I ended up forgetting to report back, but the solution was what @josevalim suggested (thank you, by the way!). We now have one base runtime.exs in the umbrella root, as well as one runtime.exs for each app that needs custom runtime configuration.

The code in mix.exs looks like this, with some explanation added:

  defp releases do
    [
      release_1: [
        version: "1.0.0",
        applications: [
          app_1: :permanent,
          app_2: :permanent,
          app_3: :permanent,
          app_4: :permanent, # assuming this app doesn't have runtime config
        ],
        include_executables_for: [:unix],
        config_providers: config_providers_for_apps([
          :app_1,
          :app_2,
          :app_3,
        ]),
        steps: [:assemble, &copy_configs/1],
      ],
      release_2: [
        version: "0.0.1",
        applications: [
          app_1: :permanent,
          app_2: :permanent,
          app_5: :permanent,
        ],
        include_executables_for: [:unix],
        runtime_config_path: "apps/app_5/config/runtime.exs", # In this case the base runtime is not the one from root, but the one in app_5
        config_providers: config_providers_for_apps([
          :app_1,
          :app_2,
        ]),
        steps: [:assemble, &copy_configs/1],
      ]
    ]
  end

  # Add the runtime.exs configuration for each provided app
  defp config_providers_for_apps(apps) do
    for app <- apps do
      {Config.Reader, path: {:system, "RELEASE_ROOT", "/apps/#{app}/config/runtime.exs"}, env: Mix.env()}
    end
  end

  # When assembling the release, we copy all the runtime.exs files defined in `config_providers` to it, keeping the relative app path to avoid collisions.
  defp copy_configs(%Mix.Release{path: release_directory_path, config_providers: config_providers} = release) do
    for {_module, path: {_context, _root, config_file_path}, env: _} <- config_providers do
      config_directory = Path.join(release_directory_path, Path.dirname(config_file_path))

      # Clean the config directory to make sure we are only including the files defined in the config_providers
      File.rm_rf!(config_directory)
      File.mkdir_p!(config_directory)

      File.cp!(Path.relative(config_file_path), Path.join(config_directory, Path.basename(config_file_path)))
    end

    release
  end

Does that help you @dylan-chong ?

3 Likes

Thank you for the comments @josevalim and @gabrielpra! That was indeed very helpful

1 Like