Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

AWSのコスト配分レポートをいじり倒し、料金を超絶見える化する

はじめに

AWS料金、どこにどれだけかかってるか、とても分かりづらくないですか?
CostExplorerを使えばそれなりに可視化できますが、例えばEC2。
EC2インスタンス料金は見えますが、そこにアタッチされたEBSやそのEBSスナップショット、そのEC2で発生したデータ通信料金などは、すべてバラバラでしか確認できません。
これらすべて、発生元のEC2に紐づけて見たいと思ったことはないでしょうか。

Auroraもインスタンス料金は見えますが、ストレージ、I/O、バックアップなど、そのインスタンスで発生している料金は紐付けて確認したいですよね。

会社の取締役から「どのサーバでいくら、どのシステムでいくら、かかってるんだ?」
と聞かれ、ぐぬぬとなってしまったのをきっかけに徹底的に見える化することにしました。

ゴール

先に完成版をお見せしたいと思います。
今回はコストを抑えたかったため、DBは使わず単純CSVファイルとAWSのBIサービスであるQuickSightを使いました。
(CSVファイルをQuickSightにロードしています)

例えば、ある月のリソース単位の料金を円グラフにするとこんな感じ
AWS料金可視化01.png

一番お高いリソース(水色の¥262,753)はこれ、Auroraなんですが、グラフをクリックしてドリルダウンすると以下のようになります。
AWS料金可視化02.png

お高いAurora料金の内訳が見れます。
例えばこのAuroraは

  • インスタンス料金で¥114,857
  • I/O料金で¥85,069
  • ストレージ料金で¥61,574
  • データ転送Outで¥1,253

かかっていることが分かります。I/Oたけぇ・・・:sweat_smile:
Cost Explorerでは、Aurora全体でI/O料金はいくら、というのは確認できますが、このようにインスタンス単位では確認できません。

QuickSightはBIツールですから、データさえあれば紹介したようなグラフ以外にも様々なグラフや表を自在に作成できます。
単月コストだけでなく、長期間のトレンドも詳細に把握できるようになりますし、削減のためのアクションプラン策定にも効果抜群です。

今回はQuickSightにロードするためのCSVデータを、AWSから提供されるコスト配分レポートをもとに作成する方法を、サンプルコード交えてご紹介したいと思います。
※コーディングの拙さはご容赦ください:laughing:

本記事に書かないこと

コスト配分タグ、コスト配分レポートについては公式ドキュメントをご確認ください。

また、QuickSightの使い方も今回は触れません。

事前準備

まずリソースへのタグ付けが必要です。今回は以下のタグを付与することとします。

タグ名 付与対象リソース
Name リソース名 Nameタグがリソース名となるようなサービスは全て。
とりあえずEC2だけでもOK
system システム名 課金対象リソースは基本全部
environment 本番ならproduction、ステージングならstaging、テストならtestなど 課金対象リソースは基本全部

全部付けるのが面倒ならとりあえず課金額が多めなリソースだけでもいいです。

データの準備

コスト配分レポートを分析するにあたり、必要なデータを用意します。
具体的には以下の2つです。

  1. EC2用マッピングデータ・・・EC2、EBS、EBSスナップショット等のマッピングデータ
  2. RDS用マッピングデータ・・・RDS、クラスターリソースID、スナップショット等のマッピングデータ

EC2用マッピングデータの取得

1は、EC2、EBS、EBSスナップショットの紐付け状態をCSV形式で取得して作成します。これは毎日作成し、日毎に保管しておきます(日毎に変動する可能性があるため)
例えば以下のようなCSVデータになります。

