08 模板管理、清单管理和并行方式
文章目录
1. 使用jinja2模块部署自定义文件
1.1 jinja2简介
ansible将jinja2模板系统用于模板文件。ansible还使用jinja2语法来引用playbook中的变量
变量和逻辑表达式置于标记或分隔符之间。例如,jinja2模板将{% EXPR %}用于表达式或逻辑(如循环),而{{ EXPR }}则用于向最终用户输出表达式或变量的结果。后一标记在呈现时将被替换为一个或多个值,对最终用户可见。使用{# COMMENT #}语法括起不应出现在最终文件中的注释。
[root@my ansible]# cat playbook/test.yml
---
- hosts: "*"
gather_facts: yes
tasks:
- name: my
template: //如果使用copy模块你会发现只会把内容copy过去,不会替换里面的事实
src: /etc/ansible/files/hosts.j2
dest: /etc/hosts
[root@my ansible]# cat files/hosts.j2
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
{# 这是一个注释 #}
{{ ansible_facts['default_ipv4']['address'] }} {{ ansible_facts['hostname'] }}
[root@my ansible]# ansible-playbook playbook/test.yml
[root@apache ~]# cat /etc/hosts
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.47.147 apache //看见了主机ip和主机名,如果使用copy模块会看见整个注释和模板
1.2构建inja2模板
jinja2模板由多个元素组成:数据、变量和表达式。在呈现jinja2模板时,这些变量和表达式被替换为对应的值。模板中使用的变量可以在playbook的vars部分中指定。可以将受管主机的事实用作模板中的变量。
使用ansible system_hostname -i inventory_file -m setup命令来获取与受管主机相关的事实。
1.3 部署jinja2模板
jinja2模板是功能强大的工具,可用于自定义要在受管主机上部署的配置文件,创建了使用于配置文件的jinja2模板后,它可以通过template模板部署到受管主机上,该模块支持将控制节点中的本地文件转移到受管主机
若要使用template模块,请使用下列语法。与src键关联的值指定来源jinja2模板,而与dest键关联的值指定要在目标主机上创建的文件。
template模块还允许指定已部署文件的所有者、组、权限和SELINUX上下文,就像file模块一样。它也可以取用validate选项运行任意命令(如visudo -c),在将文件复制到位之前检查该文件的语法是否正确。
1.4 管理模板文件
为避免系统管理员修改Ansible部署的文件,最好在模板顶部包含注释,以指示不应手动编辑该文件。
[root@my ansible]# cat files/hosts.j2
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
{# 这是一个注释 #}
#{{ ansible_managed }} //这里就是注释
{{ ansible_facts['default_ipv4']['address'] }} {{ ansible_facts['hostname'] }}
[root@my ansible]# ansible-playbook playbook/test.yml
[root@apache ~]# cat /etc/hosts
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
#Ansible managed //这里注释
192.168.47.147 apache
1.5 控制结构
用户可以在模板文件中使用jinja2控制结构,以减少重复输入,为play中的每个主机动态输入条目,或者有条件地将文本插入到文件中。
1.5.1 使用循环
inja2使用for语句来提供循环功能。在下例中,user变量替换为users变量中包含的所有值,一行一个值。
[root@my ansible]# cat vars/test.yml
info: //这是变量,变量的值
- hello 1
- hello 2
- hello 3
[root@my ansible]# cat playbook/test.yml
---
- hosts: "*"
gather_facts: yes
vars_files:
- /etc/ansible/vars/test.yml //指定变量的文件位置
tasks:
- name: my
template:
src: /etc/ansible/files/test.j2 //源j2模板
dest: /tmp/abc //目标主机创建的文件
[root@my ansible]# cat files/test.j2
{% for msg in info %}
{{ msg }} //这个值是一个变量
{% endfor %}
[root@my ansible]# ansible-playbook playbook/test.yml
[root@apache ~]# cat /tmp/abc
hello 1
hello 2
hello 3
//可以在变量的后面加东西
[root@my ansible]# cat files/test.j2
{% for msg in info %}
{{ msg }}
============
{% endfor %}
[root@my ansible]# ansible-playbook playbook/test.yml
[root@apache ~]# cat /tmp/abc
hello 1
============
hello 2
============
hello 3
============
以下示例模板使用for语句逐一运行msg变量中的所有值,将msg替换为各个值,但只取hello 2的值
[root@my ansible]# cat files/test.j2
{% for msg in info if msg == "hello 3" %}
{{ msg }}
============
{% endfor %}
[root@my ansible]# ansible-playbook playbook/test.yml
[root@apache ~]# cat /tmp/abc //只取了3
hello 3
============
值为除了hello 3的其他值
[root@my ansible]# cat files/test.j2 //取反
{% for msg in info if not msg == "hello 3" %} //也可以使用!,“!= ”的意思为不等于
{{ msg }}
============
{% endfor %}
[root@my ansible]# ansible-playbook playbook/test.yml
[root@apache ~]# cat /tmp/abc
hello 1
============
hello 2
============
//另一种格式
[root@my ansible]# cat files/test.j2
{% for msg in info %}
{% if msg != "hello 3" %}
{{ msg }}
{% endif %}
{% endfor %}
[root@my ansible]# ansible-playbook playbook/test.yml
[root@apache ~]# cat /tmp/abc
hello 1
hello 2
指定次数
[root@my ansible]# cat files/test.j2
{% for msg in info %}
{% if msg != "hello 3" %} //结果里面不会出现3,因为我们把它排除了
count {{ loop.index}} of info is {{ msg }}. //循环第几次对这个info变量循环的值是msg
{% endif %}
{% endfor %}
[root@my ansible]# ansible-playbook playbook/test.yml
[root@apache ~]# cat /tmp/abc
count 1 of info is hello 1. //出现次数
count 2 of info is hello 2. //第二次对info这个变量循环的是hello2
总结:
不需要用{{}}引用变量的场景:
debug模块里面的var参数
playbook中用when判断条件时
模块文件定义的时候{% 定义的时候%}
1.5.2 使用条件语
jinja2使用if语句来提供条件控制。如果满足某些条件,这允许用户在已部署的文件中放置一行。
[root@my ansible]# cat files/test.j2
{% if "" %}
abc
{% endif %}
[root@my ansible]# ansible-playbook playbook/test.yml
[root@apache ~]# cat /tmp/abc //条件不成立,abc不搞过来,没有东西,覆盖,所以为空
[root@apache ~]#
1.5.3 变量过滤器
jinja2提供了过滤器,更改模板表达式的输出格式(例如,输出到果JSON)。有适用于YAML和JSON等语言的过滤器。to_json过滤器使用JSON格式化表达式输出,to_yaml过滤器则使用YAML格式化表达式输出。
{{ output | to_json }}
{{ output | to_yaml }}
{{ output | to_nice_json }}
{{ output | to_nice_yaml }}
{{ output | from_json }}
{{ output | from_yaml }}
列子:
[root@my ansible]# cat vars/test.yml
info:
- name: tom
age: 20
- name: awm
age: 21
[root@my ansible]# cat files/test.j2
{% if " " %} //中间是空格
{{ info }}
{% endif %}
[root@apache ~]# cat /tmp/abc
[{'name': 'tom', 'age': 20}, {'name': 'awm', 'age': 21}]
[root@my ansible]# cat files/test.j2
{% if " " %}
{{ info | to_nice_json }}
{% endif %}
[root@my ansible]# ansible-playbook playbook/test.yml
[root@apache ~]# cat /tmp/abc
[
{
"age": 20,
"name": "tom"
},
{
"age": 21,
"name": "awm"
}
]
2 利用主机模式选择主机
2.1 引用清单文件
主机模式用于指定要作为play或临时命令的目标的主机。在最简单的形式中,清单中受管主机或主机组的名称就是指定该主机或主机组的主机模式。
在play中,hosts指定要针对其运行play的受管主机。对于临时命令,以命令行参数形式将主机模式提供给ansible命令。
本段中将通篇使用以下示例清单来演示主机模式。
[root@localhost ~]# cat myinventory
web.example.com
data.example.com
[lab]
labhost1.example.com
labhost2.example.com
[test]
test1.example.com
test2.example.com
[datacenter1]
labhost1.example.com
test1.example.com
[datacenter2]
labhost2.example.com
test2.example.com
[datacenter:children]
datacenter1
datacenter2
[new]
172.16.103.129
172.16.103.130
2.2 受管主机
最基本的主机模式是单一受管主机名称列在清单中。这将指定该主机是清单中ansible命令要执行操作的唯一主机。
通过IP地址引用受管主机存在一个问题,那就是难以记住play或临时命令所针对的主机使用了哪个IP地址。但是,如果没有可解析的主机名,我们可能必须按IP地址指定主机以进行连接。
第一种方式受管的IP:
[root@localhost ~]# vim inventory
---
- hosts: 192.168.47.147
第二种方式主机名:
[root@my ansible]# cat inventory
apache
[root@my ansible]# cat /etc/hosts //做映射后就能ping通
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.47.147 apache
[root@my ansible]# ping apache
PING apache (192.168.47.147) 56(84) bytes of data.
64 bytes from apache (192.168.47.147): icmp_seq=1 ttl=64 time=0.369 ms
64 bytes from apache (192.168.47.147): icmp_seq=2 ttl=64 time=0.423 ms
64 bytes from apache (192.168.47.147): icmp_seq=3 ttl=64 time=0.409 ms
2.3 使用组指定主机
当组名称用作主机模式时,它指定Ansible将对属于该组的成员的主机执行操作。
---
- hosts: lab
记住,有一个名为all的特别组,它匹配清单中的所有受管主机
---
- hosts: all
还有一个名为ungrouped的特别组,它包括清单中不属于任何其他组的所有受管主机:
---
- hosts: ungrouped
2.4 使用通配符匹配多个主机
若要达成与all主机模式相同的目标,另一种方法是使用*通配符,它将匹配任意字符串。如果主机模式只是带引号的星号,则清单中的所有主机都将匹配。
---
- hosts: '*'
也可使用*字符匹配包含特定子字符串的受管主机或组。
例如,以下通配符主机模式匹配以.example.com结尾的所有清单名称:
---
- hosts: '*.example.com'
以下示例使用通配符主机模式来匹配开头为192.168.2.的主机或主机组的名称:
---
- hosts: '192.168.2.*'
以下示例使用通配符主机模式来匹配开头为datacenter的主机或主机组的名称。
---
- hosts: 'datacenter*'
2.5 列表
可以通过逻辑列表来引用清单中的多个条目。主机模式的逗号分隔列表匹配符合任何这些主机模式的所有主机。
如果提供受管主机的逗号分隔列表,则所有这些受管主机都将是目标
---
- hosts: labhost1.example.com,test2.example.com,192.168.2.2
如果提供组的逗号分隔列表,则属于任何这些组的所有主机都将是目标:
---
- hosts: lab,datacenter1
也可以混合使用受管主机、主机组和通配符,如下所示:
---
- hosts: 'lab,data*,192.168.2.2'
注意
也可以用冒号(:)取代逗号。不过,逗号是首选的分隔符,特别是将IPv6地址用作受管主机名称时。
如果列表中的某一项以与符号(&)开头,则主机必须与该项匹配才能匹配主机模式。它的工作方式类似于逻辑AND。
例如,根据我们的示例清单,以下主机模式将匹配lab组中同时也属于datacenter1组的计算机:
---
- hosts: lab,&datacenter1
我们也可以通过主机模式&lab,datacenter1或datacenter,&lab指定datacenter1组中的计算机只有在同时也属于lab组时才匹配。
通过在主机模式的前面使用感叹号(!)表示从列表中排除匹配某一模式的主机。它的工作方式类似于逻辑NOT。
根据示例清单,以下示例匹配datacenter组中定义的所有主机,但test2.example.com除外:
---
- hosts: datacenter,!test2.example.com
也可以使用模式’!test2.example.com,datacenter’来获得相同的结果。
最后一个示例演示了使用匹配测试清单中的所有主机的主机模式,datacenter1组中的受管主机除外。
---
- hosts: all,!datacenter1
3. 管理动态清单
3.1 动态生成清单
前面我们用到的静态清单编写比较容易,对于管理小型基础架构而言也很方便。不过,如果要操作许多台计算机,或者在计算机更替非常快的环境中工作,可能难以让静态清单文件保持最新状态。
大多数大型IT环境中没有系统来跟踪可用的主机以及它们的组织方式。例如,可能有外部目录服务通过Zabbix等监控系统维护,或者位于FreeIPA或Active Directory服务器上。Cobbler等安装服务器或红帽卫星等管理服务可能跟踪部署的裸机系统。类似地,Amazon Web ServicesEC2或OpenStack部署等云服务,或者基于Vmware或红帽虚拟化的虚拟机基础架构可能是有关那些更替的实例和虚拟机的信息来源。
Ansible支持动态清单脚本,这些脚本在每当Ansible执行时从这些类型的来源检索当前的信息,使清单能够实时得到更新。这些脚本是可以执行的程序,能够从一些外部来源收集信息,并以JSON格式输出清单。
动态清单脚本的使用方式与静态清单文本文件一样。清单的位置可以直接在当前的ansible.cfg文件中指定,或者通过-i选项指定。如果清单文件可以执行,则它将被视为动态清单程序,Ansible会尝试运行它来生成清单。如果文件不可执行,则它将被视为静态清单。
清单位置可以在ansible.cfg配置文件中通过inventory参数进行配置。默认情况下,它被配置为/etc/ansible/hosts。
3.2 开源社区脚本
开源社区向Ansible项目贡献了大量现有的动态清单脚本。它们没有包含在ansible软件包中。这些脚本可从Ansible GigHub网站(https://github.com/ansible/ansible/tree/devel/examples)获取。
3.3 管理多个清单
Ansible支持在同一运行中使用多个清单。如果清单的位置是一个目录(不论是由-i选项设置的、是inventory参数的值,还是以某种其他方式设置的),将组合该目录中包含的所有清单文件(不论是静态还是动态)来确定清单。该目录中的可执行文件将用于检索动态清单,其他文件则被用作静态清单。
清单文件不应依赖于其他清单文件或脚本来解析。例如,如果静态清单文件指定某一个组应当是另一个组的子级,则它也需要具有该组的占位符条目,即使该组的所有成员都来自动态清单。
[root@my ansible]# rm -f inventory
[root@my ansible]# ls
! ansible.cfg files group_vars hosts playbook roles tasks vars
[root@my ansible]# ansible all -m ping
[WARNING]: Unable to parse /etc/ansible/inventory as an inventory source
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that
the implicit localhost does not match 'all'
[root@my ansible]# mkdir inventory
[root@my ansible]# ls
! files hosts playbook tasks
ansible.cfg group_vars inventory roles vars
[root@my ansible]# echo "apache" > inventory/apache //一个主机加入不同的项目
[root@my ansible]# echo "apache" > inventory/qq
[root@my ansible]# tree inventory/
inventory/
├── apache
└── qq
0 directories, 2 files
[root@my ansible]# ansible all -m ping
The authenticity of host 'apache (192.168.47.147)' can't be established.
ECDSA key fingerprint is SHA256:vM+6iTTQOdnifV0kzdrl75KMCtk1lpt8fsWbMz+QYk8.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
apache | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": false,
"ping": "pong"
}
[root@my ansible]# echo "192.168.47.147" > inventory/qq
[root@my ansible]# tree inventory/
inventory/
├── apache
└── qq
0 directories, 2 files
[root@my ansible]# cat inventory/qq
192.168.47.147
[root@my ansible]# cat inventory/apache
apache
[root@my ansible]# ansible all -m ping
apache | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": false,
"ping": "pong"
}
192.168.47.147 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": false,
"ping": "pong"
}
//争对qq这个项目ping
[root@my ansible]# ansible all -i inventory/qq -m ping
192.168.47.147 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": false,
"ping": "pong"
}
4. 配置并行
4.1 使用分叉在ansible种配置并行
当Ansible处理playbook时,会按顺序运行每个play。确定play的主机列表之后,Ansible将按顺序运行每个任务。通常,所有主机必须在任何主机在play中启动下一个任务之前成功完成任务。
理论上,Ansible可以同时连接到play中的所有主机以执行每项任务。这非常适用于小型主机列表。但如果该play以数百台主机为目标,则可能会给控制节点带来沉重负担。
Ansible所进行的最大同时连接数由Ansible配置文件中的forks参数控制。默认情况下设为5,这可通过以下方式之一来验证。
[root@localhost ~]# grep forks /etc/ansible/ansible.cfg
#forks = 5
[root@localhost ~]# ansible-config dump|grep -i forks
DEFAULT_FORKS(default) = 5
[root@localhost ~]# ansible-config list|grep -i forks
DEFAULT_FORKS:
description: Maximum number of forks Ansible will use to execute tasks on target
- {name: ANSIBLE_FORKS}
- {key: forks, section: defaults}
name: Number of task forks
例如,假设Ansible控制节点配置了5个forks的默认值,并且play具有10个受管主机。Ansible将在前5个受管主机上执行play中的第一个任务,然后在其他5个受管主机上对第一个任务执行第二轮。在所有受管主机上执行第一个任务后,Ansible将继续一次在5受管主机的组中的所有受管主机上执行下一个任务。Ansible将依次对每个任务执行此操作,直到play结束。
forks的默认值设置得非常保守。如果你的控制节点正在管理Linux主机,则大多数任务将在受管主机上运行,并且控制节点的负载较少。在这种情况下,通常可以将forks的值设置得更高,可能接近100,然后性能就会提高。
如果playbook在控制节点上运行很多代码,则应明智地提高forks限值。如果使用Ansible管理网络路由器和交换机,则大多数模块在控制节点上运行而不是在网络设备上运行。由于这会增加控制节点上的负载,因此其支持forks数量增加的能力将显著低于仅管理Linux主机的控制节点。
可以从命令行覆盖Ansible配置文件中forks的默认设置。ansible和ansible-playbook命令均提供-f或–forks选项以指定要使用的forks数量。
4.2 管理滚动更新
通常,当Ansible运行play时,它会确保所有受管主机在启动任何主机进行下一个任务之前已完成每个任务。在所有受管主机完成所有任务后,将运行任何通知的处理程序。
但是,在所有主机上运行所有任务可能会导致意外行为。例如,如果play更新负载均衡Web服务器集群,则可能需要在进行更新时让每个Web服务器停止服务。如果所有服务器都在同一个play中更新,则它们可能全部同时停止服务。
避免此问题的一种方法是使用serial关键字,通过play批量运行主机。在下一批次启动之前,每批主机将在整个play中运行。
在下面的示例中,Ansible一次在两个受管主机上执行play,直至所有受管主机都已更新。Ansible首先在前两个受管主机上执行play中的任务。如果这两个主机中的任何一个或两个都通知了处理程序,则Ansible将根据这两个主机的需要运行处理程序。在这两个受管主机上执行完play时,Ansible会在接下来的两个受管主机上重复该过程。Ansible继续以这种方式运行play,直到所有受管主机都已更新。
---
- name: Rolling update
hosts: webservers
serial: 2
tasks:
- name: latest apache httpd package is installed
yum:
name: httpd
state: latest
notify: restart apache
handlers:
- name: restart apache
service:
name: httpd
假设上一示例中的webservers组包含5个Web服务器,它们位于负载均衡器后面。将serial参数设置为2后,play一次将运行两台Web服务器。因此,5台Web服务器中的大多数服务器将始终可用。
相反,如果不使用serial关键字,将同时在5台Web服务器上执行play和生成的处理程序。这可能会导致服务中断,因为Web服务将在所有Web服务器上同时重新启动。
重要 :出于某些目的,每批主机算作在主机子集上运行的完整play。这意味着,如果整个批处理失败,play就会失败,这将导致整个playbook运行失败。
在设置了serial: 2的上一个场景中,如果出现问题并且处理的前2个主机的play失败,则playbook将中止,其余3个主机将不会通过play运行。这是一个有用的功能,因为只有一部分服务器会不可用,使服务降级而不是中断。
serial关键字也可以指定为百分比。此百分比应用于play中的主机总数,以确定滚动更新批处理大小。无论百分比为何,每一工序的主机数始终为1或以上。