boto3はIAMロールの認証情報をclient生成時に取得するが評価はAPI呼び出し時に行う
初めに
EC2上でboto3を利用したAWS API呼び出し処理においてNoCredentialsErrorが発生するということがありました。
認証情報なしのエラーなためメタデータサービスへのアクセスへの問題?大規模な障害があったわけでないので数度のリトライで回復する事象かな?と思ったのですが、対象となっている環境では複数回リトライをしても回復せず、設定上の上限を叩いているケースがあったのでその際の調査備忘録を残しておきます。
EC2のIAMロールを使う時の認証情報について
よくEC2上でAWS APIを呼ぶような処理の場合、IAMユーザーの利用を避けてIAMロールを使うようにという話はありますが、具体的にその権限をどうやってEC2上で利用しているかというところまで追っている人はまちまちかと思います。
仕組みとしてはIAMロールを利用する場合も、IAMユーザのようにアクセスキーを利用する形となっております。
このアクセスキーはインスタンスメタデータサービス(169.254.169.254)にアクセスすることで取得可能です。
ただしこのアクセスキーはIAMユーザとは異なり永続的なものではなく、一時的なものとなりEC2の場合はこれが定期的にローテーションされる形となります。
※ 最大時間自体はIAMロール側の「最大セッション時間」で変更することが可能です

IAMロールの認証情報が取得できない場合はAPI呼び出し時にエラーになる
boto3でAWS APIを呼び出す場合は、clientを作成しそれを用いてAPIを呼び出すためのメソッドを呼び出すという形になります。
もしIAMロールの認証情報が取得できない場合は、このAPIを呼び出す為のメソッド実行時に例外が発生します。
EC2では設定でメタデータサービスの有無効をインスタンス単位で設定できますので、こちらを無効化してAWS APIを呼び出すスクリプトを実行してみます。

