> ## Documentation Index
> Fetch the complete documentation index at: https://private-7c7dfe99-parallel-read-in-order-multi-part.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# 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](/cloud/clickhouse-private/reference/infrastructure-requirements). For an overview of how the operator works, see [explanation/architecture.md](/cloud/clickhouse-private/explanation/architecture).

***

## 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
* **[Krew](https://krew.sigs.k8s.io/)** -- the kubectl plugin manager, used to install the [Troubleshoot](https://troubleshoot.sh/) preflight plugin
* **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](/cloud/clickhouse-private/reference/versions):
  * `26.4.1.2596` -- ClickHouse server image tag, to mirror into your registry
  * `26.2.1.258` -- ClickHouse keeper image tag, to mirror into your registry
  * `1.22166.1` -- Operator image and Helm chart tag
  * `1.11.6` -- 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:

```bash theme={null}
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`:

```bash theme={null}
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).

```bash theme={null}
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.4.1.2596 docker://$ACR_HOST/clickhouse-server:26.4.1.2596
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.22166.1 docker://$ACR_HOST/clickhouse-operator:main-1.22166.1
skopeo copy --all docker://$SOURCE_ECR_REPO/helm/clickhouse-operator-helm:1.22166.1 docker://$ACR_HOST/helm/clickhouse-operator-helm:1.22166.1
skopeo copy --all docker://$SOURCE_ECR_REPO/helm/onprem-clickhouse-cluster:1.11.6 docker://$ACR_HOST/helm/onprem-clickhouse-cluster:1.11.6
```

Verify the copy:

```bash theme={null}
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` |

```bash theme={null}
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](/cloud/clickhouse-private/reference/infrastructure-requirements) 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.

```bash theme={null}
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):

```bash theme={null}
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.

<Note>
  **Arm deployments**

  This guide uses x86 machine types (`server.arm64=false` / `keeper.arm64=false` in Step 9). Azure also offers Arm-based VMs (`Standard_D*ps_v5` series). To run on Arm, create the node pools with an Arm machine type, use distinct labels such as `clickhouseGroup: server-arm64` / `keeper-arm64`, point the Step 9 `nodeSelector` values at them, and set `server.arm64=true` / `keeper.arm64=true`.
</Note>

### 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](/cloud/clickhouse-private/reference/chart-values)) |
| Min/desired nodes | 3 per ClickHouse cluster |
| Kubernetes labels | `clickhouseGroup=keeper` |
| Kubernetes taints | `clickhouse.com/do-not-schedule=true:NoSchedule` |

<Note>
  For production HA, run the three Keeper nodes in three distinct availability zones. If any one zone is lost, the remaining two replicas still form a quorum. See [Keeper quorum and failure domains](/cloud/clickhouse-private/reference/infrastructure-requirements#keeper-quorum-and-failure-domains).
</Note>

```bash theme={null}
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` |

```bash theme={null}
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.

<Warning>
  **Do not create Blob Lifecycle Management rules on this container**

  ClickHouse manages its own data in Blob Storage. Lifecycle Management policies (deletion, tiering, abort incomplete uploads with short timeouts) will delete or transition objects that ClickHouse still depends on, causing **data loss** and **cluster outages**. To manage data retention, use ClickHouse TTL rules and partition operations instead. See [Manage data lifecycle](/cloud/clickhouse-private/how-to/manage-data-lifecycle).
</Warning>

```bash theme={null}
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

```bash theme={null}
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.

```bash theme={null}
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.

```bash theme={null}
ACR_HOST=$ACR_NAME.azurecr.io

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

# operator image tag -- the build itself (note the main- prefix)
OPERATOR_IMAGE_TAG=main-1.22166.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`

### Create the Admin Password Secret

The `default` user's password is read from a Kubernetes Secret in the cluster's namespace. Create it before installing the chart, because the operator reads it as soon as it reconciles the cluster.

```bash theme={null}
CLUSTER_NAME=default-xx-01
PASSWORD_SECRET_NAME=clickhouse-admin-password

kubectl create namespace ns-$CLUSTER_NAME
kubectl create secret generic $PASSWORD_SECRET_NAME -n ns-$CLUSTER_NAME --from-literal=password='p@$$w0rd'
```

Use the password itself, not a hash. Single-quote it so the shell does not expand characters such as `$` in the value.

<Note>
  The chart only needs the Secret's name, so anything that produces a Secret with a `password` key in that namespace works, including External Secrets Operator, a Vault injector, or a sealed secret. Changing the value later is applied automatically; see [Reset passwords](/cloud/clickhouse-private/how-to/reset-passwords).
</Note>

### Deploy the Cluster

<Note>
  **ClickHouse versions come from the chart**

  Each release holds a tested and validated matrix of `clickhouse-server` and
  `clickhouse-keeper` versions, pinned in the cluster Helm chart. Only override
  `server.image.tag` / `keeper.image.tag` if you really need to. This applies to
  the cluster chart only, and not to the operator chart.
</Note>

```bash theme={null}
# update values below as needed
CLUSTER_NAME=default-xx-01
PASSWORD_SECRET_NAME=clickhouse-admin-password
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.11.6

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.passwordSecretRef.name=\"$PASSWORD_SECRET_NAME\"" \
    --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"}]'
```

<Tip>
  **Chart values reference**

  This install sets only the values an Azure deployment needs. For the full list of configurable values and their defaults, review the README of the `onprem-clickhouse-cluster` Helm chart.
</Tip>

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

```bash theme={null}
kubectl get pods -n ns-$CLUSTER_NAME -w
```

***

## Step 10: Verify Installation

### Port-forward the ClickHouse Service

```bash theme={null}
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

```bash theme={null}
clickhouse client --host localhost --port 9000 --user default --ask-password
```

The password is the one you put in the Secret. To read it back from the cluster instead:

```bash theme={null}
kubectl get secret $PASSWORD_SECRET_NAME -n ns-$CLUSTER_NAME -o jsonpath='{.data.password}' | base64 -d
```

Run a simple query:

```sql theme={null}
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](https://troubleshoot.sh/), a Kubernetes plugin for cluster diagnostics.

### Install the Plugin

```console theme={null}
kubectl krew install preflight
```

### Copy the Preflight Helm Chart

Add the preflight chart to your ACR copy step:

```bash theme={null}
skopeo copy --all \
  docker://$SOURCE_ECR_REPO/helm/preflight-check:1.38.7 \
  docker://$ACR_HOST/helm/preflight-check:1.38.7
```

### Run the Checks

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

```bash theme={null}
CHART_VERSION=1.38.7

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](/cloud/clickhouse-private/how-to/run-preflight-checks) page.

***

## Next Steps

* **FIPS / government compliance:** See [tutorials/deploy-government.md](/cloud/clickhouse-private/tutorials/deploy-government) 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](/cloud/clickhouse-private/how-to/configure-compute-compute-separation) to set up multiple compute groups with separate endpoints sharing a single dataset.
* **Management API:** See [tutorials/install-api.md](/cloud/clickhouse-private/tutorials/install-api) to install the optional Private API for backups and scaling operations.
* **Monitoring and alerting:** See [how-to/configure-alerting.md](/cloud/clickhouse-private/how-to/configure-alerting) to set up alerting for your deployment.
* **Troubleshooting:** See [troubleshooting.md](/cloud/clickhouse-private/troubleshooting) 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 |
