Amazon EBSスナップショットの課金対象ブロックサイズの求め方 ~ EBS direct APIへの誘い ~

Amazon EBSスナップショットの課金対象ブロックサイズの求め方 ~ EBS direct APIへの誘い ~

EBS Direct APIを用いてEBSフルスナップショットやスナップショット間のサイズを求める方法を紹介
Clock Icon2024.11.27

AWSのブロック・ストレージ・サービスのAmazon Elastic Block Store(Amazon EBS)のスナップショット機能では、初回にフルスナップショットを取得し、以降はインクレメンタルにスナップショットを取得します。
費用面でも、ベースラインとなるフルスナップショットとスナップショット間の増ブロックデータ分が課金されます。

本記事では、EBS Direct APIを用いてEBSスナップショットの課金対象となるブロックサイズを調査する方法を解説します。

EBS スナップショットとは?

EBSのスナップショットの仕組みは公式ドキュメントで図解入りで解説されています。

snapshot_1a

※ 引用元 https://docs.aws.amazon.com/ebs/latest/userguide/how_snapshots_work.html

「状態1(State 1)」 のように、初回のフルスナップショットでは、EBSボリューム単位ではなく、利用しているデータ全体がバックアップされます。

この時点で、フルスナップショット(Snap A)の10GiB分が課金対象であり、EBSボリュームの未使用分の5GiBは課金されません。

「状態2(State 2)」 のように、利用しているブロックで変更された後でスナップショットを取ると、変更のなかったブロックについてはフルスナップショットで取得したものを参照し、変更のあったブロックを追加でバックアップします。

この時点で、フルスナップショット(Snap A)取得時から変更のなかった6GiB分と2回目のスナップショット(Snap B)で変更の発生した4GiB分が課金対象です。

「状態3(State 3)」 のように、新規ブロックにデータが書き込まれた後でスナップショットを取ると、この新規に書き込まれたブロックを追加でバックアップします。

この時点で、フルスナップショット(Snap A)取得時から変更のなかった6GiB分と2回目のスナップショット(Snap B)で変更の発生した4GiB分と3回目のスナップショット(Snap C)で新規に書き込んだ2GiB分が課金対象です。

EBSスナップショットのサイズを調べる

検証環境

Amazon Linux 2023に8GiBのEBSボリュームをアタッチして、EBSスナップショットのサイズを調べてみましょう。

$ df -hT /dev/xvda1
Filesystem     Type  Size  Used Avail Use% Mounted on
/dev/xvda1     xfs   8.0G  1.5G  6.5G  19% /

フルスナップショットのサイズを調べる

EBS::ListSnapshotBlocks APIを利用すると、EBSスナップショットのブロックインデックスを取得できます。

※ 適宜ページングしてください

$ aws ebs list-snapshot-blocks --snapshot-id snap-A
{
    "Blocks": [
        {
            "BlockIndex": 0,
            "BlockToken": "AD0BAfBEZSsew0wRDdpmKDiwry2RWm+CeCIfVhMJJT6QY9gY3IgDA86a3lVC"
        },
        {
            "BlockIndex": 2,
            "BlockToken": "AD0BAdKBti8dXi3+FuzJ8cO9k7Z5xA8BLXjTpQDQqgDT4WURzRS56dUiNYX3"
        },
...
        {
            "BlockIndex": 10085,
            "BlockToken": "AD0BATXgCl/xiZs0yBmQVSLvTW8rnAiS70oen4ksgxdJoB4YxlKgT5i1qcoZ"
        },
        {
            "BlockIndex": 10086,
            "BlockToken": "AD0BARoqWJIM0rOtc40OB8s54jiM4KnaGVHVsUyrQpJhJjypNZjUwqIpAifU"
        },
        {
            "BlockIndex": 16383,
            "BlockToken": "AD0BASzCucqESNAwSsXaLAe1iqlNRrqsQ9IgxqaA85RHtY+57u5iK5tuT2CN"
        }
    ],
    "ExpiryTime": "2024-12-03T08:33:13.851000+00:00",
    "VolumeSize": 8,
    "BlockSize": 524288
}

