0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AMIの登録解除と連動してスナップショットを削除する2024夏(~北の国から~)

Posted at

北の国ではないですが(関東民)。
不要なイメージを削除したいがために、AMI登録解除をしても、スナップショット(EBS)は消えないという事を最近思い出した。思い出したそのころには大量のAMIがあり、もはやこんなの全部やってれんない、、、という状態に。

以下のリンクを見るも、どうも2024年はこの形だとなかなかうまくいかず。

image.png
↑こんな顔になりました。

というわけでClaudeさんに聞いてコード作成&テストを実施。
2024年は以下でAMI登録解除を行えば、自動で連携するスナップショットも削除できるようになる。

Lambda作成

Python3.12で動作確認済み。

import boto3
import logging
from botocore.exceptions import ClientError
import time
import json

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def wait_for_snapshot_deletion(ec2_client, snapshot_id, max_attempts=20, wait_time=15):
    # 最初の待機を追加
    time.sleep(wait_time)
    for _ in range(max_attempts):
        try:
            ec2_client.describe_snapshots(SnapshotIds=[snapshot_id])
            logger.info(f"Snapshot {snapshot_id} still exists. Waiting...")
            time.sleep(wait_time)
        except ClientError as e:
            if e.response['Error']['Code'] in ['InvalidSnapshot.NotFound', 'InvalidSnapshotID.NotFound']:
                logger.info(f"Snapshot {snapshot_id} has been deleted.")
                return True
            else:
                logger.error(f"Error checking snapshot {snapshot_id}: {str(e)}")
                return False
    logger.warning(f"Timed out waiting for snapshot {snapshot_id} to be deleted.")
    return False

def delete_snapshot(ec2_client, snapshot_id):
    try:
        ec2_client.delete_snapshot(SnapshotId=snapshot_id)
        logger.info(f"Deletion initiated for snapshot: {snapshot_id}")
        if wait_for_snapshot_deletion(ec2_client, snapshot_id):
            return True
        else:
            # スナップショットが見つからない場合も成功とみなす
            try:
                ec2_client.describe_snapshots(SnapshotIds=[snapshot_id])
                logger.warning(f"Snapshot {snapshot_id} still exists after deletion attempt.")
                return False
            except ClientError as e:
                if e.response['Error']['Code'] in ['InvalidSnapshot.NotFound', 'InvalidSnapshotID.NotFound']:
                    logger.info(f"Snapshot {snapshot_id} not found after deletion attempt. Considering it as deleted.")
                    return True
                else:
                    logger.error(f"Unexpected error checking snapshot {snapshot_id} after deletion attempt: {str(e)}")
                    return False
    except ClientError as e:
        if e.response['Error']['Code'] == 'InvalidSnapshot.InUse':
            logger.warning(f"Snapshot {snapshot_id} is currently in use. Skipping deletion.")
            return False
        else:
            logger.error(f"Error deleting snapshot {snapshot_id}: {str(e)}")
            return False

def lambda_handler(event, context):
    try:
        logger.info(f"Received event: {json.dumps(event)}")
        
        image_id = event['detail']['ImageId']
        state = event['detail']['State']
        
        logger.info(f"Processing AMI: {image_id}, State: {state}")

        ec2_client = boto3.client('ec2')

        # スナップショットの検索を実行
        paginator = ec2_client.get_paginator('describe_snapshots')
        snapshot_iterator = paginator.paginate(
            Filters=[
                {
                    'Name': 'description',
                    'Values': [f'Created by CreateImage(*) for {image_id}']
                }
            ]
        )

        deleted_snapshots = []
        failed_snapshots = []

        for page in snapshot_iterator:
            for snapshot in page.get('Snapshots', []):
                snapshot_id = snapshot['SnapshotId']
                logger.info(f"Found snapshot: {snapshot_id} for AMI: {image_id}")
                if delete_snapshot(ec2_client, snapshot_id):
                    deleted_snapshots.append(snapshot_id)
                else:
                    failed_snapshots.append(snapshot_id)

        if not deleted_snapshots and not failed_snapshots:
            logger.info(f"No snapshots found for AMI: {image_id}")
            return {
                'statusCode': 200,
                'body': f'No snapshots found for AMI: {image_id}'
            }

        result_message = (f"Processed AMI: {image_id}. "
                          f"Deleted snapshots: {deleted_snapshots}. "
                          f"Failed to delete snapshots: {failed_snapshots}.")
        logger.info(result_message)

        return {
            'statusCode': 200,
            'body': result_message
        }

    except KeyError as ke:
        error_message = f"Key Error: Unable to find key in event structure: {str(ke)}"
        logger.error(error_message)
        logger.error(f"Event structure: {json.dumps(event)}")
        return {
            'statusCode': 400,
            'body': error_message
        }
    except Exception as e:
        error_message = f"Unexpected error processing AMI: {str(e)}"
        logger.error(error_message)
        logger.error(f"Event structure: {json.dumps(event)}")
        return {
            'statusCode': 500,
            'body': error_message
        }

「スナップショットが見つからない場合も成功とみなす」と書いてますが、消す処理が早すぎて、消去確認の処理がエラー判定してしまったため、スナップショットが見つからない場合は成功という処理となった。

ハマったポイントは、どうやら過去の先人の記事だと、スナップショットの説明欄に記載があるAMI-IDをキーにしてスナップショットIDを検索していたが、この数年でAMI作成時に自動でスナップショットの説明欄に記載される文言が変わった模様。ここでスナップショットが検索されず、AMIを登録解除してもスナップショットが消えずに何度も試行錯誤して時間が溶けた。

過去(他者サイトより引用)
        response = client.describe_snapshots(
            Filters=[
                {
                    'Name': 'description',
                    'Values': [
                        'Created by CreateImage(*) for ' + imageID + ' from *',
                    ]
                }
            ]
        )
現在
            Filters=[
                {
                    'Name': 'description',
                    'Values': [f'Created by CreateImage(*) for {image_id}']
                }
            ]

スナップショットの説明欄descriptionの「from」が今はない模様。ここはご自身の環境で変更なり、条件分岐を追加していただきたい。
また、EventBridgeに変わってから、送られてくるイベントの形式も変わっているので、そこも対応している。

LambdaはIAMロールにAWS管理ポリシーのEC2FullAccessを付与している。
タイムアウト値は念のため3分程度に指定。

Lambdaのログ設定はありったけ有効にしておく。トラブル時にログを見れるように。
image.png

EventBridgeルール作成

EventBridgeのルールを新規作成し、以下のイベントパターンを作成。

{
  "source": ["aws.ec2"],
  "detail-type": ["EC2 AMI State Change"],
  "detail": {
    "State": ["deregistered"]
  }
}

ターゲットは先に作ったLambdaを指定する。

AMI登録解除

解除予定のAMI-IDをコピーしておく。
image.png

コピーしたAMI-IDを、スナップショットで検索。
image.png

↑このスナップショットが消えることを確認する。

AMIに戻り、登録解除を実施。
image.png

LambdaのCloudwatchを確認すると、正常終了していることがわかり、スナップショットを削除したと記載があり。

image.png

スナップショット画面に戻り、AMIIDで検索した状態のまま更新ボタンをクリックすると、スナップショットが消えている。
image.png

簡単に思えたことが地味に時間かかった。。。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?