AWS True Unblended Calculation – Ripple、Waveの背後にあるコードロジック


こちらの記事は、我々がAWSのコストをどのように分析し計算するかについて、アルファスのプロダクト Ripple と Wave の背後にあるプログラミングロジックを要約しています。
AWSアカウントの利用量を収集し把握するためには、AWS Cost and Usage report(CUR)を有効にし、CSV形式のビッグデータをAWSのマスターアカウント下のS3バケットにダンプするのが一番良い方法です。
これらのデータには、EC2の稼働時間、インスタンスタイプ、リザーブドインスタンス(RI)のレコード、CPU使用率、ストレージI/Oなど、使用状況の詳細と属性が全て含まれています

背景

2018年、私たちは、AWSのコストの計算と分析を行うツール Ripple と Wave の提供を開始しました。

それら二つのサービスは、共通の(アカウントの利用量とコストを計算するための)ロジックが適用されています。
そのロジックとは、インスタンスへのRIの適用、(同じ AWS Organizations 直下の)リンクされたアカウント間でのRIの再割り当て、スポットインスタンスまたはオンデマンドインスタンスのハンドリング、データ転送とDynamoDB他、AWSが提供する全ての製品の計算を行います。

我々の挑戦

AWSアカウントの利用量を収集し把握するためには、AWS Cost and Usage report(CUR)を有効にし、CSV形式のデータをAWSのマスターアカウント下のS3バケットにダンプします。
これらのデータには、EC2の稼働時間、インスタンスタイプ、リザーブドインスタンス(RI)のレコード、CPU使用率、ストレージI/Oなど、使用状況の詳細と属性が全て含まれています。

通常のAWSアカウントの場合、ひと月分の使用量が1つのCSVに含まれ、大抵数百MB程度です。
大規模にAWSを利用されるアカウントであれば、それぞれ1GB程度の複数のCSVファイルに分割されたギガバイト単位のデータが生成されることがあります。

CSVファイルの中身の各行はLineItemと呼ばれ、1時間あたりのサービス利用状況を表します。
例:

(一つのアカウントの利用量に、上記のようなLineItemが何百万も含まれていると想像してください。)

ビックデータレポートの分析または可視化のために、AWSは Redshift データウェアハウスまたは quicksight を用意しています。
しかし、二つの大きな課題があるように見えます。

– 実装するためのプログラミング知識が要求され、さらに維持することが難しい
– EC2、RDSでなどで購入できるRIを適用した再計算に関して、柔軟性がない

EC2の再計算

以下の状況を検討してみます。

MSPもしくはエンタープライズ企業が、顧客やグループ企業に請求書を発行する必要があるとします。

マスターアカウント A と、その下に 3 つのメンバーアカウント B, C, D がいるとします。

アカウント B は RI を複数所有していますが、全てを消費しませんでした。

アカウント C と D は RI を所有していませんが、アカウント A の余りの RI を消費しました。その結果、想定よりも低いコストとなります。

マスターアカウント A は AWS より請求書を受けとり、メンバーアカウント B, C, D へ正しい使用料で請求書を発行したいとします。

B: RI が適用された請求書

C: RI が適用されていない請求書

D: RI が適用されていない請求書

上記の場合、AWSから受け取った請求書は機能しません。
非ブレンドコスト、ブレンドコストのどちらを使用しても、合計は常に請求額したい金額よりも少なくなります。
つまり自分自身でEC2のコストを再計算する状況です。

私たちの会社では、そのコスト計算を True Unblended と呼んでいます。

その概念はかなり単純です:

EC2インスタンスの稼働時間 x 一時間当たりのインスタンス料金 = コスト

しかし、計算にその概念を適用すると、数式に影響を与える可能性のある要因が多数存在することにすぐに気がつくでしょう。

– Instance Type
– Size-flexibility(リージョンを考慮したLinux/Unix RI)
– Availability Zone
– Operation(オペレーションごとに価格設定は異なります)
– UsageType(オンデマンド、スポット、ディスク使用量…)
– その他色々…

計算に数式を適用する時は、全ての特質を考慮しなければなりません。
また、全てのデータは元のCSV形式のCURで定義されているので、CSVデータの各列が何を表しているのかを知っておくと便利です。その結果、必要なLineItemをフィルタできます。

lineItem/UsageStartDate <- The start date of daily calculation, eg: "2018-08-01T00:00:00Z"

lineItem/UsageEndDate

bill/BillingEntity <- who is the seller, eg: "AWS", "AWSMarketplace"

bill/PayerAccountId <- Payer ID

