Kubernetes 第三讲

存储

持久化

Pod是由多个容器组成的,在了解Pod的文件系统结构时,我们可以先看看一个Docker容器是如何被制作的。

容器根据镜像启动。我们在Dockerfile中的每一条RUN命令,会生成一层一层的镜像层,这些镜像层从下向上以栈的形式组成了一个镜像。而容器和镜像都是由多个层组成的,他们之间最大的区别就是容器的最上面的一层是读写层,叫做容器层。但镜像的所有层都是只读层,叫做镜像层。

容器启动后,Docker会在容器使用的镜像上添加一个容器层。容器运行时,所有和数据变化相关的操作都是在这个读写层中完成的,如新建文件,修改文件等。删除容器时,Docker同时会删除这个容器层。

每个容器运行时,都有自己的容器层,并在容器层中保存容器运行相关的数据。容器层之下的所有镜像层都是只读的,因此多个容器可以共享同一个镜像。

img

Pod中有若干个容器,每个容器的文件系统都是独立且一次性的。当容器因为某些原因重启时(崩溃、调度等情况),容器层就会被销毁并重新创建。

假如我们运行了一个MySQL的Pod,如果你没有对它做持久化存储,一旦这个Pod因为某些原因被销毁,那么这个Pod里的数据就会全部丢失,然后工作负载就会另起一个全新的MySQL的Pod,这个Pod是一干二净的,原来Pod的数据不会被保留也不会被恢复,这样你就得跑路了~

因此,对数据的持久化是非常有必要的!

存储卷Volume

存储 | Kubernetes

Volume 的生命周期独立于容器,Pod 中的容器可能被销毁和重建,但 Volume 会被保留。

在Docker中,我们可以通过-v参数来指定挂载目录。而在K8s中,我们需要通过定义存储卷(Volume)来满足数据持久化的需求。

Kubernetes支持很多类型的卷。同一个pod也可以挂载多个多种不同类型的卷。

在这里插入图片描述

总体来看可以分为四大类:

  1. 本地卷:hostPath-直接挂载到宿主机文件类型、emptyDir-数据的存储取决于宿主机所使用的介质,比如磁盘或 SSD 或网络存储。(这种卷的数据直接保存在宿主机的本地磁盘上,并且Pod只能在该特定节点上访问这些数据)
  2. 网络数据卷:比如Nfs、ClusterFs、Ceph,这些都是外部的存储都可以挂载到k8s上。
  3. 云盘:云服务商自行提供的存储方案。
  4. K8s的自身资源:比如Secret、ConfigMap等。

emptyDir

用于存储临时数据的简单空目录。

工作流程:

当 Pod 被分派到某个 Node 上时,emptyDir 卷会被自动创建在该Pod中,并且在 Pod 在该节点上运行期间,卷一直存在。当 Pod 因为某些原因被从节点上删除时,emptyDir 卷中的数据也会被永久删除。但是容器崩溃并不会导致 Pod 被从节点上移除,因此容器崩溃期间 emptyDir 卷中的数据是安全的。

用途:

  • 临时空间,例如用于某些应用程序运行时所需要的临时目录,且无需永久保留
  • 同一个Pod中的 一个容器需要从另一个容器中获取数据的目录(多容器共享目录)

缺点:

emptyDir不能提供数据持久化,也无法跨节点同步数据,所以仅在容器临时存放数据时使用它。

emptyDir存储卷的配置示例:

spec:
volumes:
- name: volume1
emptyDir: {}
- name: volume2
emptyDir:
medium: Memory
sizeLimit: 100Mi
  • medium

    作为卷来使⽤的emptyDir是在承载Pod的⼯作节点的实际磁盘上创建的, 因此其性能取决于节点的磁盘类型。如果需要高速的读写性能,我们可以通知Kubernetes在tmpfs⽂件系统(储存在内存⽽⾮硬盘)上创建emptyDir。因此,将emptyDir的medium设置为Memory。

  • sizeLimit

    可以限制emptyDir被容器填充的大小,则填写此项。如:100Mi,20Gi。此项在大多数卷中是通用的。

emptyDir卷是最简单的卷类型,其他类型的卷都是在它的基础上构建的,在创建空⽬录后,其他类型的卷会⽤数据填充它。

hostPath

⽤于将⽬录从⼯作节点的⽂件系统挂载到Pod中。

工作流程:

就是将工作节点中的一个实际目录挂载进Pod中,以供容器使用,提供对主机上文件的访问。它可以保证数据的持久化,即使Pod被销毁了,数据也仍然在工作节点上。它和我们在docker run时的-v参数很类似。

