4、创建 Secrets API:POST /secrets,请求体、响应体

来了,到真正动手的环节了。

上一章我们把 Secret 的资源模型拆开了。今天咱们就把它用 POST /secrets 这个 API 给「造」出来。说白了,就是客户端发个请求,服务端收下、校验、落地,然后告诉客户端「成了,这是你新生的 Secret 的身份证」。我刚开始写这类接口时,总觉得 request body 无非就是几个字段,后来被线上事故教育过,才发现细节多得让人头皮发麻。

4.1 请求体设计:不让脏数据混进来

我们先看客户端应该往服务端发什么。

根据 Kubernetes 的惯例(当然你可以根据自己的场景裁剪),创建一个 Secret 需要三个核心部分:

层级字段类型说明
顶层apiVersionstring固定为 v1
kindstring固定为 Secret
metadatanamestring必须唯一,只能小写字母、数字、连字符、点,最长 253 字符
labelsmap[string]string可选,但强烈建议带上环境标签、团队标签
annotationsmap[string]string用于说明用途、过期时间等元信息
data(键值对)map[string][]byte所有值必须 Base64 编码
stringData(键值对)map[string]string明文写入,服务端自动转为 Base64 存入 data
type—string默认为 Opaque,指定类型如 kubernetes.io/tls

嗯,这里要注意一个细节:data 和 stringData 不能同时设置同一个 key。如果你两个都写了,stringData 会覆盖 data 里对应的值。我个人习惯只用 stringData 做创建,因为开发阶段明文写着好调试,生产环境再用 CI/CD 转成 Base64 传入 data。这样做既能避免明文泄露,又能保持可读性。

为什么要 Base64?

Base64 不是加密,它只是编码。Secret 的数据在 Kubernetes 里就是存成 Base64 的,这样能兼容纯文本协议,同时也给未来加密存储腾出空间。你想想看,如果存明文,万一 etcd 被扫一眼就全曝光了。Base64 好歹让肉眼不能直接看见密码,但注意——它不是安全方案,后面我们会讲怎么用加密保护。

下面是一个完整的请求示例:

POST /api/v1/secrets
Content-Type: application/json

{
  "apiVersion": "v1",
  "kind": "Secret",
  "metadata": {
    "name": "db-credentials",
    "labels": {
      "env": "production",
      "team": "backend"
    },
    "annotations": {
      "description": "Database user/password for production RDS"
    }
  },
  "type": "Opaque",
  "stringData": {
    "username": "app_user",
    "password": "P@ssw0rd!@#"
  }
}

注意我用了 stringData,所以客户端不需要手动做 Base64 编码。服务端收到后,会把 username 和 password 转成 Base64 再写入内部的 data 字段。

4.2 响应体设计:给客户端一个「确认回执」

请求发出去,服务端校验通过,创建成功。这时候应该返回什么?

  • 状态码:201 Created。这是 RESTful 创建资源的标准,不是 200,也不是 202。
  • 响应体:返回完整的 Secret 对象,但注意 data 字段会被省略(或只返回 key 名称,不返回 value),这是安全考量。当然,如果你在内部集群里调用,也可以返回完整 data,但大多数场景建议只返回 metadata 和 type。
  • Location 头部:指向新资源的 URL,比如 /api/v1/secrets/db-credentials,方便客户端直接拿这个 URL 去 GET。

我遇到过一种情况:客户端的 SDK 只检查 200,不检查 201,结果创建成功但它认为失败了,重试了一遍,导致重复创建。所以客户端也要注意,服务端更要在文档里写清楚。

我个人习惯的响应结构:

响应体里只返回 metadata 和 type,以及一个 status 字段标记为 "Created"。如果有 audit 需求,可以在 headers 里加一个 X-Secret-Version,指向资源版本号。方便客户端做乐观锁。

来看一个典型的成功响应:

