Skip to content

Remote state

State defaults to a local SQLite file, usable from one machine only. Set [state] to a shared backend to use the same commands from a team or a CI runner.

[state]
backend    = "s3"                      # "local" (default) | "s3" | "postgres"
bucket     = "acme-atlantide-state"
key        = "prod/atlantide.json"
lock_table = "atlantide-locks"         # DynamoDB table, hash key `node_id` (S)
region     = "eu-north-1"
kms_key_id = "alias/atlantide"         # optional; SSE-S3 (AES256) otherwise

lock_ttl            = 300              # seconds a lease lasts before it lapses
lock_renew_interval = 100              # how often a live run pushes that out
[state]
backend = "postgres"                   # needs the extra: pip install atlantide[postgres]
dsn     = "postgresql://…"             # or the ATLANTIDE_STATE_DSN env var
schema  = "atlantide"

Concurrency

Locks are held per node id, not over the whole store. Apply re-diffs after acquiring the lease, and actions can appear or disappear between the two diffs, so the lease covers the whole desired graph plus everything already recorded in state.

Two applies run concurrently only if both their configs and their prior state are disjoint. Two applies over the same stack serialize.

A run that loses its lease (the hold lapsed and another run claimed it) stops and reports it. It does not roll back: a compensation is a state write, and the lease no longer authorises one. Run atlantide refresh to recover.

Preparing the backend

Atlantide does not create the bucket or the lock table. They are the trust root for shared state and must exist before first use, configured with:

  • Bucket versioning, to recover from a bad state write.
  • A DynamoDB TTL on expires_at, so abandoned leases expire.

state check verifies this setup in one pass. It also probes whether the endpoint honours conditional writes; an S3-compatible store that accepts the request but ignores the condition defeats every compare-and-swap the backend relies on.

atlantide state check              # --no-probe skips the scratch write

Managing state

atlantide state list                     # every recorded resource
atlantide state show <id>                # one resource's stored inputs and outputs
atlantide state rm <id>                  # forget a row without destroying anything

atlantide state backup                   # ./atlantide-state-<serial>-<ts>.atlas-state
atlantide state restore snap.atlas-state
atlantide state migrate --from atlantide.db      # local -> remote
atlantide state migrate --to-local atlantide.db  # remote -> local

atlantide state unlock                           # who holds what
atlantide state unlock --owner ci-runner-7 -y    # break a dead run's holds

backup and restore run under the state lock, so a snapshot cannot capture part of a concurrent apply. restore previews which nodes it will stop tracking, and refuses a snapshot if state has been written since it was taken, unless given --force. It rewrites only atlantide's record, not cloud resources, so restoring an old snapshot creates drift.

state rm removes a row that no longer describes a real resource. It does not call the provider. If the resource still exists, it becomes untracked and the next apply creates a second one. The command snapshots state first and reports the snapshot.

state migrate copies in a single write in either direction, so an interrupted migration cannot leave a partially populated destination.

Upgrading a shared backend

The schema is versioned and migrates forward automatically. Take a snapshot first, and upgrade every machine sharing the backend at the same time. An older atlantide refuses a newer store rather than misreading it.

Secrets

Secret values can come from a remote store. Config, the IR and state hold only the secret's name:

[secrets]
provider = "ssm"                       # "keyfile" (default) | "env" | "ssm"
prefix   = "/atlantide/prod/"          # secret `db_password` -> /atlantide/prod/db_password
region   = "eu-north-1"

Point secrets_key at a shared path when state is shared

The keyfile encrypts the local value store, salts the rotation digests, and seals sensitive outputs at rest.

A teammate without the keyfile cannot unseal those outputs and sees every secret field as rotated. plan detects this pattern (every secret in state mismatching, none intact) and warns instead of listing spurious updates. KMS-backed keys are not supported.