Get Started on Azure AKS

By the end of this guide, you'll have QuestDB running on AKS and sending backups to Azure Blob Storage.

You'll use an AKS cluster you already have. QuestDB will provide the credentials needed to pull the private operator and database images.

Before you begin

AKS cluster requirements

Before you start, make sure your AKS cluster has:

  • Kubernetes 1.33 through 1.36;
  • at least one worker node;
  • Linux on every schedulable worker node;
  • the Azure Disk CSI driver, disk.csi.azure.com;
  • the built-in managed-csi StorageClass;
  • enough capacity for the operator and one QuestDB pod;
  • at least 1 CPU and 2 GiB of available memory for the QuestDB pod; and
  • capacity for one 20 GiB Azure Disk.

This guide covers Linux clusters only.

You'll start with one QuestDB instance. It needs 1 CPU, 2 GiB of memory, and one Azure Disk.

Azure may bill the 20 GiB request as a 32 GiB disk SKU.

For mitigation of a partial regional outage, place worker nodes in more than one Availability Zone. Cross-AZ connectivity is not required, since all database instances communicate over a shared Object Storage Bucket.

QuestDB also needs these network paths:

  • the Kubernetes API server to reach the operator webhook on TCP 9443;
  • the operator to reach QuestDB pods on TCP 9000, 8812, and 9003;
  • worker nodes to reach registry.distribution.questdb.io; and
  • QuestDB pods to reach Azure Blob Storage over HTTPS.

Those are the paths the operator itself needs. Your ingestion and query clients reach QuestDB on a different set of ports, which the operator publishes on the cluster Services: 9000 for HTTP, the Web Console, and QWP over WebSocket; 8812 for PostgreSQL wire; and 9009 for InfluxDB Line Protocol over TCP. Allow whichever your clients use. The QWP UDP receiver on 9007/UDP is off unless you enable spec.protocols.qwp.udp.enabled, and it is unauthenticated when you do, so restrict it with a NetworkPolicy. See Services and ports for the full table.

The computer running Helm also needs HTTPS access to ghcr.io. For a private cluster, provide working DNS plus egress or private endpoints for these services.

Access and tools

Before you continue, make sure you have:

  • administrator access to the AKS cluster;
  • permission to create an Azure Storage account and Blob container;
  • permission to read the new storage account key;
  • Azure CLI;
  • kubectl;
  • Helm 3.10 or later; and
  • the PostgreSQL psql client.

Keep one Bash shell open and run the steps in order. Values you set early in the guide are reused later.

Information from QuestDB

QuestDB supplies access to the private images. In the shared design-partner channel described on the Support page, ask for:

  • the current operator version;
  • a username for registry.distribution.questdb.io; and
  • the matching password.

The same credentials work for both images. You'll store them in two Kubernetes Secrets later because Secrets belong to a single namespace.

1. Connect to your AKS cluster

Start by setting the details of your AKS cluster. Add the operator version and registry username supplied by QuestDB.

AZURE_SUBSCRIPTION='<subscription ID or name containing your AKS cluster>'
AKS_RESOURCE_GROUP='<resource group containing your AKS cluster>'
AKS_CLUSTER_NAME='<your AKS cluster name>'
OPERATOR_VERSION='<version provided by QuestDB>'
REGISTRY_USER='<username provided by QuestDB>'

Switch Azure CLI to that subscription and confirm your selection:

az account set --subscription "$AZURE_SUBSCRIPTION"
az account show \
--query '{name:name,id:id}' \
--output table

You should see the subscription that contains your AKS cluster.

Now connect kubectl:

az aks get-credentials \
--resource-group "$AKS_RESOURCE_GROUP" \
--name "$AKS_CLUSTER_NAME"

kubectl config current-context
kubectl get nodes

Check that the context and nodes belong to the cluster you want to use.

Next, read the cluster version and location from Azure:

AKS_VERSION="$(az aks show \
--resource-group "$AKS_RESOURCE_GROUP" \
--name "$AKS_CLUSTER_NAME" \
--query kubernetesVersion \
--output tsv)"

AZURE_LOCATION="$(az aks show \
--resource-group "$AKS_RESOURCE_GROUP" \
--name "$AKS_CLUSTER_NAME" \
--query location \
--output tsv)"

printf 'AKS version: %s\nAzure location: %s\n' \
"$AKS_VERSION" "$AZURE_LOCATION"

Continue if the Kubernetes version is between 1.33 and 1.36.

2. Check the cluster

Before installing QuestDB, confirm that AKS can provide its persistent disk. Check the Azure Disk CSI driver and StorageClass:

kubectl get csidriver disk.csi.azure.com

kubectl get storageclass managed-csi \
-o custom-columns=NAME:.metadata.name,PROVISIONER:.provisioner,EXPAND:.allowVolumeExpansion,MODE:.volumeBindingMode

Look for disk.csi.azure.com, true, and WaitForFirstConsumer.

