0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

S3 を Deep Archive へ ~~S3 Inventory + Athena で何件・何 GiB 動くかを数える~~

0
Posted at

はじめに

業務で、S3 Glacier Deep Archive によるコスト削減に取り組むことになりました。
これを機に、移す前に知っておきたい仕様を検証環境で確かめながらまとめてみました。

対象読者

  • S3 のライフサイクルで、Deep Archive への移行を考えている方
  • 移行の前に、対象の件数や費用を見積もりたい方
  • S3 Inventory を使ったことがない方

概要

  • Deep Archive への移行費用は件数で、削減額は容量で決まります。
  • S3 Inventory + Athena なら、件数と容量をサイズ別・プレフィックス別に SQL で数えられます。
  • ライフサイクルの既定では、128KiB 未満は移りません。
  • ただし、東京の単価だと、128KiB のファイルは元を取るのに約 25か月かかります。
  • 「180日で移行・366日で削除」のルールでは、元が取れるのは約 490KiB 以上のファイルです。

移す前に知りたかったこと: 件数と容量

移行のルール

今回は、業務で扱った次のルールを例に考えます。

  • 作成から 180日で Deep Archive へ移す
  • 作成から 366日で削除する

Deep Archive には、最低 180日分の料金がかかります。
今回は Deep Archive に 186日置いてから消すので、早期削除の料金は出ません。
※ルールを入れた初日だけは例外です。詳しくは後半の注意点で触れます。

移行費用は件数、削減額は容量で決まる

Deep Archive へ移すと、費用が2種類動きます。

費用 何で決まるか タイミング
移行のリクエスト料金 件数 移すときに一度だけ
毎月の保管料金 容量 毎月(下がる)

同じ 1GiB でも、件数が違うと結果が大きく変わります。

1GiB の持ち方 件数 移行費用(一度だけ) 毎月の削減額 元が取れるまで
1MiB × 1,024件 1,024 約 0.067 USD 約 0.023 USD 約 3か月
128KiB × 8,192件 8,192 約 0.53 USD 約 0.021 USD 約 25か月

※東京リージョンの単価で試算(2026-10-22 時点)。計算の前提は後半に書きます。

小さいファイルが多いと、移行費用が膨らみます。
そのため、容量だけでなく件数も数える必要があります。

CloudWatch の数字は、そのままでは使えない

バケットの件数と容量は、CloudWatch のメトリクスで手軽に見られます。

ただし、この2つの数字には、移行の対象にならないものも入っています。
S3 のメトリクスの説明によると、NumberOfObjects と BucketSizeBytes は次も含めて数えます。

  • バージョニングの旧バージョン
  • 削除マーカー(NumberOfObjects のみ)
  • 完了していないマルチパートアップロードの部品

また、サイズごとの内訳は分かりません。
128KiB 未満が何件あるかは、別の方法で数える必要があります。

数え方: LIST と S3 Inventory

数え方は2つ考えました。

項目 LIST API で全件取る S3 Inventory + Athena
取れるもの 実行した時点の一覧 毎日(または毎週)の一覧
回数 1回 1,000件まで。100万件なら約 1,000回 設定は1回
集計 自分でスクリプトを書く SQL で書ける
待ち時間 すぐ 初回は最大 48時間
移行前後の比較 前後で2回取って突き合わせる 日付ごとに残るので、同じ SQL で比べられる

Inventory は日付ごとに一覧が残ります。
そのため、移行前の費用の見積もりと、移行後の「問題なく移ったか」の確認を、同じ SQL でできます。
この記事では、Inventory + Athena で数える方法を試しました。

S3 Inventory + Athena の仕組み

全体の流れ

Inventory は、バケットの中身の一覧を、別のバケットへ毎日書き出す機能です。
書き出した一覧を、Athena のテーブルとして読みます。

出力先の構造

出力先には、次の形でファイルが届きます。

inventory/
└── <元バケット>/
    └── <Inventory の名前>/
        ├── data/                          ← 一覧の本体(Parquet)
        ├── 2026-10-23T01-00Z/
        │   └── manifest.json              ← その日の一覧に含まれるファイル
        └── hive/
            └── dt=2026-10-23-01-00/
                └── symlink.txt            ← Athena が読む入口

ポイントは hive/ の下です。
dt=日付 のフォルダごとに symlink.txt があり、その日の Parquet ファイルの場所が書かれています。
Athena は、この symlink.txt をたどって Parquet を読みます。

そのため、テーブルは dt で区切ります。
SQL で dt を指定すると、その日の一覧だけを数えられます。

Athena が dt で symlink.txt を選び、書いてある Parquet だけを読む流れ

