使用AWS Workload Credentials Provider在EKS中管理应用密钥的实践

本文档完整记录了在中国区 EKS 集群上部署 AWS Workload Credentials Provider 的全过程。通过 Sidecar 模式,应用可以无感知地获取 AWS Secrets Manager 中的敏感配置,无需在代码中处理复杂的 AWS 认证逻辑。

在 Kubernetes 中管理 Secret 始终是个让人头疼的问题。传统的工作流中,每个开发团队都要自己实现一套从 AWS 获取 Secret 的逻辑------有的用 SDK,有的用 init container,有的干脆写死在配置文件里。这不仅重复造轮子,还带来了严重的安全隐患。

AWS Workload Credentials Provider(WCP)的出现改变了这一切。作为 AWS Secrets Manager Agent 的演进版本,它采用了 Sidecar 代理模式------你在 Pod 中描述想要的 Secret,WCP 负责获取、缓存和提供。

在我们的实践中,整个系统体现了优雅的分层思想。这种分层设计让每个组件职责单一,便于独立演进和故障隔离。具体来说,我们将整个架构划分为四个层次:

系统分层架构

层级 组件 职责
基础设施层 EKS 集群、ECR、VPC 提供计算和网络资源
身份层 EKS Pod Identity、IAM 托管 AWS 凭证
代理层 WCP Sidecar Secret 获取、缓存、刷新
应用层 Agent 应用 业务逻辑,无感知使用 Secret

一个典型的 Pod 包含两个容器协同工作:

  • Agent 容器(业务应用):运行 LLM 代理服务,通过 localhost 访问 Secret
  • WCP 容器(Sidecar):提供本地 HTTP 服务,缓存来自 Secrets Manager 的 API Key

WCP 网络架构与通信模型

在深入部署之前,理解 WCP 的通信模型至关重要。WCP 的设计哲学是将 Secret 管理下沉到基础设施层,让应用容器无感知地使用。下图展示了 Pod 内部的网络拓扑
#mermaid-svg-5BDfbgMCFpF5DsZJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5BDfbgMCFpF5DsZJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5BDfbgMCFpF5DsZJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5BDfbgMCFpF5DsZJ .error-icon{fill:#552222;}#mermaid-svg-5BDfbgMCFpF5DsZJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5BDfbgMCFpF5DsZJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5BDfbgMCFpF5DsZJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5BDfbgMCFpF5DsZJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5BDfbgMCFpF5DsZJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5BDfbgMCFpF5DsZJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5BDfbgMCFpF5DsZJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5BDfbgMCFpF5DsZJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5BDfbgMCFpF5DsZJ .marker.cross{stroke:#333333;}#mermaid-svg-5BDfbgMCFpF5DsZJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5BDfbgMCFpF5DsZJ p{margin:0;}#mermaid-svg-5BDfbgMCFpF5DsZJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5BDfbgMCFpF5DsZJ .cluster-label text{fill:#333;}#mermaid-svg-5BDfbgMCFpF5DsZJ .cluster-label span{color:#333;}#mermaid-svg-5BDfbgMCFpF5DsZJ .cluster-label span p{background-color:transparent;}#mermaid-svg-5BDfbgMCFpF5DsZJ .label text,#mermaid-svg-5BDfbgMCFpF5DsZJ span{fill:#333;color:#333;}#mermaid-svg-5BDfbgMCFpF5DsZJ .node rect,#mermaid-svg-5BDfbgMCFpF5DsZJ .node circle,#mermaid-svg-5BDfbgMCFpF5DsZJ .node ellipse,#mermaid-svg-5BDfbgMCFpF5DsZJ .node polygon,#mermaid-svg-5BDfbgMCFpF5DsZJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5BDfbgMCFpF5DsZJ .rough-node .label text,#mermaid-svg-5BDfbgMCFpF5DsZJ .node .label text,#mermaid-svg-5BDfbgMCFpF5DsZJ .image-shape .label,#mermaid-svg-5BDfbgMCFpF5DsZJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-5BDfbgMCFpF5DsZJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5BDfbgMCFpF5DsZJ .rough-node .label,#mermaid-svg-5BDfbgMCFpF5DsZJ .node .label,#mermaid-svg-5BDfbgMCFpF5DsZJ .image-shape .label,#mermaid-svg-5BDfbgMCFpF5DsZJ .icon-shape .label{text-align:center;}#mermaid-svg-5BDfbgMCFpF5DsZJ .node.clickable{cursor:pointer;}#mermaid-svg-5BDfbgMCFpF5DsZJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5BDfbgMCFpF5DsZJ .arrowheadPath{fill:#333333;}#mermaid-svg-5BDfbgMCFpF5DsZJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5BDfbgMCFpF5DsZJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5BDfbgMCFpF5DsZJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5BDfbgMCFpF5DsZJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5BDfbgMCFpF5DsZJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5BDfbgMCFpF5DsZJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5BDfbgMCFpF5DsZJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5BDfbgMCFpF5DsZJ .cluster text{fill:#333;}#mermaid-svg-5BDfbgMCFpF5DsZJ .cluster span{color:#333;}#mermaid-svg-5BDfbgMCFpF5DsZJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5BDfbgMCFpF5DsZJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5BDfbgMCFpF5DsZJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-5BDfbgMCFpF5DsZJ .icon-shape,#mermaid-svg-5BDfbgMCFpF5DsZJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5BDfbgMCFpF5DsZJ .icon-shape p,#mermaid-svg-5BDfbgMCFpF5DsZJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5BDfbgMCFpF5DsZJ .icon-shape .label rect,#mermaid-svg-5BDfbgMCFpF5DsZJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5BDfbgMCFpF5DsZJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5BDfbgMCFpF5DsZJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5BDfbgMCFpF5DsZJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 外部访问
Pod网络命名空间
localhost:2773
HTTPS
AssumeRole
注入凭证
HTTP
Agent App

