プロダクト開発部 Ads チームの yamaYu です。最近体重と筋肉量をコントロールしたい願望があり、まずは可視化ということで体組成計を買いました。まだ全然成果は上がっていないですが、現状を把握できて良い感じです。
今回の記事ですが、最近取り組んだ S3 のコスト削減の施策において、S3 Inventory がコストの可視化の文脈で良い感じだったのでその話について書いていきます。
最終的に ↓ のような感じでプレフィックス別にコストを分析できるようにしました。
課題感
Ads チームの AWS のコストの内訳を見ると S3 のストレージ利用料金が大きな割合を占めています。
合計ストレージサイズは 2PB、オブジェクト数は 1G ありました。その大部分は長年蓄積された広告サービスのログによるものです。ログ用のバケットの中にサービスごとのプレフィックスが切られており、それぞれのサービスのログが格納されています。
コスト削減できる箇所を探して対応の優先順位をつけるために、どのプレフィックスでコストがかかっているのかを把握する必要があったのですが、プレフィックスの数が多く、階層の深さがプロジェクトごとに様々で、格納されているオブジェクトの数が膨大なため全容をすぐには把握できない状態でした。
継続的にコスト改善をしていくためにボトルネックをシュッと確認できるようにするための解決策が求められていました。
S3 のストレージ使用量を把握するための選択肢
S3 のストレージ使用量を把握するには、次のような選択肢があります。
- AWS CLI
- S3 Storage Lens
- S3 Inventory
以下でそれぞれ見ていきます。なお価格に言及している箇所については 2024 年 4 月時点の東京リージョンの価格になります。
AWS CLI
一番シンプルな方法です。AWS CLI を叩きます。
aws s3 ls s3://my_bucket/some/prefix/ --sum --human --recursive
オブジェクト数が少ない場合や分析対象のプレフィックスが明確な場合にはこれで十分だと思います。オブジェクト数が多い場合には実行にそれなりのリクエスト料金と時間がかかるため注意が必要です。
ls — AWS CLI 2.15.33 Command Reference
S3 Storage Lens
S3 の使用量を可視化したダッシュボードを提供するサービスです。無料枠としてバケットレベルでの分析が提供されています。また有料オプションの高度なメトリクスの一つにプレフィックスレベルでの分析があり、ストレージ使用量上位のプレフィックスの一覧をダッシュボード上に表示できます。
有料オプションはオブジェクト数での課金体系になっており、100 万オブジェクトあたりの月額はオブジェクトサイズによって 0.12USD 〜 0.20USD です。
簡単に設定できる一方、表示件数に制限があり、ストレージクラスでフィルタできないなどあまり柔軟な使い方はできない印象です。
Amazon S3 Storage Lens の紹介 — オブジェクトストレージに組織全体にわたる可視性を | Amazon Web Services ブログ
S3 Inventory
S3 バケット内のオブジェクトの一覧とファイルサイズやストレージクラスなどのメタデータを CSV や Parquet などの形式で定期的に出力してくれるサービスで、この記事の本命です。
オブジェクト数での課金体系になっており、100 万オブジェクトあたりの実行単価は 0.0028USD なので、日次で実行したとしても S3 Storage Lens の有料オプションよりもこちらのほうが安いです*1。
出力ファイルを Athena と連携すれば、SQL で分析することもできます。
S3 Inventory で使用量を可視化する
ストレージクラスやログの生成年でのフィルタなど柔軟な分析をするために S3 Inventory を採用しました。以下では、Terraform での設定と分析クエリの一例を解説します。
S3 Inventory を設定する
分析対象のバケットごとに S3 Inventory の設定していきます。下記は target-bucket バケットのオブジェクト一覧を output-bucket バケットに Parquet 形式*2で日次で出力する設定例です。optional_fields
にはお好みで分析に必要なカラムを追加してください。例ではコスト計算に必要な size
と storage_class
以外は省略しています。
s3_bucket_inventory | Terraform Registry
// S3 Inventory 設定するバケット resource "aws_s3_bucket" "target_bucket" { bucket = "target-bucket" } // S3 Inventory の出力先のバケット resource "aws_s3_bucket" "output_bucket" { bucket = "output-bucket" } // S3 Inventory の設定 resource "aws_s3_bucket_inventory" "target_bucket" { bucket = aws_s3_bucket.target_bucket.id name = "default" destination { bucket { format = "Parquet" bucket_arn = aws_s3_bucket.target_bucket.arn } } schedule { frequency = "Daily" } included_object_versions = "All" optional_fields = [ "Size", "StorageClass", ︙ ] }
また出力先バケットに S3 Inventory による書き込みの許可が必要なため、バケットポリシーを設定します*3。
// 出力バケットにバケットポリシーを追加する resource "aws_s3_bucket_policy" "s3inventory" { bucket = aws_s3_bucket.output_bucket.id policy = data.aws_iam_policy_document.allow_s3inventory_put_object_to_output_bucket.json } data "aws_iam_policy_document" "allow_s3inventory_put_object_to_output_bucket" { statement { sid = "AllowS3InventoryPutObjectToOutputBucket" effect = "Allow" actions = [ "s3:PutObject", ] condition { test = "StringEquals" variable = "s3:x-amz-acl" values = [ "bucket-owner-full-control", ] } condition { test = "StringEquals" variable = "aws:SourceAccount" values = [ "{your_aws_acount_id}", ] } resources = [ aws_s3_bucket.output_bucket.arn, ] principals { type = "Service" identifiers = [ "s3.amazonaws.com", ] } } }
Athena と連携する
出力ファイルを Athena から読み込むために Glue Data Catalog のデータベースとテーブルを作成します。
パーティションキーに bucket_name
を追加し、各バケットごとに設定した S3 Inventory の出力を一つのテーブルで一括で扱えるようにしています。
aws_glue_catalog_table | Terraform Registry
resource "aws_glue_catalog_database" "workspace" { name = "workspace" location_uri = "s3://${aws_s3_bucket.output_bucket.id}" } resource "aws_glue_catalog_table" "s3inventory" { database_name = aws_glue_catalog_database.workspace.name name = "s3inventory" table_type = "EXTERNAL_TABLE" parameters = { "projection.enabled" = "true" "projection.bucket_name.type" = "injected" "projection.dt.format" = "yyyy-MM-dd-HH-mm" "projection.dt.interval" = "1" "projection.dt.interval.unit" = "HOURS" "projection.dt.range" = "2024-01-01-00-00,NOW" "projection.dt.type" = "date" "storage.location.template" = "${aws_glue_catalog_database.workspace.location_uri}/$${bucket_name}/default/hive/dt=$${dt}" } storage_descriptor { location = aws_glue_catalog_database.workspace.location_uri input_format = "org.apache.hadoop.hive.ql.io.SymlinkTextInputFormat" output_format = "org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat" ser_de_info { name = "ParquetHiveSerDe" serialization_library = "org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe" } columns { name = "key" type = "string" } columns { name = "size" type = "bigint" } columns { name = "storage_class" type = "string" } ︙ } partition_keys { name = "bucket_name" type = "string" } partition_keys { name = "dt" type = "string" } }
SHOW CREATE TABLE workspace.s3inventory
の出力も載せておきます。
CREATE EXTERNAL TABLE `workspace.s3inventory`( `key` string, `size` bigint, `storage_class` string, ︙ ) PARTITIONED BY ( `bucket_name` string, `dt` string ) ROW FORMAT SERDE 'org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe' STORED AS INPUTFORMAT 'org.apache.hadoop.hive.ql.io.SymlinkTextInputFormat' OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat' LOCATION 's3://workspace' TBLPROPERTIES ( 'projection.bucket_name.type'='injected', 'projection.dt.format'='yyyy-MM-dd-HH-mm', 'projection.dt.interval'='1', 'projection.dt.interval.unit'='HOURS', 'projection.dt.range'='2024-01-01-00-00,NOW', 'projection.dt.type'='date', 'projection.enabled'='true', 'storage.location.template'='s3://workspace/${bucket_name}/default/hive/dt=${dt}' )
SQL を書いて分析する
プレフィックスごとに、ストレージ使用量、月ごとのコスト、オブジェクト数を集計するクエリです。
transform
関数で key
からプレフィックスの配列を生成している箇所がポイントです。
WITH splitted AS ( SELECT -- prefix の配列を得る (計算量削減のため深さ 5 までに絞る) -- e.g. bucket_name='bucket', key='key/to/some/objects.gz' -- -> ['bucket/key', 'bucket/key/to', 'bucket/key/to/some', 'bucket/key/to/some/objects.gz'] transform( sequence( 1, array_min( array [cardinality(split(url_decode(key), '/')), 5] ) ), x -> bucket_name || '/' || array_join(slice(split(url_decode(KEY), '/'), 1, x), '/') ) AS items, storage_class, size FROM workspace.s3inventory WHERE bucket_name IN ('output-bucket') AND dt = date_format( CURRENT_TIMESTAMP - INTERVAL '1' DAY, '%Y-%m-%d-01-00' ) ), grouped AS ( SELECT prefix, storage_class, round(sum(size) / 1000000000.0, 1) AS size_GB, sum( CASE WHEN size > 0 THEN 1 ELSE 0 END ) AS object_count -- key にはオブジェクトの実態のないパスのみのレコードも含まれる FROM splitted, unnest(items) AS t(prefix) -- prefix ごとにレコードを複製する GROUP BY 1, 2 ) SELECT prefix, storage_class, size_GB, round( size_GB * CASE storage_class WHEN 'STANDARD' THEN 0.023 -- 最初の500TB までの価格は異なるが簡単のため定数として扱う WHEN 'GLACIER' THEN 0.0045 WHEN 'DEEP_ARCHIVE' THEN 0.002 END, 1 ) AS cost_per_month_in_dollars -- ref: https://aws.amazon.com/jp/s3/pricing/ , object_count FROM grouped ORDER BY 4 DESC, 1 ASC LIMIT 1000
イメージですが、クエリを実行すると下記のような結果を得られます。どのプレフィックスでコストがかかっているのかを把握することができました。
まとめ
今回の記事では、S3 のストレージ使用量を把握するための選択肢と、S3 Inventory の設定例、分析クエリを紹介しました。筆者のチームでは、使用量のボトルネックが可視化されたことでコスト削減金額の見積もりを立てやすくなりました。また意外なところでコストがかかっていたことが発見されて実際にコスト削減にも繋がりました。
この記事が読者様のお役に立てますと幸いです。