Values built from other values
DATABASE_URL is a host, a port, a user and a password with punctuation
between them. All four usually exist already as their own keys. Writing the
composed one out by hand means remembering to change it in two places, which
nobody does — so it drifts, and is wrong in the way that takes an afternoon to
find.
A value can name another key instead:
postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/appRead that key and you get the assembled string. Change DB_PASSWORD and the
assembled string changes, including the copy already sitting in a cluster.
The two forms
${KEY} names a key in the same environment.
${project.environment.KEY} names a key anywhere in your organisation — a
shared SENTRY_DSN, a licence key every project needs, a staging credential
production is deliberately built from.
Start typing ${ in the editor and it offers the keys you can reference. Keys
in the environment you are editing come first, in the short form.
$${ when you mean the characters
Plenty of real values contain ${...} and mean it literally — shell
fragments, Compose files, CI templates. Doubling the dollar says so:
exec $${HOME}/bin/servereads back as exec ${HOME}/bin/serve, with nothing substituted.
Who can read what
References resolve with your permission, not the key's. Reading a value
built from ${payments.production.STRIPE_KEY} requires that you could have
read STRIPE_KEY in payments/production yourself.
The consequence is deliberate, and worth knowing before you rely on it: the same key can resolve for one reader and be refused for another. A service token scoped to one environment cannot read a value in it that is built from another.
When a reader is refused, they are told which environment they would have needed. They are never given a partly-assembled value, and never given the template.
Every key a read touches gets its own audit record. Revealing one value built from four produces five.
What a consumer sees
A consumer — the Kubernetes connector, the API, anything holding a service token — receives the assembled value. Resolution happens on the server, so nothing downstream needs access to the keys a value is built from.
A composed key gains no new version when the thing it names changes. The template did not change; a history full of writes nobody made could not answer "who changed this". What moves instead is the key's last-changed time, which is what a consumer watches. A projected Kubernetes Secret updates within one change-feed cycle of the referenced key being written, with nothing written to the composed key at all.
Editing one
The editor shows you the template, not what it resolves to. If it showed the assembled string, saving the row would replace the composition with a frozen copy of its own output.
A key built from others carries a link mark in the list, so you can see which values have moving parts before you open one.
What is refused
A loop. A naming B naming A is refused when you save it, naming the
loop. Refusing at read time would let you save a value that can never be read
and find out from a consumer.
A reference to somewhere you cannot see. Refused when you save it. Stored and failed later, writing templates would be a way to discover which projects and environments exist.
Deleting a key others are built from. You are told which ones would break and asked whether to go ahead. Deleting it anyway is allowed — the refusal is a question, not a wall.
A chain more than eight deep. Cycles are refused going in, but a chain assembled one key at a time by several people can be acyclic and still absurd.
What it does not do
A reference names a key, not a project-wide value: there is no scope above the environment yet. And a reference is resolved when the value is read, every time — nothing is cached, so a revoked credential stops resolving on the next request rather than the next interval.