Power Platform ALM with Azure DevOps pipelines

Most Power Platform estates start with someone editing the thing people depend on. This guide sets out how to move to a repeatable route from development to production using Azure DevOps pipelines, in the order you need to make the decisions.

Guide · · 12 min read

What ALM means here, and why it is worth the effort

The usual story goes like this: someone builds an app in the default environment, it becomes useful, more people rely on it, and changes are then made directly in the thing they depend on. Application lifecycle management is the discipline of moving away from that. You build in one place, test in another, release to production through a repeatable route, and keep the source somewhere you can see what changed and go back to it.

Azure DevOps is a sensible home for this if your organisation already uses it for other development, or wants approvals and audit in a tool that IT already controls. This guide covers the moving parts in the order you need to decide them: solutions, environments, the identity your pipelines run as, the build tools, configuration that differs between environments, and the approvals that stand between a change and production.

It assumes Dataverse-backed solutions containing canvas or model-driven apps and cloud flows. Most of it applies equally to one app maintained by one maker and to a larger team, though the payoff grows with the number of people changing things.

Solutions: unmanaged where you build, managed everywhere else

A solution is the unit you move between environments. Everything that belongs to your app should live in one: tables, columns, forms, views, apps, flows, environment variables, connection references and security roles. Create solutions under a publisher with a prefix you will be happy to see on every schema name for years. Choose the publisher and prefix once, for the organisation or the product, and do not use the default publisher.

Unmanaged solutions are for development. You can edit their components directly, and exporting one gives you the source. Managed solutions are for every environment downstream. Their components cannot be edited directly unless you allow it, they can be removed cleanly by uninstalling the solution, and importing a newer version as an upgrade removes components you deleted in development, which an unmanaged import cannot do.

The rule is simple: only import managed solutions into test and production. Importing unmanaged into production lets people make changes there that are never captured in source, and those changes are either overwritten at the next release or silently survive it. Both outcomes are the problem you were trying to solve.

Keep solutions few and cohesive. One solution per app or product area is easier to reason about than one per developer or per sprint. If several apps share a common set of tables, you can put those in their own solution and import it first, but layered dependencies make every release more complicated. Start with one solution and split only when you have a clear reason.

  • Create one publisher and prefix for your organisation's own development.
  • Create components inside the solution from the start rather than building outside it and adding later.
  • When adding an existing table to a solution, add only the components you changed rather than all of its assets.
  • Keep a short readme in the repository describing what each solution contains and the order to import them.

Environments and what each one is for

The minimum useful set is three: development, test and production, each a separate Power Platform environment with its own Dataverse database. Many teams add a fourth, a build or validation environment, which the pipeline uses to prove the solution imports cleanly from source before anything reaches test.

Development is where makers work in unmanaged solutions. If several people work on the same solution, they usually work in the same development environment, because Dataverse does not merge two people's changes to the same component in the way a code repository merges text. Larger teams sometimes give each developer an environment and treat the repository as the point of integration, but that only works when changes are small, frequent and unpacked to source.

Test is where the managed solution is imported exactly as it will be in production, by the same pipeline, and where business users check it. Production then receives the same managed artefact that passed test, not a fresh export from development.

Treat environment creation as a governed act. Restrict access to each environment with a security group, consider making production a Managed Environment if your licensing supports it so you get its additional governance controls, and keep makers out of test and production except for read access where they need it to diagnose problems. The feature set of Managed Environments has grown over time, so check current Microsoft documentation for what it includes.

Service principals and the pipeline identity

Your pipelines need an identity to sign in to each environment, and it should not be a person's account. Use a service principal: an app registration in Microsoft Entra ID, added to each environment as an application user with a security role.

The security role is usually System Administrator in the target environments, because a solution import touches almost everything. If that makes your security team uncomfortable, that is a reasonable reaction. The answer is to protect the service connection and the credential, rather than trying to deploy with less privilege and debugging partial imports. Treat the secret or certificate as you would an administrator password: store it only in the service connection, give it an expiry date, and put the renewal in someone's calendar.

