Deploying a Node.js Express API to Kubernetes with Azure DevOps

Jan 1, 2025

This guide walks you through creating and deploying a Node.js Express API to Kubernetes, leveraging Docker, Kubernetes, and Azure DevOps. It assumes basic familiarity with the Azure CLI and Azure concepts. I have created a repository for reference to help follow along with the tutorial.

Prerequisites

  • Basic understanding of Azure CLI and Kubernetes
  • Installed and configured Azure CLI
  • Docker installed locally

Create a NodeJs Express API

  1. Install dependencies and initialize configuration:
npm i express
touch tsconfig.json
  1. Create the API logic (not included in this guide).

  2. Verify that your application runs as expected locally.

Create Application Docker Image

Verify that the docker image is built without issues.

docker build -t node-ts-api .

Configure Kubernetes Objects

Define the deployment object in

deployment.yml

apiVersion: apps/v1
kind: Deployment
metadata:
name: node-ts-api
spec:
replicas: 1
selector:
matchLabels:
app: node-ts-api
template:
metadata:
labels:
app: node-ts-api
spec:
containers:
- name: nodetsapi
image: devdeveloperregistry.azurecr.io/node-ts-api:latest
ports:
- containerPort: 3000

Define the service object in

service.yml
:

apiVersion: v1
kind: Service
metadata:
name: node-ts-api
spec:
type: LoadBalancer
ports:
- port: 8080
targetPort: 3000
selector:
app: node-ts-api

Setup Infastructure as Code (IAC) using Bicep Templates

Use Bicep templates to provision an Azure Container Registry, and Kubernetes cluster.

Create Template File This bicep template file contains instructions for provisioning our resources. You can find a list of other resources in the microsoft template documentation. I've reference the Microsoft Documentation link for each resource for your reference.

// General
param location string = resourceGroup().location
param resourceGroupName string = resourceGroup().name
param subscriptionId string = subscription().subscriptionId
// Kubernetes Cluster
param clusterName string = 'devdeveloper-aks-cluster'
param nodeSize string = 'Standard_A2_v2'
param nodeCount int = 1
param k8sVersion string = ''
// Container Registry
param containerRegistryName string = 'devdeveloperregistry'
// Reference: https://learn.microsoft.com/en-us/azure/templates/microsoft.containerregistry/registries?pivots=deployment-language-bicep
resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-12-01' = {
name: containerRegistryName
location: location
identity: {
type: 'SystemAssigned'
}
sku: {
name: 'Basic'
}
properties: {
adminUserEnabled: false
}
}
// Template Reference: https://learn.microsoft.com/en-us/azure/templates/microsoft.containerservice/managedclusters?pivots=deployment-language-bicep
// Manage Node Pools Reference: https://learn.microsoft.com/en-us/azure/aks/use-system-pools?tabs=azure-cli
resource devDeveloperCluster 'Microsoft.ContainerService/managedClusters@2024-09-01' = {
location: location
name: clusterName
sku: {
name: 'Base'
tier: 'Free'
}
identity: {
type: 'SystemAssigned'
}
properties: {
agentPoolProfiles: [
{
count: nodeCount
name: 'nodepool1'
osDiskSizeGB: 30
osType: 'Linux'
vmSize: nodeSize
mode: 'System'
}
]
dnsPrefix: 'minimalaks'
}
}
output aksPrincipalId string = aks.identity.principalId

Create the Role Assignments Bicep File Here we are writing the role assignments bicep file which will create a role assignment for our Kubernetes cluster to authenticate with the container registry to pull images.

@description('The AKS principal ID')
param aksPrincipalId string
// Declare the existing ACR resource
resource acr 'Microsoft.ContainerRegistry/registries@2022-09-01' existing = {
name: 'devdeveloperregistry'
}
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(acr.Id, aksPrincipalId, 'acrpull')
scope: acrId
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') // AcrPull role
principalId: aksPrincipalId
}
}

