Server maintenance with fan-out pattern in Azure DevOps YAML

Today, I am going to show you an implementation of well-known fan-out pattern in Azure DevOps YAML (AZDO) CI/CD pipeline using Azure DevOps REST API. I will present a simple way to get an one or a few pipelines be executed from one central place called management plain in fan-out mode.

What is fan-out and how it might be useful in CI/CD workflow?

Fanout pattern in serverless architectures means spreading of a message to one or multiple destinations possibly in parallel, and not halting the process that executes the messaging to wait for any response to that message. We will use the idea to delegate execution of specific CI/CD or configuration management to separate processes in AZDO and (or) doing it in parallel.

Below is a schematic view of fan-out preset runner to maintain blue/green deployment process as a management element of release flow process.

fan-out pattern in Azure DevOps YAML

So, the solution consist of the next few parts:

  • The core element is server maintenance login. The IaC run book, which contains management steps. The runbook is responsible for managing IIS pool using appcmd.exe configuration tool. It should be noticed, the solution might be successfully applied to any kind of server OS using PowerShell, Python or whatever configuration language you prefer.
  • Management plain is AZDO pipeline which manage what preset should be executed and on what environment, blue or green production server’s pool.
  • Preset runner pipelines. Go-live, Go-offline and Recycle in my case. The set of pipelines which are responsible to maintenance logic execution with predefined set of configuration. It allows us: to avoid manual configuration each time we provide a maintenance process; to save a time to delivery; reduce a risk of misconfiguration and human factor.
  • Blue/green server pools are active and passive production legs simultaneously.
  • Load balancer is responsible for swaping

Now, let’s go deeper and see how the logic works.

Maintenance logic

As I noticed before, the solution may base on any automation tool you like, any programing language and might be adopted to manage Windows or Linux servers. I am using PowerShell runbooks and appcmd.exe tool to do so. Production environment provisioned and hosted on premises, using NetScaler load balancing solution.

The management plain looks like:

name: "IIS Maintenance-$(Date:yyMMdd).$(Rev:rr)"

trigger: none

pool:
  name: company-dotNET

parameters:
- name: Environment
  values:
  - BLUE
  - GREEN
  default: BLUE

- name: StartApp
  displayName: StartApi (AppPoolStart + OnDemand + Preload=True + WebsiteStart)
  type: boolean
  values:
  - true
  - false
  default: false
- name: StopApp
  displayName: Stop API (AppPoolStop + OnDemand + Preload=True + WebsiteStart)
  type: boolean
  values:
  - true
  - false
  default: false
- name: RecycleApp
  displayName: Recycle API (AppPoolRecycle + OnDemand + Preload=True + WebsiteStart)
  type: boolean
  values:
  - true
  - false
  default: false

variables:
- group: company-project Project Configuration

stages:
- stage: StartApp
  condition: ${{ parameters.StartApp }}
  jobs:
  - job: StartApp
    steps:
    - task: PowerShell@2
      displayName: Start App (AppPoolStart + AlwaysRunning + Preload=True + WebsiteStart)
      inputs:
        targetType: filePath
        filePath: ./Tools/AzdoRestApi/pipelines/run-pipeline.ps1
        arguments: > 
          -AzureDevOpsPAT "$(B64-TOKEN)" 
          -OrganizationName "$(organizationName)" 
          -ProjectName "$(teamProjectName)" 
          -PipelineId 101 #ID of preset runner pipeline in AZDO
          -BranchName "main"
          -TemplateParameters '{ "Environment": "${{ parameters.Environment }}", "AppPoolAction": "start", "AppPoolStartMode": "AlwaysRunning", "Preload": "True", "WebSiteAction": "start"  }'
        showWarnings: true

- stage: StopApp
  condition: ${{ parameters.StopApp }}
  jobs:
  - job: StopApp
    steps:
    - task: PowerShell@2
      displayName: Stop App (AppPoolStop + OnDemand  + Preload=False + WebsiteStop)
      inputs:
        targetType: filePath
        filePath: ./Tools/AzdoRestApi/pipelines/run-pipeline.ps1
        arguments: > 
          -AzureDevOpsPAT "$(B64-TOKEN)" 
          -OrganizationName "$(organizationName)" 
          -ProjectName "$(teamProjectName)" 
          -PipelineId 101 #ID of preset runner pipeline in AZDO
          -BranchName "main"
          -TemplateParameters '{ "Environment": "${{ parameters.Environment }}", "AppPoolAction": "stop", "AppPoolStartMode": "OnDemand", "Preload": "False", "WebSiteAction": "stop" }'
        showWarnings: true

