Skip to content
ClickHouse Docs
ClickHouse DocsClickHouse Docs

Deploy ClickHouse on Azure AKS

This tutorial walks you through deploying ClickHouse Private on Microsoft Azure using Azure Kubernetes Service (AKS), step by step. By the end, you will have a running ClickHouse cluster with Azure Blob Storage-backed storage, local NVMe SSD caching, and the ClickHouse operator managing the deployment.

For detailed infrastructure specifications, see reference/infrastructure-requirements.md. For an overview of how the operator works, see explanation/architecture.md.


Prerequisites

Before you begin, ensure you have the following tools installed:

  • Azure CLI (az) – installed and authenticated to your subscription
  • kubectl – compatible with your target AKS version
  • Helm v3.x
  • skopeo – for copying container images between registries
  • AWS CLI (aws) – with read access to the ClickHouse private ECR (<account-id-from-onboarding>.dkr.ecr.us-east-1.amazonaws.com; access details provided by ClickHouse during onboarding), used only for the image copy step

You will also need:

  • An Azure subscription with permissions to create AKS clusters, Azure Container Registry, Storage Accounts, Virtual Networks, and Managed Identities
  • A resource group for all ClickHouse resources (or reuse an existing one)
  • The component versions for the current release, also listed on Latest Release:
    • 26.2.1.525 – ClickHouse server image tag, to mirror into your registry
    • 26.2.1.258 – ClickHouse keeper image tag, to mirror into your registry
    • 1.20930.1 – Operator image and Helm chart tag
    • 1.8.7 – Cluster Helm chart tag

Step 1: Create the Azure Container Registry

Create an Azure Container Registry (ACR) to hold the ClickHouse images and Helm charts, then log in:

AZURE_RESOURCE_GROUP=rg-clickhouse
AZURE_LOCATION=eastus
ACR_NAME=clickhouseregistry   # must be globally unique, alphanumeric only

az group create --name $AZURE_RESOURCE_GROUP --location $AZURE_LOCATION

az acr create \
  --resource-group $AZURE_RESOURCE_GROUP \
  --name $ACR_NAME \
  --sku Premium \
  --location $AZURE_LOCATION

az acr login --name $ACR_NAME

The rest of this guide refers to the registry host as $ACR_HOST:

ACR_HOST=$ACR_NAME.azurecr.io

Step 2: Copy Container Images

Use skopeo to copy images from the ClickHouse ECR into your ACR. The --all flag preserves all architectures (amd64, arm64).

SOURCE_ECR_ACCOUNT_ID=<account-id-from-onboarding>
SOURCE_ECR_REPO=$SOURCE_ECR_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com

# log into the source ClickHouse ECR (requires AWS credentials provided by ClickHouse)
aws ecr get-login-password --region us-east-1 | skopeo login --username AWS --password-stdin $SOURCE_ECR_REPO

# log into your ACR
az acr login --name $ACR_NAME --expose-token --output tsv --query accessToken | \
  skopeo login --username 00000000-0000-0000-0000-000000000000 --password-stdin $ACR_HOST

# copy each image, be sure to include the --all flag
skopeo copy --all docker://$SOURCE_ECR_REPO/clickhouse-server:26.2.1.525 docker://$ACR_HOST/clickhouse-server:26.2.1.525
skopeo copy --all docker://$SOURCE_ECR_REPO/clickhouse-keeper:26.2.1.258 docker://$ACR_HOST/clickhouse-keeper:26.2.1.258
skopeo copy --all docker://$SOURCE_ECR_REPO/clickhouse-operator:main-1.20930.1 docker://$ACR_HOST/clickhouse-operator:main-1.20930.1
skopeo copy --all docker://$SOURCE_ECR_REPO/helm/clickhouse-operator-helm:1.20930.1 docker://$ACR_HOST/helm/clickhouse-operator-helm:1.20930.1
skopeo copy --all docker://$SOURCE_ECR_REPO/helm/onprem-clickhouse-cluster:1.8.7 docker://$ACR_HOST/helm/onprem-clickhouse-cluster:1.8.7

Verify the copy:

az acr repository list --name $ACR_NAME --output table

Step 3: Create the Virtual Network

Create a VNet with a subnet for AKS nodes.

Setting Value
VNet address space 10.20.0.0/16
AKS node subnet 10.20.0.0/22
Pod CIDR (overlay) 192.168.0.0/16
Service CIDR 10.0.0.0/16
DNS service IP 10.0.0.10
VNET_NAME=vnet-clickhouse
NODE_SUBNET_NAME=snet-aks-nodes