用途:

  • 如果Pod需要使用Node上的某些东西时,比如某容器需要访问Docker,可使用hostPath 挂载宿主机节点的/var/lib/docker(Docker的工作目录)
  • 某些系统级别的Pod(通常由DaemonSet管理,例如DDNS)确实需要读取节点的⽂件或使⽤节点⽂件系统来访问节点设备

DaemonSet会在每个节点上都运行一个指定的的Pod实例,无论节点是什么时候加入或离开集群。

缺点:

  1. 仅提供了单节点的数据持久化,不提供跨节点的数据同步。一般不建议使用这种存储方式(除非是单节点),因为当Pod被调度到另一个Node上时,它会找不到数据。而且理论上⼤多数Pod应该忽略它们的主机节点,因此它们不应该访问节点⽂件系统上的任何⽂件。
  2. 存在安全风险,可能会暴露特权系统凭据(例如 Kubelet)或特权 API(例如容器运行时套接字),可用于容器逃逸或攻击集群的其他部分。因此应该尽量避免使用hostPath,如确需使用,也应该限制它的范围只到所需的文件或目录,并且以只读的方式(readOnly: true)挂载。

hostPath存储卷的配置示例:

spec:
volumes:
- name: hostpath-volume
hostPath:
path: /path/to/node # 指定挂载目录
type: DirectoryOrCreate

在这里插入图片描述

实例演示:使用hostPath实现Nginx的日志持久化

指定两个容器:Nginx和BusyBox。

Nginx用于生成日志,并且将名为 logs-volume 的卷挂载到容器的 /var/log/nginx 目录。

BusyBox用来证明容器之间的数据可以实现共享,它将名为 logs-volume 的卷挂载到容器的 /logs 目录。

最后定义了名为 logs-volume 的卷,通过 hostPath 指定了它在宿主机上的路径为 /home/logs

apiVersion: v1
kind: Pod
metadata:
name: volume-hostpath
namespace: lesson-demo
spec:
containers:
- name: nginx
image: reg.redrock.team/library/nginx:latest
ports:
- containerPort: 80
volumeMounts:
- name: logs-volume
mountPath: /var/log/nginx
- name: busybox
image: busybox:1.30
command: ["/bin/sh","-c","tail -f /logs/access.log"]
volumeMounts:
- name: logs-volume
mountPath: /logs
volumes:
- name: logs-volume
hostPath:
path: /home/logs
type: DirectoryOrCreate

查看资源,Pod被调度在master3上。

image-20240505212147670

可以看到master3上已经自动生成了/home/logs目录,并且挂载了/var/log/nginx里的内容。

image-20240505214844144

连续访问三次Pod,访问被成功记录到了/home/logs/access.log中。

image-20240505215147402

进入busybox的容器中,查看busybox/logs/access.log中的内容,发现已经记录了三条访问记录。说明不同容器之间可以共享数据并进行相互交互。

image-20240505215123023

此时删除Pod后,发现数据仍然存在。

image-20240505205251540

image-20240505215200500

NFS

可以通过网络,让不同的机器、不同的操作系统可以共享彼此的文件。

当运⾏在⼀个Pod中的应⽤程序需要将数据保存到磁盘上,并且即使该Pod重新调度到另⼀个节点时也要求具有相同的数据可⽤。这就不能使⽤到⽬前为⽌我们提到的任何一种卷类型,由于这些数据需要可以从任何集群节点访问, 因此必须将其存储在某种类型的⽹络存储NAS(Network Attached Storage)中。

工作流程:

NFS服务器可以让计算机将网络中的NFS服务器共享的目录挂载到本地端的文件系统中,而在本地端的系统中来看,那个远程主机的目录就好像是自己的一个磁盘分区一样,在使用上相当便利。

用途:

一般做数据的共享存储,保证多个节点提供一致性的程序。

缺点:

  1. 容易发生单点故障,Server机宕机了后所有客户端都不能访问
  2. 在高并发下NFS性能有限
  3. 客户端无用户认证机制,且数据是通过明文传送,安全性一般(一般建议在局域网内使用)
  4. NFS的数据是明文的,对数据完整性不做验证

为了多节点数据同步及持久化,目前已有更优的cephfs方案,它也是一种网络文件系统。如果你在各大云平台购买Kubernetes容器服务,他们也会提供更底层的数据同步卷及插件,这些卷可挂载到容器中,也可卸载下来复用。但是由于NFS是最简单的多节点数据同步及持久化方案,我们使用它来演示。

NFS服务端的安装与配置(Debian系)

安装NFS服务端

apt update
apt install nfs-kernel-server

创建一个NFS共享目录

mkdir -p /root/nfs

修改/etc/exports文件(这个文件包含了NFS 服务器共享的目录),编辑内容为:

