
Spark OIDC AWS 凭证传播端到端集成测试在 Minikube moto 上本地验证完整链路【免费下载链接】sparkApache Spark - A unified analytics engine for large-scale data processing项目地址: https://gitcode.com/gh_mirrors/sp/spark本文围绕connector/credential-aws-integration-tests模块完整讲解 Spark 中 OIDC AWS 凭证传播credential propagation功能的端到端集成测试环境从 Prerequisites 准备、dev-run-integration-tests.sh包装脚本、Maven/sbt 直跑方式到各系统属性与三个测试用例的验证逻辑。读完本文你既能一键在本地 Minikube 集群上跑通这套 E2E 测试也能理解其底层Projected SA Token → STS → S3A的凭证流转机制与源码实现依据。一、测试目标验证什么链路该模块验证的是如下端到端 OIDC 凭证传播流程原文档给出的核心链路Projected SA Token - FileTokenIngestor - AwsStsCredentialProvider - moto STS - moto S3 (via S3A)其中 moto 是一个 Apache 2.0 许可的 Python 库可在本地以轻量 HTTP 服务器形式模拟 AWS S3 和 STS 服务与 Minikube 并行运行从而让整套测试完全脱离真实 AWS 账户。从源码结构看这条链路的两个关键实现分别位于AwsStsCredentialProvider实现CredentialProvider接口通过 STS 的AssumeRoleWithWebIdentityAPI 用 OIDC 身份令牌换取临时 AWS 凭证SparkOidcAwsCredentialsProviderS3A 端的凭证提供者运行在 executor 上从框架传播过来的凭证存储中读取临时凭证。二、环境准备Prerequisites原文档明确列出三项前置条件均为硬性要求Minikube 1.38.0。GitHub Actions CI 使用 2 CPU / 6 GB 内存即可跑通本地建议 4 CPU 以获得 driver 与动态分配的executor Pod 的余量minikube start --cpus 4 --memory 6144Python 3 motopip install moto[server,s3,sts]5.0.0,6.0.0Docker 可用用于把 Spark 镜像构建进 Minikube 的 daemon脚本内部会执行eval $(minikube docker-env)对齐 Docker 上下文。三、Quickstart用包装脚本一键运行最简单的方式是直接运行包装脚本可从仓库任意位置执行脚本会从自身位置解析仓库根目录connector/credential-aws-integration-tests/dev/dev-run-integration-tests.shdev-run-integration-tests.sh 依次完成六步校验前置条件check_prereqspython3 存在、moto 可import、minikube 处于 Running 状态minikube status --format{{.Host}}启动 moto 服务器python3 -m moto.server -H 0.0.0.0 -p 5000监听所有网口以便 Minikube Pod 从宿主机网关 IP 访问启动后以curl轮询最多 30 秒等待就绪并以trap cleanup EXIT保证退出时停掉 moto探测 Pod 视角的宿主机 IPresolve_moto_host对 minikube 执行minikube ssh ip route | grep default | awk {print $3}取得 VM 内默认网关探测失败时回退到localhost并打印 WARNING。同时固定宿主机侧端点为http://127.0.0.1:port——测试 JVM 跑在宿主机上走 loopback 访问 moto这与 Pod 侧的网关 IP 端点是不同的两个地址构建 Spark 与 Docker 镜像build_spark_image执行build/sbt -P${HADOOP_PROFILE} -Phadoop-cloud -Pkubernetes -Pcredential-aws -Poidc-e2e package。-Phadoop-cloud把 hadoop-aws 与 AWS SDK 打进镜像S3A 所需若未指定 tag自动生成oidc-e2e-时间戳作为镜像 tag然后调用./bin/docker-image-tool.sh -r repo -t tag build构建基础镜像烘焙 job jar由于docker-image-tool.sh只把 examples/jars 拷入镜像本模块的 jar含OidcS3ReadWriteJob等 job 类会缺失。脚本因此用临时 DockerfileFROM repo/spark:tagCOPYjob jar 到/opt/spark/jars/再构建一个repo/spark:tag-job镜像。注意注释明确说明烘焙 jar 的原因不是 classpath 缺口SparkSubmit 本来就会把local://主资源加入 driver classpath而是容器里根本没有这个 jar使用--skip-build时脚本会把repo/spark:tag自动推导为repo/spark:tag-job避免落到缺少 job 类的普通镜像上触发ClassNotFoundException通过 Maven 运行集成测试run_tests组装build/mvn integration-test -am -pl connector/credential-aws-integration-tests ...及全部-D系统属性后执行退出时停止 mototrap cleanup。脚本选项完整对照表OptionDefaultDescription--image-tag tag(generated)使用预先构建好的、带该 tag 的 Spark 镜像。--image-repo repodocker.io/kubesparkDocker 镜像仓库。--spark-image image(derived)完整镜像名覆盖--image-repo--image-tag。--deploy-mode modeminikubeKubernetes 后端minikube、docker-desktop、rancher-desktop、cloud。--namespace ns(auto-generated)Kubernetes 命名空间不存在则创建退出时删除预先存在的命名空间不删。--service-account sadefaultKubernetes 服务账户。--moto-port port5000moto 服务器端口。--role-arn arnarn:aws:iam::123456789012:role/oidc-e2e-test-roleAssumeRoleWithWebIdentity使用的 IAM 角色 ARNmoto 接受任意格式合法的 ARN。--s3-bucket bucketoidc-e2e-test-bucket在 moto 中创建的 S3 桶名。--token-file path/var/run/secrets/kubernetes.io/serviceaccount/tokendriver Pod 内的 OIDC 令牌文件路径。--skip-buildfalse跳过构建 Spark 与 Docker 镜像。--hadoop-profile profhadoop-3Hadoop Maven profile。四、使用预构建镜像运行与陈旧镜像警告已有镜像时可复用connector/credential-aws-integration-tests/dev/dev-run-integration-tests.sh \ --image-tag my-tag \ --skip-build原文档给出了一条必须重视的警告--skip-build只是复用已有镜像并不会重建它。如果你在构建镜像之后修改过connector/credential-aws或任何 Spark 源码测试会静默地跑在陈旧镜像上并可能产生误导性结果。修改代码后务必去掉--skip-build或显式重建镜像。此外与kubernetes-integration-tests不同本模块没有把镜像构建绑定到 sbt 的testtask因此镜像新鲜度完全由调用方负责。而在 CIGitHub Actions中这不是问题每次运行都在全新 runner 上从零构建镜像后才执行测试。五、Maven / sbt 直跑方式若希望自行管理 moto 和镜像可用 Maven 直接运行原文档完整命令逐行保留# 1. Start moto server (in a separate terminal or background) python3 -m moto.server -H 0.0.0.0 -p 5000 # 2. Determine the host IP reachable from Minikube pods HOST_IP$(minikube ssh ip route | grep default | awk {print \$3} | tr -d [:space:]) MOTO_ENDPOINThttp://${HOST_IP}:5000 # 3. Run the tests. Pods reach moto via the host gateway IP, while the test process # on the host reaches it on loopback (s3ClientEndpoint). build/mvn integration-test -am \ -pl connector/credential-aws-integration-tests \ -Phadoop-3 -Pkubernetes -Pcredential-aws -Poidc-e2e \ -Dspark.kubernetes.test.deployModeminikube \ -Dspark.oidc.test.stsEndpoint${MOTO_ENDPOINT} \ -Dspark.oidc.test.s3Endpoint${MOTO_ENDPOINT} \ -Dspark.oidc.test.s3ClientEndpointhttp://127.0.0.1:5000 \ -Dspark.oidc.test.sparkImagedocker.io/kubespark/spark:my-tag或用 sbtbuild/sbt \ -Phadoop-3 -Pkubernetes -Pcredential-aws -Poidc-e2e \ -Dspark.kubernetes.test.deployModeminikube \ -Dspark.oidc.test.stsEndpoint${MOTO_ENDPOINT} \ -Dspark.oidc.test.s3Endpoint${MOTO_ENDPOINT} \ -Dspark.oidc.test.s3ClientEndpointhttp://127.0.0.1:5000 \ -Dspark.oidc.test.sparkImagedocker.io/kubespark/spark:my-tag \ credential-aws-integration-tests/test几个值得注意的细节结合 pom.xml 与根 pom根 pom.xml 中定义了oidc-e2eprofile同时把connector/credential-aws和connector/credential-aws-integration-tests纳入 reactor。这样单传-Poidc-e2e也不会因依赖解析失败命令里同时传-Pcredential-aws是双保险模块 pom 将 surefire 的skipTests置为 true并让scalatest-maven-plugin只在integration-test阶段执行即单元测试阶段被跳过、集成测试单独成阶段测试依赖中包含software.amazon.awssdk:bundle供测试进程预建 S3 桶、验证结果与io.fabric8:kubernetes-clientPod/命名空间管理。注意通过spark.oidc.test.sparkImage传入的镜像必须以-Phadoop-cloud构建含 hadoop-aws 与 S3A 所需的 AWS SDK且 job jarOidcS3ReadWriteJob必须已烘焙进/opt/spark/jars。dev-run-integration-tests.sh会自动替你完成这两点手动跑时须自行保证。六、Maven / 系统属性完整对照表以下属性在 模块 pom.xml 中声明默认值并通过scalatest-maven-plugin的systemProperties注入测试 JVMPropertyDefaultDescriptionspark.kubernetes.test.deployModeminikubeKubernetes 后端。spark.kubernetes.test.imageRepodocker.io/kubesparkDocker 镜像仓库。spark.kubernetes.test.imageTagN/ADocker 镜像 tag。spark.kubernetes.test.namespace(auto-generated)测试用 Kubernetes 命名空间。spark.kubernetes.test.serviceAccountNamedefaultKubernetes 服务账户。spark.oidc.test.stsEndpointhttp://localhost:5000moto STS 端点Pod 视角即宿主机网关 IP。spark.oidc.test.s3Endpointhttp://localhost:5000moto S3 端点Pod 视角即宿主机网关 IP。spark.oidc.test.s3ClientEndpointhttp://127.0.0.1:5000moto S3 端点宿主机组测试 JVM 视角loopback。spark.oidc.test.roleArnarn:aws:iam::123456789012:role/oidc-e2e-test-roleIAM 角色 ARN。spark.oidc.test.tokenFile/var/run/secrets/kubernetes.io/serviceaccount/tokenPod 内 OIDC 令牌文件路径。spark.oidc.test.s3Bucketoidc-e2e-test-bucketmoto 中的 S3 桶名。spark.oidc.test.sparkImage(derived from repotag)完整 Spark Docker 镜像名。test.include.tags(none)逗号分隔的 ScalaTest 标签包含。test.exclude.tags(none)逗号分隔的 ScalaTest 标签排除。源码侧还有两个健壮性设计值得了解见 OidcCredentialE2ESuite属性归一化pom 中部分属性默认值为空字符串Maven 的 scalatest 插件转发空属性到 JVM 时会变成字符串null而 sbt 则干脆不设置这些属性。测试内的prop()辅助函数把未设置、空串、字面量null一律视为未配置并回退默认值保证 Maven 与 sbt 行为一致镜像快速失败当既未显式给spark.oidc.test.sparkImage、imageTag又是哨兵值N/A时测试在提交前抛出带修复指引的断言错误而不是等到 driver Pod 出现晦涩的ImagePullBackOff。七、测试用例详解TestStatusDescriptionBasic S3 read/writeImplementedProjected SA token → moto STS → Minikube 上经 S3A 写/读Mid-job token rotationImplemented身份令牌文件由 init 容器写入 emptyDir在作业中途被重写driver 在续期时重新读取S3 访问跨越轮换保持可用Late-registering executorImplemented开启动态分配后一个在凭证获取之后才注册的 executor 能经由 SparkAppConfig 注册响应拿到凭证并写入 S3结合 OidcCredentialE2ESuite.scala 与三个 job 类各用例的关键验证手段如下用例 1基础 S3 读写对应 jobOidcS3ReadWriteJob——parallelize两行数据saveAsTextFile到s3a://输出路径再textFile读回校验非空最后在 driver 日志打印成功标记OidcS3ReadWriteJob: SUCCESS测试的baseSparkConf()设置spark.security.oidc.enabledtrue、spark.security.oidc.identityToken.file、spark.security.oidc.aws.roleArn/stsEndpoint/region以及指向 moto 的 S3A 配置fs.s3a.endpoint、fs.s3a.path.style.accesstrue、fs.s3a.connection.ssl.enabledfalse但刻意不设置fs.s3a.aws.credentials.provider——以此端到端覆盖OIDC 启用且用户未显式指定 provider 时 Spark 自动配置 S3A 凭证提供者的主路径另有一个变体用例显式设置spark.hadoop.fs.s3a.aws.credentials.providerorg.apache.spark.security.aws.SparkOidcAwsCredentialsProvider覆盖用户显式 provider 不被自动配置覆盖的分支。这与 AwsStsCredentialProvider 中additionalSparkProperties()返回该键值、且UserCredentialManager仅在键未设置时才应用的自动配置逻辑相呼应断言分三层spark-submit 退出码注意注释指出waitAppCompletiontrue下 driver 到达任意终态——包括 Failed——都会让提交客户端返回 0故退出码不区分成功失败→ driver Pod 到达Succeeded且日志含成功标记 → 宿主机侧用 AWS SDK v2 的S3ClientendpointOverride指向 loopback moto、forcePathStyle轮询确认对象存在。失败时dumpDriverDiagnostics会打印 Pod phase/status 与完整日志。用例 2作业中途令牌轮换对应 jobOidcTokenRotationJob循环 8 轮、每轮睡眠 5 秒写 S3测试生成 driver Pod 模板init 容器以 uid 185/gid 0官方镜像的spark用户把初始 JWT 写入挂载到/oidc的 emptyDir并chmod 0660使 driver 容器后续可覆写——这是模拟外部可轮换令牌文件的关键init 容器复用 Spark 镜像而非 busybox避免 CI 期间拉取外部镜像的速率限制问题测试配置spark.security.oidc.identityToken.file/oidc/token、spark.security.oidc.renewal.minInterval3s、spark.security.oidc.renewal.safetyMargin590s使续期循环快速重新读取令牌轮换本身通过 fabric8 客户端exec在运行中的 driver Pod 内执行printf %s token /oidc/token并等待ExecWatch.exitCode()完成而非 WebSocket 的 onClose 回调——注释说明 onClose 不保证重定向落盘提前关闭甚至可能截断写入这正是该用例间歇性读到旧令牌的根因写入前还有 Base64URL 字符集校验防止 shell 注入权威断言初始令牌与轮换令牌使用不同的 principalsubject测试等待 driver 日志出现Loaded identity token for principal rotated以此区分真的重新读取并交换了轮换令牌与旧凭证在 moto 长 TTL 内仍有效的空操作随后验证跨轮换边界的全部 8 轮迭代输出对象均存在。用例 3迟注册 executor对应 jobOidcLateExecutorJob开启spark.dynamicAllocation.enabledshuffleTracking.enabledtrueK8s 上无外部 shuffle 服务、minExecutors0、maxExecutors3、executorIdleTimeout5s以快速缩容场景warm-up stage 触发 driver 早期获取凭证 → 作业空闲 30 秒让动态分配释放 executor → 宽 stage 强制在凭证获取之后重新申请并注册 executor每个 task 写 S3断言有两重其一从 driver 日志解析Registered executor ... with ID id行要求出现至少两个不同 executor ID证明确实发生了缩容后再注册而非同一个长生命周期 executor 平凡通过其二宽 stage 必须产出part-*文件与_SUCCESS标记。测试基础设施源码补充RBACensureDriverRbac()在测试命名空间内创建 Rolepods/services/configmaps/persistentvolumeclaims的*动词并绑定到 driver 服务账户镜像了 kubernetes-integration-tests 的 RBAC 约定对已存在对象忽略 409 Conflict保证幂等与预建命名空间可复用moto 资源准备ensureMotoResources()用宿主机侧S3Client检查并在缺失时创建测试桶标签所有用例都打了oidcE2eTagorg.apache.spark.security.aws.integrationtest.OidcE2ETest可用test.include.tags/test.exclude.tags控制清理每个用例finally中删除 driver Pod 并等待其真正消失避免残留 Pod 与下一个用例争抢 CI Minikube 的有限资源suite 结束时若命名空间是本次创建的则删除并等待其完全终结。八、架构与凭证传播路径原文档的架构图------------------------------------------------------- | GitHub Actions runner / developer machine | | | | moto_server :5000 (S3 STS emulator) | | | | ------------------------------------------------- | | | Minikube | | | | | | | | ---------------- -------------------- | | | | | Driver Pod | | Executor Pod(s) | | | | | | | | | | | | | | FileToken | | SparkOidc | | | | | | Ingestor | | AwsCredentials | | | | | | | | | Provider | | | | | | v | RPC | reads creds | | | | | | AwsSts |----| from store | | | | | | Credential | | | | | | | | | Provider | | v | | | | | | | | | S3A -- moto S3 | | | | | | v | | | | | | | | moto STS | | | | | | | ---------------- -------------------- | | | ------------------------------------------------- | -------------------------------------------------------原文档特别强调图中的RPC箭头是简化表达。凭证实际通过三条互补路径到达 executorSparkAppConfig注册响应——新注册 executor 立即可用由 late-registering-executor 用例验证UpdateUserCredentialsRPC 广播——每次续期时下发由 token-rotation 用例验证TaskDescription——携带当前凭证保证 task 运行前一定可用。安全边界同样重要原始身份令牌永远不离开 driver——传播的只是经 STS 换得的、短生命周期的服务凭证。这一点在 AwsStsCredentialProvider 中有对应实现证据异常处理会遍历 cause 链检查原始令牌是否泄漏进任何异常消息若泄漏则将 cause 链整体替换为脱敏包装原始 token 替换为[REDACTED]resolve()返回的ServiceCredential只包含fs.s3a.access.key/fs.s3a.secret.key/fs.s3a.session.token三组 S3A 属性与过期时间。provider 侧的关键配置键源码级补充AwsStsCredentialProvider 的 Javadoc 定义了它消费的配置键理解这些键有助于调试 E2E 失败配置键说明spark.security.oidc.aws.roleArn必填要扮演的 IAM 角色 ARNinit()时缺失即抛IllegalArgumentExceptionspark.security.oidc.aws.sessionName可选须匹配[a-zA-Z0-9_,.-]{2,64}未设置时由 principal 净化后派生非法字符替换为-、截断至 64 字符再回退spark-oidcspark.security.oidc.aws.durationSeconds可选900–43200 秒越界直接抛错suggestedTtl()在显式设置时返回该值否则默认 15 分钟spark.security.oidc.aws.region可选仅设置了stsEndpoint时默认us-east-1两者都未设置时交给 AWS SDK 默认区域解析AWS_REGION/AWS_DEFAULT_REGION环境变量再到~/.aws/configspark.security.oidc.aws.stsEndpoint可选非 AWS 环境MinIO、Ceph、LocalStack 等以及本测试的 moto的自定义 STS 端点STS 客户端以AnonymousCredentialsProvider构建——AssumeRoleWithWebIdentity不需要 AWS 凭证OIDC 令牌本身就是认证机制。supportedSchemes()返回s3a即该 provider 只为 S3A 方案解析凭证。九、失败排查要点汇总结合文档与源码常见失败的定位入口ImagePullBackOff镜像未构建或未加载进 Minikube daemon。确认已eval $(minikube docker-env)后构建手动跑时确认sparkImage指向的是-job后缀镜像含 job 类否则 driver 直接ClassNotFoundExceptionClassNotFoundExceptionjob 类--skip-build却指定了不带-job后缀的 tag或旧镜像未烘焙 jarPod 无法访问 moto检查spark.oidc.test.stsEndpoint/s3Endpoint是否用了 Pod 视角的宿主机网关 IPminikube ssh ip route | grep default而s3ClientEndpoint必须是宿主机 loopback——两者混用是 Minikube 环境下最典型的配置错误executor Pod 创建超时集群启用 RBAC 时默认 SA 无权创建 executor Pod本 suite 会通过ensureDriverRbac()自行授予但自定义集群/命名空间下需检查oidc-e2e-driver-role与 binding 是否成功创建轮换用例偶发读到旧令牌历史上根因是 fabric8exec只等 onClose 不等 exit code当前实现已修复若在其他客户端代码中复用该模式需引以为戒。十、小结connector/credential-aws-integration-tests用Minikube moto的纯本地组合把 Spark OIDC AWS 凭证传播这条跨 driver/executor、跨身份令牌与临时凭证边界的安全链路变成了可重复验证的自动化测试。它的工程价值不仅在于三个用例基础读写、令牌轮换、迟注册 executor覆盖了凭证传播最关键的三条分发路径也在于其可复用的做法Pod/宿主机双端点分离、job jar 烘焙成-job镜像、-Poidc-e2eprofile 的自包含依赖、幂等的命名空间与 RBAC 准备、以及基于 driver Pod 日志标记的 fail-fast 断言。相关入口文件README、模块 pom、包装脚本、测试套件、被测 provider。【免费下载链接】sparkApache Spark - A unified analytics engine for large-scale data processing项目地址: https://gitcode.com/gh_mirrors/sp/spark创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考