Goal: onboard a tenant whose infrastructure spans several Terraform stacks
(its own state per stack), with one command that opens the right pull requests
in the right order — and never has Terrantula run apply itself.
This is the lookup recipe for the pattern. For a full hands-on walkthrough, do the Advanced Quick Start; for a runnable repo, see the examples gallery.
Use it when onboarding a tenant means creating more than one entity — a tenant record, an AWS account, an Argo project, a cluster binding — each in its own directory with its own statefile, so a bad apply for one never blocks the others. For the simplest single-stack case, a plain Onboard Actionis enough.
Five conventions and primitives carry the pattern. Each is a field on the catalog kinds you already know:
| Piece | Where it lives | What it does |
|---|---|---|
| One directory per entity | Your IaC repo | Each entity owns infra/<type>/<name>/, its own statefile, keyed by path. |
| One Apply Action per entity type | Action | Apply<Type> opens a PR with that entity's Terraform; one Action covers every instance of the type. |
applier | EntityType | Names the Apply Action that auto-fires when an entity of this type is created. |
dependsOn | Action | Declares apply order by following relationship types — blocks a leaf apply until its upstreams succeed. |
postMergeDispatch | pull-request trigger | On merge, fires a repository_dispatch to one entity-generic workflow that runs terraform apply and calls back. |
The entry point is a single create-entity Action (OnboardTenant) whose
cascadeRules seed the upstream entities; from there each applier fires and
dependsOn sequences the applies.
Give every entity type that owns infrastructure an Apply Action and wire its
applier. The Apply Action's trigger is a pull-request that commits the entity's
Terraform into its own directory and dispatches the apply workflow on merge:
dependsOnA leaf entity (the tenant) usually needs its upstreams applied first. Declare each dependency as a relationship to follow plus the action that must have succeeded for the linked entity:
ApplyTenant fires the moment the Tenant is created, sees its deps aren't ready,
moves to blocked, then wakes and fires when the last upstream Apply
succeeds. The dependency graph is validated as acyclic at apply time
(DEPENDS_ON_CYCLE if you introduce a loop).
OnboardTenant creates only the Tenant; its createEntity cascade rules create
the upstream entities and the linking relationships as a side effect:
apply upserts by name, so it's safe to re-run. Connect the IaC repo via the
GitHub App so PRs can be opened and merge callbacks received, and ship the three
entity-generic workflows (terrantula-entity-apply.yml, …-destroy.yml,
…-drift.yml) — copy them from the
examples customer-iac-repo/ starter.
OnboardTenant is collection-scope, so it runs at the type level:
What happens:
pending; the cascade creates the three upstream entities
plus their relationships.applier auto-fires. The three upstream applies have no
deps, so they fire immediately and each opens a PR in its own directory.
ApplyTenant fires, finds its deps unmet, and blocks.postMergeDispatch fires the
repository_dispatch; the entity-generic workflow runs terraform apply in that
directory and POSTs success back to Terrantula's callback URL. The entity
transitions applying → active.ApplyTenant, which opens
the tenant PR. Merge it; your CI applies; the tenant goes active.Track progress at any point:
One command produced four stacks across four PRs, applied in dependency order, each
with isolated state — and Terrantula never ran terraform apply. Every step is a
reviewable PR your CI applied.
The same primitives give you surgical, per-entity operations — none require
re-running OnboardTenant:
terrantula entities trigger --id <id> --action-name ApplyAWSAccount.Deprovision<Type> — same dispatch shape,
opposite semantics; the destroy runs before the directory is removed. The
dependency graph is walked in reverse so a leaf tears down before its
upstreams. See Deprovision a tenant.terraform plan -detailed-exitcode per directory
reports drift back as an audit event; correcting it is just re-firing the Apply
Action. See Cross-source drift.
There is intentionally no project-level state-backend default — a single bucket
breaks multi-account and multi-env fleets. Each Apply Action declares its own
backend (backend.tf) in its filesblock, keyed by the entity's directory path.
The demo uses one bucket; swap in your own per Action for multi-account.
When the tenant stack needs an upstream's output (an account ID, a cluster ARN),
read it with Terraform's terraform_remote_state data source. Terrantula
orchestrates order; it does not pass outputs between stacks.
Each apply is a reviewable PR. Terrantula opens it and dispatches the apply on merge; your CI runs Terraform. Don't auto-merge if review is your gate — the ordering still holds, but you lose the human checkpoint.
dependsOn, cascadeRules, postMergeDispatch.applier field that auto-fires an Apply Action on create.