az network vnet create \
  --resource-group $AZURE_RESOURCE_GROUP \
  --name $VNET_NAME \
  --address-prefixes 10.20.0.0/16 \
  --location $AZURE_LOCATION

az network vnet subnet create \
  --resource-group $AZURE_RESOURCE_GROUP \
  --vnet-name $VNET_NAME \
  --name $NODE_SUBNET_NAME \
  --address-prefixes 10.20.0.0/22

NODE_SUBNET_ID=$(az network vnet subnet show \
  --resource-group $AZURE_RESOURCE_GROUP \
  --vnet-name $VNET_NAME \
  --name $NODE_SUBNET_NAME \
  --query id -o tsv)

See reference/infrastructure-requirements.md for detailed networking requirements.


Step 4: Create the AKS Cluster

Create the AKS cluster with OIDC issuer and Workload Identity enabled (required for Step 6). Associate it with the VNet subnet from Step 3. The initial system node pool (Standard_D4s_v3) runs cluster-internal workloads and the ClickHouse operator.

AKS_CLUSTER_NAME=aks-clickhouse

az aks create \
  --resource-group $AZURE_RESOURCE_GROUP \
  --name $AKS_CLUSTER_NAME \
  --location $AZURE_LOCATION \
  --vnet-subnet-id $NODE_SUBNET_ID \
  --network-plugin azure \
  --network-plugin-mode overlay \
  --pod-cidr 192.168.0.0/16 \
  --service-cidr 10.0.0.0/16 \
  --dns-service-ip 10.0.0.10 \
  --node-count 2 \
  --node-vm-size Standard_D4s_v3 \
  --os-sku AzureLinux \
  --enable-oidc-issuer \
  --enable-workload-identity \
  --attach-acr $ACR_NAME \
  --generate-ssh-keys

az aks get-credentials \
  --resource-group $AZURE_RESOURCE_GROUP \
  --name $AKS_CLUSTER_NAME

kubectl get nodes

Capture the OIDC issuer URL (needed for Workload Identity in Step 6):

OIDC_ISSUER=$(az aks show \
  --resource-group $AZURE_RESOURCE_GROUP \
  --name $AKS_CLUSTER_NAME \
  --query oidcIssuerProfile.issuerUrl -o tsv)

Step 5: Create Node Pools

Add two dedicated node pools for ClickHouse.

Keeper Node Pool

Setting Value
VM size Standard_D8ds_v5
OS disk 32 GB Premium SSD
Keeper data volume 50Gi recommended; not part of the node pool, provisioned later by the Helm chart via the keeper.storage values (chart values reference)
Min/desired nodes 3 per ClickHouse cluster
Kubernetes labels clickhouseGroup=keeper
Kubernetes taints clickhouse.com/do-not-schedule=true:NoSchedule
az aks nodepool add \
  --resource-group $AZURE_RESOURCE_GROUP \
  --cluster-name $AKS_CLUSTER_NAME \
  --name keeper \
  --node-count 3 \
  --node-vm-size Standard_D8ds_v5 \
  --os-sku AzureLinux \
  --node-osdisk-size 32 \
  --node-osdisk-type Managed \
  --zones 1 2 3 \
  --vnet-subnet-id $NODE_SUBNET_ID \
  --node-taints clickhouse.com/do-not-schedule=true:NoSchedule \
  --labels clickhouseGroup=keeper \
  --no-wait

Server Node Pool

The server pool uses the Standard_D*ds_v5 family. Setting --node-osdisk-type Ephemeral places the OS on the small local disk. Azure automatically mounts the larger local SSD at /mnt. The ClickHouse cache uses this disk via a hostPath volume pointing at /mnt.

Setting Value
VM size Standard_D16ds_v5 (dev) / Standard_D48ds_v5 (prod)
OS disk Ephemeral (no managed disk cost)
Min/desired nodes Equal to desired ClickHouse replicas
Kubernetes labels clickhouseGroup=server
Kubernetes taints clickhouse.com/do-not-schedule=true:NoSchedule
az aks nodepool add \
  --resource-group $AZURE_RESOURCE_GROUP \
  --cluster-name $AKS_CLUSTER_NAME \
  --name server \
  --node-count 3 \
  --node-vm-size Standard_D16ds_v5 \
  --os-sku AzureLinux \
  --node-osdisk-size 50 \
  --node-osdisk-type Ephemeral \
  --zones 1 2 3 \
  --vnet-subnet-id $NODE_SUBNET_ID \
  --node-taints clickhouse.com/do-not-schedule=true:NoSchedule \
  --labels clickhouseGroup=server \
  --no-wait