Then check the worker nodes:

kubectl get nodes \
-o custom-columns=NAME:.metadata.name,OS:.status.nodeInfo.operatingSystem,CPU:.status.allocatable.cpu,MEMORY:.status.allocatable.memory

kubectl describe nodes | grep -A 8 'Allocated resources'

Make sure a Linux node has room for a pod requesting 1 CPU and 2 GiB of memory. Before continuing, verify that your subscription has enough regional Azure Disk quota and that the node VM size supports one more attached data disk.

3. Create Blob storage

QuestDB needs a Blob container for backups. First, choose its Kubernetes namespace:

QDB_NAMESPACE='qdb-tenant'

You can choose another namespace. Use the same value throughout the guide.

By default, the new storage account will use the AKS resource group. Set a different existing resource group here if you prefer:

STORAGE_RESOURCE_GROUP="$AKS_RESOURCE_GROUP"
BLOB_CONTAINER='questdb'

Azure Storage account names must be globally unique. Create a short name, then create the account:

STORAGE_ACCOUNT="qdb$(date +%s)"
echo "Storage account: $STORAGE_ACCOUNT"

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

If that name is unavailable, change STORAGE_ACCOUNT and try again.

Now get the account key and create the Blob container:

AZURE_STORAGE_KEY="$(az storage account keys list \
--resource-group "$STORAGE_RESOURCE_GROUP" \
--account-name "$STORAGE_ACCOUNT" \
--query '[0].value' \
--output tsv)"

az storage container create \
--name "$BLOB_CONTAINER" \
--account-name "$STORAGE_ACCOUNT" \
--account-key "$AZURE_STORAGE_KEY" \
>/dev/null

az storage container exists \
--name "$BLOB_CONTAINER" \
--account-name "$STORAGE_ACCOUNT" \
--account-key "$AZURE_STORAGE_KEY" \
--query exists \
--output tsv

When the last command prints true, the container is ready.

Create the QuestDB namespace and save the storage key in a Kubernetes Secret:

kubectl create namespace "$QDB_NAMESPACE"

kubectl create secret generic questdb-azure-key \
--namespace "$QDB_NAMESPACE" \
--from-literal=AZURE_STORAGE_KEY="$AZURE_STORAGE_KEY"

unset AZURE_STORAGE_KEY

The operator uses this Secret only to configure QuestDB. The QuestDB pod makes the actual connection to Blob Storage.

4. Configure image access

The QuestDB images are private, so Kubernetes needs the credentials supplied by QuestDB. Start by creating the operator namespace:

kubectl create namespace questdb-operator-system

Set the registry password supplied by QuestDB:

export REGISTRY_PASSWORD='<password provided by QuestDB>'

Create a pull Secret in the operator namespace and another in the QuestDB namespace:

kubectl create secret docker-registry questdb-operator-registry \
--namespace questdb-operator-system \
--docker-server=registry.distribution.questdb.io \
--docker-username="$REGISTRY_USER" \
--docker-password="$REGISTRY_PASSWORD"

kubectl create secret docker-registry questdb-registry \
--namespace "$QDB_NAMESPACE" \
--docker-server=registry.distribution.questdb.io \
--docker-username="$REGISTRY_USER" \
--docker-password="$REGISTRY_PASSWORD"

unset REGISTRY_PASSWORD

Kubernetes will use the first Secret for the operator and the second for the QuestDB database image.

5. Install the operator

Now install the operator with Helm:

helm install questdb-operator \
oci://ghcr.io/questdb/charts/questdb-operator \
--namespace questdb-operator-system \
--version "$OPERATOR_VERSION" \
--set controllerManager.container.image.repository=registry.distribution.questdb.io/questdb-enterprise-operator \
--set-json 'controllerManager.imagePullSecrets=[{"name":"questdb-operator-registry"}]'

Confirm that the operator starts successfully, then check its APIs:

kubectl rollout status \
deployment/questdb-operator-controller-manager \
--namespace questdb-operator-system \
--timeout=5m

kubectl get crd \
questdbclusters.questdb.io \
questdbobjectstores.questdb.io \
questdbpromotions.questdb.io

You should see a successful rollout followed by all three CRDs.

6. Create QuestDB

You're ready to create QuestDB. The manifest below connects it to your Blob container and starts one instance with a backup every five minutes.

It uses the tested 3.3.4-enterprise image. Change the tag only when QuestDB provides another one.

