Skip to content

Commit

Permalink
Support of Azure Savings Plan (#19)
Browse files Browse the repository at this point in the history
Support of Azure Savings Plans
  • Loading branch information
marc-perreaut authored Jul 20, 2023
1 parent 127016c commit f8860ea
Show file tree
Hide file tree
Showing 12 changed files with 73 additions and 57 deletions.
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ Furthermore, this also allows service owners to easily identify their main cost
| *Instance* | A running instance or a deployment of a service. If we think of *Services* as *Classes* in object-oriented programming, then *Instances* would be the *Objects*. |
| *Dimension* | An optional dimension in the cost allocation, for example an environment like Test or Production, or a component in the architecture of the service. |
| *Meter* | A meter that measures the functional activity or throughput of a service or of a product. |
| *Amortized Cost* | The billing cost incremented with the amortized cost of the reservation purchases. |
| *On-demand Cost* | The equivalent on-demand cost, i.e. as if no reservation purchase had been made. |
| *Amortized Cost* | The billing cost incremented with the amortized cost of the commitment-based purchases (reservations, saving plans). |
| *On-demand Cost* | The equivalent on-demand cost, i.e. as if no commitment-based purchase had been made. |
| *Cost Item* | A cost that is either coming from cloud provider billing or allocated by a provider service. |
| *Cost Allocation Key* | A numerical value to allocate a share of a cost. Cost share = cost * key / total keys. |

Expand Down Expand Up @@ -90,9 +90,9 @@ Keep calm and drink coffee.
* Assuming that all cloud resources are properly tagged and that services eventually allocate all their costs to products, then the costs of all cloud resources must be equal to the cost of all products
* Similarly, the costs of a product that exclusively uses a single service would be equal to the total cost of that service
8. On-demand costs and amortized costs are supported in parallel by the model
* Reporting the two is useful to investigate whether cost variations come from reservation management or something else
* Reporting the two is also useful for service owners and product managers alike to see the savings made thanks to the reservation purchases
* Reservation savings = On-demand costs - amortized costs
* Reporting the two is useful to investigate whether cost variations come from commitment management or something else
* Reporting the two is also useful for service owners and product managers alike to see the savings made thanks to the commitment-based purchases
* Commitment savings = On-demand costs - amortized costs
9. Cycles in the cost allocation can be broken by providing a *service precedence list*
* Example: A monitoring system *M* running on top of a container service *C* is also used by the container service itself
* In this case, part of the costs of *M* should be assigned back to *C*
Expand Down Expand Up @@ -212,8 +212,13 @@ Product = product
# The service, instance, and dimensions to allocate Azure unused reservation cost
UnusedReservationService = finops
UnusedReservationInstance = reservation
UnusedReservationComponent = unused
UnusedReservationInstance = unused-commitment
UnusedReservationComponent = reservation
# The service, instance, and dimensions to allocate Azure unused savings plan cost
UnusedSavingsPlanService = finops
UnusedSavingsPlanInstance = unused-commitment
UnusedSavingsPlanComponent = savings-plan
[Cycles]
Expand Down
34 changes: 19 additions & 15 deletions cloud_cost_allocation/reader/azure_ea_amortized_cost_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,23 @@ def read_item(self, line) -> CloudCostItem:
return None

# Compute and set on-demand cost (amount with index 1)
# https://docs.microsoft.com/en-us/azure/cost-management-billing/reservations/understand-reserved-instance-usage-ea
if line["ReservationId"]:
if line["ChargeType"] == "UnusedReservation":
cloud_cost_item.cloud_on_demand_cost = 0.0 # Unused reservations are not on-demand costs
# https://learn.microsoft.com/en-us/azure/cost-management-billing/reservations/understand-reserved-instance-usage-ea
# https://learn.microsoft.com/en-us/azure/cost-management-billing/savings-plan/utilization-cost-reports
pricing_model = line["PricingModel"]
charge_type = line["ChargeType"]
if pricing_model in ["Reservation", "SavingsPlan"]:
if charge_type in ["UnusedReservation", "UnusedSavingsPlan"]:
cloud_cost_item.cloud_on_demand_cost = 0.0 # Unused commitments are not on-demand costs
else:
quantity = line["Quantity"]
unit_price = line["UnitPrice"]
if utils.is_float(quantity) and utils.is_float(unit_price):
cloud_cost_item.amounts[1] = float(quantity) * float(unit_price)
else:
error("Quantity or UnitPrice cannot be parsed in line %s", line)
cloud_cost_item.amounts[1] = 0.0 # return None?
cloud_cost_item.amounts[1] = 0.0 # Return None?

else: # No reservation: on-demand cost is same as amortized cost
else: # No unused commitment: on-demand cost is amortized cost
cloud_cost_item.amounts[1] = cloud_cost_item.amounts[0]

# Set currency
Expand All @@ -69,21 +72,22 @@ def read_item(self, line) -> CloudCostItem:
else:
error("Unexpected tag format in cost stream: '" + tag + "'")

# Process unused reservation
# Process unused commitment
config = self.cost_item_factory.config
if line['ChargeType'] == 'UnusedReservation':
cloud_cost_item.service = config.config['AzureEaAmortizedCost']['UnusedReservationService']
if 'UnusedReservationInstance' in config.config['AzureEaAmortizedCost']:
cloud_cost_item.instance = config.config['AzureEaAmortizedCost']['UnusedReservationInstance']
if charge_type in ["UnusedReservation", "UnusedSavingsPlan"]:
cloud_cost_item.service = config.config['AzureEaAmortizedCost'][charge_type + 'Service']
unused_commitment_instance = charge_type + 'Instance'
if unused_commitment_instance in config.config['AzureEaAmortizedCost']:
cloud_cost_item.instance = config.config['AzureEaAmortizedCost'][unused_commitment_instance]
else:
cloud_cost_item.instance = cloud_cost_item.service
for dimension in config.dimensions:
unused_reservation_dimension = 'UnusedReservation' + dimension
if unused_reservation_dimension in config.config['AzureEaAmortizedCost']:
unused_commitment_dimension = charge_type + dimension
if unused_commitment_dimension in config.config['AzureEaAmortizedCost']:
cloud_cost_item.dimensions[dimension] =\
config.config['AzureEaAmortizedCost'][unused_reservation_dimension]
config.config['AzureEaAmortizedCost'][unused_commitment_dimension]

else: # Not an unused reservation
else: # Not an unused commitment

# Fill cost item from tags
self.fill_from_tags(cloud_cost_item)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# Setup
setup(
name='cloud-cost-allocation',
version='1.0.5',
version='1.0.6',
description='Python library for shared, hierarchical cost allocation based on user-defined usage metrics.',
long_description=readme,
long_description_content_type='text/markdown',
Expand Down
6 changes: 3 additions & 3 deletions tests/test1/test1_cloud_cost.csv
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Date,LineNumber,Tags,CostInBillingCurrency,BillingCurrencyCode,ReservationId,ChargeType,ResourceId
03/09/2022,1,"""service"": ""container"""",""""component"": ""compute"""",""""environment"": ""prd1"""",""""partition"": ""0""",80,EUR,,Usage,/Az/virtualMachines/vm1
03/09/2022,1,"""service2"": ""container"""",""""component"": ""storage"""",""""environment"": ""prd2"""",""""partition"": ""1""",20,EUR,,Usage,/Az/disks/disk1
Date,LineNumber,Tags,CostInBillingCurrency,BillingCurrencyCode,PricingModel,ChargeType,ResourceId
03/09/2022,1,"""service"": ""container"""",""""component"": ""compute"""",""""environment"": ""prd1"""",""""partition"": ""0""",80,EUR,OnDemand,Usage,/Az/virtualMachines/vm1
03/09/2022,1,"""service2"": ""container"""",""""component"": ""storage"""",""""environment"": ""prd2"""",""""partition"": ""1""",20,EUR,OnDemand,Usage,/Az/disks/disk1
18 changes: 9 additions & 9 deletions tests/test2/test2_cloud_cost.csv
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
Date,Tags,CostInBillingCurrency,BillingCurrencyCode,ChargeType,ResourceId,ReservationId
03/10/2022,"""service"": ""Application1""",10,EUR,Usage,ResourceId1,
03/10/2022,"""service"": ""Application2""",1000,EUR,Usage,ResourceId2,
03/10/2022,"""service"": ""Application3"",""trx"": ""x""",2,EUR,Usage,ResourceId3x,
03/10/2022,"""service"": ""Application3"",""trx"": ""y""",60,EUR,Usage,ResourceId3y,
03/10/2022,"""service"": ""Application3"",""trx"": ""z""",600,EUR,Usage,ResourceId3z,
03/10/2022,"""service"": ""Application3""",140,EUR,Usage,ResourceId3xyz,
03/10/2022,"""service"": ""Application4""",110,EUR,Usage,ResourceId4,
03/10/2022,"""service"": ""Application5""",7,EUR,Usage,ResourceId5,
Date,Tags,CostInBillingCurrency,BillingCurrencyCode,ChargeType,ResourceId,PricingModel
03/10/2022,"""service"": ""Application1""",10,EUR,Usage,ResourceId1,OnDemand
03/10/2022,"""service"": ""Application2""",1000,EUR,Usage,ResourceId2,OnDemand
03/10/2022,"""service"": ""Application3"",""trx"": ""x""",2,EUR,Usage,ResourceId3x,OnDemand
03/10/2022,"""service"": ""Application3"",""trx"": ""y""",60,EUR,Usage,ResourceId3y,OnDemand
03/10/2022,"""service"": ""Application3"",""trx"": ""z""",600,EUR,Usage,ResourceId3z,OnDemand
03/10/2022,"""service"": ""Application3""",140,EUR,Usage,ResourceId3xyz,OnDemand
03/10/2022,"""service"": ""Application4""",110,EUR,Usage,ResourceId4,OnDemand
03/10/2022,"""service"": ""Application5""",7,EUR,Usage,ResourceId5,OnDemand
2 changes: 1 addition & 1 deletion tests/test3/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ Basic test for:
- Default service
- Instances
- Cost-based and cloud-tag-based cost allocation
- Azure reservations
- Azure reservations and saving plans
- Consumer tag
3 changes: 3 additions & 0 deletions tests/test3/test3.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ ConsumerComponent = consumer_component
UnusedReservationService = finops
UnusedReservationInstance = reservation
UnusedReservationComponent = unused
UnusedSavingsPlanService = finops
UnusedSavingsPlanInstance = savings-plan
UnusedSavingsPlanComponent = unused
2 changes: 2 additions & 0 deletions tests/test3/test3_allocated_cost.csv
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Date,Service,Instance,Tags,AmortizedCost,OnDemandCost,Currency,ProviderService,P
2022-03-11,finops,reservation,,5.0,0.0,EUR,,,,,,,,,,unused,az,N
2022-03-11,finops,reservation,,5.0,0.0,EUR,,,,,,,,,,unused,az,N
2022-03-11,finops,reservation,,10.0,0.0,EUR,,,,,,,,,,unused,az,N
2022-03-11,finops,savings-plan,,5.0,0.0,EUR,,,,,,,,,,unused,az,Y
2022-03-11,shared1,shared1,"service:shared1,reservation_zone:green,cloud_resource_id:resourceid1,",100.0,150.0,EUR,,,,,,,,,,,az,N
2022-03-11,shared1,shared1,,10.0,0.0,EUR,finops,reservation,,CloudTagSelector,100.0,'reservation_zone' in globals() and reservation_zone == 'green',,,,,,N
2022-03-11,shared2,instance1,"service:shared2,instance:instance1,reservation_zone:green,cloud_resource_id:resourceid2,",10.0,15.0,EUR,,,,,,,,,,,az,N
Expand All @@ -17,3 +18,4 @@ Date,Service,Instance,Tags,AmortizedCost,OnDemandCost,Currency,ProviderService,P
2022-03-11,x,x,"cloud_resource_id:resourceid0,",100.0,150.0,EUR,,,,,,,,,,,az,Y
2022-03-11,x,x,,10.0,0.0,EUR,finops,reservation,,Key,100.0,,,,,,,Y
2022-03-11,x,x,"consumer_service:n/a,consumer_instance:-,",55.0,75.0,EUR,shared1,shared1,,Cost,110.0,,,,,,,Y
2022-03-11,y,y,"service:y,cloud_resource_id:resourceid5,",120.0,150.0,EUR,,,,,,,,,,,az,Y
22 changes: 12 additions & 10 deletions tests/test3/test3_cloud_cost.csv
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
Date,Tags,CostInBillingCurrency,BillingCurrencyCode,ChargeType,ResourceId,ReservationId,Quantity,UnitPrice
03/11/2022,,100,EUR,Usage,ResourceId0,ReservationId0,2,75
03/11/2022,"""service"": ""shared1"""",""""reservation_zone"": ""green""",100,EUR,Usage,ResourceId1,ReservationId1,5,30
03/11/2022,"""service"": ""shared2"""",""""instance"": ""instance1"""",""""reservation_zone"": ""green""",10,EUR,Usage,ResourceId2,ReservationId2,1.5,10
03/11/2022,"""service"": ""shared2"""",""""instance"": ""instance1"""",""""reservation_zone"": ""green""",10,EUR,Usage,ResourceId3,ReservationId2,1.5,10
03/11/2022,"""service"": ""shared2"""",""""instance"": ""instance2"""",""""reservation_zone"": ""green""",80,EUR,Usage,ResourceId4,ReservationId3,3,40
03/11/2022,,10,EUR,UnusedReservation,,ReservationId0,,
03/11/2022,,5,EUR,UnusedReservation,,ReservationId1,,
03/11/2022,,5,EUR,UnusedReservation,,ReservationId2,,
03/11/2022,,10,EUR,UnusedReservation,,ReservationId3,,
Date,Tags,CostInBillingCurrency,BillingCurrencyCode,ChargeType,ResourceId,PricingModel,Quantity,UnitPrice
03/11/2022,,100,EUR,Usage,ResourceId0,Reservation,2,75
03/11/2022,"""service"": ""shared1"""",""""reservation_zone"": ""green""",100,EUR,Usage,ResourceId1,Reservation,5,30
03/11/2022,"""service"": ""shared2"""",""""instance"": ""instance1"""",""""reservation_zone"": ""green""",10,EUR,Usage,ResourceId2,Reservation,1.5,10
03/11/2022,"""service"": ""shared2"""",""""instance"": ""instance1"""",""""reservation_zone"": ""green""",10,EUR,Usage,ResourceId3,Reservation,1.5,10
03/11/2022,"""service"": ""shared2"""",""""instance"": ""instance2"""",""""reservation_zone"": ""green""",80,EUR,Usage,ResourceId4,Reservation,3,40
03/11/2022,,10,EUR,UnusedReservation,,Reservation,,
03/11/2022,,5,EUR,UnusedReservation,,Reservation,,
03/11/2022,,5,EUR,UnusedReservation,,Reservation,,
03/11/2022,,10,EUR,UnusedReservation,,Reservation,,
03/11/2022,"""service"": ""y""",120,EUR,Usage,ResourceId5,SavingsPlan,3,50
03/11/2022,,5,EUR,UnusedSavingsPlan,,SavingsPlan,,
14 changes: 7 additions & 7 deletions tests/test4/test4_cloud_cost.csv
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Date,Tags,CostInBillingCurrency,BillingCurrencyCode,ChargeType,ResourceId,ReservationId,
03/17/2022,"""service"": ""shared1"""",""""k"": ""x""",100,EUR,Usage,ResourceId1,
03/17/2022,"""service"": ""shared1"""",""""k"": ""y""",100,EUR,Usage,ResourceId2,
03/17/2022,"""service"": ""shared1"""",""""k"": ""z""",10,EUR,Usage,ResourceId3,
03/17/2022,"""service"": ""shared2"""",""""k"": ""x""",100,EUR,Usage,ResourceId4,
03/17/2022,"""service"": ""shared2"""",""""k"": ""y""",100,EUR,Usage,ResourceId5,
03/17/2022,"""service"": ""shared2"""",""""k"": ""z""",10,EUR,Usage,ResourceId6,
Date,Tags,CostInBillingCurrency,BillingCurrencyCode,ChargeType,ResourceId,PricingModel
03/17/2022,"""service"": ""shared1"""",""""k"": ""x""",100,EUR,Usage,ResourceId1,OnDemand
03/17/2022,"""service"": ""shared1"""",""""k"": ""y""",100,EUR,Usage,ResourceId2,OnDemand
03/17/2022,"""service"": ""shared1"""",""""k"": ""z""",10,EUR,Usage,ResourceId3,OnDemand
03/17/2022,"""service"": ""shared2"""",""""k"": ""x""",100,EUR,Usage,ResourceId4,OnDemand
03/17/2022,"""service"": ""shared2"""",""""k"": ""y""",100,EUR,Usage,ResourceId5,OnDemand
03/17/2022,"""service"": ""shared2"""",""""k"": ""z""",10,EUR,Usage,ResourceId6,OnDemand
4 changes: 2 additions & 2 deletions tests/test5/test5_cloud_cost.csv
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
Date,Tags,CostInBillingCurrency,BillingCurrencyCode,ChargeType,ResourceId,ReservationId,
04/15/2022,"""service"": ""a""",90,EUR,Usage,ResourceId1,
Date,Tags,CostInBillingCurrency,BillingCurrencyCode,ChargeType,ResourceId,PricingModel
04/15/2022,"""service"": ""a""",90,EUR,Usage,ResourceId1,OnDemand
4 changes: 2 additions & 2 deletions tests/test6/test6_cloud_cost.csv
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
Date,Tags,CostInBillingCurrency,BillingCurrencyCode,ChargeType,ResourceId,ReservationId,
07/22/2022,"""service"": ""a""",1000,EUR,Usage,ResourceId1,
Date,Tags,CostInBillingCurrency,BillingCurrencyCode,ChargeType,ResourceId,PricingModel
07/22/2022,"""service"": ""a""",1000,EUR,Usage,ResourceId1,OnDemand

0 comments on commit f8860ea

Please sign in to comment.