Microsoft Fabric Updates Blog

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

  1. Accelerating first steps using the CLIs.
  2. Using MCP servers and Fabric CLI to help define your fabric resources.
  3. Creating a workload identity with Fabric permissions.
  4. 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:

  1. Add a pipeline file and a .gitignore file to your repo.
  2. Sync your Git repo with the cloud Git provider.
  3. Define pipeline variables for the identity and backend.
  4. Add an OpenID Connect federated credential.
  5. 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"
GitHub showing the GitHub Actions variables for a repo following the successful creation of 7 variables.
GitHub Actions variables

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."
Azure Portal showing the newly created federated credential for the managed identity. The subject identifier is repo:azurecitadel/fabric_terraform_provider:ref:refs/heads/main
Federated credential for the managed identity

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
GitHub showing a successful run for the terraform.yml GitHub workflow.
Successful GitHub Workflow

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.

Entradas de blog relacionadas

Terraform Provider for Microsoft Fabric: #4 Deploying a Fabric config with Terraform in GitHub Actions

octubre 22, 2025 por Santhosh Kumar Ravindran

Managed Private Endpoints support for connecting to Private Link Services is now available in Microsoft Fabric (Public REST APIs). This has been one of the top requests from our customers and the community: the ability to securely connect Fabric Spark compute to on-premises and network-isolated data sources using the option to allowlist Fully Qualified Domain … Continue reading “Securely Accessing External and On-Premises Data Sources with Fabric Data Engineering Workloads”

octubre 13, 2025 por Matthew Hicks

Microsoft OneLake is the unified data lake for your entire organization, built into Microsoft Fabric. It provides a single, open, and secure foundation for all your analytics workloads – eliminating data silos and simplifying data management across domains. The preview of Microsoft OneLake Table APIs, a new way to programmatically manage and interact with your … Continue reading “OneLake Table APIs (Preview)”