Configuration¶
Three separate things are called "configuration", and they answer different questions:
| What it is | Where it lives | Selected by | |
|---|---|---|---|
| Environments | The typed matrix of environments the system has, and what differs between them | Config(...) in the config file |
--env |
| Inputs | A per-run value from outside the repository | -var, --var-file, [inputs] |
supplied per run |
| Profiles | Where a run points: state backend, AWS profile, parallelism | [profile.<name>] in atlantide.toml |
--profile/-P |
--profile and --env are orthogonal; neither implies the other.
The project file¶
Set the config path, the state database and any defaults once in
atlantide.toml, and commands inside the project need no flags.
The file is looked up in the working directory and then in each parent, the way git locates a repository root. Relative paths inside it resolve against the directory holding the file, not the directory you ran the command from.
config = "infra.py"
state = "atlantide.db"
parallelism = 8
aws_region = "eu-north-1"
aws_profile = "default"
aws_endpoint = "http://localhost:4566" # send every AWS call here instead
secrets_key = ".atlantide.key"
secrets_store = ".atlantide.secrets"
[aws.aliases.prod] # alternate account (multi-account)
profile = "prod-account" # a resource selects it via provider_alias="prod"
Environments¶
A Config declares every environment the system has, and what differs between
them, once and in one place:
from atlantide.core import Config, EnvSchema, Stack
class AppEnv(EnvSchema):
domain: str
size: int = 1
config = Config(
AppEnv,
envs={
"dev": {"region": "eu-north-1", "domain": "dev.example.io"},
"prod": {"region": "us-east-1", "domain": "example.io", "size": 5},
},
)
for env in config.envs(): # env: AppEnv
with Stack(env.name, config=env):
...
Each variable declares its type and, optionally, a default. Everything is
checked when the Config is constructed, so a missing or mistyped prod value
fails atlantide validate in CI rather than at the moment prod is applied.
An environment name becomes the stack name, so it must be an identifier.
The schema¶
EnvSchema is the one class Atlas-lang admits — a module-level class whose body
is annotated fields only, no methods, no decorators and no metaclass, so it is
still data. Declaring it is what makes the variables ordinary attributes: an
editor completes env.domain, and env.domian is a type error before anything
runs.
A field may be annotated str, int, float, bool, list or dict, or
X | None to make it optional and nullable. Parameterized generics such as
list[str] are not supported.
The schema can instead be a mapping of var() declarations, when the static
side does not matter:
from atlantide.core import Config, var
config = Config(schema={"size": var(int, default=1)}, envs={...})
Both forms run the same validation; the class form adds the static knowledge. A
var() without a default is required — every environment must supply it, and a
missing one is reported when the Config is built rather than wherever the
value is eventually read. default=None instead makes it optional.
Well-known keys¶
region, tags and name_prefix are implicit in every schema, so
Stack(env.name, config=env) needs no separate region=:
with Stack(env.name, config=env): # region/tags/name_prefix from the env
...
An explicit Stack argument wins over the environment, except tags, which
merge (the stack's own winning). A schema may re-declare a well-known key to
make it required or to narrow it.
Inside the body the environment is ambient — a
component reads it with current_config() rather than having
it threaded through a constructor.
Selecting one¶
atlantide plan infra.py --env prod # prod only
atlantide apply infra.py --env dev --env prod # repeatable
An unselected environment is out of scope, not undeclared: its state is never diffed and never planned for deletion, and the plan says what it left out.
envs: prod (of dev, prod) — dev is not planned and will not change
Naming an environment the config does not declare is an error, rather than a
run that silently does nothing. Stacks declared outside the config.envs()
loop — a shared common — are not part of the matrix and are unaffected by
--env, which is what keeps a shared VPC in the graph when a run is narrowed.
--env is accepted by plan, apply, validate, build and destroy. On
destroy it selects by stack rather than through the config: destroy reads no
config, only state, and an environment's stack is its name.
build records the selection in the .atlas artifact, so a deploy covers
exactly the environments the build did. Determinism is over (config, inputs,
selected environments).
Inputs¶
An input is a per-run value supplied from outside the repository — a CI build number, a fork's name prefix — and arrives as text:
env = atlantide.input("env", "dev") # with a default
count = int(atlantide.input("count")) # values arrive as strings from -var
atlantide plan -var env=prod
atlantide plan --var-file prod.toml
Inputs can be set in three places, in increasing order of precedence:
[inputs] in atlantide.toml (or [profile.<name>.inputs] for one profile),
then --var-file, then -var on the command line.
Every plan prints the inputs it actually read. Only inputs the config reads affect the result, so an unused value cannot change what a run appears to be.
An input is not an environment
What differs between environments belongs in a Config:
it is checked in, typed and validated eagerly. What differs between runs of
one environment is an input. The two compose — an environment's value may
be built from an input.
Inputs are not for secrets
atlantide.secret("name") returns a handle, not a value. Passing a secret as
an input would write the plaintext into the IR, into build artifacts and into
state.
Profiles¶
A [profile.<name>] table overlays the top level, and --profile/-P — or the
ATLANTIDE_PROFILE environment variable — selects it. A profile decides where
a run points: which state backend, which AWS account, how much parallelism.
The overlay is applied table by table, so a profile restates only the keys it changes and inherits the rest:
parallelism = 4
[profile.prod]
parallelism = 16
[profile.prod.state] # inherits the base [state] keys it omits
backend = "s3"
bucket = "acme-atlantide-state"
key = "prod/atlantide.json"
lock_table = "atlantide-locks"
Naming a profile the file does not define is an error rather than a silent fall-through to the defaults.