- stage: RecycleApi
  condition: ${{ parameters.RecycleApp }}
  jobs:
  - job: RecycleApp
    steps:
    - task: PowerShell@2
      displayName: Recycle App (AppPoolRecycle , WebsiteRecycle, StartModeNoOps)
      inputs:
        targetType: filePath
        filePath: ./Tools/AzdoRestApi/pipelines/run-pipeline.ps1
        arguments: > 
          -AzureDevOpsPAT "$(B64-TOKEN)" 
          -OrganizationName "$(organizationName)" 
          -ProjectName "$(teamProjectName)" 
          -PipelineId 101 #ID of preset runner pipeline in AZDO 
          -BranchName "main"
          -TemplateParameters '{ "Environment": "${{ parameters.Environment }}", "AppPoolAction": "recycle", "Preload": "True", "WebSiteAction": "recycle" }'
        showWarnings: true

Management plain contains three configuration presents:

  • one to bring active slot online setting
  • one to stop application and go passive offline
  • one to recycle application pool and web sites

Preset runner pipeline, in our case this is separate AZDO pipeline with ID=101, containing run book written in PowerShell. The logic is responsible for reconfiguration application pools and web sites on demand:

parameters:
- name: AppPoolName
  type: string
- name: AppPoolAction
  values:
  - start
  - stop
  - recycle
  default: start
- name: AppPoolStartMode
  values:
  - OnDemand
  - AlwaysRunning
  default: AlwaysRunning
- name: Preload
  type: boolean
  values:
  - true
  - false
  default: true
- name: WebSiteAction
  displayName: WebSite Action
  values:
  - start
  - stop
  - recycle
  default: start

steps:
- powershell: |
    $SystemDirectory = [Environment]::SystemDirectory
    cd $SystemDirectory/inetsrv

    Write-Host "##[debug] Managing Site ${{ parameters.AppPoolName }}" 
    Write-Host "WebSiteAction: ${{ parameters.WebSiteAction }}"
    Write-Host "Preload: ${{ parameters.Preload }}"


    // Web Site management
    If ('${{ parameters.WebSiteAction }}' -eq 'recycle') {
      .\appcmd.exe stop site /site.name:${{ parameters.AppPoolName }}
      .\appcmd.exe start site /site.name:${{ parameters.AppPoolName }}
    }
    Else {
      .\appcmd.exe ${{ parameters.WebSiteAction }} site /site.name:${{ parameters.AppPoolName }}
    }


    // Preload management
    .\appcmd.exe set app "${{ parameters.AppPoolName }}/" /preloadEnabled:${{ parameters.Preload }}
    Write-Host "`n"

    Write-Host "##[debug] Managing AppPool ${{ parameters.AppPoolName }}"
    Write-Host "AppPoolAction: ${{ parameters.AppPoolAction }}"
    Write-Host "AppPoolStartMode: ${{ parameters.AppPoolStartMode }}"
    .\appcmd ${{ parameters.AppPoolAction }} apppool /apppool.name:${{ parameters.AppPoolName }}


    // Start Mode management
    If ("${{parameters.AppPoolStartMode}}" -Eq "AlwaysRunning") {
      .\appcmd.exe set apppool /apppool.name:${{ parameters.AppPoolName }} -startMode:AlwaysRunning
      .\appcmd.exe set apppool /apppool.name:${{ parameters.AppPoolName }} -autoStart:true
      .\appcmd.exe set apppool /apppool.name:${{ parameters.AppPoolName }} /processModel.idleTimeout:00:00:00
      .\appcmd.exe set apppool /apppool.name:${{ parameters.AppPoolName }} /processModel.startupTimeLimit:00:10:00
    }
    Else {
      .\appcmd.exe set apppool /apppool.name:${{ parameters.AppPoolName }} -startMode:OnDemand
      .\appcmd.exe set apppool /apppool.name:${{ parameters.AppPoolName }} -autoStart:false
      .\appcmd.exe set apppool /apppool.name:${{ parameters.AppPoolName }} /processModel.idleTimeout:00:02:00
      .\appcmd.exe set apppool /apppool.name:${{ parameters.AppPoolName }} /processModel.startupTimeLimit:00:20:00
    }

    // WebAdministration log
    Import-Module WebAdministration
    Get-ItemProperty IIS:AppPools/${{ parameters.AppPoolName }} | Select *
    Get-ItemProperty IIS:AppPools/${{ parameters.AppPoolName }} | Select-Object autoStart, startMode -ExpandProperty processModel | select autoStart, startMode, startupTimeLimit, idleTimeout
    Get-ItemProperty IIS:Sites/${{ parameters.AppPoolName }} | Select-Object -ExpandProperty applicationDefaults
  displayName: 'Manage AppPool and Site'
  errorActionPreference: 'silentlyContinue'
 