/root/nfs/ *(insecure,rw,sync,no_subtree_check,no_root_squash)
# rw代表读写访问,sync代表所有数据在请求时写入共享,no_subtree_check代表不检查父目录权限,no_root_squash代表root用户具有根目录的完全管理访问权限

使用以下命令启动NFS服务器

systemctl enable rpcbind
systemctl enable nfs-server

systemctl start rpcbind
systemctl start nfs-server
exportfs -r # 重新导入配置文件

exportfs

image-20240505222446947

配置完NFS后我们就可以在Kubernetes中使用它了。

NFS存储卷的配置示例:

spec:
volumes:
- name: nfs-volume
nfs:
server: 172.20.14.180
path: /root/nfs

持久卷和持久卷声明

以上的存储方案看起来十分不错,但到⽬前为⽌,我们探索过的所有持久卷类型都要求Pod的开发⼈员了解集群中可⽤的真实⽹络存储的基础结构。例如,要创建⽀持NFS协议的卷,开发⼈员必须知道NFS节点所在的实际服务器。这对开发人员很不友好,因为他们应该专注于开发而不是部署。而且将这种涉及基础设施类型的信息塞到⼀个Pod设置中,意味着Pod设置与特定的Kubernetes集群有很⼤耦合度。这就不能在另⼀个Pod中使⽤相同的设置了。所以使⽤这样的卷并不是在Pod中附加持久化存储的最佳实践。

在理想情况下,在Kubernetes上部署应用程序的开发人员是应该不需要知道集群提供了什么存储技术,与基础设施相关的交互才是集群管理员该做的事情。当开发⼈员需要⼀定数量的持久化存储来进⾏应⽤时,可以向Kubernetes请求,就像在创建Pod时可以请求CPU、内存和其他资源⼀样。集群管理员可以对集群进⾏配置让其可以为应⽤程序提供所需的服务。

什么是持久卷和持久卷声明?

为了解决上面出现的问题,K8s给出了一种解决方案:持久卷PersistentVolume (PV)和 持久卷声明PersistentVolumeClaim(PVC)

PersistentVolume (PV) 是外部存储系统中的一块存储空间,由管理员创建和维护。与 Volume 一样,PV 具有持久性,生命周期独立于 Pod。

PersistentVolumeClaim (PVC) 是对 PV 的申请 (Claim)。PVC 通常由普通用户创建和维护。需要为 Pod 分配存储资源时,用户可以创建一个 PVC,指明存储资源的容量大小和访问模式(比如只读)等信息,Kubernetes 会查找并提供满足条件的 PV。

有了 PersistentVolumeClaim,用户只需要告诉 Kubernetes 需要什么样的存储资源,而不必关心真正的空间从哪里分配,如何访问等底层细节信息。这些需要提供存储的底层信息交给管理员来处理,只有管理员才应该关心创建 PersistentVolume 的细节信息。

持久卷(PV)

现在,为了使开发人员不用关心集群上的存储资源的具体细节,我们首先充当集群管理员来配置持久卷。之后,我们再充当开发人员,来使用持久卷完成开发。

如何申请一个PV呢?

在创建持久卷时,管理员需要告诉Kubernetes其对应的容量需求,以及它是否可以由单个节点或多个节点同时读取或写⼊。管理员还需要告诉Kubernetes当持久卷声明的绑定被删除时如何处理PersistentVolume。最后,管理员需要指定持久卷⽀持的实际存储类型、位置和其他属性。

持久卷的配置示例:

apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-pv
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
nfs:
server: 172.20.14.180
path: /root/nfs

