Authoring

What a config may say, and what each construct costs. The tutorials (walkthrough, Building on AWS) build a config up in order; this page is the flat reference to look things up in afterwards.

Declaring a resource

Resource("logical-name", **fields, lifecycle=..., depends_on=[...])

The logical name is positional and must be an identifier. Every resource also accepts lifecycle= and depends_on=; everything else is that type's own fields, whose mutability decides what a change to it costs. Use atlantide schema <type> to see them.

Reading another resource's computed field returns a lazy Ref rather than a value. That read is what creates the dependency edge — there are no string addresses to keep in sync — and it resolves at apply.

Stacks

with Stack("prod", region="eu-north-1", tags={"env": "prod"}, name_prefix="acme"):
    ...

A stack scopes everything declared in its body. Resources inside get the node id {stack}:{type}:{name}.

  • region is required. It is the default for every resource in the body that has a region field and did not pass one.
  • tags are merged into every resource in the body that has a tags field. Nested stacks merge, inner winning; a resource's own tags win over the stack's.
  • name_prefix composes the cloud name of resources whose name field is marked physical_name into {name_prefix}-{base}-{stack}. Omitted, it falls back to the enclosing stack's.

Stacks nest, and one config may declare as many as it likes — the usual shape is a for env in [...] loop around the whole block.

Per-block region

with region("us-east-1"): overrides the enclosing stack's region for the resources declared inside, and nothing else. The usual reason is a resource a service requires in a particular region, such as an ACM certificate fronting CloudFront.

Outputs

output("assets_arn", assets.arn)

output() exports a literal or a Ref under a name, namespaced by the enclosing stack — declared inside with Stack("prod") it becomes prod:assets_arn, outside any stack it lands in default. Outputs are persisted into state after apply and read back with atlantide output.

It also returns a handle, so a later stack in the same config can consume the value without repeating the name. That reference is inlined into a real dependency edge rather than resolved from state.

A stack applied by a separate config must name it explicitly:

vpc_id = StackReference("network").output("vpc_id")

StackReference resolves from the referenced stack's committed outputs at apply, so that stack must already be applied into the same state store.

Lifecycle

S3Bucket("assets", lifecycle=Lifecycle(prevent_destroy=True), ...)
Field Effect
prevent_destroy A planned DELETE — or the destroy half of a REPLACE — on this resource fails the whole plan.
create_before_destroy A REPLACE creates the replacement before destroying the old one. Falls back to destroy-first when the two would collide on identity.
ignore_changes Field names whose drift is ignored: excluded from the Merkle input_hash and from the diff's changed-field set, so a change to one never triggers UPDATE or REPLACE.
aliases Prior node ids (or bare old logical names) this resource was renamed from.

Rename without replace

Give a renamed resource its previous name as an alias and the engine maps it onto the existing state entry instead of destroying and recreating it:

S3Bucket("assets_v2", lifecycle=Lifecycle(aliases=("assets",)), ...)

When the new id is absent from state but an alias id is present, the plan rewrites the existing row to the new id.

For a stricter guard than prevent_destroy — one that covers a whole stack — see deny-destroy-in-protected.

Explicit ordering

depends_on=[other] orders one resource after another without reading anything from it. Use it when the dependency is real but carries no value — an IAM policy that must propagate before the service relying on it starts, a bucket policy that must exist before an upload.

Reading other.arn already creates the edge, so depends_on is only needed when nothing is read. Pass the resources themselves, or their node ids.

The edge is excluded from the content hash. Adding one therefore affects ordering alone, and never causes a resource to be re-planned.

Output combinators

concat, interpolate and join build a string from values that do not exist yet. Reading a computed field such as assets.arn returns a Ref, so ordinary string formatting cannot be applied to it.

S3BucketPolicy("p", bucket=assets.bucket, statements=[
    allow("s3:GetObject", on=concat(assets.arn, "/*"), principal="*"),
])

The combinators serialize as data rather than as closures. This is necessary because the config is not re-executed at apply time: the engine evaluates the recorded operation once the referenced values resolve.

Derived values

Atlas-lang provides a small set of pure functions in the global namespace, for values computed at config-evaluation time from things already known. Every one is deterministic — that is the condition of being allowed in at all.

Function Purpose
slugify(s) ASCII-fold, lowercase, non-alphanumerics to a single -. "Café Menu!""cafe-menu".
uuid5(namespace, name) Name-based UUID, stable across machines and runs.
sha256_hex(s) Hex SHA-256 of a string.
hmac_sha256_hex(key, msg) Hex HMAC-SHA256.
b64encode(s) / b64decode(s) Base64, over UTF-8 text.
to_json(v) Canonical JSON: keys sorted, no insignificant whitespace, byte-stable — safe to hash or embed in a policy document.
from_json(s) Parse JSON into plain data.
merge(a, b, ...) Deep-merge dicts left to right, later winning; nested dicts merge recursively, other types are replaced. Arguments are not mutated.

These operate on values, not on Refs. For something that will not exist until apply, use the combinators instead.

Inputs and secrets

env = atlantide.input("env", "dev")     # a per-run value, with a default
key = atlantide.secret("signing-key")   # a handle to a stored secret

atlantide.input() reads a value supplied by -var, --var-file or [inputs]; without a default, a missing input is an error naming the three places it could come from. The determinism guarantee is over (config, inputs), and only inputs the config actually reads shape the result. See Configuration.

atlantide.secret() returns a SecretRef — a name. The name is what reaches the IR, the artifact and state; the plaintext is resolved from the secrets store in-memory at apply and redacted in plan output and logs. It deliberately does not read from inputs: passing a secret as an input would write the plaintext into all three at once.

Set the value with atlantide secret set <name>, and enforce the discipline with require-secret-refs.

Multi-account

AWS resources accept provider_alias=, naming an entry under [aws.aliases.*] in atlantide.toml, to target an alternate account:

S3Bucket("assets", provider_alias="prod", ...)

The field is immutable(), so moving a resource between accounts is a replace, not an update.

What config may not do

Config is executed by Atlas-lang, not by CPython. There is no clock, no randomness, no environment, no network and no filesystem.

Constructs outside the subset are rejected before evaluation, each with the reason and the replacement:

Rejected Because
while Halting must be provable; use a bounded for.
class Define resource types in a provider, not in config.
try / raise No exceptions in config; guard with if.
async / await Config is synchronous and pure.
yield No generators; build lists with comprehensions.
global / nonlocal No mutable module or closure state; pass arguments.
:= Use a separate assignment.
del Bound values are immutable.

Imports are restricted to the atlantide.* surface — which is what makes a vendored component importable and import os not.

Bounded for loops, comprehensions, functions and conditionals are all available. Recursion is not rejected outright; evaluation runs on a fuel budget, so a runaway computation stops with a FuelExhaustedError rather than hanging.

The practical consequence for authoring: anything that must vary per run comes in through atlantide.input(), and anything that needs a class — a component, a provider — is ordinary Python living outside the config.