Everything you wanted to know about environment variables in Rails

Everything you wanted to know about environment variables in Rails

Good practices with environment variables

Featured on Hashnode

Every Rails application is using environment variables under the hood and explicitly if you will decide so. This article is a comprehensive guide focusing mostly on good practices, advanced techniques, and custom configuration options. If you know how to set environment variables (I’m sure you do!), this knowledge is the next step.

Let me start with an explanation of how environment variables work in general and then I will move deeper into practical examples of how you can write well-maintainable and readable configuration code for small and large applications.

All my notes are based on years of hands-on experience at iRonin.IT - a top software development company, where we provide custom software development and IT staff augmentation services for a wide array of technologies.

To understand environment variables

I really find it satisfying and valuable to always try to fully understand the fundamentals of the given aspect and that’s the reason why we are here. First of all: what is an environment variable? It is a configuration for the given process. A process can be nginx server, a Ruby program that is running, or anything else.

Each process has a parent. It means that each process was triggered by some other process (parent). When we acknowledge that, we need to remember:

  • Environment variables only exist with the process they belong to. If the process dies, variables die as well.

  • Processes get environment variables from a parent process and the parent can specify what variables should be exposed to children. The children cannot change their parent’s variables.

  • If you will change environment variables, they won’t be synced across other processes.

Knowing that you will understand why the environment variable will be gone after you will shut down the irb if you set it there. If you want to share variables between sessions you have to save them somewhere, for example in the .bashrc file.

The context of Rails

Rails framework is using environment variables under the hood. You probably know the ENV['RAILS_ENV'] variable that determines the name of the current environment of the application. You must set it on the staging on the production server, otherwise, the application will run in the development environment.

Usually, we need to utilize environment variables to make use of external services in our application. For all the above we need to store those variables somewhere so the processes running Rails can pick them up.

Environment variables coming from Rails

As I mentioned before, Rails is using environment variables under the hood and by setting their values we can change how the application will behave. There is more than just the variable responsible for the environment name:

  • RAILS_RELATIVE_URL_ROOT - allows defining the directory name where the Rails application sits. By default it’s / but you can change that if you want to run the application from the app1 or any other directory.

  • RAILS_CACHE_ID - allows defining expanded cache keys so you can have multiple separate cache in the same application.

  • RAILS_APP_VERSION - as above.

  • RACK_ENV - Rails will use this environment variable’s value to determine the environment name if the RAILS_ENV won’t be defined.

  • RAILS_ENV - allows defining the environment for the application.

Besides mentioned main configuration variables, Rails is also using a lot of them in Active Support or Action Mailbox libraries used internally.

Support out of the box

You don’t need any external gems to start using environment variables configuration in your application. Rails come with a solution that allows us to do this out of the box. It’s called credentials.

After generating a new Rails project, you will see two files that are responsible for handling credentials: config/master.key and config/credentials.yml.enc. The first file is a secret key which you can use to decrypt the second file. The master.key should never be published in the GIT repository.

Adding new environment variables

To edit the file you have to execute the following command:

EDITOR=nano rails credentials:edit

By changing the value of EDITOR you can tell Rails which editor you would like to use to edit the credentials. If you have the following contents of the credentials.yml.enc file:

api_key: 'my value'

You can access it by using Rails.application.credentials.api_key inside the code. The disadvantage is that every developer who has the master.key can decrypt all the credentials. To avoid such a situation you can create a configuration file and key per environment.

Separate configuration files per environment

To edit the configuration for the given environment (the command will also create the file and key if they don’t exist yet) you can run the following command:

EDITOR=nano rails credentials:edit –environment=development

After saving the file you can access the credentials as always and they will be automatically loaded with the given environment. So, if you are in the production environment, credentials will be loaded from the file located in config/credentials/production.yml.enc.

Remember to choose if you want to have a global file for credentials or file per configuration as you can’t mix the approaches. Still, with the approach with the global file you can categorize values by environment:

production: 
  api_key: 'prod'

development: 
  api_key: 'dev'

And access them explicitly Rails.application.credentials.development.api_key.

Deployment

You should use the master.key or any other environment-specific keys only in the development environment. For other environments, set the RAILS_MASTER_KEY environment variable with the content from the specific *.key file that you are using for the given environment.

After setting mentioned environment variable, you will be able to access your configuration.

Good practices

There are a few things that we need to take into account when dealing with the configuration variables to make everything as smooth and secure as possible. This section touches on this topic.

Avoid hardcoded values

The first good practice is the most obvious one. Avoid hardcoding configuration values into the code. If the given value is secret or will be different for different environments, go for the environment variables.

Ensure that critical values are set

If you use environment variables that are critical for your application to function properly, ensure that they are set. Otherwise, you might run into some serious issues that can either stop your app from starting or running properly.

The most basic way to ensure the presence of value is to use the .fetch method:

ENV.fetch('API_KEY')

When the variable won’t be set, you will get the KeyError. However, this solution does not protect you from a case where the API_KEY is set but it holds nil or empty string and you expect some other value.

In such cases, good idea is to create a initializer that verifies the most important environment variables and their values:

# config/initializers/01_verify_environment_variables.rb

required_variables = %w[API_KEY API_SECRET]

required_variables.each do |env_name|
 if ENV[env_name].blank?
   raise "Missing environment variable: #{env_name}"
 end
