Introduction
This documentation outlines the step-by-step process for deploying a sample application, in this case, Weather Dashboard, using Azure cloud services. The goal is to containerize the application, push it to Azure Container Registry (ACR), and deploy it on Azure Kubernetes Service (AKS). This deployment will demonstrate how to manage configurations, secrets, persistent storage, and health checks in a cloud-native environment using Kubernetes best practices
Prerequisites:
- Azure CLI installed and configured
- kubectl installed and configured to connect to your AKS cluster
- Docker Installed
- Basic Understanding of Kubernetes
Step 1: Create a Weather Dashboard Application
1. First, create the project structure:
mkdir -p weather-dashboard/{public,src,data}
cd weather-dashboard
2. Create a file named package.json in this directory:
{
"name": "weather-dashboard",
"version": "1.0.0",
"description": "Interactive weather dashboard with persistent storage",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2",
"moment": "^2.29.4",
"body-parser": "^1.20.2"
}
}
These are the dependencies required for our application to run
3. Create server.js in the application's root directory:
This initializes an Express server that serves the frontend, handles API routes for saving and retrieving weather search history, and reads configuration from environment variables.
const express = require('express');
const fs = require('fs');
const path = require('path');
const bodyParser = require('body-parser');
const moment = require('moment');
const app = express();
const port = process.env.PORT || 3000;
const dataDir = '/app/data';
// Middleware
app.use(express.static('public'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// Read configuration from ConfigMap
const apiEndpoint = process.env.WEATHER_API_ENDPOINT || 'https://api.example.com';
const refreshInterval = process.env.REFRESH_INTERVAL || '30';
const defaultCity = process.env.DEFAULT_CITY || 'London';
// Read API key from Secret
const apiKey = process.env.WEATHER_API_KEY || 'default-demo-key';
// Ensure data directory exists
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
console.log(`Created data directory at ${dataDir}`);
}
// Initialize history file if it doesn't exist
const historyFile = path.join(dataDir, 'search-history.json');
if (!fs.existsSync(historyFile)) {
fs.writeFileSync(historyFile, JSON.stringify([], null, 2));
}
// API endpoint to get configuration
app.get('/api/config', (req, res) => {
res.json({
apiEndpoint,
refreshInterval,
defaultCity,
hasApiKey: !!apiKey && apiKey !== 'default-demo-key',
serverTime: new Date().toISOString()
});
});
// API endpoint to save search
app.post('/api/history', (req, res) => {
const { city } = req.body;
if (!city) {
return res.status(400).json({ error: 'City name is required' });
}
try {
const history = JSON.parse(fs.readFileSync(historyFile, 'utf8'));
const newEntry = {
city,
timestamp: moment().format('YYYY-MM-DD HH:mm:ss')
};
history.unshift(newEntry);
// Keep only the last 10 entries
const updatedHistory = history.slice(0, 10);
fs.writeFileSync(historyFile, JSON.stringify(updatedHistory, null, 2));
res.json({ success: true, history: updatedHistory });
} catch (error) {
console.error('Error saving to history:', error);
res.status(500).json({ error: 'Failed to save search history' });
}
});
// API endpoint to get search history
app.get('/api/history', (req, res) => {
try {
const history = JSON.parse(fs.readFileSync(historyFile, 'utf8'));
res.json(history);
} catch (error) {
console.error('Error reading history:', error);
res.status(500).json({ error: 'Failed to read search history' });
}
});
// Start the server
app.listen(port, () => {
console.log(`Weather Dashboard running at http://localhost:${port}`);
console.log(`API Endpoint: ${apiEndpoint}`);
console.log(`Default city: ${defaultCity}`);
console.log(`Refresh interval: ${refreshInterval} minutes`);
});
4. Create public/index.html:
The main HTML page that provides the structure for the weather dashboard user interface.
Weather Dashboard
Weather Dashboard
Server time: --
Search
Current Weather
--
--°C
--
--
Search History
No search history yet
Configuration
Default City: --
Refresh Interval: -- minutes
API Key Status: --
Enter fullscreen mode
Exit fullscreen mode
5. Create public/styles.css:
Contains all styling rules and layout definitions for making the weather dashboard clean, responsive, and visually appealing.
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background-color: #f5f5f5;
color: #333;
}
.container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #ddd;
}
.server-info {
font-size: 14px;
color: #666;
}
.search-container {
display: flex;
margin-bottom: 20px;
}
input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px 0 0 4px;
font-size: 16px;
}
button {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 0 4px 4px 0;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
button:hover {
background-color: #45a049;
}
.dashboard {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.current-weather, .search-history, .config-info {
background-color: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h2, h3 {
margin-bottom: 15px;
color: #333;
}
.weather-display {
text-align: center;
}
#city-name {
font-size: 24px;
font-weight: bold;
margin-bottom: 10px;
}
#temperature {
font-size: 48px;
font-weight: bold;
margin-bottom: 10px;
}
#description {
font-size: 18px;
margin-bottom: 10px;
text-transform: capitalize;
}
#last-updated {
font-size: 14px;
color: #666;
}
.config-info ul {
list-style: none;
}
.config-info li {
margin-bottom: 8px;
}
#history-list div {
padding: 10px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
}
#history-list div:last-child {
border-bottom: none;
}
.city-history {
font-weight: bold;
}
.timestamp {
color: #666;
font-size: 14px;
}
@media (max-width: 768px) {
.dashboard {
grid-template-columns: 1fr;
}
}
Enter fullscreen mode
Exit fullscreen mode
6. Create public/app.js:
Handles frontend logic: fetching configuration and search history, updating the UI, and simulating weather data responses.
document.addEventListener('DOMContentLoaded', () => {
const cityInput = document.getElementById('city-input');
const searchBtn = document.getElementById('search-btn');
const cityName = document.getElementById('city-name');
const temperature = document.getElementById('temperature');
const description = document.getElementById('description');
const lastUpdated = document.getElementById('last-updated');
const historyList = document.getElementById('history-list');
const serverTime = document.getElementById('server-time');
const defaultCity = document.getElementById('default-city');
const refreshInterval = document.getElementById('refresh-interval');
const apiKeyStatus = document.getElementById('api-key-status');
let config = {};
// Load configuration
fetch('/api/config')
.then(response => response.json())
.then(data => {
config = data;
serverTime.textContent = new Date(data.serverTime).toLocaleString();
defaultCity.textContent = data.defaultCity;
refreshInterval.textContent = data.refreshInterval;
apiKeyStatus.textContent = data.hasApiKey ? 'Configured ✓' : 'Not Configured ✗';
// Load default city
simulateWeatherData(data.defaultCity);
})
.catch(error => console.error('Error loading config:', error));
// Load search history
function loadHistory() {
fetch('/api/history')
.then(response => response.json())
.then(history => {
if (history.length === 0) {
historyList.innerHTML = 'No search history yet';
return;
}
historyList.innerHTML = '';
history.forEach(entry => {
const historyItem = document.createElement('div');
historyItem.innerHTML = `
${entry.city}
${entry.timestamp}
`;
historyList.appendChild(historyItem);
// Make history items clickable
historyItem.addEventListener('click', () => {
simulateWeatherData(entry.city);
});
});
})
.catch(error => console.error('Error loading history:', error));
}
loadHistory();
// Simulate weather data (in a real app, this would call an actual weather API)
function simulateWeatherData(city) {
// Update UI immediately
cityName.textContent = city;
const randomTemp = Math.floor(Math.random() * 35 - 5);
temperature.textContent = `${randomTemp}°C`;
const weatherTypes = ['Sunny', 'Cloudy', 'Rainy', 'Partly Cloudy', 'Snowy', 'Windy'];
const randomWeather = weatherTypes[Math.floor(Math.random() * weatherTypes.length)];
description.textContent = randomWeather;
lastUpdated.textContent = `Last updated: ${new Date().toLocaleString()}`;
// Save to history
fetch('/api/history', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ city })
})
.then(response => response.json())
.then(() => {
loadHistory();
})
.catch(error => console.error('Error saving history:', error));
}
// Search button click event
searchBtn.addEventListener('click', () => {
const city = cityInput.value.trim();
if (city) {
simulateWeatherData(city);
cityInput.value = '';
}
});
// Enter key event
cityInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
const city = cityInput.value.trim();
if (city) {
simulateWeatherData(city);
cityInput.value = '';
}
}
});
// Set up refresh interval
setInterval(() => {
fetch('/api/config')
.then(response => response.json())
.then(data => {
serverTime.textContent = new Date(data.serverTime).toLocaleString();
});
}, 30000); // Update server time every 30 seconds
});
Enter fullscreen mode
Exit fullscreen mode
Step 2: Create Dockerfile
Create a Dockerfile in the root of the project:
# Choose a compatible base Image
FROM node:18-alpine
WORKDIR /app
# Copy package.json and package-lock.json files
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the rest of the application code
COPY . .
# Create the data directory for persistent storage
RUN mkdir -p /app/data && \
chmod 777 /app/data
# Expose the port the app will run on
EXPOSE 3000
# Start the application
CMD ["node", "server.js"]
Enter fullscreen mode
Exit fullscreen mode
Step 3: Build and Push Image to ACR
Connect to Azure and your ACR:
# Login to Azure
az login
# Set environment variables for ACR
RESOURCE_GROUP="
Enter fullscreen mode
Exit fullscreen mode
Build and tag the Docker image:
# Build and tag the image
docker build -t $ACR_LOGIN_SERVER/weather-dashboard:v1 .
# Push the image to ACR
docker push $ACR_LOGIN_SERVER/weather-dashboard:v1
Enter fullscreen mode
Exit fullscreen mode
Verify the image was pushed successfully:
# List repositories in ACR
az acr repository list --name $ACR_NAME --output table
# List tags for the specific repository
az acr repository show-tags --name $ACR_NAME --repository weather-dashboard --output table
Enter fullscreen mode
Exit fullscreen mode
Step 4: Create Kubernetes Manifests
Create a directory for Kubernetes manifests:mkdir -p k8s
cd k8s
Create configmap.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: weather-dashboard-config
data:
WEATHER_API_ENDPOINT: "https://api.weatherapi.com/v1"
REFRESH_INTERVAL: "15"
DEFAULT_CITY: "New York"
LOG_LEVEL: "info"
Enter fullscreen mode
Exit fullscreen mode
This YAML configuration defines a ConfigMap in Kubernetes, which stores key-value pairs that can be used by applications running in the cluster. It sets configuration data for our weather dashboard, including the weather API endpoint, refresh interval, default city, and log level.
Create secret.yaml:
apiVersion: v1
kind: Secret
metadata:
name: weather-dashboard-secret
type: Opaque
data:
# Base64 encoded "demo-api-key-12345"
WEATHER_API_KEY: ZGVtby1hcGkta2V5LTEyMzQ1
Enter fullscreen mode
Exit fullscreen mode
This configuration defines a Secret in Kubernetes, which securely stores the API keys for our application in an encoded format. In this case, it stores the API key for accessing the weather service, which is base64-encoded for security purposes.To encode a string in Base64 on a Linux or macOS system, you can use the following command:echo -n "your_string_here" | base64
Create persistent-volume.yaml:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: weather-dashboard-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
Enter fullscreen mode
Exit fullscreen mode
This Configuration defines a PersistentVolumeClaim (PVC) in Kubernetes, which is used to request storage resources for an application. In this case, the PVC is requesting 1Gi of storage with the access mode set to ReadWriteOnce, meaning it can be read and written to by a single node at a time.
Create deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: weather-dashboard
labels:
app: weather-dashboard
spec:
replicas: 2
selector:
matchLabels:
app: weather-dashboard
template:
metadata:
labels:
app: weather-dashboard
spec:
# Target the application pool using the node selector
nodeSelector:
agentpool: applicationpool
containers:
- name: weather-dashboard
image: ACR_LOGIN_SERVER/weather-dashboard:v1
ports:
- containerPort: 3000
env:
# Load ConfigMap values
- name: WEATHER_API_ENDPOINT
valueFrom:
configMapKeyRef:
name: weather-dashboard-config
key: WEATHER_API_ENDPOINT
- name: REFRESH_INTERVAL
valueFrom:
configMapKeyRef:
name: weather-dashboard-config
key: REFRESH_INTERVAL
- name: DEFAULT_CITY
valueFrom:
configMapKeyRef:
name: weather-dashboard-config
key: DEFAULT_CITY
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: weather-dashboard-config
key: LOG_LEVEL
# Load Secret values
- name: WEATHER_API_KEY
valueFrom:
secretKeyRef:
name: weather-dashboard-secret
key: WEATHER_API_KEY
volumeMounts:
- name: data-volume
mountPath: /app/data
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
livenessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /api/config
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
volumes:
- name: data-volume
persistentVolumeClaim:
claimName: weather-dashboard-pvc
Enter fullscreen mode
Exit fullscreen mode
This Deployment file runs two instances of the weather-dashboard application, pulling configuration and secret values from ConfigMap and Secret resources provisioned above. It also uses persistent storage, defines resource limits, and performs health checks to ensure the app is running smoothly.
Create service.yaml:
apiVersion: v1
kind: Service
metadata:
name: weather-dashboard-service
spec:
selector:
app: weather-dashboard
type: NodePort
ports:
- port: 80
targetPort: 3000
nodePort: 30080 # Specifying a nodePort in the range 30000-32767
Enter fullscreen mode
Exit fullscreen mode
This Kubernetes Service file defines a NodePort type service that exposes the weather-dashboard app on port 80, redirecting traffic to the app's internal port 3000. It uses the nodePort 30080 to allow external access to the service through the cluster nodes' IP addresses within the specified port range (30000-32767).
Step 5: Configure AKS to Access ACR
Ensure your AKS cluster has permissions to pull images from your ACR:
# Get the AKS cluster name
AKS_CLUSTER_NAME="yourAKSClusterName"
# Get the AKS Managed Identity ID
AKS_MANAGED_ID=$(az aks show -g $RESOURCE_GROUP -n $AKS_CLUSTER_NAME --query identityProfile.kubeletidentity.objectId -o tsv)
# Assign the AcrPull role to the AKS cluster
az role assignment create \
--assignee $AKS_MANAGED_ID \
--role AcrPull \
--scope $(az acr show --name $ACR_NAME --resource-group $RESOURCE_GROUP --query id --output tsv)
echo "Role assignment created for AKS to pull from ACR"
Enter fullscreen mode
Exit fullscreen mode
Step 6: Deploy the Application
Before applying the manifests, make sure to update the ACR login server in the deployment file:
# Replace ACR_LOGIN_SERVER placeholder with your actual ACR login server
sed -i "s|ACR_LOGIN_SERVER|$ACR_LOGIN_SERVER|g" deployment.yaml
Enter fullscreen mode
Exit fullscreen mode
Now apply all Kubernetes manifests:
# Create namespace (optional)
kubectl create namespace weather-app
# Apply all resources
kubectl apply -f configmap.yaml -n weather-app
kubectl apply -f secret.yaml -n weather-app
kubectl apply -f persistent-volume.yaml -n weather-app
kubectl apply -f deployment.yaml -n weather-app
kubectl apply -f service.yaml -n weather-app
# Verify the resources were created
kubectl get all -n weather-app
Enter fullscreen mode
Exit fullscreen mode
Step 7: Verify the Deployment
Check all resources
# Check all resources in the namespace
kubectl get all -n weather-app
# Check ConfigMap
kubectl get configmap weather-dashboard-config -n weather-app -o yaml
# Check Secret (note: values will be base64 encoded)
kubectl get secret weather-dashboard-secret -n weather-app -o yaml
# Check PVC
kubectl get pvc weather-dashboard-pvc -n weather-app
Enter fullscreen mode
Exit fullscreen mode
Access the application
Since we're using NodePort, you can access the application through any node's IP on the specified port (30080):
# Get nodes' external IPs
kubectl get nodes -o wide
# Get the NodePort service details
kubectl get svc weather-dashboard-service -n weather-app
Enter fullscreen mode
Exit fullscreen mode
You can access the application by navigating to http://:30080 in your browser.
Troubleshooting(Optional)
If your application was not successfully deployed, follow these steps to diagnose and resolve common issues:Using kubectl describe
# Describe a pod to get detailed information
POD_NAME=$(kubectl get pods -n weather-app -l app=weather-dashboard -o jsonpath="{.items[0].metadata.name}")
kubectl describe pod $POD_NAME -n weather-app
# Describe the deployment
kubectl describe deployment weather-dashboard -n weather-app
# Describe the service
kubectl describe service weather-dashboard-service -n weather-app
# Describe the PVC to check if it's bound properly
kubectl describe pvc weather-dashboard-pvc -n weather-app
Enter fullscreen mode
Exit fullscreen mode
Using kubectl logs
# View logs from a pod
kubectl logs $POD_NAME -n weather-app
# Follow logs in real-time
kubectl logs -f $POD_NAME -n weather-app
# If there are multiple containers in the pod, specify the container name
kubectl logs $POD_NAME -c weather-dashboard -n weather-app
Enter fullscreen mode
Exit fullscreen mode
Using kubectl exec
# Execute commands inside a pod
kubectl exec $POD_NAME -n weather-app -- ls -la /app/data
# Get an interactive shell
kubectl exec -it $POD_NAME -n weather-app -- /bin/sh
# Inside the container, you can check:
# - Environment variables
env | grep WEATHER
# - Check if data directory is writable
touch /app/data/test.txt
ls -la /app/data
# - Check application processes
ps aux
# - Check network connectivity
wget -O- localhost:3000/api/config
Enter fullscreen mode
Exit fullscreen mode
Step 9: Troubleshooting Common Issues
Image Pull Issues
If pods are stuck in ImagePullBackOff or ErrImagePull status:
# Check pod events
kubectl describe pod $POD_NAME -n weather-app
# Verify that ACR contains the image
az acr repository show --name $ACR_NAME --repository weather-dashboard
# Check if the AKS cluster has permissions to pull from ACR
az role assignment list \
--assignee $AKS_MANAGED_ID \
--scope $(az acr show --name $ACR_NAME --resource-group $RESOURCE_GROUP --query id --output tsv)
Enter fullscreen mode
Exit fullscreen mode
Volume Mount Issues
If there are persistent volume issues:
# Check if PVC is bound
kubectl get pvc -n weather-app
# Check pod events for volume-related errors
kubectl describe pod $POD_NAME -n weather-app | grep -A 10 Events:
# Check if the storage class exists and is default
kubectl get storageclass
# Check if the volume is correctly mounted
kubectl exec $POD_NAME -n weather-app -- df -h
Enter fullscreen mode
Exit fullscreen mode
Application Issues
If the application is running but not working correctly:
# Check logs for application errors
kubectl logs $POD_NAME -n weather-app
# Test the API endpoints
kubectl exec $POD_NAME -n weather-app -- wget -O- localhost:3000/api/config
# Check if the ConfigMap values are loaded as environment variables
kubectl exec $POD_NAME -n weather-app -- env | grep WEATHER
# Check if the data directory is accessible and writable
kubectl exec $POD_NAME -n weather-app -- touch /app/data/test.txt
kubectl exec $POD_NAME -n weather-app -- ls -la /app/data
Enter fullscreen mode
Exit fullscreen mode
Node Selector Issues
If pods are not scheduled on the application pool:
# Check if nodes have the correct labels
kubectl get nodes --show-labels
# Verify the node selector in the deployment
kubectl get deployment weather-dashboard -n weather-app -o jsonpath='{.spec.template.spec.nodeSelector}'
# Check pod events for scheduling issues
kubectl describe pod $POD_NAME -n weather-app
Enter fullscreen mode
Exit fullscreen mode
Step 10: Scaling and Updates
Scale the deployment
# Scale to more replicas
kubectl scale deployment weather-dashboard -n weather-app --replicas=3
# Check the scaling status
kubectl get pods -n weather-app -l app=weather-dashboard
Enter fullscreen mode
Exit fullscreen mode
Update the application
If you need to update the application:Build and push a new image version:
docker build -t $ACR_LOGIN_SERVER/weather-dashboard:v2 .
docker push $ACR_LOGIN_SERVER/weather-dashboard:v2
Enter fullscreen mode
Exit fullscreen mode
Update the deployment:
kubectl set image deployment/weather-dashboard -n weather-app weather-dashboard=$ACR_LOGIN_SERVER/weather-dashboard:v2
Enter fullscreen mode
Exit fullscreen mode
Monitor the rollout:
kubectl rollout status deployment/weather-dashboard -n weather-app
Enter fullscreen mode
Exit fullscreen mode
Final Verification
Perform a final verification of all components:
# Check all resources
kubectl get all -n weather-app
# Verify that ConfigMap data is accessible
kubectl exec $POD_NAME -n weather-app -- env | grep WEATHER
# Verify that the API is working
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="ExternalIP")].address}')
curl http://$NODE_IP:30080/api/config
# Check that the persistent volume is working
kubectl exec $POD_NAME -n weather-app -- cat /app/data/search-history.json
Enter fullscreen mode
Exit fullscreen mode
Cleanup
When you're done with the deployment, you can clean up the resources:
kubectl delete -f service.yaml -n weather-app
kubectl delete -f deployment.yaml -n weather-app
kubectl delete -f persistent-volume.yaml -n weather-app
kubectl delete -f secret.yaml -n weather-app
kubectl delete -f configmap.yaml -n weather-app
# Or delete everything in the namespace
kubectl delete namespace weather-app
Enter fullscreen mode
Exit fullscreen mode
Conclusion
By following this guide, you have successfully deployed the Weather Dashboard application to AKS using images stored in ACR. You've also learned how to configure environment variables with ConfigMaps and Secrets, manage storage with PersistentVolumeClaims, and ensure application reliability with readiness and liveness probes. This setup serves as a solid foundation for building scalable and secure cloud-native applications on Azure.