🎄

AWS EventBridge+Lambdaでスポットインスタンスの中断を処理する

2024/12/25に公開

TL;DR

AWS EventBridgeとLambdaを用いて,スポットインスタンスの中断通知を受け取った際の処理を自動化する手順を紹介します.

私たちの研究室

https://nisk.doshisha.ac.jp/

アドベントカレンダー 25日目 🎄

https://nislab-advent-calendar-2024-12.vercel.app/

スポットインスタンスとは?

余剰のEC2インスタンス容量を利用するために提供される,低コストで運用可能なインスタンスです.

  • 特徴
    • 通常のオンデマンドインスタンスと比べて大幅に割引された価格(最大90%割引)で利用可能.
    • 需要と供給による価格の変動あり.
    • AWSによる中断の可能性あり.
  • 適した用途
    • 柔軟なスケーリングが可能な分散型アプリケーション.
    • 短時間で完了するバッチ処理.
    • コスト効率を重視する一方で,中断に対応できるワークロード.


[AWS Black Belt Online Seminar]Amazon EC2スポットインスタンス
https://aws.amazon.com/jp/ec2/spot/

スポットインスタンスの中断とは?

AWSスポットインスタンスは,余剰のEC2リソースを活用することで低コストを実現しています.しかし,この仕組み上,インスタンスが突然中断される可能性があります.中断とは,AWSがスポットインスタンスを停止または終了することを指します.これは以下の理由で発生します:

  • 需要の増加:オンデマンドインスタンスの利用が増え,スポットインスタンスの提供が困難になった場合.
  • 価格の上昇:スポットインスタンスの市場価格がユーザーが設定した上限価格を超えた場合.

中断通知

突然と言っても,対処する方法はあります.そこで重要になってくるのが中断通知です.AWSはスポットインスタンスの中断を行う際に,インスタンスに「2分前の通知」を送付します.この通知は以下の方法で受け取ることができます:

  • Amazon EventBridge:中断通知をトリガーとしてアクションを実行可能.
  • EC2メタデータ:インスタンス内で直接中断情報を取得可能.

https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/spot-instance-termination-notices.html

今回やること

スポットインスタンスの概要について説明したところで,メイントピックに移ります.
今回は中断通知の受け取りをEventBridge経由で行います.そこから,Lambdaを用いて中断を処理します.
今回の中断処理は,現在activeなスポットフリートリクエストをキャンセルし,新しいスポットフリートリクエストを作成するという流れで進めます.

構成

今回の構成は以下の通りです.

各コンポーネントの役割
AWS FIS:中断通知をシミュレートし,スポットインスタンスに送付
EventBridge:中断通知を受け取り,AWS Lambdaを起動
AWS Lambda:中断処理を実行
IAM:Lambdaに中断処理に必要な権限を付与

各種サービスの概要

  • AWS Fault Injection Simulator(FIS)
    • AWSが提供するフルマネージド型のカオスエンジニアリングサービス
    • 本番環境やテスト環境でシステムの耐障害性を向上させるための実験を安全かつ計画的に実施可能
  • Amazon EventBridge
    • AWSが提供するサーバーレスのイベントバスサービス
    • 異なるAWSサービスや外部アプリケーション,独自のアプリケーション間でイベントを連携
  • AWS Lambda
    • AWSが提供するサーバーレスコンピューティングサービス
    • ユーザーはサーバーを管理することなくコードを実行
    • イベントに基づいたアクションを簡単に自動化
  • Identity and Access Management (IAM)
    • AWSリソースへのアクセスを安全に管理するためのサービス
    • どのユーザーやシステムがどのリソースにどのような操作を行えるかを詳細に制御

フロー

