Terraform Provider for Microsoft Fabric: #4 Deploying a Fabric config with Terraform in GitHub Actions
If you have been following this blog post series then you should have a working Terraform config from the first two posts, plus a managed identity from the third post that has the correct authorizations as a workload identity for use in deployment pipelines. In this last post we will configure OpenID Connect for the managed identity and run the GitHub Actions workflow.
This is the fourth and last post in the series
- Accelerating first steps using the CLIs.
- Using MCP servers and Fabric CLI to help define your fabric resources.
- Creating a workload identity with Fabric permissions.
- Deploying a fabric config with Terraform in GitHub Actions.
The primary CI/CD platforms commonly support OpenID Connect —now the de facto authentication standard, removing the risks of leaked secrets—and YAML pipelines with support for variables and secrets. In this post we will provide and example steps to plumb your workload identity into GitHub Actions and deploy the Terraform config:
- Add a pipeline file and a .gitignore file to your repo.
- Sync your Git repo with the cloud Git provider.
- Define pipeline variables for the identity and backend.
- Add an OpenID Connect federated credential.
- Run the pipeline.
Even if you are not using GitHub Actions then this page can still serve as a reference point for you to transpose the logic to your chose cloud Git platform.
Note: A more advanced setup would involve GitHub branch protection rules and multiple environments—each with its own variables and dedicated managed identity. However, this post focuses on the essentials: creating the federated credential and setting the correct environment variables to ensure the workflow runs successfully.
Configure for use in GitHub
Step one: Ensure you are in the root folder for your Terraform config and have authenticated into GitHub CLI using gh auth login.
Add a workflow YAML file
Example YAML file for running a Terraform deploy workflow on GitHub Actions. Create a .github/workflows/terraform.yml file in your repo and paste in the contents.
---
name: Terraform GitHub Actions Workflow
on:
workflow_dispatch:
inputs:
terraform_action:
description: 'Terraform Apply - choose action'
required: true
default: 'apply'
type: choice
options:
- 'apply'
- 'destroy'
run-name: Terraform ${{ github.event.inputs.terraform_action == 'destroy' && 'Destroy' || 'Apply' }}
jobs:
plan:
name: Terraform Plan
runs-on:
ubuntu-latest
permissions:
id-token: write
contents: read
concurrency: "${{vars.BACKEND_AZURE_STORAGE_ACCOUNT_NAME}}-${{vars.BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME}}"
env:
ARM_TENANT_ID: "${{ vars.ARM_TENANT_ID }}"
ARM_SUBSCRIPTION_ID: "${{ vars.ARM_SUBSCRIPTION_ID }}"
ARM_CLIENT_ID: "${{ vars.ARM_CLIENT_ID }}"
ARM_USE_AZUREAD: true
ARM_USE_OIDC: true
FABRIC_USE_OIDC: true
FABRIC_TENANT_ID: "${{ vars.ARM_TENANT_ID }}"
FABRIC_CLIENT_ID: "${{ vars.ARM_CLIENT_ID }}"
TF_VAR_subscription_id: "${{ vars.ARM_SUBSCRIPTION_ID }}"
TFVARS_FILE: "${{ vars.TFVARS_FILE }}"
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Install Terraform
uses: hashicorp/setup-terraform@v3
- name: Terraform Init
run: |
terraform init \
-backend-config="storage_account_name=${{vars.BACKEND_AZURE_STORAGE_ACCOUNT_NAME}}" \
-backend-config="container_name=${{vars.BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME}}" \
-backend-config="key=terraform.tfstate"
- name: Terraform Plan for ${{ github.event.inputs.terraform_action == 'destroy' && 'Destroy' || 'Apply' }}
run: |
if [ -n "${{ env.TFVARS_FILE }}" ]; then
echo "Using tfvars file: ${{ env.TFVARS_FILE }}"
terraform plan -out=tfplan -input=false ${{ github.event.inputs.terraform_action == 'destroy' && '-destroy' || '' }} -var-file=${{ env.TFVARS_FILE }}
else
echo "No tfvars file specified. Running terraform plan without -var-file."
terraform plan -out=tfplan -input=false ${{ github.event.inputs.terraform_action == 'destroy' && '-destroy' || '' }}
fi
- name: Create Module Artifact
run: |
stagingDirectory="staging"
mkdir -p "$stagingDirectory"
rsync -av --exclude=".git" --exclude=".terraform" --exclude=".github" --exclude="$stagingDirectory" ./ "$stagingDirectory/"
cp .terraform.lock.hcl "$stagingDirectory/"
tree -an "$stagingDirectory"
shell: bash
- name: Publish Module Artifact
uses: actions/upload-artifact@v4
with:
name: module
path: ./staging/
include-hidden-files: true
- name: Show the Plan for Review
run: terraform show tfplan
apply:
needs: plan
name: Terraform ${{ github.event.inputs.terraform_action == 'destroy' && 'Destroy' || 'Apply' }}
runs-on:
ubuntu-latest
concurrency: "${{vars.BACKEND_AZURE_STORAGE_ACCOUNT_NAME}}-${{vars.BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME}}"
permissions:
id-token: write
contents: read
env:
ARM_TENANT_ID: "${{ vars.ARM_TENANT_ID }}"
ARM_SUBSCRIPTION_ID: "${{ vars.ARM_SUBSCRIPTION_ID }}"
ARM_CLIENT_ID: "${{ vars.ARM_CLIENT_ID }}"
ARM_USE_AZUREAD: true
ARM_USE_OIDC: true
FABRIC_USE_OIDC: true
FABRIC_TENANT_ID: "${{ vars.ARM_TENANT_ID }}"
FABRIC_CLIENT_ID: "${{ vars.ARM_CLIENT_ID }}"
steps:
- name: Download a Build Artifact
uses: actions/download-artifact@v4
with:
name: module
- name: Install Terraform
uses: hashicorp/setup-terraform@v3
- name: List the Build Artifact
run: |
tree -an -I .terraform
shell: bash
- name: Terraform Init
run: |
terraform init \
-backend-config="resource_group_name=${{vars.BACKEND_AZURE_RESOURCE_GROUP_NAME}}" \
-backend-config="storage_account_name=${{vars.BACKEND_AZURE_STORAGE_ACCOUNT_NAME}}" \
-backend-config="container_name=${{vars.BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME}}" \
-backend-config="key=terraform.tfstate"
- name: Terraform ${{ github.event.inputs.terraform_action == 'destroy' && 'Destroy' || 'Apply' }}
run: terraform apply -input=false -auto-approve tfplan
The workflow needs a few variables that will be created soon. The pipeline will work even if your Terraform config makes use of the azurerm, azapi, and azuread providers in addition to the fabric provider as it has both ARM_ and FABRIC_ prefixed environment variables.
The ARM_USE_AZUREAD boolean forces terraform init to use RBAC authentication for accessing the blob storage for the remote state as the storage account has been configured to disable the access keys which are otherwise used by default.
The terraform init command also uses environment variables to configure the remote state. Having a full set of --backend-config switches remove the need for an azurerm backend block in the config.
Add a .gitignore file
If you don’t already have a .gitignore file in your repo then create one and add the following.
*.tfstate *.tfstate.* crash.log .terraform/ terraform.tfvars *.override.tf *.override.tf.json *.tfplan .env *.backup
The .gitignore file will ensure that git will not track files that match any of these files.
Push to GitHub
If you are already synced with a GitHub repo then commit and push.
git add .github/workflows/terraform.yml git commit -m "Added Terraform workflow" git push
If you have not initialised your repo yet then the following example commands will initialise your local git repo, create a new GitHub repo and then push up to sync.
owner="<my_github_organisation_or_user>" repo="<my_repo_name>" git init git add --all git commit -a -m "Initial commit" gh repo create $owner/$repo --private --source=. --remote=origin git push --set-upstream origin main
View the GitHub repo.
gh repo view --web
Add GitHub Actions variables
The GitHub Actions workflow expects a number of GitHub Actions variables. None of the values are sensitive so there is no need to define them as secrets.
Set the variable values.
terraform_resource_group_name="rg-terraform" managed_identity_name="mi-terraform" storage_account_prefix="saterraform" management_subscription_id="<subscription_guid>" workload_subscription_id="<subscription_guid>" managed_identity_client_id=$(az identity show --name $managed_identity_name --resource-group $terraform_resource_group_name --query clientId -otsv) storage_account_name="saterraform$(az group show --name $terraform_resource_group_name --query id -otsv | sha1sum | cut -c1-8)"
Create the GitHub Actions variables
gh variable set ARM_TENANT_ID --body "$(az account show --query tenantId -otsv)"
gh variable set ARM_CLIENT_ID --body "$managed_identity_client_id"
gh variable set ARM_SUBSCRIPTION_ID --body "${workload_subscription_id:-$management_subscription_id}"
gh variable set BACKEND_AZURE_SUBSCRIPTION_ID --body "$management_subscription_id"
gh variable set BACKEND_AZURE_RESOURCE_GROUP_NAME --body "$terraform_resource_group_name"
gh variable set BACKEND_AZURE_STORAGE_ACCOUNT_NAME --body "$storage_account_name"
gh variable set BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME --body "prod"

