👋

PythonバージョンアップでAssumeRoleが失敗!VPC Endpointの落とし穴

に公開

この記事は、ナウキャスト Advent Calendar 2025の3日目の記事です。

はじめに

こんにちは。ナウキャストでデータエンジニアをしている大城です。

本記事では、あるデータパイプラインで発生した「Pythonのバージョンを上げたらAssumeRoleができなくなった問題」の原因と解決策について解説します。
また、AWSで構築しているシステムの、特にネットワークと権限のトラブルシューティングの流れの一例を共有します。

AWSでシステムを構築・運用する方々の何かしらの参考になれば幸いです。

本記事における前提知識

以下の用語について基本的な知識と理解があることを前提としています。

  • 権限管理: IAM Role/Policy, STS, AssumeRole
  • ネットワーク: VPC, Subnet, NAT Gateway, VPC Endpoint

三行まとめ

  • Python 3.9→3.11のアップグレードに伴いBoto3のバージョンも上がり、STSリクエスト時にデフォルトでグローバルエンドポイントではなくリージョナルエンドポイントを使うようになりました。また、VPC Endpointを設定していました。そのため、STSリクエストがNAT GatewayからVPC Endpoint経由になった結果、接続元IPが変わり、クロスアカウントのAssumeRoleがIP制限で弾かれるようになっていました。
  • 短期の対応としては、グローバルエンドポイントを明示的に使用するようコードを修正しました。長期的には、クロスアカウント先と調整し、aws:SourceIpではなくaws:SourceVpceによる制限を行うようポリシー見直しを検討します。
  • バージョンを上げるときはライブラリの変更点にも気を配りましょう。また、VPC EndpointとプライベートDNS名有効化時の挙動には注意しましょう。

背景・前提

ある日次バッチは、アカウントAのECSタスク(タスクロール:Role A)が社外アカウントBのRole BをAssumeRoleしてアカウントBのS3からアカウントAのS3にコピーする処理を実行しています。
ECSタスクはプライベートサブネットに配置され、0.0.0.0/0 がNAT Gatewayを向くようにサブネットのルートテーブルは設定されています。
アカウントB側のRole Bの信頼ポリシーでは、Role AからのAssumeRoleが許可されており、接続元IPがNAT GatewayのグローバルIPのときのみ許可するよう制限しています。
なお、アカウントAのVPCにはSTSのVPC Endpointが作成され、プライベートDNS名が有効になっています。

システム構成イメージ(※IPアドレスは例示です)

発生した事象

日次バッチのPythonを3.9から3.11にアップグレードしたところ、インフラ構成を変えていないにも関わらず、ECSタスクでAssumeRoleが失敗するようになりました。

エラーメッセージ

"errorMessage": "User: arn:aws:sts::111111111111:assumed-role/role-a/yyyyyyyy is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::222222222222
:role/role-b",

一方で、同一サブネットに配置している作業用EC2上のAWS CLIでは問題なくAssumeRoleが成功していました。

原因

原因はBoto3のデフォルト挙動の変更とVPC設定が重なったことでした。

  • Boto3のバージョン1.40より、STSリクエストにデフォルトでリージョナルエンドポイントを使用するようになりました。
  • アカウントAのVPCでは、STSのVPC Endpointが作成され、かつプライベートDNS名が有効化されていたことにより、リージョナルエンドポイントへのアクセスはVPC Endpoint経由になっていました。
  • VPC Endpoint経由だと、STSリクエストの接続元IPはNAT GatewayのIPアドレスではなくVPCのプライベートIPとなり、Role Bの信頼ポリシーで許可しているIPとは異なるため、AssumeRoleが許可されませんでした。
  • 作業用EC2では、古いAWS CLI(Ver1)を使用しており、デフォルトではグローバルエンドポイントを使用しているため、問題なくAssumeRoleができていました。

グローバルエンドポイント / リージョナルエンドポイントとは?

STSのエンドポイントは大きく二種類あります。

グローバルエンドポイントはSTSのリリース時に構築されたエンドポイントで、実体としてはus-east-1リージョンでホストされるSTSサービスです。

リージョナルエンドポイントは、リージョンごとに用意されたSTSエンドポイントです。
レイテンシー、耐障害性などの観点で、リージョナルエンドポイントの利用が推奨されています。

参考: リージョナル AWS STS エンドポイントの使用方法 | Amazon Web Services ブログ

※ただ、2025年からグローバルエンドポイントでも自動で同一リージョンで処理されるようになるアップデートがされるようです。

Starting in early 2025, requests to the STS global endpoint will be automatically served in the same Region as your AWS deployed workloads.
(引用元: Announcing upcoming changes to the AWS Security Token Service global endpoint | AWS Security Blog )

VPC Endpoint / プライベートDNSとは?

VPC EndpointをVPC内に作成すると、AWSの各種サービスに対してパブリックなインターネットではなくAWSの内部ネットワーク経由で接続することができ、セキュアになります。

