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}.
regionis required. It is the default for every resource in the body that has aregionfield and did not pass one.tagsare merged into every resource in the body that has atagsfield. Nested stacks merge, inner winning; a resource's own tags win over the stack's.name_prefixcomposes the cloud name of resources whose name field is markedphysical_nameinto{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.