This guide covers machine-controller configuration for Microsoft Azure.
The Service Principal needs the following role assignments:
Microsoft.Compute/virtualMachines/*Microsoft.Compute/disks/*Microsoft.Network/networkInterfaces/*Microsoft.Network/publicIPAddresses/*Microsoft.Network/networkSecurityGroups/*# Create service principal
az ad sp create-for-rbac --name "machine-controller-sp" --role Contributor \
--scopes /subscriptions/SUB_ID/resourceGroups/RESOURCE_GROUP
# Output includes:
# - appId (clientID)
# - password (clientSecret)
# - tenant (tenantID)
cloudProviderSpec:
# Credentials
tenantID: "<< AZURE_TENANT_ID >>"
clientID: "<< AZURE_CLIENT_ID >>"
clientSecret: "<< AZURE_CLIENT_SECRET >>"
subscriptionID: "<< AZURE_SUBSCRIPTION_ID >>"
# Location and resources
location: "westeurope"
resourceGroup: "my-k8s-cluster-rg"
# Networking
vnetName: "k8s-vnet"
subnetName: "worker-subnet"
# VM configuration
vmSize: "Standard_D2s_v3"
assignPublicIP: false
# Tags
tags:
kubernetesCluster: "my-cluster"
Create a secret:
kubectl create secret generic azure-credentials \
-n kube-system \
--from-literal=tenantID=<TENANT_ID> \
--from-literal=clientID=<CLIENT_ID> \
--from-literal=clientSecret=<CLIENT_SECRET> \
--from-literal=subscriptionID=<SUB_ID>
Reference in MachineDeployment:
cloudProviderSpec:
tenantID:
secretKeyRef:
name: azure-credentials
key: tenantID
clientID:
secretKeyRef:
name: azure-credentials
key: clientID
clientSecret:
secretKeyRef:
name: azure-credentials
key: clientSecret
subscriptionID:
secretKeyRef:
name: azure-credentials
key: subscriptionID
cloudProviderSpec:
# Credentials (can also use environment variables)
tenantID: "<< AZURE_TENANT_ID >>"
clientID: "<< AZURE_CLIENT_ID >>"
clientSecret: "<< AZURE_CLIENT_SECRET >>"
subscriptionID: "<< AZURE_SUBSCRIPTION_ID >>"
# Location (Azure region)
location: "westeurope" # e.g., eastus, westus2, northeurope
# Resource groups
resourceGroup: "my-k8s-cluster-rg"
vnetResourceGroup: "network-rg" # Optional, if VNet is in different RG
# Availability set (optional)
availabilitySet: "worker-availability-set"
# Availability zones (optional, alternative to availability set)
zones:
- "1"
- "2"
# VM configuration
vmSize: "Standard_D2s_v3"
# Disk configuration
osDiskSize: 100 # GB, optional
dataDiskSize: 50 # GB, optional
# Image configuration
# Option 1: Use marketplace image (recommended)
imageReference:
publisher: "Canonical"
offer: "ubuntu-24_04-lts"
sku: "server-gen1"
version: "latest"
# Option 2: Use custom image
# imageID: "/subscriptions/SUB_ID/resourceGroups/RG/providers/Microsoft.Compute/images/IMAGE_NAME"
# Networking
vnetName: "k8s-vnet"
subnetName: "worker-subnet"
routeTableName: "k8s-routes" # Optional
assignPublicIP: false
# Network Security Group
securityGroupName: "worker-nsg" # Optional
# Load balancer backend pool (optional)
loadBalancerSku: "Standard" # Basic or Standard
# Tags
tags:
kubernetesCluster: "my-cluster"
environment: "production"
apiVersion: cluster.k8s.io/v1alpha1
kind: MachineDeployment
metadata:
name: azure-ubuntu-workers
namespace: kube-system
spec:
replicas: 3
selector:
matchLabels:
name: azure-ubuntu-workers
template:
metadata:
labels:
name: azure-ubuntu-workers
spec:
providerSpec:
value:
cloudProvider: "azure"
cloudProviderSpec:
location: "westeurope"
resourceGroup: "my-k8s-cluster-rg"
vnetName: "k8s-vnet"
subnetName: "worker-subnet"
vmSize: "Standard_D2s_v3"
osDiskSize: 100
assignPublicIP: false
imageReference:
publisher: "Canonical"
offer: "ubuntu-24_04-lts"
sku: "server-gen1"
version: "latest"
tenantID:
secretKeyRef:
name: azure-credentials
key: tenantID
clientID:
secretKeyRef:
name: azure-credentials
key: clientID
clientSecret:
secretKeyRef:
name: azure-credentials
key: clientSecret
subscriptionID:
secretKeyRef:
name: azure-credentials
key: subscriptionID
tags:
kubernetesCluster: "my-cluster"
operatingSystem: "ubuntu"
operatingSystemSpec:
distUpgradeOnBoot: false
versions:
kubelet: "<YOUR-KUBERNETES-VERSION>"
apiVersion: cluster.k8s.io/v1alpha1
kind: MachineDeployment
metadata:
name: azure-multi-az-workers
namespace: kube-system
spec:
replicas: 3
selector:
matchLabels:
name: azure-multi-az-workers
template:
metadata:
labels:
name: azure-multi-az-workers
spec:
providerSpec:
value:
cloudProvider: "azure"
cloudProviderSpec:
location: "westeurope"
resourceGroup: "my-k8s-cluster-rg"
zones:
- "1"
- "2"
- "3"
vnetName: "k8s-vnet"
subnetName: "worker-subnet"
vmSize: "Standard_D2s_v3"
assignPublicIP: false
tags:
kubernetesCluster: "my-cluster"
operatingSystem: "ubuntu"
versions:
kubelet: "<YOUR-KUBERNETES-VERSION>"
apiVersion: cluster.k8s.io/v1alpha1
kind: MachineDeployment
metadata:
name: azure-performance-workers
namespace: kube-system
spec:
replicas: 2
selector:
matchLabels:
name: azure-performance-workers
template:
metadata:
labels:
name: azure-performance-workers
spec:
providerSpec:
value:
cloudProvider: "azure"
cloudProviderSpec:
location: "westeurope"
resourceGroup: "my-k8s-cluster-rg"
vnetName: "k8s-vnet"
subnetName: "worker-subnet"
vmSize: "Standard_F16s_v2" # Compute-optimized
osDiskSize: 200
dataDiskSize: 500
assignPublicIP: false
tags:
kubernetesCluster: "my-cluster"
workloadType: "compute-intensive"
operatingSystem: "ubuntu"
versions:
kubelet: "<YOUR-KUBERNETES-VERSION>"
apiVersion: cluster.k8s.io/v1alpha1
kind: MachineDeployment
metadata:
name: azure-rocky-workers
namespace: kube-system
spec:
replicas: 3
selector:
matchLabels:
name: azure-rocky-workers
template:
metadata:
labels:
name: azure-rocky-workers
spec:
providerSpec:
value:
cloudProvider: "azure"
cloudProviderSpec:
location: "westeurope"
resourceGroup: "my-k8s-cluster-rg"
vnetName: "k8s-vnet"
subnetName: "worker-subnet"
vmSize: "Standard_D2s_v3"
imageReference:
publisher: "resf"
offer: "rockylinux-x86_64"
sku: "9-base"
version: "latest"
assignPublicIP: false
tags:
kubernetesCluster: "my-cluster"
operatingSystem: "rockylinux"
versions:
kubelet: "<YOUR-KUBERNETES-VERSION>"
| Size | vCPUs | RAM | Use Case |
|---|---|---|---|
| Standard_B2s | 2 | 4 GB | Development/testing |
| Standard_D2s_v3 | 2 | 8 GB | General purpose |
| Standard_D4s_v3 | 4 | 16 GB | General purpose |
| Standard_F4s_v2 | 4 | 8 GB | Compute optimized |
| Standard_E4s_v3 | 4 | 32 GB | Memory optimized |
List available sizes in a location:
az vm list-sizes --location westeurope --output table
Workers must be in a subnet that can reach:
Create NSG with required rules:
# Create NSG
az network nsg create \
--resource-group my-k8s-cluster-rg \
--name worker-nsg
# Allow kubelet API
az network nsg rule create \
--resource-group my-k8s-cluster-rg \
--nsg-name worker-nsg \
--name allow-kubelet \
--priority 100 \
--source-address-prefixes VirtualNetwork \
--destination-port-ranges 10250 \
--access Allow \
--protocol Tcp
# Allow NodePort services
az network nsg rule create \
--resource-group my-k8s-cluster-rg \
--nsg-name worker-nsg \
--name allow-nodeport \
--priority 110 \
--source-address-prefixes '*' \
--destination-port-ranges 30000-32767 \
--access Allow \
--protocol Tcp
cloudProviderSpec:
availabilitySet: "worker-availability-set"
cloudProviderSpec:
zones:
- "1"
- "2"
- "3"
Cannot use both availability sets and zones in the same MachineDeployment.
Instead of Service Principal, use Managed Identity (for Azure-hosted controllers):
cloudProviderSpec:
# Use system-assigned managed identity
useManagedIdentityExtension: true
# No need for clientID, clientSecret if using managed identity
subscriptionID: "<< SUBSCRIPTION_ID >>"
# Check machine events
kubectl describe machine <machine-name> -n kube-system
# Common issues:
# - Invalid credentials
# - Quota exceeded
# - VM size not available in region/zone
# - Network configuration errors
# View recent operations
az monitor activity-log list \
--resource-group my-k8s-cluster-rg \
--max-events 20 \
--output table
# Get VM details
az vm show \
--resource-group my-k8s-cluster-rg \
--name worker-vm-name
# Run command on VM
az vm run-command invoke \
--resource-group my-k8s-cluster-rg \
--name worker-vm-name \
--command-id RunShellScript \
--scripts "systemctl status kubelet"