Terraform: Adding a Tenant
A Tenant is an individual workload or application that lives inside a Business Unit. While a BU provides the organizational grouping (folders, shared config), a Tenant is where actual GCP projects and resources are created.
This guide walks through creating a Tenant via Backstage and explains what gets provisioned.
What Is a Tenant?
A Tenant represents a distinct workload — a microservice, a data pipeline, a web application, or any other deployable unit. When you create a Tenant, the platform provisions:
- Per-environment GCP projects (e.g.,
prj-governance-d-f6y39for dev,prj-governance-n-f6y39for non-prod,prj-governance-p-f6y39for prod) - A GitHub repository scaffolded from foundations-template
- Per-environment Workload Identity Federation (WIF) configurations and service accounts
- Per-environment Terraform Cloud workspaces for deploying the tenant's infrastructure
- Optional GAR repositories for container images and packages
- Optional budget alerts for cost management
Key Difference from Business Units
| Business Unit | Tenant | |
|---|---|---|
| GCP resource | Folders | Projects |
| WIF model | Cross-environment (single SA) | Per-environment (separate SAs) |
| Budgets | Not supported | Supported per environment |
| Contains | Tenants | Resources (Cloud Run, GCS, etc.) |
Creating a Tenant via Backstage
The recommended way to create a Tenant is through the Backstage developer portal using the create-tenant template.
Prerequisites
- An existing Business Unit (see Adding a Business Unit)
- Access to the Backstage portal
- Membership in the GitHub team that owns the parent BU
The Two-Step Wizard
The Backstage create-tenant template uses a two-step wizard:
Step 1: Basic Configuration
Fill in the tenant details:
| Field | Description | Example |
|---|---|---|
| Tenant Name | Short identifier for the workload | governance |
| Business Unit | The parent BU to create this tenant in | data |
| Description | Human-readable description | Data governance service |
| Owner | GitHub team that owns this tenant | @badal-io/data-team |
| Environments | Which environments to provision | dev, non-prod, prod |
| Branching Model | How CI/CD branches map to environments | trunk-based or environment-based |
Step 2: Terraform Module Editor
The second step presents a custom GithubTerraformModuleEditor UI component where you can:
- Select which infrastructure modules to include (Cloud Run, GCS, BigQuery, etc.)
- Configure module parameters visually
- Preview the generated Terraform code
This editor generates the initial main.tf for the tenant repository.
What Happens Next
- Backstage creates a Pull Request against the parent BU's repository (e.g.,
bu-data-80r13). - The PR adds a new Tenant module call to the BU's
terraform/main.tf. - Once the PR is reviewed and merged, Terraform Cloud applies the changes.
- The apply creates the GCP projects, GitHub repo, WIF configurations, and TFC workspaces.
- The team can then start using their new repository to deploy workloads.
What Gets Created
GCP Projects
A separate GCP project is created for each environment, inside the parent BU's environment folders:
Each project is:
- Linked to the appropriate billing account
- Attached to the Shared VPC (base or restricted) as a service project
- Protected by the environment's organization policies
- Pre-configured with the necessary APIs enabled
GitHub Repository
A repository named {bu}-{tenant}-{suffix} is created from the foundations-template:
{bu}-{tenant}-{suffix}/
├── terraform/
│ ├── configs/
│ │ ├── dev/
│ │ │ └── resources.yaml # Dev environment config
│ │ ├── non-prod/
│ │ │ └── resources.yaml # Non-prod environment config
│ │ └── prod/
│ │ └── resources.yaml # Prod environment config
│ ├── main.tf # Single module call to tenant-infra-factory
│ ├── variables.tf
│ ├── providers.tf
│ └── backend.tf
├── .github/
│ └── workflows/ # CI/CD using reusable workflows
├── catalog-info.yaml # Backstage registration
└── backstage-manifest.json # Backstage metadata
YAML-Driven Configuration
The tenant repository uses a YAML-driven approach to infrastructure. Instead of writing Terraform directly, teams define their resources in YAML files under terraform/configs/{env}/:
# terraform/configs/dev/resources.yaml
cloud_run:
- name: my-service
image: northamerica-northeast1-docker.pkg.dev/my-project/my-repo/my-image:latest
port: 8080
memory: 512Mi
cpu: 1
min_instances: 0
max_instances: 3
gcs:
- name: my-bucket
location: northamerica-northeast1
storage_class: STANDARD
bigquery:
- name: my_dataset
location: northamerica-northeast1
labels:
team: data
cost-center: engineering
The main.tf contains a single module call to tenant-infra-factory which reads these YAML configs and provisions the resources.
Supported Resources
| Resource | YAML Key | Description |
|---|---|---|
| Cloud Run | cloud_run | Managed serverless containers |
| GCS Buckets | gcs | Cloud Storage buckets |
| BigQuery | bigquery | BigQuery datasets |
| Labels | labels | Resource labels applied to all resources |
Terraform Cloud Workspaces
Per-environment TFC workspaces are created for the tenant:
| Workspace | Environment | Example |
|---|---|---|
{bu}-{tenant}-d-{suffix} | Development | data-governance-d-f6y39 |
{bu}-{tenant}-np-{suffix} | Non-production | data-governance-np-f6y39 |
{bu}-{tenant}-p-{suffix} | Production | data-governance-p-f6y39 |
Each workspace is connected to the tenant's GitHub repository and deploys the corresponding environment configuration.
Workload Identity Federation (WIF)
Each environment gets its own WIF configuration:
This ensures that a GitHub Actions workflow running against the dev branch can only access dev resources — it cannot impersonate the production service account.
Branching Models
Tenants support two branching models for CI/CD:
Trunk-Based (Push/Tag)
- All development happens on
main - Pushes to
maindeploy to dev/non-prod - Git tags (e.g.,
v1.0.0) trigger production deployments - Recommended for most teams
Environment-Based
- Separate branches per environment:
development,non-production,production - Merging to a branch deploys to that environment
- Useful when teams need independent environment control
- Mirrors the approach used by the foundation stages (e.g.,
gcp-foundations-envs)
Budget Configuration
Tenants support optional budget alerts:
budget_config = {
enabled = true
amount = 1000 # Monthly budget in USD
alerts = [0.5, 0.8, 1.0] # Alert at 50%, 80%, 100%
notification_channels = [
"projects/my-project/notificationChannels/12345"
]
}
Budget alerts are configured per environment, allowing different thresholds for dev (lower) and production (higher).
Google Artifact Registry (GAR)
If the parent BU has GAR enabled, tenant-level standard repositories are created per environment and per artifact type. These feed into the BU-level virtual repositories:
Tenant standard repo (per env)
└── BU standard repo (shared)
└── BU virtual repo (aggregated)
This tiered architecture allows teams to publish artifacts at the tenant level while consumers can pull from a single BU-level virtual endpoint.
Example: Creating the "governance" Tenant
Here is what the module call looks like in the bu-data-80r13 repository:
module "tenant_governance" {
source = "app.terraform.io/Badal_devex/create-tenant/platform"
tenant_name = "governance"
business_unit_name = "data"
business_unit_id = module.bu_data.business_unit_id
environments = ["development", "non-production", "production"]
github_org = "badal-io"
github_team = "data-team"
budget_config = {
enabled = true
amount = 500
alerts = [0.5, 0.8, 1.0]
}
}
Troubleshooting
| Issue | Resolution |
|---|---|
| Tenant projects not appearing | Check the BU workspace in TFC. The tenant module runs inside the BU workspace, not a separate one. |
| WIF authentication fails in CI | Verify the GitHub repo name matches the WIF pool's attribute condition. The subject must match the repo and branch. |
| YAML config not applied | Ensure the YAML file path matches terraform/configs/{env}/*.yaml and re-run the TFC workspace for that environment. |
| Budget alerts not firing | Verify notification channels exist in the project and the budget amount is set correctly. Budget data can take 24-48 hours to appear. |
Next Steps
- Available Features — explore all configurable features for tenants
- Terraform Cloud Integration — understand workspace hierarchy and promotion