PowerQueryでAWS Price List API (AKA the Bulk API) からオファーファイルの一覧を取得する

AWSには多岐に渡るサービスがあり、その価格表が各サービスのページに表示されています。
それと同時に`AWS Price List API`としてAPIも提供されているため、コチラを`PowerQuery`から実行してAWS価格表（オファーファイル）URL一覧を取得してみます。

今回紹介する`PowerQuery`を実行すると下記のような形で取得できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1670622/c4d75767-4707-687c-fe45-5034c644fe0f.png)

## AWSから提供されている`AWS Price List API`について

[Using the AWS Price List API](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/price-changes.html)


`AWS Price List API`としては下記2種類のAPIが存在し。
それぞれからAWS価格情報を取得する事ができます。

- Price List Service API (AKA the Query API)
- Price List API (AKA the Bulk API)

`Price List Service API (AKA the Query API)`については`AWS-CLI`の`pricing`で利用されていますが、今回は利用しません。

## Price List API (AKA the Bulk API) について

[Using the bulk API](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/using-ppslong.html)

`Price List API (AKA the Bulk API)`では下記2種類のファイルが提供されている。

- オファーインデックスファイル
- オファーファイル

オファーファイルにはAWS製品の価格されており。そのオファーファイルを見つけるためにはオファーインデックスファイルを読み解く必要があります。

なおオファーインデックスファイルにはjson形式のオファーファイルURLが記載されていますがcsvに変更することでcsv形式のオファーファイルを取得する事ができます。

## 余談　もともとはリージョン別のオファーファイルはなかった。

オファーファイルについては、過去サービス別単位での提供でしたが今現在はリージョン別のオファーファイルも用意されています。

[AWS Price List API Update – Regional Price Lists](https://aws.amazon.com/jp/blogs/aws/aws-price-list-api-update-regional-price-lists/)

なおこのオファーファイル、製品の組み合わせが多いサービスだとこれを網羅するために非常に巨大なjson、csvファイルとなります。

2021年9月2日現在。

最新の東京リージョンEC2サービスのオファーファイルは143MB（json）。
最新の全リージョンEC2サービスのオファーファイルは2.3GB（json）。

## オファーインデックスファイルとオファーファイルについて

オファーファイルのURLは下記URLから取得できるオファーインデックスファイルに含まれる情報を読み解くことで取得する事ができます。

`https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/index.json`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1670622/9b77d3d5-3ad4-e90d-0d0d-2ea75dd010a8.png)

オファーインデックスファイルはサービス毎に下記の項目を持っている。
※`savingsPlanVersionIndexUrl` と　`currentSavingsPlanIndexUrl`　についてはSavingsPlansがあるサービスのみ。

| 項目 | 摘要 | 
| ---  | --- |
| versionIndexUrl | 【インデックスファイル】バージョン単位 |
| currentVersionUrl | 【オファーファイル】全リージョン含まれている最新オファーファイル |
| currentRegionIndexUrl | 【インデックスファイル】最新のリージョン単位 |
| savingsPlanVersionIndexUrl | 【インデックスファイル】SavingsPlansバージョン単位 |
| currentSavingsPlanIndexUrl | 【インデックスファイル】SavingsPlansリージョン単位 |

それぞれの項目先頭に`https://pricing.us-east-1.amazonaws.com`をつけてアクセスします。

EC2サービスの場合は下記のような感じ。

| 種別 | URL |
| --- | --- |
| versionIndexUrl | https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonEC2/index.json |
| currentVersionUrl | https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonEC2/current/index.json |
| currentRegionIndexUrl | https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonEC2/current/region_index.json |
| savingsPlanVersionIndexUrl  | https://pricing.us-east-1.amazonaws.com/savingsPlan/v1.0/aws/AWSComputeSavingsPlan/current/index.json |
| currentSavingsPlanIndexUrl | https://pricing.us-east-1.amazonaws.com/savingsPlan/v1.0/aws/AWSComputeSavingsPlan/current/region_index.json |

