前言

作为运维工程师,你一定遇到过这样的场景:Web服务器需要上传文件,但有多台后端节点,文件放在哪台机器上都不合适。这时候,共享存储就是最好的解决方案,而NFS(网络文件系统)则是其中最简单、最经典的选择。

但是,手动部署NFS虽然步骤不多,却暗藏不少“坑”:权限怎么配?用户ID要不要统一?客户端挂载怎么写才能永久生效?

今天,我不只教你手动怎么配,更要带你一步步推导出——如何用Ansible将这个过程自动化,最终形成一个可复用、可扩展的部署剧本。

一、先搞懂NFS的核心概念

在动手之前,有几个关键概念必须理清:

1.1 NFS依赖什么服务?

很多人以为NFS只是一个服务,实际上它依赖两个东西:

  • rpcbind:负责RPC(远程过程调用)的端口映射。NFS服务启动后会在随机端口注册到rpcbind,客户端通过rpcbind找到NFS的正确端口。

  • nfs-utils:提供NFS服务端和客户端的所有工具(showmountexportfsmount.nfs等)。

小知识:服务端需要同时安装并启动nfs-serverrpcbind;客户端只需要rpcbindnfs-utils中的挂载工具。

1.2 共享目录怎么配置?

NFS的核心配置文件是/etc/exports,每一行的格式如下:

共享目录   允许访问的客户端(参数1,参数2,...)
/data/ 172.16.1.0/24(rw,sync,all_squash,anonuid=666,anongid=666)

参数解释:

参数 含义
rw 读写权限(ro是只读)
sync 同步写入,数据安全但性能稍差
all_squash 将所有客户端访问压缩为匿名用户
anonuid/anongid 匿名用户映射为哪个UID/GID

1.3 最难理解的权限问题

很多初学者在这里栽跟头:为什么客户端写进去的文件在服务端显示是nobody?为什么权限总是对不上?

核心原因:NFS默认将客户端的root用户压缩为nobody,普通用户则保留原有UID。

解决方案

  • 使用all_squash强制所有用户都走匿名映射

  • 配合anonuid=666,anongid=666统一映射到一个专用账号

  • 在服务端创建相同UID/GID的用户

这样,无论哪个客户端、用什么账号写入,文件最终都属于www用户(UID 666),完美解决权限混乱问题。

二、手动部署一遍,理清所有步骤

假设我们有这样的环境:

角色 IP地址 共享目录 挂载点
服务端 10.0.0.71 /data -
客户端 10.0.0.41 - /WebData

2.1 服务端手动步骤

# 1. 安装软件
yum install -y nfs-utils rpcbind

# 2. 创建共享目录和专用用户
groupadd -g 666 www
useradd -u 666 -g www -s /sbin/nologin -M www
mkdir /data
chown www:www /data

# 3. 配置exports文件
echo "/data/ 172.16.1.0/24(rw,sync,all_squash,anonuid=666,anongid=666)" > /etc/exports

# 4. 启动服务
systemctl start nfs
systemctl enable nfs

2.2 客户端手动步骤

# 1. 安装软件(注意:客户端不需要启动nfs服务)
yum install -y nfs-utils rpcbind

# 2. 启动rpcbind
systemctl start rpcbind
systemctl enable rpcbind

# 3. 创建挂载点
mkdir /WebData

# 4. 手动挂载(临时)
mount -t nfs 172.16.1.71:/data /WebData

# 5. 写入fstab(永久生效)
echo "172.16.1.71:/data /WebData nfs defaults 0 0" >> /etc/fstab

到这里,手动部署就完成了。但是,如果我有10台客户端呢?如果环境要重建呢?手动重复这些步骤显然不可取。

三、思考:如何用Ansible自动化?

Ansible的核心思想是声明式——你告诉它“最终状态是什么”,它帮你达到这个状态。

3.1 从手动命令到Ansible模块的映射

手动操作 Ansible模块 关键参数
yum install yum namestate=present
groupadd group namegid
useradd user nameuidgroupshellcreate_home
mkdir + chown file pathstate=directoryownergroup
写入/etc/exports copy 或 lineinfile contentdest
systemctl start/enable systemd namestateenabled
mount + 写/etc/fstab mount srcpathfstypestate=mounted

3.2 服务端Playbook的推导过程

第一步:安装软件

- name: install nfs server and rpcbind
  yum:
    name: nfs-utils,rpcbind
    state: present

第二步:准备共享目录和用户(注意顺序:先group再user再目录)

- name: create group with gid 666
  group:
    name: www
    gid: 666
    state: present

