How to set up CI/CD for Microsoft Fabric
The control plane in code, the data plane through a release mechanism: which of the four deployment patterns fits you, and why your network choice may have already made that call.

- 01
Separate the control plane from the data plane
Fabric gives you four ways to deploy items and two languages to describe infrastructure. Turning that into a tooling debate means choosing too early. The question to answer first is which things rarely change and have wide blast radius, and which change every sprint and stay local.
Layer Owner Tooling Pace Azure foundation Platform engineering Bicep or Terraform Rarely Fabric control plane Platform engineering Terraform or REST APIs Rarely Fabric items Data engineering and BI Git, fabric-cicd or pipelines Every sprint Release orchestration DevOps Azure DevOps or GitHub Actions Every change And what actually sits in each layer:
- Azure foundation: resource groups, storage, Key Vault, managed identities, Log Analytics and the Fabric capacity itself.
- Fabric control plane: workspaces, capacity assignment, domains, connections, roles and the Git wiring of each workspace.
- Fabric items: lakehouses, warehouses, notebooks, data pipelines, dataflows, semantic models, reports, eventhouses, Spark environments and variable libraries.
- Release orchestration: validation, approvals, the order of the steps and the checks afterwards.
That line is not a matter of taste. Creating a workspace touches identity, capacity and networking, and you want to see that in a plan before it happens. Changing a notebook touches nothing outside that workspace, and you specifically do not want to push it through a platform team's approval round.
Decisionput the control plane in code and let the data plane move through a release mechanism. Skip that line and one of two things happens: your infrastructure gets clicked together by hand, or your notebook change waits for aterraform apply. - 02
Pick one deployment mechanism for your items
Microsoft describes four patterns. They are not mutually exclusive, but you want one of them as the main road per solution. Two mechanisms on the same workspace means two sources of truth.
Approach Source of truth Fits Limitation Git with updateFromGit Git Teams with dev, test and prod branches Merge conflicts fabric-cicd or Items API Git Trunk-based teams that script Build it yourself Deployment pipelines Workspace Workspace-to-workspace promotion Manual setup API-driven, multi-tenant External config ISVs deploying to many customers High initial complexity The first two put Git in the lead role and differ mainly in who performs the deployment: Fabric itself, or your pipeline. The third inverts it. There the Development workspace is the source and you promote content to the next stage, with item pairing, deployment rules and a deployment history. That works well as long as you keep the first stage connected to Git; without Git you have promotion without a way back.
There is one constraint that can make this decision for you, and it does not appear in any list of benefits. Deployment pipelines do not work together with inbound and outbound access protection on a workspace. It is mutual, too: if a workspace sits in a deployment pipeline you can no longer enable outbound access protection, and if that protection is already on you cannot add the workspace to a pipeline.
Decisionsettle your network requirements before your deployment mechanism. If the workload needs workspace-level network isolation, deployment pipelines are off the table and you are left with Git integration or fabric-cicd.Pitfallnot every item type is supported by every mechanism, and that shifts with each release. Check the support matrix for the item types you actually use before you pick the mechanism, not after. - 03
Set up workspaces and Git integration
Give every environment its own workspace. In Fabric the workspace is the security and lifecycle boundary, so folders or name prefixes inside a single workspace do not give you the separation you think you have.
Environment Workspace Identity Data source Development Per developer or per team Developers Development data Test One integration workspace CI/CD service principal Test data at volume Production No manual changes Dedicated production principal Production sources Connect the first workspace to Git and put the items in a dedicated folder, for example
/fabric. Fabric writes each item as a folder named{display name}.{item type}with a.platformfile holding the metadata and the logical identity. If your pipeline YAML and scripts live in that same folder, the sync gets confused.Keep feature branches short and delete them after the merge. Fabric items are generated files; the longer two people work next to each other, the better the odds of a merge you have to settle by hand.
Decisionone repository for the whole solution, with a separate folder per workspace and a separate folder for your scripts and workflows. A repository per workspace sounds tidy, but then nobody holds a single version of the whole.Pitfallnever editlogicalIdin a.platformfile. Fabric uses that identity to tie an item in Git to an item in the workspace. Change it and the link is gone, and the next sync produces a duplicate. - 04
Put the infrastructure in code
The Fabric capacity is an ordinary Azure resource and belongs in the same IaC as the rest of your Azure foundation. For the Fabric objects on top of it, the workspaces and their Git wiring, you need the Terraform provider for Fabric or the REST APIs.
Bicep is the obvious choice if the rest of your organisation already works Azure-native. Terraform wins as soon as you want Azure and Fabric in one plan, because then the capacity and the workspace hanging off it share a state.
resource cap 'Microsoft.Fabric/capacities@2023-11-01' = { name: 'fabprod001' location: location sku: { name: 'F32' tier: 'Fabric' } properties: { administration: { members: [capacityAdminObjectId] } } }The workspace and its Git wiring in Terraform:
resource "fabric_workspace" "dev" { display_name = "ws-sales-dev" capacity_id = data.fabric_capacity.cap.id } resource "fabric_workspace_git" "dev" { workspace_id = fabric_workspace.dev.id initialization_strategy = "PreferRemote" git_provider_details = { git_provider_type = "AzureDevOps" organization_name = var.ado_org project_name = var.ado_project repository_name = var.ado_repo branch_name = "main" directory_name = "/fabric" } git_credentials = { source = "ConfiguredConnection" connection_id = fabric_connection.ado.id } }Put the Terraform state in protected cloud storage. State holds resource IDs, configuration and sometimes secrets, and a state file on a laptop or in the repo is exactly the kind of risk this whole setup is meant to remove.
Notethere is also a newer preview version of this resource. For production IaC you pin to the stable version, because a preview version may change without notice and you do not want aterraform planthat says something different from yesterday.DecisionBicep for Azure-native organisations, Terraform as soon as Azure and Fabric have to appear in the same plan. The worst answer is a bit of both, because then no single plan shows the whole. - 05
Sort out identity, and the four places it goes wrong
Automation should run as a service principal, not as an employee. A personal account in a pipeline falls outside your normal access management and breaks the day someone changes roles.
Method When Why OIDC or workload identity federation GitHub Actions, Azure DevOps No secret to rotate Managed identity Azure-hosted runners No credentials to manage Service principal with certificate Where federation is not possible Easier to govern than a secret Service principal with secret Last resort Key Vault only, with rotation PAT GitHub source control only Tied to a person, so fragile What sinks almost every first automated deployment is not the authentication but the authorisation around it. A service principal has to be allowed in four places, and all four live somewhere else:
- The tenant setting Service principals can create workspaces, connections, and deployment pipelines, for creating those three things.
- The tenant setting Service principals can call Fabric public APIs, for all other API traffic.
- An explicit role on the workspace itself. Membership of the tenant setting grants no access to content.
- Access in your Git provider. In Azure DevOps the principal has to be known in the organisation or project before the Git wiring works.
Those first two settings do not cover for each other. Enable only the first and your pipeline will happily create a workspace and then fail on the first item call, with an error that does not say which of the two is missing.
Decisionfederation over secrets, and scope the principal per environment. One principal that can reach everything is the reason a test deployment eventually touches production. - 06
Build the pipeline and the road to production
Azure DevOps and GitHub Actions do the same work here: validate, update infrastructure, deploy items, test and put production behind a gate. The difference is where the governance lives.
Azure DevOps GitHub Actions Fits Organisations on Azure Repos Teams that keep everything in the repo Authentication Service connection with federation OIDC through azure/loginApproval Environments with approvals Environments with reviewers Secrets Key Vault variable group Environment secrets A workflow that puts the two layers in the right order:
name: fabric-cicd on: push: branches: [main] paths: - "fabric/**" - "infra/**" permissions: id-token: write contents: read jobs: deploy-test: runs-on: ubuntu-latest environment: test steps: - uses: actions/checkout@v4 - uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Update the control plane run: | terraform -chdir=infra init terraform -chdir=infra apply -auto-approve - name: Deploy the items run: | pip install fabric-cicd azure-identity python scripts/deploy.py --environment testFor environment-specific values use a variable library rather than parameter files. It is supported by Git, deployment pipelines and the APIs, and it can carry its own value set per stage, so you no longer need a find-and-replace step between test and production.
The order from commit to production:
- Developer creates a short branch and works in a dedicated workspace.
- The change is committed to Git as an item definition.
- The pull request validates IaC, YAML, item definitions and secrets.
- After review the merge lands on the integration branch.
- The pipeline updates the control plane first.
- A check confirms workspace, capacity, roles and Git status.
- Only then do the items go out, through the mechanism from step 2.
- A script activates the value set and repairs broken references.
- Integration tests run on realistic data volume and capacity.
- An approval guards the transition to production.
- Production runs under its own principal with minimal rights.
- Monitoring confirms the release does what it should.
For monitoring afterwards the retention periods are worth knowing, because they decide how long you can still investigate an incident:
Source For Retention Monitoring hub Job status and history per item Continuous Workspace monitoring Queryable logs and metrics 30 days Capacity Metrics app Consumption and throttling 14 days Audit logs in Purview Who did what to which item 30 days Decisionone pipeline that runs both layers in a fixed order, with production behind an approval. Two separate pipelines that each do half look tidier, but then nobody guards the order between them.ImportantGit stores your item definitions, not your data. Agit revertrestores a notebook but does not undo what that notebook wrote to a lakehouse. So work out your rollback in terms of data impact and not just definitions, and rehearse it once before you need it.Noteworkspace monitoring and Log Analytics cannot both be on for the same workspace. Choose deliberately, because switching means removing the existing wiring first.
Common mistakes
The six you meet in almost every Fabric estate:
| Mistake | Why it hurts | Better |
|---|---|---|
| One workspace for all environments | Production changes by accident | A workspace per environment |
| Item definitions among scripts | Git sync goes wrong | A separate folder, e.g. /fabric |
| Hard-coded workspace and item IDs | Works in dev, breaks in test | Variable library |
| Deployment pipeline without Git | Promotion with no way back | Connect the first stage to Git |
| Assuming the data comes along | The lakehouse is empty after deploy | A load script after the deployment |
| No approval for production | Every merge is a production release | An environment with reviewers |
What to do next
This setup assumes your tenant, capacities and workspace topology are already in place, because everything in this guide hangs off them. See How to set up Microsoft Fabric. If you run Databricks alongside Fabric, then How to set up CI/CD for Azure Databricks is the counterpart to this guide, with the same split between platform and workload.
The process at a glance
Click a step for its key decision
Separate the control plane from the data plane
put the control plane in code and let the data plane move through a release mechanism. Skip that line and one of two things happens: your infrastructure gets clicked together by hand, or your notebook change waits for a terraform apply.
Read next
Related patterns
Related use cases
Related best practices
