Import gave you entities, but it shaped them with a thin stub entity type — just enough to land the data. Now you'll write the real definitions: the lifecycle states a cluster moves through, its typed properties, the derived metric that tracks load, and the cell that groups your clusters and decides where the next tenant goes.
This is where the imported reality becomes a model you control. Everything you write here is plain catalog YAML — the same shape from the Catalog YAML Guide, now applied to your own fleet.
A tenant fleet has two entity types: the clusters that host tenants, and the tenants themselves. Put both in a file called blueprint.yaml. Start with TenantCluster:
Three things to notice — they're what makes this a fleet definition rather than a flat resource list:
states + initialState give every cluster an explicit lifecycle. active and failed are always available implicitly; you list the rest.tenant-count metric is derived. Terrantula computes it by counting active runs_on relationships pointing at the cluster. It's always live — you never edit it, and it's exactly what placement and capacity decisions read. (You'll define runs_on in Step 3; until then an empty fleet simply reports zero, which is correct.)constraint turns that metric into a rule: no cluster hosts more than fifty tenants. It's enforced at placement time, before anything provisions.Now add the Tenant type as a second document in the same file, separated by ---:
The Tenant lifecycle is richer because tenants do more: they're pending before placement, provisioning while a PR is open, active once applied, and deprovisioning on the way out. That lifecycle is what the cascade walks when a customer churns.
Re-applying a TenantClusterdefinition that already exists from the import updates it in place to this richer shape. Your imported cluster entities keep their data and gain the new states, metric, and constraint. Import auto-detects existing types, so re-importing later won't clobber this hand-authored definition.
A cell groups entities of one type, ranks them for placement, and caps the group. This is the fleet decision Terraform never made for you. Add the cell as a third document:
Two layers of limit now work together. The entity type capped each cluster at 50 tenants; the cell caps the fleet at 500 summed across every cluster. And placementPolicy: least-loaded means when something needs a home, Terrantula sorts members by tenant-count ascending and offers the emptiest one. (round-robin and random are the alternatives.)
From the directory holding blueprint.yaml:
Terrantula validates all three documents, checks that the cell's entityType references a type that exists, and reconciles the model. You'll see the two entity types updated and the cell created.
Cells use explicit membership by default — you add entities to them. Your imported clusters exist as TenantCluster entities, but they're not in prod-clusters until you place them there. Open the dashboard and add each cluster to the cell from the cell's membership view, or add them via the API / SDK if you're scripting.
You can confirm what the cell sees from the CLI by listing the clusters:
With at least one empty cluster in the cell, a placement query (the kind your OnboardTenant Action will issue in Step 4) returns that cluster — its tenant-count is zero, so it's the least-loaded by definition. Add a second cluster and least-loaded starts doing real work: each new tenant lands on whichever cluster currently has the fewest, until the per-cluster and fleet-wide limits kick in.
TenantCluster and Tenant entity types — lifecycles, typed properties, a derived load metric, and a per-cluster constraint.prod-clusters cell with a placement policy and a fleet-wide capacity ceiling.You have shapes and a placement policy, but nothing connects tenants to clusters yet. Next you'll define the relationship that links them — and see how it powers both the derived metric and the cascade.
Prev: ← Step 1 — Import your TF state · Next: Step 3 — Add relationships →