Cheat sheet

Copy-paste reference for the commands and YAML shapes you reach for most. For the full surface, see the CLI Reference and the Catalog Schema Reference.

TIP

Every Action opens a pull request (or fires a TFC / Atmos / Atlantis run) — Terrantula never runs terraform applyitself. Your CI applies on merge.

Install & authenticate

# Install (requires Bun >= 1.0)
bun install -g @terrantula/cli
terrantula --version

# Authenticate against a self-hosted server (saved to ~/.config/terrantula/config.json)
terrantula login --base-url https://api.example.com --token terr_xxxx

# Or per-invocation / via env
terrantula --base-url https://api.example.com --token terr_xxxx <command>
export TERRANTULA_BASE_URL=https://api.example.com
export TERRANTULA_TOKEN=terr_xxxx

# Pin a default project for project-scoped commands
terrantula use <project-id>

First five minutes (local, no server)

# 1. Write a mapping config (terrantula.yaml) — see "Import mapping" below
# 2. Import existing Terraform state
terrantula import terraform --state terraform.tfstate --config terrantula.yaml

# 3. Open the local graph dashboard
terrantula dashboard

Import state

# Local state file (preview first with --dry-run)
terrantula import terraform --state terraform.tfstate --config terrantula.yaml --dry-run

# From S3
terrantula import terraform --state s3://bucket/path/terraform.tfstate --config terrantula.yaml

# From Terraform Cloud (or set TF_TOKEN_app_terraform_io)
terrantula import terraform --state tfc://my-org/my-workspace --config terrantula.yaml --tfc-token "$TFC_TOKEN"

# Multiple states in one run
terrantula import terraform --state s3://bucket/prod.tfstate --state s3://bucket/staging.tfstate --config terrantula.yaml

# Drop entities that vanished from the source
terrantula import terraform --state terraform.tfstate --config terrantula.yaml --replace

# Atmos stacks
terrantula import-atmos ./stacks --entity-type Tenant --name "{{.stack}}" --recursive

Import mapping (terrantula.yaml)

entity_types:
  - name: Server
    tf_resource_types:
      - aws_instance
relationship_types: []

Apply a catalog

# Apply a blueprint (a single multi-document YAML, or a whole directory)
terrantula apply --file blueprint.yaml
terrantula apply --file ./terrantula/

# Plan only — no writes, returns a diff (created / updated / unchanged / deleted)
terrantula apply --file blueprint.yaml --dry-run

# Allow breaking schema changes (removing a state in use, a property with values)
terrantula apply --file blueprint.yaml --force

Secrets

# Declare the secret in the catalog (value set out-of-band, never in YAML)
terrantula secrets set-value tfc-api-token --value "$TFC_TOKEN"
# Reference it inside an Action as: {{ secrets.tfc-api-token }}

Graduate local → server

# Export the local catalog as apply-shaped YAML
terrantula export --out my-fleet.yaml

# Or pipe straight into a remote apply
terrantula export --apply-to https://api.example.com --dest-project <remote-project-id>

Global flags

FlagMeaning
--base-url <url>API base URL (overrides config)
--token <token>API token (overrides config)
--org <id>Org slug for org/project-scoped commands
--project <id>Project ID for project-scoped commands
--env <name>Environment name (defaults to default)
--jsonRaw JSON output

Catalog YAML skeletons

Every document uses the same envelope (apiVersion + kind + metadata + spec), separated by ---. The catalog applies in dependency order automatically — schema kinds first, then data. Full fields per kind are in the Catalog Schema Reference.

EntityType — the shape of a thing

apiVersion: terrantula.io/v1
kind: EntityType
metadata:
  name: TenantCluster
spec:
  states: [provisioning, active, draining, decommissioned]
  initialState: provisioning
  properties:
    - { name: region, type: string, required: true }
    - { name: tier,   type: string, enum: [basic, premium], required: true }
  metrics:
    - name: tenant-count
      unit: integer
      source: { type: derived, derivedFrom: relationship-count, filter: { relationshipType: runs_on, states: [active] } }
  constraints:
    - { metric: tenant-count, max: 50 }   # per-cluster cap

Cell — placement + aggregate limits

apiVersion: terrantula.io/v1
kind: Cell
metadata:
  name: prod-clusters
spec:
  entityType: TenantCluster
  placementPolicy: least-loaded          # or round-robin | random
  constraints:
    - { metric: tenant-count, aggregate: sum, max: 500 }   # fleet-wide cap

RelationshipType — the shape of a connection

apiVersion: terrantula.io/v1
kind: RelationshipType
metadata:
  name: runs_on
spec:
  from: Tenant
  to: TenantCluster
  cardinality: many-to-one               # enforced, not advisory
  states: [assigning, active, migrating, removing]
  properties:
    - { name: namespace, type: string, required: true }

Secret — a referenced credential

apiVersion: terrantula.io/v1
kind: Secret
metadata:
  name: tfc-api-token
spec:
  description: Terraform Cloud API token for run dispatch

Action — the workflow that opens PRs

apiVersion: terrantula.io/v1
kind: Action
metadata:
  name: OnboardTenant
spec:
  associatedWith: { entityType: Tenant, scope: collection }
  parameters:
    - { name: customer_id, type: string, required: true }
    - { name: plan_tier,   type: string, enum: [basic, premium], required: true }
  recommendations:                        # cell placement
    - { name: target-cluster, cell: prod-clusters, sortBy: tenant-count, order: asc, required: true }
  operation:
    type: create-entity
    entityType: Tenant
    onTrigger: provisioning               # state while the PR is open
    onSuccess: active                     # state once applied
    onFailure: failed
    properties:
      customer_id: "{{ parameters.customer_id }}"
    createRelationship:
      relationshipType: runs_on
      to: "{{ recommendations.target-cluster.id }}"
      onSuccess: active
      properties:
        namespace: "tenant-{{ parameters.customer_id }}"
  trigger:                                # how the change reaches your runner
    type: pull-request                    # or terraform-cloud | atlantis | atmos-workflow | webhook
    repo: "{{ parameters.target_repo }}"
    base: main
    files:
      - { path: "tenants/{{ parameters.customer_id }}.tfvars.json", content: "{ \"customer_id\": \"{{ parameters.customer_id }}\" }" }

Entity & Relationship — data instances

---
apiVersion: terrantula.io/v1
kind: Entity
metadata:
  name: acme
spec:
  entityType: Tenant
  state: active
  properties: { customer_id: acme, plan_tier: premium }
---
apiVersion: terrantula.io/v1
kind: Relationship
metadata:
  name: acme-runs-on-eks-us-east-1
spec:
  relationshipType: runs_on
  from: acme                              # Tenant entity
  to: eks-us-east-1                       # TenantCluster entity
  state: active
  properties: { namespace: tenant-acme }