From IRSA to EKS Pod Identity: What We Learned

When we built a fresh multi-environment EKS platform, we had to decide how workloads would receive AWS credentials.
IAM Roles for Service Accounts (IRSA) had been the standard answer for years. It works, it is well understood, and our existing cluster already used it. But a new cluster was an opportunity to reconsider how much identity configuration needed to live in IAM trust policies, Terraform, Helm values, and Kubernetes ServiceAccount annotations at the same time.
We chose EKS Pod Identity for the new workload integrations and migrated several existing controllers and applications to the same pattern.
The result was less cluster-specific IAM, fewer AWS details in GitOps configuration, and a cleaner boundary between the platform and its workloads. The migration also corrected some assumptions we had made about credential precedence and exposed stale permissions that were easier to remove than carry forward.
This is the pattern we ended up using and the parts that deserved more attention than the happy-path documentation suggests.
IRSA and Pod Identity Solve the Same Problem Differently
Both mechanisms give a pod temporary AWS credentials based on its Kubernetes ServiceAccount. The difference is where the trust relationship and binding live.
With IRSA, each EKS cluster has an IAM OIDC provider. The workload role trusts that provider and normally restricts access with conditions for the token audience and the exact system:serviceaccount:<namespace>:<name> subject. The ServiceAccount then carries the role ARN:
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
namespace: platform
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::<account-id>:role/external-dns
The role trust policy contains the cluster’s OIDC issuer URL. Recreating the cluster or adding another cluster means adding another federated trust relationship or creating another role.
Pod Identity changes that path. The role trusts the EKS Pod Identity service principal, while an association in the EKS API binds a cluster, namespace, and ServiceAccount to that role. The Kubernetes ServiceAccount needs no AWS annotation.

