Tenant Module (terraform-platform-create-tenant)
The tenant module creates all infrastructure required for a new workload tenant within a business unit. It provisions per-environment GCP projects, a GitHub repository, Terraform Cloud workspaces, Workload Identity Federation, and optionally Google Artifact Registry repositories and budget alerts.
- Source Repository: badal-io/terraform-platform-create-tenant
- TFC Registry:
app.terraform.io/Badal_devex/create-tenant/platform
What It Creates
- GCP projects for each environment (inside the BU's environment folders)
- A GitHub repository (
{business_unit}-{tenant}-{suffix}) with optional template - TFC project and workspaces (one per environment or single, depending on branching model)
- Per-environment WIF with separate service accounts scoped to each project
- Optional GAR repositories per environment per artifact type with vulnerability scanning
- Optional budget alerts per environment with configurable thresholds
Resource Relationship Diagram
create-tenant module
├── module.gcp_projects (per env)
│ └── GCP project: {tenant}-{env_code}-{suffix}
├── module.repository (integrated_repository/github)
│ ├── GitHub repo: {bu}-{tenant}-{suffix}
│ ├── TFC project + workspaces (per env)
│ └── Per-env WIF + service accounts
├── google_artifact_registry_repository (per env x type)
│ └── GAR repos with vulnerability scanning
└── Budget alerts (per env, via project-factory)
Key Differences from the Business Unit Module
| Aspect | Business Unit Module | Tenant Module |
|---|---|---|
| GCP resources | Creates folders | Creates projects |
| WIF pattern | Cross-environment (single SA) | Per-environment (separate SAs) |
| WIF host project | Creates its own | Uses BU's project via env folder |
| Branching model | Always trunk-based, single workspace | Trunk-based or environment-based |
| Budget support | No | Yes (tenant-level + per-env overrides) |
| Scaffold files | Yes (full Terraform scaffold) | No (uses template repo) |
| GitHub App | Required (creates repos for tenants) | Not required |
| Naming pattern | bu-{name}-{suffix} | {bu}-{tenant}-{suffix} |
| TFC workspaces | Single workspace | One per environment |
Naming Convention
Resources use a randomly generated 5-character alphanumeric suffix (or a user-provided one):
| Resource | Naming Pattern | Example |
|---|---|---|
| GitHub repository | {business_unit}-{tenant}-{suffix} | data-template-b8cbu |
| GCP projects | {tenant}-{env_code}-{suffix} | template-d-b8cbu |
| TFC workspaces | {bu}-{tenant}-{env_code}-{suffix} | data-template-d-b8cbu |
Environment codes are derived from the first letter of each hyphen-separated segment of the environment name:
| Environment | Code |
|---|---|
| development | d |
| non-production | np |
| production | p |
GCP project IDs must be 6-30 characters. The tenant name is validated to be 1-22 lowercase characters starting with a letter, leaving room for the -{env_code}-{suffix} suffix.
Key Variables
| Variable | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | - | Tenant name (1-22 chars, lowercase, starts with letter). Used without the BU prefix. |
business_unit | string | Yes | - | Parent business unit name |
suffix | string | No | Random 5-char | Optional fixed suffix for resource naming |
gh_org | string | Yes | - | GitHub organization name |
environments | map(object) | Yes | - | Per-environment GCP config (see Environment Configuration) |
branching_model | object | No | {} | Either trunk_based or environment_based (see Branching Models) |
tfc | object | Yes | - | TFC config: org_id, gh_app_oauth_id, enable (default true), share_state (default false) |
template | object | No | null | Template repo: owner (defaults to gh_org), repository, include_all_branches |
teams | map(object) | No | {} | GitHub teams to grant permissions |
topics | list(string) | No | [] | Additional GitHub topics (merged with tenant, backstage-catalog) |
budget | object | No | null | Default budget for all environments (see Budget Configuration) |
gar | object | No | { enable = false } | GAR config: enable, location, artifact_types, upstream_service_agents |
Environment Configuration
Each entry in the environments map accepts:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
org_id | string | Yes | - | GCP Organization ID |
billing_account_id | string | Yes | - | GCP Billing Account ID |
parent_folder_id | string | Yes | - | GCP Folder ID (the BU's environment folder) |
service_account_roles | list(string) | Yes | - | IAM roles for the environment service account |
enable_billing_user_role | bool | No | false | Grant roles/billing.user to the SA |
additional_apis | list(string) | No | [] | Extra GCP APIs to enable |
labels | map(string) | No | {} | Additional project labels |
vpc_subnet_project_id | string | No | "" | Shared VPC host project ID |
vpc_subnets | list(string) | No | [] | Fully qualified subnet IDs for Shared VPC |
vpc_service_control_attach_enabled | bool | No | false | Attach to VPC SC perimeter (enforced) |
vpc_service_control_attach_dry_run | bool | No | false | Attach to VPC SC perimeter (dry run) |
vpc_service_control_perimeter_name | string | No | "" | VPC SC perimeter name |
enable_shared_vpc_host_project | bool | No | false | Create a shared VPC host project |
budget | object | No | null | Per-env budget override (overrides tenant-level) |
Key Outputs
| Output | Description |
|---|---|
repository | GitHub repository details (name, URL, default branch) |
terraform_project | TFC project details |
terraform_workspaces | TFC workspaces keyed by environment name |
environments | Environment config including WIF providers and service accounts |
gcp_projects | Map of GCP projects per environment (project_id, project_number, name) |
budgets | Resolved budget configuration per environment (null when no budget) |
gar_repositories | Map of GAR repos by artifact type, then by environment. Empty when GAR disabled. |
Branching Models
The tenant module supports two branching strategies. Exactly one must be configured.
Trunk-Based
A single main branch deploys to all environments. Deployments can be triggered by push (default) or tag.
branching_model = {
trunk_based = {
deployment_trigger = "push" # or "tag"
}
}
- One TFC workspace per environment, all triggered from the same branch
- Simpler workflow, suitable for most tenants
Environment-Based
Separate branches map to separate environments (e.g., development, non-production, production).
branching_model = {
environment_based = {}
}
- One TFC workspace per environment, each linked to its corresponding branch
- Promotes through environments by merging branches
- Useful when environments need independent change control
Note: Single workspace mode is NOT supported for tenants (only for business units).
Budget Configuration
Budgets support a two-level override pattern:
- Tenant-level default: Applied to all environments unless overridden
- Per-environment override: Specified in the environment's
budgetblock, takes precedence
# Tenant-level default: $1000 for all environments
budget = {
amount = 1000
alert_spent_percents = [0.5, 0.7, 1.0]
alert_spend_basis = "CURRENT_SPEND"
}
environments = {
development = {
# ... inherits $1000 budget
}
production = {
# ... overrides to $5000 budget
budget = {
amount = 5000
alert_spent_percents = [0.5, 0.8, 0.9, 1.0]
}
}
}
Budget Fields
| Field | Type | Default | Description |
|---|---|---|---|
amount | number | - | Budget amount in billing account currency |
alert_spent_percents | list(number) | [0.5, 0.7, 1.0] | Thresholds that trigger alerts |
alert_spend_basis | string | CURRENT_SPEND | CURRENT_SPEND or FORECASTED_SPEND |
alert_pubsub_topic | string | null | Pub/Sub topic for programmatic alerts |
monitoring_notification_channels | list(string) | [] | Notification channels (max 5) |
calendar_period | string | null | MONTH, QUARTER, YEAR, or CUSTOM |
custom_period_start_date | string | null | Start date (DD-MM-YYYY) for custom period |
custom_period_end_date | string | null | End date (DD-MM-YYYY) for custom period |
labels | map(string) | {} | Budget labels (max 1) |
display_name | string | null | Display name for the budget |
GAR Configuration
When enabled, the tenant module creates standard GAR repositories in each environment's project:
gar = {
enable = true
location = "us-central1"
artifact_types = ["DOCKER", "PYTHON"]
upstream_service_agents = ["sa-shared@project.iam.gserviceaccount.com"]
}
For each environment and artifact type combination, the module creates:
- A GAR repository in the environment's GCP project
- Automatic vulnerability scanning (Container Analysis + Container Scanning APIs)
- IAM bindings granting
roles/artifactregistry.readerto upstream service agents
The upstream_service_agents field is used to grant the BU-level shared project's GAR service agent read access, enabling virtual repository aggregation at the BU level.
The Tenant Repository (foundations-template)
Tenant repositories are typically created from the foundations-template via the template variable. This template provides:
- YAML-driven configuration in
terraform/configs/{env}/*.yaml - Single module call to
tenant-infra-factorythat reads YAML and provisions resources - Supported resource types: Cloud Run, GCS, BigQuery, Labels
- Reusable workflow integration for CI/CD
tenant-repo/
├── terraform/
│ ├── configs/
│ │ ├── development/
│ │ │ └── resources.yaml
│ │ ├── non-production/
│ │ │ └── resources.yaml
│ │ └── production/
│ │ └── resources.yaml
│ └── main.tf (calls tenant-infra-factory)
└── catalog-info.yaml
Usage Example
module "tenant" {
source = "app.terraform.io/Badal_devex/create-tenant/platform"
version = "~> 1.0"
name = "backstage"
business_unit = "devex"
gh_org = "badal-io"
template = {
owner = "badal-io"
repository = "foundations-template"
}
branching_model = {
environment_based = {}
}
budget = {
amount = 1000
}
environments = {
non-production = {
org_id = "758951886862"
billing_account_id = "01DEF7-F9833E-AD765A"
parent_folder_id = "folders/123456789"
service_account_roles = ["roles/run.admin", "roles/storage.admin"]
}
production = {
org_id = "758951886862"
billing_account_id = "01DEF7-F9833E-AD765A"
parent_folder_id = "folders/234567890"
service_account_roles = ["roles/run.admin", "roles/storage.admin"]
budget = {
amount = 5000
alert_spent_percents = [0.5, 0.8, 0.9, 1.0]
}
}
}
tfc = {
org_id = "Badal_devex"
gh_app_oauth_id = "ot-xxxxxxxxxxxxx"
share_state = true
}
gar = {
enable = true
location = "us-central1"
artifact_types = ["DOCKER"]
}
}
Default APIs and Roles
Every tenant project has the following APIs enabled by default:
iamcredentials.googleapis.comiam.googleapis.comserviceusage.googleapis.comcloudresourcemanager.googleapis.comcloudbilling.googleapis.com
When GAR is enabled, additionally:
artifactregistry.googleapis.comcontaineranalysis.googleapis.comcontainerscanning.googleapis.com
When budgets are configured:
billingbudgets.googleapis.com
Every service account automatically receives roles/serviceusage.serviceUsageViewer in addition to the roles specified in service_account_roles.
Project Labels
Each GCP project is automatically labeled with:
| Label | Value |
|---|---|
business_unit | The parent BU name |
environment | The environment key |
tenant | The tenant name |
Additional labels can be added via the per-environment labels field.