strato_env
A small Ruby gem that loads layered configuration into ENV at boot. Point it at one or more paths and it sets ENV vars from the values fetched. Multiple paths form layers, with later paths overriding earlier ones — so you can split common config from host-specific or environment-specific overrides.
The default backend is AWS SSM Parameter Store, but the loader is decoupled from the backend: pass any callable that takes a path and returns a Hash<String, String> (anything responding to #call(path) — Proc, lambda, Method, or a class with #call defined). That makes testing trivial and adding new backends (Secrets Manager, Vault, a YAML file) a few lines of glue.
No Rails dependency. Works in Rails, Sinatra, Lambda, scripts, or any Ruby boot process.
The name comes from the Italian word strato, meaning "layer" — the gem is about layering configuration.
- Quick start
- Layered loading
- Preview without applying
- Custom orchestration: fetch + apply
- Where to call it from
- Customising the SSM fetcher
- Custom fetchers
- Testing
- License
- Contribution guide
Quick start
$ gem install strato_env
require "strato_env"
StratoEnv.load(paths: "/myapp/staging/")
# ENV is now populated with one entry per parameter under that path.
A parameter named /myapp/staging/DATABASE_URL becomes ENV["DATABASE_URL"]. By default, SecureString parameters are decrypted automatically.
.load returns an Array<String> of the ENV var names that were set.
Layered loading
Pass an array to layer multiple paths. Later paths override earlier ones, so the rightmost path wins on collisions.
rails_env = ENV.fetch("RAILS_ENV") # "staging"
role = ENV.fetch("HOST_ROLE") # "web" or "worker"
extra_layers = ENV.fetch("EXTRA_LAYERS", "") # "ruby4_0,blah,..."
StratoEnv.load(paths: [
"/myapp/#{rails_env}/common/",
"/myapp/#{rails_env}/#{role}/",
*extra_layers.split(',').map { "/myapp/#{rails_env}/#{it}/" },
])
This pattern lets you keep shared config (database URL, API keys) under common/ and override only the host-specific values (queue URLs, log destinations) under web/ or worker/.
I also use this for validating a new deployment of an environment before promoting it, e.g. for upgrading from Ruby 3.4 to 4.0. Sometimes you have to override a couple things so I'll layer on those refinements with /myapp/staging/ruby4_0/.
Preview without applying
StratoEnv.fetch returns the merged hash without touching ENV. Useful for previewing, logging, or applying to somewhere other than ENV.
StratoEnv.fetch(paths: ["/myapp/common/", "/myapp/web/"])
# => { "DATABASE_URL" => "postgres://...", "LOG_LEVEL" => "debug" }
Custom orchestration: fetch + apply
For more complex flows — e.g. merging from multiple namespaces, diffing them, warning on overrides — fetch each source separately, merge however you like, then apply the result. StratoEnv.load(paths:) is just sugar for apply(fetch(paths: paths)).
loader = StratoEnv.new
# Fetch two namespaces independently (no ENV side effects yet).
terraform_env = loader.fetch(paths: ["/terraform/myapp/common/", "/terraform/myapp/web/"])
human_env = loader.fetch(paths: ["/myapp/common/", "/myapp/web/"])
# Apply your own policy. Here: humans win, but warn on divergence.
terraform_env.each do |name, tf_value|
next unless human_env.key?(name) && human_env[name] != tf_value
warn("[env] #{name}: human value overrides terraform.")
end
loader.apply(terraform_env.merge(human_env))
#apply(hash) writes the hash to ENV and returns the array of names set, same return shape as #load. Also available as the class method StratoEnv.apply(hash).
Where to call it from
Call StratoEnv.load as early in boot as possible, before any code reads the ENV vars it needs to populate.
Rails
For Rails apps, put the call at the top of your environment file, before Rails.application.configure. The configure block typically reads ENV (ENV.fetch("REDIS_URL"), etc.), so the fetcher has to run first.
# config/environments/production.rb
require "strato_env"
rails_env = ENV.fetch("RAILS_ENV")
role = ENV.fetch("HOST_ROLE")
StratoEnv.load(paths: [
"/myapp/#{rails_env}/common/",
"/myapp/#{rails_env}/#{role}/",
])
Rails.application.configure do
# ... now safe to read ENV ...
end
config/initializers/*.rb runs too late — the environment file's body has already executed by then.
Lambda / scripts
Just call it before reading the values:
require "strato_env"
StratoEnv.load(paths: "/myapp/prod/")
# ... rest of your script ...
Customising the SSM fetcher
The default fetcher is StratoEnv::SSMFetcher. To pass options to it (recursive lookups, decryption off, a pre-configured client), build it yourself and pass it to StratoEnv.new:
fetcher = StratoEnv::SSMFetcher.new(
recursive: true, # fetch parameters nested under the path
with_decryption: false, # leave SecureString values encrypted
client: my_ssm_client, # custom region/credentials
)
StratoEnv.new(fetcher: fetcher).load(paths: "/myapp/staging/")
StratoEnv.load and StratoEnv.fetch are class-method shortcuts for StratoEnv.new.load / StratoEnv.new.fetch — i.e. the default SSMFetcher with all defaults.
Custom fetchers
A fetcher is any callable that takes a path and returns a Hash<String, String>. Anything that responds to #call(path) works: a Proc, a lambda, a Method object, or a class with #call defined.
yaml_fetcher = ->(path) { YAML.load_file(path) }
StratoEnv.new(fetcher: yaml_fetcher).load(paths: "config/app.yml")
This is what makes the loader trivial to test: pass a stub fetcher in your specs and assert the right keys land in ENV. No AWS SDK mocking required.
StubFetcher = Data.define(:values) do
def call(_path) = values
end
stub = StubFetcher.new(values: { "DATABASE_URL" => "postgres://test" })
StratoEnv.new(fetcher: stub).load(paths: "/anything/")
ENV["DATABASE_URL"] # => "postgres://test"
Testing
For testing the bundled SSMFetcher, the aws-sdk-ssm gem ships with stub support:
client = Aws::SSM::Client.new(stub_responses: true, region: "us-east-1")
client.stub_responses(:get_parameters_by_path, {
parameters: [
{ name: "/myapp/test/DATABASE_URL", value: "postgres://localhost/test", type: "String" },
],
})
StratoEnv::SSMFetcher.new(client: client).call("/myapp/test/")
# => { "DATABASE_URL" => "postgres://localhost/test" }
For testing your loader logic, prefer a stub fetcher (see Custom fetchers) — no AWS SDK setup needed.
License
The gem is available as open source under the terms of the MIT License.
Contribution guide
Pull requests are welcome! Make sure that new code is reasonably well tested and all the checks pass. I'm happy to provide a bit of direction and guidance if you're unsure how to proceed with any of these things.