Skip to main content

IAM Roles for Service Accounts (IRSA)

IRSA binds a Kubernetes ServiceAccount to an IAM Role using the cluster's OIDC provider. This eliminates the need for EC2 instance profiles or long-lived credentials, giving each pod exactly the AWS permissions it needs — nothing more.

How IRSA Works

Pod → ServiceAccount → IAM Role → AWS API
↑ ↑
k8s annotation Trust policy
eks.amazonaws.com with OIDC condition
/role-arn
  1. EKS creates an OIDC provider for the cluster
  2. The IAM role's trust policy allows the OIDC provider to assume it, scoped to a specific namespace and service account name
  3. The amazon-eks-pod-identity-webhook (built into EKS) injects a projected token into the pod
  4. The AWS SDK exchanges that token for temporary credentials via STS

Creating an IRSA Role (Terraform)

terraform/environments/production/irsa-payments.tf
module "payments_api_irsa" {
source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
version = "~> 5.0"

role_name = "payments-api-production"

oidc_providers = {
cluster = {
provider_arn = module.eks.oidc_provider_arn
namespace_service_accounts = ["team-payments:payments-api"]
}
}
}

resource "aws_iam_policy" "payments_api" {
name = "payments-api-production"

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"]
Resource = ["arn:aws:secretsmanager:us-east-1:${data.aws_caller_identity.current.account_id}:secret:/payments/*"]
},
{
Effect = "Allow"
Action = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"]
Resource = ["arn:aws:s3:::payments-receipts-production/*"]
}
]
})
}

resource "aws_iam_role_policy_attachment" "payments_api" {
role = module.payments_api_irsa.iam_role_name
policy_arn = aws_iam_policy.payments_api.arn
}

Annotating the ServiceAccount

kubernetes/namespaces/team-payments/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: payments-api
namespace: team-payments
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/payments-api-production
# Token expiry — default 86400s (24h), minimum 600s
eks.amazonaws.com/token-expiration: "43200"
automountServiceAccountToken: true

Verifying IRSA Is Working

# Exec into the pod and test AWS access
kubectl exec -it deploy/payments-api -n team-payments -- aws sts get-caller-identity

Expected output:

{
"UserId": "AROA...:botocore-session-...",
"Account": "123456789012",
"Arn": "arn:aws:sts::123456789012:assumed-role/payments-api-production/..."
}

IRSA for Platform Components

DevOpsGenie creates IRSA roles automatically for all platform components during devopsgenie platform install:

ComponentIAM Permissions
aws-load-balancer-controllerEC2, ELB, WAF read/write
aws-ebs-csi-driverEC2 volume create/attach/delete
cluster-autoscalerEC2 Auto Scaling read/write
karpenterEC2 instance run/terminate, IAM pass-role
external-secrets-operatorSecrets Manager, SSM Parameter Store read
veleroS3 read/write for backup storage

Common Issues

Pod can't assume role — NoCredentialProviders

  • Confirm the ServiceAccount annotation is correct
  • Check the IAM trust policy includes the correct OIDC provider ARN, namespace, and service account name
  • Ensure automountServiceAccountToken: true is set

Token expired

  • Default expiry is 24h; the webhook automatically rotates it every 12h
  • For short-lived workloads, set eks.amazonaws.com/token-expiration: "600" (10 minutes minimum)
# Debug IRSA by checking projected volume
kubectl describe pod <pod-name> -n <namespace> | grep -A 5 "aws-iam-token"