以下の流れでの中断処理を想定します.

  1. スポットインスタンスの起動
    • スポットフリートリクエストを作成し,VPC内にスポットインスタンスを起動します.
    • スポットフリート:AWSの余剰リソースを活用する仕組みで,複数のインスタンスを効率的に運用します.
  2. FISで中断通知をシミュレート(中断通知の送付)
  3. 中断の検知
    EventBridgeは中断イベントをトリガーとして動作し,AWS Lambdaを起動
  4. 中断処理(Lambdaの起動)
    • Lambda関数は,以下のアクションを実行:
      • スポットフリートリクエストのキャンセル
        中断通知を受けたスポットフリートをキャンセルし,関連するインスタンスを終了
      • 新しいスポットフリートリクエストの作成

スポットインスタンスの起動

スポットフリートリクエストで,スポットインスタンスを起動します.
詳細はこちらを参考にしてください↓
https://zenn.dev/kipeco8/articles/5a500862ec7500

AWS FIS実験テンプレートの作成

AWS FISで中断通知のシミュレーションを作成して,実験を開始します.
詳細はこちらを参考にしてください↓
https://zenn.dev/kipeco8/articles/ee876e263d6a92

EventBridgeの設定

スポットインスタンスの中断通知をEventBridgeで受けるように設定します.

  1. Amazon EventBridgeのコンソールを開きます.
  2. ナビゲーションペインで,バス>ルールを選択します.
  3. ルールを作成を選択します.
  4. ルールの詳細にルールの説明と名前を入力します.今回はどちらもspot-instance-interruptedとします.
  5. イベントパターンを構築を設定します.
    • イベントソース:AWS イベントまたは EventBridge パートナーイベント
    • イベントパターン:
      {
       "source": ["aws.ec2"],
       "detail-type": ["EC2 Spot Instance Interruption Warning"]
      }
      
  6. ターゲットを選択を設定します.
    • ターゲットタイプ:AWSのサービス
    • ターゲットを選択:Lambda関数
    • 関数:中断処理で作成した関数
  7. レビューと作成に進み,ルールの作成を選択します.

中断処理

中断処理をするLambda関数を設定します.
以下のような構成で,spot_interrupted_actionという関数を作成しました.

spot_interrupted_action/
├ active_spot_fleet.py
├ config.json
└ lambda_function.py
lambda_function.py
import boto3

def get_active_spot_fleet():
    # boto3クライアントを初期化
    ec2_client = boto3.client('ec2')

    active_spot_fleet_ids = []
    next_token = None   #ページング

    try:
        while True:
            # describe_spot_fleet_requests 呼び出し
            if next_token:
                response = ec2_client.describe_spot_fleet_requests(
                    MaxResults=10,
                    NextToken=next_token
                )
            else:
                response = ec2_client.describe_spot_fleet_requests(MaxResults=10)

            # SpotFleetRequestState が "active" のものをフィルタリング
            for request in response.get("SpotFleetRequestConfigs", []):
                if request.get("SpotFleetRequestState") == "active":
                    active_spot_fleet_ids.append(request["SpotFleetRequestId"])
            
            # 次のページがない場合ループ終了
            next_token = response.get("NextToken")
            if not next_token:
                break

        # 結果の表示と返却
        if active_spot_fleet_ids:
            print("Active Spot Fleet IDs:", active_spot_fleet_ids)
        else:
            print("No active Spot Fleet Requests found")

    except Exception as e:
        print("An error occurred:", str(e))
        raise
    
    return active_spot_fleet_ids

config.json
{
    "TargetCapacity": 1,
    "SpotPrice": "0.04",
    "IamFleetRole":"your_role",
    "LaunchSpecifications": [
        {
            "ImageId": "ami-023ff3d4ab11b2525",
            "KeyName": "your_key",
            "SecurityGroups": [
              { "GroupId":"your_sg"}
            ],
            "InstanceType": "t1.micro",
            "TagSpecifications":[
                {
                    "ResourceType":"instance",
                    "Tags":[
                        {
                            "Key":"Name",
                            "Value":"interruptMe"
                        }
                    ]
                }
            ],
            "UserData":"your_userdata(encoded)"
        }
    ]
}
active_spot_fleet.py
import boto3