スクリプトは以下の通りです。
import boto3
from botocore.exceptions import NoCredentialsError, ClientError
def test_no_credential_error():
try:
print("Create session")
session = boto3.Session()
except Exception as e:
print(f"✗ Failed to create session: {e}")
return
try:
print("Create Client")
sts_client = session.client('sts')
except Exception as e:
print(f"✗ Failed to create STS client: {e}")
return
try:
print("Call AWS API")
response = sts_client.get_caller_identity()
print("✓ get_caller_identity succeeded")
print(f" - Account: {response['Account']}")
print(f" - UserId: {response['UserId']}")
print(f" - Arn: {response['Arn']}")
except NoCredentialsError as e:
print("✗ NoCredentialsError occurred!")
print(f" - Error: {e}")
print(" - This means IMDS is blocked or IAM role is not attached")
raise
except ClientError as e:
print("✗ ClientError occurred!")
print(f" - Error Code: {e.response['Error']['Code']}")
print(f" - Error Message: {e.response['Error']['Message']}")
raise
except Exception as e:
print(f"✗ Unexpected error: {type(e).__name__}")
print(f" - Error: {e}")
raise
if __name__ == '__main__':
test_no_credential_error()
※ Session()を挟んでいるのは今回の検証対象のコードの関係です。boto3.client()で直接生成しても結局その場合でもSessionオブジェクトを生成するのでフローは変わりません
こちらを実行してみるとNoCredentialsErrorはAWS API呼び出しの時点で発生しているため、sessionやclient生成時には認証情報の有無を評価していないことがわかります。
(NoCredentialsErrorなので権限不足ではない)
$ python3 meta_test.py
Create session
Create Client
Call AWS API
✗ NoCredentialsError occurred!
- Error: Unable to locate credentials
- This means IMDS is blocked or IAM role is not attached
Traceback (most recent call last):
File "/home/ec2-user/meta_test.py", line 48, in <module>
test_no_credential_error()
File "/home/ec2-user/meta_test.py", line 24, in test_no_credential_error
...
File "/home/ec2-user/.local/lib/python3.9/site-packages/botocore/signers.py", line 108, in handler
return self.sign(operation_name, request)
File "/home/ec2-user/.local/lib/python3.9/site-packages/botocore/signers.py", line 200, in sign
auth.add_auth(request)
File "/home/ec2-user/.local/lib/python3.9/site-packages/botocore/crt/auth.py", line 62, in add_auth
raise NoCredentialsError()
なお注意点としてあくまでIAMロールを利用する今回のケースであり、「アクセスキーが直接設定されている」かつ「シークレットキーが存在しない」というケースにおいてはsession生成タイミングでNoCredentialsErrorが発生します。
APIの呼び出し処理のリトライだけではダメ?
API呼び出し時に失敗するためここだけリトライしてみましょう。
裏でEC2のメタデータサービスの有無効を切り替えることで、一時的な障害からの回復を擬似的に再現します。
import boto3
from botocore.exceptions import NoCredentialsError, ClientError
import time
def test_no_credential_error():
"""
Session作成とget_caller_identity実行のタイミングを確認
NoCredentialsErrorの場合は10秒間隔でリトライ
"""
try:
print("Create session")
session = boto3.Session()
except Exception as e:
print(f"✗ Failed to create session: {e}")
return
try:
print("Create Client")
sts_client = session.client('sts')
except Exception as e:
print(f"✗ Failed to create STS client: {e}")
return
max_retries = 30
retry_count = 0
while retry_count <= max_retries:
try:
if retry_count > 0:
print(f"\nRetry attempt {retry_count}/{max_retries}")
print("Call AWS API")
response = sts_client.get_caller_identity()
print("✓ get_caller_identity succeeded")
print(f" - Account: {response['Account']}")
print(f" - UserId: {response['UserId']}")
print(f" - Arn: {response['Arn']}")
return # 成功したら終了
except NoCredentialsError as e:
print("✗ NoCredentialsError occurred!")
print(f" - Error: {e}")
print(" - This means IMDS is blocked or IAM role is not attached")
if retry_count < max_retries:
print(f" - Retrying in 10 seconds... ({retry_count + 1}/{max_retries})")
time.sleep(10)
retry_count += 1
else:
print(f" - Max retries ({max_retries}) reached. Giving up.")
raise
except ClientError as e:
print("✗ ClientError occurred!")
print(f" - Error Code: {e.response['Error']['Code']}")
print(f" - Error Message: {e.response['Error']['Message']}")
raise
except Exception as e:
print(f"✗ Unexpected error: {type(e).__name__}")
print(f" - Error: {e}")
raise
if __name__ == '__main__':
test_no_credential_error()
が、別画面でAWS CLIを操作し回復してるのにもリトライを続け回復しませんでした。
$ python3 meta_test.py
Create session
Create Client
Call AWS API
✗ NoCredentialsError occurred!
- Error: Unable to locate credentials
- This means IMDS is blocked or IAM role is not attached
- Retrying in 10 seconds... (1/30)
Retry attempt 1/30
Call AWS API
✗ NoCredentialsError occurred!
- Error: Unable to locate credentials
- This means IMDS is blocked or IAM role is not attached
- Retrying in 10 seconds... (2/30)
Retry attempt 2/30
Call AWS API
✗ NoCredentialsError occurred!
- Error: Unable to locate credentials
- This means IMDS is blocked or IAM role is not attached
- Retrying in 10 seconds... (3/30)
...
## 別画面でs3 lsが叩けるのをみてもこちらが回復しないままなのをみて途中で止めました
Retry attempt 19/30
Call AWS API
✗ NoCredentialsError occurred!
- Error: Unable to locate credentials
- This means IMDS is blocked or IAM role is not attached
- Retrying in 10 seconds... (20/30)
ここで少し気になることがあり、リトライの幅をclientの生成まで広げてみたところこちらでは途中で回復し実行に成功する形となりました。
...
Retry attempt 2/30
Create Client
Call AWS API
✗ NoCredentialsError occurred!
- Error: Unable to locate credentials
- This means IMDS is blocked or IAM role is not attached
- Retrying in 10 seconds... (3/30)
Retry attempt 3/30
Create Client
Call AWS API
✓ get_caller_identity succeeded
- Account: xxxxx
- UserId: xxxxx:i-xxxxxx
- Arn: arn:aws:sts::xxxxxxx:assumed-role/xxxxx-role/i-xxxxx
clientは認証情報を評価しないらしい
botocoreを調べていたところ認証情報の取得に関してはどうもAPI呼び出しの時ではなくclientの生成の際に行われるようです。
ではここで例外を吐くのでは...と思いきやここで取得失敗となった場合Noneを返す仕様のようです。
明示的にコメントに歴史的な経緯で〜というコメントがありました(何があったんでしょうか...)。
このコードの先を追って行ってみたのですが認証情報を持ち回っているcredentialsがNoneであっても基本的には初期化で例外を投げる処理はないようで、その結果メタデータサービスへの成功有無に関わらずclientが生成されてしまうようです。
スタックトレースに記載のある通りauth.pyで認証情報(credentials)の有無が評価されるためここで初めてNoCredentialsErrorが発生します。
しかし前述の通りcredentialsの生成自体はclient生成時に実施するため、このAPI呼び出しだけを再試行しても認証情報がないまま試行を続けるため回復しない形となります。
この関係でEC2上でIAMロールを利用する場合のboto3でメタデータエンドポイントに対する障害を考慮する場合、APIの実行のみのリトライは不十分で、clientごと再生成が必要になります。
終わりに
boto3のAPI呼び出しに関する認証関連の処理を追ってみました。
根本的な問題発生タイミングと実際の処理タイミングが合わないのは違和感がある一方、historically(歴史的な)という単語が出ているように過去に色々あったんだな〜〜と察せられるようなコメントもあったりと意外と調べてみると面白いものでした。
意外と事象自体は出会うことがありそうですが、(自分の探し方が悪いかもしれませんが)意外と情報が見当たりませんでした。
今回のようにごく一時的なエラーに関しては一時的な問題だからいいやと放置されがちですが、掘り進めてみてわかった結果より適切にハンドリングすると改善するものもありますので皆様も時間のあるような時に調査してみてください。