create_table.sql
CREATE EXTERNAL TABLE IF NOT EXISTS inventory (
  bucket string,
  key string,
  size bigint,
  last_modified_date timestamp,
  storage_class string
)
PARTITIONED BY (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.IgnoreKeyTextOutputFormat'
LOCATION 's3://<出力先バケット>/inventory/<元バケット>/<Inventory の名前>/hive/'
TBLPROPERTIES (
  "projection.enabled" = "true",
  "projection.dt.type" = "date",
  "projection.dt.format" = "yyyy-MM-dd-HH-mm",
  "projection.dt.range" = "2026-10-01-00-00,NOW",
  "projection.dt.interval" = "1",
  "projection.dt.interval.unit" = "HOURS"
);

projection(パーティション射影)を使うと、日付が増えるたびにパーティションを足す作業が要りません。
range の開始日は、Inventory を作った日より前にしておきます。

出力形式は Parquet にした

形式は CSV・ORC・Parquet から選べます。
今回は Parquet にしました。

Parquet は、表のデータを列ごとに圧縮して持つ形式です。
SQL で使う列だけを読むので、読む量が小さくなります。
また、列を名前で読むので、CSV のように DDL と列の順番を合わせなくて済みます。

出力先のバケットポリシーは自分で書く

コンソールで Inventory を作ると、出力先のバケットポリシーが自動で付きます。
Terraform や CLI で作るときは、自分で書く必要があります。

main.tf
resource "aws_s3_bucket_policy" "dest" {
  bucket = aws_s3_bucket.dest.id
  policy = jsonencode({
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "s3.amazonaws.com" }
      Action    = "s3:PutObject"
      Resource  = "${aws_s3_bucket.dest.arn}/inventory/*"
      Condition = {
        ArnLike = { "aws:SourceArn" = aws_s3_bucket.src.arn }
        # (アカウント ID と ACL の条件は省略)
      }
    }]
  })
}

Inventory の設定には、このポリシーへの参照がありません。
そのため、depends_on でポリシーを先に作るようにしました。

検証の条件

答えが分かっているデータを置き、Athena の結果と突き合わせました。

項目 内容
リージョン 東京(ap-northeast-1)
構築 Terraform(hashicorp/aws 6.x)
Inventory 毎日・Parquet・現行バージョンのみ
一覧に含めた列 サイズ・最終更新日時・ストレージクラス
ライフサイクル 0日で Deep Archive へ移行。最小サイズは既定(128KiB)

置いたデータは、次の 161件です。

プレフィックス 件数 1件のサイズ 置いたときのクラス 狙い
logs/ 120 1KiB Standard 移らないはず
edge/131071/ 2 128KiB の1バイト手前 Standard 境界の手前
edge/131072/ 2 ちょうど 128KiB Standard 境界ちょうど
edge/131073/ 2 128KiB の1バイト先 Standard 境界の先
backups/ 30 200KiB Standard 移るはず
archived/ 5 200KiB Deep Archive 最初から移っている

結果1: Athena で件数と容量を数える

Inventory が届くまで約 40時間かかった

バケットを作ったのは、10/22 の 12:03 でした。

最初の一覧が届いたのは、10/24 の 04:01 です。
作成から約 40時間かかりました。

計測は今回の1回だけです。ドキュメントでは「最大 48時間」とされているので、翌日にクエリを流す予定は立てないほうが安全です。

ストレージクラスごとの件数

storage_class 件数 バイト数
DEEP_ARCHIVE 5 1,024,000
STANDARD 156 7,053,312

置いたデータと一致しました。
読んだ量は 189 バイトでした。

128KiB の境界の前後

03_candidates.sql
SELECT
  CASE
    WHEN size < 131072 THEN '< 128KiB'
    WHEN size = 131072 THEN '= 128KiB'
    ELSE '> 128KiB'
  END AS size_class,
  count(*) AS objects,
  sum(size) AS bytes
FROM inventory
WHERE dt = (SELECT max(dt) FROM inventory)
  AND storage_class = 'STANDARD'
GROUP BY 1
ORDER BY 1;
size_class 件数 バイト数
< 128KiB 122 385,022
= 128KiB 2 262,144
> 128KiB 32 6,406,146

logs/ の 120件と edge/131071/ の2件で、122件です。
これも期待どおりでした。

読む量が小さい

クエリ 読んだ量
ストレージクラスごと 189 バイト
128KiB の前後 238 バイト
プレフィックスごと 965 バイト

Parquet なので、使う列だけを読んでいます。
なお、Athena は1クエリあたり最低 10MB 分の料金がかかります。
今回の規模では、料金の差は出ません。

