NewCompare CPU & GPU pricing across AWS, Azure & GCP
Cost Operator

Azure Setup

Install the Cost Operator (trezr) on AKS with Workload Identity, and connect the FOCUS cost export that gives each node its billed cost.

Azure Setup

On AKS, trezr runs as an Azure managed identity through Workload Identity. No secrets are stored in the cluster. It gets read-only roles and nothing else.

It reads two things:

  1. Commitments. Reserved VM Instances and Savings Plans.
  2. The FOCUS cost export. A daily Cost Management export in blob storage. This gives each node its billed cost.

Prerequisites

  • An AKS cluster on Kubernetes 1.31 or later
  • The Read Operator installed
  • az, kubectl, and helm, all pointed at this cluster
  • Owner on the subscription, or Contributor plus User Access Administrator. The setup both changes resources (the cluster, the identity) and assigns roles.
  • A Global Administrator with elevated access, for one step

Run every step in one shell. Later steps use the variables earlier steps set.

Setup

Set the cluster values

The last command should print the cluster's resource group. If it prints nothing, run az account set --subscription <id> for the cluster's subscription and try again.

export SUBSCRIPTION_ID=$(az account show --query id -o tsv)
export CLUSTER=<your-cluster>
export RESOURCE_GROUP=$(az aks list \
  --query "[?name=='$CLUSTER'].resourceGroup | [0]" -o tsv)
echo $RESOURCE_GROUP

Turn on the OIDC issuer and Workload Identity

Check first. If both print True, skip the update.

az aks show -g $RESOURCE_GROUP -n $CLUSTER \
  --query "{oidcIssuer:oidcIssuerProfile.enabled, workloadIdentity:securityProfile.workloadIdentity.enabled}" \
  -o table
az aks update -g $RESOURCE_GROUP -n $CLUSTER \
  --enable-oidc-issuer --enable-workload-identity

Existing nodes keep running.

Create trezr's identity and grant read access

az identity create -g $RESOURCE_GROUP -n devzero-trezr
export CLIENT_ID=$(az identity show -g $RESOURCE_GROUP -n devzero-trezr --query clientId -o tsv)
export PRINCIPAL_ID=$(az identity show -g $RESOURCE_GROUP -n devzero-trezr --query principalId -o tsv)

for ROLE in "Reader" "Cost Management Reader"; do
  az role assignment create --assignee-object-id $PRINCIPAL_ID \
    --assignee-principal-type ServicePrincipal \
    --role "$ROLE" --scope "/subscriptions/$SUBSCRIPTION_ID"
done

Reader reads the subscription's resources. Cost Management Reader lists the cost exports and reads cost data.

Let trezr list reservations and savings plans

Reservations and savings plans live above the subscription, so Reader does not cover them. These two role assignments are at the provider scope, above any single order. Only someone who can assign roles there can run them, usually a Global Administrator with elevated access.

az role assignment create --assignee-object-id $PRINCIPAL_ID \
  --assignee-principal-type ServicePrincipal \
  --role "Reservations Reader" --scope /providers/Microsoft.Capacity
az role assignment create --assignee-object-id $PRINCIPAL_ID \
  --assignee-principal-type ServicePrincipal \
  --role "Savings plan Reader" --scope /providers/Microsoft.BillingBenefits

If you skip this, trezr still reads node costs from the export. It cannot list your commitments.

Create the FOCUS cost export

Skip this if you already have one. Use the portal: it offers the FOCUS dataset and sets up the storage access for you.

  1. Open Cost Management exports, select this subscription as the scope, then + Create.
  2. Type of data: Cost and usage details (FOCUS). Frequency: Daily (month-to-date).
  3. Destination: any storage account and container, for example cost-exports.
  4. Format Parquet or CSV, with overwrite on. Create it, then click Run now.

Let trezr read the export's storage

The first command should list the export as FocusCost, Daily. The rest find the export's storage account and give trezr read access to it. If you have more than one FOCUS export, swap the filter for [?name=='<export-name>'].

az rest --method get --url "https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/providers/Microsoft.CostManagement/exports?api-version=2025-03-01" \
  --query "value[].{name:name,type:properties.definition.type,granularity:properties.definition.dataSet.granularity}" -o table

export STORAGE_ID=$(az rest --method get --url "https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/providers/Microsoft.CostManagement/exports?api-version=2025-03-01" \
  --query "value[?properties.definition.type=='FocusCost'] | [0].properties.deliveryInfo.destination.resourceId" -o tsv)
echo $STORAGE_ID

az role assignment create --assignee-object-id $PRINCIPAL_ID \
  --assignee-principal-type ServicePrincipal --role "Storage Blob Data Reader" \
  --scope $STORAGE_ID

Let trezr's ServiceAccount act as the identity

The echo should print an https URL. If it prints nothing, the OIDC issuer is off. Go back to the Workload Identity step.

export OIDC_ISSUER=$(az aks show -g $RESOURCE_GROUP -n $CLUSTER \
  --query oidcIssuerProfile.issuerUrl -o tsv)
echo $OIDC_ISSUER

az identity federated-credential create --name trezr \
  --identity-name devzero-trezr -g $RESOURCE_GROUP \
  --issuer "$OIDC_ISSUER" --audience api://AzureADTokenExchange \
  --subject system:serviceaccount:devzero-system:trezr

Install trezr

helm upgrade --install trezr oci://registry-1.docker.io/devzeroinc/trezr \
  --version 0.0.13 \
  --namespace devzero-system --create-namespace \
  --set trezr.clusterName=$CLUSTER \
  --set trezr.region=<your-region> \
  --set trezr.cloudProvider=azure \
  --set trezr.azure.subscriptionId=$SUBSCRIPTION_ID \
  --set trezr.azure.clientId=$CLIENT_ID \
  --set serviceAccount.annotations."azure\.workload\.identity/client-id"=$CLIENT_ID \
  --set-string podLabels."azure\.workload\.identity/use"=true

Role assignments can take a few minutes to apply.

Check it

The logs should show access_tier full, count the exports trezr found, and name the months it read.

kubectl logs -n devzero-system deploy/trezr \
  | grep -E "permission probe complete|azure cost export report built|ingesting billing month"

If the access tier is minimal, the reservations step is missing. trezr checks its access only at startup, so restart it after fixing roles: kubectl rollout restart -n devzero-system deploy/trezr.

Pick the export in DevZero

Open the cluster in DevZero, go to Cost & Resources → Commitments, and choose the export under Billing data (Azure FOCUS cost export). See Billing Exports.

Other Ways to Sign In

trezr uses Azure's default credential chain. It tries Workload Identity, then a managed identity, then a service principal. Workload Identity is the one we recommend.

Node pool managed identity. If the node pool's kubelet identity has the same roles, trezr picks it up with no extra Helm values. Every pod on those nodes can then use those roles too.

For a service principal, set its credentials instead of the Workload Identity values. The client secret is stored in a Kubernetes Secret, never in the ConfigMap.

--set trezr.azure.tenantId=<tenant-id> \
--set trezr.azure.clientId=<app-client-id> \
--set trezr.azure.clientSecret=<client-secret>

Next Steps

On this page