Deploying Windows Services using Azure DevOps

Deploying Windows Services using pipelines in Azure DevOps

In this new post, I explain how deploying Windows Services using pipelines in Azure DevOps. This helps you achieve a proper CD/CI for your Windows Service projects. Some context is provided by the Microsoft documentation.

Here the list of posts related to this one:

The source code of this post is available on GitHub.

Windows Services built on .NET Core and classic .NET Framework can be deployed using Azure DevOps to our target machine(s). They can automatically run on these machines. This process removes the need to copy files manually.

You will have the following ready:

  • An Azure DevOps account
  • Your working Windows Service code is committed in Azure DevOps Repositories
  • A target Windows machine to deploy to with an internet connection that you have access to. The target machine must have access to the URL dev.azure.com

So, I assume that the code for the Windows Service is in a repository and the pipeline will build the service in this repository and deploy it on the Windows Service machine.

Create a build pipeline

First, I created in my Azure DevOps a project called WindowsServiceTest and here I’m going to create the pipeline. Now, on the menu on the left select Pipelines and Create Pipeline.

Create your first pipeline in Azure DevOps - Deploying Windows Services using Azure DevOps
Create your first pipeline in Azure DevOps

After clicking on the create button, I can select where the code is. As I said above, the code is in Azure DevOps. So, I choose Azure Repos Git. In the following screenshot an example where to click.

Select Where is your code? - Deploying Windows Services using Azure DevOps
Select Where is your code?

After that, I have to select the repository. In my case, this is straightforward because I don’t have any other repository apart of the main one.

Select a repository
Select a repository

Now, Azure DevOps asks if I want to start with a new pipeline (Starter pipeline) or using an existing one.

Configure your pipeline
Configure your pipeline

Click on the button Show more to display a list of other pipelines. I can see this screen now.

Configure your pipeline: .NET Desktop
Configure your pipeline: .NET Desktop

Now, from the list, select .NET Desktop. This boilerplate offers us some useful settings. This will be relevant for both .NET Core and .NET Framework. This is the YAML out-of-the-box.

# .NET Desktop
# Build and run tests for .NET Desktop or Windows classic desktop solutions.
# Add steps that publish symbols, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net

trigger:
- main

pool:
  vmImage: 'windows-latest'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'

steps:
- task: NuGetToolInstaller@1

- task: NuGetCommand@2
  inputs:
    restoreSolution: '$(solution)'

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: VSTest@2
  inputs:
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'
Review your pipeline YAML - Deploying Windows Services using Azure DevOps
Review your pipeline YAML

Customize the pipeline

Before showing the pipeline, a few consideration. I have some NuGet packages stored in the Azure DevOps Artifacts. I want to restore those packages from my artifacts feed. Also, my priority is to run tests and show the Tests result and the Code coverage.

General settings

First, the general settings in the YAML. The trigger is from the main branch. The image I want to use is the windows-latest. As a variables, I set the build as a Release.

trigger:
- main

pool:
  vmImage: windows-latest

variables:
  buildConfiguration: 'Release'

After that, I will add the steps. Now, I explain step by step the tasks I want to add.

Install .NET

Now, I built the Windows Service with the version 8 of NET Core. So, the first task is to install it.

- task: UseDotNet@2
  displayName: 'Use dotnet 8'
  inputs:
    version: '8.0.x'

Use NuGet

As I said before, some of the packages are in the artifacts, I added a Nuget.config in the root of the project. The tasks I’m adding are related to install and use the NuGet tool in the pipeline. After that, I list the NuGet sources and then restore the packages for the solution.

- task: NuGetToolInstaller@1

- task: NuGetAuthenticate@1
  displayName: 'Authenticate to Azure Artifacts feed'

- script: dotnet nuget list source
  displayName: 'List NuGet sources'

- task: NuGetCommand@2
  displayName: 'NuGet Restore with custom config'
  inputs:
    restoreSolution: '**/*.sln'
    feedsToUse: config
    nugetConfigPath: '$(Build.SourcesDirectory)/NuGet.config'

This is an example of the NuGet.config I use for the project. This file must be in the root of the project.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="<feedName>" value="https://pkgs.dev.azure.com/<yourfeed>/nuget/v3/index.json" />
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
  </packageSources>
</configuration>

Build the project

Now, the next step is to build the projects. The output of the build will be save/copy in the directory ci-build in the StagingDirectory of Azure DevOps. I decided to save the build there. This provides a clear place where I can find all the files. Then, compress them for the next deployment.

- task: DotNetCoreCLI@2
  displayName: Build project
  inputs:
    command: 'build'
    projects: '**/*.csproj'
    arguments: '--configuration $(buildConfiguration) -o $(Build.StagingDirectory)/ci-build'

Run the tests

All my projects have tests. So, before continuing with the deployment, I want to be sure that all the tests passed. After that, I want to publish the collect the results of the tests and the code coverage.

- task: DotNetCoreCLI@2
  displayName: 'Run tests'
  inputs:
    command: 'test'
    projects: '**/*[Te]ests/*.csproj'
    arguments: '--configuration $(buildConfiguration) --collect "Code Coverage" --collect "XPlat Code Coverage"'

Publish the code coverage

Once, the tests are completed and passed and the code coverage determined, I will publish the result.

- task: PublishCodeCoverageResults@2
  displayName: 'Publish code coverage'
  inputs:
    summaryFileLocation: '$(Agent.BuildDirectory)/**/coverage.cobertura.xml'
    pathToSources: '$(Agent.BuildDirectory)/**/coverage'