In Step 9, set server.ssdCacheConfiguration.hostPathBaseDirectory="/mnt" (this also disables isOnEmptyDir automatically).


Step 6: Create the Storage Account and Managed Identity

Storage Account

Create an Azure Storage Account and a Blob container for ClickHouse data. Place it in the same region as the AKS cluster.

STORAGE_ACCOUNT_NAME=chstorage$RANDOM   # must be globally unique, 3-24 lowercase alphanumeric
BLOB_CONTAINER_NAME=clickhouse-data

az storage account create \
  --resource-group $AZURE_RESOURCE_GROUP \
  --name $STORAGE_ACCOUNT_NAME \
  --location $AZURE_LOCATION \
  --sku Standard_LRS \
  --kind StorageV2 \
  --allow-blob-public-access false

az storage container create \
  --account-name $STORAGE_ACCOUNT_NAME \
  --name $BLOB_CONTAINER_NAME \
  --auth-mode login

Managed Identity and Workload Identity

CLUSTER_NAME=default-xx-01
NAMESPACE=ns-$CLUSTER_NAME
UAMI_NAME=clickhouse-identity

# create the managed identity
az identity create \
  --resource-group $AZURE_RESOURCE_GROUP \
  --name $UAMI_NAME \
  --location $AZURE_LOCATION

UAMI_CLIENT_ID=$(az identity show \
  --resource-group $AZURE_RESOURCE_GROUP \
  --name $UAMI_NAME \
  --query clientId -o tsv)

UAMI_PRINCIPAL_ID=$(az identity show \
  --resource-group $AZURE_RESOURCE_GROUP \
  --name $UAMI_NAME \
  --query principalId -o tsv)

STORAGE_ACCOUNT_ID=$(az storage account show \
  --resource-group $AZURE_RESOURCE_GROUP \
  --name $STORAGE_ACCOUNT_NAME \
  --query id -o tsv)

# grant Storage Blob Data Contributor on the storage account
az role assignment create \
  --assignee-object-id $UAMI_PRINCIPAL_ID \
  --assignee-principal-type ServicePrincipal \
  --role "Storage Blob Data Contributor" \
  --scope $STORAGE_ACCOUNT_ID

# create the federated credential binding UAMI to the chart-created KSA
az identity federated-credential create \
  --resource-group $AZURE_RESOURCE_GROUP \
  --identity-name $UAMI_NAME \
  --name clickhouse-federated \
  --issuer $OIDC_ISSUER \
  --subject "system:serviceaccount:$NAMESPACE:ch-$CLUSTER_NAME-sa" \
  --audience api://AzureADTokenExchange

Workload Identity alignment:

Component Value
User-Assigned Managed Identity clickhouse-identity in $AZURE_RESOURCE_GROUP
K8s ServiceAccount (chart-created) ch-$CLUSTER_NAME-sa in namespace ns-$CLUSTER_NAME
KSA annotation azure.workload.identity/client-id=$UAMI_CLIENT_ID (set by the chart in Step 9)
Federated subject system:serviceaccount:$NAMESPACE:ch-$CLUSTER_NAME-sa

Step 7: Install Kubernetes Prerequisites

Install VolumeSnapshot CRDs

These CRDs are required by the ClickHouse operator.

kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshots.yaml

StorageClass

No manual StorageClass setup is required. The Helm chart creates the premium-ssd-v2 StorageClass automatically when you set storageClass.create=true in Step 9. This StorageClass uses the pre-installed Azure Disk CSI driver (disk.csi.azure.com) with PremiumV2_LRS disks (skuName: PremiumV2_LRS, fstype: ext4, cachingMode: None).


Step 8: Install the Operator

Log into ACR from Helm, then install the operator. Set the availability zones to match your cluster’s zones.

ACR_HOST=$ACR_NAME.azurecr.io

# operator Helm chart tag (eg 1.20930.1, not main-1.20930.1)
OPERATOR_VERSION=1.20930.1

# operator image tag -- the build itself (note the main- prefix)
OPERATOR_IMAGE_TAG=main-1.20930.1

# Azure zone IDs include the region prefix -- NOT bare numbers like "1","2","3".
# Format: <region>-<zone>, e.g. westus3-1, eastus-1, westeurope-1
# To check: kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.labels.topology\.kubernetes\.io/zone}{"\n"}{end}' | sort -u
ZONES="[\"${AZURE_LOCATION}-1\",\"${AZURE_LOCATION}-2\",\"${AZURE_LOCATION}-3\"]"