yyyy-mm-dd_ec2_mapping_sample.csv
AWS Account ID,Account Desc,System,Environment,Name,Instance ID,Volume ID,Snapshot ID
123456789012,○○用アカウント,A-system,production,web-server01,i-01xxxxxxxxx,vol-01xxxxxxxxxx,snap-0101xxxx
123456789012,○○用アカウント,A-system,production,web-server01,i-01xxxxxxxxx,vol-01xxxxxxxxxx,snap-0102xxxx
123456789012,○○用アカウント,A-system,production,web-server01,i-01xxxxxxxxx,vol-01xxxxxxxxxx,snap-0103xxxx
123456789012,○○用アカウント,A-system,production,web-server01,i-01xxxxxxxxx,vol-01xxxxxxxxxx,snap-0104xxxx
123456789012,○○用アカウント,B-system,test,web-server02,i-02xxxxxxxxxx,02xxxxxxxxxx,
123456789012,○○用アカウント,C-system,production,web-server03,i-03xxxxxxxxx,vol-03xxxxxxxxxx,snap-0301xxxx
123456789012,○○用アカウント,C-system,production,web-server04,i-04xxxxxxxxx,vol-04xxxxxxxxxx,snap-0401xxxx
123456789012,○○用アカウント,C-system,production,web-server04,i-04xxxxxxxxx,vol-04xxxxxxxxxx,snap-0402xxxx

各カラムの内容は次の通りです。

カラム 説明
AWS Account ID AWSのアカウントID。複数のアカウントをお持ちの場合はあったほうがいいでしょう
Account Desc アカウントIDの説明。無くてもいいです
System 事前準備でリソースに付与したタグ
Environment 事前準備でリソースに付与したタグ
Name 事前準備でEC2等に付与したNameタグ
Instance ID EC2インスタンスID
Volume ID 上記EC2にアタッチされているEBSボリュームID
Snapshot ID 上記EBSのスナップショットID

このCSVを作成するためのサンプルコードは次の通りです。今回はjqを使ってみたかったのでBashで書きましたが言語は何でもいいです。
メインとなる紐付けのところだけ抜粋します。
AWSCLIと、JSONパースのためjqを使います。

create_ec2_mapping_sample.sh
#!/bin/bash

# まず必要な情報をAWSCLIで取得
aws ec2 describe-volumes > ${TMPFILE1}
aws ec2 describe-instances > ${TMPFILE2}
aws ec2 describe-snapshots --filters "Name=owner-id,Values=<AWSアカウントID>" > ${TMPFILE3} #AWSアカウントIDでフィルタしないと公開されているスナップショットすべて取得してしまいます

# EBSボリューム毎にループ(EBSを基準に対応するEC2とスナップショットを特定する)
for volumeId in $(jq -r '.Volumes[] | .VolumeId' ${TMPFILE1}); do
  # アタッチされているEC2インスタンスのIDを取得
  instanceId=$(jq -r '.Volumes[] | select(.VolumeId == "'${volumeId}'") | .Attachments[].InstanceId' ${TMPFILE1})
  # EC2インスタンスのタグを取得
  nameTag=$(jq -r '.Reservations[].Instances[] | select(.InstanceId == "'${instanceId}'") | .Tags[] | select(.Key == "Name") | .Value' ${TMPFILE2})
  systemTag=$(jq -r '.Reservations[].Instances[] | select(.InstanceId == "'${instanceId}'") | .Tags[] | select(.Key == "system") | .Value' ${TMPFILE2})
  envTag=$(jq -r '.Reservations[].Instances[] | select(.InstanceId == "'${instanceId}'") | .Tags[] | select(.Key == "environment") | .Value' ${TMPFILE2})
  # EBSのスナップショット一覧を取得
  snapshotIds=$(jq -r '.Snapshots[] | select(.VolumeId == "'${volumeId}'") | .SnapshotId' ${TMPFILE3})
  # スナップショットが無かったらそのまま空文字を出力
  if [[ -z ${snapshotIds} ]]; then
    snapshotId=""
    echo "${account},${accountDesc},${systemTag},${envTag},${nameTag},${instanceId},${volumeId},${snapshotId}"
  # スナップショットがあったら1スナップショット毎に1行出力
  else
    for snapshotId in ${snapshotIds}; do
      echo "${account},${accountDesc},${systemTag},${envTag},${nameTag},${instanceId},${volumeId},${snapshotId}"
    done
  fi
done

RDS用マッピングデータの取得

2は、RDS、クラスター、クラスターのリソースID、スナップショットの紐付け状態をCSV形式で取得して作成します。これも毎日作成し、日毎に保管しておきます(日毎に変動する可能性があるため)
例えば以下のようなCSVデータになります。

