亚马逊AWS官方博客

解析 AWS 详细账单中对于跨账户使用预留实例的计费方法

需求背景

一个企业客户使用了多个AWS账号,但只使用一个AWS计费账号。在月结账单中发现了一个奇怪的现象:账单详单中记录的每个账号的当月总费用与各个账号实际支付的费用之间有比较大的差异,但是实际发生的月结总费用与详单中的总费用是匹配的。

当用户打开账单详单功能后,每个月会收到两个CSV文件:

  1. 计费账号-aws-billing-csv-.csv
  2. 计费账号-aws-billing-detailed-line-items-with-resources-and-tags-ACTS-.csv

第一个文件是汇总文件,比较小,里面对每个账号的月费用的汇总与实际收费一致。第二个文件是详细账单文件,比较大,里面对每个账号的月费用的汇总也许会与第一个文件不一致。

什么时候会发生两个文件的汇总信息不一致呢,我们发现如果一个账号A中购买了一种型号的预留实例,但是并没有完全使用,如果另一个账号B也有同样型号的实例需求,也许会使用账号A中剩余的预留实例。这种情况的发生有一定的随机性,无法提前预测,所以就有发生了两个账单中的费用不一致的情况。账单不一致的另一个原因是第一个文件中的费用是按照“谁拥有谁付费”的原则。上面例子中A账号使用了B账号的资源,那么费用还是记在B账号。而第二个文件中的费用是按照“谁使用谁付费”的原则,上面例子中A账号使用了B账号的资源,费用记在了A账号。

那么怎样才能识别哪些资源的费用是发生在其他账号(使用了其他账号的预留实例)?如何利用上面说的第二个文件来还原实际账号发生的费用呢?

 

方案概述

要查看预留实例(以下简称RI)是在哪个账号购买的,可以依据SubscriptionId。

我们只要记下账号(LinkedAccountId)与SubscriptionId的对应关系,就可以根据SubscriptionId识别这个RI应该是由哪个账号购买的。

每个资源都有一个对应的SubscriptionId。对于不同类型的RI,查找其对应的SubscriptionId是不一样的:

  1. 对于Amazon ElastiCache、Amazon Elasticsearch Service、AmazonRDS和AmazonRedshift,需要在详细账单文件中查看UsageType,如果属于上述四个类型的某个资源的UsageType包含HeavyUsage的字样,那么这个资源就是RI,其对应的SubscriptionId就是我们要找的。
  2. 对于Amazon Elastic Compute Cloud, 需要在详细账单文件中查看ItemDescription字段,如果该字段包含“认购”字样,则这个资源就是RI,需要记下其对应的SubscriptionId和LinkedAccountId。

我们对整个详细账单文件进行遍历后,就可以形成一个针对RI资源的表格,这个表格有两列,一列是LinkedAccountId,另一列是SubscriptionId。为了表示这个账号是购买RI的账号,我们将LinkedAccountId改名为OwnedAccountId。

下面这些Python语句就是实现这个功能的:

bill=pd.read_csv(billing_file,converters=StringConverter())
bill.columns = bill.columns.str.replace(':', '')
q1 = """
   SELECT DISTINCT LinkedAccountId,SubscriptionId
   FROM
   bill
   WHERE ((ItemDescription LIKE '%认购%')and (ProductName ='Amazon Elastic Compute Cloud')and (SubscriptionId <> ""))
    OR ((UsageType LIKE '%HeavyUsage%')and (ProductName ='Amazon ElastiCache')and (SubscriptionId <> ""))
    OR ((UsageType LIKE '%HeavyUsage%')and (ProductName ='Amazon Elasticsearch Service')and (SubscriptionId <> ""))
    OR ((UsageType LIKE '%HeavyUsage%')and (ProductName ='AmazonRDS')and (SubscriptionId <> ""))
    OR ((UsageType LIKE '%HeavyUsage%')and (ProductName ='AmazonRedshift')and (SubscriptionId <> ""))
   ;"""
accountID_subscriptionId=pysql(q1)
accountID_subscriptionId.to_excel('accountID_subscriptionId.xlsx', index=False)
accountID_subscriptionId.rename(columns = {'LinkedAccountId':'OwnedAccountId'}, inplace = True) 

有了上面这个RI资源的信息表(accountID_subscriptionId),下面就可以根据详单文件还原实际账号发生的费用了。

首先,我们先将RI资源的信息表与详细账单做一个JOIN,这样,在详细账单中我们就会多得到一列信息-OwnedAccountId。

merge = bill.merge(accountID_subscriptionId, left_on="SubscriptionId",right_on="SubscriptionId", how='left')

合并后的详单文件为merge。

其次,我们汇总一下发生在本账号内的费用,这些费用保存在表格cost1中:

q_bill="""
   SELECT * FROM  merge
   where (RecordType Like'%LineItem%')
   ;"""

my_bill=pysql(q_bill)

q_cost1= """
   SELECT LinkedAccountId,SUM(UnBlendedCost)
   FROM
   my_bill
   where (LinkedAccountId=OwnedAccountId) or (OwnedAccountId IS NULL)
   GROUP BY LinkedAccountId
   ;"""
   
cost1=pysql(q_cost1)
cost1.to_excel('cost1.xlsx', index=False)

 

然后,我们再根据OwnedAccountId汇总一下费用,这部分费用就是所有借用了其他账号的RI所发生的费用,我们保存在表格cost2中:

q_cost2= """
   SELECT OwnedAccountId,SUM(UnBlendedCost)
   FROM
   my_bill
   where (LinkedAccountId<>OwnedAccountId) AND (OwnedAccountId IS NOT NULL)
   GROUP BY OwnedAccountId
   ;"""
cost2=pysql(q_cost2)
cost2.to_excel('cost2.xlsx', index=False)

 

最后,将这两个表格的数据加在一起,就是实际账号发生的费用。这个汇总的数据应该与“需求背景”中提到的第一个文件的数据是一致的:

cost1.set_index('LinkedAccountId',inplace = True)
cost1.astype('float').dtypes
cost2.set_index('OwnedAccountId',inplace = True)
cost2.astype('float').dtypes
total=cost1.add(cost2, fill_value=0)
total.to_excel('total.xlsx', index=True)

 

局限

由于详细账单文件和汇总文件都会有计算误差,在账单文件中会以“Rounding”标示,且两个文件中的误差并不一致,我们无法将这些误差完全还原到每个账号的费用中。所以,当您使用我们提供的程序计算出的结果与汇总文件的结果相比,会存在微小的差异。

 

源代码

import pandas as pd
from pandasql import *

billing_file='详单文件.csv'
def pysql(q): return sqldf(q, globals())
class StringConverter(dict):
    def __contains__(self, item):
        return True

    def __getitem__(self, item):
        return str

    def get(self, default=None):
        return str

bill=pd.read_csv(billing_file,converters=StringConverter())

bill.columns = bill.columns.str.replace(':', '')

q1 = """
   SELECT DISTINCT LinkedAccountId,SubscriptionId
   FROM
   bill
   WHERE ((ItemDescription LIKE '%认购%')and (ProductName ='Amazon Elastic Compute Cloud')and (SubscriptionId <> ""))
    OR ((UsageType LIKE '%HeavyUsage%')and (ProductName ='Amazon ElastiCache')and (SubscriptionId <> ""))
    OR ((UsageType LIKE '%HeavyUsage%')and (ProductName ='Amazon Elasticsearch Service')and (SubscriptionId <> ""))
    OR ((UsageType LIKE '%HeavyUsage%')and (ProductName ='AmazonRDS')and (SubscriptionId <> ""))
    OR ((UsageType LIKE '%HeavyUsage%')and (ProductName ='AmazonRedshift')and (SubscriptionId <> ""))
   ;"""
accountID_subscriptionId=pysql(q1)
accountID_subscriptionId.to_excel('accountID_subscriptionId.xlsx', index=False)
accountID_subscriptionId.rename(columns = {'LinkedAccountId':'OwnedAccountId'}, inplace = True) 
merge = bill.merge(accountID_subscriptionId, left_on="SubscriptionId",
                      right_on="SubscriptionId", how='left')
q_bill="""
   SELECT *
   FROM
   merge
   where (RecordType Like'%LineItem%')
   ;"""

my_bill=pysql(q_bill)

q_cost1= """
   SELECT LinkedAccountId,SUM(UnBlendedCost)
   FROM
   my_bill
   where (LinkedAccountId=OwnedAccountId) or (OwnedAccountId IS NULL)
   GROUP BY LinkedAccountId
   ;"""
   
cost1=pysql(q_cost1)
cost1.to_excel('cost1.xlsx', index=False)

q_cost2= """
   SELECT OwnedAccountId,SUM(UnBlendedCost)
   FROM
   my_bill
   where (LinkedAccountId<>OwnedAccountId) AND (OwnedAccountId IS NOT NULL)
   GROUP BY OwnedAccountId
   ;"""
cost2=pysql(q_cost2)
cost2.to_excel('cost2.xlsx', index=False)
cost1.set_index('LinkedAccountId',inplace = True)
cost1.astype('float').dtypes
cost2.set_index('OwnedAccountId',inplace = True)
cost2.astype('float').dtypes
total=cost1.add(cost2, fill_value=0)
total.to_excel('total.xlsx', index=True)

 

本篇作者

刘育新

AWS ProServe 团队高级顾问,长期从事企业客户入云解决方案的制定和项目的实施工作。