Deprovision a tenant

Goal: retire a tenant from the fleet cleanly — destroy its infrastructure, walk its relationships down in the right order, and keep a permanent audit trail.

Deprovisioning is just another Action. It transitions the tenant entity, cascades its relationships, and fires a trigger (a destroy run or a PR). As always, Terrantula never runs terraform apply or destroy itself — the trigger either fires a run against your runner or opens a PR your CI applies.

How it works

DeprovisionTenant is an instance-scope transition-entity Action. A typical shape (from the SaaS-tenants demo):

  • OperationonTrigger: deprovisioning → onSuccess: decommissioned → onFailure: active (revert and investigate).
  • Cascade — the tenant's runs_on edge moves active → removing on trigger, then removing → removed on success. The relationship row is kept, never hard-deleted, so the audit trail is permanent.
  • Condition — only offered when entity.state == active.
  • Optional gracePeriodSeconds parameter — defers the final removed transition so you can pause before the relationship is torn down.

Step 1 — Fire the deprovision Action

Instance-scope Actions run against a single entity. Find the tenant's entity ID, then trigger the Action on it:

terrantula entities list --entity-type Tenant --state active
terrantula entities trigger \
  --id <tenantEntityId> \
  --action-name DeprovisionTenant

This creates an ActionRun (who fired what, with which parameters, and how it ended). The tenant transitions to deprovisioning, the runs_on edge moves to removing, and the trigger fires — a TFC destroy run, or a PR your CI applies.

Step 2 — Let the cascade and trigger complete

On a successful destroy/apply:

  1. The cascade advances runs_on to removed.
  2. The tenant transitions to decommissioned.
  3. Both the entity and relationship transitions are recorded as audit events.

If the trigger fails, the tenant reverts to active (per onFailure) and the runs_on edge stays in removing for you to investigate.

Step 3 (optional) — Hold a grace period

Pass gracePeriodSeconds > 0 to defer the final teardown — useful for a compliance window or a "are you sure?" pause before infrastructure is destroyed:

terrantula entities trigger \
  --id <tenantEntityId> \
  --action-name DeprovisionTenant \
  --parameters '{"gracePeriodSeconds": 86400}'

What changes: on destroy success, instead of advancing the runs_on edge straight to removed, Terrantula sets the edge to removing and schedules finalization for now() + gracePeriodSeconds. A deprovision.scheduled audit event is written. The entity is already decommissioned, so the fleet view shows the tenant as retired while the relationship waits to be finalized.

Step 4 — Finalize a held teardown

Because Terrantula ships no scheduler, you finalize relationships whose grace period has elapsed. Two ways:

Sweep (your scheduled job). Wire a cron / CI schedule / worker task that calls finalizeElapsedGracePeriods from @terrantula/api. It's idempotent — each row is finalized exactly once — so it's safe to run on a loop:

import { finalizeElapsedGracePeriods } from '@terrantula/api'

const count = await finalizeElapsedGracePeriods({
  finalState: 'removed',
  projectId: process.env.PROJECT_ID ?? null,
  actorType: 'system',
  actorId: 'sweeper',
})
console.log(`Finalized ${count} relationships.`)

Finalized rows emit a deprovision.finalized audit event with method: "sweep".

Force (operator escape hatch). Skip the remaining window for a single relationship via the env-scoped endpoint. Returns 409 if the relationship has no scheduled grace period:

curl -X POST \
  "$TERRANTULA_API_URL/<org>/<project>/envs/<env>/relationships/<relId>/finalize-deletion" \
  -H 'Authorization: Bearer '"$TERRANTULA_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"finalState":"removed"}'

This emits deprovision.finalized with method: "force".

Result

The tenant is decommissioned, its runs_on relationship is removed (or held then finalized), and every transition is in the audit log. The relationship row survives as a permanent record.

Audit events

Event actionWhenpayload
deprovision.scheduledGrace period set on triggergracePeriodSeconds, scheduledAt
deprovision.finalizedRelationship finalizedfinalState, method: "sweep" | "force"

Read them from the audit-events surface filtered by action=deprovision.scheduled or action=deprovision.finalized.

Caveats

A held grace period leaks resources until you finalize it

deletion_scheduled_at is a lower bound on when finalization may occur, not a guarantee. If you never run the sweep, the Terraform-managed infrastructure for the held relationship is not torn down by your CI. Monitor for relationships in removing with a deletion_scheduled_atin the past, and make sure a sweep job or a force-finalize closes them out.

Terrantula never destroys infrastructure directly

The Action opens a PR or fires a run against your runner; your CI runs terraform destroy. Terrantula owns the lifecycle transitions, the cascade, and the audit trail — not the destroy itself.

Reverse order for multi-stack tenants

When a tenant spans several stacks (the golden path), deprovision walks the dependency graph in reverse— the leaf entity tears down before its upstreams, so removing an AWS account doesn't orphan the tenant that ran in it.