2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Data Exports で S3 に出力した GZIP 形式の CUR 2.0 データを加工する Lambda 関数を作る

Posted at

AWS 利用料の詳細を把握するため、Data Exports で CUR (Cost Usage Report) 2.0 データを月単位で S3 に出力しているのですが、ここから特定の EC2 インスタンスのデータのみを別の CSV ファイルに抽出したいと思い立ちました。

CUR 2.0 データは CSV 形式のテキストファイルを GZIP 形式で圧縮して S3 に出力しているため、これを展開し、中の CSV ファイルを取り出してから抽出しなければなりません。

手作業は面倒なので、Lambda から Data Exports 出力先 S3 バケットの GZIP ファイルを展開し、EC2 インスタンスのデータのみ抽出して CSV に出力してから別の S3 バケットへ保存する処理を自動化してみたいと思います。

Lambda 関数の作成

マネジメントコンソールから「関数の作成」をします。

スクリーンショット 2025-08-09 17.05.16.png

ランタイムは Python を、アーキテクチャはコスト効率の良い arm64 を選択しました。

ここでポイントとなるのが、Lambda 関数の実行ロールです。事前に必要なポリシー、信頼関係などを設定した IAM ロールを作成しておき、それを割り当てるか、この画面で作成するか、いずれかの方法で Lambda 関数に IAM ロールを実行ロールとして割り当てます。

ここでは「基本的な Lambda アクセス権限で新しいロールを作成」で自動的に IAM ロールを作成してみました。

作成された IAM ロールを確認します。

スクリーンショット 2025-08-09 17.09.15.png

スクリーンショット 2025-08-09 17.12.50.png

作成する Lambda 関数の処理に合わせて、適宜 IAM ロールの修正が必要になるかと思います。

Lambda 関数から S3 バケットを読み書きするためには、Lambda 関数の実行ロールに設定した IAM ロールに対する許可を、S3 バケットのバケットポリシーで設定する必要があります。

S3 バケットポリシーの設定

Data Exports の出力先 S3 バケット側の設定

既存のバケットポリシーに以下のとおり、Lambda 関数の実行ロールに対して S3 バケットの読み取りを許可する設定を追記しました。

{
    "Sid": "EnableAWSLambdaToReadToS3",
    "Effect": "Allow",
    "Principal": {
        "AWS": "Lambda 関数の実行ロールの ARN"
    },
    "Action": [
        "s3:GetObject",
        "s3:ListBucket"
    ],
    "Resource": [
        "arn:aws:s3:::Data Exports の出力先バケット名",
        "arn:aws:s3:::Data Exports の出力先バケット名/*"
    ]
}

加工した CSV ファイルの出力先 S3 バケット側の設定

こちらは新規に「test01-dev-bucket-putfromlambdatest」という名前の S3 バケットを作成し、以下の内容で書き込みを許可するバケットポリシーを設定しました。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "EnableAWSLambdaToWriteToS3",
            "Effect": "Allow",
            "Principal": {
                "AWS": "Lambda 関数の実行ロールの ARN"
            },
            "Action": [
                "s3:GetObject",
                "s3:ListBucket",
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::test01-dev-bucket-putfromlambdatest",
                "arn:aws:s3:::test01-dev-bucket-putfromlambdatest/*"
            ]
        }
    ]
}

これで Lambda 関数から S3 バケットへ読み書きする準備は完了です。

S3 バケットの GZIP ファイルを展開して加工後に別バケットへ保存する Lambda 関数のサンプル

Lambda の開発のお作法が全然分からなかったのですが、最低限動く Python スクリプトのサンプルです。

イベントに、対象年月を「YYYY-MM」の形式で、対象インスタンスのインスタンス ID とボリューム ID をそれぞれキーにセットして Lambda 関数側に渡してパラメーターとします。

一旦 S3 バケットから GZIP ファイルを、Lambda 関数が扱えるローカルのパス /tmp にダウンロードし、これを展開してパラメーターで指定したインスタンス ID かボリューム ID を含む行のみを抽出して別の CSV ファイルとして /tmp に出力し、これを別の S3 バケットへアップロードするようにしました。

Python のサンプルコード

import csv
import gzip
from pathlib import Path

import boto3

CUR_BUCKET_NAME = "test01-dev-buckets-dataexports"
CUR_BUCKET_PREFIX = "CUR/test01-dataexports/data/BILLING_PERIOD="
CUR_FILE_NAME = "test01-dataexports-00001.csv.gz"
OUTPUT_BUCKET_NAME = "test01-dev-bucket-putfromlambdatest"

client = boto3.client("s3")


