4.Ansible部署文件到主机
4 - 部署文件到受管主机
实验环境
先通过以下命令搭建基础环境(创建工作目录、配置 Ansible 环境和主机清单):
# 在控制节点(controller)上创建web目录并进入,作为工作目录
[laoma@controller ~]$ mkdir web && cd web# 创建Ansible配置文件ansible.cfg,定义连接和权限提升参数
[laoma@controller web]$ cat > ansible.cfg <<'EOF'
[defaults]
remote_user = laoma # 默认远程连接用户为laoma
inventory = ./inventory # 指定主机清单文件路径为当前目录的inventory[privilege_escalation]
become = True # 允许提权(切换到其他用户)
become_user = root # 提权目标用户为root
become_method = sudo # 提权方式为sudo
become_ask_pass = False # 提权时不询问密码(需提前配置sudo免密)
EOF# 创建主机清单文件inventory,列出需要管理的主机
[laoma@controller web]$ cat > inventory <<'EOF'
controller # 控制节点自身
node1 # 受管节点1
node2 # 受管节点2
node3 # 受管节点3
node4 # 受管节点4
EOF
修改文件并将其复制到主机
Ansible 的 Files 模块库包含一系列用于文件管理的工具,能帮我们完成创建文件、复制文件、修改权限等日常 Linux 文件操作。下面逐个介绍常用模块的用法。
file 模块:管理文件 / 目录的属性(创建、删除、改权限等)
模块作用:用于设置文件 / 目录的权限、所有者、SELinux 上下文等属性,也能创建或删除文件 / 目录。
实验 1:创建文件并设置权限
实验流程:
- 编写 playbook,定义要创建的文件路径、所有者、权限等;
- 执行 playbook;
- 登录受管节点验证文件是否创建及属性是否正确。
---
- hosts: node1 # 目标主机为node1gather_facts: no # 不收集主机信息(加快执行速度)tasks:- name: Touch a file and set permissions # 任务描述:创建文件并设置权限file:path: /tmp/testfile # 目标文件路径owner: laoma # 文件所有者为laomagroup: wheel # 文件所属组为wheelmode: 0640 # 文件权限为640(注意必须带前导0,否则会解析错误)state: touch # 状态为创建文件(类似touch命令)
注意:
mode
参数必须带前导 0(如0640
)或用引号包裹(如'640'
)。如果直接写640
,Ansible 会当作十进制处理,导致权限错误(比如变成-w-------T
)。
实验 2:创建目录
实验流程:
- 编写 playbook,指定目录路径、所有者、权限;
- 执行 playbook;
- 验证目录是否创建及属性是否正确。
---
- hosts: node1gather_facts: notasks:- name: create directory # 任务描述:创建目录file:path: /webdev # 目标目录路径owner: apache # 目录所有者为apachegroup: apache # 目录所属组为apachemode: 0755 # 目录权限为755(读/写/执行权限:所有者全有,组和其他有读和执行)state: directory # 状态为创建目录(类似mkdir命令)
... # 后面playbook脚本大部分省略了这个符号(写的时候必须得有)
实验 3:删除文件
实验流程:
- 编写 playbook,指定要删除的文件路径;
- 执行 playbook;
- 验证文件是否被删除。
---
- hosts: node1gather_facts: notasks:- name: delete file # 任务描述:删除文件file:path: /tmp/testfile # 要删除的文件路径state: absent # 状态为"不存在"(即删除文件,类似rm命令)
sefcontext 模块:管理 SELinux 的持久规则
模块作用:在 SELinux 的规则库中添加或删除持久规则(类似semanage fcontext
命令),但规则不会立即生效,需要通过restorecon
等命令重新标注文件后才生效。
实验:添加 SELinux 上下文规则
实验流程:
- 编写 playbook,定义要设置的文件路径和 SELinux 类型;
- 执行 playbook 添加规则;
- (可选)在受管节点执行
restorecon
使规则生效。
---
- hosts: node1gather_facts: notasks:- sefcontext:target: '/samba(/.*)?' # 匹配/samba目录及所有子目录文件setype: samba_share_t # 设置SELinux类型为samba_share_t(用于Samba共享)state: present # 状态为"存在"(即添加规则)
注意:
sefcontext
仅修改规则库,不会直接改变现有文件的 SELinux 上下文。如果要立即生效,需配合file
模块(file
模块会实时应用上下文)或手动执行restorecon -R /samba
。
lineinfile 模块:管理文件中的单行内容
模块作用:确保文件中存在特定行、替换符合条件的行,或在指定位置插入行(类似sed
命令的单行处理)。
实验 1:向文件添加特定行
实验流程:
- 编写 playbook,指定目标文件和要添加的行;
- 执行 playbook;
- 查看文件验证行是否添加成功。
---
- hosts: node1gather_facts: notasks:- name: add line # 任务描述:添加一行内容lineinfile:path: /tmp/testfile # 目标文件路径line: 'Add this line to file' # 要添加的行内容state: present # 确保行存在(如果不存在则添加)
实验 2:在指定位置插入行
实验流程:
- 编写 playbook,指定目标文件、要插入的行,以及插入位置(在某行之前 / 之后);
- 执行 playbook;
- 验证行是否插入到正确位置。
# 在"Listen 80"行之前插入"Listen 82"
---
- hosts: node1gather_facts: notasks:- name: add line before Listen 80lineinfile:path: /etc/httpd/conf/httpd.conf # 目标文件(httpd配置文件)line: 'Listen 82' # 要插入的行(监听82端口)insertbefore: 'Listen 80' # 插入到"Listen 80"行之前state: present# 在"Listen 80"行之后插入"Listen 82"
---
- hosts: node1gather_facts: notasks:- name: add line after Listen 80lineinfile:path: /etc/httpd/conf/httpd.confline: 'Listen 82'insertafter: 'Listen 80' # 插入到"Listen 80"行之后state: present
实验 3:替换符合条件的行
实验流程:
- 编写 playbook,用正则表达式匹配要替换的行,指定新内容;
- 执行 playbook;
- 验证匹配的行是否被替换。
# 替换包含"Add"的行
---
- hosts: node1gather_facts: notasks:- name: replace line # 任务描述:替换行内容lineinfile:path: /tmp/testfileregexp: 'Add' # 正则表达式:匹配包含"Add"的行line: 'replace' # 替换后的内容state: present# 注释掉httpd的80端口监听(在行首加#)
---
- hosts: node1gather_facts: notasks:- name: comment Listen 80lineinfile:path: /etc/httpd/conf/httpd.confline: '#Listen 80' # 替换后的内容(注释掉)regexp: '^Listen 80' # 匹配以"Listen 80"开头的行state: present
replace 模块:批量替换文件中的内容
模块作用:用正则表达式匹配文件中所有符合条件的内容,并一次性替换(类似sed -i 's/原内容/新内容/g'
,会替换所有匹配项)。
实验:替换所有匹配的行
实验流程:
- 编写 playbook,定义目标文件、匹配规则和替换内容;
- 执行 playbook;
- 验证所有匹配的内容是否被替换。
---
- hosts: node1gather_facts: notasks:- name: replace multi line # 任务描述:批量替换行replace:path: /tmp/testfile # 目标文件regexp: '^Hello World.*' # 匹配以"Hello World"开头的所有行replace: 'Hello Laoma' # 替换后的内容
blockinfile 模块:管理文件中的多行文本块
模块作用:向文件中插入、更新或删除多行文本块,插入的内容会自动添加标记(# BEGIN ANSIBLE MANAGED BLOCK
和# END ANSIBLE MANAGED BLOCK
),方便后续管理。
实验:向文件添加多行文本块
实验流程:
- 编写 playbook,定义目标文件和要添加的多行内容;
- 执行 playbook;
- 查看文件验证文本块是否添加,及是否包含标记。
---
- hosts: node1gather_facts: notasks:- name: add block lines to file # 任务描述:添加多行文本块blockinfile:path: /tmp/testfile # 目标文件block: | # 要添加的多行内容(| 表示保留换行)line 1 in fileline 2 in fileaaline 3 in file sssstate: present # 确保文本块存在
执行后,文件中会添加:
# BEGIN ANSIBLE MANAGED BLOCK
line 1 in file
line 2 in fileaa
line 3 in file sss
# END ANSIBLE MANAGED BLOCK
stat 模块:获取文件的状态信息
模块作用:检索文件的详细信息(如权限、校验和、创建时间等),类似 Linux 的stat
命令,结果可用于后续任务判断。
实验:获取文件的 MD5 校验和
实验流程:
- 编写 playbook,用 stat 模块获取文件信息并保存到变量;
- 执行 playbook,通过 debug 模块输出校验和;
- 查看输出结果验证。
---
- hosts: node1gather_facts: notasks:- stat:path: /tmp/testfile # 目标文件checksum_algorithm: md5 # 指定校验和算法为MD5register: result # 将结果保存到变量result- debug:msg: "/tmp/testfile md5 is {{ result.stat.checksum }}" # 输出MD5值- debug:var: result # 输出所有文件信息(方便查看详细内容)
copy 模块:复制文件到受管节点
模块作用:将控制节点或其他远程节点的文件复制到受管节点,类似scp
命令,支持设置权限、强制覆盖等。
实验 1:复制控制节点文件到受管节点
实验流程:
- 在控制节点准备好要复制的文件(如
/tmp/testfile
); - 编写 playbook,指定源文件和目标路径;
- 执行 playbook;
- 在受管节点验证文件是否复制成功。
---
- hosts: node1gather_facts: notasks:- name: copy /tmp/testfile to remote node # 任务描述:复制文件copy:src: /tmp/testfile # 控制节点上的源文件路径dest: /tmp # 受管节点上的目标路径(保留原文件名)
说明:默认
force: yes
(强制覆盖),如果设置force: no
,则目标文件已存在时不会覆盖。
实验 2:直接写入字符串到文件
实验流程:
- 编写 playbook,用
content
参数定义文件内容; - 执行 playbook;
- 在受管节点查看文件内容是否正确。
---
- hosts: node1gather_facts: notasks:- name: write string into /tmp/testfile # 任务描述:写入字符串到文件copy:content: "hello world\n" # 要写入的内容(\n表示换行)dest: /tmp/testfile # 目标文件路径
synchronize 模块:基于 rsync 同步文件 / 目录
模块作用:封装了rsync
工具,用于高效同步文件 / 目录(速度比copy
快,适合大量文件),需要控制节点和受管节点都安装rsync
。
实验 1:同步单个文件
实验流程:
- 确保控制节点和受管节点都安装
rsync
(yum install -y rsync
); - 编写 playbook,指定源文件和目标路径;
- 执行 playbook;
- 验证文件是否同步成功。
---
- hosts: node1gather_facts: notasks:- name: synchronize file # 任务描述:同步文件synchronize:src: /tmp/testfile # 源文件(控制节点路径)dest: /tmp/ # 目标路径(受管节点)
实验 2:同步目录
实验流程:
- 准备要同步的目录(如控制节点的
/etc/sysconfig
); - 编写 playbook,指定源目录和目标路径;
- 执行 playbook;
- 验证目录内容是否同步成功。
---
- hosts: node1gather_facts: noremote_user: root # 同步目录可能需要root权限tasks:- name: synchronize directory # 任务描述:同步目录synchronize:src: /etc/sysconfig # 源目录(控制节点)dest: /tmp/ # 目标路径(受管节点,会创建sysconfig子目录)
fetch 模块:从受管节点获取文件到控制节点
模块作用:将受管节点的文件复制到控制节点,常用于收集日志、配置文件等,文件会按 “控制节点目标路径 / 受管节点名 / 原文件路径” 的结构保存。
实验:从受管节点获取文件
实验流程:
- 编写 playbook,指定受管节点的源文件和控制节点的目标路径;
- 执行 playbook;
- 在控制节点查看文件是否按预期路径保存。
---
- hosts: node1gather_facts: notasks:- name: fetch file from remote node # 任务描述:获取远程文件fetch:src: /tmp/testfile # 受管节点上的源文件dest: /tmp # 控制节点上的保存路径
执行后,文件会保存在控制节点的/tmp/node1/tmp/testfile
(结构:目标路径/受管节点名/原文件绝对路径
)。
使用 JINJA2 模板部署文件
Jinja2 模板是一种带变量和逻辑的文件,通过 Ansible 的template
模块部署到受管节点时,会自动替换变量和执行逻辑,生成个性化配置文件。
基础示例:部署动态网页
实验流程:
- 创建 Jinja2 模板文件(含变量);
- 编写 playbook,用
template
模块部署模板; - 执行 playbook,验证生成的文件是否替换了变量。
步骤 1:编写 playbook(deploy_web.yml
)
---
- name: Enable intranet services # 部署web服务hosts: node1tasks:- name: ensure latest version of httpd # 安装httpdyum:name: httpdstate: latest- name: deploy test html page # 部署动态主页template:src: index.html.j2 # 控制节点上的Jinja2模板dest: /var/www/html/index.html # 受管节点上的目标文件- name: start and enable httpd # 启动并设置开机自启service:name: httpdenabled: truestate: restarted
步骤 2:创建模板文件(index.html.j2
)
Welcome to {{ ansible_fqdn }} # {{ }} 中的变量会被替换为受管节点的完全主机名
步骤 3:执行与验证
执行 playbook 后,受管节点的/var/www/html/index.html
内容会变成:
Welcome to node1.laoma.cloud # 假设node1的完全主机名是node1.laoma.cloud
进阶示例:部署 SSH 配置文件
实验流程:
- 创建含多个变量的 Jinja2 模板;
- 在 playbook 中定义变量值;
- 部署模板并验证生成的配置文件是否正确。
步骤 1:编写模板(sshd_config.j2
)
# {{ ansible_managed }} # 会替换为"Ansible managed"(来自ansible.cfg配置)
# DO NOT MAKE LOCAL MODIFICATIONS TO THIS FILE AS THEY WILL BE LOST
Port {{ ssh_port }} # SSH端口(变量)
ListenAddress {{ ansible_facts['default_ipv4']['address'] }} # 监听IP(来自主机信息)HostKey /etc/ssh/ssh_host_rsa_key
HostKey /etc/ssh/ssh_host_ecdsa_key
HostKey /etc/ssh/ssh_host_ed25519_key
SyslogFacility AUTHPRIV
PermitRootLogin {{ root_allowed }} # 是否允许root登录(变量)
AllowGroups {{ groups_allowed }} # 允许登录的组(变量)AuthorizedKeysFile /etc/.rht_authorized_keys .ssh/authorized_keys
PasswordAuthentication {{ passwords_allowed }} # 是否允许密码认证(变量)
步骤 2:编写 playbook(deploy_ssh.yml
)
---
- name: config sshd service # 配置SSH服务hosts: node1vars: # 定义模板中用到的变量ssh_port: 1022root_allowed: "yes"groups_allowed: wheelpasswords_allowed: "yes"ansible_managed: "Ansible managed: do not edit manually" # 覆盖默认的ansible_managedtasks:- name: deploy sshd config # 部署模板template:src: sshd_config.j2dest: /root/sshd_config # 先部署到/root目录验证(实际应替换/etc/ssh/sshd_config)
Jinja2 模板语法详解
模板中通过特殊符号包裹变量和逻辑:
{{ 变量/表达式 }}
:输出变量或表达式结果(如{{ 1+1 }}
输出2
);{% 控制语句 %}
:用于循环(for
)、条件判断(if
)等(如{% for user in users %}
);{# 注释 #}
:模板内的注释,不会出现在生成的文件中。
1. for 循环:遍历列表 / 字典
作用:重复输出列表中的每个元素,类似for
循环语句。
示例 1:遍历用户列表
-
playbook 中定义变量:
vars:users:- tom- jack- Snoopy- lucy
-
模板文件(testfile.j2):
{% for user in users %} # 遍历users列表,每次取一个元素给user {{ user }} # 输出当前user {% endfor %} # 结束循环
-
生成的文件内容:
tom jack Snoopy lucy
示例 2:带索引的循环
-
模板文件(testfile.j2):
{% for user in users %} {{ loop.index }} - {{ user }} # loop.index是当前循环的序号(从1开始) {% endfor %}
-
生成的文件内容:
1 - tom 2 - jack 3 - Snoopy 4 - lucy
示例 3:生成主机列表文件
需求:在/etc/myhosts
中添加所有主机的 IP、完全主机名和主机名。
-
主机清单(
inventory
):[controllers] controller[dev] node1[test] node2[prod] node3 node4
-
playbook(
deploy_hosts.yml
):--- - name: deploy /etc/myhostshosts: all # 必须对所有主机执行,才能收集所有主机的信息tasks:- name: generate /etc/myhoststemplate:src: hosts.j2dest: /etc/myhostswhen: inventory_hostname in groups.dev # 只部署到dev组主机
-
模板文件(
hosts.j2
):127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6{% for server in groups.all %} # 遍历所有主机(groups.all是所有主机的列表) # hostvars[server]用于获取其他主机的信息(如IP、主机名) {{ hostvars[server].ansible_default_ipv4.address }} {{ hostvars[server].ansible_fqdn }} {{ hostvars[server].ansible_hostname }} {% endfor %}
2. if 条件判断:根据条件输出内容
作用:满足条件时才输出特定内容,类似if-else
语句。
示例 1:判断变量是否存在
{% if PORT is defined %} # 如果PORT变量已定义
bind-address=0.0.0.0:{{ PORT }} # 使用PORT变量
{% else %} # 否则
bind-address=0.0.0.0:3306 # 使用默认3306
{% endif %}
示例 2:循环中过滤元素
{% for num in [7,1,5,3,9] if num>3 %} # 只遍历大于3的数字
{{ num }}
{% endfor %}
生成的文件内容:
7
5
9
3. 表达式:支持运算和比较
比较运算
{{ 1 == 1 }} # 等于 → True
{{ 2 != 2 }} # 不等于 → False
{{ 2 > 1 }} # 大于 → True
{{ 2 <= 1 }} # 小于等于 → False
逻辑运算
{{ (2 > 1) or (1 > 2) }} # 或 → True
{{ (2 > 1) and (1 > 2) }} # 与 → False
{{ not true }} # 非 → False
算术运算
{{ 3 + 2 }} # 加 → 5
{{ 3 * 5 }} # 乘 → 15
{{ 2 **3 }} # 幂 → 8(2的3次方)
{{ 7 // 5 }} # 整除 → 1
{{ 17 % 5 }} # 取余 → 2
4. 过滤器:格式化输出结果
过滤器用于对变量或表达式的结果进行处理(如转换大小写、排序等),格式为{{ 变量 | 过滤器 }}
。
字符串处理
{{ 'hello' | upper }} # 转大写 → HELLO
{{ 'HELLO' | lower }} # 转小写 → hello
{{ 'hello' | capitalize }} # 首字母大写 → Hello
{{ ' hello ' | trim }} # 去除首尾空格 → hello
列表处理
{{ [3,1,7] | sort }} # 升序排序 → [1, 3, 7]
{{ [3,1,7] | max }} # 最大值 → 7
{{ [3,1,7] | sum }} # 求和 → 11
{{ [3,1,7] | join(',') }} # 用逗号连接 → 3,1,7
数字处理
{{ '123' | int }} # 转整数 → 123
{{ 'abc' | int(default=0) }} # 转换失败用默认值 → 0
{{ 12.5 | round }} # 四舍五入 → 13
{{ -5 | abs }} # 绝对值 → 5
ansible_managed 变量
用于在模板中添加 “此文件由 Ansible 管理” 的注释,避免手动修改。默认值在ansible.cfg
中定义:
[defaults]
ansible_managed = Ansible managed # 可自定义,如"Ansible managed: do not edit"
在模板中引用:
# {{ ansible_managed }} # 会替换为上述配置的值
如涉及版权问题请联系作者处理!!!!!!!!!!!