※数値は検証時点のものです。

結果2: ちょうど 128KiB は移るのか

気になった理由

2024年9月から、ライフサイクルの既定が変わりました。
ドキュメントには、128KB より小さいオブジェクトは移行しない、と書かれています。

ここで2つ疑問がありました。

  • 128KB は 128,000 バイトか、131,072 バイト(128KiB)か
  • ちょうど 128KiB は移るのか

移行費用の SQL を size > 131072 と書くか size >= 131072 と書くかが、これで変わります。

結果

ライフサイクルを入れた翌日、list-objects-v2 で確かめました。
Inventory は1日1回なので、移行後の一覧が届くのはさらに後になります。
見たいのは数件だけなので、先に API で直接見ました。

aws s3api list-objects-v2 --bucket <元バケット> \
  --query 'Contents[].[Key,Size,StorageClass]' --output text
プレフィックス 1件のサイズ 翌日のクラス
edge/131071/ 131,071 バイト STANDARD
edge/131072/ 131,072 バイト DEEP_ARCHIVE
edge/131073/ 131,073 バイト DEEP_ARCHIVE
backups/ 200KiB DEEP_ARCHIVE
logs/ 1KiB STANDARD

その後に届いた Inventory でも、プレフィックスごとに数え直しました。

04_by_prefix.sql
SELECT
  regexp_replace(key, '/[^/]+$', '') AS prefix,
  storage_class,
  count(*) AS objects
FROM inventory
WHERE dt = (SELECT max(dt) FROM inventory)
GROUP BY 1, 2
ORDER BY 1, 2;
prefix storage_class 件数
archived DEEP_ARCHIVE 5
backups DEEP_ARCHIVE 30
edge/131071 STANDARD 2
edge/131072 DEEP_ARCHIVE 2
edge/131073 DEEP_ARCHIVE 2
logs STANDARD 120

list-objects-v2 で見た結果と一致しました。
移行の前に使ったのと同じ SQL で、移行後の確認もできます。

分かったことは2つです。

  • 境界は **131,072 バイト(128KiB)**でした。131,071 バイトは移らないので、128,000 バイトではありません
  • ちょうど 128KiB は移ります。 移らないのは「128KiB 未満」だけです

最初は、移行費用の SQL を size > 131072 で書いていました。
これだと、ちょうど 128KiB の分だけ件数が少なく出ます。
実測に合わせて >= に直しました。

05_cost.sql
  AND size >= 131072  -- ちょうど 128KiB も移る

128KiB は損益分岐点ではない

サイズ別の、元が取れるまでの月数

1件あたりで、移行費用と毎月の削減額を比べました。
比べる相手は、移さずに Standard に置き続けた場合です。
取り出し(復元)はしない前提です。

1件のサイズ 元が取れるまで 186日置いたときの損益(100万件あたり)
128KiB 約 25か月 約 -49 USD
256KiB 約 12か月 約 -32 USD
512KiB 約 6か月 約 +3 USD
1MiB 約 3か月 約 +73 USD
10MiB 約 0.3か月 約 +1,326 USD

試算の前提は次のとおりです。

項目 単価(東京、2026-10-22 時点)
Deep Archive への移行 1,000件あたり 0.065 USD
S3 Standard GB 月あたり 0.025 USD
Deep Archive GB 月あたり 0.002 USD
1件ごとの上乗せ 8KB を Standard、32KB を Deep Archive の単価で

単価は AWS Price List API で取得しました。
1か月は 30日として計算しています。

上乗せは、Deep Archive に移したファイルごとに付く管理用のデータです。
名前などの 8KB には Standard の単価、復元に使う索引などの 32KB には Deep Archive の単価がかかります(ドキュメント)。
小さいファイルほど、この上乗せの割合が大きくなります。

186日しか置かないなら、約 490KiB が分かれ目

今回のルールでは、Deep Archive に置くのは 186日(約 6.2か月)です。
その間に元が取れないファイルは、移すと損になります。

サイズ別に、移してからの損益を線で追うと次のようになります。

サイズ別の累積損益。186日の時点で黒字なのは 490KiB 以上だけ

計算すると、分かれ目は約 490KiB でした。
128KiB〜490KiB のファイルは、ライフサイクルでは移りますが、移すと損です。

検証のデータでいうと、backups/ の 200KiB がこれに当たります。
元を取るのに約 16か月かかるので、186日では届きません。

分かれ目は、1件あたり「毎月の削減額 × 置く月数」が移行費用と等しくなるサイズです。
前半の単価から、次の式で出しました。

