Define your first cell

This is where the model stops being abstract. You've read the concepts and seen the YAML shapes; now you'll write a small, real piece of catalog and apply it.

We start with a cell because it's the smallest unit of fleet modeling that captures the thing Terraform couldn't do for you: placement. A cell answers "where should the next tenant go, and what's the limit?" — the fleet decision at the center of the cattle mindset. Get one cell working and you've internalized the heart of the model.

What you'll build

A prod-clusterscell that groups your production clusters, ranks them by load for placement, and caps the fleet. You'll need the cluster entity type it groups, so we define that first.

A cell needs something to group

A cell groups entities of a single type, so before the cell we need the type. We'll use TenantCluster — a shared cluster that hosts many tenants. Put this in a file called blueprint.yaml:

apiVersion: terrantula.io/v1
kind: EntityType
metadata:
  name: TenantCluster
spec:
  displayName: Tenant Cluster
  states: [provisioning, active, draining, decommissioned]
  initialState: provisioning
  properties:
    - { name: region, type: string, required: true }
    - { name: tier,   type: string, enum: [basic, premium], required: true }
  # tenant-count is derived: Terrantula counts active runs_on relationships
  # pointing at each cluster. It's the metric the cell will rank and cap on.
  metrics:
    - name: tenant-count
      unit: integer
      source:
        type: derived
        derivedFrom: relationship-count
        filter:
          relationshipType: runs_on
          states: [active]

The tenant-count metric is what makes placement meaningful — it's a live count of how loaded each cluster is. (It references a runs_on relationship type; you'll add that in Your First Fleet. For now, an empty fleet just reports zero, which is exactly what you want when placing the first tenant.)

Now the cell

Add the cell as a second document in the same file, separated by ---:

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

Read it as three decisions:

  • entityType: TenantCluster — the cell's members are clusters. A cell groups exactly one type.
  • placementPolicy: least-loaded — when something needs a home, Terrantula sorts members by tenant-count ascending and offers the emptiest one. (round-robin and random are the alternatives.)
  • constraints — a fleet-wide ceiling. aggregate: sum, max: 500 means the total tenant count across every cluster in the cell may not exceed 500. This is the capacity rule that used to live in a runbook nobody read — now Terrantula enforces it before any provisioning happens.

Apply it

From the directory holding blueprint.yaml:

terrantula apply --file blueprint.yaml

Terrantula validates both documents, checks that the cell's entityType references a type that exists, and reconciles the model. You'll see the entity type and the cell created.

Local-first by default

If you haven't pointed the CLI at a server, this applies to a local SQLite database on your machine — no signup, no infrastructure. Run terrantula dashboard afterward to see your TenantCluster type and prod-clusters cell in the fleet view. When you're ready to share with a team, terrantula login --api-url ...switches the same commands to a remote project.

Add a cluster and watch placement work

A cell with no members can't place anything, so add a cluster. Cells use explicit membership by default — you add entities to them. Once you've created a TenantCluster entity (via import or an Action) and added it to prod-clusters, the cell has a member to rank.

You can confirm what the cell sees from the CLI:

terrantula entities list --entity-type TenantCluster

With one empty cluster in the cell, a placement query (the kind an OnboardTenant Action issues) will return that cluster — its tenant-count is zero, so it's the least-loaded by definition. Add a second cluster and place a few tenants, 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.

That's the whole point of a cell. You declared where things go and how full is too full, once, in a few lines of YAML — and Terrantula now enforces it on every onboard, automatically, before anything touches your infrastructure.

What you just learned

  • A cell groups entities of one type, ranks them for placement, and caps the group.
  • Placement is driven by a metric (tenant-count) and a policy (least-loaded).
  • Capacity limits are declarative and enforced at request time, not documented in a runbook.
  • You applied real catalog YAML — the same shape every larger blueprint uses.

You've now modeled the central fleet decision. The next section turns this into a complete, working fleet: importing your existing Terraform state, defining the full entity types and cells, adding the runs_on relationship, and wiring the OnboardTenant Action that opens PRs.


Next: Your First Fleet → — the guided, end-to-end build of every primitive in order.