Jekyll2021-11-19T19:49:46+00:00https://slvrtrn.github.io///Serge’s personal blogApplication configuration using environment variables in Rust2021-11-16T21:17:18+00:002021-11-16T21:17:18+00:00https://slvrtrn.github.io//2021/11/16/application-configuration-environment-variables-rust<p>Environment variables are one of the ways to deal with your application configuration. Generally speaking, it is even preferable in case you deploy your application into a Kubernetes cluster. In a NodeJS application, this is typically done via <a href="https://www.npmjs.com/package/dotenv">dotenv</a> which reads variables from either environment itself or <code class="language-plaintext highlighter-rouge">.env</code> file.</p>
<p>As I was using NodeJS mostly with TypeScript, I was familiar with libraries such as <a href="https://www.npmjs.com/package/dotenv-safe">dotenv-safe</a> which is used for additional verification with <code class="language-plaintext highlighter-rouge">.env.example</code> file and <a href="https://www.npmjs.com/package/dotenv-parse-variables">dotenv-parse-variables</a> that allows parsing Booleans, Numbers, and Arrays instead of having everything as Strings.</p>
<p>Combining all three together, it is possible to read the configuration file with proper types without too much boilerplate, as well as crash the application if any of the variables are missing. For example:</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">dotenvParseVariables</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">dotenv-parse-variables</span><span class="dl">'</span>
<span class="k">import</span> <span class="nx">dotenvSafe</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">dotenv-safe</span><span class="dl">'</span>
<span class="k">export</span> <span class="kd">type</span> <span class="nx">Env</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">NODE_ENV</span><span class="p">:</span> <span class="dl">'</span><span class="s1">production</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">development</span><span class="dl">'</span>
<span class="na">MAGIC_STRING</span><span class="p">:</span> <span class="kr">string</span>
<span class="na">MAGIC_NUMBER</span><span class="p">:</span> <span class="kr">number</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">ENV</span> <span class="o">=</span> <span class="p">{</span>
<span class="p">...</span><span class="nx">dotenvParseVariables</span><span class="p">(</span><span class="nx">dotenvSafe</span><span class="p">.</span><span class="nx">config</span><span class="p">().</span><span class="nx">parsed</span><span class="p">),</span>
<span class="na">IS_DEVELOPMENT</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NODE_ENV</span> <span class="o">!==</span> <span class="dl">'</span><span class="s1">production</span><span class="dl">'</span><span class="p">,</span>
<span class="na">IS_PRODUCTION</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NODE_ENV</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">production</span><span class="dl">'</span><span class="p">,</span>
<span class="p">}</span> <span class="k">as</span> <span class="nx">Env</span>
</code></pre></div></div>
<p>Recently, I decided to try Rust as a language for a typical backend application containing a few Kafka consumers inside, as well as a web server to provide liveness probes and other low-key utility endpoints. The necessity to deal with the configuration files popped up immediately since it is required to configure Kafka broker host, group id, topic names, and a lot of other things. Digging through GitHub projects, eventually, I stumbled upon <a href="https://github.com/softprops/envy">envy</a>, which provided almost exact functionality as I was searching for. However, out of the box, it does not support <code class="language-plaintext highlighter-rouge">.env</code> files, so the following code:</p>
<div class="language-rs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">serde</span><span class="p">::</span><span class="n">Deserialize</span><span class="p">;</span>
<span class="nd">#[derive(Deserialize,</span> <span class="nd">Debug)]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">Config</span> <span class="p">{</span>
<span class="k">pub</span> <span class="n">magic_string</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
<span class="k">pub</span> <span class="n">magic_number</span><span class="p">:</span> <span class="nb">u16</span><span class="p">,</span>
<span class="k">pub</span> <span class="n">rust_env</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
<span class="p">}</span>
<span class="k">impl</span> <span class="n">Config</span> <span class="p">{</span>
<span class="k">pub</span> <span class="k">fn</span> <span class="nf">is_production</span><span class="p">(</span><span class="o">&</span><span class="k">self</span><span class="p">)</span> <span class="k">-></span> <span class="nb">bool</span> <span class="p">{</span>
<span class="k">self</span><span class="py">.rust_env</span> <span class="o">==</span> <span class="s">"production"</span>
<span class="p">}</span>
<span class="k">pub</span> <span class="k">fn</span> <span class="nf">is_development</span><span class="p">(</span><span class="o">&</span><span class="k">self</span><span class="p">)</span> <span class="k">-></span> <span class="nb">bool</span> <span class="p">{</span>
<span class="o">!</span><span class="k">self</span><span class="nf">.is_production</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="k">-></span> <span class="nn">std</span><span class="p">::</span><span class="nn">io</span><span class="p">::</span><span class="n">Result</span><span class="o"><</span><span class="p">()</span><span class="o">></span> <span class="p">{</span>
<span class="nn">envy</span><span class="p">::</span><span class="nn">from_env</span><span class="p">::</span><span class="o"><</span><span class="n">Config</span><span class="o">></span><span class="p">()</span><span class="o">?</span><span class="p">;</span>
<span class="nf">Ok</span><span class="p">(())</span>
<span class="p">}</span>
</code></pre></div></div>
<p>will panic despite having <code class="language-plaintext highlighter-rouge">.env</code> file with the following contents next to it:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>RUST_ENV=development
MAGIC_STRING=foobar
MAGIC_NUMBER=42
</code></pre></div></div>
<p>Not a big deal! This issue can be worked around quite easily. In case of failure to read an environment variable from the environment itself, we can implement an error handling branch that will read <code class="language-plaintext highlighter-rouge">.env</code> file contents, load key/value pairs into the environment, and call <code class="language-plaintext highlighter-rouge">envy</code> again!</p>
<p>Let’s try to do that, adding the following function and changing the main call:</p>
<div class="language-rs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">load_config</span><span class="p">()</span> <span class="k">-></span> <span class="nn">std</span><span class="p">::</span><span class="nn">io</span><span class="p">::</span><span class="n">Result</span><span class="o"><</span><span class="n">Config</span><span class="o">></span> <span class="p">{</span>
<span class="k">let</span> <span class="n">env</span> <span class="o">=</span> <span class="nn">envy</span><span class="p">::</span><span class="nn">from_env</span><span class="p">::</span><span class="o"><</span><span class="n">Config</span><span class="o">></span><span class="p">();</span>
<span class="k">match</span> <span class="n">env</span> <span class="p">{</span>
<span class="c">// if we could load the config using the existing env variables - use that</span>
<span class="nf">Ok</span><span class="p">(</span><span class="n">config</span><span class="p">)</span> <span class="k">=></span> <span class="nf">Ok</span><span class="p">(</span><span class="n">config</span><span class="p">),</span>
<span class="c">// otherwise, try to load the .env file</span>
<span class="nf">Err</span><span class="p">(</span><span class="mi">_</span><span class="p">)</span> <span class="k">=></span> <span class="p">{</span>
<span class="c">// simulate https://www.npmjs.com/package/dotenv behavior</span>
<span class="k">let</span> <span class="k">mut</span> <span class="n">file</span> <span class="o">=</span> <span class="nn">File</span><span class="p">::</span><span class="nf">open</span><span class="p">(</span><span class="s">".env"</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>
<span class="k">let</span> <span class="k">mut</span> <span class="n">content</span> <span class="o">=</span> <span class="nn">String</span><span class="p">::</span><span class="nf">new</span><span class="p">();</span>
<span class="n">file</span><span class="nf">.read_to_string</span><span class="p">(</span><span class="o">&</span><span class="k">mut</span> <span class="n">content</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>
<span class="k">for</span> <span class="n">line</span> <span class="n">in</span> <span class="n">content</span><span class="nf">.lines</span><span class="p">()</span> <span class="p">{</span>
<span class="k">let</span> <span class="n">pair</span> <span class="o">=</span> <span class="n">line</span><span class="nf">.split</span><span class="p">(</span><span class="sc">'='</span><span class="p">)</span><span class="py">.collect</span><span class="p">::</span><span class="o"><</span><span class="nb">Vec</span><span class="o"><&</span><span class="nb">str</span><span class="o">>></span><span class="p">();</span>
<span class="k">let</span> <span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span> <span class="o">=</span> <span class="k">match</span> <span class="o">&</span><span class="n">pair</span><span class="p">[</span><span class="o">..</span><span class="p">]</span> <span class="p">{</span>
<span class="o">&</span><span class="p">[</span><span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">]</span> <span class="k">=></span> <span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">),</span>
<span class="mi">_</span> <span class="k">=></span> <span class="nd">panic!</span><span class="p">(</span><span class="s">"Expected env variable pairs, got {}"</span><span class="p">,</span> <span class="n">content</span><span class="p">),</span>
<span class="p">};</span>
<span class="nn">std</span><span class="p">::</span><span class="nn">env</span><span class="p">::</span><span class="nf">set_var</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">match</span> <span class="nn">envy</span><span class="p">::</span><span class="nn">from_env</span><span class="p">::</span><span class="o"><</span><span class="n">Config</span><span class="o">></span><span class="p">()</span> <span class="p">{</span>
<span class="nf">Ok</span><span class="p">(</span><span class="n">config</span><span class="p">)</span> <span class="k">=></span> <span class="nf">Ok</span><span class="p">(</span><span class="n">config</span><span class="p">),</span>
<span class="nf">Err</span><span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="k">=></span> <span class="nd">panic!</span><span class="p">(</span><span class="s">"Failed to read the config from env: {}"</span><span class="p">,</span> <span class="n">e</span><span class="p">),</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="k">-></span> <span class="nn">std</span><span class="p">::</span><span class="nn">io</span><span class="p">::</span><span class="n">Result</span><span class="o"><</span><span class="p">()</span><span class="o">></span> <span class="p">{</span>
<span class="k">let</span> <span class="n">config</span> <span class="o">=</span> <span class="nf">load_config</span><span class="p">()</span><span class="o">?</span><span class="p">;</span>
<span class="nf">Ok</span><span class="p">(</span><span class="nd">println!</span><span class="p">(</span><span class="s">"Magic number is {}"</span><span class="p">,</span> <span class="n">config</span><span class="py">.magic_number</span><span class="p">))</span>
<span class="p">}</span>
</code></pre></div></div>
<p>After that, if we run the application, there is no more panic, and we can see the proper output:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Magic number is 42
</code></pre></div></div>
<p>Other than that, there is one more improvement we can do to our config - make it a static const available from everywhere using <a href="https://docs.rs/lazy_static/1.4.0/lazy_static/">lazy_static</a> crate. With that crate, we will allow more complex initialization to a static const (in our case, using a function call).</p>
<p>Let’s just move all the code except the main function into some <code class="language-plaintext highlighter-rouge">globals.rs</code>, adding</p>
<div class="language-rs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">lazy_static</span><span class="p">::</span><span class="nd">lazy_static!</span> <span class="p">{</span>
<span class="k">pub</span> <span class="k">static</span> <span class="k">ref</span> <span class="n">CONFIG</span><span class="p">:</span> <span class="n">Config</span> <span class="o">=</span> <span class="nf">load_config</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>
<p>there as well. In <code class="language-plaintext highlighter-rouge">main.rs</code>, we can have it as</p>
<div class="language-rs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">globals</span><span class="p">::</span><span class="n">CONFIG</span><span class="p">;</span>
<span class="k">mod</span> <span class="n">globals</span><span class="p">;</span>
<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="k">-></span> <span class="nn">std</span><span class="p">::</span><span class="nn">io</span><span class="p">::</span><span class="n">Result</span><span class="o"><</span><span class="p">()</span><span class="o">></span> <span class="p">{</span>
<span class="nn">lazy_static</span><span class="p">::</span><span class="nf">initialize</span><span class="p">(</span><span class="o">&</span><span class="n">CONFIG</span><span class="p">);</span>
<span class="nf">Ok</span><span class="p">(</span><span class="nd">println!</span><span class="p">(</span><span class="s">"Magic number is {}"</span><span class="p">,</span> <span class="n">CONFIG</span><span class="py">.magic_number</span><span class="p">))</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Now, the configuration will be initialized in a non-lazy way with either environment variables or <code class="language-plaintext highlighter-rouge">.env</code> file, if present, and will be available as a static const to any other module in the application. Goal completed!</p>
<p>For the full source code listing, please refer to <a href="https://github.com/slvrtrn/rust-service/blob/9e293c2fa15098e7ebee88621ab5fcad43e92f7b/src/globals.rs">my pet project repo</a>.</p>Environment variables are one of the ways to deal with your application configuration. Generally speaking, it is even preferable in case you deploy your application into a Kubernetes cluster. In a NodeJS application, this is typically done via dotenv which reads variables from either environment itself or .env file.