cat <<EOF | kubectl apply -f -
apiVersion: questdb.io/v1alpha1
kind: QuestDBObjectStore
metadata:
name: questdb-store
namespace: $QDB_NAMESPACE
spec:
provider: Azure
azure:
container: $BLOB_CONTAINER
accountName: $STORAGE_ACCOUNT
credentialsSecret:
name: questdb-azure-key
---
apiVersion: questdb.io/v1alpha1
kind: QuestDBCluster
metadata:
name: questdb
namespace: $QDB_NAMESPACE
spec:
image: registry.distribution.questdb.io/questdb:3.3.4-enterprise
imagePullSecrets:
- name: questdb-registry
instances: 1
storage:
storageClassName: managed-csi
size: 20Gi
resources:
requests:
cpu: "1"
memory: 2Gi
limits:
cpu: "1"
memory: 2Gi
objectStoreRef:
name: questdb-store
backup:
enabled: true
schedule: "*/5 * * * *"
root: backup/
EOF

7. Wait for QuestDB and its first backup

QuestDB may take a few minutes to start. Wait until the operator has processed the configuration and the primary is ready with observed healthy WAL writes:

generation="$(kubectl get questdbcluster questdb \
--namespace "$QDB_NAMESPACE" \
-o jsonpath='{.metadata.generation}')"
deadline=$(($(date +%s) + 600))

while true; do
observed="$(kubectl get questdbcluster questdb \
--namespace "$QDB_NAMESPACE" \
-o jsonpath='{.status.observedGeneration}')"
available="$(kubectl get questdbcluster questdb \
--namespace "$QDB_NAMESPACE" \
-o jsonpath='{range .status.conditions[?(@.type=="Available")]}{.status} {.reason}{end}')"
progressing="$(kubectl get questdbcluster questdb \
--namespace "$QDB_NAMESPACE" \
-o jsonpath='{range .status.conditions[?(@.type=="Progressing")]}{.status} {.reason}{end}')"
write_healthy="$(kubectl get questdbcluster questdb \
--namespace "$QDB_NAMESPACE" \
-o jsonpath='{range .status.conditions[?(@.type=="WriteHealthy")]}{.status} {.reason}{end}')"

if [ "$observed" = "$generation" ] && \
[ "$available" = 'True PrimaryReady' ] && \
[ "$progressing" = 'False Settled' ] && \
[ "$write_healthy" = 'True Healthy' ]; then
break
fi

if [ "$(date +%s)" -ge "$deadline" ]; then
echo 'Timed out waiting for QuestDB to become ready' >&2
kubectl describe questdbcluster questdb \
--namespace "$QDB_NAMESPACE"
exit 1
fi
sleep 5
done

echo 'QuestDB is ready'

Once QuestDB is ready, wait for its first scheduled backup:

deadline=$(($(date +%s) + 1200))
until [ "$(kubectl get questdbcluster questdb \
--namespace "$QDB_NAMESPACE" \
-o jsonpath='{.status.backup.lastBackup.status}')" = 'completed' ]; do
if [ "$(date +%s)" -ge "$deadline" ]; then
echo 'Timed out waiting for the first backup' >&2
kubectl describe questdbcluster questdb \
--namespace "$QDB_NAMESPACE"
exit 1
fi
sleep 15
done

kubectl get questdbcluster questdb \
--namespace "$QDB_NAMESPACE" \
-o jsonpath='{.status.backup.lastBackup.endTime}{" completed\n"}'

A timestamp followed by completed confirms the backup worked.

8. Connect to QuestDB

Your QuestDB instance is ready. In one terminal, forward its PostgreSQL wire and HTTP ports.

If you chose another namespace in step 3, use it here.

QDB_NAMESPACE='qdb-tenant'

kubectl port-forward \
--namespace "$QDB_NAMESPACE" \
service/questdb-rw 8812:8812 9000:9000

Leave the port-forward running. Open a second terminal, set the same namespace, and connect:

QDB_NAMESPACE='qdb-tenant' # change this if you changed it in step 3

ADMIN_SECRET="$(kubectl get questdbcluster questdb \
--namespace "$QDB_NAMESPACE" \
-o jsonpath='{.status.adminSecretName}')"

ADMIN_PASSWORD="$(kubectl get secret "$ADMIN_SECRET" \
--namespace "$QDB_NAMESPACE" \
-o jsonpath='{.data.password}' | base64 --decode)"

PGPASSWORD="$ADMIN_PASSWORD" \
psql -h 127.0.0.1 -p 8812 -U admin -d qdb

unset ADMIN_PASSWORD

Open the Web Console

The same port-forward makes the QuestDB Web Console available at http://127.0.0.1:9000. Open that address in your browser and sign in with the username admin.

To display the generated admin password, run the following in the second terminal. This prints a credential, so use a private terminal and do not copy its output into logs.

ADMIN_SECRET="$(kubectl get questdbcluster questdb \
--namespace "$QDB_NAMESPACE" \
-o jsonpath='{.status.adminSecretName}')"

kubectl get secret "$ADMIN_SECRET" \
--namespace "$QDB_NAMESPACE" \
-o jsonpath='{.data.password}' | base64 --decode
echo

When you finish, return to the first terminal and press Ctrl+C to stop the port-forward.

That's it. QuestDB is running on AKS, and its scheduled backups are going to Azure Blob Storage.