IRSAのIAM管理が地獄になったのでEKS Pod Identityに移行した話
OIDCプロバイダーのARN管理やクラスター再作成のたびにIAMポリシーを書き直す辛さ、経験ありませんか?本番移行1年で気づいたPod Identityの落とし穴と設計のコツを共有します。
IRSAでずっと戦ってた僕がPod Identityに移行した経緯
先日、チームのEKSクラスターで認証周りの大規模リファクタリングをやり切ったので、その知見を残しておく。正直最初は「またAWSが新しいものを出した」くらいの温度感だったんだけど、実際に移行してみたらIRSA(IAM Roles for Service Accounts)のときに抱えていた悩みの大半が消えた。
2024年末ごろからEKS Pod Identity AssociationsがGAになり、2025〜2026年にかけてAWSも「新規クラスターはPod Identityを使ってくれ」というスタンスを明確にしてきた。うちのチームでも既存クラスターのIRSA設定が増殖しすぎてIAMポリシーの管理が地獄になっていたので、2025年の年初から移行プロジェクトをスタートさせた。
当時の課題はこんな感じだった。箇条書きにすると短く見えるけど、どれも毎週じわじわ消耗させてくるタイプの辛さだった。
- OIDCプロバイダーのARNをポリシーのConditionに毎回書かなければいけない
- クラスターを再作成するとOIDC IDが変わってIAMポリシーを全部書き直す羽目になる
- マルチアカウント構成でクロスアカウントロールの信頼ポリシーが複雑になる
- ServiceAccountとIAMロールのマッピングをTerraformで管理しているのに、どこで何を設定しているか追いにくい
Pod Identityに移行してから1年、実際の運用で気づいたことを書いていく。
IRSAとPod Identityの本質的な違い
技術的な仕組みの差を整理してみる。IRSAはOIDCフェデレーションを使っていて、KubernetesのServiceAccountトークンをAWS STSに渡してIAMロールを取得する仕組みだ。これはこれでよく考えられているんだけど、OIDCプロバイダーとIAMロールの信頼ポリシーを常に一緒に管理しなければいけない点が辛かった。
Pod Identityは仕組みが根本的に違う。EKS Pod Identity AgentがDaemonSetとしてノードに配置されていて、Podがリンクローカル169.254.170.23にアクセスすると認証情報を返してくれる。信頼ポリシーにOIDCのARNを書く必要がなく、pods.eks.amazonaws.comというサービスプリンシパルだけで完結する。
2つの信頼ポリシーを並べると、差がよくわかる。
// IRSAの信頼ポリシー(こんなの各クラスターで書いてた)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.ap-northeast-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.ap-northeast-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E:sub": "system:serviceaccount:production:my-service"
}
}
}
]
}
// Pod Identityの信頼ポリシー(こっちはシンプル)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "pods.eks.amazonaws.com"
},
"Action": [
"sts:AssumeRole",
"sts:TagSession"
]
}
]
}
このシンプルさは地味に助かった。IAMロールを複数のクラスターや複数のNamespaceで使いまわせるし、クラスターを再作成してもIAMポリシーを触らなくていい。
Pod Identity AssociationはAWS側で作成する。CDKで書くとこんな感じになる。
import * as eks from 'aws-cdk-lib/aws-eks';
import * as iam from 'aws-cdk-lib/aws-iam';
// IAMロールはサービスプリンシパルだけでOK
const podRole = new iam.Role(this, 'MyServiceRole', {
assumedBy: new iam.ServicePrincipal('pods.eks.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonS3ReadOnlyAccess'),
],
});
// Pod Identity Associationを作成
new eks.PodIdentityAssociation(this, 'MyServicePodIdentity', {
cluster: cluster,
namespace: 'production',
serviceAccount: 'my-service',
role: podRole,
});
Kubernetes側のServiceAccountにはアノテーション不要。これが意外とでかい。IRSAだとeks.amazonaws.com/role-arnのアノテーションをServiceAccountに書かないといけなくて、Helmチャートを書くたびに「このAnnotationどこに書くんだっけ?」ってなってた。
# IRSAのとき(アノテーション必須)
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-service
namespace: production
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/my-service-role
---
# Pod Identityのとき(アノテーション不要)
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-service
namespace: production
2つを比較するとこうなる。
| 比較項目 | IRSA | Pod Identity |
|---|---|---|
| OIDCプロバイダー管理 | クラスターごとに必要 | 不要 |
| 信頼ポリシーのシンプルさ | クラスターARN依存 | サービスプリンシパルのみ |
| ServiceAccountアノテーション | 必要 | 不要 |
| クラスター再作成時の影響 | IAM更新必要 | 影響なし |
| マルチクラスター共有 | 困難 | 容易 |
| EKS Fargate対応 | 対応 | 2026年時点で非対応 |
| 対応Kubernetesバージョン | 1.13以降 | 1.24以降 |
本番で作ったアーキテクチャ構成
うちのチームは複数のAWSアカウントを使っているので、マルチアカウント対応の構成にした。全体像はこんな感じだ。
graph TB
subgraph MgmtAccount["管理アカウント"]
IAMRole1["shared-s3-reader-role\n(pods.eks.amazonaws.com信頼)"]
IAMRole2["shared-dynamodb-role\n(pods.eks.amazonaws.com信頼)"]
end
subgraph ProdAccount["本番アカウント"]
subgraph VPC["VPC (10.0.0.0/16)"]
subgraph AZ1["AZ: ap-northeast-1a"]
subgraph NG1["NodeGroup-1"]
Agent1["Pod Identity Agent\n(DaemonSet)"]
Pod1["api-service Pod"]
Pod2["worker Pod"]
end
end
subgraph AZ2["AZ: ap-northeast-1c"]
subgraph NG2["NodeGroup-2"]
Agent2["Pod Identity Agent\n(DaemonSet)"]
Pod3["api-service Pod"]
end
end
subgraph NS_prod["Namespace: production"]
SA1["ServiceAccount: api-service"]
SA2["ServiceAccount: worker"]
end
end
EKSCluster["EKS Cluster"]
PIA1["PodIdentityAssociation\napi-service → shared-s3-reader-role"]
PIA2["PodIdentityAssociation\nworker → shared-dynamodb-role"]
end
subgraph AWSServices["AWSサービス"]
S3["S3 Bucket"]
DDB["DynamoDB"]
STS["AWS STS"]
end
Pod1 -->|"169.254.170.23へリクエスト"| Agent1
Agent1 -->|"STSでロール取得"| STS
STS -->|"一時認証情報"| Agent1
Agent1 -->|"認証情報をPodに返す"| Pod1
Pod1 -->|"S3アクセス"| S3
Pod2 -->|"169.254.170.23へリクエスト"| Agent1
Agent1 --> STS
Pod2 -->|"DynamoDBアクセス"| DDB
EKSCluster --> PIA1
EKSCluster --> PIA2
PIA1 -->|"参照"| IAMRole1
PIA2 -->|"参照"| IAMRole2
SA1 -->|"紐付け"| PIA1
SA2 -->|"紐付け"| PIA2
マルチアカウントの場合、IAMロールは管理アカウントや共有アカウントに置いて、本番アカウントのEKSクラスターからクロスアカウントでAssumeRoleする構成にした。Pod Identityは信頼ポリシーがシンプルなので、クロスアカウントのセットアップもIRSAよりはるかに楽だった。
Terraformで書くとこんな感じになる。
# 管理アカウント側:IAMロールの作成
resource "aws_iam_role" "shared_s3_reader" {
name = "shared-s3-reader-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Service = "pods.eks.amazonaws.com"
}
Action = [
"sts:AssumeRole",
"sts:TagSession"
]
}
]
})
}
resource "aws_iam_role_policy_attachment" "s3_read" {
role = aws_iam_role.shared_s3_reader.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}
# 本番アカウント側:Pod Identity Associationの作成
resource "aws_eks_pod_identity_association" "api_service" {
cluster_name = aws_eks_cluster.main.name
namespace = "production"
service_account = "api-service"
role_arn = "arn:aws:iam::MGMT_ACCOUNT_ID:role/shared-s3-reader-role"
tags = {
Environment = "production"
Service = "api-service"
}
}
実際に動かしてみると、Pod内からAWS SDKを使ったときの挙動も変わっている。認証情報の取得先URLが169.254.170.23/v1/credentialsになっていて、IMDSv2とは別のエンドポイントを使う。
import boto3
import json
import urllib.request
# Pod Identity の認証情報エンドポイントを確認
try:
req = urllib.request.Request(
'http://169.254.170.23/v1/credentials',
headers={'Authorization': open('/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/token').read()}
)
with urllib.request.urlopen(req, timeout=2) as response:
creds = json.loads(response.read())
print(f"RoleArn: {creds.get('AccountId')}")
except Exception as e:
print(f"Not using Pod Identity: {e}")
# 通常のSDK利用はこれだけでOK(認証情報プロバイダーチェーンが自動で拾う)
client = boto3.client('s3', region_name='ap-northeast-1')
buckets = client.list_buckets()
print(f"Accessible buckets: {len(buckets['Buckets'])}")
移行時に踏んだ落とし穴と対処法
1年運用して、正直けっこうハマったポイントがある。順番に書いていく。
1. Pod Identity Agentアドオンの更新を忘れた
EKS Pod Identity Agentはアドオンとして管理されているので、クラスターのKubernetesバージョンアップとは別に更新が必要だ。2026年現在の最新はv1.3.x系だが、うちのクラスターは数ヶ月更新を忘れていて、新しいPodIdentityAssociationを作ったときに動作が不安定になったことがあった。「なんか認証情報が取れないPodがある」という障害調査に1時間費やして、原因がアドオンのバージョン差異だったときは虚脱感がすごかった。
# アドオンのバージョン確認
aws eks describe-addon \
--cluster-name my-cluster \
--addon-name eks-pod-identity-agent \
--query 'addon.addonVersion'
# 利用可能な最新バージョンを確認
aws eks describe-addon-versions \
--addon-name eks-pod-identity-agent \
--kubernetes-version 1.31 \
--query 'addons[0].addonVersions[0].addonVersion'
# アドオンを最新にアップデート
aws eks update-addon \
--cluster-name my-cluster \
--addon-name eks-pod-identity-agent \
--addon-version v1.3.5-eksbuild.1
2. Fargateには使えない
うちのチームはバッチ処理の一部をFargate on EKSで動かしていたのだが、2026年8月時点でPod IdentityはFargateに非対応だった。Fargateを使っているPodはIRSAのままにしておく必要があって、「EC2はPod Identity、FargateはIRSA」という混在状態になる。個人的にはこれが一番ストレスで、チームメンバーが混乱しないようにドキュメント整備が大変だった。コンテナセキュリティ全般についてはコンテナセキュリティ完全ガイド2026でも書いているので参考にしてほしい。
3. PodIdentityAssociationの上限に引っかかった
デフォルトではクラスターあたり100個のPodIdentityAssociationしか作れない。マイクロサービスが多いチームだとけっこう早めに上限に近づく。ServiceAccountの粒度設計をしっかりやっておかないと後から詰む。
うちのチームでは「同じIAMロールが必要なサービスはServiceAccountを共有する」方針にした。S3の読み取り専用ロールを使うサービスが5個あったとしても、同じs3-readerというServiceAccountを使って1個のAssociationで済ませる設計にしている。
# 複数のDeploymentが同じServiceAccountを使う
apiVersion: v1
kind: ServiceAccount
metadata:
name: s3-reader # このSAを複数のDeploymentで共有
namespace: production
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: service-a
spec:
template:
spec:
serviceAccountName: s3-reader # 共有
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: service-b
spec:
template:
spec:
serviceAccountName: s3-reader # 同じSAを使う
4. コンテキストタグの活用を知らなかった
これは落とし穴というより「最初から知っておけばよかった」系の話だ。Pod Identityはセッションタグを自動的に付与してくれる。kubernetes-namespace、kubernetes-service-account、eks-cluster-nameといったタグが認証情報のセッションに含まれるので、これをIAMポリシーの条件に使えば属性ベースのアクセス制御ができる。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"StringEquals": {
"aws:PrincipalTag/kubernetes-namespace": "production",
"aws:PrincipalTag/kubernetes-service-account": "api-service"
}
}
}
]
}
これを使うと、同じIAMロールでもNamespaceやServiceAccountによってアクセスを制限できる。最初は知らなくて、全部別々のIAMロールを作ろうとしていた。気づいてから設計が劇的に整理できたのでマジで助かった。
1年運用して見えてきたベストプラクティス
導入してから見えてきた設計原則をまとめる。正直まだ試行錯誤中の部分もあるけど、今の段階でチームに共有できる知見はこのあたりだ。
最小権限をNamespace単位で徹底する
IRSA時代に「とりあえず全Namespaceに同じロール」みたいな運用をしてしまっていたのを反省して、Pod IdentityはNamespaceを必ず指定するようにした。AssociationにNamespaceを指定することで、別Namespaceの同名ServiceAccountが誤って使えてしまうリスクを防げる。
# 正しい設定:Namespace + ServiceAccountの組み合わせ
aws eks create-pod-identity-association \
--cluster-name my-cluster \
--namespace production \
--service-account api-service \
--role-arn arn:aws:iam::123456789012:role/api-service-role
# Associationの一覧確認
aws eks list-pod-identity-associations \
--cluster-name my-cluster \
--query 'associations[*].{NS:namespace,SA:serviceAccount,Role:roleArn}' \
--output table
変更管理はTerraformかCDKで一元管理
PodIdentityAssociationをAWSコンソールやCLIで直接作ると管理が追いにくくなる。うちのチームはTerraformで全て管理していて、eks_pod_identity_associationリソースの一覧がそのままServiceAccountとIAMロールのマッピングドキュメントになっている。インフラのState管理についてはTerraformで3年分の失敗から学んだ、State管理とAI検証の正解も参考になる。
CloudTrailでPod Identityのアクセスログを見る
CloudTrailのログにはセッションタグが含まれているので、どのPodがどのAWSリソースにアクセスしたか追跡できる。Athenaでクエリをかけると便利だった。
-- どのServiceAccountがS3にアクセスしたか確認
SELECT
useridentity.sessioncontext.sessionissuer.arn AS role_arn,
useridentity.sessioncontext.attributes.principaltags['kubernetes-service-account'] AS service_account,
useridentity.sessioncontext.attributes.principaltags['kubernetes-namespace'] AS namespace,
eventsource,
eventname,
COUNT(*) AS count
FROM cloudtrail_logs
WHERE
useridentity.type = 'AssumedRole'
AND useridentity.sessioncontext.attributes.principaltags['kubernetes-service-account'] IS NOT NULL
AND eventtime > '2026-07-01'
GROUP BY 1, 2, 3, 4, 5
ORDER BY count DESC
LIMIT 20;
移行のパフォーマンス面でも確認をしておいた。IRSAとPod Identityで認証情報取得のレイテンシに差があるか気になっていたので簡単に計測した結果がこれだ。
xychart-beta
title "認証情報取得レイテンシ比較 (ms, p50/p95/p99)"
x-axis ["IRSA p50", "IRSA p95", "IRSA p99", "PodIdentity p50", "PodIdentity p95", "PodIdentity p99"]
y-axis "レイテンシ (ms)" 0 --> 300
bar [45, 120, 280, 8, 25, 60]
Pod IdentityはDaemonSetがノードにいるので、STSへのラウンドトリップが入らない分レイテンシが大幅に低い。特にCold Start直後の差が顕著で、IRSAだとp99が280msくらいかかっていたのがPod Identityでは60msになった。Lambda SnapStartほどではないが、EKS Auto Mode完全ガイド2026と組み合わせるとノード起動後のPod初期化全体が速くなる。
EKSのコスト最適化については別記事EKS コスト最適化2026|Spot/On-Demand戦略とKarpenter活用でまとめているので合わせて読んでほしい。
まとめ
1年間Pod Identityを本番運用してきた結論を書く。
| 観点 | 結果 |
|---|---|
| IRSA依存の脱却 | OIDCプロバイダー管理が消え、クラスター再作成時のIAM更新作業がゼロに。運用負荷は体感で3割減 |
| 信頼ポリシーの管理 | pods.eks.amazonaws.comを信頼するだけ。マルチクラスター・クロスアカウント設計が楽に組める |
| IAMロールの爆発抑制 | コンテキストタグを使ったABACでロール数の増加を抑制できた |
| Fargate対応 | 現時点で非対応。混在環境になる場合はドキュメントとOnboarding設計が重要 |
| アドオン管理 | Pod Identity AgentのバージョンはKubernetesアップとは別に管理が必要 |
次のアクション
- まずは新規Namespaceや新規サービスからPod Identityで始めてみる(既存のIRSA設定は壊さずに並行運用できる)
aws eks list-pod-identity-associationsでマッピングを一覧化してドキュメント代わりにする- CloudTrailのセッションタグを使ったアクセスログ分析を仕込んで、不要な権限の棚卸しをやってみる
皆さんのチームはまだIRSAをメインで使っている?Fargate非対応で移行に踏み切れていないケースがあれば、そのあたりは今後もフォローしていきたいと思っている。