Use separate app registrations for production and non-production. It costs little effort and means a leaked non-production credential cannot touch live data.

Keep the pipeline identity separate from the identity your production flows run under. Not every connector supports service principal authentication, and connections in production usually belong to a dedicated, licensed service account so that flows keep running regardless of who deployed them. Check current Microsoft guidance on flow ownership before you settle the design, as it has developed over time.

  • Create an app registration in Entra ID for Power Platform deployments, named so its purpose is obvious.
  • Create a client secret or certificate, or use workload identity federation if your build tools version and Azure DevOps organisation support it, which avoids storing a secret.
  • Add the app as an application user in each target environment through the Power Platform admin centre, and assign it a security role.
  • Create a Power Platform service connection in Azure DevOps for each environment, using the tenant ID, client ID and credential.
  • Restrict each service connection to the pipelines that need it, and add approvals and checks to the production one.

Building with the Power Platform Build Tools

The Power Platform Build Tools are an Azure DevOps extension, installed from the Visual Studio Marketplace, that wraps Power Platform CLI operations as pipeline tasks. If you know the pac commands you will recognise what each task does. A pipeline starts with the Power Platform Tool Installer task, then uses tasks such as WhoAmI, which is a useful first step to prove the connection works, Export Solution, Unpack Solution, Pack Solution, Checker, Set Solution Version and Import Solution. There are also tasks to create, copy, back up and delete environments, which make a disposable build environment practical.

Unpacking matters because it turns the solution zip into individual files you can diff and review. A pull request that shows a changed form definition or a modified flow tells a reviewer something; a binary zip tells them nothing. Canvas apps are less pleasant to review in source form than code, so do not rely on diff review alone for them, but the history is still worth having.

Version every build. The Set Solution Version task stamps the version from the pipeline's build number, so whatever is installed in test and production can be traced to a specific build and commit. Without it, you will eventually find two environments showing the same version number with different contents. Use the Checker task as a gate, not a report nobody reads. It runs solution checker against the packed solution and publishes its results as an artefact you can open from the pipeline run. Agree which severity of finding fails the build, start strict on new solutions, and on older solutions record the existing findings and stop the number growing rather than trying to fix everything before you can ship anything.

Some teams build the managed solution by importing the unmanaged source into the build environment and exporting managed from there, rather than packing managed directly from source. Both work. The import-and-export route proves the solution installs before it gets near test, at the cost of a slower pipeline. A typical arrangement uses two pipelines.

  • Export pipeline: run on demand by a developer when a change is ready, export the solution from development as both unmanaged and managed, unpack both into the repository and commit to a feature branch for a pull request.
  • Build pipeline: run on the pull request and on merge to main, pack the managed solution from source, run the Checker task and fail on serious issues, set the version, and publish the managed zip as a pipeline artefact.

Environment variables and connection references

Anything that differs between environments should not be hard-coded. Power Platform gives you two components for this, and both belong in the solution.

Environment variables hold configuration: a SharePoint site URL, a notification address, a feature flag, an API base address. Define the variable in the solution and read it in apps and flows instead of typing the value. Decide whether the solution should carry a current value. Usually it should not, so that importing into production does not overwrite production's value with development's. Remove the current value from the solution before export and supply it at import time instead. Secrets belong in Azure Key Vault, referenced by a secret-type environment variable, not in a plain text variable.

Connection references sit between a flow and a connection. The flow points at the reference, and the reference points at a real connection in each environment. This lets the same solution use a development mailbox in development and the production service account's mailbox in production without anyone editing the flow.

At import, the pipeline needs to know which value each variable takes and which connection each reference binds to. A deployment settings file handles this. Generate a template from the solution with the Power Platform CLI, keep one copy per target environment in the repository without secrets, and point the Import Solution task at the right file for each stage. Connection IDs differ per environment, so someone has to create the connections in each target once, under the right account, and record their IDs in the settings file.

  • Create every connection in test and production under the dedicated service account before the first deployment.
  • Keep one deployment settings file per environment in source control and review changes to it like code.
  • Check after each import that flows are switched on, because flows whose connection references were not resolved will not be.
  • Record which variables hold secrets and where the Key Vault for each environment lives.