def get_active_spot_fleet():
    # boto3クライアントを初期化
    ec2_client = boto3.client('ec2')

    active_spot_fleet_ids = []
    next_token = None   #ページング

    try:
        while True:
            # describe_spot_fleet_requests 呼び出し
            if next_token:
                response = ec2_client.describe_spot_fleet_requests(
                    MaxResults=10,
                    NextToken=next_token
                )
            else:
                response = ec2_client.describe_spot_fleet_requests(MaxResults=10)

            # SpotFleetRequestState が "active" のものをフィルタリング
            for request in response.get("SpotFleetRequestConfigs", []):
                if request.get("SpotFleetRequestState") == "active":
                    active_spot_fleet_ids.append(request["SpotFleetRequestId"])
            
            # 次のページがない場合ループ終了
            next_token = response.get("NextToken")
            if not next_token:
                break

        # 結果の表示と返却
        if active_spot_fleet_ids:
            print("Active Spot Fleet IDs:", active_spot_fleet_ids)
        else:
            print("No active Spot Fleet Requests found")

    except Exception as e:
        print("An error occurred:", str(e))
        raise
    
    return active_spot_fleet_ids

スポットインスタンスを中断させてみる

FISで作成した実験テンプレートを開始して,スポットインスタンスを中断させます.

FISのロググループを確認すると,i-で始まるインスタンスをターゲットとしたスポットインスタンスの中断シミュレーションが完了しているログが記録されています.

{
    "id": "exp_id",
    "log_type": "target-resolution-end",
    "event_timestamp": "2024-12-14T07:49:03.962Z",
    "version": "2",
    "details": {
        "target_resolution_end_time": "2024-12-14T07:49:03.960Z",
        "target_name": "OneSpotInstance",
        "target_type": "aws:ec2:spot-instance",
        "resolved_targets": [
            {
                "arn": "arn:aws:ec2:ap-northeast-1:xxxxxxxxxxxxx:instance/i-xxxxxxxxxxxxxxxx"
            }
        ],
        "page": 1,
        "total_pages": 1
    }
}

続いて,lambdaのロググループを確認すると,中断前のスポットフリート(sfr-2d7600a1-)のキャンセルが行われ,新しいスポットフリート(sfr-3d9e997e-)の作成が完了していることがわかります.

> 2024-12-14T07:49:17.732Z
INIT_START Runtime Version: python:3.10.v46	Runtime Version ARN: arn:aws:lambda:ap-northeast-1::runtime:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
> 2024-12-14T07:49:18.444Z
Active Spot Fleet IDs: ['sfr-2d7600a1-xxxx-xxxx-xxxx-xxxxxxxxxxxx']
> 2024-12-14T07:49:18.620Z
Cancel successfully completed.
> 2024-12-14T07:49:18.620Z
status:[{'CurrentSpotFleetRequestState': 'cancelled_terminating', 'PreviousSpotFleetRequestState': 'active', 'SpotFleetRequestId': 'sfr-2d7600a1-xxxx-xxxx-xxxx-xxxxxxxxxxxx'}]
> 2024-12-14T07:49:20.018Z
New Spot Fleet Request (request id: sfr-3d9e997e-xxxx-xxxx-xxxx-xxxxxxxxxxxx) Created
> ...

最後に

AWS EventBridgeとLambdaを用いて,スポットインスタンスの中断通知を受け取った際の処理を自動化する手順を紹介しました.
今回は,トリガーからLambdaを起動して中断処理を行いましたが,例えば,SNSで中断通知の受信をお知らせすることなどもできます.
スポットインスタンスは,中断など扱いにくい側面もある暴馬ですが,うまく飼い慣らすことで恩恵をもたらしてくれる可愛いやつでもあります 🏇

参考文献

https://zenn.dev/not75743/articles/237263493fb20b
https://www.yamamanx.com/spot-instance-action-sns/
https://qiita.com/yoshimi0227/items/ee86141f5b67c6d378c1

NISLab 小板研究室

Discussion