Deploy with Helm

With the infrastructure in place, install the confident-ai Helm chart using the Terraform outputs. The chart pulls its images from Confident AI’s registry and installs the app plus in-cluster ClickHouse. On Azure, set config.isAzureEnvironment: true and reach Blob storage through the connection string.

The recommended setup keeps app secrets in Azure Key Vault (synced by the External Secrets Operator) and runs Redis on Azure Managed Redis. Both are provisioned by the Terraform module. In-cluster Redis and a Kubernetes Secret are supported as a simpler alternative, see Simpler option.

Two things come from Confident AI with your Enterprise license:

  • Image pull credentials: an AWS access key ID and secret that let the cluster pull the first-party images. The chart uses them to mint and refresh the pull secret automatically.
  • License key (CONFIDENT_LICENSE_KEY): the signed key that enables your plan’s features.

About OPENAI_API_KEY: it backs the built-in Confident AI evaluation provider, which runs on OpenAI. It is optional. Omit it and connect your own model provider or an LLM gateway per project from AI Connections in the app instead.

1

Create the namespace

$kubectl create namespace confident-ai
2

Put the secrets in Key Vault

Store each value as its own Key Vault secret. Key Vault names cannot contain _, so use -; the chart rewrites - back to _ on the way in (for example DATABASE-URL becomes DATABASE_URL). Include the storage connection string here too, since Key Vault owns the whole secret set:

$KV_URI=$(terraform output -raw key_vault_uri)
$KV_NAME=$(echo "$KV_URI" | sed -E 's#https://([^.]+).*#\1#')
$
$az keyvault secret set --vault-name "$KV_NAME" --name DATABASE-URL --value "$(terraform output -raw database_url)"
$az keyvault secret set --vault-name "$KV_NAME" --name AZURE-STORAGE-CONNECTION-STRING --value "$(terraform output -raw storage_connection_string)"
$az keyvault secret set --vault-name "$KV_NAME" --name BETTER-AUTH-SECRET --value "$(openssl rand -hex 32)"
$az keyvault secret set --vault-name "$KV_NAME" --name OPENAI-API-KEY --value "sk-..."
$az keyvault secret set --vault-name "$KV_NAME" --name CONFIDENT-LICENSE-KEY --value "..."

This uses the vault Terraform created with confident_create_key_vault = true. If you did not enable it, either turn it on and re-apply, or use the simpler option.

4

Install the External Secrets Operator

$helm repo add external-secrets https://charts.external-secrets.io && helm repo update
$helm install external-secrets external-secrets/external-secrets \
> -n external-secrets --create-namespace --set installCRDs=true
$
$kubectl create serviceaccount external-secrets-sa -n confident-ai
$kubectl annotate serviceaccount external-secrets-sa -n confident-ai \
> azure.workload.identity/client-id=$ESO_CLIENT_ID
5

Set up ingress (for HTTPS)

Turn on the AKS application routing add-on, which runs a managed NGINX ingress controller:

$az aks approuting enable --resource-group confident-prod-rg --name <cluster_name>

The values file below uses className: webapprouting.kubernetes.io and exposes all four subdomains (app., api., evals., otel.). Add TLS with a Kubernetes secret or cert-manager that covers all four.

Only trying it out? Set ingress.enabled: false in the values file and reach the app with kubectl port-forward -n confident-ai svc/confident-frontend 3000:3000.

6

Write the values file

Save this as values.azure.yaml. Fill the bracketed values from your Terraform outputs and the credentials Confident AI gave you.

1image:
2 tag: "v2.0.18" # use the version Confident AI gives you
3
4# The chart mints and refreshes the ECR pull secret from these credentials.
5imagePullSecrets:
6 - name: ecr-registry-credentials
7imagePullSecretRefresh:
8 enabled: true
9 region: us-east-1
10 awsAccessKeyId: "<from Confident AI>"
11 awsSecretAccessKey: "<from Confident AI>"
12
13config:
14 cloudProvider: AZURE
15 isAzureEnvironment: true
16 frontendUrl: https://app.yourdomain.com
17 backendUrl: https://api.yourdomain.com
18 subdomain: yourdomain.com
19
20serviceAccount:
21 create: true
22
23storage:
24 testCasesBucket: <test_cases_container>
25 payloadsBucket: <payloads_container>
26 azure:
27 storageAccountName: <storage_account_name>
28
29# Recommended: all app secrets (including the storage connection string) come
30# from Azure Key Vault via ESO.
31secrets:
32 externalSecrets:
33 enabled: true
34 provider: azurekv
35 createStore: true
36 serviceAccountRef:
37 name: external-secrets-sa
38 azure:
39 vaultUrl: <key_vault_uri>
40 tenantId: <your-tenant-id>
41
42clickhouse:
43 internal: true
44 password: "<choose-a-password>"
45
46# Recommended: managed Redis (Azure Managed Redis) from Terraform.
47redis:
48 internal: false
49 externalUrl: <redis_url>
50
51# Required for code-based and transformer metrics (Azure Function sandbox from Terraform).
52codeExecutor:
53 provider: AZURE_FUNCTIONS
54 azure:
55 functionUrl: <code_executor_function_url>/api/execute
56
57ingress:
58 enabled: true
59 className: webapprouting.kubernetes.io
60 hosts:
61 evals: evals.yourdomain.com
62 otel: otel.yourdomain.com
7

