1. 项目概述:这不是SDK的问题,是环境、权限与上下文的三重错位
“k8s中通过aws sdk访问s3遇到的坑”——这句话在某云原生技术群出现频率极高,几乎每周都有至少3位开发者发问。我第一次看到它时正在调试一个日志归档服务,Pod里跑着Java应用,用的是AWS SDK v2,S3客户端配置了DefaultCredentialsProviderChain,本地aws configure好使,一上K8s就报Unable to load credentials from any provider in the chain。当时以为是SDK版本问题,降级、升级、换依赖全试了一遍,三天后才发现:根本不是SDK的锅,而是我们把“本地开发逻辑”直接平移进了容器世界,却忘了K8s里没有.aws/credentials文件,没有~/.aws目录,甚至没有“当前用户”这个概念。
这个标题背后藏着三个被严重低估的底层事实:第一,K8s Pod的执行上下文和本地终端完全不同,它不继承宿主机的环境变量、文件系统、IAM角色;第二,AWS SDK的凭据加载链(Credential Provider Chain)在容器内默认失效路径远多于生效路径;第三,所谓“坑”,90%以上不是SDK Bug,而是开发者对K8s安全模型、AWS IAM角色绑定机制、以及SDK初始化时机这三者的交叉理解缺失所致。它适合两类人深度阅读:一类是刚从单机部署转向K8s的后端/运维工程师,另一类是已用过ServiceAccount Annotation但总在跨命名空间或IRSA切换时掉链子的进阶使用者。你不需要精通Go或Java,但得知道Pod YAML怎么写、kubectl怎么查事件、以及aws sts get-caller-identity返回的是谁的身份。接下来我会用真实调试记录还原整个排查链条,不讲抽象原理,只说“你打开终端后该敲哪几行命令”。
2. 内容整体设计与思路拆解:为什么必须放弃“本地思维”,转而构建“声明式信任链”
2.1 传统方案失效的底层原因:凭据链在容器里断在哪一环?
AWS SDK(以Java v2为例)的DefaultCredentialsProviderChain默认按以下顺序尝试加载凭据:
SystemPropertyCredentialsProvider(JVM系统属性)EnvironmentVariableCredentialsProvider(AWS_ACCESS_KEY_ID等环境变量)WebIdentityTokenFileCredentialsProvider(用于IRSA,读取/var/run/secrets/eks.amazonaws.com/serviceaccount/token)ProfileCredentialsProvider(读取~/.aws/credentials)ContainerCredentialsProvider(ECS容器元数据端点)InstanceProfileCredentialsProvider(EC2实例元数据端点)
在K8s中,第4项(Profile)必然失败——容器里没有~/.aws;第5项(Container)只在ECS有效,K8s不提供该端点;第6项(Instance Profile)看似可行,但前提是Pod运行在EC2节点上,且节点IAM角色有S3权限——这等于把整个集群的权限都绑死在节点级,违背最小权限原则,且无法实现Pod粒度的权限隔离。所以真正能用的只剩第2项(环境变量)和第3项(IRSA)。而环境变量方案的问题在于:Access Key明文注入Pod,违反安全基线;一旦密钥轮换,所有Pod需重启;且无法做细粒度策略控制(比如只允许读取logs-bucket/2024/06/前缀)。因此,IRSA(IAM Roles for Service Accounts)成为唯一符合云原生安全规范的正解——但它不是开箱即用的,需要K8s集群启用OIDC Provider、ServiceAccount绑定IAM Role、Pod正确挂载token卷,三者缺一不可。
2.2 IRSA为何是唯一可扩展方案?看它如何重构信任传递路径
IRSA的本质,是把AWS IAM的信任关系从“机器身份”(EC2 Instance Profile)升级为“服务身份”(K8s ServiceAccount)。它的信任链是这样的:
Pod → K8s API Server(验证ServiceAccount) ↓ K8s API Server → AWS STS(用OIDC Token交换临时凭证) ↓ AWS STS → 返回临时AccessKey/SecretKey/SessionToken给Pod这个过程的关键在于:K8s API Server作为可信中介,用其OIDC Issuer URL(如https://oidc.eks.us-west-2.amazonaws.com/id/ABCD1234567890)向AWS STS证明“这个token确实是我签发的”。AWS STS则根据预先配置的OIDC Provider(在IAM控制台创建)来校验签名。一旦校验通过,STS就颁发一个有效期1小时的临时凭证,SDK自动刷新。这意味着:
- 不需要在Pod里存任何长期密钥;
- 每个ServiceAccount可绑定不同IAM Role,实现Pod级权限隔离;
- 权限变更只需更新IAM Policy,无需重启Pod;
- 审计日志里能看到具体是哪个ServiceAccount调用了S3,而非笼统的“EC2节点”。
我曾在一个金融客户项目里用环境变量方案上线,结果审计团队直接否决——因为密钥轮换流程无法自动化,且无法追溯到具体微服务。切换IRSA后,他们只用了15分钟就完成了合规检查。这不是技术炫技,而是生产环境的硬性门槛。
2.3 方案选型决策树:什么情况下该用IRSA?什么情况下可以妥协?
并非所有场景都必须上IRSA。我画了一个实操决策树,基于过去12个K8s+S3集成项目的踩坑经验:
| 场景描述 | 推荐方案 | 关键理由 | 风险提示 |
|---|---|---|---|
| 开发/测试环境,快速验证逻辑 | 环境变量(AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY) | 启动快,无需配置OIDC,适合CI流水线中的临时Job | 绝对禁止用于生产;密钥需用K8s Secret管理,不可硬编码在YAML里 |
| 生产环境,EKS集群,需Pod级权限隔离 | IRSA(强制) | 符合AWS Well-Architected安全支柱;支持自动凭证轮换;审计友好 | 必须确保集群OIDC已启用;ServiceAccount annotation格式必须严格匹配IAM Role ARN |
| 生产环境,自建K8s(非EKS),无OIDC Provider | EC2节点角色 + 节点亲和性(NodeAffinity) | 避免改造集群,利用现有EC2权限体系 | 权限颗粒度粗(整个节点共享同一角色);无法做Pod级审计;若节点被攻破,风险扩散 |
| 边缘计算场景(K3s/RKE2),资源受限 | IRSA轻量版:手动挂载token文件 + 自定义Provider | K3s 1.25+原生支持OIDC,但需手动配置--oidc-issuer-url | 配置复杂度高;需自行维护token刷新逻辑(SDK v2已内置,v1需额外处理) |
提示:如果你的集群是EKS,且创建时间在2022年之后,OIDC Provider大概率已自动启用。用
aws eks describe-cluster --name your-cluster --query "cluster.identity.oidc.issuer" --output text即可确认。别猜,直接查。
3. 核心细节解析与实操要点:IRSA配置的7个致命细节,错一个就白配
3.1 OIDC Provider配置:不是“创建就行”,而是“ARN必须精确匹配”
很多开发者卡在第一步:在IAM控制台创建OIDC Provider时,填入的Issuer URL和Audience值出错。常见错误包括:
- Issuer URL末尾带斜杠:
https://oidc.eks.us-west-2.amazonaws.com/id/ABCD1234567890/(错误) vshttps://oidc.eks.us-west-2.amazonaws.com/id/ABCD1234567890(正确) - Audience填错:应填
sts.amazonaws.com,而非amazonaws.com或eks.amazonaws.com - URL大小写敏感:
us-west-2不能写成US-WEST-2
验证方法:用curl -I https://oidc.eks.us-west-2.amazonaws.com/id/ABCD1234567890检查HTTP状态码是否为200,并查看响应头Content-Type: application/jwk-set+json。如果返回404或证书错误,说明Issuer URL根本不存在或拼写错误。
注意:Issuer URL里的ID(如
ABCD1234567890)是EKS集群的唯一标识,不是随机字符串。它由aws eks describe-cluster返回,切勿手动生成。
3.2 IAM Role信任策略:Condition字段是权限收敛的核心开关
创建IAM Role时,信任策略(Trust Policy)决定谁可以扮演它。IRSA的典型策略如下:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-west-2.amazonaws.com/id/ABCD1234567890" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "oidc.eks.us-west-2.amazonaws.com/id/ABCD1234567890:sub": "system:serviceaccount:default:my-app-sa", "oidc.eks.us-west-2.amazonaws.com/id/ABCD1234567890:aud": "sts.amazonaws.com" } } } ] }关键点在于Condition块:
sub字段必须精确匹配system:serviceaccount:<namespace>:<serviceaccount-name>,Namespace和SA名区分大小写;- 如果你想让多个SA复用同一Role,可以用通配符:
"system:serviceaccount:prod:*",但这是权限过度开放,不推荐; - 更安全的做法是为每个SA创建独立Role,或用
StringLike配合前缀:"system:serviceaccount:prod:app-*"。
我曾在一个电商项目里把sub写成system:serviceaccount:prod:my-app(少写了-sa后缀),Pod启动后kubectl logs显示InvalidIdentityTokenException,但错误日志里没提具体哪条策略不匹配——因为AWS STS只返回泛化错误。最终是用aws sts assume-role-with-web-identity --role-arn arn:aws:iam::123456789012:role/my-role --role-session-name test --web-identity-token file://token.txt --duration-seconds 900手动测试才定位到sub不匹配。
3.3 ServiceAccount Annotation:格式错误比内容错误更难排查
给ServiceAccount添加Annotation是IRSA的“激活开关”,格式必须严格:
apiVersion: v1 kind: ServiceAccount metadata: name: my-app-sa namespace: default annotations: # 正确:ARN必须完整,且无空格 eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/my-s3-reader-role常见错误:
- ARN里漏了
arn:aws:iam::前缀,只写123456789012:role/my-role; - Annotation键名写错:
eks.amazon.com/role-arn(少了个w)或aws.amazon.com/role-arn; - 值里包含引号:
"arn:aws:iam::..."(YAML里字符串加引号是合法的,但某些旧版kubectl会解析失败); - 在错误的Namespace下创建SA:比如Pod在
prod命名空间,但SA在default里,且没加--namespace prod参数。
验证方法:kubectl get sa my-app-sa -o yaml,确认输出里有eks.amazonaws.com/role-arn字段,且值正确。如果没看到,说明Annotation没生效——可能是kubectl apply时没加-n指定Namespace,或者YAML文件本身有语法错误(比如缩进不对)。
3.4 Pod Spec的隐式要求:serviceAccountName和automountServiceAccountToken必须显式声明
很多人以为只要SA存在,Pod就会自动使用它。错。Pod YAML里必须显式指定:
apiVersion: v1 kind: Pod metadata: name: my-app-pod spec: serviceAccountName: my-app-sa # 必须!否则用default SA automountServiceAccountToken: true # 必须!否则不挂载token文件 containers: - name: app image: my-app:latestserviceAccountName不写,默认用defaultSA,而defaultSA通常没绑定任何IAM Role;automountServiceAccountToken: true不写,K8s 1.22+默认为false,意味着/var/run/secrets/kubernetes.io/serviceaccount/token文件根本不会挂载到容器里,SDK自然读不到token。
验证方法:kubectl exec -it my-app-pod -- ls /var/run/secrets/kubernetes.io/serviceaccount/,应看到token、ca.crt、namespace三个文件。如果只有ca.crt和namespace,说明automountServiceAccountToken为false。
3.5 SDK初始化时机:为什么new S3Client()要放在main()最后?
AWS SDK v2的S3Client.create()是同步阻塞调用,它会在内部触发DefaultCredentialsProviderChain的完整扫描。如果此时Pod刚启动,token文件可能还没完全写入(K8s的volume mount有微小延迟),导致SDK读到空token或损坏token,抛出SdkClientException: Unable to execute HTTP request。
解决方案:在Java中,不要在静态块或构造函数里初始化S3Client,而是在应用主逻辑里延迟创建:
public class App { private static S3Client s3Client; public static void main(String[] args) { // 先等待3秒,确保token文件就绪 try { Thread.sleep(3000); } catch (InterruptedException e) {} // 再创建Client s3Client = S3Client.builder() .region(Region.US_WEST_2) .build(); // 执行业务逻辑 s3Client.listBuckets(); } }在Go中,用time.Sleep(3 * time.Second)同理。这不是hack,而是K8s volume mount的已知行为——官方文档明确指出:“The token is mounted after the container starts, so applications must be prepared to handle initial failures.”(token在容器启动后才挂载,应用需处理初始失败)。
3.6 权限策略最小化:别直接给"s3:*",用Resource和Condition锁死范围
即使IRSA配置成功,如果IAM Policy太宽泛,依然存在风险。一个典型的错误Policy:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "s3:*", "Resource": "*" } ] }这等于给了Pod整个AWS账户的S3控制权。正确做法是:
- 限定Bucket:
"arn:aws:s3:::my-app-logs-bucket"(只读Bucket本身) +"arn:aws:s3:::my-app-logs-bucket/*"(读写对象); - 限定操作:只给
"s3:GetObject"、"s3:ListBucket",删除"s3:DeleteObject"; - 限定前缀:用
Condition限制只能访问2024/06/目录:
"Condition": { "StringLike": { "s3:prefix": ["2024/06/*"] } }我曾帮某客户优化一个日志服务,原Policy允许"s3:*",审计发现它意外获得了"s3:PutBucketPolicy"权限,理论上可篡改其他Bucket的ACL。收紧后,Policy体积缩小60%,且完全满足业务需求。
3.7 日志与监控:如何快速判断是IRSA问题还是S3权限问题?
当listBuckets()失败时,错误日志往往很模糊。你需要分层排查:
第一层:确认token是否存在且可读
kubectl exec -it my-app-pod -- cat /var/run/secrets/kubernetes.io/serviceaccount/token | head -c 100
应输出一长串JWT字符串。如果报No such file or directory,说明automountServiceAccountToken为false。第二层:确认token是否有效
将token复制出来,在本地用aws sts get-caller-identity --web-identity-token $TOKEN --role-arn arn:aws:iam::123456789012:role/my-role --role-session-name test测试。如果返回InvalidIdentityToken,说明OIDC Provider配置错误;如果返回AccessDenied,说明IAM Role的Trust Policy或Permissions Policy有问题。第三层:确认S3调用是否真的用了IRSA
在Java应用里,启用SDK debug日志:-Dsoftware.amazon.awssdk.http.apache.request.debug=true \ -Dsoftware.amazon.awssdk.auth.credentials.debug=true日志里会显示
Using credentials from WebIdentityTokenFileCredentialsProvider,证明IRSA生效。如果显示Using credentials from EnvironmentVariableCredentialsProvider,说明环境变量覆盖了IRSA。
实操心得:我习惯在Pod启动后立即执行
kubectl exec -it my-app-pod -- env | grep AWS,如果看到AWS_ACCESS_KEY_ID,立刻删掉——因为环境变量优先级高于IRSA,它会静默覆盖整个凭据链。
4. 实操过程与核心环节实现:从零开始部署一个IRSA-S3集成的完整流水线
4.1 环境准备:确认EKS集群OIDC状态与工具链
首先,确认你的EKS集群已启用OIDC Provider。这不是可选项,而是IRSA的前提:
# 获取集群信息 CLUSTER_NAME="my-prod-cluster" aws eks describe-cluster --name $CLUSTER_NAME --query "cluster.identity.oidc.issuer" --output text # 输出应为类似:https://oidc.eks.us-west-2.amazonaws.com/id/ABCD1234567890 # 如果报错"An error occurred (ResourceNotFoundException)...",说明未启用,需升级集群或联系管理员同时,确保本地工具链可用:
aws-cli v2.13.0+(支持--web-identity-token参数)kubectl v1.24+(兼容IRSA的API版本)eksctl v0.140.0+(简化IRSA配置,非必需但强烈推荐)
验证kubectl连通性:kubectl get nodes,应返回节点列表。
4.2 创建IAM Role与Policy:用AWS CLI一键完成
避免在控制台点点点,用CLI脚本保证可重复性。以下命令创建一个只读S3 Bucket的Role:
# 变量定义 ACCOUNT_ID="123456789012" REGION="us-west-2" CLUSTER_ID="ABCD1234567890" NAMESPACE="prod" SERVICE_ACCOUNT="log-reader-sa" BUCKET_NAME="my-app-logs-bucket" # 1. 创建信任策略文件 cat > trust-policy.json <<EOF { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::${ACCOUNT_ID}:oidc-provider/oidc.eks.${REGION}.amazonaws.com/id/${CLUSTER_ID}" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "oidc.eks.${REGION}.amazonaws.com/id/${CLUSTER_ID}:sub": "system:serviceaccount:${NAMESPACE}:${SERVICE_ACCOUNT}", "oidc.eks.${REGION}.amazonaws.com/id/${CLUSTER_ID}:aud": "sts.amazonaws.com" } } } ] } EOF # 2. 创建Role ROLE_ARN=$(aws iam create-role \ --role-name "irsa-${NAMESPACE}-${SERVICE_ACCOUNT}-s3-reader" \ --assume-role-policy-document file://trust-policy.json \ --query 'Role.Arn' --output text) # 3. 附加权限策略(最小化) cat > s3-policy.json <<EOF { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:GetObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::${BUCKET_NAME}", "arn:aws:s3:::${BUCKET_NAME}/*" ] } ] } EOF aws iam put-role-policy \ --role-name "irsa-${NAMESPACE}-${SERVICE_ACCOUNT}-s3-reader" \ --policy-name "S3ReadPolicy" \ --policy-document file://s3-policy.json echo "Created Role ARN: $ROLE_ARN"执行后,ROLE_ARN变量将保存新Role的ARN,后续配置ServiceAccount时会用到。
4.3 部署ServiceAccount与Pod:YAML清单与验证命令
创建sa-and-pod.yaml:
--- apiVersion: v1 kind: Namespace metadata: name: prod --- apiVersion: v1 kind: ServiceAccount metadata: name: log-reader-sa namespace: prod annotations: eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/irsa-prod-log-reader-sa-s3-reader --- apiVersion: v1 kind: Pod metadata: name: log-reader-pod namespace: prod spec: serviceAccountName: log-reader-sa automountServiceAccountToken: true containers: - name: app image: amazon/aws-cli:2.13.12 command: ["/bin/sh", "-c"] args: - | echo "Testing S3 access..."; aws sts get-caller-identity; aws s3 ls s3://my-app-logs-bucket/ --region us-west-2; sleep infinity env: - name: AWS_REGION value: "us-west-2"部署并验证:
# 应用清单 kubectl apply -f sa-and-pod.yaml # 检查Pod状态 kubectl get pod -n prod # 查看Pod事件,确认无MountVolume.SetUp失败 kubectl describe pod log-reader-pod -n prod # 进入Pod,测试凭证 kubectl exec -it log-reader-pod -n prod -- aws sts get-caller-identity # 正常应返回Role ARN,且"Arn"字段包含你创建的Role名 # 测试S3访问 kubectl exec -it log-reader-pod -n prod -- aws s3 ls s3://my-app-logs-bucket/如果get-caller-identity成功但s3 ls失败,说明IAM Policy的Resource或Action配置有误,回到步骤4.2检查S3ReadPolicy。
4.4 SDK代码适配:Java v2与Go的零配置接入
Java v2(Maven依赖:software.amazon.awssdk:s3:2.20.153)
无需任何额外配置,只要确保Pod里没有AWS_ACCESS_KEY_ID环境变量,SDK会自动走IRSA流程:
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProviderChain; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.ListBucketsResponse; public class S3Reader { public static void main(String[] args) { // SDK自动检测 /var/run/secrets/.../token 文件 S3Client s3 = S3Client.builder() .region(Region.US_WEST_2) .build(); ListBucketsResponse response = s3.listBuckets(); response.buckets().forEach(bucket -> System.out.println("Bucket: " + bucket.name()) ); } }Go(SDK v2:github.com/aws/aws-sdk-go-v2/config)
同样零配置,config.LoadDefaultConfig会自动识别IRSA:
package main import ( "context" "fmt" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/s3" ) func main() { // 自动加载凭据,包括IRSA cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("us-west-2"), ) if err != nil { panic(err) } client := s3.NewFromConfig(cfg) result, err := client.ListBuckets(context.TODO(), &s3.ListBucketsInput{}) if err != nil { panic(err) } for _, b := range result.Buckets { fmt.Println("Bucket:", *b.Name) } }实操心得:在Java项目里,我习惯在
application.properties中加一行logging.level.software.amazon.awssdk.auth=DEBUG,这样启动时就能看到WebIdentityTokenFileCredentialsProvider被选中的日志,比猜强一百倍。
4.5 故障注入与恢复演练:模拟密钥轮换与Pod重建
IRSA的优势之一是自动轮换,但必须验证它是否真的可靠。我们手动触发一次轮换:
# 1. 删除Pod,强制重建 kubectl delete pod log-reader-pod -n prod # 2. 等待新Pod启动 kubectl wait --for=condition=ready pod/log-reader-pod -n prod --timeout=120s # 3. 进入新Pod,确认新token已挂载 kubectl exec -it log-reader-pod -n prod -- cat /var/run/secrets/kubernetes.io/serviceaccount/token | head -c 50 # 4. 测试S3访问(应仍成功) kubectl exec -it log-reader-pod -n prod -- aws s3 ls s3://my-app-logs-bucket/如果第4步失败,说明IRSA链路中断。此时检查:
- 新Pod的
serviceAccountName是否还是log-reader-sa(kubectl get pod log-reader-pod -n prod -o yaml | grep serviceAccountName); - 新Pod的
automountServiceAccountToken是否为true(同上,查automountServiceAccountToken字段); - ServiceAccount的Annotation是否还在(
kubectl get sa log-reader-sa -n prod -o yaml)。
真正的生产级验证,是让Pod持续运行72小时,期间观察CloudWatch Logs里是否有ExpiredTokenException——如果有,说明SDK没自动刷新,需检查SDK版本(v2.15+已修复多数刷新bug)。
5. 常见问题与排查技巧实录:那些让资深工程师也抓狂的“幽灵错误”
5.1 错误代码InvalidIdentityTokenException:90%是Issuer URL拼写错误
这是IRSA最经典的报错,日志里通常只有一行:
software.amazon.awssdk.services.sts.model.InvalidIdentityTokenException: The web identity token that was passed is invalid.新手第一反应是“token坏了”,其实90%是Issuer URL不匹配。排查步骤:
获取Pod里实际使用的Issuer URL:
kubectl exec -it log-reader-pod -n prod -- cat /var/run/secrets/kubernetes.io/serviceaccount/issuer
这个文件内容就是K8s API Server的Issuer,必须和IAM OIDC Provider里配置的完全一致。对比IAM控制台的OIDC Provider设置:
在IAM控制台 → Identity Providers → 找到你的Provider → 点击“Edit” → 查看“Provider URL”。
两者必须逐字符相同,包括协议(https)、域名、路径、ID、无尾部斜杠。终极验证:用curl直连Issuer URL:
curl -v https://oidc.eks.us-west-2.amazonaws.com/id/ABCD1234567890
如果返回403 Forbidden或404 Not Found,说明Provider根本没创建,或ID写错。
注意:
/var/run/secrets/kubernetes.io/serviceaccount/issuer文件在K8s 1.22+才默认挂载。如果该文件不存在,说明集群版本过低,需升级或手动配置。
5.2 错误代码AccessDeniedException:权限策略的“隐形陷阱”
当get-caller-identity成功,但s3 ls失败时,AccessDeniedException是标准错误。但陷阱在于:它可能由三种完全不同的原因导致:
| 原因类型 | 表现特征 | 排查命令 | 解决方案 |
|---|---|---|---|
| Trust Policy不匹配 | get-caller-identity返回的Arn是节点角色,而非你创建的IRSA Role | kubectl exec -it pod -- aws sts get-caller-identity | jq .Arn | 检查SA的eks.amazonaws.com/role-arnAnnotation是否指向正确Role |
| Permissions Policy太窄 | get-caller-identity成功,但s3 ls报AccessDenied,且Bucket存在 | kubectl exec -it pod -- aws s3 ls s3://existing-bucket/ | 检查IAM Policy的Resource是否包含Bucket ARN和对象ARNS,Action是否包含ListBucket |
| S3 Bucket策略拒绝 | get-caller-identity成功,s3 ls失败,且其他账号能访问该Bucket | aws s3api get-bucket-policy --bucket existing-bucket | 检查Bucket Policy里是否有"Principal": {"AWS": "arn:aws:iam::123456789012:root"}之类的显式拒绝 |
我曾在一个项目里卡了两天,最后发现是Bucket Policy里有一条"Effect": "Deny"规则,针对所有非特定IP的请求——而IRSA的请求源IP是EKS节点IP池,不在白名单里。解决方案不是改Bucket Policy,而是用S3 VPC Endpoint,让流量不经过公网。
5.3 Pod启动慢、卡在ContainerCreating:token卷挂载超时
有时Pod状态一直是ContainerCreating,kubectl describe pod显示:
Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 2m default-scheduler Successfully assigned prod/log-reader-pod to ip-10-0-1-100.us-west-2.compute.internal Warning FailedMount 90s (x5 over 2m) kubelet MountVolume.SetUp failed for volume "kube-api-access-xxxxx" : failed to sync secret cache: timed out waiting for the condition这是K8s的ServiceAccountTokenVolumeProjection特性在等待API Server签发token,但API Server响应慢。常见原因:
- 集群API Server负载过高;
- 节点网络延迟大(如跨AZ部署);
kube-controller-manager的--service-account-extend-token-expiration参数未开启(默认关闭,token有效期10小时,但首次签发可能延迟)。
解决方法:
- 临时增加容忍度,在Pod spec里加:
spec: # ... 其他字段 tolerations: - key: "node.kubernetes.io/unreachable" operator: "Exists" effect: "NoExecute" tolerationSeconds: 300 - 长期方案:升级EKS到1.25+,并确保
kube-controller-manager启用--service-account-extend-token-expiration=true。
5.4 SDK日志显示Using credentials from EnvironmentVariableCredentialsProvider:环境变量静默劫持
这是最隐蔽的坑。你明明没设AWS_ACCESS_KEY_ID,但SDK日志却显示用了环境变量。原因通常是:
- 基础镜像自带环境变量:比如
amazon/aws-cli镜像在Dockerfile里写了ENV AWS_ACCESS_KEY_ID=test; - K8s downward API注入:Pod spec里用了
envFrom: configMapRef,而ConfigMap里包含了AWS密钥; - 父进程继承:如果用
kubectl run启动临时Pod,它可能继承了shell的环境变量。
排查命令:
kubectl exec -it log-reader-pod -n prod -- env | grep -i aws如果输出非空,立刻删掉。解决方案:
- 在Pod spec里显式清空:
env: - name: AWS_ACCESS_KEY_ID value: "" - name: AWS_SECRET_ACCESS_KEY value: "" - 或用
envFrom时排除敏感字段:envFrom: - configMapRef: name: app-config optional: true env: - name: AWS_ACCESS_KEY_ID valueFrom: fieldRef: fieldPath: metadata.annotations['aws-creds'] # 用annotation替代env
5.5 多集群复用IRSA配置:如何避免硬编码Cluster ID
在CI/CD流水线里,同一个应用要部署到dev/staging/prod三个EKS集群,每个集群的OIDC ID都不同。如果YAML里硬编码ABCD1234567890,每次部署都要替换,极易出错。
最佳实践是用eksctl生成参数化模板:
# 用eksctl生成IRSA配置(自动填充Cluster ID) eksctl create iamserviceaccount \ --name log-reader-sa \