インターフェイス型のVPC EndpointにはプライベートDNS名有効化オプションがあり、有効化することでパブリックなAWSサービスへのリクエストがVPCエンドポイントに解決されるようになります。VPCエンドポイント経由のリクエストでは、リクエスト先から見た接続元IPはVPCのプライベートIP(具体的にはENIのIP)になります。

参考:

調査検証と対応方針検討の記録

ここからは原因調査と対応方針策定までの流れをまとめます。

ネットワーク確認

ECSタスク実行時のサブネットとルートテーブルを確認し、 0.0.0.0/0 がNAT Gatewayに向いていることを確認しました。

CloudTrailの確認

sts:AssumeRoleがFailしているので、このAPIリクエストの詳細を確認するためにCloudTrail ログを見てみます。
CloudTrailのEvent Historyを、 Event Name = AssumeRole日時(Taskの実行日時) でフィルタして確認しました。

  • 成功時イベント : regionがus-east-1、sourceIPAddressがNATのグローバルIPだった
  • 失敗時イベント : regionがap-northeast-1、sourceIPAddressが内部IP、かつ vpcEndpointId の値があった

失敗時のイベントのログを確認したことで、STSリクエストがVPC Endpoint経由になっていることが分かりました。
※ちなみに、CloudTrailでは、STSのログは対応するリージョンに切り替えないと見ることができないので注意です。例えばグローバルエンドポイントへのログはus-east-1に切り替えないと見られません。

Boto3のリポジトリを確認し、endpointに関係するIssueや変更履歴がないか確認

Pythonのバージョン前後で挙動が変わったことからライブラリのバージョン変更に原因がありそうなので、Boto3のrepositoryを見てみます。
以下のIssueとCHANGE LOGを見つけました。

記載内容から、v1.40.0からデフォルトのSTSエンドポイントがグローバルからリージョナルになったことが分かりました。

バッチのBoto3のバージョンが変わったことを確認

念の為、バッチのBoto3のバージョンを確認します。 poetry.lock の差分を見ると、変更前はバージョン1.40未満かつ変更後はバージョン1.40以上となっていることが確認できました。

STSのエンドポイントについてドキュメントを確認

STSのエンドポイントについて公式ドキュメントなどをあたって理解を深めます。

ここまでで、リクエストがVPC Endpoint経由になっている理由が、

  • Boto3のバージョンが変わってリージョナルエンドポイントを向くようになったこと
  • 本バッチのVPCではVPC EndpointがプライベートDNS有効化された状態で作成されていること

だと分かりました。

今回の対応と背景

原因が分かったので、対応策を考えます。解決策としては以下が挙げられます。

  1. コードを変更し、明示的にグローバルエンドポイントを利用する
    • メリット: これまでと同じ接続方法を続けることができ、影響範囲もコード修正箇所に限定できる
    • デメリット: STSの推奨プラクティスとは異なる
  2. 信頼ポリシーでsourceVpceを設定する
    • リージョナルエンドポイントを使いつつ、IAM Roleの信頼ポリシーで制限の掛け方を変える
      • 例)
      {
          "Version": "2012-10-17",
          "Statement": [
              {
                  "Effect": "Allow",
                  "Principal": {
                      "AWS": "arn:aws:iam::123456789012:root"
                  },
                  "Action": "sts:AssumeRole",
                  "Condition": {
                      "StringEquals": {
                          "aws:SourceVpce": "vpce-xxxxxxxxxxx"
                      }
                  }
              }
          ]
      }
      
    • メリット: リージョナルエンドポイントを使うので、STSの推奨プラクティスに沿うことができる。
    • デメリット: クロスアカウント先のアカウントBが社外アカウントのため、調整の手間とリードタイムがある。
  3. 設定でデフォルトエンドポイントを変更する
    • AWSのconfig fileのsts_regional_endpoints値や環境変数AWS_STS_REGIONAL_ENDPOINTS値を指定することで、グローバルエンドポイントを使用するように変更できる
    • デメリット: 同じECSタスク定義で行う他の処理に影響する可能性がある
  4. VPC Endpointを使用しない
    • VPC Endpointを使わなければ、リクエストはパブリックなインターネット経由、つまり今回の環境だとNAT Gateway経由になります。
    • デメリット: VPCのネットワーク設定の大きな変更となり、ECSタスク以外にも影響が出る可能性があります。更に、今回の環境では特定の機能を使うためにVPC Endpointを使用している背景があり、この手段は取れませんでした。

今回は

  • 日次バッチが落ちてしまい、急ぎ修正を行う必要があったこと
  • 他の処理への影響範囲を最小限にしたいこと
  • 本処理が日次バッチであり、過度に耐障害性やレスポンスを気にする必要がないこと

などから、「STSリクエストはリージョナルエンドポイントを推奨」というAWSの方針とは異なるものの、一次対応として1を選択しました。
ただし、理想的には2の対応をすべきなので、社外と調整を進めていく予定です。

最後に

古めのシステムや環境をアップグレードするときは、関連するライブラリで大きな挙動の変更がないか確認しましょう。
AWS環境での障害の原因調査にはCloudTrailが便利でした。皆さんもぜひ使ってみてください。

参考

Finatext Tech Blog

Discussion