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:
- Commitments. Reserved VM Instances and Savings Plans.
- 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, andhelm, 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_GROUPTurn 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 tableaz aks update -g $RESOURCE_GROUP -n $CLUSTER \
--enable-oidc-issuer --enable-workload-identityExisting 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"
doneReader 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.BillingBenefitsIf 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.
- Open Cost Management exports, select this subscription as the scope, then + Create.
- Type of data: Cost and usage details (FOCUS). Frequency: Daily (month-to-date).
- Destination: any storage account and container, for example
cost-exports. - 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_IDLet 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:trezrInstall 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"=trueRole 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
- Billing Exports -- how daily Azure costs are spread over node hours
- Configuration -- billing account scope, management groups, and other Helm values
- Troubleshooting -- Minimal access tier, export not listed