Publicerades 11 augusti 2020

Azure Pipelines – YAML pipelines

Azure Pipelines med konfigurering i YAML

Microsoft satsar hårt på den nya pipeline-modellen, YAML pipelines, och har börjat fasa ut den gamla modellen som numera benämns "Classic UI". I denna blogg sätter Andreas Hagsten tänderna i Azure YAML pipelines och beskriver hur ni sätter upp en pipeline för bygge, test samt release till flera miljöer.

Ni som har arbetat i Azure DevOps (eller annat Devops-verktyg) känner säkert till begreppet Pipelines. Orginalet, som numera benämns “Classic UI”, är ett GUI där man kan dra och släppa färdiga komponenter in i sin CI eller CD pipeline. Azure YAML pipelines är det nya och framtiden. Med det kommer versionshanterad CI/CD och mycket annat.

Kort och koncist kan man säga att Azure YAML pipelines är CI/CD i kod. Den versionshanteras precis som vanlig kod. Låt oss undersöka vad detta möjliggör innan jag går in på en “how-to”-guide.

Rätt pipelineversion

Vi har fastställt att en pipeline är kod. Med detta kommer något kraftfullt och kanske uppenbart för många av er läsare. Om man behöver släppa en gammal version av sitt system kommer även pipelinen vara den version som gällde vid den tidpunkten. Tänk er en kritisk bugg som slinker igenom i en release där stora omstruktureringar gjordes i pipelinen. I det klassiska sättet kan man, om haft turen att vara så förutseende, skapa nya CI/CD pipelines och behålla de gamla som inaktiverade. Bara det är lite omständligt, men tänk om man inte haft kvar den gamla versionen – då hade man suttit i klistret. Det klassiska GUI:t är inte versionshanterat. Givetvis är detta ett förenklat exempel – andra saker kan ha förändrats utanför själva pipelinen som gör att man måste göra förändringar.

För att summera. Versionshanterad pipeline är väldigt kraftfullt.

Samma process som vanlig kod

Med det nya sättet kan inte förändringar av en pipeline slinka igenom obemärkt. Det är kod, som tidigare nämnt. Därmed kommer den följa samma process och rutin som all annan kod. Om ni t.ex. kör med kodgranskning och pull requests, kommer man behöva göra detta även för förändringar i sin pipeline. Detta ökar teamets förståelse för sin pipeline (något jag själv inte anser vara fallet i det klassiska UI:t) och ökar kvalitén på densamma.

Pipeline i Azure DevOps

YAML är ett DSL (Domain Specific Language) och med det följer en högre inlärningskurva än det klassiska UI:t som Azure DevOps erbjuder. Lyckligtvis finns det komponenter, så kallade “Tasks”, som man dra, släppa och konfigureras via ett UI som sen översätts till YAML-kod. Man får en del av kakan från det gamla, men resultatet är ändå bara kod.

Azure YAML Pipeline komponenter
Ett axplock av komponenter till en pipeline

Bilden här innan visar editorn man möts av i Azure DevOps Pipelines. Till vänster är YAML-koden och till höger finns alla komponenter. Med denna editor som bas tänker jag nu gå igenom hur man sätter upp en pipeline för en webbapplikation. Pipelinen ska köra enhetstester, bygga artefakter samt publicera applikationen i två olika miljöer (Test och Produktion). Vilken miljö som koden åker ut i beror på vilken branch som triggar pipelinen. Bilderna nedan är slutresultatet där den första bilden är en lyckad pipeline för develop medans den andra är en lyckad pipeline för master.

Deploy till test
“Stages” för develop-bygge

 

Deploy till produktion
“Stages” för master-bygge

Definiera triggers

En trigger är något som gör att en pipeline körs. Dvs, när någon gör en push till en branch. För att en pipeline ska lyssna efter förändringar på develop och master, bygger vi en lista på följande sätt och lägger den överst i vår YAML-fil.

trigger:
- develop
- master

Stages

Nästa begrepp som är viktigt är “stages”, eller steg. Det är dessa steg som visualiseras i bilderna ovan (Test, Build, Deploy). Steg är logiska grupperingar i din pipeline. De tillåter dig att pausa, tvinga fram manuell handpåläggning och styra vägen igenom pipelinen baserat på villkor (Azure term: conditions) och beroenden (Azure term: dependsOn). Villkor och beroenden är nyckeln till att få olika flöden beroende på vilken kod som byggs. Vi vill bara att byggen baserat på “master” skall ut i produktion men inte till test – och tvärtom för “develop”.


