Step 2 — Define entity types & cells

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.

The two shapes in your 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:

apiVersion: terrantula.io/v1
kind: EntityType
metadata:
  name: TenantCluster
spec:
  displayName: Tenant Cluster
  # The lifecycle a cluster moves through, and where it starts.
  states: [provisioning, active, draining, decommissioned]
  initialState: provisioning
  properties:
    - { name: region,             type: string, required: true }
    - { name: tier,               type: string, enum: [basic, premium], required: true }
    - { name: kubernetes_version, type: string, required: true }
    - { name: arn,                type: string, description: AWS EKS cluster ARN }
  # tenant-count is derived: Terrantula counts active runs_on relationships
  # pointing at each cluster. You never set it by hand.
  metrics:
    - name: tenant-count
      unit: integer
      source:
        type: derived
        derivedFrom: relationship-count
        filter:
          relationshipType: runs_on
          states: [active]
  # The hard per-cluster ceiling.
  constraints:
    - metric: tenant-count
      max: 50

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.
  • The 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.)
  • The 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 ---:

---
apiVersion: terrantula.io/v1
kind: EntityType
metadata:
  name: Tenant
spec:
  displayName: Tenant
  states: [pending, provisioning, active, suspended, deprovisioning, failed]
  initialState: pending
  properties:
    - { name: customer_id,       type: string, required: true }
    - { name: plan_tier,         type: string, enum: [basic, premium], required: true }
    - { name: region_preference, type: string, required: true }
    - { name: contact_email,     type: string, required: true }

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.

You're overwriting the import stub on purpose

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.

The cell — where the next tenant lands

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:

---
apiVersion: terrantula.io/v1
kind: Cell
metadata:
  name: prod-clusters
spec:
  displayName: Production cluster fleet
  entityType: TenantCluster      # this cell groups clusters
  placementPolicy: least-loaded  # place on the emptiest cluster
  constraints:
    - metric: tenant-count
      aggregate: sum
      max: 500                   # ≤ 500 tenants across the whole cell

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.)

Apply the blueprint

From the directory holding blueprint.yaml:

terrantula apply --file 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.

Add your clusters to the cell

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:

terrantula entities list --entity-type TenantCluster

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.

What you just did

  • Replaced the import stub with full TenantCluster and Tenant entity types — lifecycles, typed properties, a derived load metric, and a per-cluster constraint.
  • Defined the prod-clusters cell with a placement policy and a fleet-wide capacity ceiling.
  • Placed your imported clusters into the cell so it has members to rank.

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 →