- name: create user with uid 666
  user:
    name: www
    uid: 666
    group: www
    shell: /sbin/nologin
    create_home: false

- name: create data directory
  file:
    path: /data
    state: directory
    owner: www
    group: www

第三步:写入配置文件

- name: configure nfs exports
  copy:
    content: "/data/ 172.16.1.0/24(rw,sync,all_squash,anonuid=666,anongid=666)"
    dest: /etc/exports

这里为什么用copy而不是lineinfile?因为/etc/exports通常只有一行配置,用content直接写最简单。如果是复杂配置,可以准备模板文件用template模块。

第四步:启动服务

- name: start and enable nfs server
  systemd:
    name: nfs
    state: started
    enabled: yes

3.3 客户端Playbook的推导过程

第一步:安装软件(和服务端一样)

- name: install nfs utils on client
  yum:
    name: nfs-utils,rpcbind
    state: present

第二步:启动rpcbind(客户端不需要启动nfs服务,但需要rpcbind)

- name: start and enable rpcbind
  systemd:
    name: rpcbind
    state: started
    enabled: yes

第三步:创建挂载点

- name: create mount directory
  file:
    path: /WebData
    state: directory

第四步:挂载共享目录(这个模块最强大:一次操作完成mount和写fstab)

- name: mount nfs share persistently
  mount:
    src: 172.16.1.71:/data
    path: /WebData
    fstype: nfs
    state: mounted

state=mounted的含义:

  • 如果未挂载,立即执行mount命令

  • 同时写入/etc/fstab,保证重启后自动挂载

  • 如果已挂载且参数正确,不做任何操作(幂等性)

四、完整的Ansible剧本

将上面的片段组合起来,就得到了完整的部署剧本:

服务端剧本(保存为 nfs-server.yml

- hosts: 10.0.0.71
  tasks:
    - name: install nfs server,rpcbind
      yum:
        name: nfs-utils,rpcbind
        state: present

    - name: configue nfs-utils
      copy:
        content: "/data/ 172.16.1.0/24(rw,sync,all_squash,anonuid=666,anongid=666)"
        dest: /etc/exports

    - name: create group
      group:
        name: www
        gid: 666
        state: present

    - name: create user
      user:
        name: www
        uid: 666
        group: www
        shell: /sbin/nologin
        create_home: false

    - name: create data
      file:
        path: /data
        state: directory
        owner: www
        group: www

    - name: start nfs server
      systemd:
        name: nfs
        state: started
        enabled: yes

客户端剧本(保存为 nfs-client.yml

- hosts: 10.0.0.41
  tasks:
    - name: nfs server,rpcbind of client
      yum:
        name: nfs-utils,rpcbind
        state: present

    - name: start rpcbind
      systemd:
        name: rpcbind
        state: started
        enabled: yes

    - name: create mount of directory
      file:
        path: /WebData
        state: directory

    - name: mount the shareDirectory on 10.0.0.41
      mount:
        src: 172.16.1.71:/data
        path: /WebData
        fstype: nfs
        state: mounted

一键执行

ansible-playbook -i "10.0.0.71,10.0.0.41" nfs-server.yml nfs-client.yml

五、你可能想问的几个问题

Q1:为什么服务端IP是10.0.0.71,但exports里写的是172.16.1.0/24?

这是生产环境的常见设计:管理网络(Ansible用的)和业务网络(NFS流量走的)分开。172.16.1.0/24是内网存储网络,性能更好、更安全。

Q2:如果有多台客户端怎么办?

很简单,把客户端的IP列表写进inventory文件:

[nfs_clients]
10.0.0.41
10.0.0.42
10.0.0.43

然后Playbook开头写成:

- hosts: nfs_clients

Q3:怎么验证部署成功了?

执行完Playbook后,在客户端运行:

df -h /WebData           # 查看是否挂载成功
touch /WebData/test.txt  # 测试写入

在服务端运行:

ls -l /data              # 查看文件属主应为www
showmount -e localhost   # 查看共享目录是否正确导出

六、总结:从手动到自动化的思维转变

回顾整个过程,我们经历了三个阶段:

  1. 理解原理:NFS依赖关系、exports参数含义、权限映射逻辑

  2. 手动实践:在命令行逐条执行,验证每一步的结果

  3. 自动化抽象:将命令转化为Ansible模块,关注“状态”而非“步骤”

这份剧本现在可以直接用于生产环境,更重要的是,要掌握方法论——任何重复的运维工作,都可以用同样的思路去自动化。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