end

I intentionally named this initializer with the 01_ prefix as Rails processes initializers in alphabetical order and we would like to perform this check as soon as possible.

Do not use environment variables in the business logic

It is a good practice to refer to environment variables only in the following places:

  • The config directory (initializers, application.rb or environment configuration)

  • Test helpers

If you need to access the environment variable value outside the config directory and you are not using Rails credentials, you can organize the environment variables into a settings object:

class Settings
 class << self
   def api_key
     ENV['API_KEY']
   end

   def api_secret
     ENV['API_SECRET']
   end
 end
end

If you think it’s an unnecessary layer of logic keep reading as in the next paragraph I will show you how you can extend this object to add the very useful ability to change environment variables without redeploying your server.

If you need to change environment variables in real-time

Traditional environment variables can’t be updated on the fly. To create such an ability we can use Redis and extend our Settings object in the following way:

class Settings
 include Singleton

 def api_key
   get('API_KEY')
 end

 def api_secret
   get('API_SECRET')
 end

 private

 def get(key)
   redis.get(key).presence || ENV[key]
 end

 def redis
   @redis ||= Redis.new(url: ENV.fetch("REDIS_URL"))
 end
end

If you are using Sidekiq or Redis somewhere in your application, is it a good idea to utilize the connection pool. Also, the above solution is not bulletproof in the case when the Redis is down. If you would like to use it in the production environment, consider changes to make it more reliable.

Provide environment variables for those who need it

If environment variables are necessary to run the application in the development environment, ensure that it’s obvious how to configure them for the person who would start working on this project. Put the proper information into README, create .env.example file, or share the development encryption key with proper developers.

Taking care of this can save a lot of time for new developers but it also can make it easier to create new environments.

Secure your variables

In terms of security, remember the following things to not produce any information leaks:

  • Don’t store the encryption keys in the GIT repository

  • If you are using cloud solutions like AWS, ensure that only authorized users have access to the service where environment variables are configured

  • If you are using Rails credentials, use a configuration file per environment or separate values for each environment with special sections to not mix configuration between environments

  • Don’t store environment configuration files (.env) in the GIT repository. Store .env.example with example values to let developers know what configuration is required to run the app

  • Don’t use easily guessable values for your passwords and other secrets

  • Roate the credentials on a regular basis, this prevents from exploiting compromised credentials

Remember these rules as environment variables are great for configuration values but can lead to security breaches when used not in a proper manner.

Testing code that uses environment variables

If your code utilizes environment variables to control the execution flow or simply to call some external API, you need to take it into account when writing unit tests.

Use configuration specific to the test environment

You can set a subset of configuration variables specifically for the test environment. It will be easy with Rails credentials or external libraries that are utilizing .env files (create .env.test file).

Take advantage of the settings object

If you decided to use the settings object, you can easily stub the object to get the desired value without touching the ENV itself:

settings = double(api_key: '1234567890')
expect(Settings).to receive(:instance).and_return(settings)

Refactor your code

If you don’t want to use specific configuration for the test environment and take advantage of the settings object, you can refactor your code to make it more testable.

Here is the version that is using the environment variable inside the logic:

class Request
 def call
   response = RestClient.get(ENV['API_URL'])

   response.body['name']
 end
end

You can modify the code to inject the value:

class Request
 def initialize(api_url = ENV['API_URL'])
   @api_url = api_url
 end

 def call
   response = RestClient.get(api_url)

   response.body['name']
 end
end

Now, you don’t have to stub anything. All you have to do is to pass desired value to the initializer of the class that you want to test.

Environment variables and Stimulus

If you are using Stimulus to make your Rails application more interactive, at some point, you may want to pass configuration values to your Stimulus controllers. There is an easy way to do this!

Let’s assume that you want to pass api key value to your HelloController. Start with the view:

<div data-controller="hello" data-hello-key-value="<%= Settings.instance.api_key %>">
</div>

Set a special attribute called data-{controller-name}-{value-name}-value with the desired value. Then, in the controller we have to define the value:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
 static values = {
   key: String
 }
}

With the above configuration, you can call this.keyValue inside the Stimulus controller to access the value that you passed with Settings.instance.api_key inside the view. That’s it.

Handling variables with 3rd-party solutions

Of course if you don’t want to go with Rails credentials to handle environment variables, you can use some open-source libraries. Here are the most popular ones.

Dotenv

Dotenv is great because you can manage environment variables by creating various .env files. For example, you can have multiple files, one per each environment: .env.development, .env.test, etc. Once you installed the gem, create files and they are handled automatically. You can now access your configuration files using ENV inside your code.

Figaro

Figaro is similar to Dotenv at some point. However, it presents a little bit different approach. You don’t store configuration values in .env files but in the yaml file (`config/application.yml`):

development: 
  api_key: '123'
test:
  api_key: '331'

In terms of accessing values, Figaro provides more Ruby way of doing this. You simply call Figaro.env.variable_name to access the environment variable. The gem also provides a way to ensure that environment variables are present during the initialization (it’s something I already mentioned that you should do in terms of critical environment variables).

Last but not least thing

As always, pick the solution that is the best for your, unique case. However, don’t forget to keep the values secure and values needed for development, accessible for other developers.

Happy configuring!

Didn't get enough of Ruby?

Check out our free books about Ruby to level up your skills and become a better software developer.