Amazon EBSスナップショットの課金対象ブロックサイズの求め方 ~ EBS direct APIへの誘い ~
AWSのブロック・ストレージ・サービスのAmazon Elastic Block Store(Amazon EBS)のスナップショット機能では、初回にフルスナップショットを取得し、以降はインクレメンタルにスナップショットを取得します。
費用面でも、ベースラインとなるフルスナップショットとスナップショット間の増ブロックデータ分が課金されます。
本記事では、EBS Direct APIを用いてEBSスナップショットの課金対象となるブロックサイズを調査する方法を解説します。
EBS スナップショットとは?
EBSのスナップショットの仕組みは公式ドキュメントで図解入りで解説されています。
※ 引用元 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スナップショットのブロックインデックスを取得できます。
- https://docs.aws.amazon.com/ebs/latest/APIReference/API_ListSnapshotBlocks.html
- https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ebs/list-snapshot-blocks.html
※ 適宜ページングしてください
$ 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
df
で used
となった 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% /
新規に取得したスナップショット全体のブロック数を元にサイズを調べると、df
で used
となった 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スナップショット間の増分ブロックインデックスを取得できます。
- https://docs.aws.amazon.com/ebs/latest/APIReference/API_ListChangedBlocks.html
- https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ebs/list-changed-blocks.html
※ 適宜ページングしてください
$ 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
ポリシーで許可されていない
ではでは。
参考
- https://repost.aws/knowledge-center/ebs-calculate-snapshot-size
- https://repost.aws/knowledge-center/ebs-snapshot-billing
- https://docs.aws.amazon.com/ebs/latest/APIReference/API_GetSnapshotBlock.html
- https://docs.aws.amazon.com/ebs/latest/APIReference/API_ListChangedBlocks.html
付録:スナップショットサイズ調査用スクリプト
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"])}') # スナップショット間の差分ブロック数