Azure DevOps is a suite of services provided by Microsoft Azure that allows teams to plan projects, collaborate on code development, and deploy applications. It includes services like Azure Repos for version control, Azure Pipelines for continuous integration and deployment (CI/CD), Azure Boards for project management, Azure Artifacts for package management, and Azure Test Plans for test management.
Overall, it provides a comprehensive set of tools for software development and delivery, supporting agile practices and DevOps principles.
In this blog post I am going over how to setup end to end CI/CD project using Azure Devops.
Source: https://github.com/dockersamples/example-voting-app
Pre-Requisities
-
Import the source repository into Azure DevOps portal:
-
Select
main
as the default branch: -
Create resource group
azurecicd
for the project:
$ az group create --name azurecicd --location eastus
{
"id": "/subscriptions/6fecc3e6-8a71-4138-b2a4-6527f4151493/resourceGroups/azurecicd",
"location": "eastus",
"managedBy": null,
"name": "azurecicd",
"properties": {
"provisioningState": "Succeeded"
},
"tags": null,
"type": "Microsoft.Resources/resourceGroups"
}
- Create container registry
sanchitazurecicd
in Azure Portal:
$ az acr create --name sanchitazurecicd --resource-group azurecicd --sku Standard
{
"adminUserEnabled": false,
"anonymousPullEnabled": false,
"creationDate": "2024-04-01T05:27:16.531227+00:00",
"dataEndpointEnabled": false,
"dataEndpointHostNames": [],
"encryption": {
"keyVaultProperties": null,
"status": "disabled"
},
"id": "/subscriptions/6fecc3e6-8a71-4138-b2a4-6527f4151493/resourceGroups/azurecicd/providers/Microsoft.ContainerRegistry/registries/sanchitazurecicd",
"identity": null,
"location": "eastus",
"loginServer": "sanchitazurecicd.azurecr.io",
"metadataSearch": "Disabled",
"name": "sanchitazurecicd",
"networkRuleBypassOptions": "AzureServices",
"networkRuleSet": null,
"policies": {
"azureAdAuthenticationAsArmPolicy": {
"status": "enabled"
},
"exportPolicy": {
"status": "enabled"
},
"quarantinePolicy": {
"status": "disabled"
},
"retentionPolicy": {
"days": 7,
"lastUpdatedTime": "2024-04-01T05:27:28.285623+00:00",
"status": "disabled"
},
"softDeletePolicy": {
"lastUpdatedTime": "2024-04-01T05:27:28.285666+00:00",
"retentionDays": 7,
"status": "disabled"
},
"trustPolicy": {
"status": "disabled",
"type": "Notary"
}
},
"privateEndpointConnections": [],
"provisioningState": "Succeeded",
"publicNetworkAccess": "Enabled",
"resourceGroup": "azurecicd",
"sku": {
"name": "Standard",
"tier": "Standard"
},
"status": null,
"systemData": {
"createdAt": "2024-04-01T05:27:16.531227+00:00",
"createdBy": "[email protected]",
"createdByType": "User",
"lastModifiedAt": "2024-04-01T05:27:16.531227+00:00",
"lastModifiedBy": "[email protected]",
"lastModifiedByType": "User"
},
"tags": {},
"type": "Microsoft.ContainerRegistry/registries",
"zoneRedundancy": "Disabled"
}
Implementing Continuous Integration (CI)
- Creating VM
azureagent
for running pipelines:
$ az vm create --resource-group azurecicd --name azureagent --image Ubuntu2204 --vnet-name demo-vnet --subnet default --generate-ssh-keys --output json --verbose
Use existing SSH public key file: <REDACTED>
{
"fqdns": "",
"id": "/subscriptions/6fecc3e6-8a71-4138-b2a4-6527f4151493/resourceGroups/azurecicd/providers/Microsoft.Compute/virtualMachines/azureagent",
"location": "eastus",
"macAddress": "00-0D-3A-57-DA-96",
"powerState": "VM running",
"privateIpAddress": "10.0.0.4",
"publicIpAddress": "13.90.150.209",
"resourceGroup": "azurecicd",
"zones": ""
}
-
Configure new Agent Pool named
azureagent
in project settings and run the instructions on the created VMazureagent
: -
Connect to the VM and download the binary:
$ az ssh vm --resource-group azurecicd --vm-name azureagent --subscription 6fecc3e6-8a71-4138-b2a4-6527f4151493
...
[email protected]@azureagent:~$ wget https://vstsagentpackage.azureedge.net/agent/3.236.1/vsts-agent-linux-x64-3.236.1.tar.gz
--2024-04-01 17:15:18-- https://vstsagentpackage.azureedge.net/agent/3.236.1/vsts-agent-linux-x64-3.236.1.tar.gz
Resolving vstsagentpackage.azureedge.net (vstsagentpackage.azureedge.net)... 72.21.81.200, 2606:2800:11f:17a5:191a:18d5:537:22f9
Connecting to vstsagentpackage.azureedge.net (vstsagentpackage.azureedge.net)|72.21.81.200|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 161771925 (154M) [application/octet-stream]
Saving to: ‘vsts-agent-linux-x64-3.236.1.tar.gz’
vsts-agent-linux-x64-3.236.1.tar.gz 100%[====================================================================================================>] 154.28M 114MB/s in 1.4s
2024-04-01 17:15:20 (114 MB/s) - ‘vsts-agent-linux-x64-3.236.1.tar.gz’ saved [161771925/161771925]
- Untar the binary:
[email protected]@azureagent:~/myagent$ tar zxvf vsts-agent-linux-x64-3.236.1.tar.gz
...
[email protected]@azureagent:~/myagent$ ls
bin config.sh env.sh externals license.html run-docker.sh run.sh vsts-agent-linux-x64-3.236.1.tar.gz
- Create a Personal Access Token in Azure DevOps Settings and run the config script:
[email protected]@azureagent:~/myagent$ ./config.sh
_ _
/ _ \ | ___ (_) | (_)
/ /_\ \_____ _ _ __ ___ | |_/ /_ _ __ ___| |_ _ __ ___ ___
| _ |_ / | | | '__/ _ \ | __/| | '_ \ / _ \ | | '_ \ / _ \/ __|
| | | |/ /| |_| | | | __/ | | | | |_) | __/ | | | | | __/\__ \
\_| |_/___|\__,_|_| \___| \_| |_| .__/ \___|_|_|_| |_|\___||___/
| |
agent v3.236.1 |_| (commit 1d7a476)
>> End User License Agreements:
Building sources from a TFVC repository requires accepting the Team Explorer Everywhere End User License Agreement. This step is not required for building sources from Git repositories.
A copy of the Team Explorer Everywhere license agreement can be found at:
/home/sanchitpathak17/myagent/license.html
Enter (Y/N) Accept the Team Explorer Everywhere license agreement now? (press enter for N) > Y
>> Connect:
Enter server URL > https://dev.azure.com/sanchitpathak17
Enter authentication type (press enter for PAT) >
Enter personal access token > ****************************************************
Connecting to server ...
>> Register Agent:
Enter agent pool (press enter for default) > azureagent
Enter agent name (press enter for azureagent) > azureagent
Scanning for tool capabilities.
Connecting to the server.
Successfully added the agent
Testing agent connection.
Enter work folder (press enter for _work) >
2024-04-01 17:22:27Z: Settings Saved.
- Setup Docker on the VM:
[email protected]@azureagent:~/myagent$ sudo apt install docker.io
[email protected]@azureagent:~/myagent$ sudo usermod -aG docker [email protected]
[email protected]@azureagent:~/myagent$ sudo systemctl restart docker
[email protected]@azureagent:~/myagent$ systemctl status docker
● docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
Active: active (running) since Mon 2024-04-01 17:26:25 UTC; 3s ago
TriggeredBy: ● docker.socket
Docs: https://docs.docker.com
Main PID: 4349 (dockerd)
Tasks: 8
Memory: 31.1M
CPU: 314ms
CGroup: /system.slice/docker.service
└─4349 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
- Exit the session, log back in again and then run the executable:
[email protected]@azureagent:~/myagent$ ./run.sh
Scanning for tool capabilities.
Connecting to the server.
2024-04-01 17:26:44Z: Listening for Jobs
-
Agent Pool is now online
-
Configuring path based trigger while setting up Azure DevOps Pipelines for
Results
App.
$ voting-app/azure-pipelines-result.yml
# Docker
# Build and push an image to Azure Container Registry
# https://docs.microsoft.com/azure/devops/pipelines/languages/docker
trigger:
paths:
include:
- result/*
resources:
- repo: self
variables:
# Container registry service connection established during pipeline creation
dockerRegistryServiceConnection: '37ddf373-922d-4add-b43a-076639b876e8'
imageRepository: 'resultapp'
containerRegistry: 'sanchitazurecicd.azurecr.io'
dockerfilePath: '$(Build.SourcesDirectory)/result/Dockerfile'
tag: '$(Build.BuildId)'
pool:
name: 'azureagent'
stages:
- stage: Build
displayName: Build
jobs:
- job: Build
displayName: Build
steps:
- task: Docker@2
displayName: Build an image to container registry
inputs:
containerRegistry: '$(dockerRegistryServiceConnection)'
repository: '$(imageRepository)'
command: 'build'
Dockerfile: 'result/Dockerfile'
tags: '$(tag)'
- stage: Push
displayName: Push
jobs:
- job: Push
displayName: Push
steps:
- task: Docker@2
displayName: Push an image to container registry
inputs:
containerRegistry: '$(dockerRegistryServiceConnection)'
repository: '$(imageRepository)'
command: 'push'
tags: '$(tag)'
-
Pipeline Run Succeeded:
-
Configuring path based trigger while setting up Azure DevOps Pipelines for
Voting
App.
# Docker
# Build and push an image to Azure Container Registry
# https://docs.microsoft.com/azure/devops/pipelines/languages/docker
trigger:
paths:
include:
- vote/*
resources:
- repo: self
variables:
# Container registry service connection established during pipeline creation
dockerRegistryServiceConnection: 'eae15d9c-952e-4b9e-bdc3-808d2a63c8f9'
imageRepository: 'votingapp'
containerRegistry: 'sanchitazurecicd.azurecr.io'
dockerfilePath: '$(Build.SourcesDirectory)/result/Dockerfile'
tag: '$(Build.BuildId)'
pool:
name: 'azureagent'
stages:
- stage: Build
displayName: Build an image
jobs:
- job: Build
displayName: Build
steps:
- task: Docker@2
displayName: Build an image
inputs:
containerRegistry: '$(dockerRegistryServiceConnection)'
repository: '$(imageRepository)'
command: 'build'
Dockerfile: 'vote/Dockerfile'
tags: '$(tag)'
- stage: Push
displayName: Push an image
jobs:
- job: Push
displayName: Push
steps:
- task: Docker@2
displayName: Push an image
inputs:
containerRegistry: '$(dockerRegistryServiceConnection)'
repository: '$(imageRepository)'
command: 'push'
Dockerfile: 'vote/Dockerfile'
tags: '$(tag)'
- Configuring path based trigger while setting up Azure DevOps Pipelines for
Worker
App.
# Docker
# Build and push an image to Azure Container Registry
# https://docs.microsoft.com/azure/devops/pipelines/languages/docker
trigger:
paths:
include:
- worker/*
resources:
- repo: self
variables:
# Container registry service connection established during pipeline creation
dockerRegistryServiceConnection: '357976e2-83df-43b0-8208-8236ec12a82d'
imageRepository: 'workerapp'
containerRegistry: 'sanchitazurecicd.azurecr.io'
dockerfilePath: '$(Build.SourcesDirectory)/result/Dockerfile'
tag: '$(Build.BuildId)'
pool:
name: 'azureagent'
stages:
- stage: Build
displayName: Build an image
jobs:
- job: Build
displayName: Build
steps:
- task: Docker@2
displayName: Build an image
inputs:
containerRegistry: '$(dockerRegistryServiceConnection)'
repository: '$(imageRepository)'
command: 'build'
Dockerfile: 'worker/Dockerfile'
tags: '$(tag)'
- The worker pipeline failed with error:
failed to parse platform : "" is an invalid component of "": platform specifier component must match "^[A-Za-z0-9_-]+$": invalid argument
-
Updated the Dockerfile to specify the platform for worker app in Repo section. Azure automatically picked up the change and ran the pipeline successfully.
-
All the pipeline success confirmation:
-
At this stage, all GitHub CI pipelines are migrated to Azure DevOps.
Implementing Continuous Delivery (CD)
- Now, for the CD part, first let’s create a AKS i.e. Azure Kubernetes Cluster.
$ az aks list --resource-group azuredevops_group --output table
Name Location ResourceGroup KubernetesVersion CurrentKubernetesVersion ProvisioningState Fqdn
----------- ---------- ----------------- ------------------- -------------------------- ------------------- ----------------------------------------------
azuredevops westus2 azuredevops_group 1.28.5 1.28.5 Succeeded azuredevops-dns-0y5c1lu4.hcp.westus2.azmk8s.io
$ az aks get-credentials --name azuredevops --resource-group azuredevops_group
Merged "azuredevops" as current context in /Users/sanchit/.kube/config
$ kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
aks-agentpool-12250849-vmss000000 Ready agent 4m5s v1.28.5 10.224.0.4 20.187.35.201 Ubuntu 22.04.4 LTS 5.15.0-1058-azure containerd://1.7.7-1
- Install ArgoCD
Reference: https://argo-cd.readthedocs.io/en/stable/getting_started/
$ kubectl create namespace argocd
namespace/argocd created
$ kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
$ kubectl get all -n argocd
NAME READY STATUS RESTARTS AGE
pod/argocd-application-controller-0 1/1 Running 0 35s
pod/argocd-applicationset-controller-75b78554fd-bz86c 1/1 Running 0 38s
pod/argocd-dex-server-869fff9967-9cj42 1/1 Running 0 37s
pod/argocd-notifications-controller-5b8dbb7c86-l2sc5 1/1 Running 0 37s
pod/argocd-redis-66d9777b78-rfnb2 1/1 Running 0 37s
pod/argocd-repo-server-7b8d97c767-prh92 1/1 Running 0 36s
pod/argocd-server-5c797497fb-x77xj 1/1 Running 0 36s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/argocd-applicationset-controller ClusterIP 10.0.184.112 <none> 7000/TCP,8080/TCP 41s
service/argocd-dex-server ClusterIP 10.0.115.30 <none> 5556/TCP,5557/TCP,5558/TCP 40s
service/argocd-metrics ClusterIP 10.0.249.23 <none> 8082/TCP 40s
service/argocd-notifications-controller-metrics ClusterIP 10.0.30.135 <none> 9001/TCP 40s
service/argocd-redis ClusterIP 10.0.54.169 <none> 6379/TCP 40s
service/argocd-repo-server ClusterIP 10.0.7.221 <none> 8081/TCP,8084/TCP 39s
service/argocd-server ClusterIP 10.0.149.92 <none> 80/TCP,443/TCP 39s
service/argocd-server-metrics ClusterIP 10.0.146.7 <none> 8083/TCP 39s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/argocd-applicationset-controller 1/1 1 1 39s
deployment.apps/argocd-dex-server 1/1 1 1 38s
deployment.apps/argocd-notifications-controller 1/1 1 1 38s
deployment.apps/argocd-redis 1/1 1 1 38s
deployment.apps/argocd-repo-server 1/1 1 1 37s
deployment.apps/argocd-server 1/1 1 1 37s
NAME DESIRED CURRENT READY AGE
replicaset.apps/argocd-applicationset-controller-75b78554fd 1 1 1 39s
replicaset.apps/argocd-dex-server-869fff9967 1 1 1 38s
replicaset.apps/argocd-notifications-controller-5b8dbb7c86 1 1 1 38s
replicaset.apps/argocd-redis-66d9777b78 1 1 1 38s
replicaset.apps/argocd-repo-server-7b8d97c767 1 1 1 37s
replicaset.apps/argocd-server-5c797497fb 1 1 1 37s
NAME READY AGE
statefulset.apps/argocd-application-controller 1/1 37s
Modify the service type to NodePort for argocd-server service and vote service and then add the port numbers as a inbound allow rule on the Node Instance’s NSG.
- Configure ArgoCD to connect with Azure Repo
-
First, get the secret from K8s secret resource
argocd-initial-admin-secret
in the argocd namespace and use the nodeIP:NodePort in U/I to load ArgoCD page. -
Next connect to the Azure Repo using Access Token:
-
Deploy AzureRepo manifests using ArgoCD to the AKS Cluster:
- Repo
- Create the Application
- ArgoCD Status
-
From kubectl perspective, all resources are deployed.
-
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
db-6d9f87bb9b-4ngfl 1/1 Running 0 2m18s
redis-77fccb7f9-69wgp 1/1 Running 0 2m18s
result-54b5ccfc95-xcbvc 1/1 Running 0 2m18s
vote-5655bd759-8ln2p 1/1 Running 0 2m18s
worker-7dd74bcbbb-d9slw 1/1 Running 0 2m18s
sanchit@FVFH81G2Q6LW ~/devopsprojects/azure-arm kubectl get all
NAME READY STATUS RESTARTS AGE
pod/db-6d9f87bb9b-4ngfl 1/1 Running 0 2m26s
pod/redis-77fccb7f9-69wgp 1/1 Running 0 2m26s
pod/result-54b5ccfc95-xcbvc 1/1 Running 0 2m26s
pod/vote-5655bd759-8ln2p 1/1 Running 0 2m26s
pod/worker-7dd74bcbbb-d9slw 1/1 Running 0 2m26s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/db ClusterIP 10.0.125.79 <none> 5432/TCP 2m26s
service/kubernetes ClusterIP 10.0.0.1 <none> 443/TCP 40m
service/redis ClusterIP 10.0.145.41 <none> 6379/TCP 2m26s
service/result NodePort 10.0.191.177 <none> 5001:31001/TCP 2m26s
service/vote NodePort 10.0.65.200 <none> 5000:31000/TCP 2m26s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/db 1/1 1 1 2m26s
deployment.apps/redis 1/1 1 1 2m26s
deployment.apps/result 1/1 1 1 2m26s
deployment.apps/vote 1/1 1 1 2m26s
deployment.apps/worker 1/1 1 1 2m26s
NAME DESIRED CURRENT READY AGE
replicaset.apps/db-6d9f87bb9b 1 1 1 2m26s
replicaset.apps/redis-77fccb7f9 1 1 1 2m26s
replicaset.apps/result-54b5ccfc95 1 1 1 2m26s
replicaset.apps/vote-5655bd759 1 1 1 2m26s
replicaset.apps/worker-7dd74bcbbb 1 1 1 2m26s
Vote U/I Loads as expected:
Now, at this point any change to the Azure Repo resources in k8s-specifications
, ArgoCD will pickup the change and apply it on the AKS cluster.
- Update operation
Now, we want to see what we need to do to ensure automatic updates to the pipeline and K8s resources if we make changes to vote application code to vote for “Coffee/Tea” instead of “Cats/Dogs”.
Before working on changing the pipelines for Update stage, let’s ensure our AKS cluster has authentication to pull images from our ACR.
$ kubectl create secret docker-registry sanchitazurecicd-acr-secret --docker-server sanchitazurecicd.azurecr.io --docker-username sanchitazurecicd --docker-password=<REDACTED>
secret/sanchitazurecicd-acr-secret created
Ensure that the required deployment YAMLs also has the above created secret set in the ImagePullSecret parameter.
Create a updateK8sManifest.sh
script in the same repo. Example shown below:
#!/bin/bash
set -x
# Set the repo URL
REPO_URL="https://<TOKEN>@dev.azure.com/<PROJECT>/voting-app/_git/voting-app"
# Clone the git repo into the /tmp directory
git clone "$REPO_URL" /tmp/temp_repo
# Navigate into the cloned repo directory
cd /tmp/temp_repo
# To change the image tag in a deployment.yaml file. $1 represents the service name. $2 is repository name. $3 is the build tag.
sed -i "s|image:.*|image: <ACR_LOGIN_SERVER>/$2:$3|g" k8s-specifications/$1-deployment.yaml
# Add the modified files
git add .
# Commit the changes
git commit -m "Updated Kubernetes manifest"
# Push the changes back into the repo
git push
# Cleanup: remove the tmp directory
rm -rf /tmp/temp_repo
Next, update the vote-service pipeline to include update stage with input as the script file with the arguments.
...
- stage: Update
displayName: Update
jobs:
- job: Update
displayName: Update
steps:
- task: ShellScript@2
inputs:
scriptPath: 'manifest-update-scripts/updateK8sManifest.sh'
args: 'vote $(imageRepository) $(tag)'
All 3 stages are successful on the new run for vote-service:
Now, we can see new vote deployment image tag got created in ACR votingapp:10
and the YAML file is also updated correctly and the new updated pod is running.
$ kubectl get pod vote-67b9fcd78b-kpr6s -o yaml | grep -i image
- image: sanchitazurecicd.azurecr.io/votingapp:10
imagePullPolicy: Always
Conclusion
Below picture (not very asthetic) summarizes in short the entire CI/CD workflow that we acheived. Hope this was helpful. Thank you for reading till the end!