az acr login --name $ACR_NAME
helm registry login $ACR_HOST \
  --username $(az acr credential show --name $ACR_NAME --query username -o tsv) \
  --password $(az acr credential show --name $ACR_NAME --query passwords[0].value -o tsv)

helm install clickhouse-operator \
   oci://$ACR_HOST/helm/clickhouse-operator-helm \
   --version=$OPERATOR_VERSION \
   --create-namespace \
   -n clickhouse-operator-system \
   --set-json="image.repository=\"$ACR_HOST/clickhouse-operator\"" \
   --set-json="image.tag=\"$OPERATOR_IMAGE_TAG\"" \
   --set-json='cilium.enabled=false' \
   --set-json='idleScalerEnabled=false' \
   --set-json='webhooks.enabled=false' \
   --set-json="operator.availabilityZones=$ZONES"

Step 9: Deploy a ClickHouse Cluster

Naming Your Cluster

Each ClickHouse cluster needs a unique name within the AKS cluster. Use the convention $DESCRIPTOR-$LETTERS-$ORDINAL:

  • $DESCRIPTOR – descriptive name using letters only
  • $LETTERS – reserved, use xx for simplicity
  • $ORDINAL – incrementing ordinal starting with 01
  • Example: default-xx-01

Generate Password Hash and Deploy

# this will be the `default` user's password
PASSWORD='My super secret p@$$w0rd'
if command -v sha256sum &> /dev/null; then
  HASHED_PASSWORD=$(echo -n "$PASSWORD" | sha256sum | awk '{printf $1}' | base64 | tr -d '\n')
else
  HASHED_PASSWORD=$(echo -n "$PASSWORD" | shasum -a 256 | awk '{printf $1}' | base64 | tr -d '\n')
fi

# update values below as needed
CLUSTER_NAME=default-xx-01
ACR_HOST=$ACR_NAME.azurecr.io

# Azure Blob Storage endpoint and container
AZURE_STORAGE_ENDPOINT="https://$STORAGE_ACCOUNT_NAME.blob.core.windows.net"

# these values should change depending on selected instance sizes
SERVER_CPU=16
SERVER_MEMORY=64Gi
# Keeper: high-load profile; for moderate load 2-4 CPU / 8Gi also works (see the infrastructure reference)
KEEPER_CPU=4
KEEPER_MEMORY=16Gi

# bytesPerGiRAM is a scaling factor used to automatically calculate the disk cache size.
# Set cache size to ~75% of temp storage, accounting for OS (50GB) and AKS reserved space.
# At pod startup: CONFIG_DISK_CACHE_SIZE = bytesPerGiRAM * pod_memory_limit
#
# For Standard_D16ds_v5 (64 GiB RAM, 600 GiB temp):
# bytesPerGiRAM = 7Gi -> cache = 7 * 64 = 448 GiB (~75% of 600 GiB)
CACHE_BYTES_PER_GI_RAM=7Gi

# Verify UAMI_CLIENT_ID is set -- the SA annotation will be empty and workload identity will fail if this is blank
echo "UAMI_CLIENT_ID: $UAMI_CLIENT_ID"

CHART_VERSION=1.8.7

