GKEの名前空間ごとの課金額算出

はじめに

Google Kubernetes Engine(GKE)は、Google Cloud Platform(GCP)で提供されているKubernetesのマネージドサービスです。GKEの日々の課金額は、GCPの料金レポートを見れば確認することができます。しかし、このGKEの課金額は名前空間(namespace)ごとに分かれていないため、特定の名前空間に想定以上の料金がかかっているか否かを監視できません。そこでこの問題を解決するために、GCPの公式の機能を活用し、GKEの名前空間ごとの課金額をBigQueryにより算出しました。

BigQueryには予め、GCPの詳細な課金データとGKEのリソースデータをエクスポートしておく必要があります。データのエクスポートには、公式で用意されている機能を利用します。

Cloud Billing データを BigQuery にエクスポートする

クラスタ リソースの使用方法について

クエリ

以下が、GKEの名前空間ごとの課金額を算出するクエリです。プロジェクトIDやテーブル名は、ご自身のものに置き換えてください。

DECLARE target_project_id STRING;
DECLARE target_timezone STRING;
DECLARE target_start_date DATE;
DECLARE target_end_date DATE;

DECLARE partitiontime_start_limit DATE;
DECLARE partitiontime_end_limit DATE;
DECLARE start_time_limit TIMESTAMP;
DECLARE end_time_limit TIMESTAMP;

# 対象プロジェクト
SET target_project_id = "cluster-gcp-project";
SET target_timezone = "Asia/Tokyo";
# 対象期間開始日 -- example: PARSE_DATE("%F", "2021-04-01")
SET target_start_date = DATE_ADD(DATE_TRUNC(CURRENT_DATE(target_timezone), MONTH), INTERVAL -2 MONTH);
# 対象期間終了日 -- example: PARSE_DATE("%F", "2021-04-30")
SET target_end_date = LAST_DAY(CURRENT_DATE(target_timezone));

# パーティション開始日
SET partitiontime_start_limit = DATE_ADD(target_start_date, INTERVAL -1 DAY);
# パーティション終了日
SET partitiontime_end_limit = target_end_date;
# リソース使用開始日時
SET start_time_limit = TIMESTAMP(target_start_date, target_timezone);
# リソース使用終了日時
SET end_time_limit = TIMESTAMP(target_end_date, target_timezone);

WITH billing_sku_costs AS (
    -- 課金データのテーブルから、Stock Keeping Unit(SKU)ごとの課金額と使用量を取得
    SELECT
        sku.id AS sku_id,
        ANY_VALUE(sku.description) AS sku_description,
        SUM(cost) AS cost,
        ANY_VALUE(currency) AS currency,
        SUM(usage.amount) AS usage_amount,
        ANY_VALUE(usage.unit) AS usage_unit
    FROM
        `billing-gcp-project.billing-dataset.billing-table`
    WHERE
        DATE(_PARTITIONTIME) >= partitiontime_start_limit
        AND DATE(_PARTITIONTIME) <= partitiontime_end_limit
        AND usage_start_time >= start_time_limit
        AND usage_end_time <= end_time_limit
        AND project.id = target_project_id
    GROUP BY
        sku_id
    ORDER BY
        sku_id
),  billing_sku_credits AS (
    -- 課金データのテーブルから、SKDごとの割引額を取得
    SELECT
        sku.id AS sku_id,
        ANY_VALUE(sku.description) AS sku_description,
        SUM(credits.amount) AS credit_amount,
        ANY_VALUE(currency) AS currency,
    FROM
        `billing-gcp-project.billing-dataset.billing-table`
    CROSS JOIN
        UNNEST(credits) AS credits
    WHERE
        DATE(_PARTITIONTIME) >= partitiontime_start_limit
        AND DATE(_PARTITIONTIME) <= partitiontime_end_limit
        AND usage_start_time >= start_time_limit
        AND usage_end_time <= end_time_limit
        AND project.id = target_project_id
    GROUP BY
        sku_id
    ORDER BY
        sku_id
), gke_ns_sku_usage AS (
    -- GKEリソース使用量のテーブルから、日付・名前空間・SKDの組み合わせに対する使用量を取得
    SELECT
        DATE(start_time, target_timezone) AS start_date,
        namespace,
        sku_id,
        SUM(usage.amount) AS usage_amount,
        ANY_VALUE(usage.unit) AS usage_unit,
        resource_name,
        cluster_name,
        cluster_location
    FROM
        `cluster-gcp-project.usage-metering-dataset.gke_cluster_resource_usage`
    WHERE
        DATE(_PARTITIONTIME) >= partitiontime_start_limit
        AND DATE(_PARTITIONTIME) <= partitiontime_end_limit
        AND start_time >= start_time_limit
        AND end_time <= end_time_limit
    GROUP BY
        start_date,
        namespace,
        sku_id,
        resource_name,
        cluster_name,
        cluster_location
), gke_sku_usage AS (
    -- GKEリソース使用量のテーブルから、SKDの使用量を取得
    SELECT
        sku_id,
        SUM(usage.amount) AS usage_amount,
        ANY_VALUE(usage.unit) AS usage_unit
    FROM
        `cluster-gcp-project.usage-metering-dataset.gke_cluster_resource_usage`
    WHERE
        DATE(_PARTITIONTIME) >= partitiontime_start_limit
        AND DATE(_PARTITIONTIME) <= partitiontime_end_limit
        AND start_time >= start_time_limit
        AND end_time <= end_time_limit
    GROUP BY
        sku_id
)

