古いAMIを定期的に削除する – Step Functionsバージョン

古いAMIを定期的に削除する - Step Functionsバージョン

古いAMIを定期的に削除する – Step Functionsバージョン

AMIの定期的な取得および削除に関して、AWSではDLM(Data Lifecycle Manager)が提供されています。

https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/snapshot-lifecycle.html

以下のページで、DLM入門について取り上げています。

https://awstut.com/2023/03/19/introduction-to-data-lifecycle-manager-create-ebs-snapshot-ami-periodically

ただし以下に引用した通り、DLMを使用して全てのAMIを管理することはできません。

Amazon Data Lifecycle Manager は、他の方法で作成されたスナップショットまたは AMI の管理には使用できません。

Amazon Data Lifecycle Manager

今回はDLMを使用せず、古いAMIを定期的に削除する方法を考えます。
具体的には、Lambda関数とStep Functionsを使用します。

構築する環境

Diagram of periodically delete old AMIs - Step Functions version.

Step Functionsステートマシンを作成します。
ステートマシンは大別すると2つのステートで構成されます。

  • 1つ目のステート:古いAMIデータを抽出するLambda関数を実行するステート。
  • 2つ目のステート:前ステートで取得した各AMIデータを処理するMapステート。

Mapステートは以下の2つのサブステートから構成されます。

  • 1つ目のサブステート:Lambda関数を使用してAMIを削除する。
  • 2つ目のサブステート:Lambda関数を使用してAMIに紐づくスナップショットを削除する。

今回は検証ということで、作成から1時間を超えたAMIを削除の対象とします。

関数のランタイム環境はPython3.8とします。

EventBridgeルールを作成して、このステートマシンを定期的に実行します。
具体的には、1時間に1回ステートマシンを実行するように設定します。

CloudFormationテンプレートファイル

上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。

https://github.com/awstut-an-r/awstut-soa/tree/main/02/004

テンプレートファイルのポイント解説

Step Functionsステートマシン

Resources:
  StateMachine:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      Definition:
        Comment: !Sub "${Prefix}-StateMachine"
        StartAt: ListExpiredImagesState
        States:
          ListExpiredImagesState:
            Type: Task
            Resource: !Ref Function1Arn
            Parameters:
              valid_hours: !Ref ValidHours
            ResultPath: $.images
            Next: DeleteImageMapState
          DeleteImageMapState:
            Type: Map
            MaxConcurrency: 1
            InputPath: $.images
            ItemSelector:
              describe-image.$: $$.Map.Item.Value
            ItemProcessor:
              ProcessorConfig:
                Mode: INLINE
              StartAt: DeleteImageState
              States:
                DeleteImageState:
                  Type: Task
                  Resource: !Ref Function2Arn
                  InputPath: $.describe-image
                  ResultPath: $.delete-image
                  Next: DeleteSnapshotState
                DeleteSnapshotState:
                  Type: Task
                  Resource: !Ref Function3Arn
                  InputPath: $.describe-image
                  ResultPath: $.delete-snapshots
                  End: true
            End: true
      LoggingConfiguration:
        Destinations:
          - CloudWatchLogsLogGroup:
              LogGroupArn: !GetAtt LogGroup.Arn
        IncludeExecutionData: true
        Level: ALL
      RoleArn: !GetAtt StateMachineRole.Arn
      StateMachineName: !Ref Prefix
      StateMachineType: STANDARD
Code language: YAML (yaml)

Step Functionsステートマシンの基本的な事項については、以下のページをご確認ください。

https://awstut.com/2022/06/18/introduction-to-step-functions-with-cfn

1つ目のステート

Resourcesプロパティで実行するLambda関数を設定します。
本ステートでは、後述のLambda関数1を指定します。

Parametersプロパティで、Lambda関数1に渡すパラメータを設定できます。
ここでは以下の通り、AMIの有効期限に関するパラメータを設定します。

{
  "valid_hours": 1
}
Code language: JSON / JSON with Comments (json)

本パラメータに「1」を指定して、作成されてから1時間以上経過しているAMIを削除の対象とします。

ResultPathプロパティで、関数1から返されたデータの受け取り方を設定できます。
今回は受け取った値をimagesに格納します。
以下がその具体例です。

