Building YAML CI/CD Pipelines in Azure DevOps [Part 1]
Recently, Azureish Live! returned - our new project is to build a zero touch CI/CD pipeline in Azure DevOps for the project we build last year for the show.
You can watch the whole first part of the show above. Other than a minor technical hiccup, I think we did pretty well!
In this post, we’re going to take a quick look at what we’ve got to do and what we achieved in our first part.
What we created as part of our GitWatcher project was a complete serverless solution using Azure Functions, Azure SignalR and Azure Storage for hosting a static website build with Blazor WebAssembly.
The application currently looks like the below:
The Blazor static site is ultimately something that needs to be compiled, as is the API we have. We chose to start with the API as the easiest part to build. Most importantly, for now, we have chosen only to build it and not deploy just yet to keep things simple.
The API is a simple Function App with a couple of methods to be called by the front-end. You can see the code we wrote for that over at the GitHub repo.
It is also important to note that for this project we have a single repository that contains all components. Our approach also means that we will be having one YAML pipeline per component, all in the same repository.
A couple of constraints we set ourselves:
- We want a build to run for every commit.
- We won’t be running tests because we haven’t written any (yet).
Getting setup with a stage for build
along with the relevant triggers for our GitHub Flow branching strategy is relatively straightforward. We need to define our triggers, agent pool and of course, the first stage.
trigger:
- main
- feature/*
pool:
vmImage: 'ubuntu-latest'
stages:
- stage: build
jobs:
- job: build
steps:
The triggers set here means that a build will run any time a commit is pushed (or merged into) main
along with any branches that appear under feature/*
e.g. feature/adding-pipelines
.
Our first task is to ensure we have the right SDK for dotnet core available to us - in our case, this is 3.1.x LTS as we are yet to move GitWatcher to .NET 5 (but this will happen in a future show).
To do this, we need to use the UseDotNet
task. This task allows us to explicitly define the SDK (or runtime) version of dotnet we wish to use. We now have to include this step as the default version available on the Microsoft-hosted agents is now .NET 5.
It doesn’t have to be exact, it can be a latest minor version which is what we do here by using the following configuration:
- task: UseDotNet@2
inputs:
packageType: 'sdk'
version: '3.1.x'
displayName: 'Set SDK to 3.1.x'
Now that we have that, the next thing we needed to do was to actually build the project!
As I mentioned, we haven’t written tests for this, even though we probably should!
- task: DotNetCoreCLI@2
inputs:
command: 'build'
projects: '$(Build.SourcesDirectory)/backend/backend.csproj'
displayName: 'Build backend service'
What we’re doing here is being specific about the project we want to build. As I mentioned earlier, what we have is a single repo and we don’t want to build the entire solution all at once.
Our next step is to create a package to publish:
- task: DotNetCoreCLI@2
inputs:
command: 'publish'
publishWebProjects: false
projects: '$(Build.SourcesDirectory)/backend/backend.csproj'
arguments: '-o $(Build.ArtifactStagingDirectory)'
displayName: 'dotnet publish'
We’ve set the output of the publish action to put in the ArtifactStagingDirectory
. This is where we’ll publish our artifact from to our build for later use.
Ultimately, all this command is running is the dotnet publish
command.
Then our final step is to publish the build artifact:
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
publishLocation: 'Container'
But what if we don’t want to create a build artifact every time? In my opinion, we shouldn’t need to. At least not when we’re building from feature branches on every commit for validation. We also don’t need to take up the storage for it every time we do a build and it’s not form main
(we can though, nothing stopping you doing that, it’s just not my preferred practice).
On all the steps we don’t want to run depending on the source branch, we added a condition
. For this particular build definition, we added the condition
to both the dotnet publish
and PublishBuildArtifact
tasks.
The condition itself, looks a little like this:
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
This is saying that providing the previous step succeeded, and the branch that triggered the build is main
then we’ll run this step.
The complete step with the condition looks like this:
- task: DotNetCoreCLI@2
inputs:
command: 'publish'
publishWebProjects: false
projects: '$(Build.SourcesDirectory)/backend/backend.csproj'
arguments: '-o $(Build.ArtifactStagingDirectory)'
displayName: 'dotnet publish'
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
Putting the whole thing together gives us a YAML pipeline that at this stage allows us to do continuous integration builds on every commit.
trigger:
- main
- feature/*
pool:
vmImage: 'ubuntu-latest'
stages:
- stage: build
jobs:
- job: build
steps:
- task: UseDotNet@2
inputs:
packageType: 'sdk'
version: '3.1.x'
displayName: 'Set SDK to 3.1.x'
- task: DotNetCoreCLI@2
inputs:
command: 'build'
projects: '$(Build.SourcesDirectory)/backend/backend.csproj'
displayName: 'Build backend service'
- task: DotNetCoreCLI@2
inputs:
command: 'publish'
publishWebProjects: false
projects: '$(Build.SourcesDirectory)/backend/backend.csproj'
arguments: '-o $(Build.ArtifactStagingDirectory)'
displayName: 'dotnet publish'
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
- task: PublishBuildArtifacts@1
inputs:
PathToPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
publishLocation: 'Container'
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
You’ll perhaps notice though that currently, there’s one glaring omission that means that this build will run when any commit is pushed back to the repo, regardless of project.
On the next show, we’ll be demonstrating how to solve that and moving on to infrastructure as code!
Leave a comment