Configuration service

The configuration service provides authorized publication and consumption of identified configuration data with metadata. The service supports bootstrapping.

A pluggable Ruby API is provided in ConfigurationService::Client.

The declarative specification of the service is implemented with cucumber, using a pluggable imperative orchestration providers. This allows for implementations that are not providers to the Ruby API. See ConfigurationService::Test.

A stub service provider is provided in ConfigurationService::Provider::Stub. This is used to validate the test framework architecture, and may be used as a stub configuration service in tests. Other providers are available as gems.

Service providers are registered against the ConfigurationService::ProviderRegistry.

Clients can be constructed with by the ConfigurationService::Factory.

Usage

The recommended approach to creating a configuration service client is to use the Factory.

For example, we can use the factory to create and configure a configuration service client backed by the vault provider as follows.

Our main.rb (or config.ru or whatever) is simple:

require 'bundler'
Bundler.require(:default)

service = ConfigurationService::Factory.create_client
configuraton = service.request_configuration
AcmeApplication.new(configuration.data).run

This relies on a / bundler Gemfile to load the gem that contains whatever service provider we configure in the environment:

source 'https://rubygems.org'

gem 'configuration_service-provider-vault'
gem 'acme_application'

Now we use the process environment to specify the factory context:

CFGSRV_IDENTIFIER="acme" \
CFGSRV_TOKEN="0b2a80f4-54ce-45f4-8267-f6558fee64af" \
CFGSRV_PROVIDER="vault" \
CFGSRV_PROVIDER_ADDRESS="http://127.0.0.1:8200" \
bundle exec main.rb

Note that main.rb is completely decoupled from the selection of provider and provider configuration. We could swap and/or reconfigure the provider by manipulating only the Gemfile and the environment.

Administrative client

The factory in the example above uses the process environment and (on JRuby) system properties as its context because ConfigurationService::Factory.create_client defaults to ConfigurationService::Factory::EnvironmentContext. To better understand the factory context, consider the example of a simple configuration service frontend that needs an AdminClient for administrative access to the service:

# Bad example (see below for discussion)
context = ConfigurationService::Factory::Context.new(
  "token" => "c3935418-f621-40de-ada3-cc8169f1348a",
  "provider_id" => "vault",
  "provider_config" => {
    "address" => "http://127.0.0.1:8200"
  }
)
admin_client = ConfigurationService::Factory.create_admin_client(context)

app = Rack::Builder.new do
  map "/configurations" do
    run ->(env) { [200, {'Content-Type' => 'application/json'}, [admin_client.list_configurations.to_json] }
  end
end
run app

The example above is terrible because it hardcodes an administrative token into the source. In practice, the administrative context would most likely come from the configuration service itself.

Complex example

In the following example, the ConfigurationService::Factory defaults to using the EnvironmentContext to acquire application configuration from a non-administrative ConfigurationService::Client client. Part of the application configuration is then used as the context for an administrative ConfigurationService::AdminClient, which is used as a model in the application.

configuration = ConfigurationService::Factory.create_client.request_configuration
app_name = configuration.data["app_name"]
admin_client = ConfigurationService::Factory.create_admin_client(configuration.data["cs_config"])

app = Rack::Builder.new do
  map "/" do
    run ->(env) { [200, {"Content-Type" => "application/json"}, ["Welcome to #{app_name}"]] }
  end
  map "/configurations" do
    run ->(env) { [200, {'Content-Type' => 'application/json'}, [admin_client.list_configurations.to_json] }
  end
end
run app

The above code would rely on configuration data that looked something like this:

---
app_name: ACME config service UI
cs_config:
  token: c3935418-f621-40de-ada3-cc8169f1348a
  interface: admin
  provider_id: vault
  provider_config:
    address: http://127.0.0.1:8200

If this configuration data was stored under the configuration identifier “acme”, then access to the data (via ConfigurationService::Factory::EnvironmentContext) would be arranged with a process environment like this:

CFGSRV_IDENTIFIER="acme" \
CFGSRV_TOKEN="0b2a80f4-54ce-45f4-8267-f6558fee64af" \
CFGSRV_PROVIDER="vault" \
CFGSRV_PROVIDER_ADDRESS="http://127.0.0.1:8200" \
bundle exec main.rb

So this application would use the token “0b2…4af” to access its configuration data under the identifier “acme” with a non-administrative ConfigurationService::Client client. Part of its configuration data, in this case under the key “cs_config”, provides ConfigurationService::Factory::Context for creating an administrative ConfigurationService::AdminClient, which it would then use the token “c39…48a” to operate over multiple configuration identifiers on behalf of users.