Infrastructure-as-code for provisioning Microsoft Fabric capacities, workspaces, environments, and lakehouses on Azure using Terraform.
- Architecture
- Prerequisites
- Repository layout
- Modules
- Environment configuration
- Authentication setup
- Getting started locally
- CI/CD pipeline
- Variable reference
- Outputs reference
- Fabric SKU sizing guide
- Troubleshooting
Azure Subscription
└── Resource Group (azurerm_resource_group)
└── Fabric Capacity (azurerm_fabric_capacity) ← ARM resource, billed per CU
│
│ assigned via capacity_id
▼
Microsoft Fabric (SaaS plane — microsoft/fabric provider)
├── Workspace A (fabric_workspace)
│ ├── Role Assignments (Admin / Contributor / Viewer)
│ ├── Environment (fabric_environment) [optional]
│ └── Lakehouse (fabric_lakehouse) [optional]
└── Workspace B ...
Two Terraform providers work in concert:
| Provider | Purpose |
|---|---|
hashicorp/azurerm |
Provisions the Fabric Capacity ARM resource and resource group |
microsoft/fabric |
Manages Fabric-native objects: workspaces, environments, lakehouses, role assignments |
hashicorp/azuread |
Available for AAD group/user lookups if needed |
| Tool | Minimum version | Notes |
|---|---|---|
| Terraform | 1.7.0 | Required for optional() object attributes |
| Azure CLI | 2.57.0 | Used for local auth |
| Azure subscription | — | Must have Fabric capacity quota; request via Azure portal |
| Microsoft Fabric license | — | At least F2 capacity or a trial |
| Azure AD permissions | — | Application.ReadWrite.All for Fabric provider OIDC setup |
fabric/
├── .github/
│ └── workflows/
│ ├── _terraform.yml # Reusable workflow (plan + apply)
│ ├── ci.yml # PR: parallel plan across all envs + PR comment
│ └── deploy.yml # Push to main: dev → staging → prod (approval gate)
│
├── modules/
│ ├── fabric_capacity/ # ARM capacity resource
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── fabric_workspace/ # Workspace, role assignments, environment, lakehouse
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
│
├── environments/
│ ├── dev/ # Dev environment entry point
│ │ ├── main.tf # calls root module with environment = "dev"
│ │ ├── variables.tf
│ │ └── terraform.tfvars # dev-specific values
│ ├── staging/ # Staging environment entry point
│ └── prod/ # Production environment entry point
│
├── main.tf # Root module: wires modules together
├── variables.tf # Root variable declarations
├── outputs.tf # Root outputs
├── versions.tf # Provider version pins
├── backend.tf # Remote state backend template (commented)
└── terraform.tfvars.example # Copy → terraform.tfvars, fill in values
Wraps azurerm_fabric_capacity to provision a Fabric capacity unit in an Azure resource group.
Path: modules/fabric_capacity
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
capacity_name |
string |
yes | — | Name of the capacity (must be globally unique in the region) |
resource_group_name |
string |
yes | — | Existing resource group name |
location |
string |
yes | — | Azure region (e.g. "West Europe") |
sku_name |
string |
yes | — | SKU size: F2–F2048 |
admin_members |
list(string) |
yes | — | UPNs or object IDs of capacity administrators |
tags |
map(string) |
no | {} |
Resource tags |
| Name | Description |
|---|---|
capacity_id |
Full ARM resource ID of the capacity |
capacity_name |
Name of the capacity |
capacity_sku |
Provisioned SKU name |
Creates a Fabric workspace, assigns RBAC roles, and optionally provisions an environment and/or lakehouse inside it.
Path: modules/fabric_workspace
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
workspace_key |
string |
yes | — | Unique key used in resource naming |
display_name |
string |
yes | — | Workspace display name |
description |
string |
no | "" |
Workspace description |
capacity_id |
string |
yes | — | ARM resource ID of the Fabric capacity |
admin_principals |
list(object) |
no | [] |
Principals for the Admin role |
contributor_principals |
list(object) |
no | [] |
Principals for the Contributor role |
viewer_principals |
list(object) |
no | [] |
Principals for the Viewer role |
create_environment |
bool |
no | false |
Whether to create a Fabric environment |
environment_name |
string |
no | "" |
Name of the environment (defaults to <display_name>-env) |
create_lakehouse |
bool |
no | false |
Whether to create a Lakehouse |
lakehouse_name |
string |
no | "" |
Lakehouse name (defaults to <display_name>-lakehouse) |
Each principal object has the shape:
{
id = "<aad-object-id>"
type = "User" # User | Group | ServicePrincipal
}| Name | Description |
|---|---|
workspace_id |
Fabric workspace GUID |
workspace_url |
Direct URL to open the workspace in the Fabric portal |
environment_id |
Fabric environment ID, or null |
lakehouse_id |
Fabric lakehouse ID, or null |
Each environment under environments/ is a standalone Terraform root. It pins its own required_providers block and calls the repository root module (../../) passing environment-specific values.
| Environment | Default SKU | Auto-deploy | Approval required |
|---|---|---|---|
dev |
F2 | Yes (on push to main) |
No |
staging |
F4 | Yes (after dev succeeds) | No |
prod |
F8 | No | Yes — GitHub Environment gate |
To add a workspace, edit the workspaces map in environments/<env>/terraform.tfvars:
workspaces = {
my_new_workspace = {
display_name = "My New Workspace"
description = "Purpose of this workspace"
create_environment = true
environment_name = "my-workspace-env"
create_lakehouse = true
lakehouse_name = "my-workspace-lh"
admin_principals = [
{ id = "<aad-group-object-id>", type = "Group" },
]
contributor_principals = []
viewer_principals = []
}
}-
Log in with the Azure CLI:
az login --tenant <your-tenant-id> az account set --subscription <your-subscription-id>
-
The
azurermprovider picks up the CLI token automatically whenuse_oidcis not set.
Themicrosoft/fabricprovider also uses Azure CLI credentials by default. -
Your identity needs:
Contributoron the target resource group (or subscription) for ARM resources- Fabric capacity administrator membership for workspace operations
The pipeline uses Workload Identity Federation — no long-lived secrets.
Step 1 — Create an App Registration per environment:
ENV=dev # repeat for staging, prod
az ad app create --display-name "sp-fabric-terraform-${ENV}"
APP_ID=$(az ad app list --display-name "sp-fabric-terraform-${ENV}" --query '[0].appId' -o tsv)
az ad sp create --id "$APP_ID"
OBJ_ID=$(az ad sp show --id "$APP_ID" --query id -o tsv)Step 2 — Add a federated credential:
REPO="your-org/your-repo"
# For push to main (deploy workflow)
az ad app federated-credential create --id "$APP_ID" --parameters "{
\"name\": \"github-deploy-${ENV}\",
\"issuer\": \"https://token.actions.githubusercontent.com\",
\"subject\": \"repo:${REPO}:environment:${ENV}\",
\"audiences\": [\"api://AzureADTokenExchange\"]
}"
# For pull requests (CI workflow)
az ad app federated-credential create --id "$APP_ID" --parameters "{
\"name\": \"github-pr\",
\"issuer\": \"https://token.actions.githubusercontent.com\",
\"subject\": \"repo:${REPO}:pull_request\",
\"audiences\": [\"api://AzureADTokenExchange\"]
}"Step 3 — Grant RBAC on the subscription:
SUB_ID=$(az account show --query id -o tsv)
az role assignment create \
--assignee "$OBJ_ID" \
--role "Contributor" \
--scope "/subscriptions/${SUB_ID}"For Fabric workspace operations the service principal also needs the Fabric Administrator role in the Microsoft 365 Admin Center, or be added as a capacity administrator.
Step 4 — Add GitHub Secrets (Settings → Secrets and variables → Actions):
| Secret | Value |
|---|---|
ARM_TENANT_ID |
Your Azure AD tenant ID (shared across envs) |
ARM_CLIENT_ID_DEV |
App Registration client ID for dev |
ARM_SUBSCRIPTION_ID_DEV |
Azure subscription ID for dev |
ARM_CLIENT_ID_STAGING |
App Registration client ID for staging |
ARM_SUBSCRIPTION_ID_STAGING |
Azure subscription ID for staging |
ARM_CLIENT_ID_PROD |
App Registration client ID for prod |
ARM_SUBSCRIPTION_ID_PROD |
Azure subscription ID for prod |
# 1. Clone the repo and move into an environment
cd environments/dev
# 2. Copy the example vars and fill in your values
cp ../../terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars — set subscription_id, tenant_id, principal IDs
# 3. Enable remote state (optional but recommended)
# Uncomment the backend block in environments/dev/main.tf
# then run:
terraform init \
-backend-config="storage_account_name=<your-storage-account>" \
-backend-config="resource_group_name=rg-tfstate"
# 4. Preview changes
terraform plan
# 5. Apply
terraform applyNote:
terraform.tfvarsis in.gitignore. Never commit it — it may contain object IDs and admin UPNs.
Pull Request opened / updated
└── ci.yml
├── Plan / dev ─┐
├── Plan / staging ├─ parallel
├── Plan / prod ─┘
├── Post plan summary comment on PR
└── CI gate (blocks merge if any plan returns exit code 1)
Push to main (or merged PR)
└── deploy.yml
├── Apply / dev (automatic)
├── Apply / staging (automatic, after dev succeeds)
└── Apply / prod (waits for manual approval in GitHub Environment)
Manual dispatch
└── deploy.yml (workflow_dispatch)
└── Apply / <chosen environment>
Create three GitHub Environments under Settings → Environments:
| Environment | Protection rule |
|---|---|
dev |
None — deploys automatically |
staging |
None — deploys automatically after dev |
prod |
Required reviewers — add your platform team |
The reusable workflow sets environment: ${{ inputs.environment }} on the apply job, so GitHub enforces the protection rules automatically before the job starts.
All secrets are scoped to the repository (not per-environment), but the reusable workflow selects the right secret based on the environment input.
| Secret | Scope | Description |
|---|---|---|
ARM_TENANT_ID |
All envs | Azure AD tenant ID |
ARM_CLIENT_ID_DEV |
dev | Service principal client ID |
ARM_SUBSCRIPTION_ID_DEV |
dev | Azure subscription ID |
ARM_CLIENT_ID_STAGING |
staging | Service principal client ID |
ARM_SUBSCRIPTION_ID_STAGING |
staging | Azure subscription ID |
ARM_CLIENT_ID_PROD |
prod | Service principal client ID |
ARM_SUBSCRIPTION_ID_PROD |
prod | Azure subscription ID |
| Variable | Type | Required | Default | Description |
|---|---|---|---|---|
subscription_id |
string |
yes | — | Azure subscription ID |
tenant_id |
string |
yes | — | Azure AD tenant ID |
environment |
string |
yes | — | dev | staging | prod |
location |
string |
no | "West Europe" |
Azure region |
resource_group_name |
string |
yes | — | Resource group name |
capacity_name |
string |
yes | — | Fabric capacity resource name |
capacity_sku |
string |
no | "F2" |
Fabric SKU (F2–F2048) |
capacity_admin_members |
list(string) |
yes | — | Capacity admin UPNs / object IDs |
workspaces |
map(object) |
no | {} |
Workspace definitions (see below) |
tags |
map(string) |
no | {} |
Tags applied to all Azure resources |
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
display_name |
string |
yes | — | Workspace display name |
description |
string |
no | "" |
Workspace description |
admin_principals |
list(object) |
no | [] |
Admin role principals |
contributor_principals |
list(object) |
no | [] |
Contributor role principals |
viewer_principals |
list(object) |
no | [] |
Viewer role principals |
create_environment |
bool |
no | false |
Create a Fabric environment |
environment_name |
string |
no | "" |
Environment display name |
create_lakehouse |
bool |
no | false |
Create a Fabric lakehouse |
lakehouse_name |
string |
no | "" |
Lakehouse display name |
| Output | Description |
|---|---|
resource_group_name |
Name of the provisioned resource group |
fabric_capacity_id |
Full ARM resource ID of the Fabric capacity |
fabric_capacity_name |
Capacity resource name |
workspace_ids |
map(workspace_key → workspace_id) |
workspace_urls |
map(workspace_key → https://app.fabric.microsoft.com/groups/<id>) |
environment_ids |
map(workspace_key → environment_id) — only keys where create_environment = true |
lakehouse_ids |
map(workspace_key → lakehouse_id) — only keys where create_lakehouse = true |
| SKU | Compute Units | Recommended workload |
|---|---|---|
| F2 | 2 CU | Development, proof of concept |
| F4 | 4 CU | Staging, light ETL |
| F8 | 8 CU | Small production, shared analytics |
| F16 | 16 CU | Medium production |
| F32 | 32 CU | Large teams, heavy ETL |
| F64+ | 64+ CU | Enterprise, real-time analytics |
Fabric capacity is billed per-second while active. Use the Fabric Capacity Metrics app to right-size.
Error: A resource with the ID already exists
Terraform state is out of sync with Azure. Import the existing resource:
terraform import module.fabric_capacity.azurerm_fabric_capacity.this \
/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Fabric/capacities/<name>Error: Fabric capacity quota not available in region
Request quota via Azure Portal → Subscriptions → Usage + quotas → Microsoft.Fabric.
Error: InvalidAuthenticationToken (Fabric provider)
The service principal is missing the Fabric Administrator role. Add it in the Microsoft 365 Admin Center under Settings → Microsoft Fabric → Admin API settings → Service principals can use Fabric APIs.
State lock timeout
If a pipeline run was cancelled mid-apply, the state may be locked. Find the lock ID in the error message and run:
terraform force-unlock <lock-id>Or trigger the deploy workflow with a force_unlock_id input via workflow_dispatch.
terraform fmt check fails in CI
Run terraform fmt -recursive locally before pushing.