yyyy-mm-dd_rds_mapping_sample.csv
AWS Account ID,Account Desc,System,Environment,Instance ID,Cluster ID,Resource ID,Snapshot ID
123456789012,○○用アカウント,A-system,production,db01,db01-cluster,cluster-01xxxxxxxxxx,rds:db01-cluster-2021-02-21-15-08
123456789012,○○用アカウント,A-system,test,db02,db02-cluster,cluster-02xxxxxxxxxx,rds:db02-cluster-2021-02-21-14-04
123456789012,○○用アカウント,B-system,production,db03,db03-cluster,cluster-03xxxxxxxxxx,rds:db03-cluster-2021-02-21-15-07
123456789012,○○用アカウント,B-system,production,db04-1,db04-cluster,cluster-04xxxxxxxxxx,rds:db04-cluster-2021-02-21-14-06
123456789012,○○用アカウント,B-system,production,db04-2,db04-cluster,cluster-04xxxxxxxxxx,rds:db04-cluster-2021-02-21-14-06
123456789012,○○用アカウント,B-system,production,db05-1,db05-cluster,cluster-05xxxxxxxxxx,rds:db05-cluster-2021-02-21-15-42
123456789012,○○用アカウント,B-system,production,db05-2,db05-cluster,cluster-05xxxxxxxxxx,rds:db05-cluster-2021-02-21-15-42
123456789012,○○用アカウント,C-system,production,db06,,,rds:db06-2017-11-16-02-00
123456789012,○○用アカウント,C-system,production,db06,,,rds:db06-2021-02-19-02-00
123456789012,○○用アカウント,C-system,production,db06,,,rds:db06-2021-02-20-02-00
123456789012,○○用アカウント,C-system,production,db06,,,rds:db06-2021-02-21-02-00

普通のRDSとAuroraで出力され方が異なります。
(普通のRDSはクラスターIDとクラスターのリソースIDが存在しないので空になっています)

各カラムの内容は次の通りです。(EC2の方と同じカラムは省略します)

カラム 説明
Instance ID RDSインスタンスID
Cluster ID AuroraのクラスターID
Resource ID AuroraクラスターのリソースID。普段あまり気にしないものですが、分析に必要なので取得しています

このCSVを作成するためのサンプルコードは次の通りです。EC2と同様にBashです(書きっぷりがEC2側と少し違うのはご容赦下さい)
メインとなる紐付けのところだけ抜粋します。
普通のRDSとAurora(クラスター)でデータの取得方法が変わるのでご注意ください。

create_rds_mapping_sample.sh
#!/bin/bash

# クラスタリソースID取得関数
function get_resourceid() {
  resourceId=$(jq -r '.DBClusters[] | select(.DBClusterIdentifier == "'${1}'") | .DbClusterResourceId' ${TMPFILE3})
  resourceId=${resourceId,,}
  echo ${resourceId}
}

# タグ取得関数
function get_tags() {
  if [[ ${1} == "instance" ]]; then
    rdsArn=$(jq -r '.DBInstances[] | select(.DBInstanceIdentifier == "'${2}'") | .DBInstanceArn' ${TMPFILE1})
  else
    rdsArn=$(jq -r '.DBClusters[] | select(.DBClusterIdentifier == "'${2}'") | .DBClusterArn' ${TMPFILE3})
  fi
  aws --profile ${account} rds list-tags-for-resource --resource-name "${rdsArn}" > ${TMPFILE4}
  systemTag=$(jq -r '.TagList[] | select(.Key == "system") | .Value' ${TMPFILE4})
  envTag=$(jq -r '.TagList[] | select(.Key == "environment") | .Value' ${TMPFILE4})
}

# スナップショット取得関数(非Aurora)
function get_snapshots_noaurora() {
  snapshotIds=$(jq -r '.DBSnapshots[] | select(.DBInstanceIdentifier == "'${1}'") | .DBSnapshotIdentifier' ${TMPFILE2})
  echo "${snapshotIds}"
}

# スナップショット取得関数(Aurora)
function get_snapshots_aurora() {
  snapshotIds=$(jq -r '.DBClusterSnapshots[] | select(.DBClusterIdentifier == "'${1}'") | .DBClusterSnapshotIdentifier' ${TMPFILE5})
  echo "${snapshotIds}"
}

