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

  1. Navigate to Azure Portal → Storage Accounts

  2. Click "+ Create"

  3. 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)
  4. Click "Review + Create", then "Create"

Save the storage account name and resource group for later.

Step 2: Configure Cost Export

  1. Navigate to Azure Portal → Cost Management → Exports

  2. Select scope:

    • Subscription: Choose your subscription
    • Or Management Group: For multi-subscription setups
  3. Click "+ Add" to create new export

  4. 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"
  5. Set destination:

    • Storage account: Select myazurestorageaccount
    • Container: Create new container named cost-export
    • Directory: Leave empty or use kubeadapt/
  6. Set schedule:

    • Start date: Today
    • Frequency: Daily
  7. 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:

bash
1az 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:

bash
1az 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:

bash
1az ad sp create-for-rbac --name kubeadapt-cost-reader \ 2 --role "Cost Management Reader" \ 3 --scopes /subscriptions/YOUR_SUBSCRIPTION_ID

Save the output:

json
1{ 2 "appId": "...", 3 "displayName": "kubeadapt-cost-reader", 4 "password": "...", 5 "tenant": "..." 6}

Grant additional permissions:

bash
1# 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:

json
1{ 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:

bash
1kubectl 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:

yaml
1# 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:

bash
1helm 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:

  1. Enable AAD Pod Identity or Workload Identity on your cluster

  2. Create managed identity:

bash
1az identity create \ 2 --name kubeadapt-identity \ 3 --resource-group kubeadapt-billing-rg
  1. Assign required roles:
bash
1IDENTITY_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
  1. Bind identity to pod (method varies by AAD Pod Identity version)

  2. Use simplified cloud-integration.json:

json
1{ 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

  1. Automatic Detection: Kubeadapt detects Spot VMs by checking instance metadata
  2. Rate Card API: Current spot prices fetched from Azure Consumption API
  3. Historical Data: Cost exports include actual spot usage costs

Required Permissions

Ensure service principal has these roles (already included in setup):

bash
1az 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:

bash
1kubectl logs -n kubeadapt deployment/kubeadapt-cost-analyzer | grep -i spot

In the Kubeadapt dashboard:

  1. Navigate to Node View
  2. Check Pricing Type column
  3. 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

  1. Find your billing account ID in the Azure portal
  2. 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:

yaml
1# 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:

bash
1helm 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

  1. VM Reserved Instances - 1 or 3-year commitments for specific VM sizes
  2. Azure Hybrid Benefit - Windows Server license discounts
  3. Reserved Capacity - Storage and database reservations

Viewing RI Data

RI information appears automatically in:

  1. Dashboard - Cost breakdown shows PAYG vs Reserved vs Spot
  2. Node View - Nodes show effective pricing (RI-discounted or PAYG)
  3. Cost Reports - Historical trends show RI coverage over time
  4. 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

text
1masterPayerARN
for cross-account access), Azure uses Service Principal with multi-subscription permissions and either:

  1. Centralized Cost Export - Single storage account with Management Group scope
  2. Per-Subscription Exports - Multiple exports, one per subscription
text
1Azure 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) 89└── Shared Storage Account: myazurestorageaccount 10 └── Cost Export Container (all subscriptions' data)

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):

bash
1az 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

  1. Navigate to Azure PortalCost Management (at Management Group level)
  2. Click Exports+ Add
  3. 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:

bash
1# 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:

bash
1# 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:

json
1{ 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
    text
    1storage
    configuration (centralized billing storage)
  • Same
    text
    1azureClientID
    ,
    text
    1azureClientSecret
    ,
    text
    1azureTenantID
    (shared service principal)
  • Same cost export data source

Step 5: Deploy to Each Cluster

For each of the 5 AKS clusters:

bash
1# 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)

  1. Management Group Export: Cost data from all 5 subscriptions flows into single storage account
  2. Kubeadapt in Subscription aaaa-1111:
    • Reads from centralized storage account
    • Queries cost data
    • Filters by
      text
      1SubscriptionId = aaaa-1111
      (automatically)
  3. Kubeadapt in Subscription bbbb-2222:
    • Reads from same centralized storage
    • Queries cost data
    • Filters by
      text
      1SubscriptionId = bbbb-2222
  4. 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

text
1Subscription 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

json
1{ 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

bash
1az aks update \ 2 --name my-cluster \ 3 --resource-group my-rg \ 4 --enable-workload-identity \ 5 --enable-oidc-issuer

Create Managed Identity

bash
1az identity create \ 2 --name kubeadapt-identity \ 3 --resource-group kubeadapt-billing-rg

Grant Permissions

bash
1IDENTITY_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

yaml
1opencost: 2 serviceAccount: 3 annotations: 4 azure.workload.identity/client-id: "<MANAGED_IDENTITY_CLIENT_ID>"

Simplified cloud-integration.json

json
1{ 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

AspectAWSAzure
Cross-Account Auth
text
1masterPayerARN
+ AssumeRole
Service Principal with multi-sub permissions
Billing DataCUR in Management AccountCost Export in Storage Account
Identity MethodIRSA (IAM Roles)Workload Identity (Managed Identity)
ConfigurationOne field:
text
1masterPayerARN
Same Service Principal across all clusters

Approach 1 vs Approach 2 Comparison

FeatureCentralized (Approach 1)Per-Subscription (Approach 2)
Setup ComplexityLow (one export)High (N exports)
CostLower (one storage)Higher (N storage accounts)
ManagementEasier (single source)Complex (multiple sources)
SecuritySingle principalPer-subscription principals
Recommended✅ YesOnly if Mgmt Group unavailable

Validation

Test your configuration:

bash
1# 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:

bash
1# 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:

bash
1az 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:

bash
1# 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:

Next Steps