The repo is ready, but the workflow cannot yet authenticate to the managed identity until OpenID Connect is configured.
Create an OpenID Connect federated credential
OpenID Connect is the recommended way for a pipeline to authenticate to a workload identity as it avoids the risk of leaked secrets and operational issues with secret management. This federated credential configures the subject field to the specific repo and branch.
The example is configured to:
repo:azurecitadel/fabric_terraform_provider:ref:refs/heads/main.
subject=$(gh repo view --json nameWithOwner --template '{{printf "repo:%s:ref:refs/heads/main" .nameWithOwner}}')
az identity federated-credential create --name github --identity-name $managed_identity_name --resource-group $terraform_resource_group_name --audiences "api://AzureADTokenExchange" --issuer "https://token.actions.githubusercontent.com" --subject "$subject"
echo "Added federated credential."

Note: While a managed identity can support up to twenty federated credentials, it’s common practice to maintain a one-to-one relationship between a workload identity and its workflow. This approach helps enforce least privilege by assigning only the necessary permissions.
Run the workflow
The workflow uses a workflow_dispatch trigger, i.e. it is manually triggered (there are a number of other trigger options). Use the GitHub CLI to initiate the run.
gh workflow run terraform.yml
Follow the progress on GitHub.
gh workflow view terraform.yml --web

The workflow should complete successfully and you can click on the individual jobs to view the logs against each step.
Summary
Microsoft Fabric brings all of your most important data to one place for analytics and to fuel your Agentic AI.
The Fabric CLI and Terraform Provider for Microsoft Fabric offer a wealth of new options for automation and now administrators familiar with Terraform can declaratively automate fabric administration tasks.