Atlantide

Typed, deterministic Infrastructure-as-Code for Python.

Write infrastructure in real Python, checked by your IDE and by mypy rather than by a templating language. In return you get the guarantee a config language exists to provide: the same config always produces the same plan, and the engine can demonstrate it.

pip install atlantide          # or: uv add atlantide

Python 3.11 or newer. A standalone binary that needs no Python is published with every release — see Installation.

Quickstart

atlantide init scaffolds a working project. The default minimal template uses the local provider, so it applies with no cloud credentials at all:

mkdir demo && cd demo
atlantide init
created  demo/.gitignore
created  demo/atlantide.toml
created  demo/infra.py
ok infra.py — 1 resource(s), no cycles

next:  atlantide plan
atlantide apply -y     # create it
atlantide plan         # again: unchanged

  + create  local.File:greeting
Plan: 1 to add
...
Applied: 1 to add  (0.0s)
  = noop    local.File:greeting
Plan: 1 unchanged

That second plan is the engine reading recorded hashes and matching them against the ones it just computed — no provider was called. atlantide init --template aws scaffolds the same project against AWS instead.

A quick look

from atlantide.core import Stack, output
from atlantide.policy import enforce
from atlantide.providers.aws import S3Bucket, SqsQueue

enforce("require-tags", keys=["env"])
enforce("deny-destroy-in-protected", stacks=["prod"])

for env in ["dev", "prod"]:
    with Stack(env, region="eu-north-1", name_prefix="atlantide", tags={"env": env}):
        assets = S3Bucket("assets", versioning=(env == "prod"))
        jobs = SqsQueue("jobs", fifo=True)
        output("assets_arn", assets.arn)
atlantide plan  infra.py     # preview
atlantide apply infra.py     # reconcile, in parallel
atlantide apply infra.py     # again: all NOOP, zero provider calls

Reading another resource's output, such as assets.arn, returns a lazy Ref rather than a value. That read is what creates the dependency edge; there are no depends_on declarations and no string addresses to keep in sync. The Ref resolves to the real value at apply time.

Three ideas

Enforced determinism. Configs are plain Python, but they are executed by Atlas-lang: a bounded interpreter with no clock, no randomness, no environment and no network. Two runs of the same config produce a byte-identical intermediate representation and the same content hash. This is not a convention that authors must observe — the interpreter provides no way to do otherwise.

Graph state with Merkle skip. Resources form a dependency graph, and each node carries a hash of its own inputs combined with its dependencies' hashes. Apply skips any node whose hash is unchanged without making a single provider call, and reconciles independent nodes in parallel.

Per-field mutability. Every field is declared mutable(), immutable() or computed(). Changing a mutable field produces an in-place update; changing an immutable one produces a replacement; computed fields never contribute to a diff. What a change will cost is visible in the type, before anything runs.

The pipeline

Every plan and apply runs one pipeline. Everything before the diff is pure and deterministic; everything after it touches the world.

flowchart TB subgraph pure["Deterministic — no I/O; same input, same bytes"] direction LR cfg["infra.py"] --> lang["Atlas-lang<br/>subset check + fuel-bounded eval"] lang --> ir["IR + canonical JSON (RFC 8785)<br/>stable content hash"] ir --> dag["Dependency graph<br/>Refs become edges, cycles rejected"] dag --> merkle["Two-phase Merkle<br/>input_hash per node"] end merkle --> diff{"Diff"} store[("State backend<br/>sqlite · s3+dynamodb · postgres")] -.->|prior hashes| diff diff --> plan["Plan<br/>ordered, policy-checked"] plan -->|plan| changeset["Changeset<br/>NOOP · CREATE · UPDATE · REPLACE · DELETE"] plan -->|apply| exec["Executor<br/>parallel, under a renewed lease"] exec -->|unchanged hash| skip["Skipped<br/>no provider call"] exec <-->|create · read · update · delete| prov["Providers<br/>aws · local · random · yours"] exec -->|fenced writes| store

Because the first half performs no I/O, plan needs no credentials, validate is cheap enough for a pre-commit hook, and one compiled artifact can be promoted from staging to production without the config being executed a second time.

Where to start

  • Installation — the standalone binary, or the PyPI package.
  • Coming from Terraform — the vocabulary mapping, the real differences, and where atlantide is behind.
  • How it works — the mental model, followed by a hands-on walkthrough that needs no AWS account.
  • Building on AWS — a real multi-environment stack built one resource at a time: VPC, S3, SQS, IAM, Lambda, secrets and policies.
  • CLI — every command, targeting, exit codes.
  • Authoring — stacks, outputs, lifecycle, secrets, and what config may not do.
  • Configurationatlantide.toml, inputs, profiles.
  • Policies — the built-in rules, levels, bindings.
  • Remote state — S3 and Postgres backends, locking.
  • Troubleshooting — every error, by stage.
  • Glossary — the vocabulary, in pipeline order.

Source: github.com/atlantide-org/atlantide