HTTP/1.1 201 Created
Location: /api/v1/secrets/db-credentials
Content-Type: application/json

{
  "apiVersion": "v1",
  "kind": "Secret",
  "metadata": {
    "name": "db-credentials",
    "uid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "resourceVersion": "12345",
    "creationTimestamp": "2025-04-10T08:30:00Z",
    "labels": { "env": "production", "team": "backend" }
  },
  "type": "Opaque",
  "data": {
    "username": "",
    "password": ""
  }
}

data 里的字段我给了空字符串,表示这里本来有值但不展示了。如果你觉得这样会误导客户端,干脆不返回 data 字段,只返回 dataKeys 数组也可以。嗯,没有标准答案,但团队内要统一。

4.3 异常情况:不能只考虑 happy path

创建接口最容易踩的坑,我列几个:

我曾经在线上遇到过的问题:

  • 409 Conflict —— Secret 名称已存在。这时服务端要明确告诉你:要么你用 PUT 更新,要么你换个名字。千万不要自动覆盖。
  • 422 Unprocessable Entity —— 请求体校验失败,比如 name 含大写字母,或者 data 里的 value 不是合法的 Base64。我会在错误体里放一个 details 数组,精确指出哪个字段有问题。
  • 400 Bad Request —— 请求体 JSON 语法错误,或者缺少必填字段(name 和至少一个 data 或 stringData 字段)。

看看错误响应的标准格式(也建议参考 RFC 7807 Problem Details):

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json

{
  "type": "https://yourdomain.com/errors/invalid-secret",
  "title": "Invalid Secret",
  "status": 422,
  "detail": "The secret name must only contain lowercase alphanumeric characters, '-' or '.'.",
  "instance": "/api/v1/secrets",
  "invalidFields": [
    {
      "field": "metadata.name",
      "reason": "contains uppercase 'DB-credentials'"
    }
  ]
}

这种结构能帮前端或 CLI 工具精准定位错误。你想想看,如果只返回一句「创建失败」,排查起来多痛苦。

4.4 整个流程的视觉概括

为了让思路更清晰,我用一张图把从请求到响应,以及中间校验、存储的关键步骤串起来。

POST /secrets 创建流程 客户端 发送 POST 请求 校验器 字段格式、必填、Base64 授权检查 RBAC / ABAC 存储引擎 写入 etcd / 数据库 响应组装器 返回 metadata + type 关键决策点 ① 名称唯一性检查 ② 自动转 Base64(stringData) ③ 生成 uid / resourceVersion

从图里可以看到,请求过了校验和授权两道关,才进到存储层。存储成功后,响应组装器会 strip 掉 data 字段的值,只留一个占位。虚线代表并行或异步链路,比如更新审计日志、触发回调等,这里就没画太细了。

4.5 最佳实践:接口设计时的几条建议

  • 幂等性要明确:POST 默认不幂等,但你可以让服务端做名称去重检测,避免重复创建。如果需要幂等创建,建议改用 PUT /secrets/{name},客户端自己控制名称。
  • 限制请求体大小:Secret 的 value 不要太大。Kubernetes 建议单个 Secret 不超过 1MB,我的项目里限制在 512KB 以内,否则 etcd 容易爆。
  • 给每个 Secret 打上 audit 标签:比如 audit.created-by annotation 记录操作人。我曾经因为没记录是谁建的,出问题后抓了三天日志才定位到。
  • 支持 DryRun:加一个 dryRun=All 查询参数,让客户端可以先校验请求是否合法,不真正写入。这样 CI/CD 流水线就能在 merge 前验证 YAML 的合法性。

一个小小的安全提醒:

响应体里不要返回 data 的明文值,哪怕客户端就是集群内的服务。因为响应体可能被日志系统、监控中间件截获。如果你实在需要让客户端拿到 Secret,可以单独提供一个 GET /secrets/{name}?includeData=true 并加强权限控制,而不是在创建时返回。