Create Power BI deployment pipelines automatically

Power BI Azure Pipelines This is Part 6 of a series about creating a professional developer experience for working with Power BI. If you haven't already seen the first, you may prefer to start there.

In part 4 of this series, I introduced a standard pattern for organising report files and pipelines, with a standard process for creating new reports. Repeatable patterns and processes are great candidates for automation, and in this post I'll build a report creation process that automatically configures a new report and creates its deployment pipeline 😃.

This video (5m31s) shows you the process in action, from creating a new report to seeing it deployed automatically to Power BI:

At a high level, here's how it works:

  1. A developer runs a script to create a new report. The script:
    • creates the report by copying a template report folder
    • modifies files in the copied folder to correspond to the new report
    • creates a new Git feature branch for the report, then commits and pushes the new files to the central Git repository.

  2. In the background, pushing the feature branch to the central Git repo triggers a special DevOps pipeline called the Report Pipeline Manager. This pipeline:
    • inspects the repository for report pipeline configuration YAML files (reports/*/pipeline.yaml)
    • for each identified pipeline configuration file, checks for the existence of a corresponding Azure DevOps pipeline
    • if no corresponding report deployment pipeline is found, it creates a new one.

      At this point, everything needed to deploy the report automatically has been set up! 🚀

  3. Later, the developer takes an action required to trigger a deployment pipeline – for example, they open a PR to add an updated report to a release candidate. By now, the pipeline manager has created the necessary deployment pipeline – so the report is automatically deployed to Power BI.

Let's dig into some of the detail 😊!

The report setup script set-up-new-report.cmd is a small DOS batch script which launches a PowerShell script, tools/New-PbiReport.ps1:

@echo off
echo Enter folder name for new report (no spaces!)
powershell.exe -command "& '..\tools\New-PbiReport.ps1'"

The final pause is there just to give me a chance to look at any output on the console.

Here's the PowerShell script called by the batch file, New-PbiReport.ps1:

  1. param(
  2. [Parameter(Mandatory = $true)]
  3. [ValidateNotNullOrEmpty()]
  4. [string] $FolderName,
  6. [Parameter(Mandatory = $False)]
  7. [bool] $WithGitActions = $True
  8. )
  11. $toolsFolder = $MyInvocation.MyCommand.Path | Split-Path
  12. Import-Module $toolsFolder\PbiDeployment\PbiDeployment.psm1 -Force
  13. Set-Location $toolsFolder
  15. $reportFolder = "$toolsFolder\..\reports\$FolderName"
  16. if(Test-Path -Path "$reportFolder") {
  17. throw "$reportFolder already exists. Delete the existing folder or create a new one."
  18. }
  20. # create new report folder
  21. Copy-Item `
  22. -Path "$toolsFolder\ReportTemplate" `
  23. -Destination "$reportFolder" `
  24. -Recurse
  26. Write-Host "Created report folder $reportFolder"
  28. # configure pipeline YAML files
  29. foreach($file in @('pipeline.yaml', 'metadata.yaml')) {
  30. $content = Get-Content -Path "$reportFolder\$file"
  31. $content = $content -Replace "_ReportTemplate_", "$FolderName"
  32. Set-Content -Path "$reportFolder\$file" -Value $content
  33. }
  35. # create & push feature branch
  36. if($WithGitActions -eq $true) {
  37. New-ReportBranch -FolderPath $reportFolder
  38. }

The script prompts for a FolderName value, then checks that no folder of that name already exists (lines 17-19). If the folder doesn't exist yet, it's created by copying the folder tools/ReportTemplate (along with all its contents) and renaming it (lines 21-25).

There are three files copied from the report template folder: a blank Report.pbix file, and the two YAML files pipeline.yaml and metadata.yaml. The two YAML files are edited to replace a placeholder value (“_ReportTemplate_”) with the name of the new folder (lines 28-34) – this makes the file pipeline.yaml ready for use, and starts the customisation of metadata.yaml.

The remainder of the file (lines 36-39) creates and pushes a Git feature branch for the new report by calling the custom function New-ReportBranch.

The New-ReportBranch function called at the end of script New-PbiReport.ps1 is defined in the module PbiDeployment\PbiDeployment.psm1:

  1. # Creates a new feature branch, adds a specified folder, pushes to origin.
  2. function New-ReportBranch ([string]$FolderPath) {
  3. $FolderName = Split-Path $FolderPath -Leaf
  4. $BranchName = "create-$FolderName"
  6. Invoke-Utility git checkout main
  7. Invoke-Utility git pull
  8. Invoke-Utility git checkout -b $BranchName
  9. Invoke-Utility git add "$FolderPath"
  10. Invoke-Utility git commit -m "Initialised report folder $FolderName"
  11. Invoke-Utility git push --set-upstream origin $BranchName
  13. Write-Host "`n------------------------------------------------`n"
  14. Invoke-Utility git status
  15. Write-Host ""
  16. }

The function takes the path of the new report folder as a parameter, and uses it to define the name of a new feature branch. The rest of the function invokes a series of git commands to:

  • sync my local repository with the central copy
  • create the new branch
  • commit the new files to the branch
  • push the branch to the Git server (in my case, GitHub).

Pushing the new feature branch to GitHub (or Azure DevOps) means that the central repository now has a copy of the pipeline configuration file pipeline.yaml – but no corresponding Azure DevOps pipeline “wrapper” exists yet. The pipeline manager is a special DevOps pipeline which ensures that every report's pipeline.yaml file has a corresponding wrapper pipeline in Azure DevOps.

This is the YAML definition for the pipeline manager:

    - '*'
    - main
    - rc/*
    - /powerbi-pro-devex-series/06-ReportCreation/reports
pr: none

- group: DeploymentSecrets
- name: folderPath
  value: $(System.DefaultWorkingDirectory)/powerbi-pro-devex-series/06-ReportCreation

  vmImage: ubuntu-latest

- task: PowerShell@2
  displayName: Manage report pipelines
    targetType: filePath
    filePath: $(folderPath)/tools/Sync-AzureDevOps.ps1
    arguments: >
      -AzureDevOpsOrganization ""
      -AzureDevOpsProject "PowerBiProDev"
      -RepositoryLocalPath "$(Build.Repository.LocalPath)"
      -BranchName "$(Build.SourceBranchName)"
    failOnStderr: true
    AZURE_DEVOPS_EXT_PAT: $(System.AccessToken)
    AZURE_DEVOPS_EXT_GITHUB_PAT: $(GitHubPersonalAccessToken)
    GITHUB_SERVICE_CONNECTION_ID: $(GitHubServiceConnectionId)

The trigger section indicates that the pipeline is triggered only on pushes to branches (not by any pull requests), and only when the target branch is not main or a release candidate (rc/). This means that the pipeline is triggered by every push to a feature branch – this may seem unnecessary, but it offers additional flexibility (described below).

The pattern of the pipeline body is familiar: it calls a PowerShell script 😊 – in this case, Sync-AzureDevOps.ps1. Notice however that three additional environment variables are being set:

  • AZURE_DEVOPS_EXT_PAT is set to the Azure DevOps system variable System.AccessToken – this provides an Azure DevOps personal access token, specific to the build agent executing the pipeline.
  • AZURE_DEVOPS_EXT_GITHUB_PAT is set to the pipeline variable GitHubPersonalAccessToken, a variable I've stored the value securely with other secret variables. It contains a GitHub personal access token I have created to allow access to my GitHub account.
  • GITHUB_SERVICE_CONNECTION_ID is set to the pipeline variable GitHubServiceConnectionId, another variable I've added to the deployment variable group. We'll see where its value comes from – and what it's for – in a moment.

The two access tokens, passed into environment variables, will be used to authenticate the pipeline manager when creating the new report's deployment pipeline.

Part of PowerShell script Sync-AzureDevOps.ps1 is shown below. (The full version is available in the code files that accompany this post, available on GitHub).

  1. # get existing Azure DevOps pipelines in target folder
  2. $pipelines = az pipelines list `
  3. --organization "$AzureDevOpsOrganization" `
  4. --project "$AzureDevOpsProject" | ConvertFrom-Json
  6. # check YAML pipeline definitions for corresponding Azure DevOps pipelines
  7. foreach($folderPath in Get-ChildItem -Path $reportsFolder) {
  8. if(Test-Path -Path "$folderPath/pipeline.yaml") {
  9. $pipelineFolder = "\06-ReportCreation\Reports"
  10. $pipelineName = "Deploy $(Split-Path -Leaf $folderPath) report"
  12. if(Test-DevOpsPipeline -Folder $pipelineFolder -Name $pipelineName -Existing $pipelines) {
  13. Write-Host "FOUND $pipelineFolder\$pipelineName (already exists)"
  14. continue
  15. }
  17. $yamlPath = "$folderPath/pipeline.yaml" -Replace $RepositoryLocalPath,''
  19. az pipelines create `
  20. --organization "$AzureDevOpsOrganization" --project "$AzureDevOpsProject" `
  21. --name "$pipelineName" --folder-path "$pipelineFolder" `
  22. --yaml-path "$yamlPath" --skip-first-run `
  23. --service-connection "$Env:GITHUB_SERVICE_CONNECTION_ID" --branch "$BranchName" `

Lines 36-38 use the Azure CLI command az pipelines list to list existing Azure DevOps pipelines in the specified Azure DevOps project. I haven't explicitly signed in here, because I don't need to – presenting an access token via the AZURE_DEVOPS_EXT_PAT environment variable takes care of that for me.

Lines 41 onward iterate through the list of folders in the reports directory. For each folder, the script:

  • constructs the name of the corresponding DevOps pipeline (line 44)
  • checks if it exists (line 46-49)…
  • …and if it doesn't, creates it using the Azure CLI az pipelines create command.

Although AZURE_DEVOPS_EXT_PAT simplifies logging in, I still need to allow build agents to access Azure DevOps pipelines. I also need to enable access to my GitHub repository, so the pipeline manager can access the new report's pipeline.yaml file.

Grant access to Azure DevOps

So far, the only thing I've needed is grant access to is Power BI. I did that in the first post of this series, using a specially created service principal which I allowed to access Power BI workspaces. Now I need to allow the report manager pipeline to manage other Azure DevOps pipelines.

All my Azure DevOps pipelines are executed by the project's build service, a principal created automatically when I created my Azure DevOps project. I allow the build service to edit pipelines like this:

  1. Use the ellipsis button in the top right of the Pipelines screen to find and select the “Manage security” option:

  2. On the project permissions page, select the project build service, then set the “Edit build pipeline” option to “Allow”:

Enable access to GitHub

This applies to me because I'm storing my code in GitHub. If you're in Azure DevOps you need to take a different approach – I'll that cover in a later post.

Now the build service can modify the Azure DevOps pipeline, but remember that this pipeline is just a “wrapper” – an Azure DevOps object that encapsulates the underlying YAML pipeline definition. All my code is in a GitHub repo, so for the pipeline manager to be able to encapsulate the YAML file in a new pipeline, it needs access to that repo.

In fact, I've already allowed Azure DevOps to access GitHub – when I created my first pipeline, I signed in to GitHub when prompted by Azure DevOps. This created an Azure DevOps service connection, which you can find via the “Service connections” page of your Azure DevOps project settings:

Because I created the service connection, I'm allowed to use it personally – but the pipeline manager isn't. To grant access, I access the service connections security settings using the ellipsis button in the top right (indicated in the screenshot above) then under “User permissions” I add the project build service to the project's User role:

Finally, I'm going to need to be able to refer to it in the pipeline – for that I'll need the GUID that identifies the service connection. It's displayed in the page URL as the resource_id parameter. I've indicated its position in the address bar on the screenshot, but obfuscated its value.

Look again at line 57 of the PowerShell script and you can see where the value of the GitHub service connection ID is needed. I can reference it as an environment variable because I passed it into the environment on the last line of the YAML pipeline definition.

Finally! All the code is ready, all the permissions granted – the only thing left to do is to create the pipeline manager. This is a simple Azure DevOps pipeline, using the YAML file I've already defined. It needs no additional configuration – all the necessary information is stored in the PowerShell script, or the YAML pipeline, or the variable group it references.

In this post I showed you a scripted process for creating a new Power BI report and setting up its deployment pipeline.

  • Using a report template makes it easy to ensure that all the components you want for a report – configuration information, and perhaps a standard look and feel – are set up correctly from the start.
  • Using the report setup script customises the report template, performing initial setup and adding the report to centralised version control.
  • Using a pipeline manager means that, when the report is added to centralised version control, its deployment pipeline is created automatically, ready to support your team's development workflow.
  • Next up: In the next post I'll return to managing standalone Power BI datasets – this time stored as a collection of TMDL files.

  • Code: The code for the series is available on Github. The files specific to this article are in the powerbi-pro-devex-series/06-ReportCreation folder.

  • Share: If you found this article useful, please share it!