{
  "images": [
    {
      "CreationDate": "2023-02-18T23:59:14.000Z",
      "ImageId": "ami-0871af2accbb69d20",
      "BlockDeviceMappings": [
        {
          "DeviceName": "/dev/xvda",
          "Ebs": {
            "SnapshotId": "snap-0470d37f1fce13f94",
            ...
          }
        }
      ],
      ...
    },
    {
      "CreationDate": "2023-02-19T12:45:33.000Z",
      "ImageId": "ami-0a95e4ddd73a24fc9",
      "BlockDeviceMappings": [
        {
          "Ebs": {
            "SnapshotId": "snap-0819cb0ef6defdafa",
            ...
          }
        },
        {
          "Ebs": {
            "DeleteOnTermination": false,
            "SnapshotId": "snap-01449bfd40017a2bf",
            ...
          }
        }
      ],
      ...
    },
    ...
  ]
}
Code language: JSON / JSON with Comments (json)

Lambda関数1

関数
Resources:
  Function1:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import boto3
          import datetime
          import os

          ACCOUNT_ID = os.environ['ACCOUNT_ID']
          REGION_NAME = os.environ['REGION_NAME']

          client = boto3.client('ec2', region_name=REGION_NAME)

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

            now = datetime.datetime.now(datetime.timezone.utc)

            response = client.describe_images(
              Owners=[ACCOUNT_ID])
            expired_images = []
            for image in response['Images']:
              creation_date_str = image['CreationDate']
              creation_date_dt = datetime.datetime.fromisoformat(creation_date_str.replace('Z', '+00:00'))

              diff = now - creation_date_dt
              diff_hour = diff.seconds / (60 * 60)

              if diff_hour > valid_hours:
                expired_images.append(image)

            return expired_images
      Environment:
        Variables:
          ACCOUNT_ID: !Ref AWS::AccountId
          REGION_NAME: !Ref AWS::Region
      FunctionName: !Sub "${Prefix}-function-01"
      Handler: !Ref Handler
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole1.Arn
Code language: YAML (yaml)

Lambda関数で実行するコードをインライン形式で記載します。
詳細につきましては、以下のページをご確認ください。

https://awstut.com/2022/02/02/3-patterns-for-creating-lambda-with-cloudformation

コードの内容は以下の通りです。

  • eventオブジェクトのvalid_hoursにアクセスし、有効期限の値を取得する。
  • boto3でEC2用のクライアントオブジェクトを生成後、describe_imagesメソッドを実行して、AMIのリストを取得する。
  • 各AMIの作成日時を現在日時と比較して、有効期限を超えているものを抽出する。

AMIの生成日時は「CreationDate」の値を参照して取得できますが、「2023-02-19T12:45:33.000Z」のように、ISO 8601形式です。
今回は以下のページを参考にして、文字列を加工した上で、datetime型に変換して、現在日時と比較しました。

https://note.nkmk.me/python-datetime-isoformat-fromisoformat/

IAMロール
Resources:
  FunctionRole1:
    Type: AWS::IAM::Role
    DeletionPolicy: Delete
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service:
                - lambda.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: !Sub "${Prefix}-DescribeImagesPolicy"
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ec2:DescribeImages
                Resource: "*"
Code language: YAML (yaml)

インラインポリシーでAMI情報を取得する権限を与えます。

2つ目のステート

Mapステートです。
Mapステートの基本的な事項については、以下のページをご確認ください。

https://awstut.com/2023/03/12/iteration-using-map-in-step-functions

InputPathプロパティに「$.images」を指定することによって、前ステートで設定したデータを読み込みます。

ItemSelectorプロパティで、反復処理を行う上での、各処理に渡すデータ形式を設定できます。
「describe-image.$: $$.Map.Item.Value」とすることで、例えば以下のデータが生成されて渡されます。

{
  "describe-image": {
    "CreationDate": "2023-02-18T23:59:14.000Z",
    "ImageId": "ami-0871af2accbb69d20",
    "BlockDeviceMappings": [
      {
        "DeviceName": "/dev/xvda",
        "Ebs": {
          "SnapshotId": "snap-0470d37f1fce13f94"
        },
        ...
      }
    ],
    ...
  }
}
Code language: JSON / JSON with Comments (json)