lineItem/UsageAccountId <- Linked account ID

lineItem/LineItemType <- eg: "Usage", "RIFee", "Credit", "Refund", "Fee", "Tax". 

lineItem/ProductCode <- eg: "AmazonEC2"

product/ProductName <- eg: "Amazon Elastic Compute" in full name

lineItem/UsageType <- eg: "APN1-EBS:SnapshotUsage"

lineItem/Operation <- eg: "RunInstances:0001"

lineItem/UsageAmount <- The usage quantity for specific time range

lineItem/UnblendedRate <- unblended unit price

lineItem/BlendedRate <- blended unit price

lineItem/UnblendedCost <- cost based on unblended unit price [*]

lineItem/BlendedCost <- cost based on blended unit price [*]

product/instanceType <- eg: "t2.micro", "db.m4.large"

product/region <- eg: "ap-northeast-1"

上記の列をフィルタリングしてEC2を再計算するために使用するデータを取得したら、
リージョン、アベイラビリティゾーン、オペレーション、UsageType、インスタンスタイプを考慮したEC2稼働時間の合計を分離してまとめる準備が整いました。
以下は、各LineItemにループして上記の同じベクトルを組み合わせるときに使用されるPHPコードのサンプルです。

/**
 * Handle EC2 Compute Instances, and Dedicated Hosts
 * Put results into a separated array: $this->AmazonEC2['ec2']
 *
 * Note: with a dedicated host, you purchase an entire physical host from AWS and that host is billed to you on an hourly basis just like EC2 instances are billed.
 * Dedicated hosts have the "InstanceType" with instance family prefix naming only, eg: "t3", "m4".. (there is no such "t3.medium" alike namings)
 * Dedicated hosts have the "Tenancy" as "N/A" as always and there is no size-flexibility
 */
if (
    $ProductFamily == "Dedicated Host" ||
    (
        $ProductFamily == "Compute Instance" &&
        strpos($Operation, "RunInstances") !== false
    )
) {

    $result = [
        "ProductFamily" => $ProductFamily,
        "UsageType" => $UsageType,
        "InstanceType" => $InstanceType,
        "Operation" => $Operation,
        "Tenancy" => $Tenancy,
        "AvailabilityZone" => $AvailabilityZone,
        "CostBeforeTax" => bcadd((string)$UnblendedCost, "0", 10),
        "ItemDescription" => $Description,
        "NormalizationFactor" => $NormalizationFactor,
        "UsageQuantity" => bcadd((string)$UsageAmount, "0", 10),
        "NormalizedUsageQuantity" => bcadd((string)$NormalizedUsageAmount, "0", 10),
        "NormalizedUsageQuantity_OverClock" => bcadd("0", "0", 10),
        "NormalizedUsageQuantity_Hrly" => [ $UsageStartDate .'|'. $UsageEndDate => bcadd((string)$NormalizedUsageAmount, "0", 10) ],
    ];

    if (empty($this->AmazonEC2['ec2'][$UsageRegion])) {

        $this->AmazonEC2['ec2'][$UsageRegion][] = $result;

    } else {

        foreach ($this->AmazonEC2['ec2'][$UsageRegion] as $k => $v) {

            if (
                $v["UsageType"] == $UsageType &&
                $v["InstanceType"] == $InstanceType &&
                $v["Operation"] == $Operation &&
                $v["Tenancy"] == $Tenancy &&
                $v["AvailabilityZone"] == $AvailabilityZone &&
                $v["NormalizationFactor"] == $NormalizationFactor
            ) {

                $this->AmazonEC2['ec2'][$UsageRegion][$k]['CostBeforeTax'] = bcadd($this->AmazonEC2['ec2'][$UsageRegion][$k]['CostBeforeTax'], (string)$UnblendedCost, 10);
                $this->AmazonEC2['ec2'][$UsageRegion][$k]['UsageQuantity'] = bcadd($this->AmazonEC2['ec2'][$UsageRegion][$k]['UsageQuantity'], (string)$UsageAmount, 10);
                $this->AmazonEC2['ec2'][$UsageRegion][$k]['NormalizedUsageQuantity'] = bcadd($this->AmazonEC2['ec2'][$UsageRegion][$k]['NormalizedUsageQuantity'], (string)$NormalizedUsageAmount, 10);
                $this->AmazonEC2['ec2'][$UsageRegion][$k]['NormalizedUsageQuantity_Hrly'][$UsageStartDate .'|'. $UsageEndDate] = bcadd($this->AmazonEC2['ec2'][$UsageRegion][$k]['NormalizedUsageQuantity_Hrly'][$UsageStartDate .'|'. $UsageEndDate], (string)$NormalizedUsageAmount, 10);

                return;
            }

        }

        $this->AmazonEC2['ec2'][$UsageRegion][] = $result;

    }

}

 

