EC2インスタンス起動時のRequestLimitExceeded エラーの解決方法(Boto3)


この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので十分ご注意ください。

お久しぶりです。株式会社協栄情報システム3部所属の寺尾です。

今回、AWS SDK for Python (Boto3)からEC2インスタンスを起動していたところ、RequestLimitExceededエラーに遭遇しました。

ですのでその解決方法を書いていきたいと思います。

前提条件

  • AWS SDK for Python (Boto3)実行環境

RequestLimitExceededエラーの原因

まずこちらのエラーの原因はリクエストしたAPIアクションの、リクエストレート制限もしくはリソースレート制限に引っかかってしまったことが原因です。
要は同時にインスタンスを起動しすぎてしまったようです。

ですので、このエラーが発生した場合には一定間隔の遅延を設けて再度EC2インスタンスを起動することで解決する事が可能です。

RequestLimitExceededエラーの解決方法

1.修正前スクリプト

EC2インスタンスを起動するサンプルスクリプトを以下に用意しました。

# 引数:instanceID

import sys
import boto3
import botocore

def start_instance():
    try:
        # 引数からインスタンスIDを取得して起動
        instanceID = sys.argv[1]
        ec2 = boto3.client("ec2",region_name='ap-northeast-1')
        result = ec2.start_instances(InstanceIds=[instanceID])

        return result

    except Exception as e:
        return e

# メイン処理
result = start_instance()
sys.exit(result)

instanceID = sys.argv[1]で引数となるインスタンスIDを取得して、そのインスタンスを起動するスクリプトです。
このスクリプトを複数同時に起動すると"RequestLimitExceeded"エラーが発生する場合があります。
例外はすべてエラーメッセージをそのまま返すだけになっていますので、"RequestLimitExceeded"エラーが発生してもエラーメッセージを返して終了するだけになってしまいます。

2.リトライ処理を実装する

上記のスクリプトを修正したものが以下になります。

# 引数:instanceID

import sys
import boto3
import botocore
import time

def start_instance():
    try:
        # 引数からインスタンスIDを取得して起動
        instanceID = sys.argv[1]
        ec2 = boto3.client("ec2",region_name='ap-northeast-1')
        result = ec2.start_instances(InstanceIds=[instanceID])

        return result

    except botocore.exceptions.ClientError as e:
        # もしRequestLimitExceededならリトライ処理を起動する
        if e.response['Error']['Code'] == 'RequestLimitExceeded':
            print(e)
            result = retry(instanceID,ec2)
            return result
        return e

    except Exception as e:
        return e

def retry(retry_instanceID,ec2):
    """
    5秒間のバックオフ値を設け、再度インスタンスを起動する
    """
    try:
        time.sleep(5)
        result = ec2.start_instances(InstanceIds=[retry_instanceID])

        return result

    except Exception as e:
        return e

#メイン処理
result = start_instance()
sys.exit(result)

以下の部分でRequestLimitExceededエラーを定義しています。

    except botocore.exceptions.ClientError as e:
        # もしRequestLimitExceededならリトライ処理を起動する
        if e.response['Error']['Code'] == 'RequestLimitExceeded':

もしRequestLimitExceededエラーが発生した場合、新たに追加したretry関数を起動するようにしました。
retry関数は引数となるインスタンスIDとboto3 clientを受け取り、5秒の時間をおいてから再度インスタンス起動処理を実行します。
これでRequestLimitExceededエラーは解決完了になります!

3.再帰的にリトライ処理を実装する

これで解決、だと言いたいところでしたが一つ見落としがありました。
それはリトライ処理を走らせた場合に再度RequestLimitExceededエラーが発生した場合はどうするのか?ということです。
先ほどのスクリプトですと複数回連続でRequestLimitExceededエラー起きた場合は起動が不可能です。
そこで以下のようにスクリプトを改良しました。

# 引数:instanceID

import sys
import boto3
import botocore
import time

def start_instance():
    try:
        # 引数からインスタンスIDを取得して起動
        instanceID = sys.argv[1]
        ec2 = boto3.client("ec2",region_name='ap-northeast-1')
        result = ec2.start_instances(InstanceIds=[instanceID])

        return result

    except botocore.exceptions.ClientError as e:
        # もしRequestLimitExceededならリトライ処理を起動する
        if e.response['Error']['Code'] == 'RequestLimitExceeded':
            print(e)
            result = retry(instanceID,ec2,0)
            return result
        return e

    except Exception as e:
        return e

def retry(retry_instanceID,ec2,retry_count):
    """
    5秒間のバックオフ値を設け、再度インスタンスを起動する
    """
    try:
        time.sleep(5)
        local_retry_count = retry_count + 1
        result = ec2.start_instances(InstanceIds=[retry_instanceID])

        return result

    except botocore.exceptions.ClientError as e:
        # もしRequestLimitExceededエラーなら再帰的にリトライ処理を起動する
        if e.response['Error']['Code'] == 'RequestLimitExceeded': 
            # もしリトライカウントが10回を超えていたな関数から抜ける
            if local_retry_count > 10:
                print("retries exceeded 10")
                return e
            return retry(retry_instanceID,ec2,local_retry_count)
        return e

    except Exception as e:
        return e

#メイン処理
result = start_instance()
sys.exit(result)

下記の部分でリトライ処理中に再度RequestLimitExceededエラーが発生した場合にretry関数自身をretry関数から呼び出す処理を追加しました。

    except botocore.exceptions.ClientError as e:
        # もしRequestLimitExceededエラーなら再帰的にリトライ処理を起動する
        if e.response['Error']['Code'] == 'RequestLimitExceeded': 
            # もしリトライカウントが10回を超えていたな関数から抜ける
            if local_retry_count > 10:
                print("retries exceeded 10")
                return e
            return retry(retry_instanceID,ec2,local_retry_count)
        return e

この再帰的なリトライ処理は以下のような構造になっており、最後のリトライ処理の結果が戻り値として帰ってくるようになっています

file

また、無限ループを防止するために自身が何週目のretry関数に存在しているのかを把握するための"local_retry_count"変数を実装し、10回を超えて連続してRequestLimitExceededエラーが発生した場合は関数を終了するようにしています。

これで複数回のRequestLimitExceededエラーが発生しても解決できるスクリプトの完成です!

参考

PowerShell を使用して複数の Amazon EC2 インスタンスをプログラムで起動するときに、RequestLimitExceeded エラーを回避するにはどうすればよいですか?

[AWS]boto3のエラーハンドリング(翻訳)とベストプラクティス

おわりに

ここまで読んでいただきありがとうございました。
再帰的な関数は頭の中で想像すると少しわかりにくい実装ですね。今回図にしてみたことで自分自身の理解の助けになったような気もします。
間違いなどあればぜひご指摘ください。
この記事が誰かの助けとなれば幸いです。

Last modified: 2024-02-07

Author