このブロックインデックス数を数え上げ、ブロックサイズの 512 KiBをかけると、スナップショットのサイズを取得できます。

$ aws ebs list-snapshot-blocks --snapshot-id snap-A | grep BlockIndex | wc -l
3281

$ python -c 'print(3281 * 512 / 1024 / 1024)'
1.60205078125

dfused となった 1.5G に近い値を取得できました。

スナップショット間の差分サイズを調べる

1GiBのファイルを作って、スナップショットを取得しましょう。

$ dd if=/dev/zero of=test bs=1MiB count=1024
1024+0 records in
1024+0 records out
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 6.21241 s, 173 MB/s

$ df -hT /dev/xvda1
Filesystem     Type  Size  Used Avail Use% Mounted on
/dev/xvda1     xfs   8.0G  2.5G  5.5G  32% /

新規に取得したスナップショット全体のブロック数を元にサイズを調べると、dfused となった 2.5G に近い値を取得できました。

$ aws ebs list-snapshot-blocks --snapshot-id snap-B | grep BlockIndex | wc -l
5200

$ python -c 'print(5200 * 512 / 1024 / 1024)'
2.5390625

EC2::ListChangedBlocks APIを利用すると、EBSスナップショット間の増分ブロックインデックスを取得できます。

※ 適宜ページングしてください

$ aws ebs list-changed-blocks --first-snapshot-id snap-A --second-snapshot-id snap-B
{
    "ChangedBlocks": [
        {
            "BlockIndex": 24,
            "FirstBlockToken": "AD0BAZZ0FK2scv0Jqja2pLEMuw9HiJWJj815r4t1pq4WhEYkoSpcVfy3/gk8",
            "SecondBlockToken": "AD0BAW4ngedn/O+nRGX3KCQHgP25GLgc49R7q4tnwM9N2Duzm0o//dtRIKa0"
        },
        {
            "BlockIndex": 26,
            "FirstBlockToken": "AD0BATK1bOd9xuHbVTOD2mhKDmzxfBwJS+81dAwMrHm1ljHXMvrNqfY1I7QC",
            "SecondBlockToken": "AD0BAZUHp7F6iYfe8L0aRMus6CfeOaOShNiIu4KRL9Ro2fYtqtbDWXSVWCQZ"
        },
...
        {
            "BlockIndex": 11748,
            "SecondBlockToken": "AD0BAVZ6VigBppMMsJ8TKIMKE9/QGLAM5noZH2iFA2Bq40WgLadLGWnAj3Ut"
        },
        {
            "BlockIndex": 11749,
            "SecondBlockToken": "AD0BAdspeIPr6zsWabxVt4RNb3eCMdComTSkOc+3MlIjzpxGPgLZhPJkU/jo"
        },
        {
            "BlockIndex": 11750,
            "SecondBlockToken": "AD0BAWUrnd24xG5OR5P0aSrofIhf1dIqzLtR8YK3qC5dOuWYeZHTFAIqzjiO"
        }
    ],
    "ExpiryTime": "2024-12-03T16:29:21.961000+00:00",
    "VolumeSize": 8,
    "BlockSize": 524288
}

このブロックインデックス数を数え上げ、ブロックサイズの 512 KiBをかけると、スナップショットの増分サイズを取得できます。

$ aws ebs list-changed-blocks --first-snapshot-id snap-A --second-snapshot-id snap-B | grep BlockIndex | wc -l
2089

$ python -c 'print(2089 * 512 / 1024 / 1024)'
1.02001953125

dd で新規作成した 1GiBのファイルと合致します。

経年とともにフルスナップショットサイズは単調増加し、EBSボリュームサイズに収束

先の公式ドキュメントで記載されているように、EBSスナップショットは使用したブロックに対して課金されます。