# CSVレコード出力関数
function out_csv_record() {
  if [[ -z ${1} ]]; then
    snapshotId=""
    echo "${account},${accountDesc},${systemTag},${envTag},${rdsId},${clusterId},${resourceId},${snapshotId}"
  else
    for snapshotId in ${1}
    do
      echo "${account},${accountDesc},${systemTag},${envTag},${rdsId},${clusterId},${resourceId},${snapshotId}"
    done
  fi
}

function main() {
  # タイトル行出力
  echo "AWS Account ID,Account Desc,System,Environment,Instance ID,Cluster ID,Resource ID,Snapshot ID"

  # まず必要な情報をAWSCLIで取得
  aws rds describe-db-instances > ${TMPFILE1}
  aws rds describe-db-snapshots > ${TMPFILE2}
  aws rds describe-db-clusters  > ${TMPFILE3}
  aws rds describe-db-cluster-snapshots  > ${TMPFILE5}

  # RDS毎にループ(RDSを基準に対応するクラスタとスナップショットを特定する)
  for rdsId in $(jq -r '.DBInstances[] | .DBInstanceIdentifier' ${TMPFILE1}); do
    ## 変数初期化
    systemTag=""
    envTag=""
    clusterId=""
    resourceId=""
    snapshotId=""
    ## 対応するクラスタのIDを取得
    clusterId=$(jq -r '.DBInstances[] | select(.DBInstanceIdentifier == "'${rdsId}'") | .DBClusterIdentifier' ${TMPFILE1})

    ## クラスタに対応するリソースIDを取得
    if [[ ${clusterId} == "null" ]]; then
      clusterId=""
    else
      resourceId=$(get_resourceid ${clusterId})
    fi

    ## 対応するタグ(system、environment)を取得
    get_tags instance ${rdsId}

    ## RDSのスナップショット一覧を取得
    ### Auroraじゃない場合
    if [[ -z "${clusterId}" ]]; then
      snapshotIds=$(get_snapshots_noaurora ${rdsId})
      out_csv_record "${snapshotIds}"
    ### Auroraの場合
    else
      snapshotIds=$(get_snapshots_aurora ${clusterId})
      out_csv_record "${snapshotIds}"
    fi
  done

  # Aurora Serverless毎にループ
  for clusterId in $(jq -r '.DBClusters[] | select(.EngineMode == "serverless") | .DBClusterIdentifier' ${TMPFILE3}); do
    #省略
  done

  rm -f ${TMPFILE1} ${TMPFILE2} ${TMPFILE3} ${TMPFILE4} ${TMPFILE5}
  exit 0
}

# メイン処理実行
main

コスト配分レポートの分析

