Azure pipelines are used to automatically build, test and deliver your code to any destination. The process of automatically building and testing your code is called Continuous Integration (CI), which helps to catch bugs early in the development cycle. When the pipeline also delivers code to a destination after manual approval by a user, it is called Continuous Delivery (CD). And then there is a similar approach called Continuous Deployment. When you use this approach, the delivery of your code is automated, rather than requiring manual approval for this last step. In this article I will show you how to automate the build and deploy process of a Web API to IIS using a multi-stage pipeline.
You will start with a basic YAML pipeline that builds and tests an ASP.NET Web API. After that, I will show you how to transform it into a real multi-stage pipeline that builds, tests and deploys to Internet Information Services (IIS) on a Virtual Machine (VM). The image below shows the end result of this multi-stage pipeline.
Table of Contents
Create basic build pipeline for ASP.NET Core
Let’s start by creating a basic YAML pipeline that builds an ASP.NET Core Web API. Open a web browser and navigate to your Azure DevOps project. From there, navigate to Pipelines and click on the ‘New pipeline’ button. Then, select the repository containing the ASP.NET source code and finish the pipeline configuration by selecting the ASP.NET Core YAML template as shown in the image below.
As a result, a new YAML file is generated that looks like the example shown below. The trigger defines what event will start the pipeline, which in this case happens when new code is pushed to the main branch. When the pipeline starts, you get a fresh virtual machine hosted by Microsoft on which an agent is running. This agent will execute the tasks in the pipeline.
There are three variables defined. The first variable specifies that the pipeline should build all solutions it can find in all sub folders. Then, the second variable specifies that the solutions are to be built for any CPU. And, the third variable specifies the type of build configuration. These variables are used as inputs for the various tasks.
Under steps, you find all the tasks that will run in sequence. A task is simply a packaged script or procedure that has been abstracted with a set of inputs. The first task will install the NuGet tool which is used in the second task to restore NuGet packages that the solutions contain. Then, the third task will build all solutions it can find. Finally, the last task will search for test projects and run all unit tests.
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)' msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\WebApp.zip" /p:DeployIisAppPath="Default Web Site"' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' - task: VSTest@2 inputs: platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' testSelector: 'testAssemblies' testAssemblyVer2: | **\*Test.dll **\*Tests.dll !**\*TestAdapter.dll !**\obj\**
Transform basic build pipeline into multi-stage pipeline
In the example below, the basic YAML pipeline discussed in the previous chapter is transformed into a multi-stage pipeline. For now, it only contains a build stage that executes both the build and test tasks. As you might have noticed, the tasks are placed under a job. Each pipeline contains at least one job. A job is a series of steps that run sequentially as a unit of work. When there is only one job, you are not required to define it, which is why it was left out in the previous chapter. The last task is new and publishes the build artifacts to a staging directory. When you define your deploy stage later, the first step will be to download these build artifacts.
trigger: - main stages: - stage: Build displayName: 'Build Web API' jobs: - job: 'Build' pool: vmImage: 'windows-latest' variables: solution: '**/*.sln' buildPlatform: 'Any CPU' buildConfiguration: 'Release' steps: - task: NuGetToolInstaller@1 displayName: 'Install NuGet Tool' - task: NuGetCommand@2 displayName: 'Restore Packages' inputs: restoreSolution: '$(solution)' - task: VSBuild@1 displayName: 'Start Build' inputs: solution: '$(solution)' msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\WebApp.zip" /p:DeployIisAppPath="Default Web Site"' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' - task: VSTest@2 displayName: 'Run Unit Tests' inputs: platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' testSelector: 'testAssemblies' testAssemblyVer2: | **\*Test.dll **\*Tests.dll !**\*TestAdapter.dll !**\obj\** - task: PublishBuildArtifacts@1 displayName: 'Publish Build' inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)' ArtifactName: 'drop' publishLocation: 'Container'
Create VM environment and install Windows agent
In order to deploy the published artifacts to IIS on your virtual machine, an environment needs to exist in Azure DevOps. The environment will be used in the deploy stage of your multi-stage pipeline. After creating the environment in Azure DevOps, you will need to copy the generated PowerShell script and execute it on the virtual machine. This will install an agent that will be used by the YAML pipeline to connect with the virtual machine and deploy the artifacts to IIS.
When you run the PowerShell script to install the agent, you will be asked to enter resource tags. Even though they are optional, they can be very handy. Resource tags are used to target specific virtual machines in an environment for deployment. For example, you could work with tags like: ‘windows’ and ‘production’. Then, if you use both these tags in your YAML pipeline, the deployment will only take place on the virtual machines tagged with ‘windows’ and ‘production’.
Because the agent will run as a Windows service, you are also asked to enter an account name under which the service must run. You might be able to use the SYSTEM account, but it is recommended to use a specific service account.
Extend multi-stage pipeline with a deploy stage
Let’s extend the multi-stage pipeline by adding a deploy stage. As can be seen in the example below, the deploy stage starting on line 53 depends on the build stage. Instead of using a normal job, it starts a special type of job called a deployment job. When the build stage completes successfully, the deployment job will download the build artifacts from the staging directory named ‘drop’. Then, the next task will create the web application and the corresponding application pool in IIS if it does not exist yet. To make sure nothing is held in memory, the task that follows stops the application pool. After that, the downloaded build artifacts will be deployed to IIS and the application pool will be started. As a result, the new version of the Web API is successfully deployed.
trigger: - main stages: - stage: Build displayName: 'Build Web API' jobs: - job: 'Build' pool: vmImage: 'windows-latest' variables: solution: '**/*.sln' buildPlatform: 'Any CPU' buildConfiguration: 'Release' steps: - task: NuGetToolInstaller@1 displayName: 'Install NuGet Tool' - task: NuGetCommand@2 displayName: 'Restore Packages' inputs: restoreSolution: '$(solution)' - task: VSBuild@1 displayName: 'Start Build' inputs: solution: '$(solution)' msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\WebApp.zip" /p:DeployIisAppPath="Default Web Site"' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' - task: VSTest@2 displayName: 'Run Unit Tests' inputs: platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' testSelector: 'testAssemblies' testAssemblyVer2: | **\*Test.dll **\*Tests.dll !**\*TestAdapter.dll !**\obj\** - task: PublishBuildArtifacts@1 displayName: 'Publish Build' inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)' ArtifactName: 'drop' publishLocation: 'Container' - stage: Deploy displayName: 'Deploy Web API' dependsOn: Build jobs: - deployment: 'DeployToTest' displayName: 'Deploy Web API to test environment' environment: name: API-Test resourceType: VirtualMachine variables: appName: 'ExampleWebAPI' physicalPathForApp: 'C:\APIs\$(appName)' strategy: runOnce: deploy: steps: - download: current displayName: 'Download Build' artifact: drop - task: IISWebAppManagementOnMachineGroup@0 displayName: 'IIS Create Web App If Not Exist' inputs: IISDeploymentType: 'IISWebApplication' ParentWebsiteNameForApplication: 'Default Web Site' PhysicalPathForApplication: $(physicalPathForApp) VirtualPathForApplication: '/$(appName)' AppPoolNameForApplication: $(appName) DotNetVersionForApplication: 'No Managed Code' CreateOrUpdateAppPoolForApplication: true - task: IISWebAppManagementOnMachineGroup@0 displayName: 'IIS Stop App Pool' inputs: IISDeploymentType: 'IISApplicationPool' ActionIISApplicationPool: 'StopAppPool' StartStopRecycleAppPoolName: $(appName) - task: IISWebAppDeploymentOnMachineGroup@0 displayName: 'IIS Deploy Web App' inputs: WebSiteName: 'Default Web Site' VirtualApplication: $(appName) Package: '$(Pipeline.Workspace)\drop\*.zip' - task: IISWebAppManagementOnMachineGroup@0 displayName: 'IIS Start App Pool' inputs: IISDeploymentType: 'IISApplicationPool' ActionIISApplicationPool: 'StartAppPool' StartStopRecycleAppPoolName: $(appName)
Configure multi-stage pipeline for Continuous Delivery or Continuous Deployment
As described earlier, the Continuous Delivery process requires a manual trigger for deployment, which can be as simple as pressing a button. But with Continuous Deployment, you automate the entire process from code commit to deploying to production. As developers, we would like to automate everything, so the last option appeals the most. However, you should also ask yourself if it fits in the organisation your are working for. As you look for the answer, use these questions to guide you:
- Can you deploy without approval from stakeholders?
- Do your system and gating requirements allow for end-to-end automation?
- Can you expose your customers to production changes a little at a time?
- Does your organization respond to errors in production quickly?
If you need a manual trigger, you can start with Continuous Integration and Continuous Delivery (CI/CD). You will automate the creation of production-ready code that’s always just one manual approval from deployment. Over time, you can work toward Continuous Deployment and full automation of your software delivery process. As shown in the eample below, you can easily add approval and checks to the environment in Azure DevOps.
After the pipeline has successfully gone through the build stage, the approver receives an e-mail including a link that refers to the review page, which is shown in the image below. When the user approves, the pipeline will deploy the Web API to this environment.
Conclusion
In this article I have shown you how to create a basic pipeline and transform it into a multi-stage pipeline that builds, tests and deploys an ASP.NET Web API to IIS running on a VM. When you choose to require manual approval for deployment, you are using the Continuous Delivery process. But, if you choose to automatically deploy without requiring manual approval, you are using the Continuous Deployment process. Whichever you choose, you will gain the following advantages:
- Changes are automatically built, validated, and tested.
- Code is always deployable.
- Releases receive faster customer feedback.
- Developers are more productive with fewer manual and administrative tasks.