Summary: A configuration system for Spin applications.
Owner: [email protected]
Created: March 22, 2022
Updated: July 19, 2022
It is common for applications to require configuration at runtime that isn't known at build time or is too sensitive to be stored with build artifacts:
- Logging configuration
- Per-channel (production, staging, etc) service dependency URLs
- Database secrets
Configuration within a "parent" (component or application) consists of a number of configuration "slots":
- Slots are identified by a string "key"; in order to allow unambiguous conversion to environment variables or file paths, keys are constrained:
- Keys must start with a letter (required for env vars)
- Keys consist of only lowercase ascii alphanum and
_
([a-z0-9_]
)- Only one
_
at a time and not at the end (to allow delimiting in env vars with__
)
- Only one
- Slot keys must be unique within their parent, but to allow independent development of components different parents may have identical keys
- A slot must either be marked as "required" or must be given a default value
- A slot may be marked as "secret", in which case any associated value should be handled with care (e.g. not logged)
[variables]
required_key = { required = true }
optional_key = { default = "default_value" }
secret_key = { required = true, secret = true }
Default values can use template strings to reference other slots.
[variables]
key1 = { required = true }
key2 = { default = "prefix-{{ key1 }}-suffix" }
In dependency configuration, templates strings can reference top-level config keys (those in [variables]
), "sibling" keys within the same dependency, and "ancestor" dependant configs.
- Top-level references use just the key name:
{{ top_level_key }}
- "Sibling" references use a single
.
prefix:{{ .sibling_key }}
- "Ancestor" references use multiple
.
s:{{ ..parent_dep_key }}
,{{ ...grandparent_key }}
- Circular / infinitely recursive references are not permitted
spin.toml
:
[variables]
app_root = { default = "/app" }
log_file = { default = "{{ app_root }}/log.txt" }
...
[[component.config]]
work_root = "{{ app_root }}/work" # -> "/app/work"
work_out = "{{ .work_root }}/output" # -> "/app/work/output"
[[component.dependencies.dep1.config]]
dep_root = "{{ ..work_root }}/dep" # -> "/app/work/dep"
When resolving the value of an application configuration slot, providers will be queried in-order for a value. If no value is returned by any provider, the resolution will either use the default value or fail (if the slot is "required").
Provider configuration is handled by spin at instantiation time (spin up
).
Note: Provider configuration is TBD; as TOML it could look like:
[[config-provider]]
type = "json_file"
path = "config.json"
[[config-provider]]
type = "env"
prefix = "MY_APP_"
- Environment provider
- Configured with a prefix, e.g.
SPIN_CONFIG_
key_one
->SPIN_CONFIG_KEY_ONE
- Configured with a prefix, e.g.
- File provider
{"key_one": "value-one"}
- Vault provider
spin-config.wit
// Unknown key is a runtime error
get-config: function(key: string) -> expect<string>
- Since each component gets its own instance of the
spin-config
import, the executor can resolve keys automatically and only expose a component's own config to it.
This section contains possible future features which are not fully defined here.
The above assumes only string values, but we could include some typing:
# Type can be inferred from default value:
number_key = { default = 123 }
# equivalent to:
number_key = { type = "number", default = 123 }
required_string = { type = "string", required = true }
# "bytes" would require e.g. base64 encoding in some places
encryption_key = { type = "bytes", required = true, secret = true}
For languages without component support, we could expose config as synthetic mounted files:
key1_value = File.read("/config/key1")
# Typed config; `.json` encodes values to JSON
key1_value = JSON.parse(File.read("/config/key1.json"))
# "bytes" type; `.raw` decodes from base64
encryption_key = File.read("/config/encryption_key.raw")