分かれ目の計算
分かれ目(KiB)
  = (移行単価 × 1,048,576 ÷ 月数 + 8 × Standard 単価 + 32 × Deep Archive 単価)
    ÷ (Standard 単価 − Deep Archive 単価)
  = (0.000065 × 1,048,576 ÷ 6.2 + 8 × 0.025 + 32 × 0.002) ÷ (0.025 − 0.002)
  ≈ 489

移行単価は1件あたりの値(0.065 USD ÷ 1,000件)で、ほかの単価は GB 月あたりの値です。
1,048,576 は、1GB を KiB に直した数です。
ルールの日数やリージョンが違う場合も、月数と単価を差し替えれば分かれ目を出せます。

移すファイルをサイズで絞るなら、ライフサイクルのフィルタで ObjectSizeGreaterThan を指定できます。既定の 128KiB より大きい値にすれば、損になるサイズ帯を外せます。ドキュメントでも、小さいファイルが混ざる場合はサイズのフィルタで外すことが勧められています。

自分のバケットで、移るものと消えるものを数える

単価から分かるのは、「何 KiB から得か」までです。
そのサイズ帯に何件・何 GiB あるかは、数えないと分かりません。
ここで Inventory が役に立ちます。

経過日数とサイズで、ルールを入れた日に各ファイルがどうなるかを分けます。

07_lifecycle_fate.sql
WITH aged AS (
  SELECT
    date_diff('day', last_modified_date, CAST(now() AS timestamp)) AS age_days,
    size
  FROM inventory
  WHERE dt = (SELECT max(dt) FROM inventory)
    AND storage_class = 'STANDARD'
)
SELECT
  CASE
    WHEN age_days < 180 THEN '1: <180d keep'
    -- 128KiB 未満は移らない(ちょうど 128KiB は移る)
    WHEN age_days < 366 AND size < 131072 THEN '2: 180-365d <128KiB keep'
    WHEN age_days < 366 THEN '3: 180-365d to DA'
    -- 削除はサイズに関係なく行われる
    ELSE '4: >=366d delete'
  END AS fate,
  count(*) AS objects,
  sum(size) AS bytes
FROM aged
GROUP BY 1
ORDER BY 1;

ルールを入れた日にまとめて移るのは、3 のファイルです。
その日の移行費用は 3 の件数で、下がる保管料金は 3 の容量で見積もれます。
ただし、Deep Archive に置く期間が 186日より短くなるので、後半の注意点で触れる早期削除の料金がかかる場合があります。

ルールを入れた日に区分 3 のファイルがまとめて移り、180日に届かずに消える様子

これから 180日を迎えるファイル(1)も、128KiB 以上なら 180日目から順に移り、そのたびに移行費用がかかります。
1 を 128KiB と約 490KiB で区切って数えれば、移すと損になる件数(128KiB〜490KiB)の見当がつきます。

検証環境のファイルは、どれも作成から 180日たっていません。そのため、この SQL は 1 の1行しか返さず、2〜4 の分け方は試せていません。

実際のバケットに適用するときの注意点

検証環境では起きないものの、実際のバケットでは確認が要る点です。

項目 注意点
バージョニング 有効なら旧バージョンも課金対象。Inventory は全バージョンを含め、ライフサイクルにも旧バージョン用のルールが要る
既存のライフサイクル 2024年9月より前に作った設定は、小さいファイルも移す以前の動きのまま。ルールを編集すると新しい既定(128KiB 未満は移さない)に変わる
出力先の暗号化 SSE-KMS の AWS 管理キー(aws/s3)は、Inventory の出力に使えない
ルールを入れた初日 すでに 180日を過ぎたファイルは、すぐに移る。たとえば 300日目のファイルは、66日後に消えるので早期削除の料金が出る
Inventory の片付け 毎日動き続ける。数え終わったら設定を消す

おわりに

ライフサイクルの既定は、128KiB(131,072 バイト)未満を外すだけです。
ちょうど 128KiB は移るので、移る件数は size >= 131072 で数えます。
移して元が取れるかは、Deep Archive に置く日数とファイルのサイズで決まります。
ルールの日数を決めるときは、分かれ目のサイズも一緒に出しておくと安心です。

Inventory と Athena で、出力の構造や Parquet の読み方を手で確かめられたのは、良い学習になりました。
移行前の費用の見積もりにも、移行後の確認にも使えます。

今回は、検証環境の小さなデータでの確認です。
また、作成から 180日を過ぎたファイルが無いので、経過日数で分ける SQL も試せていません。
次に機会があれば、サイズの分布を実際に数えて、ObjectSizeGreaterThan の値を決めるところまでやってみたいです。

参考

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?