Multi-Cloud Reserved Instance Coverage Report
Analyze Reserved Instance, Savings Plan, and Committed Use Discount coverage across Azure, AWS, GCP, and OCI. Identify on-demand waste, get purchase recommendations, and calculate potential savings of 30-72%.
The #1 FinOps Lever You're Probably Ignoring
Reserved Instances (RIs) and Savings Plans offer 30-72% discounts compared to on-demand pricing. Yet most organizations run at less than 50% coverage, leaving massive savings on the table.
This playbook gives you automated tools to analyze your current coverage, identify gaps, and calculate exactly how much you could save.
Typical Savings by Commitment Type
30-40%
1-Year Reserved
Lower commitment, moderate savings
55-72%
3-Year Reserved
Maximum savings for stable workloads
20-30%
Savings Plans
Flexible across instance types
What This Tool Analyzes
Current Coverage %
What percentage of your eligible compute is covered by RIs or Savings Plans
On-Demand Waste
Spend that could be converted to reserved pricing for immediate savings
Utilization Analysis
Are your existing reservations being fully used, or are you wasting commitments?
Purchase Recommendations
Specific RIs to buy based on your actual usage patterns and break-even analysis
Prerequisites
- •Python 3.8 or higher
- •Azure CLI installed and authenticated (
az login) - •Azure SDK:
pip install azure-identity azure-mgmt-consumption azure-mgmt-reservations azure-mgmt-costmanagement - •Cost Management Reader or Billing Reader role on subscription/billing scope
- •For OCI: OCI CLI configured with appropriate IAM policies
Quick Start
# Install required packages pip install azure-identity azure-mgmt-consumption azure-mgmt-reservations azure-mgmt-costmanagement pandas tabulate # Login to Azure az login # Run the coverage report python ri_coverage_report.py --subscription "your-subscription-id" # For all subscriptions in a billing account python ri_coverage_report.py --billing-account "your-billing-account-id"
The Complete Script
This Python script connects to Azure Cost Management APIs to analyze your RI coverage, utilization, and generate purchase recommendations.
#!/usr/bin/env python3
"""
Reserved Instance Coverage Report
Analyzes RI/Savings Plan coverage and generates purchase recommendations
"""
import argparse
import json
from datetime import datetime, timedelta
from azure.identity import DefaultAzureCredential
from azure.mgmt.consumption import ConsumptionManagementClient
from azure.mgmt.reservations import AzureReservationAPI
from azure.mgmt.costmanagement import CostManagementClient
import pandas as pd
from tabulate import tabulate
class RICoverageAnalyzer:
def __init__(self, credential, subscription_id=None, billing_account_id=None):
self.credential = credential
self.subscription_id = subscription_id
self.billing_account_id = billing_account_id
if subscription_id:
self.consumption_client = ConsumptionManagementClient(
credential, subscription_id
)
self.cost_client = CostManagementClient(credential)
self.reservation_client = AzureReservationAPI(credential)
def get_reservation_summaries(self, grain='daily', days=30):
"""Get reservation utilization summaries"""
end_date = datetime.utcnow()
start_date = end_date - timedelta(days=days)
scope = f"/subscriptions/{self.subscription_id}"
try:
summaries = list(
self.consumption_client.reservations_summaries.list(
scope=scope,
grain=grain,
start_date=start_date.strftime('%Y-%m-%d'),
end_date=end_date.strftime('%Y-%m-%d')
)
)
return summaries
except Exception as e:
print(f"Note: Could not fetch reservation summaries: {e}")
return []
def get_reservation_recommendations(self, scope_type='Single', look_back='Last30Days'):
"""Get RI purchase recommendations from Azure"""
scope = f"/subscriptions/{self.subscription_id}"
try:
recommendations = list(
self.consumption_client.reservation_recommendations.list(
scope=scope,
filter=f"properties/scope eq '{scope_type}' and properties/lookBackPeriod eq '{look_back}'"
)
)
return recommendations
except Exception as e:
print(f"Note: Could not fetch recommendations: {e}")
return []
def get_cost_by_pricing_model(self, days=30):
"""Analyze costs by pricing model (On-Demand vs Reserved)"""
end_date = datetime.utcnow()
start_date = end_date - timedelta(days=days)
scope = f"/subscriptions/{self.subscription_id}"
query = {
"type": "ActualCost",
"timeframe": "Custom",
"timePeriod": {
"from": start_date.strftime('%Y-%m-%dT00:00:00Z'),
"to": end_date.strftime('%Y-%m-%dT23:59:59Z')
},
"dataset": {
"granularity": "None",
"aggregation": {
"totalCost": {
"name": "Cost",
"function": "Sum"
}
},
"grouping": [
{
"type": "Dimension",
"name": "PricingModel"
},
{
"type": "Dimension",
"name": "ServiceName"
}
]
}
}
try:
result = self.cost_client.query.usage(scope=scope, parameters=query)
return self._parse_cost_query_result(result)
except Exception as e:
print(f"Error querying costs: {e}")
return None
def _parse_cost_query_result(self, result):
"""Parse the cost query result into a DataFrame"""
if not result.rows:
return pd.DataFrame()
columns = [col.name for col in result.columns]
df = pd.DataFrame(result.rows, columns=columns)
# Convert cost to float
if 'Cost' in df.columns:
df['Cost'] = pd.to_numeric(df['Cost'], errors='coerce')
return df
def calculate_coverage_metrics(self, cost_data):
"""Calculate RI coverage metrics from cost data"""
if cost_data is None or cost_data.empty:
return None
# Group by pricing model
pricing_summary = cost_data.groupby('PricingModel')['Cost'].sum()
total_cost = pricing_summary.sum()
on_demand = pricing_summary.get('OnDemand', 0) + pricing_summary.get('On Demand', 0)
reserved = pricing_summary.get('Reservation', 0) + pricing_summary.get('Reserved', 0)
savings_plan = pricing_summary.get('SavingsPlan', 0) + pricing_summary.get('Savings Plan', 0)
spot = pricing_summary.get('Spot', 0)
covered = reserved + savings_plan
coverable = on_demand + covered # Excludes spot
coverage_pct = (covered / coverable * 100) if coverable > 0 else 0
return {
'total_cost': total_cost,
'on_demand_cost': on_demand,
'reserved_cost': reserved,
'savings_plan_cost': savings_plan,
'spot_cost': spot,
'coverage_percentage': coverage_pct,
'potential_savings_low': on_demand * 0.30, # 30% conservative
'potential_savings_high': on_demand * 0.55, # 55% aggressive (3yr)
}
def get_utilization_details(self):
"""Get detailed utilization of existing reservations"""
try:
reservations = list(
self.reservation_client.reservation_order.list()
)
utilization_data = []
for order in reservations:
order_id = order.name
# Get reservations in this order
try:
items = list(
self.reservation_client.reservation.list(
reservation_order_id=order_id
)
)
for item in items:
utilization_data.append({
'order_id': order_id,
'reservation_id': item.name,
'display_name': getattr(item, 'display_name', 'N/A'),
'sku': getattr(item.sku, 'name', 'N/A') if item.sku else 'N/A',
'quantity': getattr(item, 'quantity', 0),
'term': getattr(item.properties, 'term', 'N/A') if hasattr(item, 'properties') else 'N/A',
'provisioning_state': getattr(item, 'provisioning_state', 'N/A'),
})
except Exception as e:
print(f"Could not get details for order {order_id}: {e}")
return utilization_data
except Exception as e:
print(f"Error fetching reservations: {e}")
return []
def format_recommendations(self, recommendations):
"""Format recommendations for display"""
if not recommendations:
return []
formatted = []
for rec in recommendations:
props = rec.properties if hasattr(rec, 'properties') else rec
formatted.append({
'sku': getattr(props, 'sku_name', 'N/A') if hasattr(props, 'sku_name') else getattr(props, 'recommended_resource_type', 'N/A'),
'location': getattr(props, 'location', 'N/A'),
'recommended_quantity': getattr(props, 'recommended_quantity', 0),
'term': getattr(props, 'term', 'N/A'),
'cost_with_no_ri': getattr(props, 'cost_with_no_reserved_instances', 0),
'net_savings': getattr(props, 'net_savings', 0),
'first_usage_date': str(getattr(props, 'first_usage_date', 'N/A')),
})
return formatted
def generate_report(self, days=30):
"""Generate comprehensive RI coverage report"""
print("=" * 70)
print("RESERVED INSTANCE COVERAGE REPORT")
print(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Analysis Period: Last {days} days")
print("=" * 70)
# 1. Cost Analysis by Pricing Model
print("\n📊 COST ANALYSIS BY PRICING MODEL")
print("-" * 50)
cost_data = self.get_cost_by_pricing_model(days=days)
metrics = self.calculate_coverage_metrics(cost_data)
if metrics:
print("Total Compute Cost: $%s" % format(metrics['total_cost'], ',.2f'))
print("On-Demand Cost: $%s" % format(metrics['on_demand_cost'], ',.2f'))
print("Reserved Cost: $%s" % format(metrics['reserved_cost'], ',.2f'))
print("Savings Plan Cost: $%s" % format(metrics['savings_plan_cost'], ',.2f'))
print("Spot Cost: $%s" % format(metrics['spot_cost'], ',.2f'))
print("\n🎯 COVERAGE: %.1f%%" % metrics['coverage_percentage'])
print("\n💰 POTENTIAL ANNUAL SAVINGS:")
print(" Conservative (1yr RI): $%s" % format(metrics['potential_savings_low'] * 12, ',.2f'))
print(" Aggressive (3yr RI): $%s" % format(metrics['potential_savings_high'] * 12, ',.2f'))
else:
print("Could not retrieve cost data. Check permissions.")
# 2. Existing Reservation Utilization
print("\n📈 EXISTING RESERVATION INVENTORY")
print("-" * 50)
utilization = self.get_utilization_details()
if utilization:
df = pd.DataFrame(utilization)
print(tabulate(df, headers='keys', tablefmt='grid', showindex=False))
else:
print("No existing reservations found or could not retrieve data.")
# 3. Purchase Recommendations
print("\n🛒 PURCHASE RECOMMENDATIONS")
print("-" * 50)
recommendations = self.get_reservation_recommendations()
formatted_recs = self.format_recommendations(recommendations)
if formatted_recs:
df = pd.DataFrame(formatted_recs)
print(tabulate(df, headers='keys', tablefmt='grid', showindex=False))
total_potential_savings = sum(r.get('net_savings', 0) for r in formatted_recs)
print("\nTotal Recommended Savings: $%s/year" % format(total_potential_savings, ',.2f'))
else:
print("No recommendations available. This could mean:")
print(" - You already have optimal coverage")
print(" - Usage is too variable for commitments")
print(" - Insufficient data (need 30+ days of usage)")
# 4. Action Items
print("\n✅ RECOMMENDED ACTIONS")
print("-" * 50)
if metrics and metrics['coverage_percentage'] < 70:
print("⚠️ Coverage is %.1f%% - target is 70-80%%" % metrics['coverage_percentage'])
print(" → Review purchase recommendations above")
print(" → Start with 1-year terms for new workloads")
print(" → Consider 3-year terms for stable, long-running VMs")
elif metrics and metrics['coverage_percentage'] < 50:
print("🚨 Coverage is critically low at %.1f%%" % metrics['coverage_percentage'])
print(" → Immediate action required")
print(" → Prioritize largest on-demand spenders")
else:
print("✅ Coverage looks healthy. Review quarterly to maintain.")
print("\n" + "=" * 70)
return dict(
metrics=metrics,
utilization=utilization,
recommendations=formatted_recs
)
def main():
parser = argparse.ArgumentParser(
description='Analyze Reserved Instance coverage and get recommendations'
)
parser.add_argument(
'--subscription', '-s',
help='Azure Subscription ID',
required=True
)
parser.add_argument(
'--billing-account', '-b',
help='Azure Billing Account ID (for multi-subscription analysis)',
default=None
)
parser.add_argument(
'--days', '-d',
type=int,
default=30,
help='Number of days to analyze (default: 30)'
)
parser.add_argument(
'--output', '-o',
help='Output file path for JSON report',
default=None
)
args = parser.parse_args()
print("🔐 Authenticating with Azure...")
credential = DefaultAzureCredential()
analyzer = RICoverageAnalyzer(
credential=credential,
subscription_id=args.subscription,
billing_account_id=args.billing_account
)
report = analyzer.generate_report(days=args.days)
if args.output:
# Convert to JSON-serializable format
output_data = dict(
generated_at=datetime.now().isoformat(),
subscription_id=args.subscription,
analysis_days=args.days,
metrics=report['metrics'],
recommendations=report['recommendations']
)
with open(args.output, 'w') as f:
json.dump(output_data, f, indent=2, default=str)
print("\n📄 Report saved to: " + args.output)
if __name__ == "__main__":
main()OCI Reserved Capacity Analyzer
For OCI environments, use this companion script to analyze compute capacity reservations:
#!/usr/bin/env python3
"""
OCI Reserved Capacity Coverage Report
Analyzes capacity reservations and commitment coverage in OCI
"""
import oci
from datetime import datetime, timedelta
import pandas as pd
from tabulate import tabulate
def get_oci_config():
"""Load OCI config from default location"""
return oci.config.from_file()
def analyze_capacity_reservations(config, compartment_id):
"""Analyze OCI capacity reservations"""
compute_client = oci.core.ComputeClient(config)
# Get all capacity reservations
reservations = compute_client.list_compute_capacity_reservations(
compartment_id=compartment_id
).data
results = []
for reservation in reservations:
# Get reservation details
details = compute_client.get_compute_capacity_reservation(
capacity_reservation_id=reservation.id
).data
results.append(dict(
name=details.display_name,
availability_domain=details.availability_domain,
reserved_count=details.reserved_instance_count,
used_count=details.used_instance_count,
utilization_pct=(details.used_instance_count / details.reserved_instance_count * 100)
if details.reserved_instance_count > 0 else 0,
state=details.lifecycle_state,
time_created=str(details.time_created)
))
return results
def get_cost_analysis(config, tenant_id, compartment_id, days=30):
"""Get cost data from OCI Cost Analysis"""
usage_client = oci.usage_api.UsageapiClient(config)
end_time = datetime.utcnow()
start_time = end_time - timedelta(days=days)
# Query for compute costs by pricing model
request = oci.usage_api.models.RequestSummarizedUsagesDetails(
tenant_id=tenant_id,
time_usage_started=start_time,
time_usage_ended=end_time,
granularity="MONTHLY",
query_type="COST",
compartment_depth=3,
group_by=["service", "skuPartNumber"],
filter=oci.usage_api.models.Filter(
dimensions=[
oci.usage_api.models.Dimension(
key="service",
value="COMPUTE"
)
]
)
)
response = usage_client.request_summarized_usages(request)
return response.data.items
def generate_oci_report(compartment_id, tenant_id):
"""Generate OCI capacity coverage report"""
config = get_oci_config()
print("=" * 70)
print("OCI RESERVED CAPACITY REPORT")
print("Generated: " + datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
print("=" * 70)
# Capacity Reservations
print("\n📊 CAPACITY RESERVATIONS")
print("-" * 50)
reservations = analyze_capacity_reservations(config, compartment_id)
if reservations:
df = pd.DataFrame(reservations)
print(tabulate(df, headers='keys', tablefmt='grid', showindex=False))
# Calculate overall utilization
total_reserved = sum(r['reserved_count'] for r in reservations)
total_used = sum(r['used_count'] for r in reservations)
overall_util = (total_used / total_reserved * 100) if total_reserved > 0 else 0
print("\n🎯 Overall Utilization: %.1f%%" % overall_util)
if overall_util < 80:
print("⚠️ Consider reducing reservations or migrating workloads")
else:
print("No capacity reservations found.")
print("\n" + "=" * 70)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--compartment', '-c', required=True)
parser.add_argument('--tenant', '-t', required=True)
args = parser.parse_args()
generate_oci_report(args.compartment, args.tenant)AWS Reserved Instance & Savings Plan Analyzer
For AWS environments, use this script to analyze EC2 Reserved Instances and Compute Savings Plans coverage:
#!/usr/bin/env python3
"""
AWS Reserved Instance & Savings Plan Coverage Report
Analyzes RI/SP coverage and generates purchase recommendations for AWS
"""
import boto3
from datetime import datetime, timedelta
import pandas as pd
from tabulate import tabulate
class AWSRICoverageAnalyzer:
def __init__(self, profile_name=None, region='us-east-1'):
session_args = dict(region_name=region)
if profile_name:
session_args['profile_name'] = profile_name
self.session = boto3.Session(**session_args)
self.ce_client = self.session.client('ce') # Cost Explorer
self.ec2_client = self.session.client('ec2')
def get_ri_coverage(self, days=30):
"""Get RI coverage from Cost Explorer"""
end_date = datetime.utcnow()
start_date = end_date - timedelta(days=days)
try:
response = self.ce_client.get_reservation_coverage(
TimePeriod=dict(
Start=start_date.strftime('%Y-%m-%d'),
End=end_date.strftime('%Y-%m-%d')
),
Granularity='MONTHLY',
Metrics=['Hour']
)
return response.get('Total', {})
except Exception as e:
print("Error fetching RI coverage: %s" % str(e))
return None
def get_savings_plan_coverage(self, days=30):
"""Get Savings Plan coverage"""
end_date = datetime.utcnow()
start_date = end_date - timedelta(days=days)
try:
response = self.ce_client.get_savings_plans_coverage(
TimePeriod=dict(
Start=start_date.strftime('%Y-%m-%d'),
End=end_date.strftime('%Y-%m-%d')
),
Granularity='MONTHLY'
)
return response.get('SavingsPlansCoverages', [])
except Exception as e:
print("Error fetching SP coverage: %s" % str(e))
return []
def get_ri_utilization(self, days=30):
"""Get RI utilization metrics"""
end_date = datetime.utcnow()
start_date = end_date - timedelta(days=days)
try:
response = self.ce_client.get_reservation_utilization(
TimePeriod=dict(
Start=start_date.strftime('%Y-%m-%d'),
End=end_date.strftime('%Y-%m-%d')
),
Granularity='MONTHLY'
)
return response.get('Total', {})
except Exception as e:
print("Error fetching RI utilization: %s" % str(e))
return None
def get_ri_recommendations(self, service='Amazon Elastic Compute Cloud - Compute'):
"""Get RI purchase recommendations"""
try:
response = self.ce_client.get_reservation_purchase_recommendation(
Service=service,
LookbackPeriodInDays='THIRTY_DAYS',
TermInYears='ONE_YEAR',
PaymentOption='NO_UPFRONT'
)
return response.get('Recommendations', [])
except Exception as e:
print("Error fetching recommendations: %s" % str(e))
return []
def get_savings_plan_recommendations(self):
"""Get Savings Plan purchase recommendations"""
try:
response = self.ce_client.get_savings_plans_purchase_recommendation(
SavingsPlansType='COMPUTE_SP',
TermInYears='ONE_YEAR',
PaymentOption='NO_UPFRONT',
LookbackPeriodInDays='LAST_30_DAYS'
)
return response.get('SavingsPlansPurchaseRecommendation', {})
except Exception as e:
print("Error fetching SP recommendations: %s" % str(e))
return {}
def get_current_reservations(self):
"""Get current EC2 Reserved Instances"""
try:
response = self.ec2_client.describe_reserved_instances(
Filters=[dict(Name='state', Values=['active'])]
)
reservations = []
for ri in response.get('ReservedInstances', []):
reservations.append(dict(
id=ri['ReservedInstancesId'],
instance_type=ri['InstanceType'],
count=ri['InstanceCount'],
scope=ri.get('Scope', 'Region'),
offering=ri['OfferingType'],
end_date=str(ri['End']),
state=ri['State']
))
return reservations
except Exception as e:
print("Error fetching reservations: %s" % str(e))
return []
def generate_report(self, days=30):
"""Generate comprehensive AWS RI coverage report"""
print("=" * 70)
print("AWS RESERVED INSTANCE COVERAGE REPORT")
print("Generated: " + datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
print("Analysis Period: Last %d days" % days)
print("=" * 70)
# 1. RI Coverage
print("\n📊 RESERVED INSTANCE COVERAGE")
print("-" * 50)
ri_coverage = self.get_ri_coverage(days=days)
if ri_coverage:
coverage_hours = ri_coverage.get('CoverageHours', {})
coverage_pct = float(coverage_hours.get('CoverageHoursPercentage', 0))
on_demand_hours = float(coverage_hours.get('OnDemandHours', 0))
reserved_hours = float(coverage_hours.get('ReservedHours', 0))
print("Coverage Percentage: %.1f%%" % coverage_pct)
print("On-Demand Hours: %s" % format(on_demand_hours, ',.0f'))
print("Reserved Hours: %s" % format(reserved_hours, ',.0f'))
# 2. Savings Plan Coverage
print("\n📈 SAVINGS PLAN COVERAGE")
print("-" * 50)
sp_coverage = self.get_savings_plan_coverage(days=days)
if sp_coverage:
for period in sp_coverage:
coverage = period.get('Coverage', {})
sp_pct = float(coverage.get('CoveragePercentage', 0))
print("Savings Plan Coverage: %.1f%%" % sp_pct)
# 3. RI Utilization
print("\n⚡ RESERVATION UTILIZATION")
print("-" * 50)
utilization = self.get_ri_utilization(days=days)
if utilization:
util_pct = float(utilization.get('UtilizationPercentage', 0))
print("Utilization: %.1f%%" % util_pct)
if util_pct < 80:
print("⚠️ Low utilization - consider modifying or selling unused RIs")
# 4. Current Reservations
print("\n📋 ACTIVE RESERVATIONS")
print("-" * 50)
reservations = self.get_current_reservations()
if reservations:
df = pd.DataFrame(reservations)
print(tabulate(df, headers='keys', tablefmt='grid', showindex=False))
else:
print("No active reservations found")
# 5. Recommendations
print("\n🛒 PURCHASE RECOMMENDATIONS")
print("-" * 50)
ri_recs = self.get_ri_recommendations()
if ri_recs:
for rec in ri_recs:
details = rec.get('RecommendationDetails', [])
for detail in details[:5]: # Top 5
instance_type = detail.get('InstanceDetails', {}).get('EC2InstanceDetails', {}).get('InstanceType', 'N/A')
savings = detail.get('EstimatedMonthlySavingsAmount', '0')
print(" %s: $%s/month savings" % (instance_type, savings))
sp_recs = self.get_savings_plan_recommendations()
if sp_recs:
estimated_savings = sp_recs.get('EstimatedMonthlySavingsAmount', 0)
commitment = sp_recs.get('HourlyCommitmentToPurchase', 0)
print("\nSavings Plan Recommendation:")
print(" Hourly Commitment: $%s" % commitment)
print(" Est. Monthly Savings: $%s" % estimated_savings)
print("\n" + "=" * 70)
def main():
import argparse
parser = argparse.ArgumentParser(
description='Analyze AWS RI and Savings Plan coverage'
)
parser.add_argument('--profile', '-p', help='AWS profile name', default=None)
parser.add_argument('--region', '-r', help='AWS region', default='us-east-1')
parser.add_argument('--days', '-d', type=int, default=30, help='Days to analyze')
args = parser.parse_args()
print("🔐 Authenticating with AWS...")
analyzer = AWSRICoverageAnalyzer(
profile_name=args.profile,
region=args.region
)
analyzer.generate_report(days=args.days)
if __name__ == "__main__":
main()AWS Prerequisites
# Install boto3
pip install boto3 pandas tabulate
# Configure AWS credentials
aws configure
# Or use a named profile
aws configure --profile my-finops-profile
# Required IAM permissions:
# - ce:GetReservationCoverage
# - ce:GetReservationUtilization
# - ce:GetReservationPurchaseRecommendation
# - ce:GetSavingsPlansCoverage
# - ce:GetSavingsPlansPurchaseRecommendation
# - ec2:DescribeReservedInstancesGCP Committed Use Discount Analyzer
For Google Cloud, analyze Committed Use Discounts (CUDs) for Compute Engine and other services:
#!/usr/bin/env python3
"""
GCP Committed Use Discount (CUD) Coverage Report
Analyzes CUD coverage and generates recommendations for Google Cloud
"""
from google.cloud import billing_v1
from google.cloud import compute_v1
from google.cloud import bigquery
from datetime import datetime, timedelta
import pandas as pd
from tabulate import tabulate
class GCPCUDAnalyzer:
def __init__(self, project_id, billing_account_id=None):
self.project_id = project_id
self.billing_account_id = billing_account_id
self.compute_client = compute_v1.RegionCommitmentsClient()
self.bq_client = bigquery.Client(project=project_id)
def get_active_commitments(self):
"""Get all active CUDs across regions"""
commitments = []
regions_client = compute_v1.RegionsClient()
# List all regions
regions = regions_client.list(project=self.project_id)
for region in regions:
try:
region_commitments = self.compute_client.list(
project=self.project_id,
region=region.name
)
for commitment in region_commitments:
if commitment.status == 'ACTIVE':
commitments.append(dict(
name=commitment.name,
region=region.name,
plan=commitment.plan, # TWELVE_MONTH or THIRTY_SIX_MONTH
status=commitment.status,
start_timestamp=str(commitment.start_timestamp),
end_timestamp=str(commitment.end_timestamp),
resources=self._format_resources(commitment.resources)
))
except Exception as e:
print("Error listing commitments in %s: %s" % (region.name, str(e)))
return commitments
def _format_resources(self, resources):
"""Format commitment resources for display"""
formatted = []
for resource in resources:
formatted.append("%s: %s" % (resource.type_, resource.amount))
return ", ".join(formatted) if formatted else "N/A"
def get_cost_data_from_bigquery(self, days=30):
"""Query billing export for cost analysis by SKU"""
# This requires BigQuery billing export to be enabled
query = """
SELECT
service.description as service,
sku.description as sku,
SUM(cost) as total_cost,
SUM(CASE WHEN credits.type = 'COMMITTED_USE_DISCOUNT'
THEN credits.amount ELSE 0 END) as cud_credit,
SUM(CASE WHEN credits.type = 'SUSTAINED_USE_DISCOUNT'
THEN credits.amount ELSE 0 END) as sud_credit
FROM `%s.billing_export.gcp_billing_export_v1_*`
LEFT JOIN UNNEST(credits) as credits
WHERE _PARTITIONTIME >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL %d DAY)
AND service.description LIKE '%%Compute Engine%%'
GROUP BY service.description, sku.description
ORDER BY total_cost DESC
LIMIT 20
""" % (self.project_id, days)
try:
df = self.bq_client.query(query).to_dataframe()
return df
except Exception as e:
print("BigQuery error (ensure billing export is enabled): %s" % str(e))
return None
def calculate_coverage_metrics(self, cost_data):
"""Calculate CUD coverage from billing data"""
if cost_data is None or cost_data.empty:
return None
total_cost = cost_data['total_cost'].sum()
cud_savings = abs(cost_data['cud_credit'].sum())
sud_savings = abs(cost_data['sud_credit'].sum())
# Estimate what could be saved with full CUD coverage
# CUDs typically save 57% for 3-year, 37% for 1-year
potential_additional = (total_cost - cud_savings) * 0.40
return dict(
total_compute_cost=total_cost,
cud_savings=cud_savings,
sud_savings=sud_savings,
coverage_percentage=(cud_savings / total_cost * 100) if total_cost > 0 else 0,
potential_additional_savings=potential_additional
)
def get_recommendations(self):
"""Get CUD recommendations from Recommender API"""
from google.cloud import recommender_v1
recommendations = []
try:
client = recommender_v1.RecommenderClient()
parent = "projects/%s/locations/global/recommenders/google.compute.commitment.UsageCommitmentRecommender" % self.project_id
recs = client.list_recommendations(parent=parent)
for rec in recs:
recommendations.append(dict(
name=rec.name,
description=rec.description,
priority=rec.priority.name,
primary_impact=str(rec.primary_impact)
))
except Exception as e:
print("Could not fetch recommendations: %s" % str(e))
return recommendations
def generate_report(self, days=30):
"""Generate comprehensive GCP CUD coverage report"""
print("=" * 70)
print("GCP COMMITTED USE DISCOUNT REPORT")
print("Generated: " + datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
print("Project: " + self.project_id)
print("=" * 70)
# 1. Active Commitments
print("\n📋 ACTIVE COMMITMENTS")
print("-" * 50)
commitments = self.get_active_commitments()
if commitments:
df = pd.DataFrame(commitments)
print(tabulate(df, headers='keys', tablefmt='grid', showindex=False))
print("\nTotal active commitments: %d" % len(commitments))
else:
print("No active commitments found")
# 2. Cost Analysis (requires BigQuery billing export)
print("\n📊 COST ANALYSIS")
print("-" * 50)
cost_data = self.get_cost_data_from_bigquery(days=days)
metrics = self.calculate_coverage_metrics(cost_data)
if metrics:
print("Total Compute Cost: $%s" % format(metrics['total_compute_cost'], ',.2f'))
print("CUD Savings Applied: $%s" % format(metrics['cud_savings'], ',.2f'))
print("SUD Savings Applied: $%s" % format(metrics['sud_savings'], ',.2f'))
print("\n🎯 CUD COVERAGE: %.1f%%" % metrics['coverage_percentage'])
print("\n💰 POTENTIAL ADDITIONAL SAVINGS:")
print(" With more CUDs: $%s/month" % format(metrics['potential_additional_savings'], ',.2f'))
else:
print("Enable BigQuery billing export for detailed cost analysis")
# 3. Recommendations
print("\n🛒 RECOMMENDATIONS")
print("-" * 50)
recommendations = self.get_recommendations()
if recommendations:
for rec in recommendations:
print("• [%s] %s" % (rec['priority'], rec['description']))
else:
print("No recommendations available")
print("Tip: Check Cloud Console > Recommendations for CUD suggestions")
# 4. CUD Savings Reference
print("\n📈 GCP CUD SAVINGS REFERENCE")
print("-" * 50)
print("1-Year Commitment: 37%% discount")
print("3-Year Commitment: 57%% discount")
print("\nTip: Use spend-based CUDs for flexibility across machine types")
print("\n" + "=" * 70)
def main():
import argparse
parser = argparse.ArgumentParser(
description='Analyze GCP Committed Use Discount coverage'
)
parser.add_argument('--project', '-p', required=True, help='GCP Project ID')
parser.add_argument('--billing-account', '-b', help='Billing Account ID')
parser.add_argument('--days', '-d', type=int, default=30, help='Days to analyze')
args = parser.parse_args()
print("🔐 Authenticating with GCP...")
analyzer = GCPCUDAnalyzer(
project_id=args.project,
billing_account_id=args.billing_account
)
analyzer.generate_report(days=args.days)
if __name__ == "__main__":
main()GCP Prerequisites
# Install Google Cloud SDK and Python libraries
pip install google-cloud-billing google-cloud-compute google-cloud-bigquery google-cloud-recommender pandas tabulate
# Authenticate
gcloud auth application-default login
# Enable required APIs
gcloud services enable compute.googleapis.com
gcloud services enable cloudbilling.googleapis.com
gcloud services enable bigquery.googleapis.com
gcloud services enable recommender.googleapis.com
# For cost analysis, enable BigQuery billing export:
# Console > Billing > Billing export > BigQuery export
# Required IAM roles:
# - roles/compute.viewer
# - roles/billing.viewer
# - roles/bigquery.dataViewer
# - roles/recommender.computeViewerMulti-Cloud Commitment Comparison
| Cloud | Commitment Type | 1-Year Savings | 3-Year Savings | Flexibility |
|---|---|---|---|---|
| Azure | Reserved Instances | ~36% | ~60% | Exchange allowed |
| Azure | Savings Plans | ~20% | ~33% | Cross-region, cross-size |
| AWS | Reserved Instances | ~40% | ~60% | Convertible option |
| AWS | Compute Savings Plans | ~30% | ~52% | Any instance family |
| GCP | Committed Use Discounts | ~37% | ~57% | Spend-based option |
| OCI | Capacity Reservations | ~35% | ~50% | Guaranteed capacity |
* Savings percentages are approximate and vary by instance type, region, and payment option
Understanding the Output
Coverage Percentage
The percentage of your compute spend that's covered by commitments (RIs + Savings Plans).
<50%
Critical - Act Now
50-70%
Needs Improvement
70-85%
Healthy Target
Utilization Rate
How much of your existing reservations are being used. Low utilization (<80%) means you're paying for capacity you're not using. Consider exchanging or selling unused RIs.
Break-Even Analysis
Recommendations include break-even points. If a VM has been running consistently for the break-even period (typically 7-9 months for 1yr, 14-18 months for 3yr), the RI will save money.
RI Purchase Decision Framework
Buy 3-Year RIs When:
- • Workload has been stable for 6+ months
- • No planned migration or decommission
- • Maximum savings is priority (55-72% discount)
- • Can pay upfront or partial upfront
Buy 1-Year RIs When:
- • New workloads proving stability
- • Uncertain long-term requirements
- • Want flexibility to resize/exchange
- • 30-40% savings is acceptable
Use Savings Plans When:
- • Workloads change instance types frequently
- • Multi-region deployments
- • Want automatic application of discounts
- • Prefer flexibility over maximum savings
Stay On-Demand When:
- • Workload runs less than 7 months/year
- • Highly variable or unpredictable usage
- • Planning to migrate to different service
- • Dev/test environments with inconsistent usage
Sample Report Output
====================================================================== RESERVED INSTANCE COVERAGE REPORT Generated: 2025-12-15 14:32:17 Analysis Period: Last 30 days ====================================================================== 📊 COST ANALYSIS BY PRICING MODEL -------------------------------------------------- Total Compute Cost: $127,543.00 On-Demand Cost: $89,280.10 Reserved Cost: $31,456.90 Savings Plan Cost: $6,806.00 Spot Cost: $0.00 🎯 COVERAGE: 30.0% 💰 POTENTIAL ANNUAL SAVINGS: Conservative (1yr RI): $321,408.36 Aggressive (3yr RI): $589,248.66 📈 EXISTING RESERVATION INVENTORY -------------------------------------------------- +------------------+----------------+----------+-------+ | reservation_id | sku | quantity | term | +==================+================+==========+=======+ | res-001 | Standard_D4s_v3| 10 | P3Y | | res-002 | Standard_E8s_v3| 5 | P1Y | | res-003 | Standard_D2s_v3| 20 | P1Y | +------------------+----------------+----------+-------+ 🛒 PURCHASE RECOMMENDATIONS -------------------------------------------------- +------------------+------------+----------+-------+---------------+ | sku | location | quantity | term | net_savings | +==================+============+==========+=======+===============+ | Standard_D8s_v5 | eastus | 15 | P1Y | $42,840.00 | | Standard_E4s_v5 | eastus | 8 | P1Y | $18,432.00 | | Standard_D4s_v5 | westus2 | 12 | P3Y | $67,392.00 | +------------------+------------+----------+-------+---------------+ Total Recommended Savings: $128,664.00/year ✅ RECOMMENDED ACTIONS -------------------------------------------------- ⚠️ Coverage is 30.0% - target is 70-80% → Review purchase recommendations above → Start with 1-year terms for new workloads → Consider 3-year terms for stable, long-running VMs ======================================================================
Automate Monthly Reports
Set up automated monthly RI coverage reports using Azure Functions or a simple cron job:
# Run on the 1st of every month at 8 AM
0 8 1 * * /usr/bin/python3 /path/to/ri_coverage_report.py \
--subscription "your-sub-id" \
--output "/reports/ri-coverage-$(date +\%Y\%m).json" \
2>&1 | mail -s "Monthly RI Coverage Report" finops-team@company.comAzure DevOps Pipeline
trigger:
schedules:
- cron: "0 8 1 * *" # 1st of month at 8 AM
displayName: Monthly RI Report
branches:
include:
- main
pool:
vmImage: 'ubuntu-latest'
steps:
- task: AzureCLI@2
inputs:
azureSubscription: 'Your-Service-Connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
pip install azure-identity azure-mgmt-consumption azure-mgmt-reservations pandas tabulate
python ri_coverage_report.py --subscription $(SUBSCRIPTION_ID) --output ri-report.json
- task: PublishBuildArtifacts@1
inputs:
pathtoPublish: 'ri-report.json'
artifactName: 'ri-coverage-report'Pro Tips
Don't over-commit. Target 70-80% coverage, not 100%. Leave room for variable workloads and new projects.
Review quarterly. Run this report every quarter and adjust commitments as your infrastructure evolves.
Stack discounts. Azure Hybrid Benefit + RIs can stack for up to 80%+ savings on Windows VMs.
Exchange unused RIs. Azure allows exchanging RIs for different SKUs. Don't let unused capacity go to waste.