1つ目のサブステート

Lambda関数2を実行するステートです。

InputPathプロパティに「$.describe-image」と指定することで、先述のItemSelectorプロパティで設定した値を受け取り、関数に渡すことができます。
以下が関数に渡すデータの具体例です。

{
  "CreationDate": "2023-02-18T23:59:14.000Z",
  "ImageId": "ami-0871af2accbb69d20",
  "BlockDeviceMappings": [
    {
      "DeviceName": "/dev/xvda",
      "Ebs": {
        "SnapshotId": "snap-0470d37f1fce13f94"
      },
      ...
    }
  ],
  ...
}
Code language: JSON / JSON with Comments (json)

ResultPathプロパティで、関数2から返されたデータの受け取り方を設定できます。
今回は受け取った値をdelete-imageに格納します。
以下がその具体例です。

{
  "describe-image": {
    ...
  },
  "delete-image": {
    "ResponseMetadata": {
      "RequestId": "951ccf72-e138-4026-b851-33bb4364c6f3",
      "HTTPStatusCode": 200,
      "HTTPHeaders": {
        ...
      },
      "RetryAttempts": 0
    }
  }
}
Code language: JSON / JSON with Comments (json)

Lambda関数2

関数
Resources:
  Function2:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import boto3
          import os

          REGION_NAME = os.environ['REGION_NAME']

          client = boto3.client('ec2', region_name=REGION_NAME)

          def lambda_handler(event, context):
            image_id = event['ImageId']
            deregister_image_response = client.deregister_image(
              ImageId=image_id
            )
            return deregister_image_response
      Environment:
        Variables:
          REGION_NAME: !Ref AWS::Region
      FunctionName: !Sub "${Prefix}-function-02"
      Handler: !Ref Handler
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole2.Arn
Code language: YAML (yaml)

deregister_imageメソッドでAMIを削除します。

メソッド実行時のレスポンスデータを返します。

IAMロール
Resources:
  FunctionRole2:
    Type: AWS::IAM::Role
    DeletionPolicy: Delete
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service:
                - lambda.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: !Sub "${Prefix}-DeleteImagePolicy"
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ec2:DeregisterImage
                Resource: "*"
Code language: YAML (yaml)

AMI削除のために必要な権限を与える内容です。

2つ目のサブステート

Lambda関数3を実行するステートです。

こちらのステートでも、InputPathプロパティに「$.describe-image」と指定することで、先述のItemSelectorプロパティで設定した値を受け取り、関数に渡します。

ResultPathプロパティで、関数3から返されたデータの受け取り方を設定できます。
今回は受け取った値をdelete-snapshotsに格納します。
以下がその具体例です。

{
  ...
  },
  "delete-image": {
    ...
  },
  "delete-snapshots": [
    {
      "ResponseMetadata": {
        "RequestId": "95088418-f80f-4501-a538-64d7a1f71711",
        "HTTPStatusCode": 200,
        "HTTPHeaders": {
          ...
        },
        "RetryAttempts": 0
      }
    }
  ]
}
Code language: JSON / JSON with Comments (json)

Lambda関数3

関数
Resources:
  Function3:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import boto3
          import os

          REGION_NAME = os.environ['REGION_NAME']

          client = boto3.client('ec2', region_name=REGION_NAME)

          def lambda_handler(event, context):
            image_id = event['ImageId']

            responses = []
            for block_device in event['BlockDeviceMappings']:
              snapshot_id = block_device['Ebs']['SnapshotId']
              delete_snapshot_response = client.delete_snapshot(
                SnapshotId=snapshot_id
              )
              print(delete_snapshot_response)
              responses.append(delete_snapshot_response)
            return responses
      Environment:
        Variables:
          REGION_NAME: !Ref AWS::Region
      FunctionName: !Sub "${Prefix}-function-03"
      Handler: !Ref Handler
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole3.Arn
Code language: YAML (yaml)

delete_snapshotメソッドでAMIに関連づいたスナップショットを削除します。

メソッド実行時のレスポンスデータを返します。

