Multi Stage Azure Pipeline for Golang
I have been playing around with Azure DevOps as part of my experiment with various CI/CD tools. I will be sharing my views and comparison of various CI/CD tools in a separate post.
For now lets focus on how we can create a templatized Multi Staged Azure Pipeline.
GitHub Repo : https://github.com/ashokrajar/mylabs-go
Multi Stage Azure Pipeline Demo : https://dev.azure.com/ashokrajar/testpad
We will be building a simple Golang cli application for demo purpose.
shell # ./mylabs-go
Hello Home !!
shell # ./mylabs-go version
0.2.1
shell #
Let’s demonstrate how we do Build => Test => Deploy(In our case it willbe Release Binary Artifact) using Azure Multi Stage build.
What all Tests will we be running ?
- Unit Tests
- Code Coverage
- Vulnerability Test (we will be using ShiftLeft)
All these tests will be executed in parallel for multiple versions of Golang.
Our GOAL
Create a dynamically configurable Azure Pipeline for Golang application for multiple platforms.
Multi Stage Azure Pipeline Demo : https://dev.azure.com/ashokrajar/testpad
A Multi Stage build which looks like this
Embedded Test Results
Embedded Code Coverage Results
Embedded Vulnerability Scan Reports
Alright enough showing off let’s get our hands dirty.
Design/Create a Azure Pipeline to achieve our GOAL
Step 1 : Create folders/files hierarchy
For creating reusable template we need a proper folders/files hierarchy design.
Step 2 : Create azure-pipeline.yml
We have designed a pipeline config which will trigger the builds for commits to master, dev & release/* branches and also or pull request to master branch. At the same time it will build will not be triggered for changes to non-project files.
trigger:
batch: true # Ensure batch execution for very active repos
branches:
include:
- master
- dev
- release/*
paths:
exclude:
- README.md
- .gitignore
pr:
autoCancel: True # Auto cancel if active pull request updated
branches:
include:
- master
paths:
exclude:
..... removed for brevity .....
variables:
GOPATH: '$(Pipeline.Workspace)/gowork'
stages:
# We will be building stages in following steps
Before we start creating multiple stages lets create some reusable templates.
Step 3 : Creating Shared Templates
Azure provides a powerful templating functionality which let you define reusable content, logic, and parameter.
Template : templates/azure/steps/setupgo.yml
parameters:
goVersion: '1.14'
steps:
- task: GoTool@0
displayName: 'Use Go ${{ parameters.goVersion }}'
inputs:
version: ${{ parameters.goVersion }}
- script: |
set -e -x
mkdir -p '$(GOPATH)/bin'
echo '##vso[task.prependpath]$(GOROOT)/bin'
echo '##vso[task.prependpath]$(GOPATH)/bin'
displayName: 'Create Go Workspace'
This will setup the Golang workspace with the default version of 1.14. Which can be overridden when calling the template from the pipeline config files.
Template : templates/azure/steps/buildapp.yml
steps:
- task: Go@0
displayName: 'Build Application Binary'
inputs:
command: 'build'
workingDirectory: '$(System.DefaultWorkingDirectory)'
arguments: '-o $(Build.BinariesDirectory)/mylabs-go'
Similarly this template helps build the Golang application.
Template : templates/azure/jobs/build.yml
parameters:
name: ''
pool: ''
jobs:
- job: ${{ parameters.name }}
pool: ${{ parameters.pool }}
steps:
- template: ../steps/setupgo.yml
- template: ../steps/buildapp.yml
This template will help executing of the build stage of the the Golang application.
But Wait ! What ? More templates inherited inside the template ? YES!, we are just make using the templates we designed in the previous steps
Template : templates/azure/jobs/test.yml
jobs:
- job: RunTests
strategy:
matrix:
GoVersion_1_13:
go.version: '1.13'
GoVersion_1_14:
go.version: '1.14'
pool:
vmImage: 'ubuntu-18.04'
steps:
- template: ../steps/setupgo.yml
parameters:
goVersion: '$(go.version)'
- script: |
set -e -x
go version
go get -u github.com/jstemmer/go-junit-report
go get github.com/axw/gocov/gocov
go get github.com/AlekSi/gocov-xml
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0
curl https://cdn.shiftleft.io/download/sl > $(go env GOPATH)/bin/sl && chmod a+rx $(go env GOPATH)/bin/sl
displayName: 'Install Dependencies'
- script: |
set -e -x
golangci-lint run
displayName: 'Run Code Quality Checks'
- script: |
set -e -x
go test -v -coverprofile=coverage.txt -covermode count ./... > test_results.txt
go-junit-report < test_results.txt > report.xml
displayName: 'Run Unit Tests'
- task: PublishTestResults@2
displayName: 'Publish Test Results'
inputs:
testRunner: JUnit
testResultsFiles: $(System.DefaultWorkingDirectory)/**/report.xml
- script: |
set -e -x
gocov convert coverage.txt > coverage.json
gocov-xml < coverage.json > coverage.xml
displayName: 'Run Code Coverage Tests'
- task: PublishCodeCoverageResults@1
displayName: 'Publish Code Coverage'
inputs:
codeCoverageTool: Cobertura
summaryFileLocation: $(System.DefaultWorkingDirectory)/**/coverage.xml
- script: |
curl https://cdn.shiftleft.io/download/sl > $BUILD_SOURCESDIRECTORY/sl && chmod a+rx $BUILD_SOURCESDIRECTORY/sl
$BUILD_SOURCESDIRECTORY/sl analyze --wait --tag branch=$BUILD_SOURCEBRANCHNAME --tag app.group=MyLabs --tag app.language=go --app MyLabs-G0 --cpg --go ./...
displayName: 'Run Vulnerability Checks'
env:
SHIFTLEFT_ORG_ID: $(SHIFTLEFT_ORG_ID)
SHIFTLEFT_ACCESS_TOKEN: $(SHIFTLEFT_ACCESS_TOKEN)
- script: |
set -e -x
docker run \
-v "$(Build.SourcesDirectory):/app:cached" \
-v "$(Build.ArtifactStagingDirectory):/reports:cached" \
shiftleft/sast-scan scan --src /app \
--out_dir /reports/CodeAnalysisLogs
displayName: "Perform Vulnerability Scan"
continueOnError: "true"
- task: PublishBuildArtifacts@1
displayName: "Publish Vulnerability Scan Results"
inputs:
PathtoPublish: "$(Build.ArtifactStagingDirectory)/CodeAnalysisLogs"
ArtifactName: "CodeAnalysisLogs"
publishLocation: "Container"
This template will perform these actions,
- Setup Golang
- Install Dependencies
- Run Code Quality Checks
- Run Unit Tests
- Publish Test Results into Pipeline
- Publish Coverage Results into Pipeline
- Run ShiftLeft Inspect/anAlyse Vulnerability Scan
- Run ShiftLeft SAST Vulnerability Scan
- Publish ShiftLeft SAST Vulnerability Scan Results into Pipeline
Testing on multiple versions.
strategy:
matrix:
GoVersion_1_13:
go.version: '1.13'
GoVersion_1_14:
go.version: '1.14'
Also note the strategy we have defined, if you want to support wider version of Golang just add more version, here it’s that simple.
Template : templates/azure/jobs/release.yml
parameters:
name: ''
pool: ''
jobs:
- job: ${{ parameters.name }}
pool: ${{ parameters.pool }}
steps:
- template: ../steps/setupgo.yml
- template: ../steps/buildapp.yml
- task: CopyFiles@2
displayName: 'Copy binary files to Artifact Stage Directory'
inputs:
sourceFolder: $(Build.BinariesDirectory)
targetFolder: $(Build.ArtifactStagingDirectory)
- task: PublishBuildArtifacts@1
displayName: 'Publish Build Artifacts'
inputs:
artifactName: $(Agent.OS)
- task: Bash@3
displayName: 'Get/Set Application/Package Version'
inputs:
targetType: 'inline'
script: |
set -e -x
version=`./mylabs-go version`
echo "##vso[task.setvariable variable=MYLABSCLI_VERSION;]$version"
workingDirectory: $(Build.BinariesDirectory)
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master'))
- task: Bash@3
displayName: 'Get/Set OS Specific Package Feed Name'
inputs:
targetType: 'inline'
script: |
set -e -x
OS_NAME=`echo "$(Agent.OS)" | tr "[:upper:]" "[:lower:]"`
echo "##vso[task.setvariable variable=FEED_NAME;]$OS_NAME"
workingDirectory: $(Build.BinariesDirectory)
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master'))
- task: UniversalPackages@0
displayName: 'Publish Release Artifacts'
inputs:
command: 'publish'
publishDirectory: '$(Build.ArtifactStagingDirectory)'
feedsToUsePublish: 'internal'
vstsFeedPublish: '1354bdaa-1b77-41d3-a573-e85080e85d85/90f9f1a3-3b7f-4814-aea6-f06d7842d9af'
vstsFeedPackagePublish: $(FEED_NAME)
versionOption: 'custom'
versionPublish: $(MYLABSCLI_VERSION)
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master'))
This template will do these actions,
- Setup Golang
- Build and produce Golang application binary artifact
- Set environment variable for OS specific Azure Artifact Universal Package Feed
- Get/Set Application version to be published in the Azure Artifact Universal Package Feed
- Push the application binary into Azure Artifact Universal Package
Alright now we have created a reusable build template how do we use this in the pipeline ?
Step 4 : Create Build Stage
azure-pipeline.yml
stages:
- stage: Build
jobs:
- template: templates/azure/jobs/build.yml # Linux Build
parameters:
name: 'Linux_Build'
pool:
vmImage: 'ubuntu-18.04'
- template: templates/azure/jobs/build.yml # macOS Build
parameters:
name: 'Mac_Build'
pool:
vmImage: 'macos-10.14'
Now you can see how the parmeterized template helped us to reuse the sample for different build binary based on the Operating System.
Step 5 : Create Test Stage
azure-pipeline.yml
stages:
..... removed for brevity .....
- stage: Test
jobs:
- template: templates/azure/jobs/test.yml
I don’t have to explain here as it’s self explanatory.
Final Step : Create Release Stage
azure-pipeline.yml
stages:
..... removed for brevity .....
- stage: Release
jobs:
- template: cicd/jobs/release.yml
parameters:
name: 'Linux_Release'
pool:
vmImage: 'ubuntu-18.04'
- template: cicd/jobs/release.yml
parameters:
name: 'Mac_Release'
pool:
vmImage: 'macos-10.14'
This stage will create release for multiple Operating System.