[EC2] 不要なAMIを一括で削除したい

アイキャッチ AWS EC2

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こむろ@今だけ東京です。

はじめに

担当プロジェクトでは、Golden AMI方式でデプロイプロセスを構成しているのですが、検証環境では比較的頻繁にデプロイを実行します。そしてふと気づきました。はて?AMIが増えると問題ないのだろうか?

AMI の登録解除を確認すると、EBSのSnapshotが残っているため、こちらが課金対象です。検証環境を調べてみるとその数なんと300個超え。これはよろしくありません。塵も積もれば山となる、不要なものは整理していきましょう。そこで、手始めにすでに利用しないであろうAMIを削除していきます。

AMIの削除は一発で終わらない

先程のドキュメントを確認してみると、どうやらAMIの削除は少々面倒そう。今回はEBS BackedのAMIなので、以下の手順が必要です。

  1. AMIの登録解除
  2. 登録解除したAMIに紐付いたSnapshotの削除

さすがに200個超えをマネジメントコンソールからポチポチやるのはとても面倒です。さらにマネジメントコンソールでやると、表示がなかなか更新されずに削除確認するのにも非常に時間がかかります。この作業をAPIを使ってスクリプトで自動化します。

準備

まずは今回以下の条件としています。

  • AMIにはカスタムタグが指定されていること。 Protected というBooleanの値を入力するタグが存在するものとします。
  • 全てのAMIは、同一のOwnerIDで作成されているものとします。

スクリーンショット 2017-06-14 15.22.32

削除したくないAMIがある場合は、ここのタグの値を true にしておくか、タグそのものを削除すると削除対象外となる想定です。

AMIのリストを抽出する

今回は全て自動化せずに一旦リストファイルを作成します。

$ aws ec2 describe-images --profile <your profile> --owners "<your ownerid>" --filters "Name=tag:Protected,Values=false" \
     | jq -r '.Images[] \
     | {imageId: .ImageId, snapshotId: .BlockDeviceMappings[].Ebs.SnapshotId, creationDate: .CreationDate} \
     | [.imageId, .snapshotId, .creationDate] \
     | @csv' \
     | sort -t, -k3 > delete_images.csv

全パラメータの説明。

  • aws ec2 describe-images: AMIの情報はEC2のAPIを利用します。
  • --profile: AWSのプロファイルを指定します。
  • --owners: AMI作成者のOwnerIdを指定します。
  • --filters: タグの合致条件を記述します。今回は Protected=false のAMIを抽出したいので上記のように記載します。こちらを参照。
  • jq -r: jqへパイプ。 -r でダブルクオーテーションを削除。
  • .Images[]: DescribeImagesのレスポンスは、Imagesという名前の配列で返却されるので配列を分解
  • {imageId: .ImageId, snapshotId: .BlockDeviceMappings[].Ebs.SnapshotId, creationDate: .CreationDate}: レスポンスのJsonをパースして必要な情報のみを再度Jsonに整形し直します。
  • [.imageId, .snapshotId, .creationDate]: CSVに出力するには配列として出力する必要があります。前段で新たなJsonとして整形してあるので、そちらのKey名を利用します。
  • @csv': CSV形式で出力します。
  • sort -t, -k3: デリミタをカンマで指定し、3つ目の項目。つまり creationDate でソートします。

これで削除対象のAMIリストは作成できました。タグの合致条件は必須です。AMIには削除回避のフラグなどがありません。そのため、重要なAMIが消されないように最新の注意を払う必要があります。そのため、削除可能なAMIには明示的にタグを入れるようにしました。 *1 今回は Protected というタグの値が false になっているもののみを抽出します。タグがないもの、false 以外が入力されているものは抽出しません。

"ami-68f01b09","snap-6a790b50","2016-01-27T07:35:49.000Z"
"ami-cff51eae,,"snap-27272f1b","2016-01-27T09:20:34.000Z"
"ami-6bbc8405",,"2016-01-29T03:16:54.000Z"
...

一部Snapshotが登録されていないものも有りますね。

リストに記載してあるAMIを削除する

前段で作成したCSVを食わせてザクザク削除していくプログラムをPythonで作成しました。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import csv
import click
import time
import botocore
from boto3.session import Session

profile = "<your aws profile>"
session = Session(profile_name=profile)
ec2_client = session.client('ec2')

# AMI登録解除
def unregister_ami(ami_id, is_dryrun=True):
    """Unregister EC2 AMI"""
    response = ec2_client.deregister_image(
        DryRun=bool(is_dryrun),
        ImageId=str(ami_id)
    )
    return response.get('ResponseMetadata', {}).get('HTTPStatusCode', -1) == 200

# Snapshot削除
def delete_snapshot(snapshot_id, is_dryrun=True):
    """Delete snapshot"""
    response = ec2_client.delete_snapshot(
        DryRun=bool(is_dryrun),
        SnapshotId=str(snapshot_id)
    )
    return response.get('ResponseMetadata', {}).get('HTTPStatusCode', -1) == 200

# msのsleep
msleep = lambda x: time.sleep(x/1000.0)

def process_from_csv(file_name, is_dryrun):
    """Execute from CSV file"""
    with open(str(file_name), 'r') as file:
        reader = csv.reader(file)
        for row in reader:
            try:
                unregistered = unregister_ami(row[0], bool(is_dryrun)) # unregister ami
                deleted = delete_snapshot(row[1], bool(is_dryrun)) # delete snapshot
                print("{ami_id}, {unregistered}, {deleted}".format(ami_id=row[0],unregistered=unregistered,deleted=deleted))
                msleep(500) # sleep in milisec
            except botocore.exceptions.ClientError as e:
                print("{}, Failed, {}".format(row[0], e))
                msleep(500) # sleep in milisec

@click.command()
@click.argument('file')
@click.option('--dry-run', '-D', type=bool, help='Dry Run', default=True)
def execute_unregister(file, dry_run):
    """Option parser"""
    file_name = str(file)
    process_from_csv(file_name, bool(dry_run))

def main():
    """main function"""
    execute_unregister()

if __name__ == '__main__':
    main()

基本的に自分は意図せず実行してしまうのがコワイので、DryRunオプションをデフォルトでTrueに設定しました。

成功したかどうかは全てHTTPステータスコードの判定のみです。HTTPステータスコードが200かそうでないかで判定しています。とりあえず成功したかどうかを確認したかったので。

実行結果

$ python delete_ami.py deleted_ami.csv --dry-run=False
Unregister AMI: ami-68f01b09, DryRun: False
ami-68f01b09, True, True
Unregister AMI: ami-68f01b09, DryRun: False
ami-68f01b09, Failed, An error occurred (InvalidAMIID.Unavailable) when calling the DeregisterImage operation: The image ID 'ami-68f01b09' is no longer available
Unregister AMI: ami-cff51eae, DryRun: False
ami-cff51eae, True, True
...

正常に削除できました。

マネジメントコンソールではちゃんと消えたかどうかを確認しながら、削除していくのは少々億劫です。ちょっとだけ楽できました。良かったですね。

余談

書いてる途中で気づいたんですが、スナップショットの削除を自動的にやってくれるやつがありましたね。車輪の再開発をしてしまった。

参照

脚注

  1. ぼくはこの条件を考慮しなかったせいで大事なベースAMI削除をやらかしました。