4、创建 Secrets API:POST /secrets,请求体、响应体
来了,到真正动手的环节了。
上一章我们把 Secret 的资源模型拆开了。今天咱们就把它用 POST /secrets 这个 API 给「造」出来。说白了,就是客户端发个请求,服务端收下、校验、落地,然后告诉客户端「成了,这是你新生的 Secret 的身份证」。我刚开始写这类接口时,总觉得 request body 无非就是几个字段,后来被线上事故教育过,才发现细节多得让人头皮发麻。
4.1 请求体设计:不让脏数据混进来
我们先看客户端应该往服务端发什么。
根据 Kubernetes 的惯例(当然你可以根据自己的场景裁剪),创建一个 Secret 需要三个核心部分:
| 层级 | 字段 | 类型 | 说明 |
|---|---|---|---|
| 顶层 | apiVersion | string | 固定为 v1 |
| kind | string | 固定为 Secret | |
| metadata | name | string | 必须唯一,只能小写字母、数字、连字符、点,最长 253 字符 |
| labels | map[string]string | 可选,但强烈建议带上环境标签、团队标签 | |
| annotations | map[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 整个流程的视觉概括
为了让思路更清晰,我用一张图把从请求到响应,以及中间校验、存储的关键步骤串起来。
从图里可以看到,请求过了校验和授权两道关,才进到存储层。存储成功后,响应组装器会 strip 掉 data 字段的值,只留一个占位。虚线代表并行或异步链路,比如更新审计日志、触发回调等,这里就没画太细了。
4.5 最佳实践:接口设计时的几条建议
- 幂等性要明确:POST 默认不幂等,但你可以让服务端做名称去重检测,避免重复创建。如果需要幂等创建,建议改用 PUT /secrets/{name},客户端自己控制名称。
- 限制请求体大小:Secret 的 value 不要太大。Kubernetes 建议单个 Secret 不超过 1MB,我的项目里限制在 512KB 以内,否则 etcd 容易爆。
- 给每个 Secret 打上 audit 标签:比如
audit.created-byannotation 记录操作人。我曾经因为没记录是谁建的,出问题后抓了三天日志才定位到。 - 支持 DryRun:加一个
dryRun=All查询参数,让客户端可以先校验请求是否合法,不真正写入。这样 CI/CD 流水线就能在 merge 前验证 YAML 的合法性。
一个小小的安全提醒:
响应体里不要返回 data 的明文值,哪怕客户端就是集群内的服务。因为响应体可能被日志系统、监控中间件截获。如果你实在需要让客户端拿到 Secret,可以单独提供一个 GET /secrets/{name}?includeData=true 并加强权限控制,而不是在创建时返回。