さて、ここからが本題です。(長くてすみません・・・:confounded:

まずコスト配分レポートがどのようなものなのか分からないとイメージが湧きづらいと思いますので、今回の分析で使用するカラムだけでもご説明したいと思います(カラム数が非常に多いので全部説明するのは避けます)

コスト配分レポートの中身

以下のようなカラムがあります。

代表的なカラム 説明
LinkedAccountId 123456789012 料金が発生したAWSアカウントID
ProductName Amazon Elastic Compute Cloud
Amazon Relational Database Service
など
料金が発生したAWSサービスの名前
UsageType APN1-BoxUsage:r4.large
APN1-DataTransfer-Out-Bytes
など
料金発生対象の詳細
Operation RunInstances
CreateVolume-Gp2
など
料金発生時のオペレーション(イベント)
イベントをキャッチして実行するようなコードを書いたことある方ならピンとくると思います
ItemDescription \$0.0152 per On Demand Linux t2.micro Instance Hour
$0.05 per GB-Month of snapshot data stored - Asia Pacific (Tokyo)
など
料金の説明
UsageStartDate 2020-12-01 23:00:00
など
料金発生の対象日時
UnBlendedCost 1.9354838712
など
料金の金額(USD)
ResourceId i-xxxxxxxx
arn:aws:rds:ap-northeast-1:123456789012:db:XXXXXX
リソースのARNや、EC2とかの場合はインスタンスIDが入ります
user:Name 事前に設定したNameタグ
user:system 事前に設定したsystemタグ
user:environment 事前に設定したenvironmentタグ

コスト配分レポートを変換する

コスト配分レポートと、事前に用意したマッピングデータを使って変換処理をかけていきます。
変換処理のポイントは、「Name」(user:Name)カラムに値を入れていくことです。
この「Name」カラムを料金集計のキーにします。
例えば冒頭にお見せしたグラフの例で言うと、

  • EC2インスタンス
  • アタッチされているEBSボリューム
  • そのEBSボリュームのスナップショット
  • その他EC2に紐づく料金レコード(データ転送など)

これらの料金レコードの「Name」カラムには全てEC2インスタンス名を入れます。
これにより、とあるEC2インスタンスとそれに付随する料金がまとめて何ドルか可視化でき、ドリルダウンするとさらにその内訳が可視化できるようになるわけです。

今回はPython3です。(bashはオススメしません)
CSVを扱うため、pandasを使いました。

少し長いのでいくつかに分割します。

data_transfer_sample.py
import pandas as pd
import re

# コスト配分レポートをpandasデータフレーム化
# この際、使うカラムだけ抽出する
df = pd.read_csv('./123456789012-2020-12.csv',
                 usecols = ['LinkedAccountId', 'ProductName', 'UsageType', 'Operation', 'ReservedInstance',
                            'ItemDescription', 'UsageStartDate', 'UnBlendedCost', 'ResourceId', 'user:system',
                            'user:environment', 'user:Name'],
                 dtype = 'object')

## 見やすくするため、タグ名につく「user:」を削除しておく
df = df.rename(columns={'user:system': 'System', 'user:environment': 'Environment', 'user:Name': 'Name'})
## 不要な行を削除しておく
df = df[~df.ItemDescription.str.contains('Tax of type CT')]            # 消費税
df = df[~df.ItemDescription.str.contains('Total for linked account')]  # アカウント毎のサマリ行

まずコスト配分レポートをpandasデータフレーム化します。
カラム名を調整したり、不要なレコードの削除をしておきます。
データ型はobjectを指定します。そのままだとUnblendedCostなどの少数がfloatにされてしまうのでこれを避けるためです。
つぎ。

data_transfer_sample.py
# 1行(各料金レコード)毎にName列を埋めていく
for index, row in df.iterrows():
    ## ResourceIdが空になっているレコードへの処理
    if pd.isnull(row['ResourceId']):
        ### EC2系レコード
        if row['ProductName'] == 'Amazon Elastic Compute Cloud':
            #### EIP料金
            if row['Operation'] == 'AssociateAddressVPC':
                row['Name'] = 'EIP'
            #### EC2 RI料金
            elif re.search('.*USD.*hourly fee per.*', row['ItemDescription']):
                row['Name'] = 'EC2 RI applied instance fee'
            #### EC2系その他
            else:
                row['Name'] = 'EC2 Other'
        ### RDS系レコード
        elif row['ProductName'] == 'Amazon Relational Database Service':
            #### RDSバックアップ料金
            if re.search('.*ChargedBackupUsage.*', row['UsageType']):
                row['Name'] = 'RDS Backup Storage'
            #### RDS RI 前払い料金
            elif re.search('.*Sign up charge for subscription.*', row['ItemDescription']):
                row['Name'] = 'RDS RI Upfront'
            #### RDS RI料金
            elif re.search('.*USD.*hourly fee per.*', row['ItemDescription']):
                row['Name'] = 'RDS RI applied instance fee'
        ### Dynamo系レコード
        elif row['ProductName'] == 'Amazon DynamoDB':
            row['Name'] = 'Dynamo DataTransfer'
        ### CloudWatch系レコード
        elif row['ProductName'] == 'AmazonCloudWatch':
            row['Name'] = 'CloudWatch'
        ### CloudTrail系レコード
        elif row['ProductName'] == 'AWS CloudTrail':
            row['Name'] = 'CloudTrail'
        ### IoT系レコード
        elif row['ProductName'] == 'AWS IoT':
            row['Name'] = 'IoT'
        ### SQS系レコード
        elif row['ProductName'] == 'Amazon Simple Queue Service':
            row['Name'] = 'SQS Other'
### 以下略

Dataframeを1行ずつチェックし、Name列を埋めていきます。
コスト配分レポートには、ResourceId列が空のレコードとそうじゃないレコードがあるので、まず空のレコードの部分の処理です。
ResourceIdが空ということは、どこのリソースで発生した料金なのかわからないということです。
なので一旦NameにはAWSのサービス名を入れます。QuickSightでの分析時は、ItemDescription列の内容で何の料金かある程度は分かります。
つぎ。

data_transfer_sample.py
    ## ResourceIdが空でないレコードへの処理
    else:
        ### ELB系レコード
        if row['ProductName'] == 'Elastic Load Balancing':
            ### ALBレコードの場合NameにALB名をセット
            if re.search('.*loadbalancer\/app\/.*', row['ResourceId']):
                row['Name'] = row['ResourceId'].split('/')[2]
            ### CLBレコードの場合NameにCLB名をセット
            elif re.search('.*loadbalancer\/.*', row['ResourceId']):
                row['Name'] = row['ResourceId'].split('/')[1]
            else:
                row['Name'] = 'ELB Other'  # NLB未使用なのでどんなレコードが出てくるかわからずこんな書き方で逃げてます
        ### EC2系レコード
        elif row['ProductName'] == 'Amazon Elastic Compute Cloud':
            ### Name設定済の場合は何もしない
            if not pd.isnull(row['Name']):
                pass
            ### NAT Gatewayレコードの場合はNameにNAT GatewayのIDをセット
            elif re.search('.*natgateway.*', row['ResourceId']):
                row['Name'] = row['ResourceId'].split('/')[1]
            ### 上記以外(EBS、スナップショット、T系インスタンスの課金クレジット、その他EC2系料金)
            else:
                ### レコードと同じ日のEC2マッピングデータをpandasデータフレーム化
                record_date = row['UsageStartDate'][0:10]
                df_ec2 = pd.read_csv('./' + record_date + '_ec2_mapping.csv', dtype = 'object')
                ### スナップショットの場合は後段の処理のためリソースIDを抽出しておく
                if re.search('.*snapshot.*', row['ResourceId']):
                    row['ResourceId'] = row['ResourceId'].split('/')[1]
                ### EC2マッピングデータからResourceIdが見つかればそこからNameを取得する
                ### 一致するResourceIdを持つ行を検索
                matched_row = df_ec2[(df_ec2['Instance ID'] == row['ResourceId']) |
                                     (df_ec2['Volume ID'] == row['ResourceId']) |
                                     (df_ec2['Snapshot ID'] == row['ResourceId'])]
                ### 検索にマッチする行があればその行からName、System、Environmentを取得
                if not len(matched_row.index) == 0:
                    row['Name'] = matched_row.iloc[0]['Name']
                    row['System'] = matched_row.iloc[0]['System']
                    row['Environment'] = matched_row.iloc[0]['Environment']
                    if pd.isnull(row['Name']):
                        if re.search('.*snap-.*', row['ResourceId']):
                            row['Name'] = '未使用EBSのスナップショット'
                        else:
                            row['Name'] = '未使用EBS'
                else:
                    row['Name'] = '削除済EBS、対応するEBSの無いスナップショット、リテンションされたスナップショット、AMIのスナップショット等'
                ### 使い終わったDataframeを破棄
                del df_ec2
        ### RDS系レコード
        elif row['ProductName'] == 'Amazon Relational Database Service':
            ### クラスタバックアップ料金の場合
            if re.search('.*cluster-backup.*', row['ResourceId']):
                row['Name'] = 'RDS Backup Storage'
            else:
                # レコードと同じ日のRDSマッピングデータをpandasデータフレーム化
                record_date = row['UsageStartDate'][0:10]
                df_rds = pd.read_csv('./' + record_date + '_rds_mapping.csv', dtype = 'object')
                ### コスト配分レポートのResourceIdにはARNが入っているので余計な文字列をカットしておく
                row['ResourceId'] = row['ResourceId'].split(':')[6]
                ### RDSマッピングデータから一致するResourceIdを持つ行を検索
                matched_row = df_rds[(df_rds['Instance ID'] == row['ResourceId']) |
                                     (df_rds['Cluster ID'] == row['ResourceId']) |
                                     (df_rds['Resource ID'] == row['ResourceId']) |
                                     (df_rds['Snapshot ID'] == row['ResourceId'])]
                ### 検索にマッチする行があればその行からName(クラスターID)、System、Environmentを取得
                if not len(matched_row.index) == 0:
                    row['Name'] = matched_row.iloc[0]['Cluster ID']
                    row['System'] = matched_row.iloc[0]['System']
                    row['Environment'] = matched_row.iloc[0]['Environment']
                    ### クラスターIDが空だった場合は非AuroraなのでインスタンスIDを取得
                    if pd.isnull(row['Name']):
                        row['Name'] = matched_row.iloc[0]['Instance ID']
                ### 使い終わったDataframeを破棄
                del df_rds
        ### CloudWatch系レコード
        elif row['ProductName'] == 'AmazonCloudWatch':
            row['Name'] = 'CloudWatch'
        ### Route53系レコード
        elif row['ProductName'] == 'Amazon Route 53':
            row['Name'] = 'Route53'
        ### DynamoDB系レコード
        elif row['ProductName'] == 'Amazon DynamoDB':
            row['Name'] = 'Dynamo - ' + row['ResourceId'].split('/')[1]
        ### Kinesis系レコード
        elif row['ProductName'] == 'Amazon Kinesis':
            row['Name'] = 'Kinesis - ' + row['ResourceId'].split('/')[1]
        ### データ転送系レコード
        elif row['ProductName'] == 'AWS Database Migration Service':
            if re.search('*CloudFront*', row['UsageType']):
                row['Name'] = 'CloudFront'
            elif re.search('*DataTransfer*', row['UsageType']):
                row['Name'] = 'DataTransfer'
            elif re.search('*InstanceUsg*', row['UsageType']):
                row['Name'] = 'DMS Instance'
            else:
                row['Name'] = 'DMS Other'
        ### DirectConnect系レコード
        elif row['ProductName'] == 'AWS Direct Connect':
            row['Name'] = 'DirectConnect'
        ### CloudFront系レコード
        elif row['ProductName'] == 'Amazon CloudFront':
            row['Name'] = 'DirectConnect'
        ### KMS系レコード
        elif row['ProductName'] == 'AWS Key Management Service':
            row['Name'] = 'KMS'
        ### SQS系レコード
        elif row['ProductName'] == 'Amazon Simple Queue Service':
            row['Name'] = 'SQS - ' + row['ResourceId'].split(':')[5]
        ### Lambda系レコード
        elif row['ProductName'] == 'AWS Lambda':
            row['Name'] = 'Lambda'
        ### S3系レコード
        elif row['ProductName'] == 'Amazon Simple Storage Service':
            row['Name'] = 'S3 - ' + row['ResourceId']
        ### SNS系レコード
        elif row['ProductName'] == 'Amazon Simple Notification Service':
            row['Name'] = 'SNS'
        ### Athena系レコード
        elif row['ProductName'] == 'Amazon Athena':
            row['Name'] = 'Athena - ' + row['ResourceId'].split(':')[5]
        ### Secrets Manager系レコード
        elif row['ProductName'] == 'AWS Secrets Manager':
            row['Name'] = 'Secrets Manager'
### 以下略

そしてResourceIdが空じゃない場合の処理です。
ここは実際のコスト配分レポートとにらめっこしながら、Nameに何を入れていくのか考えつつ書いていく感じです。
EC2とRDSは、事前に用意したマッピングデータを使い、EBSやスナップショット等のデータを紐付けていくようにNameに値を入れていきます。

時々、ResourceIdに対してsplitをかけていますが、これはARNからリソース名を抜き出すためです。
ARNのままでも分かるっちゃ分かりますが、さすがに長すぎて見づらいので。

data_transfer_sample.py
# CSV出力
df.to_csv('out.csv', encoding='utf-8', index=False, quoting=0, quotechar='"')

最後にcsv化してS3にアップし、それをQuickSightで読み込ませます。

まとめ

コードや文章が拙く、分かりづらいところあったらすみません。
私の職場ではこれで定期的にレポートを作成してコスト削減につなげています。BIってすごいなと素直に思いました。

また、コードの書き方で「こうしたほうがいいよ!」とか「ウチではこうしたよ!」なんかあればぜひ教えてください!
勉強したいので:sweat:

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away