AWSコストエクスプローラーAPIと気軽につきあう(2019)


この記事ははてなエンジニアAdvent Calendar 2019の5日目のエントリーです。

  • 続編が2020年に書かれました。2020年も定期的にコストエクスプローラーAPIとつきあっています。

はじめに

AWSのコストエクスプローラのデータは12ヶ月しか保存されないので、なるべく外部で保存したいですよね。幸い、コストエクスプローラのデータにアクセスするためのAPIがちゃんと提供されているので、気軽に利用することができます。特に難しいこともないのですが、これから「よーしAWS費用の集計をシステム化しちゃうぞー」という人のために、かんたんに足場を作るための手順を整理しておくことにします。

API更新時の説明において以下のように言及されている通り、コンソール経由よりも詳細なデータアクセスが可能なので、ぜひ面倒くさがらずコスト管理のパイプライン構築を進めていきましょう。

グループ化 – コストエクスプローラーウェブアプリケーションで提供されるグループ化は 1 レベルですが、API では 2 レベルが提供されます。たとえば、まずサービス別にコストまたは RI 使用率をグループ化してから、リージョン別にグループ化できます。

この記事で取り上げているのは2019/12現在の情報であり、API仕様は変更される可能性があります。また気軽ではありますがコストエクスプローラAPIは呼び出し単価高いAPI(リクエストあたり 0.01USD)であったりするのでご注意ください。

さて。

Cloud9 を起動しましょう

もちろん Cloud9 を使います。そして Typescript です。

特に理由がなければ Amazon Linux で立てるといいでしょう。気をつけるべきなのは、パブリックサブネットで立てるということです。今回は CostExplorerApiDev という名前で立ち上げました。

起動したら、下部ターミナルから npm のライブラリをセットアップします。

npm init -y
npm install -D typescript ts-node aws-sdk @types/node prettier
npx tsc --init

typescript の設定を開き、Format on Save は有効にしておきます。

tsconfig は以下のようにしている。最低限。

tsconfig.json
{
  "compilerOptions": {
    "target": "es2018",
    "module": "commonjs",
    "lib": ["es2018"],
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true
  }
}

さて、 vi index.ts して、動作確認をします。

index.ts
console.log("hello typescript");

npx ts-node index.ts してちゃんと実行されることを確認したら、次に進みましょう。

CostExplorer APIを利用する

普段見ているコストエクスプローラのレポート画面に相当するデータ取得は、GetCostAndUsage 呼び出しによって行うことができます。公式の情報は薄めなので、必要に応じてAPI情報をサードパーティから参照します。

ここでは、以下のようなデータがほしいとしましょう。

  • 月次で
  • 2019/06- 2019/11 の範囲で
  • 償却されたコスト(AMORTIZED COST)を
  • クレジット(Credit), 払い戻し(Refund)、税金(Tax)、前払い金(Upfront)に関するレコードを除外1
  • 連結アカウントごとにグループ化する

これは自分が普段利用しているコストエクスプローラのフィルタ条件を整理したものです。

キャッシュフロー上の実際の支払い金額が知りたいのではなく、発生ベースでその月に使用したアカウントごとの費用が知りたいとしましょう。クレジットや払い戻しによる費用の上下、各種の事前チャージのお金の動きは除外したいものとします。簡単のためサンプルでは console に出力しているだけですが、AWSのクレデンシャルがありSDKからAPI叩けているなら、結果を S3 に置くとか SNS に投げるのにとくべつな工夫は必要ないかと思います。

get_cost.ts
import * as AWS from "aws-sdk";

// Cost Explorer は us-east-1 指定が必要
const costexplorer = new AWS.CostExplorer({ region: "us-east-1" });

const params = {
  TimePeriod: {
    Start: "2019-06-01",
    End: "2019-11-01"
  },
  Filter: {
    Not: {
      Dimensions: {
        Key: "RECORD_TYPE",
        Values: ["Credit", "Refund", "Tax", "Upfront"]
      }
    }
  },
  Granularity: "MONTHLY",
  Metrics: ["AMORTIZED_COST"],
  GroupBy: [{ Type: "DIMENSION", Key: "LINKED_ACCOUNT" }]
};

costexplorer.getCostAndUsage(params, (err, data) => {
  if (err) {
    console.error(err);
  } else {
    console.log(data);
  }
});

これを実行します。うまくデータが取得できれば以下のようにコンソールに出力されると思います。なお今回は簡単のためにページングは考慮していません。結果オブジェクトに NextPageToken が含まれていたら後続を読み込む処理が必要になります。

ma2saka:~/environment $ npx ts-node get_cost.ts
{ GroupDefinitions: [ { Type: 'DIMENSION', Key: 'LINKED_ACCOUNT' } ],
  ResultsByTime:
   [ { TimePeriod: [Object],
       Total: {},
       Groups: [Array],
       Estimated: false },
     { TimePeriod: [Object],
       Total: {},
       Groups: [Array],
       Estimated: false },
     { TimePeriod: [Object],
       Total: {},
       Groups: [Array],
       Estimated: false },
     { TimePeriod: [Object],
       Total: {},
       Groups: [Array],
       Estimated: false },
     { TimePeriod: [Object],
       Total: {},
       Groups: [Array],
       Estimated: false } ] }

