Lambda 関数を利用して EBS スナップショットの差分サイズを計測し、その値を CloudWatch に出力してみた
はじめに
テクニカルサポートの 片方 です。
EBS スナップショットは増分スナップショットのため、最後のスナップショットが作成された後に変更されたデータブロックと新規のデータブロックのみが、次のスナップショットの取得対象となります。
そのため、Lambda 関数を活用して、スナップショット間の差分サイズを自動的に計測し、CloudWatch メトリクスとして出力してみました。
ボリュームから作成する最初のスナップショットは、常に完全なスナップショットです。これには、スナップショットの作成時にボリュームに書き込まれたすべてのデータブロックが含まれます。同じボリュームの後続スナップショットは、増分スナップショットです。それらには、最後のスナップショットが作成されてからボリュームに書き込まれた、変更されたデータブロックと新規のデータブロックのみが含まれます。
取得方法
スナップショットから取得された差分バックアップサイズを確認する方法として、
EBS direct API の ListChangedBlocks でスナップショット間で差分のあるブロックインデックスの数を求め、そちらに 512 KiB を乗算することで差分のバックアップサイズを求めることが可能です。
なお、作成した Lambda 関数は、EBS ボリュームの最新 2 つのスナップショット間の差分サイズを計算し、CloudWatch メトリクスとして記録します。
ブロックインデックスは、512 KiB ブロック単位の論理インデックスです。
やってみた
以下の順番で実装します。
- 実行ロール作成
- Lambda 関数作成
実行ロール
信頼関係
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
アタッチするポリシー例
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:DescribeSnapshots",
"ec2:DescribeVolumes"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ebs:ListChangedBlocks",
"ebs:ListSnapshotBlocks"
],
"Resource": "arn:aws:ec2:*::snapshot/*"
},
{
"Effect": "Allow",
"Action": [
"cloudwatch:PutMetricData"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
}
]
}
※ 適宜修正してください。
Lambda 関数
Python 3.13 で作成しました。
実行ロールでは、既存のロールを使用するを選択し、先ほど作成したロールを指定します。
※ 適宜修正してください。
Lambda 関数例
import boto3
import os
import json
from datetime import datetime, timezone
def lambda_handler(event, context):
"""
メイン関数: EBSボリュームの最新スナップショット間の差分サイズを計算し、CloudWatchに送信する
Parameters:
event (dict): Lambda呼び出しイベント。volumeIdまたはvolumeIdsパラメータを含むことができる
context (object): Lambda実行コンテキスト
Returns:
dict: 処理結果を含むレスポンス
"""
# 環境変数からボリュームIDのリストを取得
volume_ids_str = os.environ.get('VOLUME_IDS', '')
volume_ids = [vid.strip() for vid in volume_ids_str.split(',') if vid.strip()]
if not volume_ids:
# イベントからボリュームIDを取得する試み
if 'volumeId' in event:
volume_ids = [event['volumeId']]
elif 'volumeIds' in event:
volume_ids = event['volumeIds']
else:
raise ValueError("ボリュームIDが指定されていません")
results = []
for volume_id in volume_ids:
try:
# このボリュームの最新スナップショットペアを分析
result = analyze_latest_snapshot_pair(volume_id)
if result:
results.append(result)
except Exception as e:
print(f"ボリューム {volume_id} の処理中にエラー: {str(e)}")
return {
'statusCode': 200,
'body': json.dumps(results, default=str)
}
def analyze_latest_snapshot_pair(volume_id):
"""
指定されたボリュームの最新2つのスナップショット間の差分を分析する
Parameters:
volume_id (str): 分析対象のEBSボリュームID
Returns:
dict: 分析結果を含む辞書、またはスナップショットが不足している場合はNone
"""
ec2_client = boto3.client('ec2')
ebs_client = boto3.client('ebs')
cloudwatch = boto3.client('cloudwatch')
# このボリュームの全スナップショットを取得
response = ec2_client.describe_snapshots(
Filters=[
{'Name': 'volume-id', 'Values': [volume_id]},
{'Name': 'status', 'Values': ['completed']}
]
)
snapshots = response['Snapshots']
if len(snapshots) < 2:
print(f"ボリューム {volume_id} には比較可能なスナップショットが不足しています")
return None
# 作成日時でソート
snapshots.sort(key=lambda x: x['StartTime'])
# 最新の2つを取得
older_snapshot = snapshots[-2]
newer_snapshot = snapshots[-1]
# 差分ブロックをカウント
total_blocks = count_changed_blocks(
ebs_client,
older_snapshot['SnapshotId'],
newer_snapshot['SnapshotId']
)
# 差分サイズを計算(1ブロック = 512 KiB)
diff_size_kb = total_blocks * 512
diff_size_mb = diff_size_kb / 1024
diff_size_gb = diff_size_mb / 1024
# 時間差を計算(時間単位)
time_diff = (newer_snapshot['StartTime'] - older_snapshot['StartTime']).total_seconds() / 3600
# 結果を作成
result = {
'volume_id': volume_id,
'first_snapshot_id': older_snapshot['SnapshotId'],
'second_snapshot_id': newer_snapshot['SnapshotId'],
'first_date': older_snapshot['StartTime'],
'second_date': newer_snapshot['StartTime'],
'time_difference_hours': time_diff,
'changed_blocks': total_blocks,
'diff_size_kb': diff_size_kb,
'diff_size_mb': diff_size_mb,
'diff_size_gb': diff_size_gb
}
# CloudWatchにメトリクスを送信
timestamp = datetime.now(timezone.utc)
cloudwatch.put_metric_data(
Namespace='EBS/SnapshotDiff',
MetricData=[
{
'MetricName': 'DiffSizeGiB',
'Value': diff_size_gb,
'Unit': 'Gigabytes',
'Dimensions': [
{'Name': 'VolumeId', 'Value': volume_id}
],
'Timestamp': timestamp
}
]
)
print(f"ボリューム {volume_id} の分析結果: 差分サイズ {diff_size_gb:.2f} GiB")
return result
def count_changed_blocks(ebs_client, first_snapshot_id, second_snapshot_id):
"""
2つのスナップショット間で変更されたブロック数をカウントする
Parameters:
ebs_client: boto3 EBSクライアント
first_snapshot_id (str): 比較元のスナップショットID(古い方)
second_snapshot_id (str): 比較先のスナップショットID(新しい方)
Returns:
int: 変更されたブロックの総数
"""
total_blocks = 0
next_token = None
# ページネーション処理を行いながら全ての変更ブロックを取得
while True:
params = {
'FirstSnapshotId': first_snapshot_id,
'SecondSnapshotId': second_snapshot_id
}
if next_token:
params['NextToken'] = next_token
try:
# EBS Direct APIを使用して変更されたブロックを取得
response = ebs_client.list_changed_blocks(**params)
# 現在のページの変更ブロック数をカウント
if 'ChangedBlocks' in response:
total_blocks += len(response['ChangedBlocks'])
# 次のページがあるかチェック
if 'NextToken' in response:
next_token = response['NextToken']
else:
break
except Exception as e:
print(f"ブロック取得中にエラー: {str(e)}")
break
return total_blocks
環境変数の設定
VOLUME_IDS: 監視対象の EBS ボリューム ID をカンマ区切りで指定します。
例: vol-0123456789abcdef0,vol-0abcdef1234567890
もちろん、単一のボリューム ID のみを指定することも可能です。
タイムアウト設定
デフォルトのタイムアウト値(3秒)では短すぎるため、延長します。
タイムアウトを「30秒」に設定しました。
これで実装は終了です。お疲れ様でした。
検証してみた
EC2 で Windows Server 2022 を起動させた後に初回スナップショットを取得。その後適当なファイルを設置して増分スナップショットを取得しました。
Lambda 関数をテストします。
CloudWatch にカスタムメトリクスとして出力されているか確認します。
- 名前空間: EBS/SnapshotDiff
- メトリクス名: DiffSizeGiB
- ディメンション: VolumeId
取得された差分が表示されています。成功です!
その後も引き続き試しましたが、成功しました!
補足
検証では、ゼロデータのみの 5GB ファイルを作成した場合と、実際のランダムデータを含む 1GB ファイルを作成した場合の 2 つのシナリオをテストしました。
結果、5GB のゼロデータファイルを作成した場合の差分は約 0.13GB しか検出されませんでしたが、1GB のランダムデータファイルでは約 1.21 GB の差分が検出されました。ドキュメントに記載はないものの EBS スナップショットがゼロデータブロックを効率的に最適化しているのではないかと推察しました。
※ gp3 のボリュームタイプで検証
まとめ
本ブログが誰かの参考になれば幸いです。
参考資料
- Amazon EBS スナップショットの仕組み - Amazon EBS
- EBS direct API を使用して EBS スナップショットの内容にアクセスする - Amazon EBS
- EBS direct API の概念 - Amazon EBS
アノテーション株式会社について
アノテーション株式会社は、クラスメソッド社のグループ企業として「オペレーション・エクセレンス」を担える企業を目指してチャレンジを続けています。「らしく働く、らしく生きる」のスローガンを掲げ、様々な背景をもつ多様なメンバーが自由度の高い働き方を通してお客様へサービスを提供し続けてきました。現在当社では一緒に会社を盛り上げていただけるメンバーを募集中です。少しでもご興味あれば、アノテーション株式会社WEBサイトをご覧ください。