AWSアカウントのCURデータで上記のコードを実行した結果は次のようになります。

Array
(
    [ap-northeast-1] => Array
        (
            [0] => Array
                (
                    [ProductFamily] => Compute Instance
                    [UsageType] => APN1-BoxUsage:t3.micro
                    [InstanceType] => t3.micro
                    [Operation] => RunInstances
                    [Tenancy] => Shared
                    [AvailabilityZone] => ap-northeast-1a
                    [CostBeforeTax] => 9.7920000000
                    [ItemDescription] => $0.0136 per On Demand Linux t3.micro Instance Hour
                    [NormalizationFactor] => 0.5
                    [UsageQuantity] => 720.0000000000
                    [NormalizedUsageQuantity] => 360.0000000000
                    [NormalizedUsageQuantity_OverClock] => 0.0000000000
                )

        )

    [us-west-2] => Array
        (
            [0] => Array
                (
                    [ProductFamily] => Compute Instance
                    [UsageType] => USW2-BoxUsage:t2.micro
                    [InstanceType] => t2.micro
                    [Operation] => RunInstances
                    [Tenancy] => Shared
                    [AvailabilityZone] => us-west-2a
                    [CostBeforeTax] => 8.3520000000
                    [ItemDescription] => Linux/UNIX (Amazon VPC), t2.micro reserved instance applied
                    [NormalizationFactor] => 0.5
                    [UsageQuantity] => 720
                    [NormalizedUsageQuantity] => 360
                    [NormalizedUsageQuantity_OverClock] => 360
                )

            [1] => Array
                (
                    [ProductFamily] => Compute Instance
                    [UsageType] => USW2-BoxUsage:t2.micro
                    [InstanceType] => t2.micro
                    [Operation] => RunInstances
                    [Tenancy] => Shared
                    [AvailabilityZone] => us-west-2c
                    [CostBeforeTax] => 8.3520000000
                    [ItemDescription] => $0.0116 per On Demand Linux t2.micro Instance Hour
                    [NormalizationFactor] => 0.5
                    [UsageQuantity] => 0
                    [NormalizedUsageQuantity] => 0
                    [NormalizedUsageQuantity_OverClock] => 360
                )

        )

)

 

上記の合計使用量の結果から、各リージョンの各インスタンス使用量の合計を取得します。
次に行うことは以下です:

  • 1時間ごとにRIの使用制限のチェックを行う(RIの1時間ごとの制限とは、複数の対象インスタンスが同時に実行されている場合、リザーブドインスタンスの課金特典は、1時間に最大3,600秒まで、全てのインスタンスに同時に適用されます。それ以降は、オンデマンド料金が適用されます。)
  • マッチするインスタンスにRIを適用する
    • RIのインスタンスサイズの柔軟性を考慮する(ゾーンRI/リージョンRIの違いについて。リージョンRIはサイズの柔軟性を持ちますが、ゾーンRIはそれがありません。)
    • ゾーンRI/リージョンRIの検討
    • クロスアカウント共有の検討
    • オンデマンド稼働時間を適切にカバーするためにRIを適用する

該当のコードは非常に長いので、ここに全部貼り付けることはしません。
しかし、インスタンスオペレーションはそれぞれで価格が異なるため、正しくオペレーションを考慮する必要があります。

case 'RunInstances':
    return 'Amazon EC2 running Linux/UNIX';
    break;
case 'RunInstances:000g':
    return 'Amazon EC2 running SUSE Linux';
    break;
case 'RunInstances:0010':
    return 'Amazon EC2 running Red Hat Enterprise Linux';
    break;
case 'RunInstances:0002':
    return 'Amazon EC2 running Windows';
    break;
case 'RunInstances:0006':
    return 'Amazon EC2 running Windows with SQL Server Standard';
    break;
case 'RunInstances:0102':
    return 'Amazon EC2 running Windows with SQL Server Enterprise';
    break;
case 'RunInstances:0202':
    return 'Amazon EC2 running Windows with SQL Server Web';
    break;
case 'RunInstances:0800':
    return 'Amazon EC2 running Windows (Bring your own license)';
    break;

適切なRIが適用されると、そのアカウントの正しい費用がJSON形式で取得されます。