Install the chart

The chart is published to GHCR as an OCI artifact:

$helm install confident-ai \
> oci://ghcr.io/confident-ai/charts/confident-ai \
> --version 0.1.0 \
> -n confident-ai \
> -f values.azure.yaml
$
$kubectl get pods -n confident-ai -w

The ClickHouse operator starts first, then a migrations job runs, then the app pods come up. This takes a few minutes.

8

Verify

$kubectl get externalsecret -n confident-ai # STATUS should be SecretSynced
$kubectl get svc -n app-routing-system # note the EXTERNAL-IP

Create app., api., evals., and otel. DNS records for that IP, then open https://app.yourdomain.com and sign in.

Simpler option: in-cluster Redis and a Kubernetes Secret

If you would rather not run Key Vault or managed Redis, the chart can hold secrets in a Kubernetes Secret and run Redis in the cluster. This is less production-hardened (secrets live in the cluster, Redis has no managed backups), but it removes the Key Vault, federated-credential, and ESO steps. Skip steps 2 through 4 above, and replace the secrets and redis blocks in the values file with:

1secrets:
2 data:
3 DATABASE_URL: "<database_url>"
4 AZURE_STORAGE_CONNECTION_STRING: "<storage_connection_string>"
5 BETTER_AUTH_SECRET: "<openssl rand -hex 32>"
6 OPENAI_API_KEY: "sk-..."
7 CONFIDENT_LICENSE_KEY: "<your license key>"
8
9redis:
10 internal: true

Code executor key

The Azure Function sandbox is protected by a function key, which you read from the Azure portal after Terraform creates the Function. Add it as a secret so the app can call the sandbox:

  • Key Vault (recommended): az keyvault secret set --vault-name "$KV_NAME" --name CODE-EXECUTOR-AZURE-FUNCTION-KEY --value "<function key>"
  • Simpler option: add CODE_EXECUTOR_AZURE_FUNCTION_KEY: "<function key>" to secrets.data.

For production, enable the nightly ClickHouse backup to Blob storage, authenticated with the storage connection string. Provision the backup container and see the Disaster Recovery page for the credential detail. Add this under your existing clickhouse: block and helm upgrade:

1clickhouse:
2 backup:
3 enabled: true
4 provider: azure
5 schedule: "0 2 * * *" # nightly at 02:00 UTC
6 azure:
7 container: <clickhouse_backup_container>

Troubleshooting

SymptomCause and fix
externalsecret never reaches SecretSyncedThe federated-credential subject must be exactly system:serviceaccount:confident-ai:external-secrets-sa, and the external-secrets-sa annotation must carry the ESO identity’s client ID. Recheck both.
A Key Vault secret does not reach the appKey Vault names use -, and the chart maps them back to _. Name secrets DATABASE-URL, not DATABASE_URL.
Blob storage access failsConfirm config.isAzureEnvironment: true and that AZURE-STORAGE-CONNECTION-STRING is in Key Vault (or in secrets.data on the simpler option).
Ingress never gets an EXTERNAL-IPThe application routing add-on is not enabled. Run az aks approuting enable and keep className: webapprouting.kubernetes.io.
Frontend returns 500 with ENOTFOUND confident-backendThe frontend resolves backend services by their chart-prefixed names. Keep fullnameOverride: confident (the chart default); do not change it.
ClickHouse Keeper logs Not authenticatedStale PersistentVolumeClaims from a previous failed install. helm uninstall, kubectl delete pvc -n confident-ai --all, then reinstall.
ImagePullBackOff on the app imagesConfirm imagePullSecretRefresh is enabled and the AWS keys from Confident AI are correct.

Updating and tearing down

  • Change the app: edit values.azure.yaml, then helm upgrade confident-ai oci://ghcr.io/confident-ai/charts/confident-ai --version 0.1.0 -n confident-ai -f values.azure.yaml.
  • Rotate a secret: write a new version to Key Vault. ESO re-syncs it, then restart the pods to pick up the change (they hold secrets as env vars until they restart): kubectl rollout restart deployment -n confident-ai.
  • Change infrastructure: edit the Terraform config and terraform apply.
  • Remove everything: helm uninstall confident-ai -n confident-ai, helm uninstall external-secrets -n external-secrets, then terraform destroy, then az group delete --name confident-prod-rg --yes if you want the Step 1 resources gone.