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 registry26.2.1.258– ClickHouse keeper image tag, to mirror into your registry1.20930.1– Operator image and Helm chart tag1.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_NAMEThe rest of this guide refers to the registry host as $ACR_HOST:
ACR_HOST=$ACR_NAME.azurecr.ioStep 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.7Verify the copy:
az acr repository list --name $ACR_NAME --output tableStep 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 nodesCapture 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-waitServer 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-waitIn 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 loginManaged 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://AzureADTokenExchangeWorkload 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.yamlStorageClass
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, usexxfor simplicity$ORDINAL– incrementing ordinal starting with01- 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 -wStep 10: Verify Installation
Port-forward the ClickHouse Service
kubectl port-forward svc/c-default-xx-01-server-any 9000:9000 -n ns-default-xx-01This forwards port 9000 to your local machine.
Connect and Run a Query
clickhouse client --host localhost --port 9000 --password $PASSWORDRun 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 preflightCopy 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.4Run 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
- FIPS / government compliance: See tutorials/deploy-government.md to apply FIPS 140-3 certificates and TLS configuration on top of this infrastructure.
- Compute-Compute separation: See how-to/configure-compute-compute-separation.md to set up multiple compute groups with separate endpoints sharing a single dataset.
- Management API: See tutorials/install-api.md to install the optional Private API for backups and scaling operations.
- Monitoring and alerting: See how-to/configure-alerting.md to set up alerting for your deployment.
- Troubleshooting: See troubleshooting.md for common issues and solutions.
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 |