インデックスファイルからバージョン別・リージョン別のインデックスファイルURLが取得できました。

なお`versionIndexUrl`ファイルの中に含まれる`offerVersionUrl`項目は全リージョンのオファーファイルとなりますが。

リージョン別AWSサービスの場合、`index.json`を`region_index.json`に変更するとリージョン別のインデックスファイルが取得できます。

こっちはオファーファイル。
- https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonEC2/20210831222446/index.json

こっちはオファーインデックスファイル。
- https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonEC2/20210831222446/region_index.json

この動作、[AWS Price List API Update – Regional Price Lists](https://aws.amazon.com/jp/blogs/aws/aws-price-list-api-update-regional-price-lists/) の`currentVersionUrl`と`currentRegionIndexUrl`の関係をみてそんな感じだろうと思い使っていましたが。

改めてドキュメント読み返していて`current`ではなくバージョン別でもこの形式となると記述されている所が無い気がします？

なおリージョン別ファイル提供開始以前のバージョンにはregion_index.jsonが存在しないようです。

また`CodeCommit`、`AWSGlobalAccelerator`などのサービスは`region_index.json`ファイル自体は存在するのですが、`regions`項目が空になっているようです。

[https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AWSCodeCommit/20170419201333/region_index.json](https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AWSCodeCommit/20170419201333/region_index.json)

```json
{
  "formatVersion" : "v1.0",
  "disclaimer" : "This pricing list is for informational purposes only. All prices are subject to the additional terms included in the pricing pages on http://aws.amazon.com. All Free Tier prices are also subject to the terms included at https://aws.amazon.com/free/",
  "regions" : { }
}
```


## PowerQueryでオファーファイルの一覧を取得する

ここまで理解できば各種プログラムでオファーファイルが取得できるようになっているかと思います。

今回はオファーファイルURL一覧を取得する`PowerQuery`を紹介します。

オファーインデックスファイルを見ればわかりますが、`SavingsPlans`とそれ以外にわかれているため。
それぞれつくります。

### 全サービス全バージョン全リージョン別

全サービス全バージョン全リージョン別（リージョン別ファイルがない場合は全リージョン）。
サービス、リージョン毎にwebリクエストをするため実行すると処理時間がかなり掛かるかと思います。

欲しいサービス等でフィルタを掛けて利用するのが良いかと思います。

```PowerQuery
let
//baseURI
    baseURI="https://pricing.us-east-1.amazonaws.com",
//インデックスオファーファイル取得
    Source = Json.Document(Web.Contents("https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/index.json")),
    offers = Source[offers],
    #"Converted to Table" = Record.ToTable(offers),
//サービス別のフィルタをいれるならココ
//versionIndexUrl展開
    #"Expanded Value" = Table.ExpandRecordColumn(#"Converted to Table", "Value", {"offerCode", "versionIndexUrl"}, {"offerCode", "versionIndexUrl"}),
    #"Add versionIndexUrlFull" = Table.AddColumn(#"Expanded Value", "versionIndexUrlFull", each #"baseURI"&[versionIndexUrl]),
    #"Added versionIndexUrlContents" = Table.AddColumn(#"Add versionIndexUrlFull", "versionIndexUrlContents", each Record.ToTable(Record.Field(Json.Document(Web.Contents([versionIndexUrlFull])),"versions"))),
    #"ExpandedTable versionIndexUrlContents" = Table.ExpandTableColumn(#"Added versionIndexUrlContents", "versionIndexUrlContents", {"Value"}, {"Value"}),
    #"ExpandedRecord Value" = Table.ExpandRecordColumn(#"ExpandedTable versionIndexUrlContents", "Value", {"versionEffectiveBeginDate", "versionEffectiveEndDate", "offerVersionUrl"}, {"versionEffectiveBeginDate", "versionEffectiveEndDate", "offerVersionUrl"}),
    #"Added offerVersionUrlFull" = Table.AddColumn(#"ExpandedRecord Value", "offerVersionUrlFull", each #"baseURI"&[offerVersionUrl]),
    #"Added regionIndex" = Table.AddColumn(#"Added offerVersionUrlFull", "regionIndex", each Text.Replace([offerVersionUrlFull],"index.json","region_index.json")),
//region_indexが存在しない場合があるのでhttpResponseを取得して判断
    #"Added regionIndexhttpResponse" = Table.AddColumn(#"Added regionIndex", "httpResponse", each Record.Field(Value.Metadata(Web.Contents([regionIndex],[ManualStatusHandling = {400,401,403,404,429,500,503}])),"Response.Status")),
    #"Added RegionIndexContents" = Table.AddColumn(#"Added regionIndexhttpResponse", "RegionIndexContents", each if [httpResponse] = 200 then
Table.SelectColumns(Record.ToTable(Record.Field(Json.Document(Web.Contents([regionIndex])),"regions")),"Value") else null),
    #"ExpandedTable RegionIndexContents" = Table.ExpandTableColumn(#"Added RegionIndexContents", "RegionIndexContents", {"Value"}, {"Value"}),
    #"ExpandedRecord RegionIndexContents" = Table.ExpandRecordColumn(#"ExpandedTable RegionIndexContents", "Value", {"regionCode", "currentVersionUrl"}, {"regionCode", "currentVersionUrl"}),
//MEMO 一部サービスではregion_index.jsonは存在するがregionsが空のケースがある
//currentVersionUrlが取得できなかった時は全リージョンのオファーファイル
    #"Added offerFile" = Table.AddColumn(#"ExpandedRecord RegionIndexContents", "offerFile", each if [currentVersionUrl] = null then [offerVersionUrlFull] else #"baseURI"&[currentVersionUrl]),
    #"Added offerFile-csv" = Table.AddColumn(#"Added offerFile", "offerFile-csv", each Replacer.ReplaceText([offerFile],"json","csv")),
    #"Removed Other Columns" = Table.SelectColumns(#"Added offerFile-csv",{"Name", "offerCode", "versionIndexUrlFull", "versionEffectiveBeginDate", "versionEffectiveEndDate", "offerVersionUrlFull", "regionIndex", "httpResponse", "regionCode", "offerFile", "offerFile-csv"}),
    #"Sorted Rows" = Table.Sort(#"Removed Other Columns",{{"Name", Order.Ascending}, {"versionEffectiveBeginDate", Order.Ascending}, {"regionCode", Order.Ascending}})
in
    #"Sorted Rows"
```

### SavingsPlans全リージョン全バージョン

```PowerQuery
let
    baseURI="https://pricing.us-east-1.amazonaws.com",
    Source = Json.Document(Web.Contents("https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/index.json")),
    offers = Source[offers],
    #"Converted to Table" = Record.ToTable(offers),
    #"Expanded Value" = Table.ExpandRecordColumn(#"Converted to Table", "Value", {"savingsPlanVersionIndexUrl", "offerCode"}, {"savingsPlanVersionIndexUrl", "offerCode"}),
    #"Filtered Rows" = Table.SelectRows(#"Expanded Value", each ([savingsPlanVersionIndexUrl] <> null)),
    #"Add savingsPlanVersionIndexUrl" = Table.AddColumn(#"Filtered Rows", "savingsPlanVersionIndexUrlFull", each baseURI&[savingsPlanVersionIndexUrl]),
    #"Add savingsPlanVersionIndexContents" = Table.AddColumn(#"Add savingsPlanVersionIndexUrl", "savingsPlanVersionIndexContents", each Json.Document(Web.Contents([savingsPlanVersionIndexUrlFull]))),
    #"Expanded savingsPlanVersionIndexContents" = Table.ExpandRecordColumn(#"Add savingsPlanVersionIndexContents", "savingsPlanVersionIndexContents", {"versions"}, {"versions"}),
    #"ExpandedList versions" = Table.ExpandListColumn(#"Expanded savingsPlanVersionIndexContents", "versions"),
    #"ExpandedRecord versions" = Table.ExpandRecordColumn(#"ExpandedList versions", "versions", {"publicationDate", "offerVersionUrl"}, {"publicationDate", "offerVersionUrl"}),
    #"Added offerVersionUrlFull" = Table.AddColumn(#"ExpandedRecord versions", "offerVersionUrlFull", each baseURI&[offerVersionUrl]),
    #"Added offerVersionUrlContents" = Table.AddColumn(#"Added offerVersionUrlFull", "offerVersionUrlContents", each Json.Document(Web.Contents([offerVersionUrlFull]))[regions]),
    #"ExpandedList offerVersionUrlContents" = Table.ExpandListColumn(#"Added offerVersionUrlContents", "offerVersionUrlContents"),
    #"ExpandedRecord offerVersionUrlContents" = Table.ExpandRecordColumn(#"ExpandedList offerVersionUrlContents", "offerVersionUrlContents", {"regionCode", "versionUrl"}, {"regionCode", "versionUrl"}),
    #"Added offerFile" = Table.AddColumn(#"ExpandedRecord offerVersionUrlContents", "offerFile", each baseURI&[versionUrl]),
    #"Added offerFile-csv" = Table.AddColumn(#"Added offerFile", "offerFile-csv", each Replacer.ReplaceText([offerFile],"json","csv")),
    #"Removed Other Columns" = Table.SelectColumns(#"Added offerFile-csv",{"Name", "offerCode", "savingsPlanVersionIndexUrlFull", "publicationDate", "offerVersionUrlFull", "regionCode", "offerFile", "offerFile-csv"}),
    #"Sorted Rows" = Table.Sort(#"Removed Other Columns",{{"Name", Order.Ascending}, {"publicationDate", Order.Ascending}, {"regionCode", Order.Ascending}})
in
    #"Sorted Rows"
```

### 最新のバージョンのみ　全サービス全リージョン別

```PowerQuery
let
//baseURI
    baseURI="https://pricing.us-east-1.amazonaws.com",
//インデックスオファーファイル取得
    Source = Json.Document(Web.Contents("https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/index.json")),
    offers = Source[offers],
    #"Converted to Table" = Record.ToTable(offers),
    //サービス別のフィルタをいれるならココ
//versionIndexUrl展開
    #"Expanded Value" = Table.ExpandRecordColumn(#"Converted to Table", "Value", {"currentRegionIndexUrl", "offerCode"}, {"currentRegionIndexUrl", "offerCode"}),
    #"Add currentRegionIndexUrlFull" = Table.AddColumn(#"Expanded Value", "currentRegionIndexUrlFull", each #"baseURI"&[currentRegionIndexUrl]),
    #"Added currentRegionIndexContents" = Table.AddColumn(#"Add currentRegionIndexUrlFull", "versionIndexUrlContents", each Record.ToTable(Record.Field(Json.Document(Web.Contents([currentRegionIndexUrlFull])),"regions"))),
    #"ExpandedTable versionIndexUrlContents" = Table.ExpandTableColumn(#"Added currentRegionIndexContents", "versionIndexUrlContents", {"Value"}, {"Value"}),
    #"ExpandedRecord Value" = Table.ExpandRecordColumn(#"ExpandedTable versionIndexUrlContents", "Value", {"regionCode", "currentVersionUrl"}, {"regionCode", "currentVersionUrl"}),
    #"Added currentVersionUrlFull" = Table.AddColumn(#"ExpandedRecord Value", "currentVersionUrlFull", each #"baseURI"&[currentVersionUrl]),
//MEMO 一部サービスではregion_index.jsonは存在するがregionsが空のケースがある
//currentVersionUrlFullが取得できなかった時は全リージョンのオファーファイル
    #"Added offerFile" = Table.AddColumn(#"Added currentVersionUrlFull", "offerFile", each if [currentVersionUrlFull] = null then Replacer.ReplaceText([currentRegionIndexUrlFull],"region_index","index") else [currentVersionUrlFull]),
//csv形式
    #"Added offerFile-csv" = Table.AddColumn(#"Added offerFile", "offerFile-csv", each Replacer.ReplaceText([offerFile],"json","csv")),
    #"Removed Other Columns" = Table.SelectColumns(#"Added offerFile-csv",{"Name", "offerCode", "regionCode", "currentRegionIndexUrlFull", "offerFile", "offerFile-csv"}),
    #"Sorted Rows" = Table.Sort(#"Removed Other Columns",{{"Name", Order.Ascending}, {"regionCode", Order.Ascending}})
in
    #"Sorted Rows"
```

### 最新のバージョンのみ　SavingsPlans全リージョン

```PowerQuery
let
    baseURI="https://pricing.us-east-1.amazonaws.com",
    Source = Json.Document(Web.Contents("https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/index.json")),
    offers = Source[offers],
    #"Converted to Table" = Record.ToTable(offers),
    #"Expanded Value" = Table.ExpandRecordColumn(#"Converted to Table", "Value", {"offerCode", "currentSavingsPlanIndexUrl"}, {"offerCode", "currentSavingsPlanIndexUrl"}),
    #"Filtered Rows" = Table.SelectRows(#"Expanded Value", each ([currentSavingsPlanIndexUrl] <> null)),
    #"Add savingsPlanVersionIndexUrl" = Table.AddColumn(#"Filtered Rows", "currentSavingsPlanIndexUrlFull", each baseURI&[currentSavingsPlanIndexUrl]),
    #"Add currentSavingsPlanIndexUrlContents" = Table.AddColumn(#"Add savingsPlanVersionIndexUrl", "currentSavingsPlanIndexUrlContents", each Json.Document(Web.Contents([currentSavingsPlanIndexUrlFull]))),
    #"ExpandedRecord currentSavingsPlanIndexUrlContents" = Table.ExpandRecordColumn(#"Add currentSavingsPlanIndexUrlContents", "currentSavingsPlanIndexUrlContents", {"publicationDate", "regions"}, {"publicationDate", "regions"}),
    #"ExpandedList regions" = Table.ExpandListColumn(#"ExpandedRecord currentSavingsPlanIndexUrlContents", "regions"),
    #"ExpandedRecord regions" = Table.ExpandRecordColumn(#"ExpandedList regions", "regions", {"regionCode", "versionUrl"}, {"regionCode", "versionUrl"}),
    #"Added versionUrlFull" = Table.AddColumn(#"ExpandedRecord regions", "offerVersionUrlFull", each baseURI&[versionUrl]),
    #"Added offerFile" = Table.AddColumn(#"Added versionUrlFull", "offerFile", each [offerVersionUrlFull]),
    #"Added offerFile-csv" = Table.AddColumn(#"Added offerFile", "offerFile-csv", each Replacer.ReplaceText([offerFile],"json","csv")),
    #"Removed Other Columns" = Table.SelectColumns(#"Added offerFile-csv",{"Name", "offerCode", "currentSavingsPlanIndexUrlFull", "publicationDate", "regionCode", "offerFile", "offerFile-csv"}),
    #"Sorted Rows" = Table.Sort(#"Removed Other Columns",{{"Name", Order.Ascending}, {"publicationDate", Order.Ascending}, {"regionCode", Order.Ascending}})
in
    #"Sorted Rows"
```

## 総評

これでオファーファイルの一覧を取得できたわけですが。
AWSが提供するサービスの多さと、EC2サービスでは組み合わせが爆発してるさまをみると凄まじいの一言。