Skip to Content
Course content

141: Environment-Specific Configuration

Click on the "Edit" button in the top corner of the screen to edit your slide content.

In the early days of a project, it's tempting to just hardcode your API keys or database URLs. It works, it's fast, and you can see exactly where the data is coming from. But as soon as you have a "development" machine and a "production" server, that approach becomes a liability. You don't want to accidentally charge a real credit card while testing a feature, and you definitely don't want your production secrets committed to GitHub.

The Danger of Hardcoded Credentials

Let's build a simple PaymentClient. I want this client to talk to a third-party payment gateway. If I start like this, I'm already making a mistake:

class PaymentClient
  API_KEY = "sk_test_4eC39HqLyjWDarjtT1zdp7dc" # Oops, hardcoded!
  BASE_URL = "https://api.sandbox.paymentgateway.com"

  def process_payment(amount)
    puts "Connecting to #{BASE_URL} with key #{API_KEY}..."
    # Logic to send the request would go here
  end
end

client = PaymentClient.new
client.process_payment(100)

This is fine for ten minutes. But what happens when we go live? I'd have to manually change these strings, commit them, and deploy. If I forget? I'm running production traffic against a sandbox environment. Not ideal.

Switching to Environment Variables

The standard way to handle this in Ruby is via ENV. It's a built-in Ruby hash that gives you access to your shell's environment variables. It keeps secrets out of the source code entirely.

class PaymentClient
  def initialize
    @api_key = ENV['PAYMENT_API_KEY']
    @base_url = ENV['PAYMENT_BASE_URL']
  end

  def process_payment(amount)
    raise "Missing configuration!" if @api_key.nil?
    puts "Connecting to #{@base_url} with key #{@api_key}..."
  end
end

Now, I can run this in my terminal as PAYMENT_API_KEY=test_key PAYMENT_BASE_URL=https://sandbox.api.com ruby payment_client.rb. It's cleaner, but managing ten different variables in a shell command is a nightmare. We need a way to group these by environment.

Structuring Configuration with YAML

I usually prefer a config.yml file for non-sensitive defaults, combined with environment variables for the actual secrets. It gives you a clear map of what your environments look like. I'll create a file that looks like this:

# config.yml
development:
  base_url: "https://api.sandbox.paymentgateway.com"
  timeout: 30
production:
  base_url: "https://api.paymentgateway.com"
  timeout: 10

Now, let's update the client to load this file based on an APP_ENV variable.

require 'yaml'

class PaymentClient
  def initialize
    # Load the YAML file
    config_data = YAML.load_file('config.yml')
    
    # Determine which environment we are in
    env = ENV['APP_ENV'] || 'development'
    
    # Merge YAML defaults with a sensitive ENV key
    @config = config_data[env]
    @api_key = ENV['PAYMENT_API_KEY']
  end

  def process_payment(amount)
    puts "Env: #{ENV['APP_ENV'] || 'development'}"
    puts "Connecting to #{@config['base_url']} with key #{@api_key}..."
  end
end

The "Defaulting to Production" Trap

Here is where I messed up on a project a couple of years ago. I thought it was "safer" to default to production if the environment wasn't specified, thinking that would prevent the app from failing in the wild. I wrote something like env = ENV['APP_ENV'] || 'production'.

That is a catastrophic mistake. I spent a whole afternoon debugging why my local development machine was trying to hit the production database. Always default to the safest, most restricted environment—usually development or test.

If you look at my code above, I used ENV['APP_ENV'] || 'development'. This ensures that if I'm just running the script locally without any fancy setup, I can't possibly accidentally wipe out production data.

By combining a YAML file for structural configuration and ENV` for secrets, you get a system that is easy to reason about and safe to commit to version control (provided you add config.yml to your .gitignore if it contains anything sensitive, or keep only non-secrets there).




📋 Practical Task

Build a Multi-Environment Weather Service Configurator

Your task is to create a WeatherService class that handles configuration for three environments: development, test, and production.

  • Create a weather_config.yml file containing a api_endpoint and a cache_ttl (time-to-live) for each of the three environments.
  • Implement the WeatherService class so that it:
    • Loads the weather_config.yml file.
    • Determines the current environment using ENV['WEATHER_ENV'], defaulting to 'development'.
    • Retrieves an API_KEY from ENV['WEATHER_API_KEY'].
  • Add a method connection_details that prints the endpoint, the TTL, and the API key being used.
  • Test your implementation by running the script twice in your terminal: once with WEATHER_ENV=production and once with no environment variables set.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.