Ansible 使用 lineinfile 模块修改配置文件

需要用 Ansible 修改配置文件,其实就是在某个文件末尾添加几行内容。直观地想,直接用 shell 模块,echo 加 >> 就完事了。
但仔细一琢磨,很可能会引发一些意想不到的问题,比如:

  • 如果需要添加的配置已经存在,echo 仍会向配置文件底部添加同样的内容
  • 如果添加配置的任务重复执行多次,则配置文件中也会多次出现重复的内容。无法做到幂等
  • 如何做到,当对应的配置已经存在,则将该配置改为期望的值;当对应的配置不存在,不做任何操作(有就修改,没有就不动。好像可以用 sed)
  • 如何安全地移除指定的配置项

诸如此类。运维工作常常要关系到生产环境。任何无法预期的效果都可能产生严重的影响。而单纯使用 echo 和 >> 向配置文件中添加内容,具有很大的不确定性。
当然可以形成一个 Shell 脚本,对各种边界进行足够的检查和判定,但这会导致代码量变大,结构复杂难以标准化;同时也容易出现遗漏的情况。

实际上 Ansible 内置的 lineinfile 就是专门用来处理上述任务的模块。

比如针对如下内容的配置文件 test_conf.ini:

1
2
FIRST=true
SECOND=2

需要添加一行配置 THIRD=3。

可以运行如下内容的 playbook change_config.yml:

1
2
3
4
5
6
7
8
9
10
- name: change configuration
gather_facts: false
hosts: localhost

tasks:
- name: change content in test_conf.ini
lineinfile:
path: /home/starky/projects/ansible/practice/test_conf.ini
regexp: '^THIRD'
line: THIRD=3

其中 lineinfile 模块的 path 参数用于指定目标配置文件的路径;regexp 参数则用于指定对文件内容进行匹配时使用的正则表达式;最后的 line 参数表示希望在目标文件中出现的内容。

具体的步骤为:

  • 检查 line 对应的内容是否存在于 path 对应的目标文件中
  • 若已经存在。则目标文件符合要求,不对该文件做任何操作
  • 若不存在。通过 regexp 指定的正则表达式对目标文件进行匹配
  • 若 regexp 匹配到文本行,则将该行内容修改为 line 指定的内容
  • 若 regexp 未匹配到文本行,则将 line 对应的内容作为新的一行添加到目标文件末尾

运行效果:

1
2
3
4
5
6
7
8
9
$ ansible-playbook change_config.yml

PLAY [change configuration] *********************************************************************************************************

TASK [change content in test_conf.ini] **********************************************************************************************
changed: [localhost]

PLAY RECAP **************************************************************************************************************************
localhost : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

此时 test_conf.ini 配置文件的内容被修改为:

1
2
3
FIRST=true
SECOND=2
THIRD=3

若再次运行 ansible-playbook change_config.yml 命令:

1
2
3
4
5
6
7
8
9
$ ansible-playbook change_config.yml

PLAY [change configuration] *********************************************************************************************************

TASK [change content in test_conf.ini] **********************************************************************************************
ok: [localhost]

PLAY RECAP **************************************************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

可以看到修改配置文件的任务执行结果为 ok,而不同于上一次的 changed。这表示 lineinfile 模块对配置文件的内容进行了检查,发现需要添加的配置行已经存在,因此未做任何改动。符合幂等的原则。

假如将配置文件中的 THIRD=3 改为 THIRD=false,再次运行 playbook:

1
2
3
4
5
6
7
8
9
$ ansible-playbook change_config.yml

PLAY [change configuration] *********************************************************************************************************

TASK [change content in test_conf.ini] **********************************************************************************************
changed: [localhost]

PLAY RECAP **************************************************************************************************************************
localhost : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

正则表达式 ^THIRD 会匹配到配置文件的第三行 THIRD=false,再将该行内容替换为 THIRD=3。

最终仍可以得到我们想要的内容:

1
2
3
FIRST=true
SECOND=2
THIRD=3

对于 Ansible playbook 而言,我们只需要关注期望达到的状态,而不用纠结为了达到该状态需要执行哪些步骤。
如 lineinfile 模块,line 指定的内容即为我们期望目标文件达到的状态。即该文件最终一定会包含一行与 line 相同的文本。
不管该行内容是本就已经存在的,还是通过修改 regexp 匹配到的文本行得到的,还是直接在目标文件末尾新增的。而我们只需要定义 path、regexp、line 三个参数即可。

其他用法

backrefs

lineinfile 默认的行为是若 line 指定的内容未存在,regexp 正则表达式也没有任何匹配,就在文件末尾添加一行 line 指定的内容。
backrefs 参数可以修改此行为。当 backrefs 设定为 true 时,若 line 指定的内容不存在,正则表达式也没有匹配。则不做任何操作。

比如如下 playbook:

1
2
3
4
5
6
7
8
9
10
11
- name: change configuration
gather_facts: false
hosts: localhost

tasks:
- name: change content in test_conf.ini
lineinfile:
path: /home/starky/projects/ansible/practice/test_conf.ini
regexp: '^THIRD'
line: 'THIRD=3'
backrefs: true

当目标文件的内容如下:

1
2
FIRST=true
SECOND=2

playbook 实际不会对其做任何修改,不会在文件末尾添加 THIRD=3。只有当文件中存在如 THIRD=false 这类内容时,playbook 才会完成匹配并替换对应的行。

没有 backrefs 表示匹配就替换,不匹配就在文件末尾添加;有 backrefs 表示匹配就替换,不匹配就不动。

删除一行内容
1
2
3
4
5
6
7
8
9
10
- name: change configuration
gather_facts: false
hosts: localhost

tasks:
- name: change content in test_conf.ini
lineinfile:
path: /home/starky/projects/ansible/practice/test_conf.ini
regexp: '^THIRD'
state: absent
在匹配行前/后添加
1
2
3
4
5
6
7
8
9
10
- name: change configuration
gather_facts: false
hosts: localhost

tasks:
- name: change content in test_conf.ini
lineinfile:
path: /home/starky/projects/ansible/practice/test_conf.ini
insertbefore: '^FIRST'
line: 'ZERO=false'
1
2
3
4
5
6
7
8
9
10
- name: change configuration
gather_facts: false
hosts: localhost

tasks:
- name: change content in test_conf.ini
lineinfile:
path: /home/starky/projects/ansible/practice/test_conf.ini
insertafter: '^THIRD'
line: 'FOURTH=4'

需要注意以下两点:

  • 当 line 指定的内容已经存在于目标文件中时,不管其具体在什么位置,目标文件都不会做任何修改
  • 当 insertbefore 或 insertafter 指定的正则表达式没有任何匹配时,都会在文件末尾添加 line 指定的内容

官网示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# NOTE: Before 2.3, option 'dest', 'destfile' or 'name' was used instead of 'path'
- name: Ensure SELinux is set to enforcing mode
ansible.builtin.lineinfile:
path: /etc/selinux/config
regexp: '^SELINUX='
line: SELINUX=enforcing

- name: Make sure group wheel is not in the sudoers configuration
ansible.builtin.lineinfile:
path: /etc/sudoers
state: absent
regexp: '^%wheel'

- name: Replace a localhost entry with our own
ansible.builtin.lineinfile:
path: /etc/hosts
regexp: '^127\.0\.0\.1'
line: 127.0.0.1 localhost
owner: root
group: root
mode: '0644'

- name: Replace a localhost entry searching for a literal string to avoid escaping
lineinfile:
path: /etc/hosts
search_string: '127.0.0.1'
line: 127.0.0.1 localhost
owner: root
group: root
mode: '0644'

- name: Ensure the default Apache port is 8080
ansible.builtin.lineinfile:
path: /etc/httpd/conf/httpd.conf
regexp: '^Listen '
insertafter: '^#Listen '
line: Listen 8080

- name: Ensure php extension matches new pattern
lineinfile:
path: /etc/httpd/conf/httpd.conf
search_string: '<FilesMatch ".php[45]?$">'
insertafter: '^\t<Location \/>\n'
line: ' <FilesMatch ".php[34]?$">'

- name: Ensure we have our own comment added to /etc/services
ansible.builtin.lineinfile:
path: /etc/services
regexp: '^# port for http'
insertbefore: '^www.*80/tcp'
line: '# port for http by default'

- name: Add a line to a file if the file does not exist, without passing regexp
ansible.builtin.lineinfile:
path: /tmp/testfile
line: 192.168.1.99 foo.lab.net foo
create: yes

# NOTE: Yaml requires escaping backslashes in double quotes but not in single quotes
- name: Ensure the JBoss memory settings are exactly as needed
ansible.builtin.lineinfile:
path: /opt/jboss-as/bin/standalone.conf
regexp: '^(.*)Xms(\d+)m(.*)$'
line: '\1Xms${xms}m\3'
backrefs: yes

# NOTE: Fully quoted because of the ': ' on the line. See the Gotchas in the YAML docs.
- name: Validate the sudoers file before saving
ansible.builtin.lineinfile:
path: /etc/sudoers
state: present
regexp: '^%ADMIN ALL='
line: '%ADMIN ALL=(ALL) NOPASSWD: ALL'
validate: /usr/sbin/visudo -cf %s

# See https://docs.python.org/3/library/re.html for further details on syntax
- name: Use backrefs with alternative group syntax to avoid conflicts with variable values
ansible.builtin.lineinfile:
path: /tmp/config
regexp: ^(host=).*
line: \g<1>{{ hostname }}
backrefs: yes

参考资料

ansible.builtin.lineinfile – Manage lines in text files