Runtime configuration help

I have an application running on Elixir 1.10.3 and I’m trying to get the runtime configurations figured out. My releases.exs file contains

import Config

config :my_application,
  sample: "helloworld"

I can then proceed to start the daemon and remote into the program and run this:

Application.fetch_env!(:my_application, :sample)

And get the expected “helloworld” response. It is to my understanding that I can change the contents of that file (within the _build directory), change that value to something else, and then restart the program, and it will reload the configuration in the releases.exs file, right? This doesn’t seem to work though. Is there something else I need to add? I want to move some variables from compile time to be changeable at runtime

1 Like

I’m not sure you should ever be modifying files in the _build directory, since that directory is for artifacts of the build, I would try rebuilding the release after you make changes to the source releases.exs file. Also, are you sure the _build directory you are referring to is the one for the release and not just for the app when you are running it locally with mix?

Also, the function of releases.exs is to provide runtime configuration in the way that you could read environment variables in via something like System.fetch_env/1 at runtime. I don’t think the function of “runtime configuration” in this context pertains to changing the file that reads the runtime config itself. Rather, the function of releases.exs is so that anything inside that file is evaluated when you run the app, instead of when you generate your release like the other configuration files.

2 Likes

Ooooh okay I think I get it now. I’m sure the _build directory I’m referring to is a part of a release. I do run mix release to make the actual release, and not what’s present in the dev environment when you use iex -S mix. Is there some sort of best practices for setting these environment variables at runtime? I’ve done environment variables for compile time. Would it be as simple as say, having my environment variables in a file, run source varibles_file.sh and then restarting the app?

Yes, that’s one of the ways.

Another way is to put those env vars in the command, and put that command in a shell script.

Yet another way is to containerize your release and put those env vars in the container.

The most secure way IMHO, is to carry those env vars from your dev machine to the prod machine through SSH. See the SendEnv in /etc/ssh/ssh_config on your dev machine, and the AcceptEnv in /etc/ssh/sshd_config (with a d) on your prod machine. In this way, those env vars are nowhere to be found on the prod machine after you close the SSH connection.

2 Likes

In addition to the methods that @Aetherus mentioned, the way I do it is that I add them as passthroughs in my docker-compose.yml file under the environment key, and then I send over a .env file via SCP to the deploy server which is populated by my CI with the correct environment variables. That way, if I SSH into my deployment server and I need to restart the app, etc. all the environment variables will still be correctly loaded as they are saved into that file and not simply passed in once when the app was started.

It’s currently being rewritten for GitHub actions but you can take a look at my process here: https://github.com/jswny/deploy

2 Likes

Wow this is probably one of the coolest things I’ve read. That sending environment variables over SSH idea is amazing

1 Like

One thing I am not certain about after seeing all this. I was still under the impression that a restart could reload the environment variables, but that doesn’t seem to be true. There has to be a full stop and a full start. And doing that too fast seems to keep the app from starting as a daemon again.

That’s interesting. What’s the command for restarting the app which causes the env vars not being reloaded?

I built my program with mix release and started the app with _build/dev/rel/myapp/bin/myapp daemon…works great. Before doing my restart I edited my .env file with the new value for my key sample. My releases config is:

import Config

config :myapp,
  sample: System.fetch_env!("SAMPLE")

If I run echo $SAMPLE I get my value of “HELLO”. Likewise, if I run Application.fetch_env!(:myapp, :sample) I get that, as well as System.fetch_env!("SAMPLE")

I then edit the .env file and change SAMPLE to “WORLD”, then source .env and verify that the change is correct and then restart the app with

_build/dev/rel/myapp/bin/myapp restart …this does not recognize the environment variable change. Not unless I do a full stop and start the daemon again

Confirmed. I noticed that the OS process is not actually restarted. The PID of that OS process keeps the same after restarting using bin/myapp restart ($PWD is the release path). I think that’s the reason why the env vars are not updated in that OS process.

1 Like

So that makes me wonder then…what would be the appropriate way? I am thinking of a scenario where you have to have as little downtime as possible, and that few seconds of downtime just to reload a config (full stop and start) could be damaging to your business model. With some platforms like node, you can send a signal to a node process and it will reload configs off disk. That’s more what I’m shooting for here and I thought the runtime configuration could be the solution. I know there are hot upgrades, but I feel like a compile and a revision bump is a bit excessive for what could be a small variable change?

I can’t imagine Elixir/Erlang not having something like this considering the full intent of it being a platform built around resiliency

1 Like

I wanted to revisit this, and perhaps do this in what I think would be an Elixir sort of method…

What if there was a config file on disk (JSON, YAML, whatever) with an absolute path defined in your mix config files. Create a GenServer to sleep for X minutes, read that file, and store the values. Then the rest of the program can fetch those values from the GenServer. This would make it so you didn’t have to compile, stop, and start the daemon. That would fit my use case. Otherwise have something that watches for the file changing and do the same having a GenServer read the configs.