Therefore, open an executed pipeline, I can see there are some new tabs: Tests and Code Coverage.

Show the test results and code coverage in the pipeline - Deploying Windows Services using Azure DevOps
Show the test results and code coverage in the pipeline

Under the tab Tests, I see all the tests and if they passed or not.

Azure DevOps: test result in the pipeline - Deploying Windows Services using Azure DevOps
Azure DevOps: test result in the pipeline

The Code Coverage tab shown how much code is covered by tests. Generally speaking, the code coverage should be around 77% to get a good coverage of the projects.

Azure DevOps: code coverage in the pipeline - Deploying Windows Services using Azure DevOps
Azure DevOps: code coverage in the pipeline

Zip the files

Now that the build is done, I want to zip the files in the ci-build folder. The resulted zip file will be publish in the artifacts to be deploy later in the machine.

- task: ArchiveFiles@2
  inputs:
    rootFolderOrFile: '$(Build.StagingDirectory)/ci-build'
    includeRootFolder: false
    archiveType: 'zip'
    archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'
    replaceExistingArchive: true

So, the configuration I use create a zip file and the name is the BuildId (a number). I don’t want to include the root of the folder but only the content of the folder. The file must be a zip file format. If the zip file exists, it can be replaced.

Publish artifacts

Finally, I want to publish the zip file in the artifacts. With that, the release will be use this artifact to deploy the Windows Service into the Windows Server machine or any other target machines. This will allow us for deploying Windows Services using Azure DevOps.

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)'
    ArtifactName: 'drop'
    publishLocation: 'Container'

Full YAML

trigger:
- main

pool:
  vmImage: windows-latest

variables:
  buildConfiguration: 'Release'

steps:
- task: UseDotNet@2
  displayName: 'Use dotnet 8'
  inputs:
    version: '8.0.x'

- task: NuGetToolInstaller@1

- task: NuGetAuthenticate@1
  displayName: 'Authenticate to Azure Artifacts feed'

- script: dotnet nuget list source
  displayName: 'List NuGet sources'

- task: NuGetCommand@2
  displayName: 'NuGet Restore with custom config'
  inputs:
    restoreSolution: '**/*.sln'
    feedsToUse: config
    nugetConfigPath: '$(Build.SourcesDirectory)/NuGet.config'

- task: DotNetCoreCLI@2
  displayName: Build project
  inputs:
    command: 'build'
    projects: '**/*.csproj'
    arguments: '--configuration $(buildConfiguration) -o $(Build.StagingDirectory)/ci-build'

- task: DotNetCoreCLI@2
  displayName: 'Run tests'
  inputs:
    command: 'test'
    projects: '**/*[Te]ests/*.csproj'
    arguments: '--configuration $(buildConfiguration) --collect "Code Coverage" --collect "XPlat Code Coverage"'

- task: PublishCodeCoverageResults@2
  displayName: 'Publish code coverage'
  inputs:
    summaryFileLocation: '$(Agent.BuildDirectory)/**/coverage.cobertura.xml'
    pathToSources: '$(Agent.BuildDirectory)/**/coverage'

- task: ArchiveFiles@2
  inputs:
    rootFolderOrFile: '$(Build.StagingDirectory)/ci-build'
    includeRootFolder: false
    archiveType: 'zip'
    archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'
    replaceExistingArchive: true

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)'
    ArtifactName: 'drop'
    publishLocation: 'Container'

Set the Deployment group

Now, to deploy the services on the Windows Servers or any other machine, I need to specify our deployment target. Go to Deployment Groups and then New or Add a deployment group.

Deployment group in Azure DevOps - Deploying Windows Services using Azure DevOps
Deployment group in Azure DevOps

Give your Deployment group a Name and description and click Create:

New deployment group on Azure DevOps - Deploying Windows Services using Azure DevOps
New deployment group on Azure DevOps

On the next screen, Azure DevOps gives me the PowerShell script I have to execute on the target machines. This will associate the machine with this group and I can use them later in the ReleasesClick on Use a Personal access token, then Click Copy to clipboard:

Deployment groups script to run - Deploying Windows Services using Azure DevOps
Deployment groups script to run

Now go to your target machine, Open an Administrator-privileged Powershell command prompt, paste your script and then execute. This will take a while, maybe 2 to 5 minutes. You should see something like the following if all is well, indicating that the Azure Pipelines agent is installed on the machine correctly:

Script in execution - Deploying Windows Services using Azure DevOps
Script in execution

During the configuration, there are some questions. I use the default values.

  • Enter deployment group tags for agent? (Y/N) (press enter for N): I pressed Enter
  • Enter enable SERVICE_SID_TYPE_UNRESTRICTED for agent service (Y/N) (press enter for N): I pressed Y
  • Enter User account to use for the service (press enter for NT AUTHORITY/SYSTEM): I pressed Enter
  • Enter whether to prevent service starting immediately after configuration is finished? (Y/N) (press enter for N): I pressed Enter

So, now the configuration is completed. This is a example of what I can see after that.

Setup completed - Deploying Windows Services using Azure DevOps
Setup completed

Now, in the Deployment groups in Azure DevOps, I see the Target machine group. It has 1 machine. The machine is online.

Deployment groups in Azure DevOps
Deployment groups in Azure DevOps

Wrap up

In this first post about deploying Windows Services using Azure DevOps, I showed how to create the pipeline. This pipeline builds the solution of a Windows Server. Additionally, I demonstrated how to publish the artifact. In the next post, I will show how to create the release to deploy and run the Windows Server.

Happy coding!

Related posts

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.