Deploy the Templates to Azure Here we are deploying our resources to Azure using the deployment command. We first create a resoure group to contain our resources (we can later delete the resource group and all it's containing resources later). Second, we deploy our resources referencing the

azuredeploy.bicep
file. Lastly, we create role assignments using our
roleassignments.bicep
file.

# Create Resource Group
az group create --name <resource-group-name> --location 'Central US'
# Deploy AKS and ACR
az deployment group create \
--resource-group <resource-group-name> \
--template-file azuredeploy.bicep
# Fetch outputs
az deployment group show \
--resource-group <resource-group-name> \
--name <deployment-name> \
--query properties.outputs -o json
# Deploy Role Assignment
az deployment group create \
--resource-group <resource-group-name> \
--template-file roleAssignment.bicep \
--parameters aksPrincipalId=<principle-id>

Setup Kubernetes Deployment Pipeline using Azure DevOps

This section covers deploying docker containers to our Kubernetes cluster using Azure Pipelines. This allows us to push continiously update our application images. This is achieved using a two staged pipeline defined in a YAML pipeline file. This file can be generated inside the Devops interface. Once your pipeline file is defined you can run the pipeline to deploy your applications.

  1. Stage One: Build Application and Update Azure Container Registry
  2. Stage Two: Deploy Applications to Kubernetes Cluster
trigger:
- master
resources:
- repo: self
variables:
# Container registry service connection established during pipeline creation
dockerRegistryServiceConnection: "f908d294-cc22-4ef7-9658-c29d3df43b12"
imageRepository: "node-ts-api"
containerRegistry: "devdeveloperregistry.azurecr.io"
dockerfilePath: "**/Dockerfile.dev"
tag: "$(Build.BuildId)"
imagePullSecret: "devdeveloperregistry8892c7e1-auth"
# Agent Pool Name
poolName: "Personal Laptop"
stages:
- stage: Build
displayName: Build stage
jobs:
- job: Build
displayName: Build
pool:
name: $(poolName)
steps:
- task: Docker@2
displayName: Build and push an image to container registry
inputs:
command: buildAndPush
repository: $(imageRepository)
dockerfile: $(dockerfilePath)
containerRegistry: $(dockerRegistryServiceConnection)
tags: |
$(tag)
- upload: manifests
artifact: manifests
- stage: Deploy
displayName: Deploy stage
dependsOn: Build
jobs:
- deployment: Deploy
displayName: Deploy
pool:
name: $(poolName)
environment: "barnacleDevelopmentskubernetestest-1499.default"
strategy:
runOnce:
deploy:
steps:
- task: KubernetesManifest@0
displayName: Create imagePullSecret
inputs:
action: createSecret
secretName: $(imagePullSecret)
dockerRegistryEndpoint: $(dockerRegistryServiceConnection)
- task: KubernetesManifest@0
displayName: Deploy to Kubernetes cluster
inputs:
action: deploy
manifests: |
$(Pipeline.Workspace)/manifests/deployment.yml
$(Pipeline.Workspace)/manifests/service.yml
imagePullSecrets: |
$(imagePullSecret)
containers: |
$(containerRegistry)/$(imageRepository):$(tag)

Hit deployed API

  1. Get the LoadBalancer IP Address Use the following command to retrieve the external IP address of your deployed API's LoadBalancer service:
kubectl get services
Look for the EXTERNAL-IP field of the node-ts-api service.
  1. Access the API Copy the LoadBalancer IP address and paste it into your browser, followed by the appropriate endpoint (e.g., /api or /). For example:
http://<EXTERNAL-IP>:8080

References

  1. Deploying a Kubernetes Cluster to Azure Pipelines
  2. Azure Bicep Templates
  3. Microsoft.ContainerRegistry/registries Bicep Template Reference
  4. Microsoft.ContainerService/managedClusters Bicep Template Reference
  5. Microsoft.Sql/servers Bicep Template Reference
  6. Microsoft.Sql/servers/databases Bicep Template Reference
  7. Self-Hosted Azure Agents
  8. Azure DevOps Kubernetes Lab