Multi Stage Azure PipeLine for Golang

Ashok Raja
6 min readApr 28, 2020

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 will be 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

Pipeline View

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.

├── azure-pipelines.yml
├── go.mod
├── main.go
├── main_test.go
├── shiftleft.yml
└── templates
└── azure
├── jobs
│ ├── build.yml
│ ├── release.yml
│ └── test.yml
└── steps
├── buildapp.yml
└── setupgo.yml

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.

--

--

Ashok Raja

I’m a Computer Engineer by profession and a Traveler by heart. I love all things Computers, Travelling, Trekking and Biking.