-- 日付・名前空間・SKDの組み合わせに対する課金額を計算
SELECT
    gke_ns_sku_usage.start_date,
    gke_ns_sku_usage.namespace,
    gke_ns_sku_usage.sku_id,
    (billing_sku_costs.cost + IFNULL(billing_sku_credits.credit_amount, 0))
        * (gke_sku_usage.usage_amount / billing_sku_costs.usage_amount)
        * (gke_ns_sku_usage.usage_amount / gke_sku_usage.usage_amount)
    AS namespace_sku_billing,
    billing_sku_costs.sku_description,
    gke_ns_sku_usage.resource_name,
    gke_ns_sku_usage.cluster_name,
    gke_ns_sku_usage.cluster_location
FROM gke_ns_sku_usage
LEFT JOIN
    gke_sku_usage
ON
    gke_ns_sku_usage.sku_id = gke_sku_usage.sku_id
LEFT JOIN
    billing_sku_costs
ON
    gke_ns_sku_usage.sku_id = billing_sku_costs.sku_id
LEFT JOIN
    billing_sku_credits
ON
    gke_ns_sku_usage.sku_id = billing_sku_credits.sku_id
ORDER BY
    gke_ns_sku_usage.start_date,
    gke_ns_sku_usage.namespace,
    billing_sku_costs.sku_description

使い方

  • target_projrct_idに、GKEを利用しているプロジェクトを指定してください。複数プロジェクトのGKEをまとめて対象にする場合は、WHERE句でのプロジェクトIDを指定する条件を削除してください。
  • target_timezoneに、使用したいタイムゾーンを設定してください。
  • 課金額を算出したい期間を指定してください。target_start_dateが開始日、target_end_dateが終了日です。

課金額は、日付・名前空間・SKUの組に対して算出されます。出力結果は以下の通りです。

  • 使用日
  • 名前空間
  • SKUのID
  • 課金額(割引適用済み)
  • SKUの説明(名前)
  • 使用したリソースの名前(抽象的な種類)
  • 名前空間が所属するクラスタの名前
  • 名前空間が所属するクラスタのロケーション

解説

まず初めに、本記事のクエリは公式ドキュメントの「名前空間で分類されたコストをクエリで取得する方法」を参考に作成しましたが、独自のものであるため、算出の正確さは保証しません。私が試した限りでは、多少のズレはあるものの概ね合っていると感じました。おそらく、完全に正確に算出することはできないでしょう。

クエリの中身を見てきます。名前空間ごとの課金額を算出するSELECT句の前にWITH句があり、そのWITH句は4つのブロックから成り立っています。それぞれブロックについて説明していきます。


WITH句

1つ目のブロックでは、課金データのテーブルから、SKDごとの課金額と使用量を取得しています。後で、このSKUごとの課金額(実際は2つ目のブロックの割引額を足した額)を名前空間で按分することになります。SKUというのは、簡単に説明すると、課金額を計算するために必要な、詳細なリソースの単位です。GKEで利用するSKUの例として、以下のものがあります。

N1 Predefined Instance Core running in Japan
N1 Predefined Instance Ram running in Japan
Preemptible N1 Predefined Instance Core running in Japan
Preemptible N1 Predefined Instance Ram running in Japan
Storage PD Capacity in Japan

ちなみに、SKU使用量の単位は、その種類によって異なり、CPUはseconds、メモリとストレージはbyte-secondsです。

SELECT
    sku.id AS sku_id,
    ANY_VALUE(sku.description) AS sku_description,
    SUM(cost) AS cost,
    ANY_VALUE(currency) AS currency,
    SUM(usage.amount) AS usage_amount,
    ANY_VALUE(usage.unit) AS usage_unit
FROM
    `billing-gcp-project.billing-dataset.billing-table`
WHERE
    DATE(_PARTITIONTIME) >= partitiontime_start_limit
    AND DATE(_PARTITIONTIME) <= partitiontime_end_limit
    AND usage_start_time >= start_time_limit
    AND usage_end_time <= end_time_limit
    AND project.id = target_project_id
GROUP BY
    sku_id
ORDER BY
    sku_id

2つ目のブロックでは、課金データのテーブルから、SKDごとの割引額を取得しています。この割引額は負の値で、後の計算では、1つ目のブロックで取得した課金額に足すことになります。

SELECT
    sku.id AS sku_id,
    ANY_VALUE(sku.description) AS sku_description,
    SUM(credits.amount) AS credit_amount,
    ANY_VALUE(currency) AS currency,