:8080
WCP Sidecar

HTTP Server
AWS Secrets Manager
EKS Pod Identity
用户

通信时序(Agent 调用 LLM 的完整流程)
GLM-5.2 API Secrets Manager WCP Sidecar Agent App 用户 GLM-5.2 API Secrets Manager WCP Sidecar Agent App 用户 #mermaid-svg-fHalPDfKBf69eSij{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fHalPDfKBf69eSij .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fHalPDfKBf69eSij .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fHalPDfKBf69eSij .error-icon{fill:#552222;}#mermaid-svg-fHalPDfKBf69eSij .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fHalPDfKBf69eSij .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fHalPDfKBf69eSij .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fHalPDfKBf69eSij .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fHalPDfKBf69eSij .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fHalPDfKBf69eSij .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fHalPDfKBf69eSij .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fHalPDfKBf69eSij .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fHalPDfKBf69eSij .marker.cross{stroke:#333333;}#mermaid-svg-fHalPDfKBf69eSij svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fHalPDfKBf69eSij p{margin:0;}#mermaid-svg-fHalPDfKBf69eSij .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fHalPDfKBf69eSij text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-fHalPDfKBf69eSij .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-fHalPDfKBf69eSij .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-fHalPDfKBf69eSij .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-fHalPDfKBf69eSij .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-fHalPDfKBf69eSij #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-fHalPDfKBf69eSij .sequenceNumber{fill:white;}#mermaid-svg-fHalPDfKBf69eSij #sequencenumber{fill:#333;}#mermaid-svg-fHalPDfKBf69eSij #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-fHalPDfKBf69eSij .messageText{fill:#333;stroke:none;}#mermaid-svg-fHalPDfKBf69eSij .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fHalPDfKBf69eSij .labelText,#mermaid-svg-fHalPDfKBf69eSij .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-fHalPDfKBf69eSij .loopText,#mermaid-svg-fHalPDfKBf69eSij .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-fHalPDfKBf69eSij .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-fHalPDfKBf69eSij .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-fHalPDfKBf69eSij .noteText,#mermaid-svg-fHalPDfKBf69eSij .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-fHalPDfKBf69eSij .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fHalPDfKBf69eSij .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fHalPDfKBf69eSij .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fHalPDfKBf69eSij .actorPopupMenu{position:absolute;}#mermaid-svg-fHalPDfKBf69eSij .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-fHalPDfKBf69eSij .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fHalPDfKBf69eSij .actor-man circle,#mermaid-svg-fHalPDfKBf69eSij line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-fHalPDfKBf69eSij :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 1. Pod 启动阶段 TTL 300秒,预取完成 2. 请求处理阶段 Header: X-Aws-Parameters-Secrets-Token 无需调用 AWS API GetSecretValue(llm/api/config)返回 Secret + 缓存到内存POST /chat {"message":"你好"}GET localhost:2773/secretsmanager/get返回 Secret (缓存命中 <1ms)POST open.bigmodel.cn/...Authorization: Bearer <API Key>返回 LLM 响应{"response":"我是GLM..."}