Release pipelines, approvals and checks

The release pipeline takes the managed artefact from the build and imports it into test, then production. In YAML pipelines, model each target as an Azure DevOps environment and deploy with a deployment job against it. Approvals and checks attach to the environment rather than the pipeline, so they apply whichever pipeline tries to deploy there. If you use classic release pipelines, pre-deployment approvals on each stage do the same job.

Choose import behaviour deliberately. When a release removes components, import it as an upgrade so deleted components are removed from the target; the Import Solution task can stage an upgrade and the Apply Solution Upgrade task completes it. For a purely additive change, a simple update is quicker. Decide which your pipeline does by default and write it down, because the difference matters on the day someone deletes a column.

Back up production before each release. The Backup Environment task takes a manual backup as a pipeline step, giving you a restore point tied to the release. Restoring is a blunt instrument, since it rolls back data as well, so treat it as a last resort and plan to fix forward with a new version in most cases.

Solutions do not carry data. Reference rows such as categories, lookup values or settings held in Dataverse tables need a separate route, usually the Configuration Migration tool to produce a data file and a package deployed with the Deploy Package task. Decide early whether you need it, because retrofitting it is tedious. A sensible baseline for approvals looks like this.

  • Protect the main branch with a policy requiring a pull request, a reviewer who is not the author, and a successful build validation run.
  • Deploy to test automatically when main builds successfully.
  • Require approval from a named business owner for production, and from IT when a change touches security roles or the data model.
  • Add a business hours check to production if your support model needs releases to land when people are around.
  • Restrict the production service connection so only the release pipeline can use it.

What goes wrong

Most failures in Power Platform ALM are process failures rather than tooling failures. These are the ones to plan for.

  • Someone edits in production. Managed solutions make this harder but not impossible, because unmanaged changes can be layered on top. Remove maker access to production and check solution layers when something behaves unexpectedly.
  • The pipeline credential expires and every deployment fails. Track expiry dates and renew ahead of time.
  • A connection reference points at a person's connection, the person leaves and flows stop. Use the dedicated account and check connection ownership after every first import.
  • Environment variable values travel with the solution and production picks up development's site URL. Remove current values before export.
  • Two developers export at once and one overwrites the other's commit. Run exports through the pipeline, one at a time, and keep changes small.
  • A solution depends on a component that lives elsewhere or was never added. A build environment import catches this before test does, which is the main argument for having one.
  • An urgent fix is needed while a larger change is half built in development, and the export takes both. Hide unfinished work behind an environment variable flag, or keep a separate development environment, rebuilt from main, for fixes.
  • Test drifts from production because someone imported a one-off solution to try something. Rebuild or refresh test from production periodically so it reflects what is really live.

Ownership, and the alternatives to consider

Name an owner for each part. The Power Platform or centre of excellence team owns environments, service principals, service connections and pipeline templates. Developers own their solutions, exports and pull requests. A named business owner approves production releases for each app. Security reviews the pipeline identity's permissions and how its credential is handled. Write this into the repository readme so new developers find it on day one.

Azure DevOps is not the only route. Power Platform has its own in-product pipelines, which let makers deploy between environments without Azure DevOps, and native Git integration for solutions is developing. GitHub Actions for Power Platform covers similar ground to the Build Tools for teams that use GitHub. These options and their licensing requirements have changed and continue to change, so confirm the current position against Microsoft documentation.

As a rough guide, in-product pipelines suit estates of citizen developers who need a safe route to production with little setup. Azure DevOps suits teams that want source control, pull request review, approvals and audit in the same place as the rest of their development. We usually start with one app and one pipeline, prove the route end to end, and then make it the template for everything else.

Recognise the problem?

Take the free Cloud Modernisation Assessment: 18 questions, then your score and a personalised report on what to fix first.

Get my free Cloud Modernisation Assessment