Architecture, Automation, Azure, Azure DevOps, Professional, YAML Pipelines

Multi-Stage YAML Pipelines in a Microservice Architecture. Making Life Easier

MultiStage Pipelines can make deployments much much easier. Be sure to check out my previous post an overview of YAML Pipelines in Azure DevOps

A common question that tends to arise when dealing with these pipelines is how to embrace the concept when implementing microservices. For those that are not aware microservice architecture is an architecture structure where there may be multiple projects or endpoints that are catered to a small (micro) function. This is opposed to how applications used to be very monolithic. There are quite a few positives to architecting solutions this way. The least of which is the ability to deploy/update small sections of code, be able to better scope changes, etc.. Here is some more information on microservice design patterns.

How this design patterns translates into a deployment strategy is that each endpoint/project should follow a very close set of steps for it’s deployment and maintenance. The basic YAML Pipeline will say that each service will have it’s own pipeline…..That’s is great; however, what if we want to maintain the ability to easily redeploy all services. This would happen say when doing major software version upgrades, standing up a new environment, an environment refresh, etc… Well can’t we have both? The ability to deploy a single service will also deploying an entire platform? Enter what I am referring to as a “Viewer-Controler” YAML deployment strategy.

In an ordinary approach to a YAML Multi-Stage deployment pipeline the pipeline would look like:

resources:
  repositories:
  - repository: repositoryTemplate
    type: git
    name: JF Demo/YAMLTemplates
trigger:
  branches:
    include:
    - master
pool:
  vmImage: windows-latest
variables:
- name: regionNameEUS
  value: East US
- name: regionabrvEUS
  value: eus
- name: regionNameWUS
  value: West US
- name: regionabrvWUS
  value: wus
- name: environmentNameDEV
  value: dev
- name: environmentNameTST
  value: tst
- group: DemoApp
- name: solutionPath
  value: JohnOSTApp
- name: testAppProjectName
  value: TestApp
- name: testAppName
  value: johnostdemoapp
- name: secondAppName
  value: johnostdemoapp2
- name: secondAppProjectName
  value: SecondApp
stages:
- stage: Build_TestApp
  jobs:
  - job: Build_TestApp
    dependsOn: []
    steps:
    - task: DotNetCoreCLI@2
      displayName: dotnet restore
      inputs:
        command: restore
        projects: $(Build.SourcesDirectory)/$(solutionPath)/TestApp**/*.csproj
    - task: DotNetCoreCLI@2
      displayName: dotnet build
      inputs:
        projects: $(Build.SourcesDirectory)/$(solutionPath)/TestApp**/*.csproj
        arguments: --configuration Release
    - task: DotNetCoreCLI@2
      displayName: dotnet test
      inputs:
        command: test
        projects: $(Build.SourcesDirectory)/$(solutionPath)/TestApp.Tests/*.csproj
        arguments: '--configuration Release --collect "Code coverage" '
  - job: Publish_TestApp
    dependsOn:
    - Build_TestApp
    condition: and(succeeded(),eq( true, 'true'))
    steps:
    - task: DotNetCoreCLI@2
      displayName: dotnet publish
      inputs:
        command: publish
        publishWebProjects: false
        projects: $(Build.SourcesDirectory)/$(solutionPath)/TestApp**/*.csproj
        arguments: --configuration Release --output $(build.artifactstagingdirectory)
        zipAfterPublish: True
    - task: PublishBuildArtifacts@1
      displayName: 'Publish Artifact: drop'
........

This same block would be unique for each pipeline. This would work in successfully deploying at the pipeline level. Sure we can also do a better job of leveraging templates in our process; however, still how would we go about deploying the entire platform? This can still be a puzzle.

Introduce the Viewer-Controller deployment design. These terms should sound familiar if you have background in an Model-Viewer-Controller (MVC) design pattern in application development. Essentially same concept with Viewer-Controller. Our Viewer here will contain the necessary components to define our pipeline and we leverage the Controller to do the heavy lifting and orchestration of calling subsequent templates. Thus when we need to deploy an entire platform we have a Viewer of calling all the Controllers. Everything else is built for us and we can guarantee with 100% certainty that each component is being deployed the exact same way in an individual service deployment vs an entire environment deployment.

So what does this look like for your YAML files?

The Viewer YAML for an individual pipeline may look like:

trigger:
    branches:
     include:
       - YAML_Pipeline
   
pool:
    vmImage: 'ubuntu-latest'

resources:
  repositories:
  - repository: YAMLTemplates
    type: git
    name: Test Repo/YAML_Templates
   
name: $(TeamProject)_$(Build.DefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r)
stages:
- template: webapp1-template.yml

Our webapp1-template would look like:

parameters:
- name: serviceName
  type: string
  default: WebApp1

stages:
- template: stages/build.yml@YAMLTemplates
  parameters:
    environmentName: Build
    serviceName: ${{ parameters.serviceName}}
- template: stages/env_deploy.yml@YAMLTemplates
  parameters:
    environmentName: Dev
    serviceName: ${{ parameters.serviceName}}

Can see our template is going to do all the heavy lifting in this scenario and call a set of shared YAML tasks in a shared repository. This is key as our microservices may lie in different repos. As such our reusable templates should be in their own repo as well. The only difference will be what we pass into the build/deployment process. i.e. artifacts, publish settings, etc..

So this is great what if we want to deploy all the micro services. We’d set up a Viewer similar to the one outlined about; however our Controller will look like:

trigger:
    branches:
     include:
       - YAML_Pipeline
   
pool:
    vmImage: 'ubuntu-latest'

resources:
  repositories:
  - repository: YAMLTemplates
    type: git
    name: Test Repo/YAML_Templates
   
name: $(TeamProject)_$(Build.DefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r)
stages:
- template: webapp1-template.yml
- template: webapp2-template.yml

See just one line of code! That’s it! Now to get to this level if you noticed we made some default values in our Controller. Which is key for saving us some space; however, we can also override certain things. Let’s say we have a parameter to denote the region we are going to deploy to. Set that up as a defaulted parameter in our Controller and if we want to do something multi region just call the Controller and override the parameter.

Next in the series I’ll show some ways to organize and outline YAMLTemplates that we may look at using across multiple repositories.