The two paths can be summarized as:
IRSA
Pod -> projected OIDC token -> STS web identity -> IAM role
Pod Identity
Pod -> Pod Identity Agent -> EKS Auth API -> IAM role
The agent runs on each supported node. For a new pod using an associated ServiceAccount, EKS injects the environment and token data used by the AWS SDK’s container credential provider. The agent exchanges that token through the EKS Auth API and returns temporary role credentials.
AWS documents the full flow in Understand how EKS Pod Identity works.
Why the New Model Fit Better
The first improvement was reusable role trust.
An IRSA role names a specific cluster issuer in its trust policy. A Pod Identity role instead trusts pods.eks.amazonaws.com. The role trust is no longer coupled to an OIDC issuer URL, although the association itself remains specific to a cluster, namespace, and ServiceAccount.
The second improvement was keeping the binding out of Helm values. Under IRSA, a chart often needs a role ARN so it can annotate the ServiceAccount. That pushes an AWS infrastructure detail through the GitOps configuration even though Terraform created the role.
With Pod Identity, Terraform manages the association through the EKS API. The chart only needs to create or reference the correctly named ServiceAccount.
We also no longer needed to create and maintain an IAM OIDC provider for Pod Identity consumers. That removed the issuer and certificate-thumbprint plumbing we had previously built around a tls_certificate data source.
Finally, EKS managed add-ons can expose Pod Identity as part of the add-on configuration. The EBS CSI driver was a good example: its identity association could be managed next to the add-on instead of passed through a Kubernetes annotation.
The Terraform Pattern
Each workload received a small Terraform module containing its trust policy, IAM role, scoped permissions, and Pod Identity association.
The core trust and association are intentionally plain:
data "aws_iam_policy_document" "pod_identity" {
statement {
actions = [
"sts:AssumeRole",
"sts:TagSession",
]
principals {
type = "Service"
identifiers = ["pods.eks.amazonaws.com"]
}
}
}
resource "aws_iam_role" "workload" {
name = "example-workload"
assume_role_policy = data.aws_iam_policy_document.pod_identity.json
}
resource "aws_eks_pod_identity_association" "workload" {
cluster_name = var.cluster_name
namespace = var.namespace
service_account = var.service_account
role_arn = aws_iam_role.workload.arn
depends_on = [
aws_eks_addon.pod_identity_agent,
aws_iam_role_policy_attachment.workload,
]
}
The trust policy is generic. The association carries the Kubernetes identity.
For application workloads deployed into several namespaces, one role can have several exact associations:
locals {
workload_associations = {
development = {
namespace = "app-development"
service_account = "app-development"
}
staging = {
namespace = "app-staging"
service_account = "app-staging"
}
}
}
resource "aws_eks_pod_identity_association" "workload" {
for_each = local.workload_associations
cluster_name = var.cluster_name
namespace = each.value.namespace
service_account = each.value.service_account
role_arn = aws_iam_role.workload.arn
}
This worked well for charts whose ServiceAccount names included the environment. It also made name mismatches visible in Terraform review instead of hiding them in a templated annotation.
For the EBS CSI managed add-on, the association sits directly on the add-on resource:
resource "aws_eks_addon" "pod_identity_agent" {
cluster_name = var.cluster_name
addon_name = "eks-pod-identity-agent"
}
resource "aws_eks_addon" "ebs_csi" {
cluster_name = var.cluster_name
addon_name = "aws-ebs-csi-driver"
pod_identity_association {
role_arn = aws_iam_role.ebs_csi.arn
service_account = "ebs-csi-controller-sa"
}
depends_on = [
aws_eks_addon.pod_identity_agent,
aws_iam_role_policy_attachment.ebs_csi,
]
}
The exact add-on support and recommended ServiceAccount can be inspected through the EKS API. Terraform’s aws_eks_addon resource supports the same association block.
Migration Was Also a Permission Audit
We migrated platform controllers that needed access to services such as Route53, ACM, ECR, ELB, S3, and KMS. We also moved application workloads away from a legacy shared IRSA role.
The tempting migration would have been to copy each existing policy, change the trust relationship, and declare success. We instead treated the move as a least-privilege review.
One application role included broad Secrets Manager access because its SDK configuration declared a Secrets Manager client. Reviewing the real call paths and AWS activity showed that the application never used it. The new Pod Identity role omitted that permission.
Identity migrations are useful cleanup points because every retained action has to be justified. A working legacy policy is evidence that the application can run with those permissions; it is not evidence that every permission is necessary.
Credential Precedence Was the Important Surprise
Our initial assumption was that adding a Pod Identity association would make new pods prefer Pod Identity, even while the IRSA annotation remained.
That assumption was wrong.
Pod Identity is part of the AWS SDK container credential provider. IRSA uses the web identity provider, which appears earlier in the default credential chain. AWS explicitly states that credentials earlier in the chain continue to be used after a Pod Identity association is added.
That behavior is useful because it creates a safe overlap:
- Install the Pod Identity Agent.
- Confirm the workload uses an SDK version that supports Pod Identity.
- Create the Pod Identity role and association.
- Leave the IRSA annotation in place while reviewing the infrastructure.
- Remove the annotation.
- Recreate the pods.
- Verify the role used by the new pods.
Existing pods do not switch credential providers in place. They retain the environment, projected tokens, and credential behavior injected when they were created. Removing an annotation from a ServiceAccount is therefore not enough; the workload must roll.
A practical verification sequence looks like:
kubectl annotate serviceaccount example \
--namespace app-development \
eks.amazonaws.com/role-arn-
kubectl rollout restart deployment example \
--namespace app-development
kubectl exec deployment/example \
--namespace app-development \
-- aws sts get-caller-identity
The last command assumes the application image contains the AWS CLI. Otherwise, use a temporary diagnostic pod with the same ServiceAccount and a supported AWS CLI or SDK.
Only after the returned ARN matches the Pod Identity role should the legacy IRSA role or trust statement be removed. The cluster OIDC provider can be removed only after an inventory confirms that nothing still relies on IRSA.
Exact Names and Deployment Ordering Matter
An association matches one exact namespace and ServiceAccount name. app-staging/app-staging and app-staging/app are different identities.
If the names do not match, EKS does not inject Pod Identity credentials. The SDK may then reach the node role if access to the instance metadata service is available; otherwise AWS authentication fails. Restricting pod access to node credentials remains an important defense against this kind of configuration error.
Terraform dependencies help order the agent, IAM policy attachments, and associations. They do not automatically coordinate an independent GitOps controller. Our operational rule was to apply and verify the identity infrastructure before allowing GitOps to roll the workload.
Terminology also drifted during the migration. Outputs and variable descriptions that still said “IRSA role ARN” made the new model harder to understand even when the resources were correct. Names and comments should describe the current behavior, not preserve the history of how it was implemented.
Pod Identity Does Not Replace Every OIDC Use
This change applies to workloads running inside EKS. It does not replace GitHub Actions federation into AWS.
GitHub Actions still uses its own OIDC issuer and IAM trust conditions. That is a separate boundary: an external CI system requesting AWS credentials, not an EKS pod receiving credentials through the EKS Auth API.
Calling both mechanisms “OIDC” without naming the caller makes migration discussions unnecessarily confusing. We now distinguish between workload identity inside EKS and CI federation from GitHub.
Current Limits
Pod Identity is not universal. According to AWS’s current Pod Identity restrictions, it supports pods on Linux EC2 worker nodes. It does not support Fargate pods, Windows pods, Outposts, EKS Anywhere, or Kubernetes clusters running outside EKS.
Each ServiceAccount can have one association, and a cluster supports up to 5,000 associations. The associated Pod Identity role must be in the same AWS account as the EKS cluster.
Cross-account access is still possible. AWS supports an optional target role in another account, with the same-account Pod Identity role acting as the first role in the chain. The target IAM role documentation covers that flow.
IRSA therefore remains relevant for unsupported compute types, older SDKs, and environments outside the Pod Identity support boundary. Existing IRSA installations also do not need to migrate only for novelty; the operational simplification should justify the change.
Migration Checklist
- Install the
eks-pod-identity-agentadd-on. - Confirm the workload’s AWS SDK supports Pod Identity.
- Create a least-privilege role trusted by
pods.eks.amazonaws.com. - Create the exact cluster, namespace, and ServiceAccount association.
- Keep the IRSA annotation during the overlap period.
- Remove the annotation and recreate the pods.
- Verify the caller with
aws sts get-caller-identity. - Monitor the workload’s real AWS API calls.
- Delete unused IRSA roles and policies only after verification.
- Remove the cluster OIDC provider only when no IRSA consumers remain.
The Takeaway
Pod Identity did not change what our workloads were allowed to do. It changed how that permission was connected to Kubernetes.
The reusable service-principal trust policy reduced cluster-specific IAM. Terraform associations kept role ARNs out of Helm values. Managed add-ons fit into the same model. Most importantly, the migration created a reason to re-check permissions instead of reproducing old access automatically.
The safest migration path also turned out to be the opposite of our first assumption: create the association while IRSA still wins, then remove the annotation and recreate the pods when ready to switch.
That detail is small, but it is the difference between a controlled identity migration and merely hoping the SDK chooses the credential source you intended.