Cross-source drift

Goal: keep the graph honest about the external source each entity came from. When a tenant's Terraform state, Atmos stacks, or S3 state changes after you last imported it, this recipe shows you how to see the divergence and reconcile it.

Two kinds of drift — make sure this is the one you want
  • Cross-source drift (this page) — an entity vs. the external IaC source it was imported from. Surfaced as a per-entity status: current, stale, or missing.
  • Drift events — an entity's stored properties vs. what an internal reconciler observes inside Terrantula. Separate system, separate records. See the Drift events reference. :::

How it works

Three primitives carry cross-source drift:

  1. Sync stamp — every imported entity records where it came from (sync_source) and when (last_synced_at). An entity is stale once its stamp is older than a 24-hour threshold, and missing if it wasn't present in the most recent scan of its source.
  2. Import source — one record per source (a state URI plus a reconciliation policy). The CLI registers and refreshes this automatically on every terrantula import terraform / import atmos.
  3. Drift proposal — under the human-approval policy, a rescan writes per-entity diffs here for a human to approve or reject instead of mutating entities directly.

Step 1 — See what's drifting

List the registered import sources, then read a per-source drift rollup (totals plus any pending proposals). Import sources are project-scoped:

terrantula import-sources list
terrantula import-sources drift --id <importSourceId>

The rollup returns per-entity drift status (current / stale / missing) and the count of pending proposals. For an ad-hoc query across the whole project, the entities API also accepts a driftStatus filter — GET /<org>/<project>/envs/<env>/entities?driftStatus=stale.

Step 2 — Choose a reconciliation policy

Each import source carries exactly one policy. Pick it based on whether the external source is the undisputed source of truth:

PolicyWhat a rescan doesUse it when…
auto-update (default)Applies incoming source state directly to the entities and re-stamps them.The source is the source of truth — e.g. the TF state for a fleet you own end-to-end.
human-approvalComputes per-entity diffs and writes them to drift proposals as pending; entities are not mutated. New entities still apply directly (creation is non-destructive).The source might disagree with Terrantula about an entity's authoritative shape and you want a human to mediate — typically production cells.

A repeated rescan under human-approval supersedes prior pending proposals for the same entity, so a scheduled rescan loop never piles up zombie proposals.

Step 3 — Reconcile

Re-import (the simplest path). Pull fresh state and re-import against the same source. Each successful import re-stamps the touched entities, clearing stale, and refreshes the import-source record:

terraform state pull > terraform.tfstate
terrantula import terraform --state terraform.tfstate --config terrantula.yaml

For a TFC-backed source, point --state straight at the workspace and skip the manual pull:

terrantula import terraform --state tfc://my-org/my-workspace --config terrantula.yaml

Drop entities that are genuinely gone. If an entity is missing because the resource was removed from the source, re-import with --replace (preview with --dry-run first). --replace only deletes entities previously synced from the same source:

terrantula import terraform --state terraform.tfstate --config terrantula.yaml --replace --dry-run

Approve or reject proposals (human-approval sources). When a rescan produced pending diffs, decide each one. Approving applies the stored diff and re-stamps the entity; rejecting discards it.

terrantula import-sources approve-proposal --id <importSourceId> --proposal-id <proposalId>
terrantula import-sources reject-proposal  --id <importSourceId> --proposal-id <proposalId>

Step 4 — Keep drift visible automatically

Terrantula ships no built-in scheduler. Rescan is a plain HTTP endpoint your own scheduler calls — and because every import terraform / import atmos refreshes the import-source record, a scheduled re-import is itself a rescan with no glue code. Wire any self-hosted scheduler:

# .github/workflows/terrantula-rescan.yml
on:
  schedule:
    - cron: '*/15 * * * *'   # every 15 minutes
jobs:
  rescan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: terraform -chdir=infra state pull > prod.tfstate
      - run: terrantula import terraform --state prod.tfstate --config terrantula.yaml
        env:
          TERRANTULA_API_URL: ${{ secrets.TERRANTULA_API_URL }}
          TERRANTULA_TOKEN:   ${{ secrets.TERRANTULA_TOKEN }}

A cron job, a GitLab CI schedule, an Atmos workflow, or a systemd timer works the same way. The whole path runs against the Apache 2.0 backend — no SaaS dependency, so an OSS-first deployment can drive it.

Result

The graph reflects the current source state. stale entities are re-stamped to current, missing entities are resolved (re-synced or dropped), and any pending proposals are decided. Re-running the recipe is idempotent.

Caveats

:::info A rescan is observational — it never provisions A rescan applies declarative entity state against Terrantula's catalog so the graph matches what the source reports. It never runs terraform apply

. Actual infrastructure changes still flow through Actions → PRs → your CI.

Pre-existing entities may not appear in a source rollup

Entities imported before import-source tracking existed are sync-stamped but have no registered import-source record, so they're skipped by the per-source rollup (it matches on the source URI). They still appear under the driftStatus entity filter. To backfill, re-run import terraform / import atmos once against the same source — registration is idempotent on (project, env, source URI)and creates the record on the next run.

Don't fix drift by editing the graph

The UI is a read-only projection. Don't try to close a discrepancy by editing an entity's properties, state, or relationships in the dashboard — it can't write those. Reconcile the source (re-import) for observed state; open an Action's PR for intended changes.