An Introduction to Microsoft Azure and Kubernetes using Helm and Docker Images<!-- --> | <!-- -->Patrick Desjardins Blog
Patrick Desjardins Blog
Patrick Desjardins picture from a conference

An Introduction to Microsoft Azure and Kubernetes using Helm and Docker Images

Posted on: August 5, 2022

Kubernetes is a convenient way to deploy an infrastructure of services in a central place. Once you have your service Docker images built and deployed you can use Kubernetes to deploy multiple instances across the cloud. In this article, we will see how Microsoft Azure can use a Kubernetes configuration generated with Helm and deploy many images from Azure Registry Container.

Create an Azure Kubernetes Service (AKS)

Few informations is required that was build when we created our Azure Docker Image Repository. You need to use the Azure resource group name after --resource-group. The name of the Kubernetes service is after --name. The --attach-acr is the name of the Azure Container Registry (acr).

1az aks create --resource-group realtimepixel_resourcegroup --name realpixelask --location eastus --attach-acr realtimepixel --generate-ssh-keys

There is few things to know:

  1. The name cannot have underscore. Even if you enclose with double quote it does not work
  2. The command takes a while to run. Expect at least 1 minute.

Most online tutorial shows to use the Azure CLI (az command) but it is also possible to create the Kubernetes service on the Microsoft Azure Portal.

azure kbs creation

Configure Github Workflow

You can edit the existing Github workflow defined previously that was building the image and pushing it into Azure Container Registry (ACR). However, I decided to create a new workflow allowing me to decide manually when to push the image into production.

Here is the full Github workflow that I store into the repository in .github/workflows/k8sdeploy.yml

2 workflow_dispatch:
4# Environment variables available to all jobs and steps in this workflow
6 REGISTRY_NAME: realtimepixel
7 CLUSTER_NAME: realpixelask
8 CLUSTER_RESOURCE_GROUP: realtimepixel_resourcegroup
9 NAMESPACE: realtimepixel-prod
12 build:
13 runs-on: ubuntu-latest
14 steps:
15 - uses: actions/checkout@main
17 # Set the target Azure Kubernetes Service (AKS) cluster.
18 - uses: azure/aks-set-context@v1
19 with:
20 creds: ${{ secrets.AZURE_CREDENTIALS }}
21 cluster-name: ${{ env.CLUSTER_NAME }}
22 resource-group: ${{ env.CLUSTER_RESOURCE_GROUP }}
24 # Create namespace if doesn't exist
25 - run: |
26 kubectl create namespace ${{ env.NAMESPACE }} --dry-run=client -o json | kubectl apply -f -
28 - name: Helm tool installer
29 uses: Azure/setup-helm@v1
31 - name: Azure Login
32 uses: Azure/login@v1.1
33 with:
34 creds: ${{ secrets.AZURE_CREDENTIALS }}
36 - name: Get Latest Tag Redis
37 id: latesttagredis
38 run: |
39 tag_redis=$(az acr repository show-tags --name ${{env.REGISTRY_NAME}} --repository realtimepixel_redis --top 1 --orderby time_desc -o tsv)
40 echo "::set-output name=tag_redis::$tag_redis"
42 - name: Tag Redis
43 run: echo "Tag Redis is ${{ steps.latesttagredis.outputs.tag_redis }}"
45 - name: Get Latest Tag Backend
46 id: latesttagbackend
47 run: |
48 tag_backend=$(az acr repository show-tags --name ${{env.REGISTRY_NAME}} --repository realtimepixel_backend --top 1 --orderby time_desc -o tsv)
49 echo "::set-output name=tag_backend::$tag_backend"
51 - name: Tag Backend
52 run: echo "Tag Backend is ${{ steps.latesttagbackend.outputs.tag_backend }}"
54 - name: Get Latest Tag Frontend
55 id: latesttagfrontend
56 run: |
57 tag_frontend=$(az acr repository show-tags --name ${{env.REGISTRY_NAME}} --repository realtimepixel_frontend --top 1 --orderby time_desc -o tsv)
58 echo "::set-output name=tag_frontend::$tag_frontend"
60 - name: Tag Frontend
61 run: echo "Tag Frontend is ${{ steps.latesttagfrontend.outputs.tag_frontend }}"
63 - name: Deploy
64 run: >
65 helm upgrade realtimepixel ./kubernetes/realtimepixel
66 --install
67 --namespace=${{ env.NAMESPACE }}
68 --set namespace=${{env.NAMESPACE}}
69 --set image.pullpolicy=IfNotPresent
70 --set image.redis.repository=${{env.REGISTRY_NAME}}
71 --set image.redis.tag=${{ steps.latesttagredis.outputs.tag_redis }}
72 --set image.backend.repository=${{env.REGISTRY_NAME}}
73 --set image.backend.tag=${{ steps.latesttagbackend.outputs.tag_backend }}
74 --set image.frontend.repository=${{env.REGISTRY_NAME}}
75 --set image.frontend.tag=${{ steps.latesttagfrontend.outputs.tag_frontend }}

Here is a description of what is going on:

  1. The first section called env are variable that can be used across the whole workflow. It is a simple way to configure data in a central place for the script. It defined the registry (Azure Container Registry) name, the Kubernetes cluster name created in this blog post, the Azure resource group (previous article) and the Kubernetes namespace.

  2. The second section connect to AKS: Azure Kubernetes Service

  3. We create the namespace

  4. We then connect with Helm and Azure Login. From now, we are ready to perform some commands

  5. First, we get the latest image tag for each image

  6. Finally, we use helm to install or update the Kubernetes configuration. What is important is to override a lot of values from kubernetes\realtimepixel\values.yaml (Helm values file)