{
    "ap-northeast-1": [
        {
            "ProductFamily": "Compute Instance",
            "UsageType": "APN1-BoxUsage:t3.micro",
            "InstanceType": "t3.micro",
            "Operation": "RunInstances",
            "Tenancy": "Shared",
            "AvailabilityZone": "ap-northeast-1a",
            "CostBeforeTax": "9.7920000000",
            "ItemDescription": "$0.0136 per on-demand t3.micro EC2 Linux\/UNIX instance hour (or partial hour)",
            "NormalizationFactor": 0.5,
            "UsageQuantity": "720.0000000000",
            "NormalizedUsageQuantity": "360.0000000000"
        }
    ],
    "us-west-2": [
        {
            "ProductFamily": "Compute Instance",
            "UsageType": "USW2-BoxUsage:t2.micro",
            "InstanceType": "t2.micro",
            "Operation": "RunInstances",
            "Tenancy": "Shared",
            "AvailabilityZone": "us-west-2a",
            "CostBeforeTax": "0",
            "ItemDescription": "t2.micro Reserved Instance applied.",
            "NormalizationFactor": 0.5,
            "UsageQuantity": "0",
            "NormalizedUsageQuantity": "0"
        },
        {
            "ProductFamily": "Compute Instance",
            "UsageType": "USW2-BoxUsage:t2.micro",
            "InstanceType": "t2.micro",
            "Operation": "RunInstances",
            "Tenancy": "Shared",
            "AvailabilityZone": "us-west-2c",
            "CostBeforeTax": "0",
            "ItemDescription": "t2.micro Reserved Instance applied.",
            "NormalizationFactor": 0.5,
            "UsageQuantity": "0",
            "NormalizedUsageQuantity": "0"
        },
        {
            "ProductFamily": "Compute Instance",
            "UsageType": "USW2-BoxUsage:t2.micro",
            "InstanceType": "t2.micro",
            "Operation": "RunInstances",
            "Tenancy": "Shared",
            "AvailabilityZone": "us-west-2a",
            "CostBeforeTax": "33.4080000000",
            "ItemDescription": "$0.0116 per on-demand t2.micro EC2 Linux\/UNIX instance hour (or partial hour)",
            "NormalizationFactor": 0.5,
            "UsageQuantity": 2880,
            "NormalizedUsageQuantity": 1440,
            "OverClockHour": true
        },
        {
            "ProductFamily": "Compute Instance",
            "UsageType": "USW2-BoxUsage:t2.micro",
            "InstanceType": "t2.micro",
            "Operation": "RunInstances",
            "Tenancy": "Shared",
            "AvailabilityZone": "us-west-2c",
            "CostBeforeTax": "8.3520000000",
            "ItemDescription": "$0.0116 per on-demand t2.micro EC2 Linux\/UNIX instance hour (or partial hour)",
            "NormalizationFactor": 0.5,
            "UsageQuantity": 720,
            "NormalizedUsageQuantity": 360,
            "OverClockHour": true
        },
        {
            "RIFee": "yes",
            "UsageType": "USW2-BoxUsage:t2.micro",
            "InstanceType": "t2.micro",
            "Operation": "RunInstances",
            "UsageQuantity": "720.0000000000",
            "CostBeforeTax": "2.4480000000",
            "ItemDescription": "$0.0034 hourly fee per reserved Standard 1-Year Partial Upfront t2.micro instance"
        },
        {
            "RIFee": "yes",
            "UsageType": "USW2-BoxUsage:t2.micro",
            "InstanceType": "t2.micro",
            "Operation": "RunInstances",
            "UsageQuantity": "720.0000000000",
            "CostBeforeTax": "2.8080000000",
            "ItemDescription": "$0.0039 hourly fee per reserved Convertible 1-Year Partial Upfront t2.micro instance"
        }
    ]
}

上記の結果をGUIで表示すると、次の図のようになります。
(AWSのビリングレポートのダッシュボードと非常によく似ています)

もちろん、正規化係数、マルチ/シングルAZ配置、無料利用枠など、EC2, RDS, ElastiCache, etc の再計算を引き起こす要因はたくさんありますが、全体的な考え方はこの記事で説明しています。
理解するのは簡単ですが、達成することもまた複雑です。
AWSのドキュメントを読み、CURデータを時間をかけて分析することが必要です。

ビックデータ関連の技術は急成長しています。
貴重なデータをインテリジェントに掘り下げて人間の読める構造にまとめることができれば、全てのAWSのユーザにとって、予算をより深く理解し、管理し、予測するための有益な情報が得られるでしょう。

Related Posts