Fan-out execution with Azure DevOps REST API

To execute a pipeline in fan-out, I leverage Azure DevOps REST API:

First, create a JSON post request which contains all of parameters defined above, stages to run, proxy settings and even number of build artifact (if need it).

Second, set up authentication header with appropriate access token generated in AZDO.

And, Invoke-RestMethod:

Invoke-RestMethod -Method POST -ContentType 'application/json' -Headers $header -Body $body -Uri $url;
Please find, complete logic in my azdo-tools GitHub repository.

Below is presented main execution logic:

[CmdletBinding()]

### Usage example
# [string] $AzureDevOpsPAT = <PAT>,
# [string] $OrganizationName = 'company-prod',
# [string] $ProjectName = 'project',
# [string] $PipelineId = 101ex. company.project (https://dev.azure.com/company-prod/projecting/_build?definitionId=101),
# [string] $BranchName ~~ refs/heads/<branch-name>, default refs/heads/main
# [string[]] $StagesToSkip #stagesToSkip ~~ [ "Test", "Dev" ], default []

param(
    [Parameter(Mandatory=$true)]
    [string] $AzureDevOpsPAT,
    [Parameter(Mandatory=$true)]
    [string] $OrganizationName,
    [Parameter(Mandatory=$true)]
    [string] $ProjectName,
    [Parameter(Mandatory=$true)]
    [string] $PipelineId,
    [Parameter(Mandatory=$false)]
    [string] $BranchName,
    [Parameter(Mandatory=$false)]
    [string] $BuildId,
    [Parameter(Mandatory=$false)]
    [string] $Proxy,
    [Parameter(Mandatory=$false)]
    [string[]] $StagesToSkip,
    [Parameter(Mandatory=$false)]
    [string] $TemplateParameters
)    
    
$url = 'https://dev.azure.com/'+ $OrganizationName + '/' + $ProjectName + '/_apis/pipelines/' + $PipelineId + '/runs?api-version=6.0-preview.1';
$header = @{Authorization=("Basic {0}" -f $AzureDevOpsPAT)};

if([string]::IsNullOrWhiteSpace($BranchName))
{
   $BranchName = "refs/heads/main"
}

if([string]::IsNullOrWhiteSpace($StagesToSkip))
{
    $StagesToSkip = '[]'
}

if([string]::IsNullOrWhiteSpace($TemplateParameters))
{
    $TemplateParameters = '{}'
}

$buildAdd =""
if($BuildId -ne "latest")
{    $buildAdd = ',
        "pipelines": {
                "build": {
                    "version": "' + ${BuildId} + '"
                }
         }
    '
}

$body = '{
    "resources": {
        "repositories": {
            "self": {
                "refName": "' + ${BranchName} + '"
            }
        }' + ${buildAdd} + 
    '},
    "stagesToSkip":' + $StagesToSkip + ',
    "templateParameters":' + $templateParameters + '
}';

Write-Output $body

if ([string]::IsNullOrWhiteSpace($Proxy))
{
    $response = Invoke-RestMethod -Method POST -ContentType 'application/json' -Headers $header -Body $body -Uri $url;    
} 
else 
{
    $response = Invoke-RestMethod -Method POST -ContentType 'application/json' -Headers $header -Body $body -Uri $url -Proxy $Proxy; 
}

Write-Output $response.resources.pipelines
$webui = $response._links.web.href
Write-Host "`n`n"
Write-Host "##[section]pipeline web ui link: $webui`n`n"

In the post, I presented a way to create a maintenance process in Azure DevOps using pipelines and REST API. The maintenance process executed leveraging PowerShell scripts and YAML configuration.

I hope, you like the solution, if yes, please, click the link below or follow me on twitter.

Be an ethical, save your privacy!

subscribe to newsletter

and receive weekly update from our blog

By submitting your information, you're giving us permission to email you. You may unsubscribe at any time.

Leave a Comment

Discover more from #cybertechtalk

Subscribe now to keep reading and get access to the full archive.

Continue reading