Lambdaでスポットリクエストのキャンセルをしてみた

Lambdaでスポットリクエストのキャンセルをしてみた

Lambdaでスポットリクエストのキャンセルをしてみました
Clock Icon2024.10.05

はじめに

こんにちは、アノテーションのなかたです。
今回は、Lambdaでスポットリクエストのキャンセルをしてみました。
以下の記事が元となっており、一部手順についてもこちらを参照しています。
https://dev.classmethod.jp/articles/spot-request-from-lambda-by-launch-template/

今回やりたいこと

まず以下のように、起動テンプレートによってスポットリクエストが作成されている環境を前提としています。
image-2.png
ここから、スポットリクエストとスポットインスタンスを削除したいというのが今回の目的です。
image-2.png
そのため、Lambdaから削除する方法を検証してみました。
スクリーンショット 2024-10-05 18.08.22

やってみる

1. Lambda関数の作成

Python3.12環境で関数を作成します。

関数の流れとしては、

  1. Nameタグから対象のスポットリクエストを見つける
  2. スポットリクエストに紐づいているスポットインスタンスを見つける
  3. それぞれを削除する

のようになります。
そのため、コードとしては以下のようになりました。

ソースコード
lambda_function.py
import boto3
import json
import os
from botocore.exceptions import ClientError

def lambda_handler(event, context):
    # 環境変数からリージョンを取得、設定されていない場合はデフォルト値を使用
    region = os.environ.get('AWS_REGION', 'ap-northeast-1')

    # EC2クライアントの初期化
    ec2_client = boto3.client('ec2', region_name=region)

    # 環境変数からスポットリクエストのNameタグを取得
    spot_request_name = os.environ.get('SPOT_REQUEST_NAME')

    if not spot_request_name:
        return {
            'statusCode': 400,
            'body': '環境変数 SPOT_REQUEST_NAME が設定されていません。'
        }

    try:
        # 指定されたNameタグを持つスポットリクエストを検索
        spot_requests = ec2_client.describe_spot_instance_requests(
            Filters=[
                {'Name': 'tag:Name', 'Values': [spot_request_name]},
                {'Name': 'state', 'Values': ['active', 'open']}
            ]
        )

        if not spot_requests['SpotInstanceRequests']:
            return {
                'statusCode': 404,
                'body': '指定されたNameタグを持つアクティブなスポットリクエストが見つかりませんでした。'
            }

        # 複数のスポットリクエストに対応するため、スポットリクエストIDとインスタンスIDを取得
        spot_request_ids = []
        instance_ids = []
        for request in spot_requests['SpotInstanceRequests']:
            spot_request_ids.append(request['SpotInstanceRequestId'])
            if 'InstanceId' in request:
                instance_ids.append(request['InstanceId'])

        # スポットリクエストのキャンセル
        cancel_response = ec2_client.cancel_spot_instance_requests(
            SpotInstanceRequestIds=spot_request_ids
        )

        # インスタンスの終了
        if instance_ids:
            terminate_response = ec2_client.terminate_instances(
                InstanceIds=instance_ids
            )

        # キャンセルと終了の成功を確認
        cancelled_ids = [req['SpotInstanceRequestId'] for req in cancel_response['CancelledSpotInstanceRequests']]
        terminated_ids = instance_ids if instance_ids else []

        return {
            'statusCode': 200,
            'body': json.dumps({
                'CancelledSpotRequests': cancelled_ids,
                'TerminatedInstances': terminated_ids
            })
        }

    except ClientError as e:
        # エラーハンドリング
        error_message = e.response['Error']['Message']
        return {
            'statusCode': 500,
            'body': f'エラーが発生しました: {error_message}'
        }

こちらのスクリプトは、Claude 3.5 Sonnetを使用して作成し、一部を私が修正しました。

こちらのコードをデプロイします。

2. 環境変数の設定

Lambda関数の実行に使用する環境変数を次のように設定します。
スポットリクエストを特定するため、起動テンプレートに設定したNameタグの値を設定します。

SPOT_REQUEST_NAME: スポットリクエストのNameタグの値

image-2.png
東京リージョン以外を指定したい場合は以下の環境変数も追加します。

AWS_REGION: リージョン名

3. 実行ロールの変更

以下のJSONをインラインポリシーとして、Lambda関数の実行ロールに追加します。

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Effect": "Allow",
			"Action": [
				"ec2:DescribeInstances",
				"ec2:DescribeSpotInstanceRequests",
				"ec2:CancelSpotInstanceRequests",
				"ec2:TerminateInstances"
			],
			"Resource": "*"
		}
	]
}

それぞれ Action の目的は次のようになります。

  • ec2:DescribeInstances
    スポットインスタンスを見つけるため
  • ec2:DescribeSpotInstanceRequests
    スポットリクエストを見つけるため
  • ec2:CancelSpotInstanceRequests
    スポットリクエストをキャンセルするため
  • ec2:TerminateInstances
    スポットインスタンスを削除 / 終了するため

4. 検証環境の準備

以下の図のように、検証環境を準備していきます。
image-2.png
まず、スポットリクエストを行う起動テンプレートからスポットインスタンスを起動します。
スクリーンショット 2024-10-04 16.18.37
起動テンプレートのリソースタグからリソースタイプとしてスポットインスタンスリクエストを選択します。
スクリーンショット 2024-10-04 17.50.38
これにより、スポットリクエストにNameタグを付与するよう設定できます。
スクリーンショット 2024-10-04 17.54.37.png

起動テンプレートからインスタンスを作成し、スポットリクエストが作成されます。
image
また、インスタンスも起動しています。
スクリーンショット 2024-10-04 16.12.06

5. 実行

では、環境の準備が完了したため、作成したLambda関数を実行します。

Lambda関数の出力
Response
{
  "statusCode": 200,
  "body": "{\"CancelledSpotRequests\": [\"sir-g1gps1nm\"], \"TerminatedInstances\": [\"i-093cec4bfa3104366\"]}"
}

正常に実行が完了したようです。

スポットリクエストの画面でも確認します。
スクリーンショット 2024-10-04 10.42.48.png
スポットリクエストの状態がcanceledとなっているのが確認できました。
スクリーンショット 2024-10-05 18.58.10
また、スポットインスタンスも削除されていることが確認できました。

まとめ

今回の検証から、私なりに重要な部分をまとめてみました。

  • Lambda からスポットリクエストをキャンセルする場合は、スポットインスタンスの終了も実行する必要がある
  • 起動テンプレートからスポットリクエストにタグを付与するには、リソースタグのリソースタイプとしてスポットインスタンスリクエストを選択する
  • Lambda関数で他のリソースを操作する場合は、実行ロール権限の確認が重要である

参考

https://dev.classmethod.jp/articles/cfn-ec2-spot-instance/
https://qiita.com/comefigo/items/dcdb47afc27de4a5c31f
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2/client/cancel_spot_instance_requests.html

アノテーション株式会社について

アノテーション株式会社はクラスメソッドグループのオペレーション専門特化企業です。サポート・運用・開発保守・情シス・バックオフィスの専門チームが、最新 IT テクノロジー、高い技術力、蓄積されたノウハウをフル活用し、お客様の課題解決を行っています。当社は様々な職種でメンバーを募集しています。「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、アノテーション株式会社 採用サイトをぜひご覧ください。

この記事をシェアする

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.