Deploying a Node.js Express API to Kubernetes with Azure DevOps
Jan 1, 2025This 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
- Install dependencies and initialize configuration:
npm i expresstouch tsconfig.json
-
Create the API logic (not included in this guide).
-
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/v1kind: Deploymentmetadata:name: node-ts-apispec:replicas: 1selector:matchLabels:app: node-ts-apitemplate:metadata:labels:app: node-ts-apispec:containers:- name: nodetsapiimage: devdeveloperregistry.azurecr.io/node-ts-api:latestports:- containerPort: 3000
Define the service object in
:service.yml
apiVersion: v1kind: Servicemetadata:name: node-ts-apispec:type: LoadBalancerports:- port: 8080targetPort: 3000selector: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.
// Generalparam location string = resourceGroup().locationparam resourceGroupName string = resourceGroup().nameparam subscriptionId string = subscription().subscriptionId// Kubernetes Clusterparam clusterName string = 'devdeveloper-aks-cluster'param nodeSize string = 'Standard_A2_v2'param nodeCount int = 1param k8sVersion string = ''// Container Registryparam containerRegistryName string = 'devdeveloperregistry'// Reference: https://learn.microsoft.com/en-us/azure/templates/microsoft.containerregistry/registries?pivots=deployment-language-bicepresource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-12-01' = {name: containerRegistryNamelocation: locationidentity: {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-cliresource devDeveloperCluster 'Microsoft.ContainerService/managedClusters@2024-09-01' = {location: locationname: clusterNamesku: {name: 'Base'tier: 'Free'}identity: {type: 'SystemAssigned'}properties: {agentPoolProfiles: [{count: nodeCountname: 'nodepool1'osDiskSizeGB: 30osType: 'Linux'vmSize: nodeSizemode: '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 resourceresource 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: acrIdproperties: {roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') // AcrPull roleprincipalId: 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
file. Lastly, we create role assignments using ourazuredeploy.bicep
file.roleassignments.bicep
# Create Resource Groupaz group create --name <resource-group-name> --location 'Central US'# Deploy AKS and ACRaz deployment group create \--resource-group <resource-group-name> \--template-file azuredeploy.bicep# Fetch outputsaz deployment group show \--resource-group <resource-group-name> \--name <deployment-name> \--query properties.outputs -o json# Deploy Role Assignmentaz 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.
- Stage One: Build Application and Update Azure Container Registry
- Stage Two: Deploy Applications to Kubernetes Cluster
trigger:- masterresources:- repo: selfvariables:# Container registry service connection established during pipeline creationdockerRegistryServiceConnection: "f908d294-cc22-4ef7-9658-c29d3df43b12"imageRepository: "node-ts-api"containerRegistry: "devdeveloperregistry.azurecr.io"dockerfilePath: "**/Dockerfile.dev"tag: "$(Build.BuildId)"imagePullSecret: "devdeveloperregistry8892c7e1-auth"# Agent Pool NamepoolName: "Personal Laptop"stages:- stage: BuilddisplayName: Build stagejobs:- job: BuilddisplayName: Buildpool:name: $(poolName)steps:- task: Docker@2displayName: Build and push an image to container registryinputs:command: buildAndPushrepository: $(imageRepository)dockerfile: $(dockerfilePath)containerRegistry: $(dockerRegistryServiceConnection)tags: |$(tag)- upload: manifestsartifact: manifests- stage: DeploydisplayName: Deploy stagedependsOn: Buildjobs:- deployment: DeploydisplayName: Deploypool:name: $(poolName)environment: "barnacleDevelopmentskubernetestest-1499.default"strategy:runOnce:deploy:steps:- task: KubernetesManifest@0displayName: Create imagePullSecretinputs:action: createSecretsecretName: $(imagePullSecret)dockerRegistryEndpoint: $(dockerRegistryServiceConnection)- task: KubernetesManifest@0displayName: Deploy to Kubernetes clusterinputs:action: deploymanifests: |$(Pipeline.Workspace)/manifests/deployment.yml$(Pipeline.Workspace)/manifests/service.ymlimagePullSecrets: |$(imagePullSecret)containers: |$(containerRegistry)/$(imageRepository):$(tag)
Hit deployed API
- Get the LoadBalancer IP Address Use the following command to retrieve the external IP address of your deployed API's LoadBalancer service:
kubectl get servicesLook for the EXTERNAL-IP field of the node-ts-api service.
- 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
- Deploying a Kubernetes Cluster to Azure Pipelines
- Azure Bicep Templates
- Microsoft.ContainerRegistry/registries Bicep Template Reference
- Microsoft.ContainerService/managedClusters Bicep Template Reference
- Microsoft.Sql/servers Bicep Template Reference
- Microsoft.Sql/servers/databases Bicep Template Reference
- Self-Hosted Azure Agents
- Azure DevOps Kubernetes Lab