Skip to content

Configuration

Atlantide has three kinds of configuration:

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 independent; neither implies the other.

The project file

atlantide.toml sets the config path, the state database and other defaults, so commands inside the project need no flags.

The file is found by searching the working directory and then each parent, as git locates a repository root. Relative paths in it resolve against the directory containing the file, not the directory the command runs 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 and what differs between them, 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 a type and an optional default. All values are checked when the Config is constructed, so a missing or mistyped prod value fails atlantide validate in CI rather than the prod apply.

An environment name becomes the stack name, so it must be an identifier.

The schema

EnvSchema is the only class Atlas-lang accepts: a module-level class whose body contains annotated fields only, with no methods, decorators or metaclass. Because the variables are 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.

Alternatively, the schema can be a mapping of var() declarations, which provides no static type information:

from atlantide.core import Config, var

config = Config(schema={"size": var(int, default=1)}, envs={...})

Both forms run the same validation. A var() without a default is required: every environment must supply it, and a missing value is reported when the Config is built, not where the value is read. default=None 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 takes precedence over the environment, except tags, which merge (the stack's own taking precedence). A schema may re-declare a well-known key to make it required or to narrow its type.

Inside the body the environment is ambient: a component reads it with current_config() instead of receiving it 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 or planned for deletion, and the plan lists what it excluded.

envs: prod (of dev, prod) — dev is not planned and will not change

Naming an environment the config does not declare is an error. Stacks declared outside the config.envs() loop, such as a shared common, are not part of the matrix and are unaffected by --env, so a shared VPC stays in the graph when a run is narrowed.

--env is accepted by plan, apply, validate, build and destroy. On destroy it selects by stack name, because destroy reads state only, not config, and an environment's stack is its name.

build records the selection in the .atlas artifact, so deploy covers the same environments as the build. Determinism is over (config, inputs, selected environments).

Inputs

An input is a per-run value supplied from outside the repository, such as a CI build number or a fork's name prefix. Inputs arrive 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), --var-file, and -var on the command line.

Every plan prints the inputs it read. Only inputs the config reads affect the result.

An input is not an environment

Values that differ between environments belong in a Config, which is checked in, typed and validated at construction. Values that differ between runs of one environment are inputs. An environment's value may be built from an input.

Inputs are not for secrets

atlantide.secret("name") returns a handle, not a value. A secret passed as an input is written in plaintext to the IR, build artifacts and state.

Profiles

A [profile.<name>] table overlays the top level and is selected with --profile/-P or the ATLANTIDE_PROFILE environment variable. A profile sets where a run points: state backend, AWS account and parallelism.

The overlay is applied per table, so a profile sets 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; there is no fallback to the defaults.