Lambda 不要なAMIとスナップショットをまとめて削除する

起動テンプレート自動更新起動設定自動更新の記事を投稿しましたが、こういう自動化をしていると、気づけばAMIが山のように溜まってしまいます。

AMIに紐付いたスナップショットは、(大抵の場合少額とはいえ)ストレージ料金が発生します。

AWSコンソールのAMIのページから、不要なAMIを登録解除することがあると思いますが、紐付いたスナップショットまでは自動で消えてくれません。

紐付いたスナップショットをスナップショットの画面から検索して削除するのが地味に面倒だったので、一括削除できるようなLambda関数を作ってしまいます。

Lambda関数

処理の流れはシンプルで、「AMI削除→紐付いたスナップショットを検索して削除」という処理を、与えられたAMIのリストを回して繰り返し行うだけです。

import boto3
import logging
from botocore.exceptions import ClientError


ec2 = boto3.client('ec2')


def unregister_ami(delete_ami_id):
    # Unregister AMI.
    try:
        response = ec2.deregister_image(ImageId=str(delete_ami_id))
    except ClientError as e:
        if e.response['Error']['Code'] == 'InvalidAMIID.Unavailable':
            print('InvalidAMIID.Unavailable: ' + delete_ami_id)
            return True
        elif e.response['Error']['Code'] == 'InvalidAMIID.NotFound':
            print('InvalidAMIID.NotFound: ' + delete_ami_id)
            return True
        else:
            print(e.response['Error']['Code'])
            print(e.response['Error']['Message'])
            raise e
    print('Unregister AMI: ' + delete_ami_id)
    return response.get('ResponseMetadata', {}).get('HTTPStatusCode', -1) == 200


def delete_related_snapshots(delete_ami_id):
    # Get target snapshots.
    try:
        response = ec2.describe_snapshots(
            Filters=[
                {
                    'Name': 'description',
                    'Values': ['Created by CreateImage(*) for ' + delete_ami_id + '*',]
                }
            ]
        )
    except ClientError as e:
        print(e.response['Error']['Code'])
        print(e.response['Error']['Message'])
        raise e

    # Delete target snapshots.
    for snapshot in response['Snapshots']:
        try:
            response = ec2.delete_snapshot(SnapshotId=snapshot['SnapshotId'])
            if response.get('ResponseMetadata', {}).get('HTTPStatusCode', -1) != 200:
                raise('# Cannot delete snapshot: %s', snapshot)
            print('Delete Snapshot: ' + snapshot['SnapshotId'])
        except ClientError as e:
            print(e.response['Error']['Code'])
            print(e.response['Error']['Message'])
            logging.error("# Delete snapshot error: %s", e)
        
    return


def lambda_handler(event, context):
    delete_amis = event['delete_amis']
    print(delete_amis)

    for delete_ami in delete_amis:
        # Delete AMI.
        unregister_ami(delete_ami)
    
        # Delete Snapshots.
        delete_related_snapshots(delete_ami)
    
    return

Lambda関数には、以下の権限を付与します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "ec2:DeregisterImage",
                "ec2:DeleteSnapshot",
                "ec2:DescribeSnapshots"
            ],
            "Resource": "*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": (略)
        },
        {
            "Sid": "VisualEditor2",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": (略)
        }
    ]
}

使い方

  1. AWSコンソールのAMIの画面で、不要なAMIをチェックしていきます。
  2. 画面下部にチェックしたAMIのidがずらずらと表示されるので、これをコピーします。
  3. 適当なエディタで整形し、Lambda関数のテストイベントに下記のようなデータを投入して実行すれば、AMIとスナップショットが削除されます。
{
    "delete_amis": [
        "ami-0123456789abcdef1",
        "ami-0123456789abcdef2",
        "ami-0123456789abcdef3"
    ]
}

余談

スナップショットの削除部分のみをLambda関数で作っておき、AMIの削除操作を行った際に、自動で関連スナップショットを削除するようなことも可能(CloudTrail利用)で、そのようにした方が効率的で良いかと思います。

一方であまり削除処理系を自動化しすぎると、大事なデータの消失などが発生する懸念もあるため、「起動テンプレートの更新処理に連動して古い起動テンプレートや古いAMIを削除する」ような処理の実装は控えています。

参考

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です