EC2(EBSボリューム)利用開始直後であれば、 $ df 結果とフルスナップショットサイズは同等ですが、EBSボリュームを使うにつれ、EBSボリュームの未使用ブロックは減っていき、EBSボリュームサイズに収束します。

EBSを世代管理してバックアップしている場合、一番古いEBSスナップショットがベースラインとなるフルスナップショットです。

EBSボリュームの全ブロックが利用されている場合、フルスナップショットのサイズはEBSボリュームサイズに等しくなります。

$ df で確認できる実データサイズに関係なく、EBSボリュームの使用ブロックに対して課金されることにご留意ください。

必要以上にEBSボリュームサイズを大きく確保していると、スナップショットのコスト増にも繋がることを意味します。

EBS direct APIはReadOnlyAccessポリシーで許可されていない

今回紹介した参照系 EBS direct API はAWSマネージドの ReadOnlyAccess ポリシーで許可されていません。

そのため、EBSスナップショットサイズを確認したい場合、ReadOnlyAccess とは別に

  • EBS::ListChangedBlocks
  • EBS::ListSnapshotBlocks

等を許可する必要があります。

EBSスナップショットのコスト感を把握するにはCost Allocation Tagを利用しよう

EBSスナップショットはCost Allocation Tagに対応しています。

まずはタグを使い、EBSスナップショット全体、あるいは、EBSボリューム単位のざっくりとしたコストを把握しましょう。

まとめ

AWSアカウントに占めるEBSスナップショットの利用費が直感以上に高かったことから、EBS Direct APIでフルスナップショットサイズとスナップショット間のサイズを具体的に計算する機会がありました。

ポイントは以下です

  • EBSはブロックストレージ
  • EBSスナップショットはボリュームの使用しているブロックに対してバックアップが行われ、課金が発生する
  • EBS Direct APIでスナップショット単体、スナップショット間の差分のサイズを求められる
  • EBSボリュームのサイズを大きく確保すると、思わぬ課金につながる
  • 参照系 EBS Direct API は AWSマネージドの ReadOnlyAccess ポリシーで許可されていない

ではでは。

参考

付録:スナップショットサイズ調査用スクリプト

EBSボリュームを取得し、ボリュームごとにフルスナップショット、及び、スナップショット間の差分のブロック数を取得する簡易スクリプトを紹介します

import boto3

ec2 = boto3.client('ec2')
ebs = boto3.client('ebs')

# EBSボリューム一覧
volumes = ec2.describe_volumes()['Volumes']

for volume in volumes:
    volume_id = volume['VolumeId']

    # EBSスナップショット一覧
    snapshots = ec2.describe_snapshots(
        Filters=[
            {
                'Name': 'volume-id',
                'Values': [volume_id]
            },
        ],
        OwnerIds=['self']
    )['Snapshots']

    # スナップショットを古い順にソート
    sorted_snapshots = sorted(snapshots, key=lambda x: x['StartTime'], reverse=False)

    if sorted_snapshots:
        full_snapshot = sorted_snapshots[0]['SnapshotId']
        res = ebs.list_snapshot_blocks(SnapshotId= full_snapshot)
        num_blocks = 0
        while True:
            num_blocks += len(res["Blocks"])
            if 'NextToken' in res:
                res = ebs.list_snapshot_blocks(SnapshotId= full_snapshot, NextToken=res['NextToken'])
            else:
                break
        print(f'{volume_id},,{full_snapshot},{num_blocks}') # 初回フルスナップショットのブロック数

        for i in range(len(sorted_snapshots) -1):
            first, second = sorted_snapshots[i]['SnapshotId'], sorted_snapshots[i+1]['SnapshotId']
            res = ebs.list_changed_blocks(FirstSnapshotId=first, SecondSnapshotId=second)
            print(f'{volume_id},{first},{second},{len(res["ChangedBlocks"])}') # スナップショット間の差分ブロック数

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.