Azure
Learn how to integrate Kubeadapt with your Microsoft Azure infrastructure for accurate cost tracking, spot VM pricing, and reserved instance visibility.
Overview
Kubeadapt provides comprehensive Azure integration capabilities:
- Cloud Billing Integration - Connect to Azure Cost Export for accurate cloud costs
- Spot VM Pricing - Real-time pricing for Azure Spot Virtual Machines
- Reserved VM Instances - Automatic tracking of RI utilization and coverage
Prerequisites
- Azure subscription with appropriate permissions
- Kubeadapt installed via Helm chart
- kubectl access to your cluster
- Azure CLI configured (for setup steps)
- Contributor or Cost Management Contributor role
Part 1: Cloud Billing Integration
Step 1: Create Storage Account for Cost Export
-
Navigate to Azure Portal → Storage Accounts
-
Click "+ Create"
-
Configure storage account:
- Resource group: kubeadapt-billing-rg
- Storage account name: myazurestorageaccount (must be globally unique)
- Region: Choose your primary region
- Performance: Standard
- Redundancy: LRS (Locally Redundant Storage)
-
Click "Review + Create", then "Create"
Save the storage account name and resource group for later.
Step 2: Configure Cost Export
-
Navigate to Azure Portal → Cost Management → Exports
-
Select scope:
- Subscription: Choose your subscription
- Or Management Group: For multi-subscription setups
-
Click "+ Add" to create new export
-
Configure export settings:
- Export name: kubeadapt-cost-export
- Export type: "Daily export of month-to-date costs"
- Metric: "Actual Cost (Usage and Purchases)"
- Export type: "Amortized cost" (recommended) or "Actual cost"
-
Set destination:
- Storage account: Select myazurestorageaccount
- Container: Create new container named cost-export
- Directory: Leave empty or use kubeadapt/
-
Set schedule:
- Start date: Today
- Frequency: Daily
-
Click "Create"
Initial export runs immediately. Subsequent exports run daily at ~8 AM UTC.
Step 3: Get Storage Account Credentials
Get storage account access key:
bash1az storage account keys list \ 2 --account-name myazurestorageaccount \ 3 --resource-group kubeadapt-billing-rg \ 4 --query '[0].value' \ 5 --output tsv
Save this key securely.
Get storage account connection string:
bash1az storage account show-connection-string \ 2 --name myazurestorageaccount \ 3 --resource-group kubeadapt-billing-rg \ 4 --query 'connectionString' \ 5 --output tsv
Step 4: Create Service Principal
Create service principal with Cost Management access:
bash1az ad sp create-for-rbac --name kubeadapt-cost-reader \ 2 --role "Cost Management Reader" \ 3 --scopes /subscriptions/YOUR_SUBSCRIPTION_ID
Save the output:
json1{ 2 "appId": "...", 3 "displayName": "kubeadapt-cost-reader", 4 "password": "...", 5 "tenant": "..." 6}
Grant additional permissions:
bash1# Storage Blob Data Reader - read cost exports 2az role assignment create \ 3 --assignee <appId> \ 4 --role "Storage Blob Data Reader" \ 5 --scope /subscriptions/YOUR_SUBSCRIPTION_ID/resourceGroups/kubeadapt-billing-rg/providers/Microsoft.Storage/storageAccounts/myazurestorageaccount 6 7# Reader role - read resource metadata 8az role assignment create \ 9 --assignee <appId> \ 10 --role "Reader" \ 11 --scope /subscriptions/YOUR_SUBSCRIPTION_ID
Step 5: Configure Kubeadapt with Azure Integration
Create a cloud-integration.json file:
json1{ 2 "azure": { 3 "storage": [ 4 { 5 "subscriptionID": "<YOUR_SUBSCRIPTION_ID>", 6 "account": "myazurestorageaccount", 7 "container": "cost-export", 8 "path": "", 9 "cloud": "public", 10 "authorizer": { 11 "authorizerType": "AzureAccessKey", 12 "accessKey": "<STORAGE_ACCESS_KEY>", 13 "account": "myazurestorageaccount" 14 } 15 } 16 ], 17 "azureClientID": "<SERVICE_PRINCIPAL_APP_ID>", 18 "azureClientSecret": "<SERVICE_PRINCIPAL_PASSWORD>", 19 "azureTenantID": "<TENANT_ID>", 20 "azureOfferDurableID": "MS-AZR-0003P" 21 } 22}
Azure Cloud Options:
- public - Azure Public Cloud (most common)
- gov - Azure Government
- china - Azure China
- germany - Azure Germany
Offer Durable ID: Common values
- MS-AZR-0003P - Pay-As-You-Go
- MS-AZR-0017P - Enterprise Agreement (EA)
- MS-AZR-0148P - Enterprise Dev/Test
- MS-AZR-0063P - MSDN Platforms
Find your offer ID at: Azure Portal → Subscriptions → Your Subscription → Properties
Create Kubernetes secret:
bash1kubectl create secret generic cloud-integration \ 2 --from-file=cloud-integration.json \ 3 --namespace kubeadapt
Update your Helm values file to mount the service principal secret:
yaml1# values.yaml 2opencost: 3 opencost: 4 exporter: 5 cloudIntegrationSecret: "cloud-integration" 6 cloudCost: 7 enabled: true 8 9 # Mount Azure service principal secret 10 extraVolumes: 11 - name: service-key-secret 12 secret: 13 secretName: azure-service-key 14 15 extraVolumeMounts: 16 - mountPath: /var/secrets 17 name: service-key-secret
Apply the configuration:
bash1helm upgrade kubeadapt kubeadapt/kubeadapt \ 2 --namespace kubeadapt \ 3 -f values.yaml
Alternative: Using Managed Identity (AKS)
For AKS clusters, use Azure Managed Identity instead of service principal credentials:
-
Enable AAD Pod Identity or Workload Identity on your cluster
-
Create managed identity:
bash1az identity create \ 2 --name kubeadapt-identity \ 3 --resource-group kubeadapt-billing-rg
- Assign required roles:
bash1IDENTITY_ID=$(az identity show --name kubeadapt-identity --resource-group kubeadapt-billing-rg --query principalId -o tsv) 2 3# Cost Management Reader 4az role assignment create \ 5 --assignee $IDENTITY_ID \ 6 --role "Cost Management Reader" \ 7 --scope /subscriptions/YOUR_SUBSCRIPTION_ID 8 9# Storage Blob Data Reader 10az role assignment create \ 11 --assignee $IDENTITY_ID \ 12 --role "Storage Blob Data Reader" \ 13 --scope /subscriptions/YOUR_SUBSCRIPTION_ID/resourceGroups/kubeadapt-billing-rg/providers/Microsoft.Storage/storageAccounts/myazurestorageaccount 14 15# Reader 16az role assignment create \ 17 --assignee $IDENTITY_ID \ 18 --role "Reader" \ 19 --scope /subscriptions/YOUR_SUBSCRIPTION_ID
-
Bind identity to pod (method varies by AAD Pod Identity version)
-
Use simplified cloud-integration.json:
json1{ 2 "azure": { 3 "storage": [ 4 { 5 "subscriptionID": "<YOUR_SUBSCRIPTION_ID>", 6 "account": "myazurestorageaccount", 7 "container": "cost-export", 8 "path": "", 9 "cloud": "public" 10 } 11 ], 12 "azureOfferDurableID": "MS-AZR-0003P" 13 } 14}
Managed Identity authentication happens automatically, no client credentials or access keys needed.
Part 2: Spot VM Pricing
Azure Spot VM prices are retrieved from Azure's Rate Card API. The service principal must have appropriate permissions.
How It Works
- Automatic Detection: Kubeadapt detects Spot VMs by checking instance metadata
- Rate Card API: Current spot prices fetched from Azure Consumption API
- Historical Data: Cost exports include actual spot usage costs
Required Permissions
Ensure service principal has these roles (already included in setup):
bash1az role assignment create \ 2 --assignee <SERVICE_PRINCIPAL_APP_ID> \ 3 --role "Billing Reader" \ 4 --scope /subscriptions/YOUR_SUBSCRIPTION_ID
Verification
Check that spot VMs are detected:
bash1kubectl logs -n kubeadapt deployment/kubeadapt-cost-analyzer | grep -i spot
In the Kubeadapt dashboard:
- Navigate to Node View
- Check Pricing Type column
- Spot VMs should show "Spot" label
Customer-Specific Pricing (Enterprise Agreements)
If your organization has an Enterprise Agreement or Partner Agreement with Azure, you can configure Kubeadapt to use the Consumption Price Sheet API for account-specific discounts.
Prerequisites
- Find your billing account ID in the Azure portal
- Find your subscription's offer ID (Azure Portal → Subscriptions → Your Subscription → Properties)
Configuration
Update your Helm values file to add billing account and offer ID:
yaml1# values.yaml 2opencost: 3 opencost: 4 exporter: 5 extraEnv: 6 AZURE_BILLING_ACCOUNT: "<YOUR_BILLING_ACCOUNT_ID>" 7 AZURE_OFFER_ID: "MS-AZR-0003P" # Your subscription offer ID
Apply the configuration:
bash1helm upgrade kubeadapt kubeadapt/kubeadapt \ 2 --namespace kubeadapt \ 3 -f values.yaml
Part 3: Reserved VM Instances
Reserved Instance data is automatically retrieved from your Azure cost export. RIs are applied at billing time and reflected in the cost data.
What Gets Tracked
With Azure cost export configured, Kubeadapt automatically shows:
- RI Utilization - How much of your purchased RI capacity is being used
- RI Coverage - Percentage of VM usage covered by RIs
- Effective Discount - Actual discount percentage from reservations
- Net Savings - Savings compared to Pay-As-You-Go pricing
RI Types Supported
- VM Reserved Instances - 1 or 3-year commitments for specific VM sizes
- Azure Hybrid Benefit - Windows Server license discounts
- Reserved Capacity - Storage and database reservations
Viewing RI Data
RI information appears automatically in:
- Dashboard - Cost breakdown shows PAYG vs Reserved vs Spot
- Node View - Nodes show effective pricing (RI-discounted or PAYG)
- Cost Reports - Historical trends show RI coverage over time
- Namespace View - Per-namespace costs reflect RI savings
Troubleshooting RI Tracking
Issue: RIs not appearing
- Verify cost export has been running for at least 24 hours
- Check export includes "PricingModel" column
- Ensure service principal has "Cost Management Reader" role
Issue: RI amounts seem incorrect
- RIs are applied at the EA enrollment or subscription level
- Shared scope RIs may apply across multiple subscriptions
- Check reservation properties in Azure Portal → Reservations
Multi-Subscription Setup (Enterprise Agreement / Management Groups)
For organizations with multiple Azure subscriptions under an Enterprise Agreement or Management Groups, you need to configure Kubeadapt to access cost data across all subscriptions.
Architecture Overview
Unlike AWS (which uses
1masterPayerARN- Centralized Cost Export - Single storage account with Management Group scope
- Per-Subscription Exports - Multiple exports, one per subscription
text1Azure Tenant (Enterprise Agreement) 2├── Management Group (Optional) 3│ ├── Subscription 1: aaaa-1111 (AKS Cluster 1) 4│ ├── Subscription 2: bbbb-2222 (AKS Cluster 2) 5│ ├── Subscription 3: cccc-3333 (AKS Cluster 3) 6│ ├── Subscription 4: dddd-4444 (AKS Cluster 4) 7│ └── Subscription 5: eeee-5555 (AKS Cluster 5) 8│ 9└── Shared Storage Account: myazurestorageaccount 10 └── Cost Export Container (all subscriptions' data)
Approach 1: Centralized Cost Export (Recommended)
This approach uses a single storage account with Management Group-scoped cost export that includes all subscriptions.
Step 1: Create Centralized Storage Account
In your primary subscription (or billing subscription):
bash1az storage account create \ 2 --name myazurestorageaccount \ 3 --resource-group kubeadapt-billing-rg \ 4 --location eastus \ 5 --sku Standard_LRS
Step 2: Create Management Group-Scoped Cost Export
- Navigate to Azure Portal → Cost Management (at Management Group level)
- Click Exports → + Add
- Configure:
- Export name: kubeadapt-cost-export
- Scope: Management Group (includes all subscriptions)
- Export type: Daily export of month-to-date costs
- Metric: Actual Cost (Usage and Purchases)
- Storage account: myazurestorageaccount
- Container: cost-export
Step 3: Create Service Principal with Multi-Subscription Access
Create service principal that can access all subscriptions:
bash1# Create service principal 2az ad sp create-for-rbac --name kubeadapt-multi-sub \ 3 --role "Cost Management Reader" \ 4 --scopes /providers/Microsoft.Management/managementGroups/YOUR_MANAGEMENT_GROUP_ID
Grant storage access:
bash1# Get service principal object ID 2SP_OBJECT_ID=$(az ad sp list --display-name kubeadapt-multi-sub --query '[0].id' -o tsv) 3 4# Grant Storage Blob Data Reader on billing storage account 5az role assignment create \ 6 --assignee $SP_OBJECT_ID \ 7 --role "Storage Blob Data Reader" \ 8 --scope /subscriptions/PRIMARY_SUBSCRIPTION_ID/resourceGroups/kubeadapt-billing-rg/providers/Microsoft.Storage/storageAccounts/myazurestorageaccount
Step 4: Configure cloud-integration.json
For all 5 AKS clusters, use the same configuration:
json1{ 2 "azure": { 3 "storage": [ 4 { 5 "subscriptionID": "aaaa-1111-1111-1111-111111111111", 6 "account": "myazurestorageaccount", 7 "container": "cost-export", 8 "path": "", 9 "cloud": "public", 10 "authorizer": { 11 "authorizerType": "AzureAccessKey", 12 "accessKey": "<STORAGE_ACCESS_KEY>", 13 "account": "myazurestorageaccount" 14 } 15 } 16 ], 17 "azureClientID": "<SERVICE_PRINCIPAL_APP_ID>", 18 "azureClientSecret": "<SERVICE_PRINCIPAL_PASSWORD>", 19 "azureTenantID": "<TENANT_ID>", 20 "azureOfferDurableID": "MS-AZR-0017P" 21 } 22}
Key Point: All clusters use:
- Same configuration (centralized billing storage)text
1storage - Same ,text
1azureClientID,text1azureClientSecret(shared service principal)text1azureTenantID - Same cost export data source
Step 5: Deploy to Each Cluster
For each of the 5 AKS clusters:
bash1# Create secret 2kubectl create secret generic cloud-integration \ 3 --from-file=cloud-integration.json \ 4 --namespace kubeadapt 5 6# Deploy or upgrade Kubeadapt 7helm upgrade kubeadapt kubeadapt/kubeadapt \ 8 --namespace kubeadapt \ 9 -f values.yaml
How It Works (Centralized Approach)
- Management Group Export: Cost data from all 5 subscriptions flows into single storage account
- Kubeadapt in Subscription aaaa-1111:
- Reads from centralized storage account
- Queries cost data
- Filters by (automatically)text
1SubscriptionId = aaaa-1111
- Kubeadapt in Subscription bbbb-2222:
- Reads from same centralized storage
- Queries cost data
- Filters by text
1SubscriptionId = bbbb-2222
- No Double-Counting: Each Kubeadapt-Opencost only reports costs for its own subscription
Approach 2: Per-Subscription Export
If Management Group-scoped export is not available, configure each subscription separately.
Architecture
text1Subscription 1: aaaa-1111 (AKS Cluster 1) 2 └── Storage: sub1-billing → Kubeadapt reads sub1 costs 3 4Subscription 2: bbbb-2222 (AKS Cluster 2) 5 └── Storage: sub2-billing → Kubeadapt reads sub2 costs 6 7Subscription 3: cccc-3333 (AKS Cluster 3) 8 └── Storage: sub3-billing → Kubeadapt reads sub3 costs 9 10...
Configuration for Subscription 1
json1{ 2 "azure": { 3 "storage": [ 4 { 5 "subscriptionID": "aaaa-1111-1111-1111-111111111111", 6 "account": "sub1billing", 7 "container": "cost-export", 8 "path": "", 9 "cloud": "public", 10 "authorizer": { 11 "authorizerType": "AzureAccessKey", 12 "accessKey": "<SUB1_STORAGE_ACCESS_KEY>", 13 "account": "sub1billing" 14 } 15 } 16 ] 17 } 18}
Each subscription gets its own:
- Storage account
- Cost export
- cloud-integration.json configuration
Alternative: Using Azure Managed Identity
For better security, use Workload Identity (Azure's equivalent of AWS IRSA).
Enable Workload Identity on AKS
bash1az aks update \ 2 --name my-cluster \ 3 --resource-group my-rg \ 4 --enable-workload-identity \ 5 --enable-oidc-issuer
Create Managed Identity
bash1az identity create \ 2 --name kubeadapt-identity \ 3 --resource-group kubeadapt-billing-rg
Grant Permissions
bash1IDENTITY_PRINCIPAL_ID=$(az identity show --name kubeadapt-identity --resource-group kubeadapt-billing-rg --query principalId -o tsv) 2 3# Cost Management Reader 4az role assignment create \ 5 --assignee $IDENTITY_PRINCIPAL_ID \ 6 --role "Cost Management Reader" \ 7 --scope /subscriptions/SUBSCRIPTION_ID 8 9# Storage Blob Data Reader 10az role assignment create \ 11 --assignee $IDENTITY_PRINCIPAL_ID \ 12 --role "Storage Blob Data Reader" \ 13 --scope /subscriptions/SUBSCRIPTION_ID/resourceGroups/kubeadapt-billing-rg/providers/Microsoft.Storage/storageAccounts/myazurestorageaccount
Configure Helm Values
yaml1opencost: 2 serviceAccount: 3 annotations: 4 azure.workload.identity/client-id: "<MANAGED_IDENTITY_CLIENT_ID>"
Simplified cloud-integration.json
json1{ 2 "azure": { 3 "storage": [ 4 { 5 "subscriptionID": "aaaa-1111-1111-1111-111111111111", 6 "account": "myazurestorageaccount", 7 "container": "cost-export", 8 "path": "", 9 "cloud": "public" 10 } 11 ] 12 } 13}
No credentials needed - Workload Identity handles authentication automatically.
Key Differences from AWS
| Aspect | AWS | Azure |
|---|---|---|
| Cross-Account Auth | text 1masterPayerARN | Service Principal with multi-sub permissions |
| Billing Data | CUR in Management Account | Cost Export in Storage Account |
| Identity Method | IRSA (IAM Roles) | Workload Identity (Managed Identity) |
| Configuration | One field: text 1masterPayerARN | Same Service Principal across all clusters |
Approach 1 vs Approach 2 Comparison
| Feature | Centralized (Approach 1) | Per-Subscription (Approach 2) |
|---|---|---|
| Setup Complexity | Low (one export) | High (N exports) |
| Cost | Lower (one storage) | Higher (N storage accounts) |
| Management | Easier (single source) | Complex (multiple sources) |
| Security | Single principal | Per-subscription principals |
| Recommended | ✅ Yes | Only if Mgmt Group unavailable |
Validation
Test your configuration:
bash1# Check if cloud costs are being retrieved 2kubectl logs -n kubeadapt deployment/kubeadapt-cost-analyzer | grep -i "azure" 3 4# Verify secret is mounted correctly 5kubectl describe pod -n kubeadapt -l app=cost-analyzer | grep cloud-integration 6 7# Check API connectivity 8kubectl exec -n kubeadapt deployment/kubeadapt-cost-analyzer -- \ 9 curl -s http://localhost:9003/healthz
Recommendation: Use Management Group export for centralized visibility.
Troubleshooting
Common Issues
Issue: "Storage account access denied"
- Verify service principal has "Storage Blob Data Reader" role
- Check storage account firewall settings allow access
- Ensure container name matches export configuration
Issue: "Cost export not found"
- Wait 24 hours after creating export for initial data
- Check export status in Azure Portal → Cost Management → Exports
- Verify container and directory paths match configuration
Issue: "Authentication failed"
- Verify client ID, secret, and tenant ID are correct
- Check service principal hasn't expired
- Ensure subscription ID matches the one with cost exports
Issue: "Rate Card API errors"
- Verify service principal has "Billing Reader" role
- Some subscription types don't support Rate Card API (use cost export data)
- Check Azure service health for API availability
Issue: "Spot pricing missing"
- Spot usage appears in cost export with "Spot" pricing model
- Verify nodes are using spot eviction policy
- Check that VMSS or VM scale sets are configured for Spot
Issue: "Cross-subscription access denied"
- Grant service principal reader role on all subscriptions:
bash
1az role assignment create \ 2 --assignee <SERVICE_PRINCIPAL_APP_ID> \ 3 --role "Reader" \ 4 --scope /subscriptions/<SUBSCRIPTION_ID>
Validation
Test your configuration:
bash1# Check if cloud costs are being retrieved 2kubectl logs -n kubeadapt deployment/kubeadapt-cost-analyzer | grep -i "azure" 3 4# Verify secret is mounted correctly 5kubectl describe pod -n kubeadapt -l app=cost-analyzer | grep cloud-integration 6 7# Check API connectivity 8kubectl exec -n kubeadapt deployment/kubeadapt-cost-analyzer -- \ 9 curl -s http://localhost:9003/healthz
Testing Storage Access
Verify cost export files exist:
bash1az storage blob list \ 2 --account-name myazurestorageaccount \ 3 --container-name cost-export \ 4 --auth-mode login \ 5 --output table
Debugging Service Principal
Check service principal permissions:
bash1# List all role assignments 2az role assignment list \ 3 --assignee <SERVICE_PRINCIPAL_APP_ID> \ 4 --output table 5 6# Verify specific role 7az role assignment list \ 8 --assignee <SERVICE_PRINCIPAL_APP_ID> \ 9 --role "Cost Management Reader" \ 10 --scope /subscriptions/<SUBSCRIPTION_ID>
Support
For additional help:
- Review Cost Attribution Concepts
- Contact authors@kubeadapt.io
Next Steps
- AWS Integration - Configure Amazon Web Services
- GCP Integration - Configure Google Cloud Platform
- Dashboard Overview - Explore cost monitoring features
- Available Savings - Review optimization recommendations