Thursday, December 5, 2024
Deploy D365 FO Code to Power Platform Environment with Azure DevOps Pipelines
Posted by
As Dynamics 365 for Finance and Operations (D365 FO) evolves, so does its deployment model. Lifecycle Services (LCS) has been the cornerstone of D365 FO deployments for nearly eight years, but Microsoft is steering towards Power Platform environments. While LCS is still operational, the transition to Power Platform environments will eventually be mandatory, even though no official end date has been announced.
For now, projects can start managing certain environments within the Power Platform framework. This brings new challenges and opportunities, particularly in how deployments are handled. If your project already uses Power Platform environments, you’ll need a modern pipeline in Azure DevOps to manage installations.
This guide not only explains the differences between LCS and Power Platform deployments but also provides a detailed, reusable YAML pipeline for deploying to both environments, helping you streamline your transition.
Required Azure DevOps Extensions
To build and deploy your packages, you need the following Azure DevOps extensions:
- Dynamics 365 Finance and Operations Tools: This extension handles tasks such as building deployable packages and creating artifacts for D365 FO environments.
- Power Platform Build Tools: This extension is required for deployment tasks in Power Platform environments, including authentication, deployment, and artifact management.
Understanding the Change: LCS vs. Power Platform Deployments
Deployments for LCS and Power Platform environments differ significantly, especially in the tasks and workflow. Below, we compare the processes and highlight the changes required.
Build Automation Reference
If you're new to D365 FO Azure pipelines, it's highly recommended to familiarize yourself with the official documentation on build automation using Microsoft-hosted agents and Azure Pipelines.
Create Deployable Package
This task generates a deployable package for D365 FO. In LCS deployments, this package is a zipped all-in-one package, while Power Platform deployments require a different format.
Ensure you're using version 2 of the XppCreatePackage@2
task for
compatibility. Key parameters include:
CreateCloudPackage
: Generates a package for Power Platform environments.CreateRegularPackage
: Generates a package for LCS environments.
- task: XppCreatePackage@2
inputs:
XppToolsPath: >
$(Pipeline.Workspace)/NuGets/Microsoft.Dynamics.AX.Platform.CompilerPackage
XppBinariesPath: '$(Build.BinariesDirectory)'
XppBinariesSearch: '*'
CreateCloudPackage: true
CloudPackagePlatVersion: '7.0.0.0'
CloudPackageAppVersion: '10.0.0.0'
CloudPackageOutputLocation: >
$(Build.ArtifactStagingDirectory)/PowerPlatform
CreateRegularPackage: true
DeployablePackagePath: >
$(Build.ArtifactStagingDirectory)/LCS/AllInOne_$(Build.BuildNumber).zip
For a detailed explanation of the XppCreatePackage
task parameters and how to
configure them, refer to the official Microsoft documentation.
XppCreatePackage Output
Below is an example of how the outputs are structured after running the
XppCreatePackage
task:
LCS/
AllInOne_1.1.24340.14.zip
PowerPlatform/
PackageAssets/
[Content_Types].xml
customizations.xml
d365fodevblogmodule_1_0_0_1_managed.zip
en-us/
EndHtml/
WelcomeHtml/
ExampleSolution_1_0_0_1_managed.zip
ImportConfig.xml
manifest.ppkg.json
solution.xml
TemplatePackage.dll
LCS Upload Task
With Power Platform deployments, you no longer need to upload the package to the LCS Asset Library. This eliminates the need for the LCSAssetUpload task, simplifying the process.
- task: LCSAssetUpload@2
name: 'AssetUpload'
displayName: 'Upload Package to LCS'
inputs:
serviceConnectionName: 'YourLCSServiceConnectorName'
projectId: 'YourLCSProjectId'
assetType: '10'
assetPath: >
$(Pipeline.Workspace)/drop/LCS/AllInOne_$(Build.BuildNumber).zip
assetName: 'AllInOne_$(Build.BuildNumber)'
Deployment Tasks: LCS vs. Power Platform
Deployment involves significant changes. With LCS, you use the LCSAssetDeploy task, while Power Platform deployments require a series of Power Platform-specific tasks.
LCS Deployment
- task: LCSAssetDeploy@4
displayName: 'Deploy to LCS environment'
inputs:
serviceConnectionName: 'YourLCSServiceConnectorName'
projectId: 'YourLCSProjectId'
environmentId: 'YourLCSEnvironmentId'
fileAssetId: 'LCSAssetUploadOutput'
deploymentType: 'hq'
releaseName: '$(Build.BuildNumber)'
Power Platform Deployment
For Power Platform, you authenticate first and then deploy using task
PowerPlatformDeployPackage
.
- task: PowerPlatformToolInstaller@2
inputs:
DefaultVersion: true
- task: PowerPlatformWhoAmi@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: 'YourPowerPlatformConnector'
Environment: 'https://[YourDataverseEnvironmentName].crm4.dynamics.com/'
- task: PowerPlatformDeployPackage@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: 'YourPowerPlatformConnector'
Environment: 'https://[YourDataverseEnvironmentName].crm4.dynamics.com/'
PackageFile: >
$(Pipeline.Workspace)/drop/PowerPlatform/TemplatePackage.dll
MaxAsyncWaitTime: '180'
Key parameters:
PowerPlatformSPN
: Service connection for authentication.Environment
: URL of the target Power Platform environment.
Complete CI/CD Pipeline Example
Below is a full pipeline example demonstrating multiple stages for building and deploying code. It supports both LCS and Power Platform deployments.
YAML Pipeline for Deploying D365 FO to LCS and Power Platform Environments
trigger:
branches:
include:
- main
name: '1.1.$(Year:yy)$(DayOfYear).$(Rev:r)'
variables:
EnvironmentType: 'PowerPlatform' # PowerPlatform or LCS
AppSuitePackage: 'Microsoft.Dynamics.AX.ApplicationSuite.DevALM.BuildXpp'
App1Package: 'Microsoft.Dynamics.AX.Application1.DevALM.BuildXpp'
App2Package: 'Microsoft.Dynamics.AX.Application2.DevALM.BuildXpp'
PlatPackage: 'Microsoft.Dynamics.AX.Platform.DevALM.BuildXpp'
ToolsPackage: 'Microsoft.Dynamics.AX.Platform.CompilerPackage'
D365LCSEnv: 'YourAzureDevOpsEnvForLCS'
LCSServiceConnector: 'YourLCSServiceConnectorName'
LCSProjectId: 'YourLCSProjectId'
D365Env: 'YourAzureDevOpsEnvForD365FO'
PowerPlatformConnector: 'YourPowerPlatformConnector'
EnvironmentDataverseUrl: 'https://[YourDataverseEnvironmentName].crm4.dynamics.com/'
LCSEnvironmentId: 'YourLCSEnvironmentId'
stages:
- stage: Build
displayName: 'Build Dynamics 365 solution'
jobs:
- job: XppCompileJob
displayName: 'Compile X++'
pool:
name: 'Azure Pipelines'
vmImage: 'windows-latest'
demands:
- msbuild
- visualstudio
steps:
- checkout: self
- task: NuGetCommand@2
displayName: 'NuGet install packages'
inputs:
command: custom
arguments: 'install -Noninteractive "$(Build.SourcesDirectory)/Pipeline/Config/packages.config" -ConfigFile "$(Build.SourcesDirectory)/Pipeline/Config/nuget.config" -Verbosity Detailed -ExcludeVersion -OutputDirectory "$(Pipeline.Workspace)/NuGets"'
- task: XppUpdateModelVersion@0
inputs:
XppSourcePath: '$(Build.SourcesDirectory)/App/SourceCode'
XppDescriptorSearch: '**\Descriptor\*.xml'
XppLayer: '10'
VersionNumber: '$(Build.BuildNumber)'
- task: CopyFiles@2
displayName: 'Copy binary dependencies to build binaries directory: $(Build.BinariesDirectory)'
inputs:
SourceFolder: '$(Build.SourcesDirectory)/App/SourceCode'
Contents: '**/bin/**'
TargetFolder: '$(Build.BinariesDirectory)'
- task: VSBuild@1
displayName: 'Build solution'
inputs:
solution: '$(Build.SourcesDirectory)/Solution/BuildSolution/BuildSolution.sln'
vsVersion: 'latest'
msbuildArgs: >
/p:BuildTasksDirectory="$(Pipeline.Workspace)/NuGets/$(ToolsPackage)/DevAlm"
/p:MetadataDirectory="$(Build.SourcesDirectory)/App/SourceCode"
/p:FrameworkDirectory="$(Pipeline.Workspace)/NuGets/$(ToolsPackage)"
/p:ReferenceFolder="$(Pipeline.Workspace)/NuGets/$(PlatPackage)/ref/net40;$(Pipeline.Workspace)/NuGets/$(App1Package)\ref\net40;$(Pipeline.Workspace)/NuGets/$(App2Package)/ref/net40;$(Pipeline.Workspace)/NuGets/$(AppSuitePackage)/ref/net40;$(Build.SourcesDirectory)/App/SourceCode;$(Build.BinariesDirectory)"
/p:ReferencePath="$(Pipeline.Workspace)/NuGets/$(ToolsPackage)"
/p:OutputDirectory="$(Build.BinariesDirectory)"
- task: XppCreatePackage@2
displayName: 'Create Deployable Package'
inputs:
XppToolsPath: '$(Pipeline.Workspace)/NuGets/$(ToolsPackage)'
XppBinariesPath: '$(Build.BinariesDirectory)'
XppBinariesSearch: '*'
CreateCloudPackage: true
CloudPackagePlatVersion: '7.0.0.0'
CloudPackageAppVersion: '10.0.0.0'
CloudPackageOutputLocation: '$(Build.ArtifactStagingDirectory)/PowerPlatform'
CreateRegularPackage: true
DeployablePackagePath: '$(Build.ArtifactStagingDirectory)/LCS/AllInOne_$(Build.BuildNumber).zip'
- task: PublishBuildArtifacts@1
displayName: 'Publish artifact: drop'
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
condition: succeeded()
- stage: LCSUpload
displayName: 'Upload to Lifecycle Services (LCS)'
dependsOn:
- Build
condition: eq(variables['EnvironmentType'], 'LCS')
jobs:
- deployment: Upload
displayName: 'Asset upload'
pool:
name: 'Azure Pipelines'
vmImage: 'windows-latest'
environment: $(D365LCSEnv)
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: 'drop'
patterns: 'drop/LCS/AllInOne_$(Build.BuildNumber).zip'
- task: LCSAssetUpload@2
name: 'AssetUpload'
displayName: 'Upload Package to LCS'
inputs:
serviceConnectionName: $(LCSServiceConnector)
projectId: $(LCSProjectId)
assetType: '10'
assetPath: >
$(Pipeline.Workspace)/drop/LCS/AllInOne_$(Build.BuildNumber).zip
assetName: 'AllInOne_$(Build.BuildNumber)'
- stage: Deploy
displayName: 'Deploy to environment'
dependsOn:
- Build
- LCSUpload
condition: |
and(
succeeded('Build'),
or(
eq(variables['EnvironmentType'], 'PowerPlatform'),
succeeded('LCSUpload')
)
)
jobs:
- deployment: Deploy
displayName: 'Asset deployment'
timeoutInMinutes: 360
pool:
name: 'Azure Pipelines'
vmImage: 'windows-latest'
environment: '$(D365Env)'
strategy:
runOnce:
deploy:
steps:
- ${{ if eq(variables['EnvironmentType'], 'PowerPlatform') }}:
- download: current
artifact: 'drop'
patterns: 'drop/PowerPlatform'
- task: PowerPlatformToolInstaller@2
inputs:
DefaultVersion: true
- task: PowerPlatformWhoAmi@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: $(PowerPlatformConnector)
Environment: $(EnvironmentDataverseUrl)
- task: PowerPlatformDeployPackage@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: $(PowerPlatformConnector)
Environment: $(EnvironmentDataverseUrl)
PackageFile: >
$(Pipeline.Workspace)/drop/PowerPlatform/TemplatePackage.dll
MaxAsyncWaitTime: '180'
- ${{ else }}:
- task: LCSAssetDeploy@4
displayName: 'Deploy to LCS environment'
inputs:
serviceConnectionName: $(LCSServiceConnector)
projectId: $(LCSProjectId)
environmentId: $(LCSEnvironmentId)
fileAssetId: $[ stageDependencies.LCSUpload.Upload.outputs['Upload.AssetUpload.FileAssetId'] ]
deploymentType: 'hq'
releaseName: '$(Build.BuildNumber)'
Conclusion
The shift from LCS to Power Platform is more than just a technical change—it’s an opportunity to modernize your deployment processes. By adapting your Azure DevOps pipelines with the Power Platform Build Tools and updated tasks, you’ll be ready for the future of D365 FO deployments.
If you’d like to share your experience with Power Platform deployments or have any questions, feel free to connect with me on LinkedIn. I’d be happy to discuss and exchange ideas!