配置项:

  • Capacity:指定 PV 的容量
  • accessModes:指定访问模式为 ReadWriteOnce,支持的访问模式有:
    • ReadWriteOnce:读写权限,并且只能被单个Node挂载。
    • ReadOnlyMany :只读权限,允许被多个Node挂载。
    • ReadWriteMany:读写权限,允许被多个Node挂载。
  • persistentVolumeReclaimPolicy:指定当 PV 的回收策略为 Recycle,支持的策略有:
    • Retain:需要管理员手工回收
    • Recycle:清除 PV 中的数据并使PV重新可用,效果相当于执行 rm -rf /path/*(将被弃用)
    • Delete:删除存储资源并删除PV

使用该命令查看PV。注意PV不属于任何一个命名空间,它跟节点⼀样是集群层⾯的资源。

kubectl get pv

image-20240506210842821

此时的PV没有被任何PVC绑定,因此它的状态是Available可用状态。

持久卷声明(PVC)

现在我们扮演开发人员来使用之前创建的持久卷来开发应用。

我们现在需要使用刚才创建的PV,这个PV是不能像存储卷那样直接在Pod使用的,我们得先创建一个持久卷声明,告诉Kubernetes我们现在需要一个PV,Kubernetes会根据这个清单(PVC)来寻找一个合适的PV。

持久卷声明的配置实例:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-pvc
namespace:
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: "" # 下一部分将详述

当创建好声明,Kubernetes就会自动找到适当的持久卷并将其绑定到声明,持久卷的容量必须⾜够⼤以满⾜声明的需求,并且卷的访问模式必须包含声明中指定的访问模式。在该⽰例中,声明请求5G的存储空间和ReadWriteOnce访问模式。之前创建的持久卷符合刚刚声明中的这两个条件,所以它被绑定到对应的声明中。我们可以通过检查声明来查看。

创好持久卷声明之后,使用以下命令查看所有持久卷声明:

kubectl get pvc

访问模式的简写含义:

  • RWO(ReadWriteOnce):仅允许单个节点挂载读写。
  • ROX(ReadOnlyMany):允许多个节点挂载只读。
  • RWX(ReadWriteMany):允许多个节点挂载读写这个卷。

image-20240506211151286

PV与PVC已经成功绑定(Bound)

PV显⽰被绑定在default/nfs-pvc的PVC上,这个default部分是PVC所在的命名空间(在默认命名空间中创建的声明),我们之前有提到过PV是集群范围的,因此不能在特定的命名空间中创建,但是PVC又只能在特定的命名空间创建,所以PV和PVC只能被同⼀命名空间内的Pod创建使⽤。

在Pod中使用持久卷声明

要在Pod中使⽤持久卷,需要在Pod的卷中引⽤持久卷声明名称。

apiVersion: v1
kind: Pod
metadata:
name: database
spec:
volumes:
- name: data
persistentVolumeClaim:
claimName: nfs-pvc
containers:
- image: busybox:latest
name: database
command: ["sleep"]
args: ["999999"]
volumeMounts:
- name: data
mountPath: /var/db

我们进入容器,在/var/db下新增一个文件,然后退出容器,发现nfs共享目录/root/nfs也新增了同样的文件。

image-20240506215618770

说明Pod已经成功使用了这个持久卷。

回收持久卷

删除Pod和PVC后,PV的状态为Released,不像之前的Available

image-20240506221056517

此时我们再次apply刚才删除的PVC,此时的PVC的状态为Pending,PV并没有和PVC成功绑定。

这是因为之前已经使⽤过这个卷,所以它可能包含前⼀个声明⼈的数据,如果集群管理员还没来得及清理,那么就不应该将这个卷绑定到全新的声明中。

根据回收策略进行回收

  • 手动回收持久卷

    通过将persistentVolumeReclaimPolicy设置为Retain从⽽通知到Kubernetes,我们希望在创建持久卷后将其持久化,让Kubernetes可以在持久卷从持久卷声明中释放后仍然能保留它的卷和数据内容。⼿动回收持久卷并使其恢复可⽤的⽅法是删除和重新创建持久卷资源。当这样操作时,你将决定如何处理底层存储中的⽂件:可以删除这些⽂件,也可以妥善保存或者复⽤它们。

  • 自动回收持久卷

    存在两种其他可⾏的回收策略:RecycleDelete

    第⼀种Recycle删除卷的内容并使卷可⽤于再次声明,通过这种⽅式,持久卷可以被不同的持久卷声明和Pod反复使⽤。此回收策略即将被弃用,不建议使用。

    ⽽另⼀种Delete策略删除底层存储及PV。

并不是所有类型的持久卷都支持这三种回收策略,在创建持久卷之前,⼀定要检查卷中所⽤到的特定底层存储⽀持什么回收策略。

持久卷的动态配置——存储类(StorageClass)

到目前为止,集群管理员仍然需要按照开发人员的需求来手动配置持久卷。即每出现一个持久卷声明,就需要一个对应的持久卷。这样做还是有些麻烦,还好Kubernetes可以通过动态配置持久卷来自动执行任务。

集群管理员可以创建一个持久卷配置,并且定义一个或多个存储类对象(StorageClass),这样开发人员就可以在其持久卷声明中引用存储类,K8s将根据持久卷声明动态分配一个符合要求的持久卷。

与管理员预先提供⼀组持久卷不同的是,动态配置持久卷需要定义若干个StorageClass,并允许系统在每次通过持久卷声明请求时创建⼀个新的持久卷。

存储类与持久卷类似,它也是一种集群资源。

创建一个存储类

如果你的集群部署在云服务提供商上,一般云服务提供商会提供好StorageClass资源,在使用时只需要指定SC的名称(storageClassName)即可。但我们的集群部署在本地,所以我们需要手动创建一个或多个StorageClass资源,然后才能创建新的持久卷。

helm创建storageClass资源

我们在这里安装longhorn

Longhorn | DocumentationLonghorn | Documentation

longhorn是一个轻量级且功能强大的云原生 Kubernetes 分布式存储平台,可以在任意基础设施上运行。

它有以下的优点:

  1. 可以创建跨集群灾难恢复卷,以便可以从第二个 Kubernetes 集群中的备份中快速恢复主 Kubernetes 集群中的数据
  2. 跨多个节点和数据中心复制块存储以提高可用性
  3. 将备份数据存储在 NFS 或 AWS S3 等外部存储中,然后从备份中还原卷等等

首先安装open-iscsi。longhorn依赖此协议。iSCSI 是一种远程存储协议,允许在 IP 网络上通过标准以太网连接访问存储资源。和NFS很类似,都是一种远程访问存储数据的网络协议。

apt install open-iscsi

然后使用helm安装

helm repo add longhorn https://charts.longhorn.io
helm repo update

kubectl create ns longhorn
helm install longhorn longhorn/longhorn -n longhorn

image-20240508001449518

删除时参照如下方法,不要直接helm uninstall

Uninstall Longhorn - 《Longhorn v1.4.1 Documentation》 - 书栈网 · BookStack

使用存储类

创建好StorageClass资源后,我们在PVC中直接指定storageClass资源名称即可,K8s就会自动为我们分配一个符合要求的PV。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-test
spec:
storageClassName: longhorn # 指定storageClass的名称
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi

apply后storageClass为我们自动分配了一个PV

image-20240507235405935

动态配置的持久卷其容量和访问模式是在PVC中所要求的。它的回收策略是Delete,这意味着当PVC被删除时,PV也将被删除。

集群管理员可以创建具有不同性能或其他特性的多个存储类,然后研发⼈员再决定对应每⼀个声明最适合的存储类。

存储类的好处在于,声明是通过名称引⽤它们的。因此,只要StorageClass名称在所有这些名称中相同,PVC定义便可跨不同集群移植。

不指定存储类的动态配置

我们在用NFS创建storageClass资源时指定了它为默认的存储类,当我们创建PVC时,如果不指定storageClassName,它就会默认使用你设置为默认的(default)作为存储类。

如果我们需要手动创建PV,不使用存储类,我们只需要把storageClassName: ""设置为空字符即可。

配置

向容器内传递命令行与参数

每个应用程序都是可执行程序文件,例如Nginx、Tomcat、MySQL等,但我们使用中,通常不会通过默认的配置参数来运行应用,一般都需要自定义符合我们场景的配置,那么就需要定义配置文件来完成。那我们的应用运行在容器中,应该如何定义配置信息呢?

Docker容器中的命令行与参数的传递

向容器命令传递参数

比如Dockerfile中的CMDENTRYPOINT指令。

ENTRYPOINT 指令用于设置容器启动时要执行的默认命令或程序,而 CMD 指令用于为 ENTRYPOINT 提供默认参数。当同时存在 ENTRYPOINTCMD 指令时,CMD 中的内容会被解释为覆盖 ENTRYPOINT 指定程序的默认参数。

比如这个Dockerfile。ENTRYPOINT 指定了在容器启动时要运行的命令,即 echo "Hello,",而 CMD 指定了作为参数传递给 ENTRYPOINT 指定的程序的默认参数,即 "world"。因此,当你启动这个容器时,它会打印出 "Hello, world"

FROM ubuntu
ENTRYPOINT ["echo", "Hello,"]
CMD ["world"]

但如果你在运行容器时覆盖了CMD时,那么CMD ["world"]中的world会被换位good morning,容器最后会打印出"Hello, good morning"

# 添加参数docker run <image> <arguments>
docker run my-image good morning

这样可以方便用户灵活地更改参数,从而无需更改Dockerfile。

将定义好的配置文件嵌入镜像文件中

你可以提前把配置文件写好,然后COPY或ADD在容器的指定位置。或者你也可以通过RUN指令搭配echo、sed之类的命令同样向配置文件中写入指定参数以达到更改配置文件的效果。

通过环境变量(Environment Variables)传递配置数据

通过环境变量为容器提供配置信息是最常见的使用方式,例如,使用MySQL官方提供的镜像文件启动MySQL容器时使用的MYSQL_ROOT_PASSWORD环境变量,它用于为MYSQL服务器的root用户设置登陆密码。

你可以在docker run时通过-e参数向环境变量传值即能实现应用配置。

基于Docker卷传送配置文件

你也可以事先将配置文件放置于宿主机之上的某个路径中,而后在启动容器时将其挂载进容器里。但这也依赖于用户需要事先将配置数据提供在宿主机上的特定路径下,而且在多主机模型中,若容器存在被调度至任一主机运行的可能性时,用户还需要将配置共享到任一宿主机来确保容器能够正常地获取到它们。

在Kubernetes中覆写命令行与参数

在Kubernetes中定义容器时,镜像的ENTRYPOINT和CMD均可以被覆盖,仅需在容器定义中分别设置属性command和args的值。

示例:

apiVersion: v1
kind: Pod
metadata:
name: test
spec:
containers:
- image: alpine:latest
name: test
command: ["sleep"]
args: ["999999"]

绝⼤多数情况下,只需要设置⾃定义参数。命令⼀般很少被覆盖,除⾮针对⼀些未定义ENTRYPOINT的通⽤镜像,例如busybox。

注意 command和args字段在pod创建后⽆法被修改。

如果你想传递多个参数,args是一个列表,在这个列表中指定多个字符串即可。

args: ["1","2","3"]

image-20240509171351227

可以看到其父进程(PID为1)为我们指定的命令与参数

为容器设置环境变量

在Kubernetes中使用镜像启动容器时,可以在Pod资源或Pod模版资源为容器配置使用env参数来定义所使用的环境变量列表。

apiVersion: v1
kind: Pod
metadata:
name: mysql
spec:
containers:
- name: mysql
image: mysql:5.7
env:
- name: MYSQL_ROOT_PASSWORD # 通过env指定了root用户的密码
value: "redrock"

ConfigMap

Kubernetes之ConfigMap详解及实践 - 知乎 (zhihu.com)

ConfigMaps | Kubernetes

为什么要引入ConfigMap?

如果一台服务器上部署多个服务:nginx、tomcat、apache等,那么这些配置都存在这个节点上。如果有一个服务出现问题,需要修改配置文件,每台物理节点上的配置都需要修改,这种方式肯定满足不了线上大批量的配置变更要求。所以,K8s 中引入了 ConfigMap资源对象,可以当成 Volume 挂载到 Pod 中,实现统一的配置管理。简单来说,一个ConfigMap对象就是一系列配置数据的集合,这些数据可“注入”到Pod对象中,并为容器应用所使用,注入方式有挂载为存储卷传递为环境变量两种。

优点:将配置存放在独⽴的资源对象中有助于在不同环境(开发、测试、质量保障和⽣产等)下拥有多份同名配置清单。Pod是通过名称引⽤ConfigMap的,因此可以在多环境下使⽤相同的Pod定义描述,同时保持不同的配置值以适应不同环境。

ConfigMap是名称空间级的资源,因此引用它的Pod必须处于同一名称空间中。

image-20230424205526940

创建ConfigMap

命令

kubectl create configmap <map-name> <data-source>

<map-name>为ConfigMap对象的名称,<data-source>是数据源。数据源可以是键值对类型的数据,也可以指定文件或目录来获取。

通过键值创建

创建一个名为mysql-config的ConfigMap,键值对第一个key为mysql_ip值为1.2.3.4 键值对第二个key为mysql_port值为3306

kubectl create configmap mysql-config \
--from-literal=mysql_ip=1.2.3.4 --from-literal=mysql_port=3306

image-20240509184756957

通过文件创建

命令

kubectl create configmap <configmap_name> --from-file=<[key=]source>

配置文件以键值对形式写入

mysql-config.yaml

mysql_ip: 1.2.3.4
mysql_port: 3306
kubectl create configmap mysql-config --from-file=./mysql-config.yaml

image-20240509185235800

通过目录创建

当配置文件数量较多时,我们可以在--from-file选项后所跟的路径指向一个目录路径就能把目录下的所有文件一同创建同一个 ConfigMap 资源中。

命令

kubectl create configmap <configmap_name> --from-file=<path-to-directory>

例如

ls /data/configs/nginx/conf.d/
myserver.conf myserver-gzip.cfg myserver-status.cfg

kubectl create configmap nginx-config-files --from-file=/data/configs/nginx/conf.d/

这种情况下,kubectl会为⽂件夹中的每个⽂件单独创建条⽬。image-20230424211107489

当从文件创建ConfigMap时,所有条⽬第⼀⾏最后的管道符号表⽰后续的条⽬值是多⾏字⾯量。(字面量是一种在 ConfigMap 中直接指定键和值的方式)

image-20240509185807309

直接通过资源配置清单创建
apiVersion: v1
kind: ConfigMap
metadata:
name: mysql-config
data:
mysql_ip: 1.2.3.4
mysql_port: 3306

向Pod中传递ConfigMap作为环境变量

格式

valueFrom:
configMapKeyRef:
key: # 要引用ConfigMap对象中某键的键名
name: # 要引用的ConfigMap对象的名称
optional: # 用于为当前Pod资源指明此引用是否为可选

创建一个ConfigMap

kubectl create configmap mysql-config --from-literal=mysql_password=redrock
valueFrom单一映射
apiVersion: v1
kind: Pod
metadata:
name: mysql
spec:
containers:
- name: mysql
image: mysql:5.7
env:
- name: MYSQL_ROOT_PASSWORD # 通过env指定了root用户的密码
valueFrom:
configMapKeyRef:
name: mysql-config # 要引用的ConfigMap对象的名称
key: mysql_password # 要引用ConfigMap对象中某键的键名
#optional: # 用于为当前Pod资源指明此引用是否为可选。当值为true时,即使ConfigMap不存在,容器也可以正常启动
envFrom全部映射

如果ConfigMap包含不少条⽬,Kubernetes提供了暴露ConfigMap的所有条⽬作为环境变量的⼿段。

apiVersion: v1
kind: Pod
metadata:
name: mysql
spec:
containers:
- name: mysql
image: mysql:5.7
envFrom:
- prefix: HTCPG_ # 给键加前缀可以避免从多个ConfigMap引入键值数据时产生键key重名(名称冲突)的问题
configMapKeyRef:
name: mysql-config # CM资源名称

你也可以向command和args字段中引用环境变量,使用$(VAR_NAME)的格式

......
command: ["mysql"]
args: ["-u","root","-p","$(MYSQL_ROOT_PASSWORD)"]
以ConfigMap存储卷形式挂载Pod读取配置文件

环境变量或者命令⾏参数值作为配置值通常适⽤于变量值较短的场景。由于ConfigMap中可以包含完整的配置⽂件内容,当你想要将其暴露给容器时,可以借助前⾯章节提到过的⼀种称为configMap卷的特殊卷格式。

ConfigMap卷会将ConfigMap中的每个条⽬均暴露成⼀个⽂件。运⾏在容器中的进程可通过读取⽂件内容获得对应的条⽬值。这种⽅法主要适⽤于传递较⼤的配置⽂件给容器。

先创建一个cm

kubectl create cm stu-config --from-file=/root/class3/cm-volume --from-literal=country=China

image-20240509202150107

apiVersion: v1
kind: Pod
metadata:
name: stu-mount
spec:
volumes:
- name: config-volume
configMap: # configMap形式存储卷
name: stu-config
containers:
- name: test-container
image: busybox
command: [ "/bin/sh", "-c", "sleep 1000000" ]
volumeMounts:
- name: config-volume
mountPath: /data/config

image-20240509202304229

ConfigMap的所有条目都以文件的形式挂载进了Pod中

如果只想要ConfigMap的部分内容,并自定义文件名,可通过items来配置,如下:

apiVersion: v1
kind: Pod
metadata:
name: stu-mount
spec:
volumes:
- name: config-volume
configMap:
name: stu-config
items: # 引用部分内容
- key: Tom.yaml
path: tom.yaml # 自定义文件名
- key: Mary.yaml
path: mary.yaml
containers:
- name: test-container
image: busybox
command: [ "/bin/sh", "-c", "sleep 1000000" ]
volumeMounts:
- name: config-volume
mountPath: /data/config

要注意挂载某⼀⽂件夹会隐藏该⽂件夹中已存在的⽂件。如果挂载⽂件夹是/etc,该⽂件夹通常包含不少重要⽂件。由于/etc下的所有⽂件不存在,容器极⼤可能会损坏。如果你希望添加⽂件⾄某个⽂件夹如/etc,绝不能采⽤这种⽅法。

如何能挂载ConfigMap对应⽂件⾄现有⽂件夹的同时不会隐藏现有⽂件?volumeMount额外的subPath字段可以被⽤作挂载卷中的某个独⽴⽂件或者是⽂件夹,⽆须挂载完整卷。挂载任意⼀种卷时均可以使⽤subPath属性。可以选择挂载部分卷⽽不是挂载完整的卷。

在/etc下增加一个Tom.yaml,而不是把全部覆盖掉/etc

apiVersion: v1
kind: Pod
metadata:
name: stu-mount-1
spec:
volumes:
- name: config-volume
configMap:
name: stu-config
containers:
- name: test-container
image: busybox
command: [ "/bin/sh", "-c", "sleep 1000000" ]
volumeMounts:
- name: config-volume
mountPath: /etc/Tom.yaml
subPath: Tom.yaml

image-20240509203852081

ConfigMap的更新

更新ConfigMap之后ConfigMap卷中对应⽂件的更新可能耗费数分钟。如果使用环境变量的方法,这些值将不会更新;如果在ConfigMap卷中使用了subPath属性,则这些文件也不会更新。在使用时需要注意这一点,必要时手动重启Pod。

Secret

到⽬前为⽌传递给容器的所有信息都是⽐较常规的⾮敏感数据。然⽽配置通常会包含⼀些敏感数据,如证书和私钥,需要确保其安全性。为了存储与分发此类信息,Kubernetes提供了⼀种称为Secret的单独资源对象用来传递敏感配置。

Secret结构与ConfigMap类似,均是键/值对的映射,但Secret专门用于保存机密数据。Secret的使⽤⽅法也与ConfigMap相同,可以将Secret条⽬作为环境变量传递给容器,或将Secret条⽬暴露为卷中的⽂件。

Kubernetes通过仅仅将Secret分发到需要访问Secret的pod所在的机器节点来保障其安全性。另外,Secret只会存储在节点的内存中,永不写⼊物理存储,这样从节点上删除Secret时就不需要擦除磁盘了。另外Secret通过base64编码存储于其中的敏感数据,这样做有两点好处:

  1. base64可以将一些非文本对象编码为文本值,如图片等。注意其大小不能超过1MB。
  2. base64编码后的数据人类不可读,这避免了一些时候操作人员无意中看到并记住密码的情况。

因此应采⽤ConfigMap存储⾮敏感的⽂本配置数据,采⽤Secret存储天⽣敏感的数据,通过键来引⽤。如果⼀个配置⽂件同时包含敏感与⾮敏感数据,该⽂件应该被存储在Secret中。

Secret有三种类型:

  • Opaque:base64编码格式的 Secret,用来存储密码、密钥等。

  • Service Account:用来访问Kubernetes API,由Kubernetes自动创建,并且会自动挂载到Pod的 /run/secrets/kubernetes.io/serviceaccount目录中。

  • DockerConfig:用来存储私有docker registry的认证信息。

默认令牌(Service Account)

默认被挂载⾄所有容器的Secret,用于容器访问Kubernetes API的身份认证。

创建并使用Opaque类型Secret

创建一个Secret用来存储mysql的root密码

kubectl create secret generic mysql-passwd --from-literal=passwd=redrock

image-20240509215248413

passwd被自动base64编码

在Pod中使用Secret

apiVersion: v1
kind: Pod
metadata:
name: mysql-secret
spec:
containers:
- name: mysql
image: mysql:5.7
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-passwd
key: passwd

通过Volume挂载到容器内部时,当该Secret的值发生变化时,容器内部具备自动更新的能力,但是通过环境变量设置到容器内部该值不具备自动更新的能力。所以一般推荐使用Volume挂载的方式使用Secret。

apiVersion: apps/v1 
kind: Deployment
metadata:
name: easybanner-deployment
namespace: bot
labels:
app: easybanner
spec:
replicas: 1
selector:
matchLabels:
app: easybanner
template:
metadata:
labels:
app: easybanner
spec:
volumes:
- name: easybanner-secret-config
secret:
secretName: easybanner-secret
containers:
- name: easybanner
image: reg.redrock.team/library/easybanner:1.0
env:
- name: App_ID
valueFrom:
secretKeyRef:
name: easybanner-secret
key: App_ID
- name: App_Secret
valueFrom:
secretKeyRef:
name: easybanner-secret
key: App_Secret
- name: URL
valueFrom:
secretKeyRef:
name: easybanner-secret
key: URL
- name: GIN_MODE
valueFrom:
secretKeyRef:
name: easybanner-secret
key: GIN_MODE
volumeMounts:
- name: easybanner-secret-config
mountPath: /app/secrets
readOnly: true
ports:
- containerPort: 8080

创建并使用DockerConfig Secret(镜像拉取Secret)

⼤部分组织机构不希望它们的镜像开放给所有⼈,因此会使⽤私有镜像仓库。部署⼀个pod时,如果容器镜像位于私有仓库,Kubernetes需拥有拉取镜像所需的证书。

我们可以直接create secret,只是参数有些差异。

kubectl create secret docker-registry secret-dockercfg --docker-username=xxx --docker-password=xxx --docker-email=xxx

而我们在宿主机docker login时,用户名和密码信息被保存在~/.docker/config.json,我们也可以对这个文件进行base64编码后填入.dockerconfigjson字段中。

base64 ~/.docker/config.json

apiVersion: v1
kind: Secret
metadata:
name: secret-dockercfg
type: kubernetes.io/dockerconfigjson #指定类型 使用新版本的
data:
.dockerconfigjson: |
"<base64 encoded ~/.docker/config.json file>"

然后再Pod中使用Secret

apiVersion: v1
kind: Pod
metadata:
name: pull-images
spec:
imagePullSecrets: # 在这里指定Secret名称
- name: secret-dockercfg
containers:
- image: zhangxinhui02/nginx:private
name: nginx-private