Azure DevOps: How to Build, Test And Deploy to Azure Kubernetes Service

Written by naseem | Published 2020/05/16
Tech Story Tags: devops | azure-devops | kubernetes | cicd | continuous-integration | continuous-deployment | containers | orchestration

TLDR Azure DevOps: How to Build, Test And Deploy to Azure Kubernetes Service with Azure Devops. We will walk through how to build and push an image to registry and then deploying it to AKS and running integration tests against it using SoapUI Pro Pro. This is where we will deploy the containers into the service. The build pipeline is build pipeline and the second pipeline is release pipeline. The release pipeline is to have a new Docker Image in Azure Contaoner registry. This will be the artifact we will deploying.via the TL;DR App

I have been using Azure Devops for a while. Like most of the cloud products out there this is one which gets constant refresh. My plan is to document the steps for building, testing and deploying an app to Azure Kubernetes Service using Azure Devops. So let's start.
Prerequisties
  • Github or Bitbucket
  • Azure Kubernetes Service
  • Azure Container Registry
  • Soap UI Pro (you need Pro edition for CICD).
  • Azure Devops
  • Azure Devops Agent hosted on your Windows VM. (needed for SoapUI Pro)

Flow Diagram

1) My code & Dockerfile
What I got is a simple .NET web api project. And below is my Dockerfile.
2) My Github link
3) Azure Container Registry
This is where we push our Docker image to. Below is a screenshot of Azure Container Registry from Azure Portal.
Azure Kubernetes Service.
This is the Azure managed Kubernetes Service. This is where we will deploy the containers into.
Azure Devops
Now that we have talked about the prerequisite required we will get ritht to it. We will walk through how we configure Azure Devops to build and push Docker image to registry and then deploying that image to AKS and running integration tests against it using SoapUI Pro
So we will create 2 pipelines in Azure Devops. (Details below).
  • The first one is build pipeline
  • and the second pipeline is release pipeline.
At the end of build pipeline the expected output is to have a new Docker Image in Azure Contaoner registry. This will be the artifact we will deploying to AKS as part of release pipeline.
Below is my yaml file.
# Dotnet
# Unit the dotnet project. xUnit and NSubtitute
# Docker
# Build and push an image to Azure Container Registry
# https://docs.microsoft.com/azure/devops/pipelines/languages/docker
# Publsih
# Now we get the tag of the published id and update the k8 Deployment yaml image
# https://docs.microsoft.com/azure/devops/pipelines/languages/docker

trigger:
- master

resources:
- repo: self
 

variables:
  # Container registry service connection established during pipeline creation
  dockerRegistryServiceConnection: '7c46ccde-aa97-4bd0-be94-abcd31bbe20b'
  containerRegistry: 'dockerstore'
  imageRepository: 'hospital'
  dockerfilePath: '**/Dockerfile'
  tag: '$(Build.BuildId)-$(Build.SourceVersion)'
  
  # Agent VM image name
  vmImageName: 'ubuntu-latest'