At this point, the Helm command pushes the instruction to Azure Kubernetes Service. Going in the portal you can see under Workloads the deployment.

azure k8s workloads

Everything should be running as expected! The screenshot shows three orange symbols next to my three deployments because there is an issue with the Kubernetes configuration which is outside the goal of this post. However, it is still interesting to know that you can drill and see the error reason being: ErrImageNeverPull.

Debug ErrImageNeverPull

1kubectl get pods -n realtimepixel-prod
2kubectl describe pod redis-deployment-6495cd48cc-fhzjg -n realtimepixel-prod

The last command gives more information saying the policy is to Never

2Type Reason Age From Message
3---- ------ ---- ---- -------
4Normal Scheduled 59m default-scheduler Successfully assigned realtimepixel-prod/redis-deployment-6495cd48cc-fhzjg to aks-agentpool-28884595-vmss000002
5Warning Failed 57m (x12 over 59m) kubelet Error: ErrImageNeverPull
6Warning ErrImageNeverPull 4m36s (x260 over 59m) kubelet Container image "" is not present with pull policy of Never

Trying to see what is really sent to Kubernetes using the template command:

1helm template realtimepixel ./kubernetes/realtimepixel --set namespace=realtimepixel-prod --set image.pullPolicy=Always --set --set image.redis.tag=123123 --set --set image.backend.tag=123123 --set --set image.frontend.tag=123123 > temp.yaml

I found a case sensitive issue with the image.pullPolicy.

Debug CrashLoopBackOff

There is a command to get the health of all your pods. In my case, some of them were crashing:

1kubectl get pods -n realtimepixel-prod

Resulted with:

2backend-deployment-69c99548d9-g2w5d 0/1 CrashLoopBackOff 10 (3m28s ago) 30m
3backend-deployment-69c99548d9-wrhzp 0/1 CrashLoopBackOff 10 (3m42s ago) 30m
4backend-deployment-7dfbc4f7f8-m99kl 0/1 CrashLoopBackOff 10 (3m51s ago) 109m
5backend-deployment-7dfbc4f7f8-vbp24 0/1 CrashLoopBackOff 10 (4m4s ago) 109m
6frontend-deployment-6f88fdb587-2p2lk 1/1 Running 0 30m
7redis-deployment-5d48cc44bd-8w869 1/1 Running 0 30m

kubectl describe pod backend-deployment-69c99548d9-g2w5d -n realtimepixel-prod

kubectl logs backend-deployment-69c99548d9-g2w5d -n realtimepixel-prod

1> start:production
2> node -r ts-node/register/transpile-only -r tsconfig-paths/register build/backend/src/index.js
4Error: Cannot find module '/node/build/backend/src/index.js'
5 at Function.Module._resolveFilename (node:internal/modules/cjs/loader:933:15)
6 at Function.Module._resolveFilename.sharedData.moduleResolveFilenameHook.installedValue (/node/node_modules/@cspotcode/source-map-support/source-map-support.js:679:30)
7 at Function.Module._resolveFilename (/node/node_modules/tsconfig-paths/src/register.ts:90:36)
8 at Function.Module._load (node:internal/modules/cjs/loader:778:27)
9 at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
10 at node:internal/main/run_main_module:17:47 {
11 code: 'MODULE_NOT_FOUND',
12 requireStack: []
1kubectl exec -it backend-deployment-69c99548d9-g2w5d -n realtimepixel-prod -- sh

But was producing:

1error: unable to upgrade connection: container not found...

You can test it by creating a Pod for the problematic image that will not restart using the following commands:

1kubectl run debug-demo -n realtimepixel-prod --restart=Never
2kubectl get pods -n realtimepixel-prod
3kubectl exec -it debug-demo -n realtimepixel-prod -- sh

However, the problem will remain that when the Kube Control runs the image that it will crash. However, the log above is providing good information. In my case I realized two things:

  1. When testing locally, I wasn't testing properly. The build was passing because I had a node_modules with a dependency that was installed when performing npm run install which was adding all the devDependencies. On Github, performing the same command, with the NODE_ENV to production was causing npm install to install only the dependencies without the devDependencies.
  2. I added the --target in the build to ensure that only the production multi-stage is executed

Still Not Working!

1docker build -f ./services/backend/Dockerfile --target production --build-arg NODE_ENV=production .
2docker images
3docker run 40d4941a9092
4docker ps

Take the image id from the ps command:

1docker run -it 40d4941a9092 bash

I could see the error. Now time to make the container not crash but to get into:

1docker run 40d4941a9092 /bin/sh -c "while true; do sleep 2; df -h; done"

In another console:

1docker run -it 40d4941a9092 bash

At that point, I saw that the build was messing around the folders of the ouput of TypeScript. I modified the path and was good to go.


At this point, the image was created with a build that was successful.

azure kubernetes deployement

I would say that the experience was enriching. However, one question kept getting in the back of my mind: why isn't the Azure Portal guiding me with more than a single keyword for the failure. As we saw, by messing around with commands, we found the root cause, but some support would have been great. Nonetheless, if something happens to you, now you should be equipped to diagnose a little bit better.