在整个通信过程中,WCP 的性能表现是我们最关心的指标。基于实际测试,我们收集了以下关键性能数据:

指标 数值 说明
缓存命中延迟 ~1ms 内存读取,无网络 IO
缓存未命中 ~50-100ms 需调用 AWS API
默认 TTL 300s 可配置
预取数量 2 个 llm/api/config, test/wcp/demo

在决定使用 Sidecar 模式之前,我们对比了三种不同的部署方案。每种方案都有其特定的适用场景和权衡。建议生产环境使用 Sidecar 模式。WCP 的设计初衷就是 per-Pod 代理,每个应用有自己的缓存实例,故障域隔离。

方案 优点 缺点 适用场景
Sidecar 隔离性好,故障不扩散,缓存独立 额外容器开销 推荐,生产环境
DaemonSet 资源共享,节点级缓存 单点故障,权限共享 测试环境
SDK 直连 简单直接 重复造轮子,无缓存 不推荐

配置详解

以下是完整的 Dockerfile,包含了 Rust 依赖的中国区镜像加速配置:

dockerfile 复制代码
#==============================================================================
# Stage 1: Builder
#==============================================================================
FROM public.ecr.aws/docker/library/rust:1.92-bookworm AS builder

# 配置中国镜像加速 (Rust crates)
RUN mkdir -p /usr/local/cargo && \
    echo '[source.crates-io]' > /usr/local/cargo/config.toml && \
    echo 'replace-with = "rsproxy-sparse"' >> /usr/local/cargo/config.toml && \
    echo '[source.rsproxy-sparse]' >> /usr/local/cargo/config.toml && \
    echo 'registry = "sparse+https://rsproxy.cn/index/"' >> /usr/local/cargo/config.toml && \
    echo '[net]' >> /usr/local/cargo/config.toml && \
    echo 'git-fetch-with-cli = true' >> /usr/local/cargo/config.toml

WORKDIR /src
COPY . .
RUN CARGO_HOME=/usr/local/cargo cargo build --release && \
    cp /src/target/release/aws-workload-credentials-provider /aws-workload-credentials-provider

#==============================================================================
# Stage 2: Runtime
#==============================================================================
FROM public.ecr.aws/docker/library/debian:bookworm-slim

RUN apt-get update && \
    apt-get install -y --no-install-recommends ca-certificates && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY --from=builder /aws-workload-credentials-provider /app/aws-workload-credentials-provider
RUN chmod 0755 /app/aws-workload-credentials-provider

ENTRYPOINT ["/app/aws-workload-credentials-provider"]
CMD ["sm", "start", "--config", "/etc/wcp/config.toml"]

在这个 Dockerfile 中,有几个关键配置需要特别注意:

配置 说明
rsproxy-sparse 中国区 Rust crates 镜像,加速依赖下载
bookworm Debian 12,glibc 2.36
readOnlyRootFilesystem: true 运行时根文件系统只读(安全最佳实践)

ConfigMap

WCP 采用 TOML 格式配置。核心参数说明:

配置项 默认值 说明
ttl_seconds 300 Secret 缓存过期时间(秒)
cache_size 1000 最大缓存 Secret 数量
http_port 2773 WCP 监听端口
ssrf_headers X-Aws-Parameters-Secrets-Token 请求必须携带的 SSRF 防护头部
prefetch.secrets - 启动时预取的 Secret 列表
yaml 复制代码
apiVersion: v1
kind: ConfigMap
metadata:
  name: wcp-config
  namespace: <namespace>
data:
  config.toml: |
    [logging]
    log_level = "INFO"
    log_to_file = false

    [capabilities.secrets_manager]
    enabled = true
    http_port = 2773
    region = "<region>"
    max_conn = 800

    [capabilities.secrets_manager.cache]
    ttl_seconds = 300
    cache_size = 1000

    [capabilities.secrets_manager.security]
    ssrf_headers = ["X-Aws-Parameters-Secrets-Token"]
    ssrf_env_variables = ["AWS_TOKEN"]

    [capabilities.secrets_manager.prefetch]
    cache_buffer_ratio = 0.8
    max_jitter_seconds = 5

    [[capabilities.secrets_manager.prefetch.secrets]]
    secret_id = "llm/api/config"

Deployment

Sidecar 模式 Deployment 关键配置:

配置项 说明
localhost:2773 WCP 只监听 127.0.0.1,仅本 Pod 可访问
emptyDir (Memory) WCP 使用内存临时存储
serviceAccountName 绑定 EKS Pod Identity 的 ServiceAccount
yaml 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: agent-app
  namespace: <namespace>
spec:
  replicas: 1
  template:
    spec:
      serviceAccountName: wcp-app-sa
      containers:
        - name: agent
          image: <account-id>.dkr.ecr.<region>.amazonaws.com.cn/agent-app:latest
          ports:
            - containerPort: 8080
          env:
            - name: WCP_HOST
              value: "localhost"
            - name: WCP_PORT
              value: "2773"
            - name: WCP_TOKEN
              value: "<namespace>-token-12345"
            - name: SECRET_NAME
              value: "llm/api/config"
          readinessProbe:
            httpGet:
              path: /health
              port: 8080

        - name: wcp-provider
          image: <account-id>.dkr.ecr.<region>.amazonaws.com.cn/wcp:latest
          args:
            - sm
            - start
            - --config
            - /etc/wcp/config.toml
          env:
            - name: AWS_REGION
              value: <region>
            - name: AWS_TOKEN
              value: "<namespace>-token-12345"
          volumeMounts:
            - name: wcp-config
              mountPath: /etc/wcp
              readOnly: true
            - name: wcp-tmp
              mountPath: /tmp
      volumes:
        - name: wcp-config
          configMap:
            name: wcp-config
        - name: wcp-tmp
          emptyDir:
            medium: Memory

Agent 应用代码

Agent 应用通过 localhost:2773 访问 WCP 获取 Secret,必须携带 SSRF Token:

go 复制代码
func getEnv(key, defaultValue string) string {
    if value := os.Getenv(key); value != "" {
        return value
    }
    return defaultValue
}

func getSecretFromWCP() (*LLMConfig, error) {
    url := fmt.Sprintf("http://%s:%s/secretsmanager/get?secretId=%s", 
        getEnv("WCP_HOST", "localhost"),
        getEnv("WCP_PORT", "2773"),
        getEnv("SECRET_NAME", "llm/api/config"))

    req, _ := http.NewRequest("GET", url, nil)
    req.Header.Set("X-Aws-Parameters-Secrets-Token", 
        getEnv("WCP_TOKEN", ""))

    client := &http.Client{Timeout: 10 * time.Second}
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var result struct {
        SecretString string `json:"SecretString"`
    }
    json.NewDecoder(resp.Body).Decode(&result)

    var llmConfig LLMConfig
    json.Unmarshal([]byte(result.SecretString), &llmConfig)

    return &llmConfig, nil
}

测试验证

Pod 状态检查:

bash 复制代码
$ kubectl get pods -n <namespace> -o wide
NAME                            READY   STATUS    RESTARTS   AGE
agent-app-748ddb77d5-mwbms      2/2     Running   0          5m

WCP 日志确认预取成功:

复制代码
2026-06-28T11:18:40.818Z INFO - listening on http://127.0.0.1:2773
2026-06-28T11:18:45.728Z INFO - Pre-fetch complete: success=2, failed=0

Health 检查:

bash 复制代码
$ kubectl exec -n <namespace> deployment/agent-app -c agent -- \
  curl -s http://localhost:8080/health

{"status":"healthy"}

Chat 对话测试:

bash 复制代码
$ kubectl exec -n <namespace> deployment/agent-app -c agent -- \
  curl -s -X POST http://localhost:8080/chat \
  -H "Content-Type: application/json" \
  -d '{"message":"你好"}'

响应示例:

json 复制代码
{
  "model": "GLM-5.2",
  "response": "我是GLM,由Z.ai开发的大语言模型..."
}

整个方案的核心价值在于:让应用专注于业务逻辑,将 Secret 管理交给基础设施。WCP 的本地缓存机制将 Secret 获取延迟从 50-100ms 降至 1ms,同时 SSRF 防护确保了即使在高风险场景下也能安全地提供 Secret 服务。

最后需要说明的是,本文档只展示了 WCP 的 Secrets Manager 能力。实际上 WCP 还支持 ACM 证书导出 功能------它可以自动从 AWS Certificate Manager 导出 SSL/TLS 证书到本地文件系统,并支持证书到期自动刷新和自定义刷新命令。如果你的应用需要管理 HTTPS 证书,WCP 同样可以通过 Sidecar 或系统服务的方式提供自动化的证书管理。