stages:
- stage: UnitTestBuildAndPublish
  displayName: Unit Test then Build and Push Docket to Register then Publish of release pipeline
  jobs:    
  - job: UnitTest
    displayName: Running Unit tests for the Hospital Microservice
    pool:
      vmImage: $(vmImageName)
    steps:    
    - task: DotNetCoreCLI@2
      inputs:
        command: 'test'
  - job: Build
    dependsOn: UnitTest
    displayName: Build and push to container registry
    pool:
      vmImage: $(vmImageName)
    steps:    
    - task: Docker@2
      displayName: Build and push an image to container registry
      inputs:
        command: buildAndPush
        repository: 'hospital'
        dockerfile: $(dockerfilePath)
        containerRegistry: $(containerRegistry)
        tags: |
          $(tag)
  - job: PreReleasePrepForhospitalMicroservice
    dependsOn: Build
    displayName: Pre Release Preparation (Bash build id and Publish for Release pipeline)
    pool:
      vmImage: $(vmImageName)
    steps:
    - task: Bash@3
      inputs:
        targetType: 'inline'
        script: |
          # Write your commands here            
                       
            cat '$(Build.SourcesDirectory)/hospital.yaml'              
            value=`cat '$(Build.SourcesDirectory)/hospital.yaml'`              
            value=${value//##BUILD_ID##/$(tag)}            
            echo "$value" > '$(Build.SourcesDirectory)/hospital_build.yaml'             
            value1=`cat '$(Build.SourcesDirectory)/hospital_build.yaml'`             
            echo "$value1"
            mkdir '$(Pipeline.Workspace)/hospital'  
            echo 'after creation of hospital'  
    - task: PublishPipelineArtifact@1
      inputs:
        targetPath: '$(Pipeline.Workspace)'
        artifact: 'hospital'
        publishLocation: 'pipeline'
Before talking about what is happening in above pipeliene; it maybe better to look at Azure Pipeline hierachy first.
stages:
- stage: A
  jobs:
  - job: A1
    timeoutInMinutes: 10
    pool:
      vmImage: 'ubuntu-16.04'
    steps:
    - bash: echo "Hello world"
  - job: A2
    steps:
    - bash: echo "A"
- stage: B
  jobs:
  - job: B1
    steps:
    - bash: echo "B"
  - job: B2
    steps:
    - bash: echo "A"
So the hierarchy is like above. You can have a list of stages which is the top level. Underneath it you can have a list of jobs which can further be broken down into steps and then steps into tasks. Also you can assign the kind of Build agent you want at job level. So you can have one job using Windows 10 agent another using Ubuntu.
OK now that the pipeline hierarchy is clear; let's go back to the original build pipeline I have above.
In that we have only 1 Stage called UnitTestBuildAndPublish.
There are 3 jobs within it.
  • Job1 called UnitTest
  • Job2 called Build There is a task within this Job called Docker@2 which is for Build and pushing to Azure Container Registry.
  • Job3 called PreReleasePrepForhospitalMicroservice. There are two tasks withing this. In the first task I use the bash command to get hold of the K8 yaml file and replace the placeholder with the imagetag. Then in the second task I use the PublishPipelineArtifact. This is kind of pushing the artifact so i can get hold of this in the release pipeline. There I need info on the Imagetag that was pushed to Azure Container Registry.(https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/publish-pipeline-artifact?view=azure-devops)
    On whether the jobs runs in sequence or in parallel. By default they run parallel. But in above yaml file you may notice the "dependsOn" tag under each job. You will note that Build job "dependsOn" Unit test project to complete. And Prepublish job "dependsOn" Build Job. SO in essense they are executed in an sequential manner.
  • Sequence: UnitTest > Build(&Push Docker Image to Registry > Prepublish
  • Variables in above build pipeline.
    • dockerRegistryServiceConnection: This is the Azure Container Registry connection string. This is preconfigured.
    • containerRegistry: 'dockerstore' is the ACR service name,
    • imageRepository: This is image repositor (or microservice name.
    • dockerfilePath: Relative path to the Dockerfile in Github
    • tag: Docker Image tag. We are using the BuildId and the Github Commitid for traceablity from Docker Image to Github code.
    Release Pipeline
    Below is the screenshot of my release pipeline tasks page. As you can see I am using two Agents.
    • Hosted Agent running Ubuntu 18.04
    • Custom Agent on Windows 10 VM
      SoapUI Pro requires a Windows 10 OS to run it's tests.
    Download Pipeline Artifact (running on Ubuntu agent)
    So first step is to Download the Pipeline Artifact. We need this for K8 yaml file. In the Build pipleine we have updated the ##BuildID## placeholder with the real tag of the image that was pushed to ACR.
    Kubectl Apply (running on Ubuntu agent)
    We will use this task to apply the above yaml file against the "NM" namespace of AKS. The namespace is a way to logically have multpile environments in Kubernetes. The above yaml shows that the pods and services will be deployed to "NM" namespace.
    The above command confirms that the services and pods are deployed in that namespace.
    Bash Script Task (running on Ubuntu agent)
    This is more of a hack I put in there. This task just sleeps and holds the pipeline for few seconds. I wanted to ensure that the Pods are given enough time to be up and running. This is required because if the Pods are not Up SoapUI Pro will fail all the tests. Which is not the right thing.
    Azure SQL Dacpac Task (running on Ubuntu agent)
    This is for database deployment. I am not doing anything currently for this project. That is why it is in disabled state. But this is the task you would use to run the DDL and DML statements against your Azure SQL.
    API Management - Create/Update API (running on Windows 10 agent)
    This is task, I got from the market lace which helps me deploying API definitions to Azure API Management. It accepts OAS 3.0 defintion that I build using Swagger editor. Below is my API definition.
    openapi: 3.0.0
    # Added by API Auto Mocking Plugin
    servers:
      - description: SwaggerHub API Auto Mocking
        url: https://virtserver.swaggerhub.com/BRB/Hospital/1.0.0
    info:
      description: Demo API on Hospital. 
      version: "1.0.0"
      title: Hospital
      contact:
        email: [email protected]
      license:
        name: Apache 2.0
        url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
    tags:
      - name: Hospital
        description: Hospital related matters
    paths:
      /hospital/{hospitalId}:
        get:
          tags:
            - Hospital
          summary: Finds hospital by id
          operationId: GetHospitalById
          description: |
            By passing in the valid id, you can search for
            the hospial details in the database
          parameters:
            - in: path
              name: hospitalId
              description: pass the hospitalId for looking up the database
              required: true
              schema:
                type: integer
              example: 415
          responses:
            '200':
              description: search result matching criteria
              content:
                application/json:
                  schema:
                    $ref: '#/components/schemas/Hospital'
            '400':
              description: bad input parameter
        delete:
          tags:
            - Hospital
          summary: deletes a hospital from list
          operationId: DeleteHospial
          description: Deletes a hospital from list
          parameters:
          - name: hospitalId
            in: path
            description: Hospital id to delete
            required: true
            schema:
              type: string
          responses:
            '400':
              description: "Invalid ID supplied"
            '404':
              description: "Hospital not found"
      /hospital:
        post:
          tags:
            - Hospital
          summary: adds a hospital to the list
          operationId: AddHospital
          description: Adds a hospital to the list
          parameters:
          - name: Authorization
            in: header
            required: true
            schema:
              type: string
          responses:
            '201':
              description: hospital added
            '400':
              description: invalid input, object invalid
            '409':
              description: hospital already exists
          requestBody:
            content:
              application/json:
                schema:
                  $ref: '#/components/schemas/Hospital'
            description: Add Hospital to list
        patch:
          tags:
            - Hospital
          summary: Updates a hospital in the list
          operationId: UpdateHospital
          description: Updates a hospital to the list
          responses:
            '200':
              description: hospital updated
            '400':
              description: invalid input, object invalid
            '404':
              description: hospital does not exists
          requestBody:
            content:
              application/json:
                schema:
                  $ref: '#/components/schemas/Hospital'
            description: Updates Hospital in the list
    components:
      schemas:
        Hospital:
          type: object
          required:
            - Id
            - Name
            - Address
            - City
            - Pincode
          properties:
            id:
              type: integer
              example: 56
            name:
              type: string
              example: Epidemic Diseases Hospial
            Address:
              type: string
              example: MAJESTIC
            City:
              type: string
              example: Bangalore
            Pincode:
              type: integer
              example: 562110
       
    SoapUI Pro for Azure DevOps (running on Win10 agent)
    After all of the above steps are completed; it is time to do the integration tests. And make sure everything is complying. SLA will be met. We didn't break anything.
    Once the tests are completed SoapUI exports some of the reports to Azure Devops. Below is one such report.
    Once all the test passes; we can promote the deployments to higher environments. This too can be automated nicely in Azure Devops. Each environment is called stages and we can add Manual Approver too as part of environment promotion. Below is a screenshot of the stages graphic that Azure Devops provide.
    So that's it. This is my github page: https://github.com/mohammednaseem/aksistio-hospital
    if you run into trouble with Azure Devops let me know.

    Written by naseem | https://github.com/mohammednaseem
    Published by HackerNoon on 2020/05/16