文章目录
- 概述
- 卷
- 卷的常用类型
- emptyDir
- 边车容器
- HostPath
- nfs
- PV/PVC
- 静态供给 PV 和 PVC
- 创建静态 PV
- 创建 pvc
- 创建 pod 应用 pvc
- 动态供给 PV 和 PVC
- 创建 StorageClass
- 创建 pvc
- 创建 pod 使用 pvc
- PV 的生命周期
- 内置存储对象
- ConfigMap
- Secret
- 配置文件自动重新加载方案
- **1. 应用内动态检测文件变更**
- **2. 通过信号触发重新加载**
- **3. 使用 Reloader 或类似工具**
- **4. 手动重启 Pod**
- 拉取镜像的脚本
概述
在虚拟机的环境中,应用程序的数据通常存储在本地磁盘上,即使重启虚拟机也不会数据丢失。但是在 pod 中。pod 的特点就是 “临时性” ,随着 pod 的重建,容器中的数据也会消失,这将导致一些应用程序读不到之前的数据。因此 pod 引入了持久化这个概念,也就是 “卷”
卷
卷是 pod 中存储数据和共享数据的一个抽象概念。它提供了一种将存储设备挂载进容器的机制。
卷的常用类型
卷的分类 | 卷类型 | 说明 |
---|---|---|
临时存储 | emptyDir | 用于 pod 中,容器之间的共享 |
本地存储 | hostPath | 将节点文件系统上的文件或者目录挂载到 pod 中 |
对象存储 | ConfigMap,Secret | k8s 内置的存储对象,用于存储应用程序配置和敏感数据 |
自建存储系统 | NFS,Ceph,ISCSI | 将自建的存储系统挂载到 pod 中 |
存储对象 | persistentVolumesClaim(PVC) | 与 PV 持久卷配合使用 |
emptyDir
empytDir 用于在 pod 中实现容器之间的数据共享,与 pod 的生命周期一致,当 pod 被删除时,对应的目录也会销毁
[root@k8s-master ~]# cat emptyDir.yaml
apiVersion: v1
kind: Pod
metadata:name: pod-emptydir
spec:containers:- image: docker.io/library/centos:latestimagePullPolicy: IfNotPresentname: appcommand: ["/bin/sh","-c","for i in {1..10000};do echo $i >> /opt/file.txt;sleep 1;done"]volumeMounts:- name: datamountPath: /opt- image: docker.io/library/centos:latestimagePullPolicy: IfNotPresentname: sidecar # 边车容器command: ["/bin/sh","-c","tail -f /opt/file.txt"]volumeMounts:- name: datamountPath: /optvolumes:- name: dataemptyDir: {}
[root@k8s-master ~]# kubectl apply -f emptyDir.yaml
pod/pod-emptydir created
[root@k8s-master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
pod-emptydir 2/2 Running 0 3s
在上面的例子中,我们定义了两个容器和一个 emptyDir 卷,该卷被挂载到两个容器中的同一个目录下了,因此该目录中的文件可以被彼此访问
[root@k8s-master ~]# kubectl exec -it pod-emptydir --container app -- /bin/bash
Defaulted container "app" out of: app, sidecar
[root@pod-emptydir /]# cd opt/
[root@pod-emptydir opt]# ls
file.txt
[root@k8s-master ~]# kubectl exec -it pod-emptydir --container sidecar -- /bin/bash
[root@pod-emptydir /]# cd opt/
[root@pod-emptydir opt]# ls
file.txt
边车容器
边车容器(Sidecar Container)是与主应用容器在同一个Pod中运行的辅助容器。它们通过提供额外的服务或功能(如日志记录、监控、安全性或数据同步)来增强或扩展主应用容器的功能,而无需直接修改主应用代码。边车容器与主容器共享网络和存储命名空间,使得它们能够紧密交互并共享资源。
除了边车容器,Kubernetes还支持其他类型的容器,包括:
-
标准容器(Application Containers):这是最常见的容器类型,用于运行主要的应用逻辑。
-
Init 容器:这些容器在应用容器启动之前运行,用于执行一些初始化任务,比如设置配置文件或者等待外部服务就绪。Init 容器在Pod中的所有应用容器启动前完成执行并退出。
-
Ephemeral 容器:这是一种临时性的容器,它们缺少对资源或执行的保证,并且永远不会自动重启。Ephemeral 容器主要用于调试目的,允许用户加入一个临时容器到正在运行的Pod中,用于调试。
HostPath
hostpath 卷用来将宿主机的目录挂载进容器,这使得容器可以访问宿主机的数据,由于挂载的是宿主机的目录,因此在容器被销毁后,数据并不会丢失。
但是在 k8s 卷的分类中,我们还有一个专门做持久卷的,名叫 PV/PVC 。那这两种挂载方式到底差距在哪呢:
HostPath:
- 直接挂载宿主机本地路径到容器,适合单节点和开发环境。
- 不支持跨节点存储,数据与特定节点绑定。
- 无自动扩展功能,存储容量和管理完全依赖宿主机。
- 没有 Kubernetes 对存储的生命周期管理,容器删除后数据可能丢失。
PV/PVC:
- Kubernetes 管理的持久化存储,可以通过 PVC 请求动态存储资源。
- 支持跨节点、云存储等多种后端,具备高可用性和扩展性。
- 具备自动扩展、容量管理和存储生命周期管理功能。
- 支持不同存储策略(如保留、删除等),适用于生产环境。
那我们在 yaml 文件中如何挂载它呢?
[root@k8s-master ~]# cat hostPath.yaml
apiVersion: v1
kind: Pod
metadata:name: hostpath-pod
spec:containers:- name: hostpath-containerimage: docker.io/library/nginx:latestimagePullPolicy: IfNotPresentvolumeMounts:- name: hostpath-volumemountPath: /usr/share/nginx/htmlvolumes:- name: hostpath-volumehostPath:path: /data # 这会挂载宿主机的 /data 目录到容器的 /usr/share/nginx/html 目录type: DirectoryOrCreate
[root@k8s-master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
hostpath-pod 1/1 Running 0 4s
在该 yaml 文件中,我们将 /usr/share/nginx/html 挂载到容器里面的 data 目录下了
hostPath 所支持的卷的类型 (这些参数在 yaml 文件中有对应)
取值 | 作用 |
---|---|
”“ | 该字段为空或者未指定,默认是 DirectoryOrCreate |
DirectoryOrCreate | 如果指定的目录不存在,则会自动创建空目录并为其赋值 0755 |
Directory | 指定的目录必须存在 |
FileOrCreate | 和上面那个差不多,在空文件被创建出来之后默认赋权 0644 |
File | 指定文件必须存在 |
Socket | 指定套接字文件必须存在 |
CharDevice | 指定的字符设备必须存在 |
BlockDevice | 指定的块设备必须存在 |
k8s 在 pod 启动时会检查路径是否与期望类型所匹配,如果不匹配或者类型检查异常,pod 会呈现 ContainerCreateing 状态
hostpath 卷不支持存储容器限制,并且可使用的存储容量受主机文件系统限制
nfs
将 nfs 服务器挂载到 pod 中,实现 pod 之间的数据共享。我们在之前还提到过一个 emptyDir 和 nfs 挂载的实现的功能是一致的,那他们的区别在哪呢?
NFS 和 emptyDir 的主要区别是:
- 持久性:
- NFS:数据持久化,Pod 删除后数据保留。
- emptyDir:数据临时存储,Pod 删除时数据丢失。
- 共享范围:
- NFS:可以跨多个节点和 Pod 共享数据。
- emptyDir:仅限单个 Pod 内的容器共享数据。
- 适用场景:
- NFS:需要跨 Pod 和节点共享数据的场景。
- emptyDir:适合 Pod 内部容器间共享临时数据。
pv对接nfs共享,使用静态创建的方式创建pvc
[root@k8s-master ~]# cat nginx.json
{"apiVersion": "apps/v1","kind": "Deployment","metadata": {"name": "nginx"},"spec": {"selector": {"matchLabels": {"app": "nginx"}},"template": {"metadata": {"labels": {"app": "nginx"}},"spec": {"containers": [{"image": "docker.io/library/nginx:latest","imagePullPolicy": "IfNotPresent","name": "nginx","volumeMounts": [{"name": "data","mountPath": "/data"}]}],"volumes": [{"name": "data","nfs": {"server": "192.168.142.139","path": "/data/nfs"}}]}}}
}
mkdir -p /data/nfs
chmod 777 /data/nfs/
vim /etc/exports
/data/nfs *(no_root_squash,rw,no_all_squash)
systemctl restart nfs-server.service
showmount -e 192.168.142.139[root@k8s-slave1 ~]# df -h | grep 192.168.142.139:/data/nfs
df: /var/lib/kubelet/pods/6210f716-6da2-4a14-b320-33b169b684bc/volumes/kubernetes.io~nfs/mypv1: Stale file handle
192.168.142.139:/data/nfs 17G 6.3G 11G 38% /var/lib/kubelet/pods/fc2ccf93-693b-44b8-93f7-6d60f2f1b412/volumes/kubernetes.io~nfs/data
PV/PVC
在之前,我们已经简单的介绍了一下 PV/PVC 与 hostPath 之间的对比,也是知道了 PV/PVC 存在的意义,那么什么是 PV ? 什么是 PVC ?
持久卷(PV)就是 Kubernetes 用来管理集群中存储的工具,它让你不用关心底层存储的具体实现,只要把它用作存储就行。简单来说,就是让你的数据在 Pod 重启或销毁后还能保留下来。
持久卷声明(PVC)是用户申请存储的方式,简单来说,就是你向 Kubernetes 请求一个存储空间。PVC 就像是一个 “存储请求单”,你告诉 Kubernetes 需要多大的存储空间,Kubernetes 会找一个合适的持久卷(PV)来满足这个请求。
你可以把 PV 和 PVC 想象成 房子 和 租房合同 的关系:
-
PV(持久卷) 就是一个已经建好的 房子,它有一定的空间和资源,可能是物理硬盘、NFS 共享或云存储等。
-
PVC(持久卷声明) 就是你去 租房,向 Kubernetes 提出需要多大面积的房子,Kubernetes 会找一个合适的 房子(PV) 给你。
所以,PVC 是你用来申请 PV 的工具,PVC 就像是租房合同,PV 是你租到的房子。
在 PV/PVC 的世界里还有两个非常重要的概念:PV 静态供给,PV 动态供给
-
静态供给:管理员手动创建好 PV(持久卷),然后用户通过 PVC(持久卷声明) 来请求匹配的 PV。这种方式下,管理员负责管理存储资源。
-
动态供给:当用户创建 PVC 时,Kubernetes 会根据 StorageClass 自动创建一个符合要求的 PV。这种方式下,管理员不需要事先创建 PV,Kubernetes 会自动提供存储。
下面是两个示例:
静态供给 PV 和 PVC
创建静态 PV
[root@k8s-master ~]# cat pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:name: static-pv
spec:capacity:storage: 10GiaccessModes:- ReadWriteOnce # 只能被一个节点挂载persistentVolumeReclaimPolicy: Retain # PV 删除后数据保留storageClassName: standardhostPath:path: /data/static-pv # 宿主机路径
persistentVolumeReclaimPolicy 参数有三个值可以选
-
Retain:保留数据。PV 被释放后,存储资源不会被回收,管理员需要手动处理该 PV。数据保留在原位置,可以进行手动清理或重新绑定到新的 PVC。
-
Recycle(已弃用,不再推荐使用):回收数据。在这种模式下,PV 被释放后,Kubernetes 会尝试清除存储中的数据(通常是执行
rm -rf /some-directory/*
操作),然后将 PV 设置为可以重新绑定的状态。这种方式已被 Kubernetes 弃用。 -
Delete:删除数据。PV 被释放后,Kubernetes 会自动删除与 PV 相关联的存储资源(如删除一个云存储卷)。数据将被永久删除。
创建 pvc
[root@k8s-master ~]# cat pvc.yaml
# pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:name: static-pvc
spec:accessModes:- ReadWriteOnceresources:requests:storage: 10Gi # 请求10GB的存储storageClassName: standard
创建 pod 应用 pvc
pull-image.sh 是我自己编写的脚本,专门用来拉取镜像的,如果小伙伴们也是 container 无法拉取镜像,不妨可以试一试我这个脚本,脚本放到最后
[root@k8s-master ~]# ./pull-image.sh nginx:latest 192.168.142.139 192.168.142.140 192.168.142.141
镜像 nginx:latest 已存在于本地
Docker save nginx:latest
Docker save nginx:latest is successful
Sending nginx-latest.tar to 192.168.142.139
root@192.168.142.139's password:
nginx-latest.tar 100% 187MB 947.0MB/s 00:00
Successfully sent nginx-latest.tar to 192.168.142.139
root@192.168.142.139's password:
镜像 nginx:latest 已存在于 192.168.142.139
Sending nginx-latest.tar to 192.168.142.140
root@192.168.142.140's password:
nginx-latest.tar 100% 187MB 355.8MB/s 00:00
Successfully sent nginx-latest.tar to 192.168.142.140
root@192.168.142.140's password:
导入镜像到 192.168.142.140
root@192.168.142.140's password:
unpacking docker.io/library/nginx:latest (sha256:466e72df2f0b10ecb0dc90dc99d523a1c6432b764ebcbcdab3f0a7ef5cd4e061)...done
ctr import nginx-latest.tar is successful on 192.168.142.140
Sending nginx-latest.tar to 192.168.142.141
root@192.168.142.141's password:
nginx-latest.tar 100% 187MB 325.1MB/s 00:00 1
Successfully sent nginx-latest.tar to 192.168.142.141
root@192.168.142.141's password:
导入镜像到 192.168.142.141
root@192.168.142.141's password:
unpacking docker.io/library/nginx:latest (sha256:466e72df2f0b10ecb0dc90dc99d523a1c6432b764ebcbcdab3f0a7ef5cd4e061)...done
ctr import nginx-latest.tar is successful on 192.168.142.141
[root@k8s-master ~]# cat pod1.yaml
# pod.yaml
apiVersion: v1
kind: Pod
metadata:name: static-pod
spec:containers:- name: nginximage: docker.io/library/nginx:latestimagePullPolicy: IfNotPresentvolumeMounts:- mountPath: /usr/share/nginx/htmlname: static-storagevolumes:- name: static-storagepersistentVolumeClaim:claimName: static-pvc
[root@k8s-master ~]# kubectl apply -f pod1.yaml
pod/static-pod created
[root@k8s-master ~]# kubectl get pod
static-pod 1/1 Bound 0 5s
以上就是静态供给了,但是相较于静态供给,还是动态供给香多了
动态供给 PV 和 PVC
在动态供给中,管理员配置了存储类(StorageClass),Kubernetes 会根据 PVC 的需求动态创建一个 PV。
创建 StorageClass
# storage-class.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:name: dynamic-storage
provisioner: kubernetes.io/aws-ebs # 使用 AWS EBS 或者可以是其他存储提供商
parameters:type: gp2
创建 pvc
[root@k8s-master ~]# cat dynamic-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:name: dynamic-pvc
spec:accessModes:- ReadWriteOnceresources:requests:storage: 1GistorageClassName: dynamic-storage # 确保 PVC 引用正确的 StorageClass
创建 pod 使用 pvc
[root@k8s-master ~]# cat pod-dynamic.yaml
# pod-dynamic.yaml
apiVersion: v1
kind: Pod
metadata:name: dynamic-pod
spec:containers:- name: nginximage: docker.io/library/nginx:latestimagePullPolicy: IfNotPresentvolumeMounts:- mountPath: /usr/share/nginx/htmlname: dynamic-storagevolumes:- name: dynamic-storagepersistentVolumeClaim:claimName: dynamic-pvc
最后的结果和上面静态供给是差不多的
演示到这里,pv / pvc 大概就是这么用的
PV 的生命周期
最后,我们再来聊一聊 pv 的生命周期:pv 的生命周期包含多个阶段:
- Provisioning PV 供给: 可以通过静态供给和动态供给创建 PV
- Binding 绑定: 在 PV 创建后,并且处于 available 状态时,PVC 与 PV 绑定,此时 PV 就会转换成 Bound
- Using 使用: 当 PVC 与 PV 成功绑定之后,Pod 获取到 PV 的存储资源,从而将容器中的数据存储到外部的存储系统中
- Releasing 释放: 当 PVC 被删除时 , PV 也会随之被删除,具体行为由回收策略决定。
- Reclaiming 回收: 释放后,根据回收策略执行相应的操作,具体的操作就是上面讲到的 Retain ,Recycle ,Delete
另外,动态供给创建的 pv ,回收策略默认使用的是 Delete,但是这个可以在 StorageClass 的 yaml 文件中通过 reclaimPolicy 参数进行修改
内置存储对象
ConfigMap
它是一种专门用来存储各种配置文件的 pod,它以键值对的形式存储保存数据。
例如 nginx 的配置文件等等
下面是一个简单的示例:
[root@k8s-master ~]# cat nginx-proxy-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:name: nginx-config
data:a.conf: |server {listen 80;server_name a.example.com;location / {proxy_pass http://192.168.142.139:8080}}
当我们看到 data 的时候是不是似曾相识,这个就是 nginx 反向代理的配置文件
[root@k8s-master ~]# kubectl apply -f nginx-proxy-configmap.yaml
configmap/nginx-config created
[root@k8s-master ~]# kubectl get cm
NAME DATA AGE
kube-root-ca.crt 1 11d
nginx-config 1 7s
那 ConfigMap 如何配合 PV 使用呢?
# nginx-proxy-pod.yaml
apiVersion: v1
kind: Pod
metadata:name: nginx-proxy
spec:containers:- name: webimage: docker.io/library/nginx:latestimagePullPolicy: IfNotPresentvolumeMounts:- name: configmountPath: /etc/nginx/conf.d # 将 nginx 的配置文件挂载进容器volumes:- name: configconfigMap:name: nginx-config
[root@k8s-master ~]# kubectl get pod
nginx-proxy 1/1 Running 0 46s[root@k8s-master ~]# kubectl exec nginx-proxy -- ls /etc/nginx/conf.d a.conf
/etc/nginx/conf.d:
a.conf
每次挂载新的配置文件都有一个等待的过程,在这个过程中,存在一种自动更新的机制。kubelet 组件会定期检查 pod 挂载的 ConfigMap 对象中的数据是否发生变更,如果发生变更,就会将最新的配置文件再次加载到容器中去,以确保 pod 始终是最新的配置。
如果只想挂载指定的 键 或者 文件名,我们还可以这样:
volumes:
- name: configconfigMap:name: nginx-configitems:- key: "a.conf"path: "a.example.com.conf"
这样,只有 ConfigMap 中键为 a.conf 的数据会被挂载到容器中。
还有一件事,默认情况下 /etc/nginx/conf.d 目录下是会有一个 defalut.conf 文件的,但是当我们将 a.conf 挂载进去之后,它就不见了,这其实是因为我们的挂载操作是覆盖操作。然而,我们要如何避免这种事情呢?
volumeMounts:
- name: configmountPath: /etc/nginx/conf.d/a/example.com.confsubPath: a.example.com.conf
volumes:
- name: configconfigMap:name: nginx-configitems:- key: "a.conf"path: "a.example.com.conf"
但是要注意的是,当我们使用了 subPath 之后,kubelet 不会自动更新新的数据到 pod 中
Secret
它比较敏感,因为它主要负责存储一些特别重要的信息,比如找密码,密钥证书等。
secret 支持三种类型
-
Opaque(默认类型):
- 描述:最常用的
Secret
类型。用于存储任意的键值对数据,这些数据会被以 base64 编码的形式存储。 - 示例用途:存储用户名和密码、API 密钥等。
- 默认类型:如果在
Secret
的定义中没有指定类型,Kubernetes 会默认使用Opaque
类型。
- 描述:最常用的
apiVersion: v1
kind: Secret
metadata:name: mysecret
type: Opaque
data:username: bXl1c2VybmFtZQ== # base64 编码的用户名password: bXlwYXNzd29yZA== # base64 编码的密码
-
kubernetes.io//service-account-token:
- 描述:此类型的
Secret
存储的是服务账户的令牌,它会自动由 Kubernetes 控制平面创建和管理。 - 示例用途:为 pod 提供与 Kubernetes API 通信所需的身份验证令牌。
- 创建方式:当你创建服务账户时,Kubernetes 会自动创建与该账户关联的
Secret
。
- 描述:此类型的
apiVersion: v1
kind: Secret
metadata:name: default-token-abcdeannotations:kubernetes.io/service-account.name: default
type: kubernetes.io/service-account-token
data:token: <base64 encoded token>ca.crt: <base64 encoded ca.crt>
-
kubernetes.io//dockercfg 和 kubernetes.io//dockerconfigjson:
- 描述:这两种类型的
Secret
用于存储 Docker 配置文件,通常用于存储 Docker 仓库的认证信息。 - 示例用途:在
Secret
中存储 Docker 的认证信息,以便 Kubernetes 能够从私有 Docker 仓库拉取镜像。 - 区别:
kubernetes.io/dockercfg
存储.dockercfg
配置文件,而kubernetes.io/dockerconfigjson
存储的是 Docker 配置 JSON 文件(更现代的格式)。
示例
kubernetes.io/dockerconfigjson
: - 描述:这两种类型的
apiVersion: v1
kind: Secret
metadata:name: my-docker-secret
type: kubernetes.io/dockerconfigjson
data:.dockerconfigjson: <base64 encoded docker config json>
当然在 k8s 的一些老版本中是这第三个:docker-registry ,generic 以及 tls
- Kubernetes 1.2 是 Secret 类型支持的重要版本:
Opaque
(默认类型)、kubernetes.io/service-account-token
和kubernetes.io/dockercfg
同时被引入。 - Kubernetes 1.9 开始推荐使用
kubernetes.io/dockerconfigjson
,这是更现代的私有镜像认证方式。 - 当然这两种方式只是 不同表达方式,它们并没有严格意义上的更新替换,只是在文档和实践中有些变化。
配置文件自动重新加载方案
当 ConfigMap 和 Secret 以卷的方式被挂载到容器中时,如果它们发生改变,那么最新的数据也会被更新到 pod 中。为了使得最新的配置生效,应用程序还需要有自动检测和处理变更的能力,具体的实现方式有以下几种:
1. 应用内动态检测文件变更
- 原理:应用程序自行监控挂载文件的变更,并动态加载最新内容。
- 实现方式:
- 使用文件监听工具(如 inotify)。
- 定期轮询挂载路径,检测文件更新。
- 适用场景:轻量级服务,业务逻辑明确,适合直接修改应用代码。
2. 通过信号触发重新加载
- 原理:监听指定信号(如
SIGHUP
),触发配置重新加载。 - 实现方式:
- 在应用程序内设置信号处理器。
- 配合文件挂载变更,使用工具发送信号:
kill -SIGHUP <PID>
- 适用场景:对应用进行小幅改动即可实现,适合中等规模服务。
3. 使用 Reloader 或类似工具
- 原理:借助外部工具监控 ConfigMap 或 Secret 变更,触发 Pod 滚动更新。
- 实现方式:
- 部署 Reloader(或类似工具)。
- 在 Pod 上配置注解:
metadata:annotations:reloader.stakater.com/match: "true"
- 适用场景:需要在更新时完全重启服务,适合大型分布式服务。
4. 手动重启 Pod
- 原理:手动触发 Pod 滚动更新,以应用最新配置。
- 实现方式:
- 修改 Deployment 或 StatefulSet 的 annotation:
kubectl patch deployment <name> -p '{"spec":{"template":{"metadata":{"annotations":{"date":"<current-timestamp>"}}}}}'
- 适用场景:非频繁更新情况下的简易方案。
拉取镜像的脚本
仅支持检测 tar 包,具体的使用方法,开袋即食,跑完脚本就直接写 yaml 文件就可以了,前提是我们要下 yaml 文件的 image 下再加一条 imagePullPolicy: IfNotPresent / Never 。然后还有一点要注意的就是如果本地有镜像,并且已经 docker save 了,那么镜像务必使 tar 结尾,不然脚本会重新为你 docker save ,然后就是具体的使用方法,在上面,已经有示范了
#!/bin/bash# 检查 Docker 是否安装
check_docker_installed() {if ! docker -v > /dev/null 2>&1; thenecho "Docker is not installed"exit 1fi
}# 检查是否提供了足够的参数
check_args() {if [ "$#" -lt 2 ]; thenecho "Usage: $0 <image-name> <host1> [<host2> ...]"exit 1fi
}# 检查本地是否存在指定的镜像
check_local_image() {local image_name=$1if docker images --format '{{.Repository}}:{{.Tag}}' | grep -q "$image_name"; thenecho "镜像 $image_name 已存在于本地"return 0elseecho "正在拉取 Docker 镜像 $image_name..."if docker pull "$image_name" > /dev/null 2> pull.log; thenecho "Docker 镜像 $image_name 拉取成功"return 0elseecho "Docker 镜像 $image_name 拉取失败。详情查看 pull.log。"exit 1fifi
}# 检查 tar 文件是否存在
check_tar_file() {local image_name=$1local tar_file=$2if [ -f "$tar_file" ]; thenecho "镜像文件 $tar_file 已经存在"return 0elseecho "Docker save $image_name"if docker save -o "$tar_file" "$image_name" 2> save.log; thenecho "Docker save $image_name is successful"return 0elseecho "Docker save $image_name failed. Check save.log for details."exit 1fifi
}# 发送和导入镜像到其他主机
send_and_import_image() {local image_name=$1local tar_file=$2shift 2 # 移动参数,使得 $@ 包含剩余的主机地址for host in "$@"; doecho "Sending $tar_file to $host"scp "$tar_file" "root@$host:/root/" && echo "Successfully sent $tar_file to $host" || echo "Failed to send $tar_file to $host"# 检查目标主机上是否存在镜像if ssh root@$host "docker images --format '{{.Repository}}:{{.Tag}}' | grep -q '$image_name'"; thenecho "镜像 $image_name 已存在于 $host"elseecho "导入镜像到 $host"if ssh root@$host "ctr -n k8s.io images import /root/$tar_file"; thenecho "ctr import $tar_file is successful on $host"elseecho "ctr import $tar_file failed on $host"fifidone
}# 主函数
main() {check_docker_installedcheck_args "$@"local image_name=$1local iso="${image_name//:/-}"local tar_file="$iso.tar"check_local_image "$image_name"check_tar_file "$image_name" "$tar_file"send_and_import_image "$image_name" "$tar_file" "${@:2}"
}# 调用主函数
main "$@"