上一篇聊了ConfigMap把配置和镜像解耦的基本玩法过了一遍。今天这篇接着K8S存储系列往下走核心是Secret——一句话概括就是凡是密码、Token、证书、私钥这类不能明文躺平在YAML里的东西都归它管。文中从创建开始覆盖挂载文件、路径映射、环境变量注入和私有仓库认证大部分是生产环境里每天都要用的操作适合刚看完ConfigMap准备进阶的初级运维也适合想把Secret这块彻底补齐的K8S使用者。1. Secret是什么为什么单独拎出来讲1.1 Secret与ConfigMap的核心差异K8S里Secret和ConfigMap长得几乎一模一样都是把数据从镜像里抽出来再通过挂载或环境变量的方式注入Pod。但两者定位完全不同ConfigMap放的是配置比如配置文件、URL、开关项就算被不小心看到也没多大事Secret放的是敏感凭证比如数据库密码、API Token、TLS私钥一旦落到不该看的人手里就是直接的安全事故。K8S对Secret的照顾体现在几个层面首先它从形态上强制分离敏感数据让你在提交YAML到Git仓库之前至少有一个“不能明文”的意识门槛其次Secret支持更细粒度的访问控制可以通过RBAC限制谁能读取、谁能修改最后Secret在节点上可以选择加密存储配合KMS这类插件做到落盘加密。实操中我见过不少团队把密码直接塞ConfigMap里项目一多就到处裸奔真出了事才回过来补课其实一开始选对类型就省掉后面一堆麻烦。1.2 大多数人都会踩的认知坑base64不等于加密刚上手的人最容易把Secret当成“加密存储”这是最大的误区。用kubectl get secret xxx -o yaml看一眼就知道里面的值只是做了Base64编码拿命令解一下明文立刻出来本质和明文没区别。它真正的作用是帮你避开“在YAML和Git里直接写明文”这个最粗的坑。想要真正的加密得靠K8S对Secret的ETCD加密配置或者外部KMS集成。所以公司内部如果审计严格YAML文件本身也要做加密管理不能因为用了Secret就掉以轻心。1.3 Secret的主要类型一览K8S内置了多种Secret类型不同场景用不同type不只是默认的Opaque。我把常用的整理一下类型典型用途Kubernetes类型键Opaque任意自定义键值密码、Token等generickubernetes.io/dockerconfigjson私有镜像仓库认证docker-registrykubernetes.io/tlsTLS证书与私钥tlskubernetes.io/service-account-tokenServiceAccount令牌系统自动管理实战中遇到最多的是Opaque和dockerconfigjsonTLS类型创建标准证书Secret时也很常用。不同类型只是data里的字段格式约束不同底层机制完全一致。2. 创建Secret的几种主流方式2.1 用kubectl命令行从字面量创建最快速的创建方式是用kubectl create secret generic适合临时验证或写初始化脚本时直接用。比如要给应用配置数据库密码和API Tokenkubectl create secret generic app-secret \ --from-literalDB_PASSWORDPssw0rd \ --from-literalAPI_TOKENtok_xxxxx创建完可以用kubectl get secret app-secret -o yaml查看里面多了data字段键和值都是Base64编码。有一点要注意--from-literal的命令行方式会出现在Shell历史里如果是多人在同一台机器上操作建议要么用文件方式要么用声明式YAML管理避免敏感信息留在历史记录中。如果希望覆盖已有Secret可以在命令末尾加--dry-runclient -o yaml导YAML再kubectl apply -f -这样既保留了可读性又能走GitOps流程。我生产环境里经常用这个组合拳kubectl create secret generic app-secret \ --from-literalDB_PASSWORD新密码 \ --dry-runclient -o yaml | kubectl apply -f -2.2 从文件创建证书私钥的最好出处当数据本身是文件时比如TLS证书、私钥、配置文件使用--from-file要方便得多。它会自动把文件内容塞进Secret对应的键里键名默认是文件名kubectl create secret generic my-cert \ --from-filetls.crtserver.crt \ --from-filetls.keyserver.key上面的写法把server.crt作为值键名改成tls.crt。如果不加键名会直接用原始文件名。还有一种用法是--from-fileconfig/把目录下所有文件都变成Secret的键适合一次打包多个证书。如果用--from-file读取过大的文件要注意Secret整体体积限制。ETCD默认支持的单对象大小是1.5MB左右单个Secret最大也就是1MB不适用于大文件存储这是设计定位决定的。2.3 声明式YAML创建团队协作时最推荐的还是声明式方式把Secret定义写进Git仓库走Review和CI流程。Opaque类型的YAML长这样apiVersion: v1 kind: Secret metadata: name: app-secret namespace: default type: Opaque data: DB_PASSWORD: UEAzc3cwcmQ API_TOKEN: dG9rX3h4eHh4注意data里的值必须是Base64。如果你不想自己敲Base64可以用stringData字段K8S在创建时会自动帮你编码YAML文件里直接写明文然后kubectl apply即可。这种写法审阅时能直接看到值安全性存疑但如果配合加密Git仓库使用体验非常好。我还遇到过直接把kubectl create secret的dry-run结果落盘管理的场景这种方式UI上看起来最直观不过时间久了YAML会变长不如用Kustomize的generatorOptions统一管理。2.4 创建后校验不只是get看一下创建完Secret别急着走建议先验证一下数据是否正确解出kubectl get secret app-secret -o jsonpath{.data.DB_PASSWORD} | base64 -d如果值是中文或特殊字符直接base64 -d可能因为换行符出问题用echo -n包裹更稳妥。生产环境我习惯把这段写成一个小函数想检查哪个键就传参进去效率高很多。3. 把Secret挂载成文件默认行为、路径映射和subPath3.1 默认挂载会整个覆盖目录把Secret挂载成文件是日志采集类应用、传统Java应用读取配置文件最常见的场景。默认写法示意如下apiVersion: apps/v1 kind: Deployment metadata: name: app spec: template: spec: containers: - name: app image: myapp:1.0 volumeMounts: - name: secret-volume mountPath: /etc/app-secret readOnly: true volumes: - name: secret-volume secret: secretName: app-secret这样做会把Secret里每个键变成一个文件文件名为键名内容为解码后的值。许多第一次上手的人会在这踩坑mountPath目录下的原有内容全被覆盖了。比如容器镜像里/etc/app目录本身有东西挂载后全被Secret内容顶掉。我给出的建议是尽量把Secret挂载到专门的空目录里比如/etc/secrets或者用subPath只挂载单个文件。千万别偷懒直接挂到一个系统目录上否则容器可能直接起不来。3.2 items路径映射改键名、挑字段、映射路径Secret里有多个键但你可能只想挂载其中一部分或者你想把某个键挂到特定路径比如把DB_PASSWORD挂载成/etc/secrets/password。这时需要items配置volumes: - name: secret-volume secret: secretName: app-secret items: - key: DB_PASSWORD path: db/password - key: API_TOKEN path: api/token挂载后容器内会出现/etc/app-secret/db/password和/etc/app-secret/api/token没有被items列出的键就不会挂载。路径支持子目录自动创建。这种用法最大的优势是把键名和文件名解耦也能保持容器内路径规范统一。3.3 subPath挂载单文件覆盖不碰目录里其他内容如果你只想把Secret里的某一个键挂到已有文件上比如替换/etc/nginx/nginx.confvolumeMounts里加subPath就行volumeMounts: - name: secret-volume mountPath: /etc/app/config.yaml subPath: config.yaml readOnly: true这里subPath的值是Secret里的键名挂载后/etc/app/config.yaml就会被替换成Secret里config.yaml键的内容而/etc/app目录下其他原有文件原封不动。需要注意subPath挂载会失去K8S对Secret文件更新的自动联动能力后续Secret更新后subPath挂载的文件不会自动刷新。要更新就得重新创建Pod或手动触发滚动。所以生产上如果依赖配置热更新优先别用subPath。3.4 挂载权限与只读策略Secret文件默认权限是0444也就是所有用户可读、不可写。这符合安全诉求但确实有特殊场景需要放开写权限比如某些应用启动时要写配置文件。可以通过defaultMode修改volumes: - name: secret-volume secret: secretName: app-secret defaultMode: 0600我一般会建议即使应用需要写也别把整个Secret目录挂成可写改成单个文件挂载加0644把影响面降到最小。另一个经验是把readOnly: true显式写上因为Secret本来就是敏感内容防止容器内进程意外篡改Secret文件内容。4. 通过环境变量注入Secret4.1 单键注入valueFrom加secretKeyRef有些应用不读文件只认环境变量。比如Spring Boot里常见的数据库密码、第三方服务Token环境变量方式更直接。单个键注入写法如下env: - name: DB_PASSWORD valueFrom: secretKeyRef: name: app-secret key: DB_PASSWORD这样容器里的DB_PASSWORD环境变量会带上Secret里对应键的值。注意如果用kubectl exec进容器查看环境变量值是明文可见的但至少不会躺在镜像或YAML里。4.2 批量注入envFrom省掉一大堆重复代码Secret里键很多时一个个写valueFrom很折磨人。用envFrom可以一次性把所有键变成环境变量envFrom: - secretRef: name: app-secret前提是Secret里的键本身符合环境变量命名规则像my.key、app-config这类含点号或横杠的键会被忽略掉不会注入。实测中不止一次遇到这类坑Secret创建没问题但Pod起来后环境变量就是少几个排查半天发现是键名不合法。如果需要同时注入多个Secret可以写多个secretRef也可以和env混用后者优先级更高。注意如果两个Secret的键重名会有覆盖情况执行时要先确认键不冲突。4.3 环境变量方式的短板要提前知道环境变量注入最大的问题是无法热更新。当Secret修改后文件挂载方式会在kubelet周期内自动更新通常几十秒内但环境变量一旦注入除非Pod重启否则进程拿到的一直是旧值。如果应用对配置变更要求实时性比较高建议优先走文件挂载。另外有个小技巧很多ConfigMap或Secret的键是下划线风格但应用希望环境变量名是全大写风格。这种情况下可以写两个Secret一个存原始键一个专门映射成环境变量名或者直接用env逐个映射看具体需求权衡。5. 私有镜像仓库认证imagePullSecrets实战5.1 创建docker-registry类型的Secret从私有仓库拉镜像时kubelet需要一套认证信息。手动创建的命令如下kubectl create secret docker-registry my-registry \ --docker-serverregistry.example.com \ --docker-usernamereadonly_user \ --docker-passwordreadonly_pass \ --docker-emailopsexample.com这里的docker-email虽然是必填参数但在现代Docker Registry认证中作用不大大多数Registry只要兼容Docker Registry v2协议都不校验邮箱。如果公司私有仓库用的是自建Registry只要用户名密码对得上就行。执行后查看一下Secret的类型会看到kubernetes.io/dockerconfigjsondata里是.dockerconfigjson字段Base64解码后是标准的Docker配置JSON结构。5.2 在Pod中指定imagePullSecrets创建完Secret需要让它生效。最直接的方式是在Pod模板里声明spec: imagePullSecrets: - name: my-registry这个字段写在Pod的spec层级不是container层级。如果使用Deployment、StatefulSet就在spec.template.spec.imagePullSecrets下写。一个Pod可以配置多个imagePullSecretskubelet会按顺序尝试直到拉取成功。我踩过的一个坑是有些云厂商提供的内置镜像加速器会覆盖用户的Registry配置导致指定了imagePullSecrets也拉不到镜像。这种情况要么走公司统一镜像仓库要么在节点上配置containerd的registry mirror别在Pod层做过多的绕路。5.3 更省心的方式给ServiceAccount绑定imagePullSecrets如果一个命名空间下大部分Pod都要从同一私有仓库拉镜像一个个指定imagePullSecrets太啰嗦。K8S允许在ServiceAccount上预置Secret这样只要Pod使用对应ServiceAccount就会自动带上认证。给默认ServiceAccount绑定可以省掉所有Pod的声明apiVersion: v1 kind: ServiceAccount metadata: name: default imagePullSecrets: - name: my-registry执行完kubectl apply后后续创建的Pod都会自动附加my-registry这个Secret除非Pod里显式覆盖了imagePullSecrets。这是个很实用的最佳实践在测试环境和多团队共享集群时能省大量重复配置。如果某个Pod不希望使用默认自动附加的Secret也可以显式指定imagePullSecrets: []来清空。不过生产环境里建议对敏感命名空间的默认ServiceAccount做严格管控避免权限扩散。5.4 私有Registry自签证书的坑自建的私有仓库如果用了自签TLS证书光有imagePullSecrets还不够kubelet跟Registry握手时依然会报证书错误。具体解决方式是在节点或containerd层面配置自签CA证书这块和Secret关系不大但出问题时经常被误以为是imagePullSecrets写错了。另一个相关问题是HTTPS与HTTP。如果Registry只支持HTTP需要在containerd配置里给该地址加skip_verify或insecure。这里提醒一下安全扫描严格的团队不建议连裸HTTP的私有仓库至少得挂一层TLS。6. Secret的更新、轮转与排错实录6.1 文件挂载的Secret更新后会不会自动生效Secret挂载成文件的场景下kubelet会周期性地同步Secret内容到节点本地然后更新挂载的文件。实测下来修改Secret后大约30秒到1分钟容器里读到的文件内容会变。但这里有个隐含条件应用得能感知文件变化。很多应用只在启动时读一次配置文件文件内容改了它也不知道。解决方法有几种一是应用自己监听文件变化二是把更新流程做成滚动发布让新版Pod用新Secret三是通过sidecar做配置重载。生产里比较推荐的是第二种把Secret的版本号或数据hash打进Pod注解里作为滚动发布的触发条件。有些团队会写一个工具把Secret内容做MD5在Deployment环境变量里注入这个MD5Secret一变Deployment的Template哈希跟着变控制器自动滚动重启。这是最省事且可靠的做法。6.2 环境变量注入的Secret为什么重启也没更新环境变量注入后Pod不重启就永远用旧值。就算你把Secret改了然后kubectl delete pod手动重启有些Pod尤其是节点资源紧张时会调度到旧Pod所在节点kubelet可能用了缓存导致还是旧值。遇到这种问题最稳的办法是先滚动更新Deployment用自身机制保证新Pod拉到新Secret。如果希望避免这种来回折腾直接选择文件挂载方式。另外所有环境变量相关的Secret变更都应该纳入部署流程比如走GitOps自动触发滚动而不只是手动改Secret。6.3 从PodPending到ImagePullBackOff一条完整排查链私有仓库认证问题排查时建议按顺序检查先看事件再查Secret再查权限最后查仓库侧。kubectl describe pod my-pod如果事件里出现ImagePullBackOff先看具体错误信息。unauthorized或no basic auth credentials说明认证信息不匹配或没传manifest unknown可能是镜像标签不对tls handshake timeout大概率是网络或证书问题。接着用kubectl get secret确认Secret存在并且命名空间和Pod一致。Secret是有命名空间隔离的写错命名空间会导致Pod完全找不到认证信息。然后手动验证一下Secret里的账号密码能不能直接登录仓库kubectl get secret my-registry -o jsonpath{.data.\.dockerconfigjson} | base64 -d解出来的内容结构是{auths:{registry.example.com:{auth:Base64编码的用户名密码}}}如果这一串能解出正确的用户名密码那Secret本身没问题问题就在Pod引用或仓库侧。6.4 常见问题速查表问题现象可能原因解决建议挂载目录变成Secret文件原内容丢失没有使用subPath改为subPath挂载单个文件Secret修改后环境变量不更新环境变量机制天然不支持热更新切换文件挂载或滚动发布items指定后部分键没挂载键名拼写错误或items写错检查Secret实际键名并对照items私有仓库拉取报unauthorizedimagePullSecrets名字错误或账号没权限核对Secret内容、账号权限Pod里看到的Secret权限不对defaultMode设置错误按需求调整defaultMode容器读的文件内容是Base64挂载源写成了data字段直接挂确认挂载的是解码后的值不要自造卷Secret用的时间久了我个人的体会是它的设计重点不是加密而是提供一套强制分离敏感信息的机制把安全和配置管理从“自觉”变成“规范”。真正生产上最好的实践是配合外部密钥管理服务做加密和轮转把Secret当作运行时引用而不是把秘密直接塞进Secret就算完事。文件挂载优先于环境变量注入ServiceAccount绑定imagePullSecrets优先于每个Pod单独声明这两条原则能帮你避开大部分不必要的问题。如果项目规模再大一些建议给Secret加一层版本管理和自动轮转的流水线这比在YAML里手写一堆Base64要省心得多。
阅读完成 · 觉得有帮助?