Skip to main content

Typed Python IaC engine: deterministic Atlas-lang config, graph state, parallel reconciliation

Project description

Atlantide

Typed, deterministic Infrastructure-as-Code for Python.

Atlantide is an IaC engine built on three ideas:

  • Enforced determinism. Configs are written in plain Python but executed by Atlas-lang — a bounded interpreter with no clock, randomness, environment, or network access. Two runs of the same config produce byte-identical intermediate representation and a stable content hash.
  • Graph state with Merkle skip. Resources form a dependency graph. A two-phase Merkle input_hash lets apply skip unchanged nodes with zero provider calls, and independent nodes reconcile in parallel.
  • Typed resources with per-field mutability. Each field is declared mutable(), immutable(), or computed(). Changing a mutable field is an in-place UPDATE; changing an immutable one is a REPLACE (destroy + create); computed fields never diff.

Install

Requires Python ≥ 3.11. Uses uv.

uv sync

Quickstart

Write a config in Python — valid for your IDE and mypy, run by Atlas-lang:

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

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)

Reading another resource's output (assets.arn) returns a lazy Ref — it wires the dependency and resolves to the real value at apply time.

Authoring helpers

  • Output combinatorsconcat, interpolate, join build a value from an apply-time Ref without knowing it yet (the language isn't re-run at apply, so they serialize as data, not closures):

    S3BucketPolicy("p", bucket=assets.bucket, statements=[
        allow("s3:GetObject", on=concat(assets.arn, "/*"), principal="*"),
    ])
    
  • Rename without replace — give a renamed resource its old id (or bare old name) so it maps to the existing state node instead of destroy + create:

    S3Bucket("assets_v2", lifecycle=Lifecycle(aliases=("assets",)), ...)
    
  • Per-block regionwith region("us-east-1"): overrides the stack's region for the resources inside (e.g. an ACM cert for CloudFront).

  • Components — library-authored L2 groups (e.g. aws.SecureBucket) bundle several resources behind one parameterized object; children auto-namespace so a component used twice never collides. They can also be published in a git repo and imported by anyone — see Components.

CLI

uv run atlantide plan    infra.py                    # preview changes
uv run atlantide apply   infra.py                    # reconcile (parallel)
uv run atlantide apply   infra.py                    # re-run: all NOOP, zero provider calls
uv run atlantide refresh                             # detect drift vs live state
uv run atlantide graph   infra.py --format mermaid   # dependency graph
uv run atlantide destroy                             # tear down

State defaults to atlantide.db; override with --state. Mutating commands (apply / deploy / destroy / refresh) take --confirm/-y, --region, --parallelism/-p, and — where they change state — --on-failure rollback|halt (default rollback: undo completed nodes on failure). plan and refresh support --json and --detailed-exitcode (0 = no change, 2 = changes, 1 = error).

Config path, state db, and defaults can be fixed once in atlantide.toml so commands take no flags inside a project:

config      = "infra.py"
state       = "atlantide.db"
parallelism = 8
aws_region  = "eu-north-1"
aws_profile = "default"
aws_endpoint = "http://localhost:4566"   # e.g. LocalStack
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"
endpoint = "http://localhost:4566"

Additional commands:

  • build / verify / deploy — portable .atlas artifacts (content-hashed, provider-version pinned): compile once, promote the same bytes anywhere.

  • refresh — read live provider state, report drift; --write syncs it back.

  • secret — manage the local AES-GCM encrypted name→value store; resources reference secrets by name (SecretRef), resolved at apply:

    uv run atlantide secret set app/signing-key       # value prompted (hidden) if omitted
    uv run atlantide secret get app/signing-key -r    # print plaintext (--reveal required)
    uv run atlantide secret list                       # names only, no values
    uv run atlantide secret rm  app/signing-key        # remove
    
  • componentadd / lock / vendor / verify components published in public git repos; imported as atlantide.components.<alias>. See Components.

  • resources / schema — discover resource types and inspect a type's fields.

Components

L2 constructs — several resources behind one parameterized object, like Pulumi's ComponentResource or a CDK Construct. Config uses components but can't define them: Atlas-lang bans class, so a component is ordinary Python written by a library or provider author.

from atlantide.core import Component, child
from atlantide.providers.aws import S3Bucket

class SecureBucket(Component):
    def __init__(self, name, *, bucket):
        self.bucket = child(S3Bucket, "assets", bucket=bucket)
        # ...plus a TLS-only hardening policy wired to self.bucket

A component owns no IR node of its own — its children lower as normal flat resources, and each child's logical name is namespaced under the instance ({component}-{child}), so instantiating a component twice never collides.

Publishing & consuming

Components can be shared in a public git repo and imported by anyone. There is no live URL import (Node-style): config is a deterministic sandbox — it may import only atlantide.* and cannot touch the network — so a fetch at eval time would break both. Instead you fetch once, pinned to a commit + content hash (the terraform init model), and import the vendored code locally under the atlantide.components.* namespace, which the sandbox already permits.

# fetch, pin (commit + content hash) into atlantide.lock, vendor into .atlantis/
uv run atlantide component add https://github.com/acme/secure-bucket --ref v1.2.0 --as acme --subdir src
uv run atlantide component verify   # re-hash vendored trees vs the lock (tamper/drift)
uv run atlantide component vendor    # rebuild .atlantis/ from the lock alone (e.g. fresh checkout)
uv run atlantide component lock      # re-resolve declared refs -> commits
# infra.py — imported and used like any other resource
from atlantide.components.acme import SecureBucket
SecureBucket("assets", bucket="acme-assets")

add records the source under [components.acme] in atlantide.toml and the resolved pin in atlantide.lock. Commit atlantide.lock; git-ignore .atlantis/ (it's derived — vendor rebuilds it from the lock). A build artifact also records each component's commit as provenance.

Trust: a published component runs as trusted Python, like a provider — vet what you add. After that, integrity rests on the pin, and verify fails on any tamper or drift. See examples/components/ for a runnable publish-and-consume walkthrough.

How it works

Every plan/apply runs the same pipeline — pure, deterministic stages first, then the effectful reconcile:

flowchart LR
    A[Config .py] --> B[Atlas-lang<br/>+ IR + hash]
    B --> C[Graph<br/>+ Merkle]
    C --> D[Diff vs<br/>prior state]
    D --> E[Plan<br/>+ policy]
    E -->|plan| F[Changeset]
    E -->|apply| G[Parallel apply<br/>skip unchanged]

Steps

  1. Atlas-lang — the config is validated (Python subset — no while, class, dunder, eval, or non-allowlisted imports) and evaluated by a fuel-bounded interpreter with deterministic builtins only.
  2. Registry → IR — evaluated resources, Refs, and output()s lower into an IR graph; Refs become dependency edges.
  3. Canonicalize — IR serializes to canonical JSON (RFC 8785) and gets a stable content hash; two runs are byte-identical.
  4. Graph — the dependency graph is built and checked for cycles (Tarjan).
  5. Merkle — each node gets a two-phase input_hash in topological order.
  6. Diff — desired hashes compare against prior state → per-node action (NOOP / CREATE / UPDATE / REPLACE / DELETE); per-field mutability decides UPDATE vs REPLACE.
  7. Plan — actions ordered (creates/updates topo, deletes reversed; REPLACE = destroy-before-create); prevent_destroy blocks protected deletes.
  8. Policy — plan-time bindings evaluate against the changeset; mandatory violations block apply.
  9. Apply — under a whole-state lock, the executor reconciles independent nodes in parallel, calls providers, skips Merkle-unchanged nodes (zero calls), and persists incrementally. On failure it rolls back completed nodes as a saga by default (--on-failure halt instead leaves state as-is, resumable). Outputs print.

Providers

  • localFile, Null. Credential-free; runs in CI.
  • randomUuid, Password, Id, Timestamp. Values generated once at apply, then pinned in state.
  • awsS3Bucket, S3BucketPolicy, SqsQueue, IamRole, IamPolicy, LambdaFunction, SnsTopic, SnsSubscription, DynamoDbTable, CloudWatchLogGroup, Vpc, Subnet, SecurityGroup. Plus IAM policy helpers (allow, deny, assume_role, ServicePrincipal) and the SecureBucket L2 component. Any resource may set provider_alias= to target an alternate account. LocalStack-friendly via --region / aws_endpoint.

Architecture

Layered, with import boundaries enforced by import-linter:

Layer Responsibility
core/ Resource, field helpers, Output/Ref, Provider ABC, registry, Stack
lang/ Atlas-lang: subset validator, fuel-bounded interpreter, deterministic builtins
ir/ IR model, lowering (Ref → edges), canonical JSON (RFC 8785), content hash, two-phase Merkle, .atlas artifacts
graph/ Graph build, Tarjan cycle detection, async Kahn scheduler
state/ StateBackend ABC — sqlite (default) + memory; whole-state lock
reconcile/ Diff (Merkle NOOP-skip), planner, parallel executor (saga rollback / resume), refresh
policy/ Modular per-resource policy engine (@policy / enforce; mandatory blocks, advisory warns)
secrets/ SecretsProvider ABC — env + AES-GCM keyfile store; secrets referenced by name, resolved at apply
engine/ Engine — orchestrates compile → plan → apply/destroy/refresh/build/deploy; owns the state lock
providers/ Provider implementations (local, random, aws)
components/ Published components: git fetch, atlantide.lock pinning, vendored-tree hashing, and the import mount
cli/ Typer app; console/render/json/diagram/progress output, project config

Examples

  • examples/aws/ — a per-environment stack of ~14 resources each, wiring cross-resource and cross-provider dependencies automatically.
  • examples/components/ — authoring a publishable component and consuming it from another project (git-pinned, vendored).

Development

uv sync --extra dev
uv run pytest            # tests (memory + sqlite backends), 90% coverage floor
uv run mypy              # strict type check
uv run ruff check
uv run lint-imports      # architecture contracts

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

atlantide-0.1.0.tar.gz (305.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

atlantide-0.1.0-py3-none-any.whl (182.9 kB view details)

Uploaded Python 3

File details

Details for the file atlantide-0.1.0.tar.gz.

File metadata

  • Download URL: atlantide-0.1.0.tar.gz
  • Upload date:
  • Size: 305.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.16 {"installer":{"name":"uv","version":"0.9.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for atlantide-0.1.0.tar.gz
Algorithm Hash digest
SHA256 b736b31c9cb42a9b5591d11badc98a0e98b84040247662eb40e3cad15908c01b
MD5 61010f3ef211df022b06c1d57712ed04
BLAKE2b-256 0dd557a842a2bad06bbdd4f0b20238d388d47f56d3a633ca769891d6042e51ea

See more details on using hashes here.

File details

Details for the file atlantide-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: atlantide-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 182.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.16 {"installer":{"name":"uv","version":"0.9.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for atlantide-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f2a498dad19edc514992d0e46d94d0b74c46d8cfcfff33870f63dc0f40cb2956
MD5 076130016f33293699b384588a6e04d6
BLAKE2b-256 be64de506ae49a6de453630bf34332ab52110e1a44250c90e47b9e00ffdddaa8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page