大家好,欢迎准时来到《云原生核心技术》系列的第九篇!
至此,我们已经取得了巨大的进步:我们不仅能用 Deployment 部署和管理应用,还能通过 Service 和 Ingress 将其安全、优雅地暴露给外部世界。我们部署的 Nginx 应用就像一个训练有素、不知疲倦的员工,能自我修复,也能平滑升级。
但我们忽略了一个重要的问题:现实世界的应用,并非都是像 Nginx 这样简单的“无状态”应用。它们往往有更复杂的需求:
- 它们需要读取配置文件,比如数据库的连接地址、端口号等。
- 它们需要保存敏感信息,比如 API 密钥、数据库密码。
- 它们需要持久化地存储数据。想象一下,如果你的数据库 Pod 重启一次,所有用户数据就都消失了,那将是灾难性的!
今天,我们就来学习 K8s 提供的三大法宝,专门用来解决这些“有状态”的难题:ConfigMap、Secret 和 Volume。
一、配置管理双雄:ConfigMap 与 Secret
在 K8s 中,我们强烈推荐将配置与应用镜像分离。你不应该把配置文件直接打包进 Docker 镜像里。为什么?因为配置(比如数据库地址)在开发、测试、生产环境中都是不同的。如果把配置写死在镜像里,你就需要为每个环境打一个新镜像,这完全违背了“一次构建,到处运行”的原则。
K8s 提供了两种资源对象来优雅地解决这个问题。
1. ConfigMap: 明文配置的管家
ConfigMap
顾名思义,就是“配置地图”,它专门用来存储非敏感的、明文的键值对配置。
比喻时间:把
ConfigMap
想象成一张贴在冰箱门上的备忘录便签。上面写着“Wi-Fi密码:MyHomeWiFi”、“牛奶在第二层”等公开信息。家里任何一个成员(Pod)都可以随时查看。
如何使用 ConfigMap?
你可以通过两种主要方式将 ConfigMap 中的数据“注入”到 Pod 中:
- 作为环境变量:将 key-value 直接变成容器内的环境变量。
- 作为文件挂载:将 key-value 变成文件,挂载到容器的指定目录中。这种方式对于传统的、需要读取配置文件的应用(如 Spring Boot 的
application.properties
)非常友好。
动手:用 ConfigMap 定制 Nginx 欢迎页
让我们来创建一个 ConfigMap,用它来动态修改 Nginx 的默认页面内容。
第一步:创建 ConfigMap
创建一个名为 nginx-configmap.yaml
的文件:
# nginx-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-index-html
data:
# key 是 "index.html",value 是整个 HTML 文件的内容
index.html: |
<!DOCTYPE html>
<html>
<head>
<title>Welcome from ConfigMap!</title>
</head>
<body>
<h1>This page is dynamically configured by a Kubernetes ConfigMap!</h1>
<p>We are learning about stateful applications.</p>
</body>
</html>
应用它:kubectl apply -f nginx-configmap.yaml
第二步:修改 Deployment 以使用 ConfigMap
现在,修改我们之前的 nginx-deployment.yaml
文件。我们需要添加一个 volumes
和 volumeMounts
部分,将这个 ConfigMap 挂载到 Nginx 存放 index.html
的目录 /usr/share/nginx/html
。
# nginx-deployment-with-configmap.yaml
apiVersion: apps/v1
kind: Deployment
# ... metadata, replicas, selector 不变 ...
spec:
replicas: 3
selector:
matchLabels:
app: nginx-server
template:
metadata:
labels:
app: nginx-server
spec:
containers:
- name: nginx
image: nginx:1.21
ports:
- containerPort: 80
# 新增内容:定义卷挂载
volumeMounts:
- name: nginx-index-volume # 挂载点的名字,需要和下面的 volume 名字对应
# 挂载到容器内的目标路径
mountPath: /usr/share/nginx/html
# 新增内容:定义 Pod 级别的卷
volumes:
- name: nginx-index-volume # 卷的名字
# 卷的类型是 ConfigMap
configMap:
# 使用名为 nginx-index-html 的 ConfigMap
name: nginx-index-html
应用修改后的 Deployment:kubectl apply -f nginx-deployment-with-configmap.yaml
第三步:验证
等待 Pod 更新完成后,再次访问你的 Nginx 服务(通过 http://nginx.test
或 NodePort)。你会惊喜地发现,欢迎页面已经变成了我们用 ConfigMap 定义的内容!
2. Secret: 敏感信息的保险箱
Secret
和 ConfigMap 非常相似,但它是专门为存储敏感数据而设计的,比如密码、Token、SSH 密钥等。
比喻时间:如果 ConfigMap 是冰箱门上的便签,那
Secret
就是锁在卧室抽屉里的密码本。它也是给家人(Pod)用的,但你不会把它公开展示。
重要提示:默认情况下,K8s 只是将 Secret 的内容进行了 Base64 编码,它不是加密! Base64 是一种任何人都可以解码的编码方式。Secret 的“安全”主要体现在 K8s 的 RBAC(基于角色的访问控制)上,你可以精细地控制哪些用户或服务账号有权读取某个 Secret。
Secret 的创建和使用方式与 ConfigMap 几乎完全相同,只是 kind
变成了 Secret
。
二、数据持久化:Volume, PV 和 PVC
Pod 的生命周期是短暂的,它们随时可能被销毁和重建。Pod 内部文件系统的任何数据都会随着 Pod 的消亡而灰飞烟灭。这对于无状态应用没问题,但对于数据库、消息队列等有状态应用来说是致命的。
我们需要一种方法,让数据能够独立于 Pod 的生命周期而存在。这就是 Volume 的使命。
Volume 的核心思想:解耦
K8s 的存储设计非常精妙,它将“存储的使用”和“存储的提供”这两个环节进行了解耦。这是通过三个核心概念实现的:
PersistentVolume (PV): 持久卷。
- 角色:集群管理员(运维人员)。
- 职责:负责提供存储。管理员会事先创建好各种类型的存储资源(比如一块 10GB 的快速 SSD,或是一块 100GB 的慢速 HDD),并将它们定义成 PV 对象,放入 K8s 的“存储资源池”中。
- 比喻:PV 就像是房产开发商建好的、待出租的仓库。仓库有大有小,位置有好有坏。
PersistentVolumeClaim (PVC): 持久卷声明。
- 角色:应用开发者(用户)。
- 职责:负责申请存储。开发者不需要关心存储具体是怎么来的(是NFS还是Ceph),他只需要提交一个“声明”,说明“我需要一个 5GB 大小、可以被多个 Pod 读写的存储”。
- 比喻:PVC 就像是租户提交的一份租房申请:“我需要一个 100 平方米的仓库”。
StorageClass: 存储类。
- 角色:自动化机制。
- 职责:动态地创建 PV。在没有 StorageClass 的情况下,管理员需要手动创建 PV。有了 StorageClass,当 K8s 收到一个 PVC 申请,并且没有现成的 PV 能满足时,StorageClass 会自动调用底层存储插件(比如云厂商的磁盘服务)来创建一个全新的 PV,并与该 PVC 绑定。这是目前最主流的方式。
- 比喻:StorageClass 就像一个全自动的房产中介。你提交租房申请,它能立刻凭空给你变出一套符合要求的仓库来。
整个流程:开发者创建 PVC (租房申请) -> K8s(或 StorageClass)找到/创建一个匹配的 PV (仓库) -> K8s 将 PVC 和 PV 绑定 -> 开发者在 Pod 中引用 PVC (拿到仓库钥匙),将这个 Volume 挂载到容器的某个路径下。
这样一来,即使 Pod (租客) 挂掉了,数据(存放在仓库里的货物)依然安全地保管在 PV 中。新的 Pod (新租客) 启动后,只要引用同一个 PVC (使用同一把钥匙),就能继续访问之前的数据。
动手:让 Nginx 拥有持久化的日志
让我们来实践一下,创建一个 PVC,并将其挂载到 Nginx Pod 中,用来持久化存储访问日志。
第一步:创建 PVC (持久卷声明)
创建一个 nginx-pvc.yaml
文件:
# nginx-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nginx-log-pvc
spec:
# 访问模式: ReadWriteOnce 表示该卷只能被单个节点读写
accessModes:
- ReadWriteOnce
# 资源请求
resources:
requests:
# 申请 1GB 的存储空间
storage: 1Gi
# storageClassName: standard # 在 Minikube 中,通常有一个名为 "standard" 的默认 StorageClass
应用它:kubectl apply -f nginx-pvc.yaml
第二步:修改 Deployment 以使用 PVC
再次修改我们的 Deployment,这次我们添加对 PVC 的引用。
# nginx-deployment-with-pvc.yaml
apiVersion: apps/v1
kind: Deployment
# ...
spec:
# ...
template:
# ...
spec:
containers:
- name: nginx
image: nginx:1.21
ports:
- containerPort: 80
volumeMounts:
# 我们把日志卷挂载到 Nginx 默认的日志目录
- name: nginx-log-volume
mountPath: /var/log/nginx
volumes:
# 定义卷,这次的类型是 persistentVolumeClaim
- name: nginx-log-volume
persistentVolumeClaim:
# 引用我们刚刚创建的 PVC 的名字
claimName: nginx-log-pvc
注意:为了演示,你可以把之前 ConfigMap 的挂载去掉,也可以保留,它们不冲突。
应用它:kubectl apply -f nginx-deployment-with-pvc.yaml
第三步:验证数据持久性
- 找到一个 Nginx Pod 的名字:
kubectl get pods
- 进入该 Pod,在日志目录里创建一个测试文件:
kubectl exec -it <your-nginx-pod-name> -- bash # 进入 Pod 后执行: echo "This is a persistent log file." > /var/log/nginx/my-test.log cat /var/log/nginx/my-test.log exit
- 现在,手动删除这个 Pod:
kubectl delete pod <your-nginx-pod-name>
- 等待 Deployment 创建一个新的 Pod。
- 获取新 Pod 的名字,并进入新的 Pod:
kubectl exec -it <new-nginx-pod-name> -- bash # 再次查看文件是否存在: cat /var/log/nginx/my-test.log
你会发现,my-test.log
文件依然存在!数据成功地在 Pod 重启后得以保留。
总结
今天我们解锁了在 K8s 中处理有状态应用的核心技能。通过这三大法宝,我们的应用变得更加完善和健壮:
- ConfigMap:管理非敏感配置,实现配置与代码的分离。
- Secret:安全地管理密码、密钥等敏感信息。
- PV/PVC 模型:为我们的应用提供了独立于 Pod 生命周期的、可靠的持久化存储。
到目前为止,我们已经逐个学习了部署一个真实世界应用所需的几乎所有 K8s 核心组件。就像学完了乐高的所有基本积木块,是时候将它们拼装成一个复杂的、令人惊叹的模型了!
在下一篇,我们将迎来本系列的终极实战:从零开始,将一个包含 Spring Boot 后端、MySQL 数据库和 Redis 缓存的复杂微服务应用,完整地部署到我们的 Kubernetes 集群中!这将是你展示和巩固所学知识的最佳机会。准备好了吗?我们下一篇见!