今天上午我被K8s的ConfigMap搞疯了——明明Pod启动正常,明明目录里能看到配置文件,可程序就是读不到,报错报得我怀疑人生。
背景:一个看似毫无难度的需求
今天要在K8s上部署一个服务,Java 写的,需求很明确:
- 应用启动需要读取好几份配置文件,比如application.properties、logback.xml这些,为了方便管理,统一放在ConfigMap里;
- 程序运行的时候,还会动态生成一个授权文件,路径是 /app/config/license_token —— 也就是说,/app/config这个目录,既要放配置(读),还要允许程序写文件。
我当时心想,这还不简单?ConfigMap挂载目录,程序直接读写,一套操作行云流水,结果刚部署就翻车了。
ConfigMap挂载居然是只读的?
一开始我写的YAML配置很常规,就是把ConfigMap直接挂载到/app/config目录:
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
k8s.kuboard.cn/name: prod-app-ci
workload.user.cattle.io/workloadselector: apps.deployment-prod-app-ci
name: prod-app-ci
namespace: app
spec:
replicas: 1
selector:
matchLabels:
workload.user.cattle.io/workloadselector: apps.deployment-prod-app-ci
template:
metadata:
labels:
workload.user.cattle.io/workloadselector: apps.deployment-prod-app-ci
namespace: app
spec:
containers:
- image: 'app:2026_04_09_16_47_13'
imagePullPolicy: IfNotPresent
name: app
ports:
- containerPort: 8080
hostPort: 8080
name: http-8080
protocol: TCP
volumeMounts:
- mountPath: /app/config
name: app-config
volumes:
- name: app-config
configMap:
name: app-config
部署Pod,启动程序,结果直接炸了,日志里全是这个报错:
panic: open /app/config/license_token: read-only file system
我当时整个人都懵了:???我就挂个配置目录而已,怎么就只读了?我也没写readOnly啊?
赶紧去查文档,才发现一个被我忽略的知识点——ConfigMap挂载的目录,天生就是只读的。
这不是K8s的bug,是它的设计逻辑:ConfigMap是用来存储配置的,属于“静态资源”,为了防止误改,默认就是只读挂载。
得,第一个坑踩实了,重新想思路。
解决思路:ConfigMap + emptyDir
既然ConfigMap不能写,那我就找个能写的目录呗。最常用的就是emptyDir,临时可写目录,刚好符合需求。
我的思路很简单:
- 用emptyDir创建一个可写的临时目录;
- 用initContainer(初始化容器),把ConfigMap里的配置文件,复制到这个emptyDir目录里;
- 主容器再挂载这个emptyDir目录——这样既可以读到配置,又能让程序写文件。
修改后的YAML是这样的,当时觉得这就是标准解法,稳得一批:
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
k8s.kuboard.cn/name: prod-app-ci
workload.user.cattle.io/workloadselector: apps.deployment-prod-app-ci
name: prod-app-ci
namespace: app
spec:
replicas: 1
selector:
matchLabels:
workload.user.cattle.io/workloadselector: apps.deployment-prod-app-ci
template:
metadata:
labels:
workload.user.cattle.io/workloadselector: apps.deployment-prod-app-ci
namespace: app
spec:
initContainers:
- command:
- sh
- '-c'
- 'cp -r /config/* /app/config/ && chown -R 1001:1001 /app'
image: 'busybox:1.32'
imagePullPolicy: IfNotPresent
name: init
volumeMounts:
- mountPath: /config
name: app-config
- mountPath: /app/config
name: config-tmp
containers:
- image: 'app:2026_04_09_16_47_13'
imagePullPolicy: IfNotPresent
name: app
ports:
- containerPort: 8080
hostPort: 8080
name: http-8080
protocol: TCP
volumeMounts:
- mountPath: /app/config
name: config-tmp
volumes:
- name: app-config
configMap:
name: app-config
- name: config-tmp
emptyDir: {}
梳理一下链路,特别清晰:
ConfigMap → initContainer(复制) → emptyDir → 主容器
部署Pod,这次没报错,启动成功了。我当时还暗自庆幸,还好反应快,第一个坑顺利解决。
结果,第二个坑马上就来了。
第二个坑
文件明明在,却读不到?玄学了。
Pod启动正常,我赶紧exec进去查看,确认配置文件是不是复制成功了:
kubectl exec -it 你的Pod名 -- sh
cd /app/config
输出结果很正常,所有配置文件都在:
application.properties logback.xml xxx.conf
我心里一块石头落了地,觉得没问题了。可当我尝试查看文件内容时,直接懵了:
cat application.properties
报错如下,直接给我整破防了:
No such file or directory
???什么情况?ls明明能看到文件,cat却提示不存在?
我反复执行ls和cat,确认不是自己输错了文件名,可问题就是这么诡异——文件“看得见、摸不着”,读不到任何内容。
这时候我已经有点急躁了,生产环境下,一个看似简单的配置,居然卡了两次。
真相大白
冷静下来,我觉得问题肯定出在“复制”这一步。于是我用ls -l命令,查看一下文件的详细信息,这一看,终于发现了关键线索:
ls -l /app/config
输出结果让我恍然大悟:
lrwxrwxrwx 1 root root 23 Jun 8 14:30 application.properties -> ..data/application.properties
lrwxrwxrwx 1 root root 18 Jun 8 14:30 logback.xml -> ..data/logback.xml
原来如此!这些我以为的“配置文件”,根本不是普通文件,全是符号链接(软链接)!
我再深入查看一下目录结构,终于摸清了ConfigMap的底层逻辑:
/app/config/
├── application.properties -> ..data/application.properties
├── logback.xml -> ..data/logback.xml
└── ..data/
├── application.properties
└── logback.xml
简单说就是:我们看到的配置文件,只是一个“快捷方式”,真正的文件内容,其实存放在上级的..data目录里。
真正的坑点
cp命令复制的是“链接”,不是文件内容。
现在再回头看我initContainer里的复制命令:
cp -r /config/* /app/config/
这一步看似没问题,实则藏着致命错误——cp命令默认复制的是“符号链接本身”,而不是链接指向的真实文件内容。
也就是说,我复制到emptyDir里的,只是一个个“快捷方式”,而这些快捷方式指向的..data目录,在emptyDir里根本不存在!
所以才会出现:ls能看到文件(其实是链接),但cat的时候,链接指向的目标不存在,就报错“没有这个文件或目录”。
找到问题根源,我瞬间松了口气——原来不是玄学,是我忽略了ConfigMap的底层实现细节。
最终解决:一个参数,救命了
解决方法其实特别简单,就给cp命令加一个参数:-L
修改后的复制命令是这样的:
cp -rL /config/. /app/config/
重点说一下这个-L参数的作用:跟随符号链接,复制链接指向的真实文件内容,而不是复制链接本身。
还有一个小细节,把/config/*改成/config/.,这样能把隐藏文件(如果有的话)也一起复制,避免遗漏。
修复后,再验证一次
重新部署Pod,exec进去查看:
ls -l /app/config
这次的输出就正常了,再也不是符号链接了:
-rw-r--r-- 1 root root 568 Jun 8 14:35 application.properties
-rw-r--r-- 1 root root 1245 Jun 8 14:35 logback.xml
再cat文件,完美读取:
cat application.properties # 正常输出配置内容
启动程序,程序也能正常生成license_token文件,读写都没问题——困扰我1个小时的坑,终于解决了。
总结
这次踩坑,看似是两个独立的问题,本质上是我对ConfigMap的底层实现了解不够深,总结下来就一句话:
ConfigMap里的“文件”,其实是符号链接,不是真文件
由此衍生出两个典型坑点,大家一定要记牢:
坑点一:ConfigMap挂载目录,默认只读
👉 只要你的目录需要“写”操作(比如生成文件、修改配置),就不能直接挂载ConfigMap;
👉 正确做法:用emptyDir(临时)或PVC(持久化)当可写目录,通过initContainer复制ConfigMap内容。
坑点二:cp命令默认复制“链接”,不是内容
👉 复制ConfigMap里的文件时,一定要加-L参数,跟随符号链接,复制真实内容;
👉 避免用cp -r /config/*,改用cp -rL /config/.,防止遗漏隐藏文件。
延伸一下
可能有人会问:K8s为什么要搞这种“软链接+..data”的结构?多麻烦啊?
其实这是为了支持ConfigMap热更新,而且是原子操作,特别优雅:
- 当你更新ConfigMap内容时,K8s会创建一个新的..data目录,存放新的配置文件;
- 然后瞬间切换符号链接的指向,从旧的..data指向新的..data;
- 整个过程是原子的,不会出现“配置一半更新、一半未更新”的情况,也不用重启Pod。
搞懂这个逻辑,你就会明白,这个“麻烦”的设计,其实是K8s的用心之处。
最后
今天这个坑,给我的最大感受就是:K8s里很多“玄学问题”,其实都不是玄学,只是我们没看透它的底层逻辑。
就像这次的ConfigMap,看似简单的挂载和复制,背后藏着符号链接的机制,一旦忽略,就会踩坑。
如果你也遇到过这种“文件在,却读不到”“K8s行为很诡异”的问题,不妨往“底层实现”上多想想,大概率能找到突破口。
另外,也提醒自己和各位同行:生产环境的坑,从来都不是大问题,而是一个个小细节的疏忽。多踩一次坑,多记一个细节,下次就能少走很多弯路。
后续优化使用pvc
cm是注入的方式去进行热更新。如果是类似于nfs挂载的话,就会有多次读写IO。

wouuwvmgtqlvmzjgzeymywyphsrkkw