在 Kubernetes 中实现 NFS 动态存储供应:生产级实践指南
在 Kubernetes 环境中,持久化存储是支撑有状态应用的关键基础设施。NFS(Network File System)作为一种成熟、简单且广泛支持的共享文件系统,常被用于开发测试环境或对高可用性要求不极端苛刻的生产场景。然而,Kubernetes 原生并不支持 NFS 的动态供应(Dynamic Provisioning),这意味着管理员需要手动创建 PersistentVolume(PV)对象,这在大规模集群中是不可持续的。
本文将详细介绍如何利用社区项目 nfs-subdir-external-provisioner,在 Kubernetes 集群中实现 NFS 存储的动态供应,并提供一套完整的、可直接用于生产环境的 YAML 模板和最佳实践,同时涵盖NFS 服务器部署、离线环境镜像管理和自建 Harbor 私有仓库的集成方案。
一、NFS 服务器部署与配置
在开始 Kubernetes 集成之前,必须先准备一个稳定、安全的 NFS 服务器。以下是基于 Ubuntu 22.04 的详细部署步骤。
1.1 安装 NFS 服务端
在一台专用服务器(例如 192.168.1.10)上执行以下操作:
# 更新系统并安装 NFS 服务端
sudo apt update
sudo apt install -y nfs-kernel-server
# 创建用于 Kubernetes 的共享目录
sudo mkdir -p /data/k8s/monitor
sudo chown nobody:nogroup /data/k8s/monitor
sudo chmod 777 /data/k8s/monitor
1.2 配置共享目录
编辑 /etc/exports 文件,定义共享策略:
# /etc/exports
/data/k8s/monitor 192.168.1.0/24(rw,sync,no_subtree_check,no_root_squash)
参数说明:
192.168.1.0/24:允许访问的客户端网段,请替换为你的 Kubernetes 集群节点所在网段。rw:读写权限。sync:同步写入磁盘,保证数据一致性,但性能略低。如需更高性能,可改为async,但有数据丢失风险。no_subtree_check:禁用子树检查,提升性能。no_root_squash:允许 root 用户保留其权限。这是关键选项,因为容器内进程可能以 root 身份运行。
1.3 启动并验证 NFS 服务
# 重新加载 exports 配置
sudo exportfs -ra
# 重启 NFS 服务
sudo systemctl restart nfs-kernel-server
# 设置开机自启
sudo systemctl enable nfs-kernel-server
# 验证本地导出
sudo exportfs -v
1.4 配置防火墙
确保 NFS 相关端口对 Kubernetes 节点开放:
# Ubuntu (UFW)
sudo ufw allow from 192.168.1.0/24 to any port nfs
# 或者开放具体端口
sudo ufw allow 2049/tcp
sudo ufw allow 111/tcp
sudo ufw allow 111/udp
1.5 从 Kubernetes 节点验证连接
在任意一个 Kubernetes 工作节点上执行:
# 安装 NFS 客户端
sudo apt install -y nfs-common
# 测试挂载
sudo showmount -e 192.168.1.10
# 应输出: /data/k8s/monitor 192.168.1.0/24
# 手动挂载测试(可选)
sudo mkdir -p /mnt/test-nfs
sudo mount -t nfs 192.168.1.10:/data/k8s/monitor /mnt/test-nfs
echo "test" | sudo tee /mnt/test-nfs/test.txt
sudo umount /mnt/test-nfs
如果以上步骤均成功,说明 NFS 服务器已准备就绪。
二、核心概念与工作原理
2.1 为什么需要动态供应?
在 Kubernetes 中,用户通过 PersistentVolumeClaim(PVC)来申请存储资源。静态供应模式下,集群管理员必须预先创建好 PV,其大小、访问模式等属性必须与 PVC 请求精确匹配。这种方式在小型环境中可行,但在多租户、多应用的动态环境中,会带来巨大的管理负担。
动态供应通过 StorageClass 对象解耦了存储的定义与使用。用户只需在 PVC 中指定所需的 storageClassName,一个名为 Provisioner 的控制器便会自动创建底层存储(如云盘、NFS 子目录)并生成对应的 PV 对象,从而实现“按需分配”。
2.2 nfs-subdir-external-provisioner 是什么?
nfs-subdir-external-provisioner 是一个由 Kubernetes SIG Storage 维护的外部供应器(External Provisioner)。它本身不提供 NFS 服务,而是作为一个智能代理,利用你已有的 NFS 服务器来实现动态供应。
其工作原理如下:
- 监听事件:Provisioner 以 Deployment 形式运行在集群中,监听所有 PVC 的创建事件。
- 匹配 StorageClass:当发现 PVC 的
storageClassName与自身管理的StorageClass匹配时,触发供应逻辑。 - 创建子目录:Provisioner 在 NFS 服务器的根共享目录下,创建一个以
{namespace}-{pvc-name}-{pv-name}命名的子目录。 - 注册 PV:向 Kubernetes API Server 注册一个新的 PV 对象,其后端指向刚刚创建的 NFS 子目录。
- 绑定 PVC:Kubernetes 的持久卷控制器将新创建的 PV 与原始 PVC 绑定。
整个过程对用户透明,用户只需像使用云原生存储一样声明 PVC 即可。
三、生产级部署模板
以下是一套经过验证的、可直接应用于生产环境的 YAML 模板。请根据你的实际环境(NFS 服务器地址、共享路径等)修改相应的值。
3.1 RBAC 权限配置 (01-nfs-rbac.yaml)
此文件定义了 Provisioner 运行所需的最小权限集,包括操作 PV/PVC、读取 StorageClass 以及进行 Leader 选举的能力。
# File: nfs-on-k8s/01-nfs-rbac.yaml
# 创建专用的 ServiceAccount
apiVersion: v1
kind: ServiceAccount
meta
name: nfs-client-provisioner
namespace: nfs-provisioner
---
# 定义集群角色,授予操作持久化资源的权限
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
meta
name: nfs-client-provisioner-runner
rules:
- apiGroups: [""]
resources: ["persistentvolumes"]
verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "watch", "update"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "update", "patch"]
---
# 将集群角色绑定到 ServiceAccount
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
meta
name: run-nfs-client-provisioner
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
namespace: nfs-provisioner
roleRef:
kind: ClusterRole
name: nfs-client-provisioner-runner
apiGroup: rbac.authorization.k8s.io
---
# 定义命名空间内的角色,用于 Leader 选举
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
meta
name: leader-locking-nfs-client-provisioner
namespace: nfs-provisioner
rules:
- apiGroups: [""]
resources: ["endpoints"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
---
# 将角色绑定到 ServiceAccount
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
meta
name: leader-locking-nfs-client-provisioner
namespace: nfs-provisioner
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
namespace: nfs-provisioner
roleRef:
kind: Role
name: leader-locking-nfs-client-provisioner
apiGroup: rbac.authorization.k8s.io
3.2 Provisioner 控制器部署 (02-nfs-deployment.yaml)
这是核心组件。请特别注意 NFS_SERVER 和 NFS_PATH 的值,它们必须与你的 NFS 服务器配置完全一致。
# File: nfs-on-k8s/02-nfs-deployment.yaml
apiVersion: apps/v1
kind: Deployment
meta
name: nfs-client-provisioner
namespace: nfs-provisioner
spec:
replicas: 1
# 使用 Recreate 策略确保任何时候只有一个实例在运行
strategy:
type: Recreate
selector:
matchLabels:
app: nfs-client-provisioner
template:
meta
labels:
app: nfs-client-provisioner
spec:
# 使用前面创建的 ServiceAccount
serviceAccountName: nfs-client-provisioner
containers:
- name: nfs-client-provisioner
# 【关键】在离线或私有仓库环境中,替换为你的镜像地址
image: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2
# 关键:在离线环境中必须设置为 IfNotPresent
imagePullPolicy: IfNotPresent
volumeMounts:
- name: nfs-root
# Provisioner 将在此路径下创建子目录
mountPath: /persistentvolumes
env:
# PROVISIONER_NAME 必须与 StorageClass 中的 provisioner 字段一致
- name: PROVISIONER_NAME
value: k8s-sigs.io/nfs
- name: NFS_SERVER
value: 192.168.1.10 # 替换为你的 NFS 服务器 IP
- name: NFS_PATH
value: /data/k8s/monitor # 替换为你的 NFS 共享路径
volumes:
- name: nfs-root
nfs:
server: 192.168.1.10 # 与上面的 NFS_SERVER 保持一致
path: /data/k8s/monitor # 与上面的 NFS_PATH 保持一致
3.3 生产级 StorageClass 定义 (03-nfs-storageclass.yaml)
此文件定义了名为 nfs-client-prod 的存储类,包含了生产环境所需的关键参数。
# File: nfs-on-k8s/03-nfs-storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
meta
name: nfs-client-prod
provisioner: k8s-sigs.io/nfs
parameters:
# 【关键】数据保留策略:PVC删除时重命名目录而非直接删除
archiveOnDelete: "true"
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
allowVolumeExpansion: false
mountOptions:
- vers=4.1
- noatime
- nodiratime
- hard
- intr
- nconnect=8
3.4 测试用例 (test-pvc.yaml 和 test-pod.yaml)
用于验证整个动态供应流程是否正常工作。
# File: nfs-on-k8s/test-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
meta
name: test-nfs-pvc
spec:
# 引用我们创建的 StorageClass
storageClassName: nfs-client-prod
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
# File: nfs-on-k8s/test-pod.yaml
apiVersion: v1
kind: Pod
meta
name: test-nfs-pod
spec:
containers:
- name: nginx
image: nginx:1.29.4
imagePullPolicy: IfNotPresent
volumeMounts:
- name: data
mountPath: /usr/share/nginx/html
volumes:
- name: data
# 挂载前面创建的 PVC
persistentVolumeClaim:
claimName: test-nfs-pvc
四、生产环境 StorageClass 参数详解
在生产环境中,StorageClass 的配置需要优先考虑数据安全和系统稳定性。
archiveOnDelete: "true":这是最重要的数据保护机制。当 PVC 被删除时,NFS 子目录会被重命名为archived-*,而不是直接删除,有效防止了因误操作导致的数据永久丢失。reclaimPolicy: Retain:将数据删除的最终控制权交给存储管理员。即使 PV 对象被删除,后端数据依然保留,为恢复操作提供了最后的机会。volumeBindingMode: WaitForFirstConsumer:延迟 PV 的绑定,直到使用该 PVC 的 Pod 被调度。这可以避免在集群初始化阶段就创建大量无用的 PV,优化了资源分配。allowVolumeExpansion: false:NFS 本身没有卷大小的概念,所谓的“扩容”只是修改了元数据,容易导致应用层和存储层的认知不一致。在没有完善的配额管理前,禁用此功能更为安全。mountOptions:vers=4.1:使用更健壮的 NFS v4.1 协议。noatime, nodiratime:禁止更新访问时间戳,提升 I/O 性能。hard, intr:保证数据一致性的同时,允许请求被中断。nconnect=8:建立多个 TCP 连接以并行传输数据,大幅提升吞吐量。
五、部署与验证流程
5.1 应用配置
按照顺序应用上述 YAML 文件:
# 1. 创建命名空间
kubectl create namespace nfs-provisioner
# 2. 应用 RBAC
kubectl apply -f nfs-on-k8s/01-nfs-rbac.yaml
# 3. 部署 Provisioner
kubectl apply -f nfs-on-k8s/02-nfs-deployment.yaml
# 4. 创建 StorageClass
kubectl apply -f nfs-on-k8s/03-nfs-storageclass.yaml
5.2 验证 Provisioner
确保 Provisioner Pod 处于 Running 状态:
kubectl get pods -n nfs-provisioner
5.3 执行测试
# 创建测试 PVC
kubectl apply -f nfs-on-k8s/test-pvc.yaml
# 创建测试 Pod
kubectl apply -f nfs-on-k8s/test-pod.yaml
# 验证
kubectl get pvc test-nfs-pvc # 应为 Bound
kubectl exec test-nfs-pod -- ls /usr/share/nginx/html
同时,登录你的 NFS 服务器,检查共享目录下是否生成了新的子目录。
六、离线与私有化环境的镜像管理策略
在企业内网、安全隔离或无外网访问的环境中,无法直接从 registry.k8s.io 等公共仓库拉取镜像。我们需要采用两种主要策略来解决这个问题:本地预加载和自建私有仓库。
6.1 策略一:本地预加载(适用于小规模或临时环境)
此方法将镜像直接导入到每个 Kubernetes 节点的容器运行时中。
步骤 1:准备镜像
在一个有网络的机器上,拉取并保存镜像:
docker pull registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2
docker save registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2 -o nfs-provisioner.tar
步骤 2:分发并导入
将 .tar 文件复制到所有 Kubernetes 工作节点,并导入到 containerd:
# 导入到 k8s.io 命名空间
sudo ctr -n k8s.io images import nfs-provisioner.tar
步骤 3:验证
crictl images | grep nfs-subdir-external-provisioner
优点:简单直接,无需额外基础设施。
缺点:难以维护和扩展,每次更新镜像都需要手动同步到所有节点。
6.2 策略二:自建 Harbor 私有仓库(推荐用于生产环境)
Harbor 是一个开源的企业级 Docker Registry 项目,提供了权限管理、漏洞扫描、镜像复制等高级功能,是管理私有镜像的理想选择。
步骤 1:部署 Harbor
可以使用 Helm Chart 或 Docker Compose 在内网部署 Harbor。假设你的 Harbor 地址为 harbor.local.com。
步骤 2:推送镜像到 Harbor
# 重新打标签
docker tag registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2 harbor.local.com/library/nfs-subdir-external-provisioner:v4.0.2
# 登录并推送
docker login harbor.local.com
docker push harbor.local.com/library/nfs-subdir-external-provisioner:v4.0.2
步骤 3:配置 Kubernetes 节点信任 Harbor(如果使用 HTTPS 自签名证书)
将 Harbor 的 CA 证书添加到所有 Kubernetes 节点的信任库中。
步骤 4:修改 Deployment 配置
将 02-nfs-deployment.yaml 中的镜像地址指向你的私有仓库,并移除 imagePullPolicy: IfNotPresent(因为现在可以从私有仓库动态拉取):
containers:
- name: nfs-client-provisioner
image: harbor.local.com/library/nfs-subdir-external-provisioner:v4.0.2
# imagePullPolicy: IfNotPresent # 可注释掉,使用默认策略
步骤 5:配置 ImagePullSecrets(如果 Harbor 需要认证)
# 创建 Secret
kubectl create secret docker-registry harbor-secret \
--docker-server=harbor.local.com \
--docker-username=admin \
--docker-password=yourpassword \
-n nfs-provisioner
# 在 Deployment 的 spec.template.spec 下添加
imagePullSecrets:
- name: harbor-secret
优点:
- 集中管理:所有镜像统一存储,便于版本控制和审计。
- 自动化:CI/CD 流水线可以直接推送和拉取镜像。
- 安全性:支持基于角色的访问控制(RBAC)和内容信任。
- 可扩展:支持跨数据中心的镜像复制。
七、总结与注意事项
通过 nfs-subdir-external-provisioner,我们可以轻松地为 Kubernetes 集群赋予 NFS 动态供应能力。对于镜像管理,自建 Harbor 私有仓库是生产环境的最佳选择,它提供了集中化、安全且可扩展的解决方案。
然而,在生产环境中使用 NFS 仍需谨慎:
- 单点故障:NFS 服务器本身是一个单点。对于关键业务,应考虑部署高可用的 NFS 集群(例如使用 DRBD + Pacemaker 或 NFS-Ganesha)。
- 性能瓶颈:NFS 的性能受限于网络带宽和服务器 I/O 能力,不适合高吞吐或低延迟的应用场景。
- 权限管理:需要仔细规划 NFS 导出选项(如
no_root_squash)和容器内的用户 ID(UID),以避免权限问题。 - 备份策略:务必为 NFS 服务器上的数据制定定期备份计划。
尽管如此,对于日志收集、配置共享、开发测试等场景,NFS 动态供应仍然是一个高效、经济的解决方案。
七、参考资源
nfs-subdir-external-provisioner
文章分类标签
Kubernetes, DevOps, SRE, 存储, NFS, 容器化, 运维自动化, 云原生, Harbor, 镜像仓库