概要
開発環境にEC2のスポットインスタンスを利用することはよくあると思います。その中でも「永続的リクエスト」を使っているとついついインスタンスだけを「終了」し、スポットリクエストをキャンセルし忘れることありませんか?リクエスト有効期間内であれば、自動的に同じタイプのインスタンスがゾンビのごとくのように再作成され、無駄な課金が発生します。
インスタンスの削除時にスポットリクエストからキャンセルするように運用手順で決めていてもついつい忘れがちなので、自動化する方法を紹介します。
前提条件のまとめ
- スポットインスタンスを使用している
- 永続的リクエストである
- リクエスト有効期限内である
構成
EventBridgeを使用することで、インスタンス終了イベント時に指定したLambdaを実行し、スポットリクエストをキャンセルします。
EventBridge
イベントとしてインスタンスの状態変更通知で「Terminated」を追加します。
{
"detail-type": [
"EC2 Instance State-change Notification"
],
"source": [
"aws.ec2"
],
"detail": {
"state": [
"terminated"
]
}
}
Lambda
Lambdaにアタッチされているロールに以下の権限(インスタンスの情報取得、スポットリクエストのキャンセル)を含めればOKです。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ec2:CancelSpotInstanceRequests",
"ec2:DescribeInstances"
],
"Resource": "*"
}
]
}
EventBridgeから渡される変数は以下になります。
同時に複数のインスタンスを削除した場合でも、1つずつのイベントとして送られますので、
Lambda側では基本的に単数を想定した実装でよいでしょう。
{
"version": "0",
"id": "xxxxxxxxxxxxxxxxxxxxxx",
"detail-type": "EC2 Instance State-change Notification",
"source": "aws.ec2",
"account": "xxxxxxxxxx",
"time": "2020-10-26T09:28:11Z",
"region": "ap-northeast-1",
"resources": [
"arn:aws:ec2:ap-northeast-1:xxxxxx:instance/i-xxxxxxxxxxxx"
],
"detail": {
"instance-id": "i-xxxxxxxxxxxx",
"state": "terminated"
}
}
eventのinstance-idからインスタンスの情報を取得し、関連するスポットリクエストをキャンセルする。
import json
import boto3
def lambda_handler(event, context):
'''
EC2インスタンス削除時に関連するスポットリクエストをキャンセルする
'''
print('--------event----------')
print(event)
print('------------------')
client = boto3.client('ec2')
event_detail = event['detail']
if event_detail['state'] != 'terminated':
return
# インスタンスIDで終了済みのスポットインスタンスを取得する
response = client.describe_instances(
Filters=[{
'Name': 'instance-state-name',
'Values': ['terminated']
},{
'Name': 'instance-lifecycle',
'Values': ['spot']
}],
InstanceIds=[event_detail['instance-id']]
)
# 対象がなければ、終了
if len(response['Reservations']) <= 0:
return
spot_request_id = ''
for reservations in response['Reservations']:
for ins in reservations['Instances']:
spot_request_id = ins['SpotInstanceRequestId']
print('--------cancel id----------')
print(spot_request_id)
print('------------------')
# スポットリクエストをキャンセル
client.cancel_spot_instance_requests(SpotInstanceRequestIds=[spot_request_id])