För att definiera ett steg, eller “stage” bygger man på en lista av “stages”. I exemplen nedan har jag tagit bort alla detaljer men hela filen återfinns i slutet av posten.

stages:
- stage: Tests
- stage: Build
- stage: Deploy_test
- stage: Deploy_Production

Vi bygger på denna och lär oss om “conditions” eller villkor. Ett sant/falsk-uttryck som bestämmer om steget skall utföras eller ej. Den skarpsynta kan se att villkoren för deploystegen också innefattar en kontroll mot vilken branch som koden kommer ifrån. Det är alltså så här vi kan styra vår pipeline och publicera rätt kod till rätt miljö.

stages:
- stage: Tests
- stage: Build
  condition: succeeded()
- stage: Deploy_test
  condition: and(succeeded(), eq(variables['build.sourceBranchName'], 'develop'))
- stage: Deploy_Production
  condition: and(succeeded(), eq(variables['build.sourceBranchName'], 'master'))

Just nu har vi en rak beroendekedja. Alla steg beror på det föregående steget. Det vi får är en rak pipeline, inte som vi vill ha enligt bilden ovan där båda deploystegen beror på steget Build. Vi lägger på dependsOn till båda deploystegen (behövs bara för det sista i vårat exempel, men det är en god idé att explicit peka ut sitt beroende).

stages:
- stage: Tests
- stage: Build
  condition: succeeded()
- stage: Deploy_test
  dependsOn: 'Build'
  condition: and(succeeded(), eq(variables['build.sourceBranchName'], 'develop'))
- stage: Deploy_Production
  dependsOn: 'Build'
  condition: and(succeeded(), eq(variables['build.sourceBranchName'], 'master'))

Nu har vi alla generella byggstenar för att ha en, och endast en pipeline som sköter både CI (bygge och test) samt CD (deployment) med divergerande pipelines beroende på branch.

Sammanfattning

Triggers, stages, conditions och beroenden (dependsOn). Dessa fyra begrepp utgör en bra grundplåt i en pipeline. Det finns såklart en uppsjö av andra begrepp när man gräver djupare. Bara fantasin sätter sina gränser på vad man kan göra. Känn och kläm gärna på en YAML-pipeline om ni har möjlighet. Det tog mig ett par försök och ett par “WTF:s” innan jag fick “grepp” om det.

YAML i sin helhet

trigger:
- develop
- master
variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'
stages:
- stage: Test
  pool:
    vmImage: 'windows-latest'
  jobs:
  - job: 'Build_solution'
    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)'
- stage: Build
  condition: succeeded('Test')
  pool:
    vmImage: 'windows-latest'
  jobs:
  - job: 'Build_solution'
    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: PublishBuildArtifacts@1
      inputs:
        PathtoPublish: '$(Build.ArtifactStagingDirectory)'
        ArtifactName: '$(Build.SourceBranchName)_drop'
        publishLocation: 'Container'
- stage: Deploy_Test
  displayName: 'Deploy to test'
  dependsOn: 'Build'
  condition: and(succeeded('Build'), eq(variables['build.sourceBranchName'], 'develop'))
  pool:
    vmImage: 'windows-latest'
  jobs:
  - job: 'Deploy'
    steps:
    - download: current
      artifact: develop_drop
    - task: AzureRmWebAppDeployment@4
      inputs:
        ConnectionType: 'AzureRM'
        azureSubscription: 'YOUR SUBSCRIPTION'
        appType: 'webApp'
        WebAppName: 'YamlLabb'
        packageForLinux: '$(Pipeline.Workspace)/$(Build.SourceBranchName)_drop/*.zip'
- stage: Deploy_Production
  displayName: 'Deploy to production'
  dependsOn: 'Build'
  condition: and(succeeded(), eq(variables['build.sourceBranchName'], 'master'))
  pool:
    vmImage: 'windows-latest'
  jobs:
  - job: 'Deploy'
    steps:
    - download: current
      artifact: master_drop
    - task: AzureRmWebAppDeployment@4
      inputs:
        ConnectionType: 'AzureRM'
        azureSubscription: 'YOUR SUBSCRIPTION'
        appType: 'webApp'
        WebAppName: 'YamlLabb'
        packageForLinux: '$(Pipeline.Workspace)/$(Build.SourceBranchName)_drop/*.zip'

Andreas Hagsten, Infozone