古いAMIを定期的に削除する – Step Functionsバージョン
AMIの定期的な取得および削除に関して、AWSではDLM(Data Lifecycle Manager)が提供されています。
https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/snapshot-lifecycle.html
以下のページで、DLM入門について取り上げています。
ただし以下に引用した通り、DLMを使用して全てのAMIを管理することはできません。
Amazon Data Lifecycle Manager は、他の方法で作成されたスナップショットまたは AMI の管理には使用できません。
Amazon Data Lifecycle Manager
今回はDLMを使用せず、古いAMIを定期的に削除する方法を考えます。
具体的には、Lambda関数とStep Functionsを使用します。
構築する環境
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ステートマシンの基本的な事項については、以下のページをご確認ください。
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関数で実行するコードをインライン形式で記載します。
詳細につきましては、以下のページをご確認ください。
コードの内容は以下の通りです。
- 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ステートの基本的な事項については、以下のページをご確認ください。
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ステートマシンを定期的に実行する方法については、以下のページをご確認ください。
環境構築
CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。
CloudFormationスタックを作成し、スタック内のリソースを確認する
CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。
各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。
- 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から作成されたリソースを確認します。
ステートマシンを確認します。
正常に作成されています。
ステートマシンがMapステートと、3つのLambda関数で構成されていることがわかります。
EventBridgeルールを確認します。
EventBridgeルールが作成されています。
1時間ごとに上述のステートマシンを実行する内容です。
動作確認
AMI作成
準備が整いましたので、テスト用のAMIを作成します。
AMIとスナップショットが作成されました。
詳細は以下の通りです。
- AMI:ami-0d548220085c3a287
- スナップショット:snap-03741d041ff197f39
Step Functionsステートマシン実行1回目
ステートマシンの動作を確認します。
ステートマシンが動作しています。
EventBridgeルールによって、自動的に実行されました。
実行結果の詳細を見ると、処理は正常に終了しています。
ただしMapステートの途中で終了しています。
これは削除の対象となるAMIが存在しなかったためです。
先ほどAMIを作成しましたが、有効期限の1時間を経過していないため、まだ削除の対象とはなりません。
Step Functionsステートマシン実行2回目
ステートマシンの次の実行まで待機します。
1時間後、再度ステートマシンが実行されます。
詳細を確認します。
今回は全てのステートを実行した上で、正常に終了しています。
終了時の出力を確認します。
[
{
"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およびスナップショットの状況を確認します。
確かにAMIおよびスナップショットが削除されました。
まとめ
Lambda関数とStep Functionsを使用して、古いAMIを定期的に削除する方法をご紹介しました。