Bare Terraform + GitLab CI (one file per tenant)

Tags: Substrate · Automation · Self-host Substrate: bare Terraform + GitLab CI

The GitLab CI sibling to the GitHub Actions one-file-per-tenant demo. Same scenario, same pattern — each tenant is one file under tenants/, Terraform iterates with for_each — for a SaaS operator on GitLab running plan-on-MR and apply-on-merge.

What you'll see

GitLab's MR support is on the roadmap, so today this demo uses the pipeline-dispatch path. OnboardTenant runs least-loaded placement, creates a Tenant entity in provisioning, then a webhook trigger POSTs to the GitLab pipeline trigger API with TENANT_* and TERRANTULA_CALLBACK_* variables embedded. The pipeline:

  1. prepare-tenant writes tenants/acme.tfvars.json and publishes it as a pipeline artifact.
  2. terraform:validate, terraform:plan, and terraform:apply run in sequence, each pulling the artifact via needs: so Terraform sees the new tenant file.
  3. After a successful apply, the apply job's after_script commits the tenant file to the default branch (for persistence) and POSTs the outcome to the callback URL.
  4. Terrantula transitions the tenant to active (or failed).

The customer's review workflow, state file, and AWS credentials are unchanged. The GitLab pipeline applies; Terrantula never runs terraform apply.

Try it

Setup takes under 30 minutes:

# Apply the cattle blueprint.
terrantula apply --file examples/bare-tf-gitlab-ci-onefile-per-tenant/blueprint.yaml

# Store the GitLab pipeline trigger token as a Terrantula secret.
terrantula secrets set gitlab-trigger-token --value <your-pipeline-trigger-token>

Add the CI/CD variables (AWS_* and a GITLAB_COMMIT_TOKEN) in your GitLab project, seed the cluster fleet, then fire an onboard:

CANDIDATES=$(terrantula cells recommend prod-clusters)
TARGET_ID=$(echo "$CANDIDATES" | jq -r '.[0].id')

terrantula actions run \
  --action-name OnboardTenant \
  --parameters "{\"customer_id\":\"acme\",\"plan_tier\":\"premium\",\"region_preference\":\"us-east-1\",\"contact_email\":\"acme@example.com\",\"gitlab_project_id\":\"<your-project-id>\"}" \
  --recommendations "{\"target-cluster\":\"${TARGET_ID}\"}"

You can validate the pipeline YAML locally without a live runner:

python3 -c "import yaml, sys; yaml.safe_load(sys.stdin.read())" \
  < examples/bare-tf-gitlab-ci-onefile-per-tenant/.gitlab-ci.yml \
  && echo "YAML parses OK"

Key files

FileWhat it is
blueprint.yamlThe Terrantula schema + OnboardTenant action (self-contained).
terraform/main.tfThe TF module that iterates over tenants/.
tenants/.gitkeepThe directory tenant config files land in.
.gitlab-ci.ymlThe reference pipeline — drop into your repo root or include: it.

View on GitHub

examples/bare-tf-gitlab-ci-onefile-per-tenant