IAMロール
Resources:
  FunctionRole3:
    Type: AWS::IAM::Role
    DeletionPolicy: Delete
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service:
                - lambda.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: !Sub "${Prefix}-DeleteSnapshotPolicy"
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ec2:DeleteSnapshot
                Resource: "*"
Code language: YAML (yaml)

スナップショット削除のために必要な権限を与える内容です。

EventBridgeルール

Resources:
  Rule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub "${Prefix}-EventsRule"
      ScheduleExpression: rate(1 hour)
      State: ENABLED
      Targets:
        - Arn: !Ref StateMachineArn
          Id: !Ref StateMachineName
          RoleArn: !GetAtt EventsRuleRole.Arn
Code language: YAML (yaml)

Step Functionsステートマシンを定期的に実行するルールを設定します。

スケジュールの記法はrate式を使用して、1時間に1回、ステートマシンを実行するように設定します。

EventBridgeを使用して、Step Functionsステートマシンを定期的に実行する方法については、以下のページをご確認ください。

https://awstut.com/2023/03/05/use-eventbridge-to-run-step-functions-periodically

環境構築

CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。

CloudFormationスタックを作成し、スタック内のリソースを確認する

CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

https://awstut.com/2021/12/02/cloudformation-nested-stacks

各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。

  • Step Functionsステートマシン:soa-02-004
  • Lambda関数1:soa-02-004-function-01
  • Lambda関数2:soa-02-004-function-02
  • Lambda関数3:soa-02-004-function-03
  • EventBridgeルール:fa-120-EventsRule

AWS Management Consoleから作成されたリソースを確認します。

ステートマシンを確認します。

Detail of Step Functions 1.

正常に作成されています。
ステートマシンがMapステートと、3つのLambda関数で構成されていることがわかります。

EventBridgeルールを確認します。

Detail of EventBridge 1.
Detail of EventBridge 2.

EventBridgeルールが作成されています。
1時間ごとに上述のステートマシンを実行する内容です。

動作確認

AMI作成

準備が整いましたので、テスト用のAMIを作成します。

Detail of EC2 1.

AMIとスナップショットが作成されました。

Detail of EC2 2.
Detail of EC2 3.

詳細は以下の通りです。

  • AMI:ami-0d548220085c3a287
  • スナップショット:snap-03741d041ff197f39

Step Functionsステートマシン実行1回目

ステートマシンの動作を確認します。

Detail of Step Functions 2.

ステートマシンが動作しています。
EventBridgeルールによって、自動的に実行されました。

Detail of Step Functions 3.

実行結果の詳細を見ると、処理は正常に終了しています。

ただしMapステートの途中で終了しています。
これは削除の対象となるAMIが存在しなかったためです。
先ほどAMIを作成しましたが、有効期限の1時間を経過していないため、まだ削除の対象とはなりません。

Step Functionsステートマシン実行2回目

ステートマシンの次の実行まで待機します。

1時間後、再度ステートマシンが実行されます。

Detail of Step Functions 4.

詳細を確認します。

Detail of Step Functions 5.

今回は全てのステートを実行した上で、正常に終了しています。

終了時の出力を確認します。

[
  {
    "describe-image": {
      "CreationDate": "2023-02-24T12:49:49.000Z",
      "ImageId": "ami-0d548220085c3a287",
      "BlockDeviceMappings": [
        {
          "DeviceName": "/dev/xvda",
          "Ebs": {
            "SnapshotId": "snap-03741d041ff197f39",
            ...
          }
        }
      ],
      ...
    },
    "delete-image": {
      "ResponseMetadata": {
        "HTTPStatusCode": 200,
        ...
    },
    "delete-snapshots": [
      {
        "ResponseMetadata": {
          "HTTPStatusCode": 200,
          ...
        }
      }
    ]
  }
]
Code language: JSON / JSON with Comments (json)

確かにMapステート内のAMIおよびスナップショット削除のステートが実行されたことがわかります。

最後にAMIおよびスナップショットの状況を確認します。

Detail of EC2 4.
Detail of EC2 5.

確かにAMIおよびスナップショットが削除されました。

まとめ

Lambda関数とStep Functionsを使用して、古いAMIを定期的に削除する方法をご紹介しました。