§ How-to · Specify
Run one specification in several environments
Select dev, staging or prod with a tag, keep the differences in one table and give each environment its own state.
On this page
One specification serves every environment. A tag selects the environment, one table holds what differs, and the state path follows the tag.
Work in an empty directory. The commands are for a POSIX shell.
1. Declare the environment
env.cue holds the environment, the table and the state path.
package estate
import "pravo.dev/pravo"
// The environment: dev unless -t env=... says otherwise.
env: *"dev" | "staging" | "prod" @tag(env)
// What differs between environments.
_envs: {
dev: {workers: 1, retention: 7}
staging: {workers: 2, retention: 30}
prod: {workers: 3, retention: 90}
}
// The row of the selected environment.
cfg: _envs[env]
// One state directory per environment.
$settings: pravo.#Settings & {state: path: "state/\(env)"}
- The disjunction lists the allowed values,
*marks the default and@tag(env)lets-t env=...fill the field. _envsis hidden, so pravo does not send it to a provider.cfgis the row thatenvselects.state.pathis computed fromenv, so each environment has its own state.
2. Write the resources once
main.cue reads cfg and never names an environment.
package estate
import (
rnd "pravo.dev/providers/hashicorp/random"
loc "pravo.dev/providers/hashicorp/local"
)
$random: rnd.#Provider
$local: loc.#Provider
server: $random.#Resource.random_pet & {length: cfg.workers}
conf: $local.#Resource.local_file & {
filename: "\(env).conf"
content: "env \(env)\nserver \(server.id)\nretention \(cfg.retention)\n"
}
Success! 2 resources, 0 data sources, 0 ephemeral, 0 function calls, 0 outputs across 2 providers.
3. Select an environment
pravo eval -e prints one expression. Ask for the row with and without a tag.
workers: 1
retention: 7
workers: 3
retention: 90
Without a tag, env takes its default. pravo keeps no current environment. Every command evaluates the specification again with the tags it receives, so pass the same -t to plan, apply and destroy.
4. Apply each environment
Apply complete! Resources: 2 added, 0 changed, 0 destroyed. [txn <id>]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed. [txn <id>]
The addresses are the same in both runs, server and conf. The state is not.
dev
prod
retention 7
retention 90
The pravo state commands take -t, and read the state of the environment they name. Without -t they read the default environment.
conf
server
5. Add an environment
A new environment is a new value in the disjunction and a new row in the table. Start with the value only.
package estate
import "pravo.dev/pravo"
// The environment: dev unless -t env=... says otherwise.
env: *"dev" | "staging" | "prod" | "qa" @tag(env)
// What differs between environments.
_envs: {
dev: {workers: 1, retention: 7}
staging: {workers: 2, retention: 30}
prod: {workers: 3, retention: 90}
}
// The row of the selected environment.
cfg: _envs[env]
// One state directory per environment.
$settings: pravo.#Settings & {state: path: "state/\(env)"}
qa is allowed and has no row. pravo validate does not notice, because a value that is not yet concrete is not an error for it.
Success! 2 resources, 0 data sources, 0 ephemeral, 0 function calls, 0 outputs across 2 providers.
Error: server.length: cfg: undefined field: qa
pravo eval -c requires every value to be concrete. Run it on cfg for every environment, in a loop, before you merge.
workers: 1
retention: 7
workers: 2
retention: 30
workers: 3
retention: 90
Error: -e "cfg": undefined field: qa:
./env.cue:16:12
The message names the row that is missing. Add it.
package estate
import "pravo.dev/pravo"
// The environment: dev unless -t env=... says otherwise.
env: *"dev" | "staging" | "prod" | "qa" @tag(env)
// What differs between environments.
_envs: {
dev: {workers: 1, retention: 7}
staging: {workers: 2, retention: 30}
qa: {workers: 1, retention: 14}
prod: {workers: 3, retention: 90}
}
// The row of the selected environment.
cfg: _envs[env]
// One state directory per environment.
$settings: pravo.#Settings & {state: path: "state/\(env)"}
Plan: 2 to add, 0 to change, 0 to destroy.
6. Refuse an unknown environment
A value outside the disjunction is rejected when pravo evaluates the specification, before any provider starts.
Error: evaluating .:
env: 4 errors in empty disjunction:
env: conflicting values "dev" and "sandbox":
./env.cue:6:7
env: conflicting values "prod" and "sandbox":
./env.cue:6:27
env: conflicting values "qa" and "sandbox":
./env.cue:6:36
env: conflicting values "staging" and "sandbox":
./env.cue:6:15
To make the tag mandatory, remove *"dev" | from the field. A command without -t, including each pravo state command, then stops with $settings.state.path is not concrete. pravo providers lock, pravo providers schema and pravo mod verify do not apply -t, so in such a root they stop with the same error.
Clean up
Destroy complete! Resources: 0 added, 0 changed, 2 destroyed. [txn <id>]
Destroy complete! Resources: 0 added, 0 changed, 2 destroyed. [txn <id>]
What next
- B4, Inputs, tags, environments teaches each part of this recipe.
- Remote state puts one prefix per environment in a shared bucket.
- CI runs one job per environment.
- Tags and variables and Settings are the reference for
@tagand$settings.