中身を実際に見たければ console.log(JSON.stringify(data)); とかすると良いです。ここでは事情により2割愛します。

ResultsByTime に月ごとのデータが配列として格納されています。ResultsByTime[].TimePeriodStart,End が入ってきます。MONTHLYなので、Startが対象月の初日(もしくは指定した開始日)、Endが対象月の翌月初日(もしくは指定した終了日)になります。

ResultsByTime[].Estimated は推定値が集計範囲に含まれていることを表します。今回のような集計をする場合、まだ締まっていない月のデータが Estimated: true になると思えばよさそうです。

ResultsByTime[].Groups にその集計期間におけるアカウントごとの集計値が並びます。ここでは税金や前払金などを除いたデータを月次で集計した値が得られます。

今回は償却コスト(Amotized Cost)をメトリクスとして集計対象にしました。これによってRIの費用などが月々に償却されて乗ります。指定できるのはこの他に、

BLENDED_COST, UNBLENDED_COST, AMORTIZED_COST, NET_UNBLENDED_COST, NET_AMORTIZED_COST, USAGE_QUANTITY, NORMALIZED_USAGE_AMOUNT

などがありますね。 https://docs.aws.amazon.com/ja_jp/awsaccountbilling/latest/aboutv2/ce-advanced.html などでなんとなく解説されている。3メトリクスは同時に複数指定できます。メトリクス名は Screaming snake case でも Upper Camel Case でも有効みたいだけど、どっちが正なんだろう。

グループ化のタイプとして DIMENSION を、キーとして LINKED_ACCOUNT を指定しています。連結アカウント単位です。タイプを DIMENSION とした場合、値としては以下のようなものが指定できます。

AZ, INSTANCE_TYPE, LINKED_ACCOUNT, OPERATION, PURCHASE_TYPE, REGION, SERVICE, USAGE_TYPE, USAGE_TYPE_GROUP, RECORD_TYPE, OPERATING_SYSTEM, TENANCY, SCOPE, PLATFORM, SUBSCRIPTION_ID, LEGAL_ENTITY_NAME, DEPLOYMENT_OPTION, DATABASE_ENGINE, CACHE_ENGINE, INSTANCE_TYPE_FAMILY, BILLING_ENTITY, RESERVATION_ID

なお、GetCostAndUsage API 経由ではアカウント情報はIDまでしか取れません。アカウントの一覧を取得して付き合わせるには一度 Organizations#listAccounts API を呼び出して辞書を作っておくのが楽だと思います。

グループ化のタイプには TAG を指定することもできます。その場合にはキーとしてリソースタグのキー名を指定します。日本語で書くとちょっとわかりづらいところですが、クエリ書いてみればまあわかる。

以下は、「ある特定のアカウントID」に対し、「OwnerTeam」というリソースタグの値で月額費用を集計します。変更点は Filter 条件と GroupBy の指定です。

awesome.ts
const params = {
    TimePeriod: {
        Start: '2019-06-01',
        End: '2019-11-01',
    },
    Filter: {
        And: [{
            Dimensions: {
                Key: "LINKED_ACCOUNT",
                Values: ["特定のアカウントID"]
            }
        }, {
            Not: {
                Dimensions: {
                    Key: "RECORD_TYPE",
                    Values: ["Credit", "Refund", "Tax", "Upfront"]
                }
            }
        }, ]
    },
    Granularity: "MONTHLY",
    Metrics: ["AMORTIZED_COST"],
    GroupBy: [{ Type: "TAG", Key: "OwnerTeam" }]
}

Filter はちょっと試行錯誤が必要かもしれませんが、そんなに複雑なものでないのですぐ慣れます。Expressionを適宜参照してください。

Lambda などで動かす場合、ce:Get* を許可する

ce:Get* を許してあげましょう。公式のサンプルでは ce:* を許可していますが、権限は狭いに越したことはありません。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "ce:Get*",
            "Resource": "*"
        }
    ]
}

まとめ

Cloud9 は実行環境がついたターミナルとしか使ってなくて、実はぜんぶ vim で書いてました。すみません。Typescript ではあるんですが、Javascript と書いたコードは区別がつきませんでした。これも、そういうもんですね。

6日目は @ne_sachirou さんです。よろしくお願いします。


  1. 裏側のデータ構造をある程度想像した上で集計処理を想像する必要があります。どのアカウントの、どんなタグが付与されたどんなリソースを何分/何GB利用したのか、といった履歴テーブルに集計クエリを打つのだと考えるとわかりやすいです。 

  2. 会社のアカウントのデータ出すわけにもいきませんが、個人アカウントのデータは少なすぎて集計のしがいがなかった。 

  3. 日本語訳で読むと苦しい気持ちになる。純償却コスト、純非ブレンドコストって言われてもエッてなります。設定値との対応関係が読み取りづらいのでドキュメントは大人しく英語で読みましょう。