Tutorial: Use a canary deployment strategy for Kubernetes
Azure DevOps Services | Azure DevOps Server 2022
This step-by-step guide covers how to use the Kubernetes manifest task with the canary
strategy. A canary deployment strategy deploys new versions of an application next to stable, production versions.
You use the associated workflow to deploy the code and compare the baseline and canary app deployments. Based on the evaluation, you decide whether to promote or reject the canary deployment.
This tutorial uses Docker Registry and Azure Resource Manager service connections to connect to Azure resources. For an Azure Kubernetes Service (AKS) private cluster or a cluster that has local accounts disabled, an Azure Resource Manager service connections is a better way to connect.
Prerequisites
An Azure DevOps project with at least User permissions.
An Azure account. Create an account for free.
An Azure Container Registry instance with push privileges.
An Azure Kubernetes Service (AKS) cluster deployed. You can attach the AKS cluster to your Azure Container registry cluster when you deploy the AKS cluster or afterwards.
A GitHub account. Create a free GitHub account.
A fork of the https://github.com/MicrosoftDocs/azure-pipelines-canary-k8s GitHub repository.
Important
During the following procedures, you might be prompted to create a GitHub service connection or be redirected to GitHub to sign in, install Azure Pipelines, or authorize Azure Pipelines. Follow the onscreen instructions to complete the process. For more information, see Access to GitHub repositories.
GitHub repository files
The GitHub repository contains the following files:
File | Description |
---|---|
./app/app.py | A simple, Flask-based web server . The file sets up a custom counter for the number of good and bad responses, based on the value of the success_rate variable. |
./app/Dockerfile | Used for building the image with each change to app.py. Each change triggers the build pipeline to build the image and push it to the container registry. |
./manifests/deployment.yml | Contains the specification of the sampleapp deployment workload corresponding to the published image. You use this manifest file for the stable version of the deployment object and for deriving the baseline and canary variants of the workloads. |
./manifests/service.yml | Creates the sampleapp service. This service routes requests to the pods spun up by the stable, baseline, and canary deployments. |
./misc/fortio.yml | Sets up a fortio deployment. This deployment is a load-testing tool that sends a stream of requests to the deployed sampleapp service. The request stream routes to pods under the three deployments: stable, baseline, and canary. |
Create service connections
- In your Azure DevOps project, go to Project settings > Pipelines > Service connections.
- Create a Docker Registry service connection named azure-pipelines-canary-acr that's associated with your Azure Container Registry instance.
- Create a Azure Resource Manager service connection with workload identity named azure-pipelines-canary-k8s for your resource group.
Add the build stage
In your Azure DevOps project, go to Pipelines > Create Pipeline or New pipeline.
Select GitHub for your code location, and select your forked azure-pipelines-canary-k8s repository.
On the Configure tab, choose Starter pipeline.
On the Review tab, replace the pipeline YAML with the following code.
trigger: - main pool: vmImage: ubuntu-latest variables: imageName: azure-pipelines-canary-k8s # name of ACR image dockerRegistryServiceConnection: azure-pipelines-canary-acr # name of ACR service connection imageRepository: 'azure-pipelines-canary-k8s' # name of image repostory containerRegistry: example.azurecr.io # name of Azure container registry tag: '$(Build.BuildId)' stages: - stage: Build displayName: Build stage jobs: - job: Build displayName: Build pool: vmImage: ubuntu-latest steps: - task: Docker@2 displayName: Build and push image inputs: containerRegistry: $(dockerRegistryServiceConnection) repository: $(imageName) command: buildAndPush Dockerfile: app/Dockerfile tags: | $(tag)
If the Docker registry service connection that you created is associated with a container registry named
example.azurecr.io
, then the image is set toexample.azurecr.io/azure-pipelines-canary-k8s:$(Build.BuildId)
.Select Save and run and ensure the job runs successfully.
Edit the manifest file
In your repository fork, edit manifests/deployment.yml to replace <foobar>
with your container registry's URL, for example example.azurecr.io/azure-pipelines-canary-k8s
.
Set up continuous deployment
Now, set up continuous deployment, deploy the canary stage, and promote or reject the canary through manual approval.
Create an environment
You can deploy with YAML or Classic.
- In your Azure DevOps project, go to Pipelines > Environments and then select Create environment or New environment.
- On the first New environment screen, enter akscanary under Name, select Kubernetes under Resource, and select Next.
- Fill out the Kubernetes resource screen as follows:
- Provider: Select Azure Kubernetes Service.
- Azure subscription: Select your Azure subscription.
- Cluster: Select your AKS cluster.
- Namespace: Select New and enter canarydemo.
- Select Validate and create.
Add the canary stage
Go to Pipelines, select the pipeline you created, and select Edit.
Replace the entire pipeline YAML with the following code.
This code changes the
Docker@2
step you ran previously to use a stage, and adds two more steps to copy the manifests and misc directories as artifacts for consecutive stages to use.The code also moves some values to variables for easier usage later in the pipeline. In the
containerRegistry
variable, replace<example>
with the name of your container registry.trigger: - main pool: vmImage: ubuntu-latest variables: imageName: azure-pipelines-canary-k8s dockerRegistryServiceConnection: azure-pipelines-canary-acr imageRepository: 'azure-pipelines-canary-k8s' containerRegistry: <example>.azurecr.io tag: '$(Build.BuildId)' stages: - stage: Build displayName: Build stage jobs: - job: Build displayName: Build pool: vmImage: ubuntu-latest steps: - task: Docker@2 displayName: Build and push image inputs: containerRegistry: $(dockerRegistryServiceConnection) repository: $(imageName) command: buildAndPush Dockerfile: app/Dockerfile tags: | $(tag) - publish: manifests artifact: manifests - publish: misc artifact: misc
Add another stage at the end of the YAML file to deploy the canary version. Replace the values
my-resource-group
andmy-aks-cluster
with your resource group and Azure Kubernetes Service cluster name.trigger: - main pool: vmImage: ubuntu-latest variables: imageName: azure-pipelines-canary-k8s dockerRegistryServiceConnection: azure-pipelines-canary-acr imageRepository: 'azure-pipelines-canary-k8s' containerRegistry: yourcontainerregistry.azurecr.io #update with container registry tag: '$(Build.BuildId)' stages: - stage: Build displayName: Build stage jobs: - job: Build displayName: Build pool: vmImage: ubuntu-latest steps: - task: Docker@2 displayName: Build and push image inputs: containerRegistry: $(dockerRegistryServiceConnection) repository: $(imageName) command: buildAndPush Dockerfile: app/Dockerfile tags: | $(tag) - publish: manifests artifact: manifests - publish: misc artifact: misc - stage: DeployCanary displayName: Deploy canary dependsOn: Build condition: succeeded() jobs: - deployment: Deploycanary displayName: Deploy canary pool: vmImage: ubuntu-latest environment: 'akscanary' strategy: runOnce: deploy: steps: - task: KubernetesManifest@1 displayName: Create Docker Registry Secret inputs: action: 'createSecret' connectionType: 'azureResourceManager' azureSubscriptionConnection: 'azure-pipelines-canary-sc' azureResourceGroup: 'my-resource-group' kubernetesCluster: 'my-aks-cluster' secretType: 'dockerRegistry' secretName: 'my-acr-secret' dockerRegistryEndpoint: 'azure-pipelines-canary-acr' - task: KubernetesManifest@1 displayName: Deploy to Kubernetes cluster inputs: action: 'deploy' connectionType: 'azureResourceManager' azureSubscriptionConnection: 'azure-pipelines-canary-sc' azureResourceGroup: 'my-resource-group' kubernetesCluster: 'my-aks-cluster' strategy: 'canary' percentage: '25' manifests: | $(Pipeline.Workspace)/manifests/deployment.yml $(Pipeline.Workspace)/manifests/service.yml containers: '$(containerRegistry)/$(imageRepository):$(tag)' imagePullSecrets: 'my-acr-secret' - task: KubernetesManifest@1 displayName: Deploy Forbio to Kubernetes cluster inputs: action: 'deploy' connectionType: 'azureResourceManager' azureSubscriptionConnection: 'azure-pipelines-canary-sc' azureResourceGroup: 'my-resource-group' kubernetesCluster: 'my-aks-cluster' manifests: '$(Pipeline.Workspace)/misc/*'
Select Validate and save, and save the pipeline directly to the main branch.
Add manual approval for promoting or rejecting canary deployment
You can intervene manually with YAML or Classic.
- Create a new Kubernetes environment called akspromote.
- Open the new akspromote environment from the list of environments, and select Approvals on the Approvals and checks tab.
- On the Approvals screen, add your own user account under Approvers.
- Expand Advanced, and make sure Allow approvers to approve their own runs is selected.
- Select Create.
Add promote and reject stages to the pipeline
Go to Pipelines, select the pipeline you created, and select Edit.
Add the following
PromoteRejectCanary
stage at the end of your YAML file that promotes the changes.- stage: PromoteRejectCanary displayName: Promote or Reject canary dependsOn: DeployCanary condition: succeeded() jobs: - deployment: PromoteCanary displayName: Promote Canary pool: vmImage: ubuntu-latest environment: 'akspromote' strategy: runOnce: deploy: steps: - task: KubernetesManifest@1 displayName: Create Docker Registry Secret for akspromote inputs: action: 'createSecret' connectionType: 'azureResourceManager' azureSubscriptionConnection: 'azure-pipelines-canary-sc' azureResourceGroup: 'my-resource-group' kubernetesCluster: 'my-aks-cluster' secretType: 'dockerRegistry' secretName: 'my-acr-secret' dockerRegistryEndpoint: 'azure-pipelines-canary-acr' - task: KubernetesManifest@1 displayName: promote canary inputs: action: 'promote' connectionType: 'azureResourceManager' azureSubscriptionConnection: 'azure-pipelines-canary-sc' azureResourceGroup: 'my-resource-group' kubernetesCluster: 'my-aks-cluster' strategy: 'canary' manifests: '$(Pipeline.Workspace)/manifests/*' containers: '$(containerRegistry)/$(imageRepository):$(tag)' imagePullSecrets: 'my-acr-secret' ```
Add the following
RejectCanary
stage at the end of the file that rolls back the changes.- stage: RejectCanary displayName: Reject canary dependsOn: PromoteRejectCanary condition: failed() jobs: - deployment: RejectCanary displayName: Reject Canary pool: vmImage: ubuntu-latest environment: 'akscanary' strategy: runOnce: deploy: steps: - task: KubernetesManifest@1 displayName: Create Docker Registry Secret for reject canary inputs: action: 'createSecret' connectionType: 'azureResourceManager' azureSubscriptionConnection: 'azure-pipelines-canary-sc' azureResourceGroup: 'kubernetes-testing' kubernetesCluster: 'my-aks-cluster' secretType: 'dockerRegistry' secretName: 'my-acr-secret' dockerRegistryEndpoint: 'azure-pipelines-canary-acr' - task: KubernetesManifest@1 displayName: Reject canary deployment inputs: action: 'reject' connectionType: 'azureResourceManager' azureSubscriptionConnection: 'azure-pipelines-canary-sc' azureResourceGroup: 'my-resource-group' kubernetesCluster: 'my-aks-cluster' namespace: 'default' strategy: 'canary' manifests: '$(Pipeline.Workspace)/manifests/*' ```
Select Validate and save, and save the pipeline directly to the main branch.
Deploy a stable version
For the first run of the pipeline, the stable version of the workloads, and their baseline or canary versions, don't exist in the cluster. Deploy a stable version of the sampleapp
workload as follows.
You can deploy a stable version with YAML or Classic.
- In app/app.py, change
success_rate = 50
tosuccess_rate = 100
. This change triggers the pipeline, building and pushing the image to the container registry, and also triggers theDeployCanary
stage. - Because you configured an approval on the
akspromote
environment, the release waits before running that stage. On the build run summary page, select Review and then select Approve.
Once approved, the pipeline deploys the stable version of the sampleapp
workload in manifests/deployment.yml to the namespace.
Initiate the canary workflow and reject the approval
The stable version of the sampleapp
workload now exists in the cluster. Next, make the following change to the simulation application.
- In app/app.py, change
success_rate = 50
tosuccess_rate = 100
. This change triggers the pipeline, building and pushing the image to the container registry, and also triggers theDeployCanary
stage. - Because you configured an approval on the
akspromote
environment, the release waits before running that stage. - On the build run summary page, select Review and then select Reject in the subsequent dialog box. This rejects deployment.
Once rejected, the pipeline prevents the code deployment.
Clean up
If you're not going to continue to use this application, delete the resource group in Azure portal and the project in Azure DevOps.