Step 3 — Add relationships

You have clusters and a cell, and you have a Tenant type — but nothing connects a tenant to the cluster it runs on. That connection is a relationship, and it's load-bearing: it's what the derived tenant-count metric counts, what enforces capacity, and what the cascade walks to tear things down in the right order.

In this step you'll define the runs_on relationship type and learn how dependsOn gives the cascade its ordering — so a tenant churning never removes infrastructure before the things that depend on it.

The relationship type

A relationship type is the shape of a connection between two entity types: directional, typed, with enforced cardinality. Add this fourth document to your blueprint.yaml:

---
apiVersion: terrantula.io/v1
kind: RelationshipType
metadata:
  name: runs_on
spec:
  displayName: Runs On
  from: Tenant              # the from-end entity type
  to: TenantCluster         # the to-end entity type
  cardinality: many-to-one  # many tenants per cluster, one cluster per tenant
  states: [assigning, active, migrating, removing]
  properties:
    - { name: namespace,    type: string, required: true }
    - { name: allocated_at, type: string }

Walk the fields:

  • from / to make the relationship directional. A Tenant runs on a TenantCluster, not the other way around.
  • cardinality: many-to-one is enforced, not advisory. Many tenants per cluster, but one cluster per tenant — Terrantula will reject an attempt to give a single tenant two active runs_on links. The graph stays consistent because the rule lives in the type.
  • states give the link its own lifecycle (assigning → active → migrating → removing), independent of the entities it joins.
  • properties live on the relationship when they're facts about the link, not either endpoint. The per-tenant Kubernetes namespace belongs here: it's true of this tenant on this cluster, not of the tenant or the cluster alone.

Apply the updated blueprint:

terrantula apply --file blueprint.yaml

Now the tenant-count metric you defined in Step 2 has something to count. Every runs_on relationship in the active state increments the load on the cluster it points at — automatically, the moment the link is created.

The metric was waiting for this

Back in Step 2, tenant-count was defined to count active runs_on relationships — but runs_on didn't exist yet, so every cluster read zero. Now that the relationship type is live, placement and capacity become real: as relationships are created, tenant-count climbs, least-loadedre-ranks, and the per-cluster and cell constraints start enforcing.

How relationships drive the cascade

Relationships are also how Terrantula knows what depends on what — and that's what makes ordered lifecycle changes possible. The cascade walks relationships when an entity changes state and propagates the change to connected entities and links, in dependency order.

There are two complementary mechanisms, and it's worth knowing which is which:

1. cascadeRules on an Action's operation transition relationships as a side effect of an entity changing state. For example, a deprovision Action marks a tenant's runs_on link removing the moment the run is triggered, so the derived metric reflects the teardown immediately:

operation:
  type: transition-entity
  onTrigger: deprovisioning
  onSuccess: decommissioned
  onFailure: active
  cascadeRules:
    # When the tenant starts deprovisioning, mark its runs_on link 'removing'.
    - relationshipType: runs_on
      fromState: active
      toState: removing
      phase: on-trigger
      direction: from
    # Once the destroy succeeds, mark it 'removed'.
    - relationshipType: runs_on
      fromState: removing
      toState: removed
      phase: on-success
      direction: from

Each rule fires at a well-defined phase of the run — on-trigger, on-success, or on-failure — so the relationship's lifecycle stays in lockstep with the entity's.

2. dependsOn on an Action declares ordering between Actions across a relationship. When applying one entity requires that the thing it depends on is applied first, you spell that out:

dependsOn:
  - relationship: runs_on
    appliedAction: ApplyTenantCluster

This reads: "before this Action runs, the entity at the other end of the runs_on relationship must have had its ApplyTenantCluster Action applied." It's how a multi-stack onboard sequences itself — provision the cluster before the tenant that runs on it — without you ordering the runs by hand.

Ordered teardown is the whole point

This is the answer to "deprovisioning rot" from the cattle mindset. Because relationships carry direction and cardinality, Terrantula knows the dependency order. The cascade tears things down safely — nothing removed before its dependents — instead of relying on a runbook nobody reads. And every cascade-driven change still reaches your infrastructure the same way: through a pull request, never a direct terraform apply.

What you just did

  • Defined the runs_on relationship type with enforced many-to-one cardinality, its own lifecycle states, and a per-link namespace property.
  • Activated the derived tenant-count metric — placement and capacity are now real.
  • Learned the two ordering mechanisms: cascadeRules (transition relationships as entities change state) and dependsOn (sequence Actions across a relationship).

You now have the full static model: shapes, placement, and connections. The last piece is the verb — an Action that grows the fleet. Next you'll wire OnboardTenant, whose trigger opens a pull request.


Prev: ← Step 2 — Define entity types & cells · Next: Step 4 — Wire an Action →