def lambda_handler(event, context):
    # event の "TargetTerm" キーに "YYYY-MM" の形式で対象の月を文字列でセットする。
    target_term = event["TargetTerm"]

    # Data Exports の出力先 S3 バケットから /tmp に gzip ファイルをダウンロードする。
    bucket_prefix = CUR_BUCKET_PREFIX + target_term
    key = bucket_prefix + "/" + CUR_FILE_NAME
    cur_file_path = "/tmp/" + CUR_FILE_NAME
    client.download_file(CUR_BUCKET_NAME, key, cur_file_path)

    if not Path(cur_file_path).exists():
        return '"' + cur_file_path + '"' + " does not exist."

    if Path(cur_file_path).suffix != ".gz":
        return '"' + cur_file_path + '"' + " is not gzip file."

    # event の "Instances" キーに "InstanceId", "VolumeId" のキーを持つ Dict 要素をセットする。
    # これらのキーの値は抽出したいインスタンスのインスタンス ID とボリューム ID をセットする。
    instances = event["Instances"]
    instances_number = len(instances)
    if instances_number == 0:
        return "no instance data in event."

    # /tmp にダウンロードした gzip ファイルを展開し、CUR 2.0 の CSV から対象インスタンスの行のみ抽出する。
    extracted_from_cur = list()
    with gzip.open(cur_file_path, "rt") as f:
        reader = csv.DictReader(f)
        for row in reader:
            for n in range(0, instances_number):
                instance = instances[n]
                if (
                    row["line_item_resource_id"] == instance["InstanceId"]
                    or row["line_item_resource_id"] == instance["VolumeId"]
                ):
                    extracted_from_cur.append(
                        [
                            row["bill_billing_period_start_date"],
                            row["bill_billing_period_end_date"],
                            row["line_item_product_code"],
                            row["line_item_line_item_description"],
                            row["line_item_line_item_type"],
                            row["line_item_operation"],
                            row["line_item_resource_id"],
                            row["line_item_unblended_rate"],
                            row["line_item_usage_amount"],
                            row["line_item_unblended_cost"],
                            row["resource_tags"],
                        ]
                    )

    if extracted_from_cur == []:
        return "no instance data in CUR csv."

    # /tmp に新規作成する CSV ファイルに CUR 2.0 の CSV から抽出した行データを書き込む。
    output_path = "/tmp/" + target_term + ".csv"
    with open(output_path, "w") as f:
        writer = csv.writer(f)
        writer.writerow(
            [
                "bill_billing_period_start_date",
                "bill_billing_period_end_date",
                "line_item_product_code",
                "line_item_line_item_description",
                "line_item_line_item_type",
                "line_item_operation",
                "line_item_resource_id",
                "line_item_unblended_rate",
                "line_item_usage_amount",
                "line_item_unblended_cost",
                "resource_tags",
            ]
        )
        writer.writerows(extracted_from_cur)

    if not Path(output_path).exists():
        return '"' + output_path + '"' + " does not exist."

    # 抽出した CSV ファイルを S3 バケットにアップロードする。
    client.upload_file(output_path, OUTPUT_BUCKET_NAME, Path(output_path).name)

    return 0

(参考)Lambda 関数のデプロイ方法

Lambda 関数のデプロイ方法は色々あると思いますが、今回 VS Code に「AWS Toolkit」という拡張をインストールして使ったのがとても便利でした。

Lambda 関数を作成するアカウントに対して適切な権限のクレデンシャルを取得できるように AWS CLI のプロファイルを設定しておけば、VS Code で開発し、VS Code からデプロイしてくれます。

Lambda 関数へ渡すイベント

Lambda 関数に渡すイベントの JSON は次のとおりです。

{
  "TargetTerm": "2025-08",
  "Instances": [
    {
      "InstanceId": "抽出したい対象のインスタンス ID",
      "VolumeId": "抽出したい対象のボリューム ID"
    }
  ]
}

Lambda 関数のテスト

マネジメントコンソールから上記のイベントを設定してテストします。

スクリーンショット 2025-08-09 17.57.18.png

期待したリターンコードが返ってきました。

スクリーンショット 2025-08-09 18.07.39.png

Lambda 関数を作成すると CloudWatch Logs のロググループが自動で作られており、CloudWatch Logs 側にも同様なログが記録されます。

スクリーンショット 2025-08-09 18.15.58.png

S3 バケットに期待通り Lambda 関数からファイルをアップロードできていました。

スクリーンショット 2025-08-09 18.14.30.png

こういった AWS リソースを扱う処理を作るのに Lambda は便利ですね。

Gov-JAWS#3 の宣伝

2025/08/20 の水曜日 12:00 から、公共分野の JAWS-UG である Gov-JAWS の第 3 回が開催されます!ガバメントクラウドをはじめ公共系の AWS 活用に興味のある人はぜひ参加をお願いします!

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?