Azure DevOps: How to Build, Test And Deploy to Azure Kubernetes Service
Tags: devops, aks, cicd, kubernetes
Published: 2020-05-27 06:39:05

I have been using Azure DevOps for a while. Like most of the cloud products out there this is one which gets a 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.

### Prerequisites ###
* 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 ###
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/b94v5tkbxwn9oyem9bfo.png)

#### 1) My code & Dockerfile ####
What I got is a simple .NET Web API project. And below is my Dockerfile.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/bvkvr3my4dn4dha21urt.png)

#### 2) My Github link to project ####
https://github.com/mohammednaseem/aksistio-hospital

#### 3) Azure Container Registry #### 
This is the repository where the Docker image is hosted. Below is a screenshot of the Azure Container Registry from Azure Portal.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/vdqvb7dhtij47pq7pb2t.png)

#### 4) Azure Kubernetes Service ####
Now that we have talked about the prerequisite required we will get right to it. We will walk through how we configure Azure DevOps to build and push Docker image to the 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 the build pipeline
* and the second pipeline is the release pipeline.

### Build pipeline ###
At the end of the build pipeline the expected output is to have a new Docker image in Azure Container registry. This will be the artifact we will deploying to AKS as part of the release pipeline.

Below is the build pipeline's YAML file.

```yaml
# 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 Build Pipeline hierachy first.


```yaml
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 that can further be broken down into steps and then steps into tasks. Also you can assign the kind of Build agent you want at the 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. But 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. 
[Publish-Pipeline-Artifact](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/publish-pipeline-artifact?view=azure-devops)

On whether the jobs run in sequence or in parallel. By default, they run parallel. But in the above YAML file you may notice the "dependsOn" tag under each job. You will note that the Build job "dependsOn" Unit test project to complete. And Prepublish job "dependsOn" Build Job. So in essence they are executed in a sequential manner.
So the Sequence is UnitTest > Build(&Push Docker Image to Registry > Prepublish

####Variables in above build pipeline.####

* dockerRegistryServiceConnection: This is the Azure Container Registry's connection string. This is preconfigured.
* containerRegistry: 'dockerstore' is the ACR service name,
* imageRepository: This is an image repository (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 traceability 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.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/zorln77znoz9gepdv7ps.png)

* Hosted Agent running Ubuntu 18.04
  * Needed to build Linux Docker images for deployment to Linux Nodepool in AKS.
* Custom Agent on Windows 10 VM
  * SoapUI Pro requires a Windows 10 OS to run its tests.
  * API Management tasks also require Windows 10 OS to run.

####Download Pipeline Artifact (running on Ubuntu agent)####
So the first step is to Download the Pipeline Artifact. We need this for the K8 YAML file. In the Build pipeline 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 multiple environments in Kubernetes. The above YAML shows that the pods and services will be deployed to "NM" namespace.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/320hqepdrdu7n98kzqn5.png)

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 a 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. 

####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 a 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 a task, I got from the marketplace which helps me deploying API definitions to Azure API Management. It accepts OAS 3.0 definition that I build using Swagger editor. Below is my API definition.

```yaml
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: m.naseem@outlook.com
  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.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/0p5ejp7dffypxoxiagdi.png)

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 provides.


![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/za5wozdocs1qr4f05u4w.png)