helm install $CLUSTER_NAME \
    oci://$ACR_HOST/helm/onprem-clickhouse-cluster \
    --version=$CHART_VERSION \
    -n ns-$CLUSTER_NAME \
    --create-namespace \
    --set-json='baseConfiguration.cloud="azure"' \
    --set-json='storageClass.create=true' \
    --set-json="account.hashedPassword=\"$HASHED_PASSWORD\"" \
    --set-json="serviceAccount.annotations={\"azure.workload.identity/client-id\":\"$UAMI_CLIENT_ID\"}" \
    --set-json="server.image.repository=\"$ACR_HOST/clickhouse-server\"" \
    --set-json='server.arm64=false' \
    --set-json='server.storage.s3.type="azure_blob_storage"' \
    --set-json="server.storage.s3.endpoint=\"$AZURE_STORAGE_ENDPOINT\"" \
    --set-json="server.storage.s3.bucketName=\"$BLOB_CONTAINER_NAME\"" \
    --set-json="server.storage.s3.region=\"$AZURE_LOCATION\"" \
    --set-json="server.ssdCacheConfiguration.hostPathBaseDirectory=\"/mnt\"" \
    --set-json="server.ssdCacheConfiguration.bytesPerGiRAM=\"$CACHE_BYTES_PER_GI_RAM\"" \
    --set-json="server.podPolicy.nodeSelector.clickhouseGroup=\"server\"" \
    --set-json="server.podPolicy.resources.limits.cpu=\"$SERVER_CPU\"" \
    --set-json="server.podPolicy.resources.limits.memory=\"$SERVER_MEMORY\"" \
    --set-json="server.podPolicy.resources.requests.cpu=\"$SERVER_CPU\"" \
    --set-json="server.podPolicy.resources.requests.memory=\"$SERVER_MEMORY\"" \
    --set-json='server.tolerations=[{"effect":"NoSchedule","key":"clickhouse.com/do-not-schedule","operator":"Exists"}]' \
    --set-json="keeper.image.repository=\"$ACR_HOST/clickhouse-keeper\"" \
    --set-json='keeper.arm64=false' \
    --set-json="keeper.podPolicy.nodeSelector.clickhouseGroup=\"keeper\"" \
    --set-json="keeper.podPolicy.resources.limits.cpu=\"$KEEPER_CPU\"" \
    --set-json="keeper.podPolicy.resources.limits.memory=\"$KEEPER_MEMORY\"" \
    --set-json="keeper.podPolicy.resources.requests.cpu=\"$KEEPER_CPU\"" \
    --set-json="keeper.podPolicy.resources.requests.memory=\"$KEEPER_MEMORY\"" \
    --set-json="keeper.storage.resources.requests=\"50Gi\"" \
    --set-json='keeper.tolerations=[{"effect":"NoSchedule","key":"clickhouse.com/do-not-schedule","operator":"Exists"}]'

Monitor the rollout (the operator creates server pods after keepers are healthy):

kubectl get pods -n ns-$CLUSTER_NAME -w

Step 10: Verify Installation

Port-forward the ClickHouse Service

kubectl port-forward svc/c-default-xx-01-server-any 9000:9000 -n ns-default-xx-01

This forwards port 9000 to your local machine.

Connect and Run a Query

clickhouse client --host localhost --port 9000 --password $PASSWORD

Run a simple query:

SELECT 1;

Expected output:

   ┌─1─┐
1. │ 1 │
   └───┘
1 row in set. Elapsed: 0.001 sec.

Step 11: Run Preflight Checks

To validate the readiness of your cluster we recommend running preflight checks. The preflight checks use Troubleshoot, a Kubernetes plugin for cluster diagnostics.

Install the Plugin

kubectl krew install preflight

Copy the Preflight Helm Chart

Add the preflight chart to your ACR copy step:

skopeo copy --all \
  docker://$SOURCE_ECR_REPO/helm/preflight-check:1.38.4 \
  docker://$ACR_HOST/helm/preflight-check:1.38.4

Run the Checks

Use helm template to render the preflight spec, then pipe it to kubectl preflight:

CHART_VERSION=1.38.4

helm template clickhouse-preflight \
    oci://$ACR_HOST/helm/preflight-check \
    --version=$CHART_VERSION \
    --set preflight.cloud=azure \
    --set preflight.clickhouseClusterName=$CLUSTER_NAME | \
kubectl preflight -

This validates node labels, StorageClass configuration, and other requirements. The output shows each check and its status. If a check fails, it includes recommendations on how to fix the issue.

For more details see the How To: Run Preflight Checks page.


Next Steps


Appendix: AWS to Azure Component Mapping

AWS Component Azure Equivalent Notes
ECR Azure Container Registry (ACR) az acr login; attach to AKS with --attach-acr
S3 Azure Blob Storage Endpoint: https://<account>.blob.core.windows.net; set server.storage.s3.type="azure_blob_storage"
IAM IRSA Azure Workload Identity (UAMI + OIDC federated credential) Annotate the KSA with azure.workload.identity/client-id; no static credentials
EBS CSI (gp3-encrypted) Azure Disk CSI (premium-ssd-v2) Pre-installed in AKS; chart creates the StorageClass with storageClass.create=true
NVMe instance store (RAID via DaemonSet) Standard_D*ds_v5 local SSD Azure auto-mounts at /mnt; use hostPathBaseDirectory=/mnt — no DaemonSet or RAID needed
Availability Zones (us-east-1a) Azure Zones (westus3-1) Format is <region>-<number>, not a letter suffix
VPC Azure Virtual Network (VNet) Use Azure CNI overlay (--network-plugin-mode overlay)
EKS AKS Enable --enable-oidc-issuer --enable-workload-identity at cluster creation
Navigation