在生产环境中,静态的、长期有效的AWS Access Key ID 和 Secret Access Key 是一个必须被根除的安全隐患。将它们作为GitHub Secrets存储,用于CI/CD流水线,本质上只是将风险从开发者的本地机器转移到了GitHub的设置页面。一旦泄露,其后果不堪设想。我们团队的目标很明确:构建一个完全自动化的流程,通过git push部署和更新运行在AWS EKS上的Ray集群,整个过程无需任何长期凭证。同时,我们的算法工程师和数据科学家需要通过公司的SAML身份提供商(IdP)无缝、安全地访问这些集群进行调试和监控,而不是通过共享的、难以审计的kubeconfig文件。
这个任务的核心是解决两种身份的认证与授权问题:机器身份(GitHub Actions Runner)和人类身份(工程师)。我们的解决方案最终融合了GitHub Actions对OIDC的支持,以及AWS IAM对SAML联邦认证的能力,构建了一个端到端的零信任部署与访问管道。
第一阶段:为机器身份建立信任 - GitHub OIDC与AWS IAM的联邦
问题的起点是如何让GitHub Actions的Runner在不持有任何预置密钥的情况下,获得操作AWS资源的临时权限。这正是OpenID Connect (OIDC)的用武之地。其基本流程是:
- GitHub Actions在工作流运行时,会向其自身的OIDC提供商请求一个JSON Web Token (JWT)。这个JWT包含了关于当前工作流的详细信息,如代码仓库、分支、触发事件等。
- 我们将AWS IAM配置为信任GitHub的OIDC提供商。
- 工作流将这个JWT提交给AWS Security Token Service (STS) 的
AssumeRoleWithWebIdentityAPI。 - STS验证JWT的签名和声明(claims),如果符合我们在IAM Role信任策略中定义的条件,就会向Runner颁发一组有时效性的、具备特定权限的临时AWS凭证。
这种方式的安全性在于,凭证是临时的,且其权限被严格限定在IAM Role的策略范围内。更重要的是,信任关系是基于可验证的JWT声明建立的,而不是一个可能被泄露的静态密钥。
1.1 在AWS IAM中创建OIDC身份提供商
这是建立信任的第一步。我们需要告诉AWS,我们信任来自token.actions.githubusercontent.com的JWT。
# 使用AWS CLI创建一个OIDC身份提供商
# 注意:你需要从GitHub的OIDC配置端点获取指纹
# 这通常可以通过浏览器访问 https://token.actions.githubusercontent.com/.well-known/openid-configuration
# 然后找到 jwks_uri,再从中提取TLS证书指纹。
# 为方便起见,大部分情况下指纹是固定的,但生产环境务必自行验证。
THUMBPRINT=$(echo | openssl s_client -servername token.actions.githubusercontent.com -showcerts -connect token.actions.githubusercontent.com:443 2>/dev/null | openssl x509 -fingerprint -noout |- sed 's/://g' | cut -d'=' -f2)
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com \
--thumbprint-list $THUMBPRINT
# 验证提供商是否创建成功
aws iam list-open-id-connect-providers
这里的$THUMBPRINT是关键的安全环节,它确保了我们正在与真正的GitHub OIDC端点通信。
1.2 创建用于GitHub Actions的IAM Role
这个角色将是GitHub Actions工作流在AWS中行动的“身份”。其核心是信任策略(Trust Policy),它精确地定义了谁(哪个GitHub仓库、哪个分支)可以代入这个角色。
github_actions_role_trust_policy.json:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::YOUR_AWS_ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:ref:refs/heads/main"
}
}
}
]
}
这个策略非常重要:
-
Principal.Federated: 指明了我们信任的OIDC提供商。 -
Condition.StringLike: 这是实现最小权限原则的关键。sub声明包含了仓库和分支信息。这里的配置意味着只有your-org/your-repo仓库的main分支上的工作流才能代入这个角色。你可以使用通配符,例如repo:your-org/*:ref:refs/heads/feature-*来匹配所有feature分支。
接下来,创建角色并附加权限策略。
# 创建IAM Role
aws iam create-role \
--role-name GitHubActions-EKS-Ray-Deployer \
--assume-role-policy-document file://github_actions_role_trust_policy.json
# 创建权限策略 (Permissions Policy)
# 这个策略需要赋予操作EKS集群、可能还有EC2、VPC等资源的权限。
# 这是一个示例,生产环境需要更精细化的权限。
cat <<EOF > eks_ray_deployer_permissions.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"eks:DescribeCluster",
"eks:ListClusters",
"eks:AccessKubernetesApi"
],
"Resource": "arn:aws:eks:REGION:ACCOUNT_ID:cluster/YOUR_EKS_CLUSTER_NAME"
},
{
"Effect": "Allow",
"Action": "sts:GetCallerIdentity",
"Resource": "*"
}
// ... 其他部署可能需要的权限
]
}
EOF
# 创建策略并附加到角色
POLICY_ARN=$(aws iam create-policy --policy-name EKS-Ray-Deployer-Policy --policy-document file://eks_ray_deployer_permissions.json --query 'Policy.Arn' --output text)
aws iam attach-role-policy \
--role-name GitHubActions-EKS-Ray-Deployer \
--policy-arn $POLICY_ARN
至此,AWS端的配置完成。我们已经建立了一个安全的信任通道。
第二阶段:构建自动化的Git驱动部署流水线
现在,我们来构建GitHub Actions工作流。这个工作流将在每次向main分支推送代码时,自动部署或更新EKS上的Ray集群。
.github/workflows/deploy-ray-cluster.yml:
name: Deploy Ray Cluster to EKS
on:
push:
branches:
- main
workflow_dispatch: # 允许手动触发
permissions:
id-token: write # 必须,用于向GitHub OIDC提供商请求JWT
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/GitHubActions-EKS-Ray-Deployer
aws-region: YOUR_AWS_REGION
# role-session-name 可选,但有助于审计追踪
role-session-name: GitHubActions-Ray-Deploy-${{ github.sha }}
- name: Setup kubectl and helm
run: |
echo "Setting up kubectl..."
curl -o kubectl https://s3.us-west-2.amazonaws.com/amazon-eks/1.27.1/2023-04-19/bin/linux/amd64/kubectl
chmod +x ./kubectl
sudo mv ./kubectl /usr/local/bin/
kubectl version --client
echo "Setting up helm..."
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh
helm version
- name: Configure kubectl for EKS
run: |
aws eks update-kubeconfig --name YOUR_EKS_CLUSTER_NAME --region YOUR_AWS_REGION
# 验证是否能连接到集群
kubectl get nodes
- name: Deploy KubeRay Operator
run: |
# 幂等地安装或升级KubeRay Operator
helm repo add kuberay https://ray-project.github.io/kuberay-helm/
helm repo update
helm upgrade --install kuberay-operator kuberay/kuberay-operator --version 1.0.0 --namespace ray-system --create-namespace
- name: Deploy Ray Cluster Manifest
run: |
# 将RayCluster的定义文件应用到集群
# 这个YAML文件应该在你的代码仓库中进行版本控制
kubectl apply -f ./ray-cluster/production.yaml
这个工作流有几个关键点:
-
permissions: id-token: write: 这是授权工作流从GitHub获取OIDC JWT的魔法。没有它,一切都无法工作。 -
aws-actions/configure-aws-credentials: 这个官方Action负责处理JWT交换和设置临时凭证为环境变量,后续的aws或kubectl命令可以直接使用。 - 幂等性:使用
helm upgrade --install和kubectl apply确保了流水线可以重复运行,无论是初次部署还是后续更新,行为都是一致的。
我们的Ray集群定义文件ray-cluster/production.yaml也应该是生产级的:
# ray-cluster/production.yaml
apiVersion: ray.io/v1
kind: RayCluster
metadata:
name: production-ray-cluster
namespace: ray-system
spec:
rayVersion: '2.9.0'
# Ray head pod
headGroupSpec:
rayStartParams:
dashboard-host: '0.0.0.0'
# 在生产环境中,启用日志归档和指标非常重要
metrics-export-port: '8080'
template:
spec:
containers:
- name: ray-head
image: rayproject/ray:2.9.0
ports:
- containerPort: 6379 # GCS
- containerPort: 8265 # Dashboard
- containerPort: 10001 # Client
- containerPort: 8080 # Metrics
resources:
requests:
cpu: "2"
memory: "8Gi"
limits:
cpu: "4"
memory: "16Gi"
# 生产环境中必须配置存活和就绪探针
livenessProbe:
httpGet:
path: /
port: 8265
initialDelaySeconds: 10
periodSeconds: 5
readinessProbe:
httpGet:
path: /
port: 8265
initialDelaySeconds: 5
periodSeconds: 5
# Ray worker pods
workerGroupSpecs:
- groupName: small-cpu-worker
replicas: 2
minReplicas: 1
maxReplicas: 10 # 启用自动伸缩
rayStartParams: {}
template:
spec:
containers:
- name: ray-worker
image: rayproject/ray:2.9.0
resources:
requests:
cpu: "4"
memory: "16Gi"
limits:
cpu: "4"
memory: "16Gi"
现在,每当main分支有更新,一个安全的、无凭证的CI/CD流程就会自动将最新的Ray集群配置同步到EKS。机器身份的问题已经解决。
第三阶段:为人类身份建立信任 - SAML与EKS的集成
接下来是更复杂的部分:如何让我们的工程师通过他们的公司账户(例如Okta, Azure AD)安全地访问这个EKS集群。我们需要将SAML IdP与AWS IAM集成,然后将IAM身份映射到Kubernetes的RBAC角色。
流程如下:
- 工程师执行
kubectl命令。 -
aws-iam-authenticator(由aws cli在后台调用)发现需要认证,将用户重定向到公司SAML IdP的登录页面。 - 用户完成SSO登录。IdP向浏览器返回一个SAML断言。
- 浏览器将SAML断言POST到AWS登录端点。
- AWS STS验证SAML断言,并代入一个预先配置好的IAM Role,返回临时AWS凭证给
aws-iam-authenticator。 -
aws-iam-authenticator使用这些临时凭证生成一个Kubernetes令牌,并将其交给kubectl。 -
kubectl向EKS API Server出示令牌。 - EKS API Server将令牌交回给AWS进行验证,确认其有效性,并获取该令牌对应的IAM身份(ARN)。
- EKS通过
aws-authConfigMap查找该IAM ARN被映射到了哪个Kubernetes用户或组。 - 基于Kubernetes的RBAC策略,EKS判断该用户/组是否有权限执行请求的操作。
3.1 配置AWS IAM与SAML IdP
这一步高度依赖于你使用的IdP,但通用步骤是:
- 在你的IdP(如Okta)中,创建一个新的SAML 2.0应用,集成AWS。
- 从AWS IAM控制台下载联邦元数据XML文件,并上传到IdP应用配置中。
- 在AWS IAM中,创建一个SAML身份提供商,并上传从IdP下载的元数据XML文件。
- 创建一个专供SAML用户代入的IAM Role。其信任策略将指向SAML提供商。
saml_user_role_trust_policy.json:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::YOUR_AWS_ACCOUNT_ID:saml-provider/YOUR_SAML_PROVIDER_NAME"
},
"Action": "sts:AssumeRoleWithSAML",
"Condition": {
"StringEquals": {
"SAML:aud": "https://signin.aws.amazon.com/saml"
}
}
}
]
}
创建角色:
aws iam create-role \
--role-name EKS-SAML-User-Access \
--assume-role-policy-document file://saml_user_role_trust_policy.json
注意:这个角色不需要附加任何AWS权限策略。它的唯一目的就是作为AWS和Kubernetes之间的身份桥梁。对集群资源的访问权限将完全由Kubernetes RBAC控制。这是一个常见的错误认知,即试图在这个IAM Role上附加EKS相关的权限。
3.2 映射IAM Role到Kubernetes RBAC
EKS通过一个名为aws-auth的特殊ConfigMap来管理IAM身份到Kubernetes身份的映射。这个ConfigMap位于kube-system命名空间。
# 获取现有的aws-auth ConfigMap
kubectl get configmap aws-auth -n kube-system -o yaml > aws-auth.yaml
编辑aws-auth.yaml文件,在mapRoles部分添加我们的SAML角色映射。
aws-auth.yaml (片段):
apiVersion: v1
kind: ConfigMap
metadata:
name: aws-auth
namespace: kube-system
data:
mapRoles: |
- rolearn: arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/EKS-SAML-User-Access
username: "{{SessionName}}"
groups:
- ray-cluster-users
# ... 其他已存在的角色映射,例如EKS节点组的角色
-
rolearn: 我们在上一步创建的SAML角色的ARN。 -
username: "{{SessionName}}": 这是一个模板变量。当用户通过SAML登录时,他们的IdP用户名(例如[email protected])会被作为SessionName传递。这确保了每个用户在Kubernetes中都有一个唯一的身份。 -
groups: 这是最关键的部分。我们将所有通过SAML登录的用户都分配到了ray-cluster-users这个Kubernetes组。
现在,我们可以创建一个ClusterRole和ClusterRoleBinding来赋予这个组具体的权限。
ray-user-rbac.yaml:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: ray-user-role
rules:
- apiGroups: ["", "ray.io"]
resources: ["pods", "pods/log", "services", "rayclusters", "rayjobs"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods/portforward"]
verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: ray-user-binding
subjects:
- kind: Group
name: ray-cluster-users # 绑定到我们在aws-auth中定义的组
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: ray-user-role
apiGroup: rbac.authorization.k8s.io
这个RBAC策略赋予了ray-cluster-users组的成员查看Ray相关资源、获取日志以及执行port-forward(用于访问Dashboard)的权限,但禁止他们修改或删除任何东西。
应用这些配置:
# 更新aws-auth ConfigMap
kubectl apply -f aws-auth.yaml
# 创建RBAC规则
kubectl apply -f ray-user-rbac.yaml
3.3 工程师的本地kubeconfig配置
最后一步是告诉工程师如何配置他们的本地环境。他们的kubeconfig文件不再包含任何密钥,而是定义了一个执行器。
~/.kube/config (片段):
# ... cluster和context定义 ...
users:
- name: saml-user
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: aws
args:
- "eks"
- "get-token"
- "--cluster-name"
- "YOUR_EKS_CLUSTER_NAME"
# --role-arn 是可选的,但建议显式指定以避免混淆
# - "--role-arn"
# - "arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/EKS-SAML-User-Access"
env:
- name: "AWS_PROFILE"
value: "saml" # 假设用户的AWS CLI配置了一个用于SAML登录的profile
当工程师运行任何kubectl命令时,exec部分会被触发,aws eks get-token命令会启动SAML登录流程,获取临时凭证,并最终为kubectl提供一个有效的令牌。
整体架构与流程的可视化
为了更清晰地展示这两个并行的认证流程,我们可以用Mermaid图来描绘它们。
CI/CD OIDC认证流程:
sequenceDiagram
participant GH as GitHub Actions Runner
participant GHOIDC as GitHub OIDC Provider
participant AWS_STS as AWS STS
participant EKS as EKS API Server
GH->>GHOIDC: 请求JWT (携带仓库/分支信息)
GHOIDC-->>GH: 返回JWT
GH->>AWS_STS: 调用AssumeRoleWithWebIdentity(JWT)
Note right of AWS_STS: 验证JWT签名和声明
(repo: your-org/your-repo:ref:refs/heads/main)
AWS_STS-->>GH: 颁发临时AWS凭证
GH->>EKS: 使用临时凭证执行kubectl/helm命令
EKS-->>GH: 操作成功
人类用户SAML认证流程:
sequenceDiagram
participant User as 工程师
participant CLI as kubectl / aws-iam-authenticator
participant IdP as SAML IdP
participant AWS_STS as AWS STS
participant EKS as EKS API Server
participant AuthMap as aws-auth ConfigMap
User->>CLI: 执行 kubectl get pods
CLI->>IdP: 重定向用户进行SSO登录 (通过浏览器)
IdP-->>CLI: 返回SAML断言
CLI->>AWS_STS: 调用AssumeRoleWithSAML(SAML断言)
AWS_STS-->>CLI: 颁发临时AWS凭证
CLI->>EKS: 使用凭证生成并出示K8s令牌
EKS->>AWS_STS: 验证令牌,获取IAM Role ARN
AWS_STS-->>EKS: 令牌有效,返回ARN: EKS-SAML-User-Access
EKS->>AuthMap: 查找ARN映射
AuthMap-->>EKS: 映射到K8s组: ray-cluster-users
Note right of EKS: 基于RBAC检查 'ray-cluster-users' 组
是否有 'get pods' 权限
EKS-->>User: 返回pods列表
当前方案的局限性与未来展望
我们成功地构建了一个健壮、安全、可审计的自动化部署与访问系统。它消除了长期密钥,并将身份验证的责任委托给了专门的身份提供商。然而,这个方案并非没有改进空间。
aws-auth ConfigMap的管理是一个潜在的痛点。随着角色和用户的增多,这个单一文件会变得臃肿且难以维护,容易因手动编辑而出错。未来的一个迭代方向是探索使用AWS IAM Roles for Service Accounts (IRSA)来更精细化地管理Pod级别的权限,或者引入像Teleport这样的身份感知访问代理,将Kubernetes的认证与授权从aws-auth中解耦出来,实现更动态和基于角色的访问控制。
对于Ray Dashboard的访问,当前的RBAC配置允许用户通过kubectl port-forward来访问,这在安全和易用性上都不是最优解。更优雅的方案是在EKS中部署一个OAuth2-proxy,将其置于Dashboard Service的前面,并将它与我们的SAML IdP集成,从而为Web界面提供真正的SSO体验。
最后,当前的流水线专注于部署。一个完整的GitOps循环还应包括环境清理、预览环境的动态创建与销毁,以及更复杂的发布策略(如金丝雀发布)。这些都可以基于当前这个坚实的身份认证基础之上,作为后续平台工程建设的一部分逐步完善。