FROM
    `billing-gcp-project.billing-dataset.billing-table`
CROSS JOIN
    UNNEST(credits) AS credits
WHERE
    DATE(_PARTITIONTIME) >= partitiontime_start_limit
    AND DATE(_PARTITIONTIME) <= partitiontime_end_limit
    AND usage_start_time >= start_time_limit
    AND usage_end_time <= end_time_limit
    AND project.id = target_project_id
GROUP BY
    sku_id
ORDER BY
    sku_id

3つ目のブロックでは、GKEリソース使用量のテーブルから、日付・名前空間・SKDの組み合わせに対する使用量を取得します。日付には、SKUの使用開始日を用いています。resource_nameは、抽象的なリソースの種類です。cpu, memory, storageという3種類があります。

SELECT
    DATE(start_time, target_timezone) AS start_date,
    namespace,
    sku_id,
    SUM(usage.amount) AS usage_amount,
    ANY_VALUE(usage.unit) AS usage_unit,
    resource_name,
    cluster_name,
    cluster_location
FROM
    `cluster-gcp-project.usage-metering-dataset.gke_cluster_resource_usage`
WHERE
    DATE(_PARTITIONTIME) >= partitiontime_start_limit
    AND DATE(_PARTITIONTIME) <= partitiontime_end_limit
    AND start_time >= start_time_limit
    AND end_time <= end_time_limit
GROUP BY
    start_date,
    namespace,
    sku_id,
    resource_name,
    cluster_name,
    cluster_location

4つ目のブロックでは、GKEリソース使用量のテーブルから、SKDごとの使用量を取得しています。後の計算では、この使用量と名前空間ごとの使用量の比を用いて、名前空間ごとの課金額を算出します。

SELECT
    sku_id,
    SUM(usage.amount) AS usage_amount,
    ANY_VALUE(usage.unit) AS usage_unit
FROM
    `cluster-gcp-project.usage-metering-dataset.gke_cluster_resource_usage`
WHERE
    DATE(_PARTITIONTIME) >= partitiontime_start_limit
    AND DATE(_PARTITIONTIME) <= partitiontime_end_limit
    AND start_time >= start_time_limit
    AND end_time <= end_time_limit
GROUP BY
    sku_id

SELECT句

メインである課金額の算出の部分について説明します。
まず、SKUの課金額に、SKUの割引額を足しています。

(billing_sku_costs.cost + IFNULL(billing_sku_credits.credit_amount, 0))

次に、GKEでのSKU合計使用量/課金データのSKU合計使用量をかけます。GKEのノードにはGCEが使われており、これをかけないと、同じGCPプロジェクト内に存在する、GKE以外で使用しているGCEインスタンスの分の課金額まで含めてしまいます。これをかけることで、GKEだけの、SKU単位での課金額が求められます。

(billing_sku_costs.cost + IFNULL(billing_sku_credits.credit_amount, 0))
        * (gke_sku_usage.usage_amount / billing_sku_costs.usage_amount)

最後に、名前空間のSKU合計使用量/GKEのSKU合計使用量をかけることで、名前空間で利用した分の課金額が求められます。WITH句の3ブロック目と4ブロック目で、名前空間のSKU使用量を取得するときは、テーブルにgke_cluster_resource_consumptionではなく、gke_cluster_resource_usageを用いてください。Kubernetesは、全体のリクエスト量をもとにノードのオートスケーリングを行っており、課金額に関係するのは、リクエスト量を取得できるgke_cluster_resource_usageであるためです。

(billing_sku_costs.cost + IFNULL(billing_sku_credits.credit_amount, 0))
    * (gke_sku_usage.usage_amount / billing_sku_costs.usage_amount)
    * (gke_ns_sku_usage.usage_amount / gke_sku_usage.usage_amount)

もし、GKE以外にGCEを利用しておらず、GKE専用のプロジェクトを持っている場合は、より正確に課金額を算出することができます。名前空間のSKU合計使用量/GKEのSKU合計使用量のみを用いて、以下で算出します。

(billing_sku_costs.cost + IFNULL(billing_sku_credits.credit_amount, 0))
        * (gke_ns_sku_usage.usage_amount / gke_sku_usage.usage_amount)

以上が、GKEの名前空間ごとの課金額を算出するクエリの解説です。
※前提がかけていたり、クエリが綺麗でないかもしれませんが、ご了承ください。

おわりに

非常に強力なツールであるKubernetesですが、そのコスト管理は詳細の把握ができないために難しいものになっています。しかし、本記事で紹介した方法であれば、その出力結果を分析したり可視化することで、名前空間という意味のある単位でコストをコントロールすることが可能になります。 Kubernetesは、数多くのコンテナを運用するためには必要不可欠です。そのため、把握が難しいコスト管理を諦めず、GCPの公式の機能を活用して挑戦してみましょう。

参考文献

ShtockData

お問い合わせフォーム

お問い合わせ項目を選択してください