在Kubernetes环境里跑Java后端服务,时间一长必然会撞上“API密钥往哪放”的难题。美团开放平台的appKey和appSecret,如果像老项目那样直接写进application.yml或Dockerfile,一旦代码仓库被误设为公开,或者构建镜像被拉取,密钥就裸奔了。我见过不止一次因为这类事故导致账号被刷、费用异常的案例。这篇文章就把我实际整理出来的K8s Secret管理方案完整讲一遍,从创建、注入到权限加固、问题排查,覆盖Java服务接入美团API密钥的所有关键环节。适合刚把Spring Boot服务迁到K8s的开发,也适合被业务方追着问“为什么密钥又泄露”的运维和SRE。
1. 为什么我坚持不把美团密钥写进application.yml
1.1 先复盘一次真实的密钥泄露现场
去年帮一个朋友排查线上事故,他们的Java服务调用美团API时一直报签名错误,我登录到容器里一看环境变量,MEITUAN_API_KEY和MEITUAN_API_SECRET直接躺在进程环境里,而且这两个值是从ConfigMap注入的。我问他们ConfigMap怎么来的,回答是部署时用kubectl从application-prod.yml转出来的,而那份YAML就放在GitLab的一个私有仓库里。
问题就出在这条链路上。后来某个同事把仓库可见性调成了Internal,几天后美团那边报警说该账号有异常查询,费用突然涨了好几倍。虽然最后没有造成永久性损失,但整个复盘过程非常痛苦:密钥怎么泄露的、被谁拉走了、有没有被用于其他接口,全部要靠猜。所以我的第一个原则就是:敏感信息绝不能进入代码仓库、不能进入镜像、不能进入ConfigMap明文。
1.2 Secret和ConfigMap到底差在哪
很多人会问:我都用Kubernetes了,为什么不把配置全部塞进ConfigMap,还要单独搞一个Secret?这两个资源在用法上几乎一样,都可以通过环境变量或文件挂载的方式交给Pod,但设计定位完全不同。
ConfigMap面向的是非敏感的配置文本,比如服务端口、日志级别、开关项。Secret则专门用来保存需要保护的数据,比如数据库密码、第三方API的appKey、私钥证书。Secret在Kubernetes里有一层额外的保护机制,比如可以配合RBAC做细粒度授权,可以设置immutable防止误改,可以在etcd层加密。更重要的是,Secret在序列化时会做一次base64编码,但这只是一种传输编码,不是加密。
一定要破除一个误区:不要以为Secret里存的base64字符串就是安全的。base64本质上就是明文的一种变形,任何能读取Secret的人都可以用一行命令解出来。Secret的安全取决于Kubernetes的访问控制、etcd存储加密,以及你周边的管控流程,而不是那一层编码。这也是我在团队分享时反复强调的:Secret只是提供了“更安全处理敏感信息的机制”,不代表你用了Secret就万事大吉。
1.3 Kubernetes Secret的几种类型,美团场景该选哪种
Secret的类型由type字段标识,最常用的是Opaque,也就是普通的不透明数据。针对美团开放平台API密钥,直接用Opaque就够了。但我在实际项目里还会遇到其他场景,这里一并列出来:
| Secret类型 | 用途 | 示例 |
|---|---|---|
| Opaque | 存任意键值对,最适合API密钥、数据库密码 | appKey、appSecret |
| kubernetes.io/dockerconfigjson | 存储Docker registry凭据 | 拉取私有镜像仓库 |
| kubernetes.io/tls | 存储TLS证书和私钥 | Ingress证书 |
| kubernetes.io/service-account-token | ServiceAccount的Token | 集群内部认证 |
如果你同时还需要从私有镜像仓库拉取Java运行基础镜像,那需要单独创建一个dockerconfigjson类型的Secret,用kubectl create secret docker-registry来生成,不要把配置文件里的认证信息手动塞进Opaque Secret。我在一个项目里见过有人把整个.docker/config.json当成Opaque的value来挂载,最后因为换行和缩进问题折腾了很久,完全没必要。
2. 创建和管理Secret的正确姿势
2.1 从kubectl命令创建,但要注意命令行泄露
对美团API密钥这种临时性Secret,我通常先用kubectl create secret快速验证。某天我创建了一个测试用密钥:
kubectl create secret generic meituan-api-key \ --namespace=meituan-prod \ --from-literal=appKey=your-app-key \ --from-literal=appSecret=your-app-secret这种方式适合快速打通链路,但有两点要提醒大家。第一,--from-literal会完整出现在shell历史里,如果你用的是共享跳板机,其他登录用户可能从~/.bash_history里看到密钥。第二,如果团队有审计要求,这种方式几乎无法追踪密钥的创建者。
因此,我更推荐的方式是先用本地文件保存密钥,再通过--from-file导入。比如把美团密钥写在一个只有自己能读的临时文件里:
kubectl create secret generic meituan-api-key \ --namespace=meituan-prod \ --from-file=appKey=./meituan-appKey.txt \ --from-file=appSecret=./meituan-appSecret.txt注意文件内容末尾的换行符问题,这个后面在排障章节会专门讲。
2.2 用YAML声明式管理,配合Kustomize避免明文入库
做正经项目时,我更推荐把Secret以声明式方式管理。但别直接把编码后的字符串写死在Git里,否则等于把密钥从application.yml搬到了Secret仓库。一个折中方案是使用Kustomize的secretGenerator,它可以在部署渲染时动态生成Secret,并对value做正确编码。
举个例子,在kustomization.yaml里:
apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization secretGenerator: - name: meituan-api-key namespace: meituan-prod type: Opaque envs: - meituan-secret.env然后在meituan-secret.env文件里写上:
APP_KEY=your-app-key APP_SECRET=your-app-secret我通常会把.env文件加入.gitignore,或者用sops等工具对文件做加密后再入Git。Kustomize在kubectl apply -k .时会把APP_KEY和APP_SECRET自动变成Secret的data字段,并且会通过hash后缀来触发Deployment的滚动更新,这个特性在后面排障里很有用。
2.3 命名、标签和命名空间隔离
Secret管理不能只看一个资源,需要从可维护性角度做规范。我习惯给Secret加上app、env、managed-by标签,并且严格按命名空间隔离。比如meituan-prod和meituan-dev各建各的Secret,绝不能共享。否则开发环境一旦误改动,生产服务也会受到牵连。
一个我在真实项目里踩过的坑是:开发环境的ServiceAccount因为某些历史原因,被授予了所有命名空间Secret的读取权限。后来开发同学测试时用kubectl get secret -A直接导出了生产环境的美团密钥。问题不在于他心术不正,而在于权限没有最小化。所以Secret的命名空间隔离和RBAC是两个必须同时做的动作。
2.4 更新和回滚:Secret也要有版本意识
普通ConfigMap改了之后,很多团队会让Pod重新读取。但Secret的情况更敏感,因为它很可能被多个服务共用。我不太建议直接在同一个Secret上原地更新value,因为一旦写入错误,所有引用它的服务都会立刻拿到坏数据,而且很难快速回滚到上一个状态。
更稳妥的办法是使用immutable: true声明Secret不可变,每次密钥变更就创建一个新版本的Secret,然后修改Deployment引用。比如:
apiVersion: v1 kind: Secret metadata: name: meituan-api-key-v2 namespace: meituan-prod type: Opaque immutable: true data: appKey: YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo= appSecret: c2VjcmV0LXZhbHVlLTEyMzQ1Ng==然后把Deployment里的secretKeyRef.name从meituan-api-key改成meituan-api-key-v2。这样做的好处是:每次变更都是显式的切换,有问题可以直接切回旧版本,并且不会出现“同一个Secret被多个服务热更新到不同版本”的混乱状态。
3. 将美团API密钥安全注入Java后端服务的三种方式
3.1 环境变量注入:最简单,但要配合Spring Boot的Relaxed Binding
环境变量是Kubernetes最基础的注入方式。在Deployment里这么写:
spec: template: spec: containers: - name: meituan-service image: registry.example.com/meituan-service:latest env: - name: MEITUAN_APP_KEY valueFrom: secretKeyRef: name: meituan-api-key key: appKey - name: MEITUAN_APP_SECRET valueFrom: secretKeyRef: name: meituan-api-key key: appSecretJava端对应的配置有两种做法。第一个是用@Value直接读取环境变量:
@Value("${MEITUAN_APP_KEY}") private String appKey; @Value("${MEITUAN_APP_SECRET}") private String appSecret;第二个更优雅,用Spring Boot的@ConfigurationProperties配合Relaxed Binding,让MEITUAN_APP_KEY自动映射到meituan.app-key:
meituan: app-key: ${MEITUAN_APP_KEY} app-secret: ${MEITUAN_APP_SECRET}然后在Java里定义配置类:
@Component @ConfigurationProperties(prefix = "meituan") public class MeituanProperties { private String appKey; private String appSecret; // getter/setter }我的经验是:环境变量方式适合把密钥当作“进程启动时就固定的参数”来用,简单直观,排查也方便。但它有一个明显的短板——Secret更新后,运行中的Pod不会自动感知,必须重建Pod才能让新环境变量生效。所以如果团队希望密钥轮换时服务不重启,就需要看下一种方式。
3.2 文件挂载注入:天然支持动态更新,但要注意缓存
Secret以文件形式挂载到Pod内的指定路径,在一些需要动态刷新密钥的场景下更好用。Deployment配置示例:
spec: template: spec: containers: - name: meituan-service image: registry.example.com/meituan-service:latest volumeMounts: - name: meituan-secret-volume mountPath: /etc/meituan readOnly: true volumes: - name: meituan-secret-volume secret: secretName: meituan-api-key这样Kubernetes会为Secret里的每个key生成一个文件,比如/etc/meituan/appKey和/etc/meituan/appSecret。Java服务里可以这样读取:
@Component public class MeituanKeyLoader { private final Path keyPath = Path.of("/etc/meituan/appKey"); private final Path secretPath = Path.of("/etc/meituan/appSecret"); public String getAppKey() throws IOException { return Files.readString(keyPath).trim(); } public String getAppSecret() throws IOException { return Files.readString(secretPath).trim(); } }在这个基础上,可以再加一层定时刷新机制:用ScheduledExecutorService每隔几分钟重新读取文件,然后把密钥缓存在内存里,下次调美团API时用最新的值。这样Secret在Kubernetes侧被更新后,kubelet会周期性地把新文件同步到容器,虽然通常有几十秒的延迟,但对日常轮换足够。
需要注意,这里有个经典的坑:如果应用开启了spring-boot-devtools或者用了某些配置热加载插件,文件变化可能被频繁扫描,反而影响性能。我的做法是只在自定义的加载器里读取,不直接依赖Spring的@ConfigurationProperties热刷新。
3.3 subPath和projected volume的组合:既隔离目录又能多配置合并
如果直接挂载整个Secret到/etc/meituan,而这个目录在镜像里还有其他文件,那么Secret会把该目录完全覆盖。我在一个老项目中吃过亏:镜像里的/etc/meituan存放了证书,结果挂载Secret后证书全部不可见,应用直接启动失败。
解决办法是使用subPath单独挂载某个key到指定路径:
volumeMounts: - name: meituan-secret-volume mountPath: /etc/meituan/appKey subPath: appKey readOnly: true - name: meituan-secret-volume mountPath: /etc/meituan/appSecret subPath: appSecret readOnly: true但是subPath有一个缺陷:Secret更新后,通过subPath挂载的文件不会自动刷新,因为它是直接绑定到某个路径的节点。所以如果既要保留目录里的其他文件,又要支持动态更新,我更推荐用projected volume,把多个Secret和ConfigMap合并挂载到一个干净的目录,并且kubelet会处理更新同步。
下面这个例子把美团密钥和另一个数据库密码Secret组合挂载到/etc/app-config:
volumes: - name: app-config projected: sources: - secret: name: meituan-api-key items: - key: appKey path: meituan-appKey - key: appSecret path: meituan-appSecret - secret: name: database-password items: - key: password path: db-password然后Java里统一从/etc/app-config下读文件。这样目录结构清晰,而且多个敏感配置不会互相覆盖。
3.4 进阶方案:External Secrets和Secrets Store CSI
当你的服务越来越多,Secret分散在多个命名空间,或者你希望以云厂商KMS、Vault作为唯一密钥源,可以考虑用External Secrets Operator或者Secrets Store CSI Driver。这不是本篇文章的重点,但我可以给一个选型建议:如果你的基础设施已经用了云服务商,优先用云KMS + Secrets Store CSI,因为它可以直接把云上的密钥映射成K8s Secret或挂载文件;如果是自建的Vault,则External Secrets更适合把Vault里的密钥同步到K8s Secret中。
但是,对于美团API密钥这种单业务需要的场景,我个人觉得用原生Secret就足够了,不需要引入太多组件。过多的抽象层有时候会让排障变得痛苦,尤其是当应用报“key not found”时,你还要从Secret、ExternalSecret、Vault三个地方查问题。
4. Secret的权限控制与安全加固
4.1 用RBAC把Secret的读取权关进笼子
Secret安全的第一道门是RBAC。很多集群为了省事,直接给开发者绑定edit角色,而edit角色默认拥有对Secret的完整读写权限。这意味着任何能登录控制台或持有kubeconfig的开发人员,都能轻易看到生产密钥。
我建议在命名空间内为应用创建独立的ServiceAccount,只授予它读取自己所需Secret的权限。比如:
apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: namespace: meituan-prod name: secret-reader rules: - apiGroups: [""] resources: ["secrets"] resourceNames: ["meituan-api-key"] verbs: ["get"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: namespace: meituan-prod name: meituan-service-secret-reader subjects: - kind: ServiceAccount name: meituan-service namespace: meituan-prod roleRef: kind: Role name: secret-reader apiGroup: rbac.authorization.k8s.io这里我用resourceNames把范围缩小到单个Secret,最大程度避免“拿到一个ServiceAccount就能读整个命名空间Secret”的情况。实际运营中,我还见过有人为了省事给ServiceAccount绑了cluster-admin,后来容器被入侵后攻击者直接拿到了所有命名空间的Secret,那就不是单次事故的问题了。
4.2 开启etcd加密,防止磁盘泄露
只做RBAC还不够,因为Kubernetes的Secret默认以base64明文存储在etcd里。如果etcd的备份文件被误传到公开空间,或者磁盘被脱机读取,Secret依然会暴露。
Kubernetes提供的官方方案是配置EncryptionConfiguration,在API Server启动参数里指定:
apiVersion: apiserver.config.k8s.io/v1 kind: EncryptionConfiguration resources: - resources: - secrets providers: - kms: apiVersion: v2 name: myKMSProvider endpoint: unix:///var/run/kms-plugin.sock cachesize: 100 - identity: {}如果你用的是云厂商托管集群,控制台一般有“密钥加密”选项,可以直接启用。自建集群则建议接入KMS插件,比如使用Vault或云KMS。启用之后,Secret在etcd中就不再是明文,即使备份文件泄露,也很难直接还原出密钥。
4.3 开启审计日志,留下读取痕迹
密钥管理的另一个重要环节是审计。Kubernetes的API Server审计日志可以记录谁在什么时间读取了哪个Secret。尤其在多团队共用集群的场景下,这份日志是事后追责的关键证据。
我建议至少开启RequestResponse级别的审计策略,并筛选出对Secret资源的读取事件。一个简化的审计策略示例:
apiVersion: audit.k8s.io/v1 kind: Policy rules: - level: Metadata resources: - group: "" resources: - secrets verbs: - get - list - watch omitStages: - RequestReceived审计日志通常会打到/var/log/kubernetes/audit,再接入日志平台。这样每次有人用kubectl get secret读取密钥,都能在日志系统里留下记录。
4.4 防止密钥被日志打出来
即使K8s层面做得再严密,Java应用自身也可能把密钥泄露到日志里。最常见的两种情况:一是在启动时打印所有配置项,二是在异常堆栈中输出了请求参数,而参数里携带了签名所需的secret。
我处理过一起事故:一个同事在@ConfigurationProperties配置类里写了toString(),并且在日志里打印了整个对象,结果美团appSecret跟着输出到了ELK。虽然日志系统有权限控制,但日志平台的访问面远大于K8s RBAC,风险非常高。
建议在Java侧做两层防护。第一层,任何时候都不要打印密钥本身,自定义配置类里的toString()要屏蔽敏感字段。第二层,使用logback或log4j2自定义脱敏转换器,对日志中匹配密钥模式的内容做掩码处理。比如:
public class SensitiveDataMaskingConverter extends MessageConverter { private static final Pattern PATTERN = Pattern.compile("(appSecret[=:])\\S+"); @Override public String convert(ILoggingEvent event) { String message = event.getFormattedMessage(); return PATTERN.matcher(message).replaceAll("$1******"); } }在logback.xml里注册这个转换器,可以避免密钥随着异常日志流到下游系统。
4.5 密钥轮换:用双版本发布避免中断
美团开放平台的API密钥一般允许在控制台重置。如何安全轮换是需要设计的。如果直接把旧Secret覆盖成新值,正在运行的服务会立刻用新密钥请求,但假如新密钥在美团侧还没完全生效,就会有一段时间的签名失败。
我采用的是“双版本”发布法:在美团控制台同时配置新旧两个密钥(如果平台支持),或先在服务中新增一个使用新密钥的灰度配置,观察稳定后再删除旧密钥。在K8s里,对应的Secret会保存两个版本:
data: appKey: new-app-key oldAppKey: old-app-keyDeployment先切换到新key正常后,再把旧key从Secret中移除。整个过程不需要中断服务。
5. 常见问题与排查技巧实录
5.1 Secret值带换行符导致签名失败
这是我在实际项目里遇到频率最高的坑。用--from-file创建Secret时,如果文件末尾有换行符,Secret里保存的value就带着一个\n。Java通过Files.readString()读出来后直接作为签名串去调用美团接口,美团服务端计算签名时用的字符串没有这个换行,两边签名永远对不上。
排查方法非常简单:进入容器后用xxd查看文件末尾:
kubectl exec -it meituan-service-pod -- sh xxd /etc/meituan/appSecret | tail如果看到末尾有0a,就是换行符。解决方式是在Java读取时执行.trim(),或者在创建Secret前用printf不要用echo:
printf '%s' 'your-app-secret' > ./appSecret.txt5.2 Java启动时环境变量为null或空
环境变量注入方式下,最常见的问题是secretKeyRef里的key名称写错。注意,Secret的key是大小写敏感的。如果Secret里定义的是appKey,Deployment里写成appkey,Pod也能创建成功,但环境变量不会赋值,Java里读取到的就是null。
我习惯在启动时通过.env文件打印环境变量名是否存在,但一定不能打印值:
kubectl exec -it <pod> -- sh -c 'test -n "$MEITUAN_APP_KEY" && echo "key exists" || echo "key missing"'5.3 更新Secret后Pod里的值还是旧的
如果用的是环境变量注入,Secret更新后Pod不会自动感知,必须重启Pod。我推荐在Deployment的Pod模板上加一个annotation,用来记录Secret的哈希或更新时间,这样Secret一变,Deployment spec就会变化,kubectl rollout restart会自动触发滚动更新:
template: metadata: annotations: secret-hash: abc123如果用的是Kustomize的secretGenerator,它生成的Secret name会自带hash,并在Deployment引用变更时自动触发滚动更新,这就是为什么我前面推荐Kustomize的原因。
如果用的是文件挂载方式,理论上kubelet会在几十秒内刷新文件,但Java进程可能缓存了旧内容,所以我前面提到需要自己实现定时重读。另外,subPath挂载不会自动刷新,这一点要特别小心。
5.4 挂载Secret后镜像里的目录被完全覆盖
这是很多新手会踩的坑。比如镜像里/etc/meituan目录是应用配置的一部分,你把Secret volume的mountPath直接指向它,Kubernetes会以tmpfs形式覆盖整个目录,原本的logback.xml、证书文件全部消失。
解决方式有三种:一是把Secret挂载到一个全新的路径,比如/etc/meituan-secret;二是使用subPath只挂载需要的key文件;三是用projected volume把所有配置统一放到独立目录,应用通过路径拼接读取。根据我的经验,方式一最不容易造成误覆盖,维护起来也最简单。
5.5 Secret超过1MB导致API Server响应慢
Kubernetes对Secret的资源大小有隐式限制,单个Secret不能超过1MB。这个值通常不会引起程序员注意,但如果你尝试把美团接口的完整签名证书、Java可执行文件甚至日志包塞进Secret,API Server的etcd查询压力会明显上升,甚至影响整个集群控制面。
正确做法是:Secret只放“小体积敏感信息”,大文件用对象存储或独立的密钥管理服务。比如私钥证书可以放到Secret,但几十MB的模型文件就不应该放进来。
5.6 查看Secret时提示Forbidden
如果你在业务集群中收到Error from server (Forbidden): secrets is forbidden: User "system:serviceaccount:xxx:default" cannot list resource "secrets",说明当前ServiceAccount或用户没有对应权限。这时按照4.1节方式补充Role和RoleBinding即可。但我也要提醒一句:不要为了省事直接授予cluster-admin,更合理的做法是授予最小权限,并在出现权限问题时先确认用户通过什么身份在访问集群。
6. 从原理到落地:一次完整的Secret注入示例
6.1 设计一个生产可用的美团密钥Secret
假设我们需要为Java Spring Boot服务配置两个美团开放平台参数:appKey和appSecret。我会用YAML加Kustomize的方式,而不是命令行,因为这样可审计、可重复。
创建一个生成Secret的环境文件meituan-secret.env:
MEITUAN_APP_KEY=真实应用Key MEITUAN_APP_SECRET=真实应用Secret注意这个文件不要提交到公开仓库,最好用sops加密后入Git。然后在kustomization.yaml里引用:
apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: meituan-prod secretGenerator: - name: meituan-api-key type: Opaque envs: - meituan-secret.env执行kubectl apply -k .后,Kubernetes会生成一个类似meituan-api-key-<hash>的Secret。这个自动生成的hash会让后续Deployment引用时自动触发更新。
6.2 Java服务配置类和Deployment
在Java服务里,我倾向于用文件挂载方式,这样将来可以支持密钥热更新。先创建配置类:
@Configuration @ConfigurationProperties(prefix = "meituan") public class MeituanConfig { private String appKey; private String appSecret; public String getAppKey() { return appKey; } public void setAppKey(String appKey) { this.appKey = appKey; } // getter/setter }然后在application.yml中配置占位符,指向挂载文件内容:
meituan: app-key: ${MEITUAN_APP_KEY:} app-secret: ${MEITUAN_APP_SECRET:}但因为在K8s里我们会把密钥挂载为文件,而不是设置环境变量,所以我更喜欢在启动类或配置类里通过绝对路径读取:
@Component public class MeituanKeyProvider { private final Path appKeyFile = Path.of("/etc/meituan-secret/appKey"); private final Path appSecretFile = Path.of("/etc/meituan-secret/appSecret"); public String getAppKey() throws IOException { return Files.readString(appKeyFile).trim(); } public String getAppSecret() throws IOException { return Files.readString(appSecretFile).trim(); } }对应的Deployment片段:
apiVersion: apps/v1 kind: Deployment metadata: name: meituan-service namespace: meituan-prod spec: replicas: 2 selector: matchLabels: app: meituan-service template: metadata: labels: app: meituan-service spec: serviceAccountName: meituan-service containers: - name: meituan-service image: registry.example.com/meituan-service:latest ports: - containerPort: 8080 volumeMounts: - name: meituan-secret mountPath: /etc/meituan-secret readOnly: true volumes: - name: meituan-secret projected: sources: - secret: name: meituan-api-key items: - key: MEITUAN_APP_KEY path: appKey - key: MEITUAN_APP_SECRET path: appSecret这里使用projected volume,一是避免目录覆盖,二是把两个key映射成易读的文件名。实际部署时还要加上resources、探针等配置,这里就不展开了。
6.3 验证Secret是否安全生效
部署完成后,验证分为几步。先确认Secret已创建:
kubectl get secret -n meituan-prod再确认Pod能正常启动,并进入容器检查文件内容是否存在,但不要直接查看value:
kubectl exec -it deployment/meituan-service -n meituan-prod -- ls -l /etc/meituan-secret最后调一下健康检查接口,确认服务能成功调用美团API。如果签名报错,优先检查换行符和文件内容是否和美团控制台展示的一致。
6.4 将来如何优雅轮换密钥
当需要轮换美团密钥时,我会先在美团控制台生成新密钥,然后更新meituan-secret.env,重新执行kubectl apply -k .。Kustomize生成的Secret name会变化,Deployment里的引用是通过secretGenerator自动注入的,所以执行apply后Kubernetes会自动滚动更新Pod。大约一两分钟后,所有实例都会读取到新密钥。
如果新密钥调用美团接口失败,需要快速回滚,可以用旧的Kustomize版本重新apply,或者保留一个meituan-api-key-legacy的Secret,让Java服务先降级到旧密钥。实际项目中,我会在Deployment里同时挂载新旧两份Secret,用环境变量或启动参数控制使用哪一份,避免强制通过Git回滚来解决问题。
最后再分享一个我自己的习惯:无论要不要做密钥轮换,我都会在Java服务的日志配置里加一层脱敏过滤器,并且在配置类的注释里明确写下“任何情况下不要打印密钥”。安全这件事,不能只靠Kubernetes的某一个特性,而是要靠代码、集群、流程一起兜底。你这次认真把Secret的创建、注入、权限、轮换都过一遍,后面再遇到其他第三方API密钥,就不会手忙脚乱了。