Ansible学习笔记

目录

1、Ansible搭建(基于CentOS 7.9)

1.1、在控制节点和被控节点获取epel源

1.2、安装Ansible

2、理论

3、基础配置

3.1、Ansible发送指令的原理

3.2、Ansible配置文件

3.3、配置文件参数说明

3.4、编写主机清单

3.5、配置ssh免密及提权

3.6、Ansible操作对象

3.7、Ad-hoc指令

4、常用模块

4.1、shell模块

4.2、command模块

4.3、raw模块:

4.4、script模块:

4.5、authorized_keys模块

4.6、file模块

4.7、Copy模块:

4.8、group模块

4.9、user模块

4.10、yum模块

4.11、apt模块

4.12、service模块

4.13、systemd模块

4.14、cron模块

4.15、synchronize模块

4.16、filesystem模块

4.17、mount模块

4.18、get_url 模块

4.19、unarchive模块

4.20、assemble模块:

5、Playbook

5.1、Ansible Playbook简介

5.2、Playbook基本语法

5.3、Playbook简单示例

5.4、ansible-playbook常用选项

5.5、Multiple Plays

6. Ansible Playbook的结构及handler用法

6.1、playbook的结构说明

6.2、Target section

6.3、Playbook中的远程用户

6.4、Playbook中的hosts

6.5、Task section

6.6、Handler section

6.7、补充:handler的另外一种定义方(一对多的方式)

7. Ansible变量之自定义变量

简单说明

7.1 在Inventory中定义变量

7.1.1、定义主机变量

7.1.2、定义主机组变量

7.2.、在Playbook中定义变量

7.2.1、变量的定义方式

7.2.2、使用与调试变量

7.3、定义变量:

7.3.1、第一种方式:

7.3.2、第二章方式:

7.4、变量取值:

7.5、关于变量取值的思路:

7.6、变量优先级:(从低到高)

8. Ansible变量之fact

fact简介

8.1、自定义fact

8.1.1、手动设置fact

8.1.2、使用set_fact模块定义新的变量

8.2、手动采集fact

8.3、启用fact缓存

8.3.1、Json文件fact缓存后端

8.3.2、Redis fact缓存后端

8.3.3. Memcached fact缓存后端

8.4、关闭fact

9. Ansible魔法变量及变量优先级

魔法变量

9.1、hostvars

9.2、inventory_hostname(获取主机清单中定义的主机名)

9.3、group_names(任务在哪个主机上运行,则获取该主机所在的组)

9.4、groups(获取主机清单中定义的所有组)

9.5、play_hosts

9.6、inventory_dir

9.7、inventory_file

9.8、变量优先级

10. 使用lookup生成变量

简单说明

10.1、file

10.2、pipe

10.3、env

10.4、url

10.5、template

10.6、csvfile

10.7、redis_kv

10.8、etcd

10.9、password

10.10、dnstxt

10.11、补充:

11. Ansible Playbook条件语句

简介

11.1、when关键字

11.1.1、when基本使用

11.1.2、比较运算符

11.1.3、逻辑运算符

11.2、条件判断与tests

11.2.1、判断变量

11.2.2、判断执行结果

11.2.3、判断路径

11.2.4、判断字符串

11.2.5、判断整除

11.2.6、其他tests

11.3、条件判断与block

11.3.1、block

11.3.2、rescue

11.3.3、always

11.4、条件判断与错误处理

11.4.1、fail模块

11.4.2、failed_when

11.4.3、changed_when

11.5、在循环语句中使用条件语句

12. Ansible Playbook with_X循环语句

循环语句

简介

12.1、with_items

12.1.1、场景一: 循环打印inventory中所有未分组的主机

12.1.2、场景二: 直接在with_items中定义被循环的列表

12.1.3、场景三: 在with_items中定义更复杂的列表

12.2、with_list

12.3、with_flattened

12.4、with_together

12.5、with_nested

12.6、with_indexed_items

12.7、with_sequence

12.9、with_dict

12.10、with_subelement

12.11、with_file

12.12、with_fileglob

12.13、with_lines

12.14、do-Until循环

13. Ansible Playbook loop循环语句

loop关键字说明

13.1、loop_control

13.2、with_list

13.3、with_flattened

13.4、with_items

13.5、with_indexed_items

13.6、with_together

13.7、with_nested/with_cartesian

13.8、with_sequence

13.9、with_random_choice

13.10、with_dict

13.11、with_subelements

13.12、使用zip_longest过滤器将两个列表中的元素对齐合并

13.13、在循环语句中注册变量

14. Ansible文件管理模块及Jinja2过滤器

14.1、常用文件管理模块

14.1.1、file

14.1.2、synchronize

14.1.3、copy

14.1.4、fetch

14.1.5、lineinfile

14.1.6、stat

14.1.7、blockinfile

14.2、Jinja2模板管理

Jinja2简介

14.2.1、在playbook中使用jinja2

14.2.2、Jinja2条件语句

14.2.3、Jinja2循环语句

14.2.3、Jinja2过滤器

15. Ansible Playbook高级用法

15.1、任务委托

15.2、本地执行

15.3、任务暂停

15.4、滚动执行

15.5、只执行一次

15.6、设置环境变量

15.7、交互式提示

16. Ansible Playbook之tags

简介

16.1、为task打tag

16.2、使用tag

16.2.1、执行指定tag的task

16.2.2、排除指定tag的task

16.2.3、查看playbook中的所有tag

16.3、打tag的几种方式

16.3.1、为一个任务指定一个标签

16.3.2、为一个任务指定多个标签

16.3.3、为一个play指定一组标签

17. Ansible Playbook Include

说明

17.1、include

17.1.1、tasks include

17.1.2、handlers include

17.1.3、playbook include

17.2、include_tasks

17.2.1、基本使用

17.2.2、在include_tasks中使用tags

17.3、import_tasks

17.4、在handlers中使用include_tasks及import_tasks

17.5、import_playbook

18. Ansible Playbook Roles

18.1、角色(roles)

18.1.1、role的基本构成

18.1.2、在playbook中使用roles

18.1.3、pre_tasks和post_tasks

18.1.4、role的依赖

18.2、Ansible Galaxy

19、Ansible Vault配置加密

简介

19.1、Ansible-vault常用操作

19.2、Ansible-vault配置示例

20、动态Inventory管理

20.1、动态主机管理模块

20.1.1、add_host

20.1.2、group_by

20.2、动态inventory管理

20.2.1动态inventory简介

20.2.2、动态inventory脚本规约

20.2.3、动态inventory脚本示例

21. Ansible性能调优

简介

21.1、开启ansible性能监测

21.2、任务优化

21.3、优化ssh连接

21.4、pipelining

21.5、并发执行

22. Ansible调试

22.1、运行前检查

22.2、打印详细输出信息

22.3、使用debug模块

22.4、使用assert模块

22.5、限制特定的task运行

22.5.1、指定任务执行

22.5.2、分步执行

22.5.3、tags

23. Ansible lineinfile模块

简介

23.1、修改匹配行

23.2、在匹配行前或后添加内容

23.3、在匹配行前添加

23.4、在匹配行后添加

23.5、修改文件内容及权限

23.6、删除一行内容

23.7、如果有匹配的行则修改该行,如果不匹配则添加

23.8、参数backrefs,backup说明

23.9、使用validate验证文件是否正确修改


1、Ansible搭建(基于CentOS 7.9)

1.1、在控制节点和被控节点获取epel源

[root@controller ~]# wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo

1.2、安装Ansible

[root@controller ~]# yum clean all  #清除缓存
[root@controller ~]# yum makecache    #建立缓存
[root@controller ~]# yum install -y ansible  #开始安装
[root@controller ~]# ansibel --version  #查看Ansible的版本及信息

2、理论

控制端使用ssh服务连接被控端

控制端默认使用root用户控制被控端,工作目录在用户家目录

3、基础配置

3.1、Ansible发送指令的原理

Ansible底层为Python编写,因此Ansible的各种指令在执行时都会转换成Python脚本

1、下发Ansible命令时,先转换成Python脚本 然后临时保存到主控端的~/.ansible/tmp目录

2、执行Ansible命令时,使用ssh服务将主控端~/.ansible/tmp目录下的Python脚本发送到被控端,被控端执行脚本

3、执行完成后,将~/.ansible/tmp下的Python脚本删除

3.2、Ansible配置文件

优先级:

        最高:定义变量为ANSIBLE_CONFIG = /tmp/ansible.cfg

        第二:当前目录下的ansible.cfg(最优)

        第三:当前用户家目录下的 ~/.ansible.cfg

        最后:默认配置文件/etc/ansible/ansible.cfg

[root@controller ~]# vim /etc/ansible/ansible.cfg    #默认配置文件(一般不用)

自己创建一个目录,方便以后打包分享给别人
[root@controller ~]# mkdir ansible
[root@controller ~]# cp /etc/ansible/ansible.cfg ansible/    #将ansible默认配置文件拷贝过来
[root@controller ansible]# vi ansible.cfg    #修改主机清单文件所在路径为当前目录下的inventory
    inventory      = ./inventory    #以配置文件所在目录的相对路经
[root@controller ansible]# cp /etc/ansible/hosts inventory    将默认主机清单文件拷贝到当前目录并重命名为inventory(如果之前没有编写主机清单,则不用拷贝,直接新建一个名为inventory的文件进行编写即可)

3.3、配置文件参数说明

[defaults]

inventory      = ./inventory    #主机清单文件所在位置
#library        = /usr/share/my_modules/    
#module_utils   = /usr/share/my_module_utils/    
#remote_tmp     = ~/.ansible/tmp    #被控端临时存储目录
#local_tmp      = ~/.ansible/tmp    #主控端临时存储目录
#plugin_filters_cfg = /etc/ansible/plugin_filters.yml
#forks          = 5    #并发数(一次性连接的被控端)
#poll_interval  = 15    #探测任务完成情况的时间
#sudo_user      = root
#ask_sudo_pass = True    
#ask_pass      = True    #相当于命令行中的-k,密码验证,默认是false
#transport      = smart
#remote_port    = 22    #被控端ssh服务的端口号
#module_lang    = C
#module_set_locale = False
remote_user = ansible     #远程主机的连接用户
[privilege_escalation]
become=True    #是否提权
become_method=sudo    #使用什么提权
become_user=root    #提权到什么用户
become_ask_pass=False    #提权需不需要密码
module_name = command    #如果不指定模块,默认使用command模块
host_key_checking = False    #是否进行ssh密钥验证(如果为False则自动接收被控端的公钥)

3.4、编写主机清单

在主机清单中写入被控端 IP 地址或本地hosts表中解析的主机名(可直接写IP地址)

配置本地hosts文件解析主机名
[root@controller ~]# vim /etc/hosts 
    127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
    ::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
    192.168.91.11 servera
    192.168.91.12 serverb
    192.168.91.13 serverc
    192.168.91.14 serverd
    192.168.91.15 servere
    192.168.91.16 serverf

编写Ansible主机清单文件 
[root@controller ~]# vim /etc/ansible/hosts 
    serverf
    [a]
    servera
    serverb
    [b]
    serverc
    [c]    
    serverd
    servere
    [d:children]    #d组包含b和c组(带有children标签的组,只能写组,不能写主机)
    b
    c    
[root@controller ansible]# ansible-inventory -i file --list -y --output file.yaml    #将ini格式转换成yaml格式的主机清单
[root@controller ansible]# ansible all -i file --list-hosts    
    解释:
        -i:指定主机清单文件,当前为file                                                                                                                                                                                                                                                                                                        

3.5、配置ssh免密及提权

被控端创建普通用户
[root@controller ansible]# ansible all -m user -a 'name=ansible state=present' -k
[root@controller ansible]# ansible all -m shell -a 'echo 1 | passwd --stdin ansible' -u root -k

生成公私钥并将公钥发送到各个被控端,默认存放在~/.ssh目录
[root@controller ansible]# ssh-keygen   
[root@controller ansible]# ssh-copy-id ansible@servera  
[root@controller ansible]# ssh-copy-id ansible@serverb
[root@controller ansible]# ssh-copy-id ansible@serverc
[root@controller ansible]# vim ansible.cfg 
    remote_user = ansible
        解释:
              remote_user:指定被控端使用的用户,此处指定为ansible用户    
          
免密制作已完成,接下来配置提权         
[root@controller ansible]# ansible all -m shell -a 'echo "ansible ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/ansible' -u root -k
[root@controller ansible]# vim ansible.cfg
    [privilege_escalation]
    become=True    #是否提权
    become_method=sudo    #使用什么提权
    become_user=root    #提权到什么用户
    become_ask_pass=False    #提权需不需要密码

验证免密及提权是否配置成功:                                                                                                                                  
[root@controller ansible]# ansible all -m shell -a 'ls /root'

3.6、Ansible操作对象

操作对象:(all和ungrouped属于ansible内置组)
[root@controller ~]# ansible all -m ping    #对所有主机进行操作
[root@controller ansible]# ansible ungrouped -m ping    #对不属于任何组的主机进行操作
[root@controller ~]# ansible servera -m ping    #对某台主机进行操作
[root@controller ~]# ansible servera,serverb -m ping    #对多台主机进行操作
[root@controller ~]# ansible a -m ping    #对a组进行操作
[root@controller ~]# ansible 'server*' -m ping

3.7、Ad-hoc指令

一个ad-hoc命令的执行,需要按以下格式进行执行:(不加模块名的话,默认使用command模块)

        语法格式:

ansible 主机或组 -m 模块名 -a '模块参数'  ansible参数
查看主机清单中的主机
[root@controller ~]# ansible all --list-hosts   
    解释:
        all:表示主机清单中所有的主机
[root@controller ~]# ansible all -m ping  
[root@controller ~]# ansible servera -m shell -a 'ls' -k -u root  
    解释:
        -m:指定模块
        -a:指定模块参数(需要执行的命令)
        -k:使用密码连接(默认使用ssh免密连接)
        -u:指定被控端操作的用户
[root@controller ansible]# ansible all -m user -a 'name=user1 state=present' 
    解释:
        user:用户模块
        name=user1:指定创建用户名为user1
        state=present:执行状态为创建
[root@controller ansible]# ansible all -m shell -a pwd -k   #查看当前被控端使用的用户,pwd最准确

查看模块帮助
[root@controller ansible]# ansible-doc -l    #查看ansible当前支持的所有模块
[root@controller ansible]# ansible-doc -l user    #查看user模块的帮助

4、常用模块

4.1、shell模块

shell模块包含如下选项:

  • creates:指定一个文件名,如果该文件存在,则该命令不执行
  • removes:指定一个文件名,如果该文件不存在,则该命令执行
  • free_form:要执行的linux指令
  • chdir:在执行指令之前,先切换到该指定的目录,默认工作目录是远程主机的家目录。
  • cmd:后面接shell命令,可
[root@controller ansible]# ansible all -m shell -a 'creates=/tmp/file1 pwd'    #如果被控端的/tmp/file1存在,则不运行pwd命令
[root@controller ansible]# ansible all -m shell -a 'removes=/tmp/file1 pwd'    #如果被控端的/tmp/file1不存在,则运行pwd命令
[root@controller ansible]# ansible all -m shell -a 'chdir=/root ls'    #修改工作目录

4.2、command模块

  • 该模块通过-a跟上要执行的命令可以直接执行,不过命令里如果有带有如下字符部分则执行不成功 “ "", "|", "&" ;
[root@controller ~]# ansible all -a 'ls /root'

4.3、raw模块:

  • 用法和shell模块一样,也可以执行任意命令,就像在本机执行一样;和command、shell模块不同的是其没有chdir、creates、removes参数

4.4、script模块:

  • 将管理端的shell 在被管理主机上执行,其原理是先将shell 复制到远程主机,再在远程主机上执行,原理类似于raw模块。
#要执行的脚本文件script.sh内容如下: 

#/bin/bash
ifconfig
df -hT

# 执行ansible指令:
ansible 10.212.52.252 -m script -a 'script.sh' 

4.5、authorized_keys模块

用于向被控端推送公钥,通常用于在ansible第一次连接被控端时向其推送ansible主控端的管理公钥。

常用选项:

  • user: 指定将公钥推送给被控端的哪个用户
  • key:指定被推送的公钥的内容
  • path:默认情况下,会将公钥推送至被控端用户家目录的.ssh/authorized_keys文件中,可通过该配置项自定义该路径
  • state:是推送还是删除,present|absent
[root@controller ansible]# ansible-galaxy collection install ansible.posix    #安装相关模块,需要连接网络
[root@controller ansible]# ansible all -m authorized_key -a 'user=root key="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8IhPtFKeUKjUX/Q4g+UZittMD/9swAYd9dgv0PHhbro4MlSOVbHAldkg6+0kt04qcjUMLm6I2fM4ozbcN+/l0gpGYPJ/qPRS5ghHYg58iYSie+plmrmipmVG724WNpZaHd0aL8mVN0EF3ngqFgLEegvPbK0P8ZynHFyYJFT8vhqf/mRwlxqIoGuImyztpqmwXnTnEH4zvX46BtiQcCItFJIlyCFaEiCDeuKht4FBOfDVMiW9qkZ98811JQUJD+ndW3PW3T9tirXaic2iquq/xjWiUbW6jftgK/ExIt2syOgOXjvlrmCknbMfJcxjQfX6Yk0sEZWS4POiWep+z9CH9 root@controller" state=present'

4.6、file模块

file模块主要用于远程主机上的文件操作,file模块包含如下选项:

  • force:需要在两种情况下强制创建软链接,一种是源文件不存在但之后会建立的情况下;另一种是目标软链接已存在,需要先取消之前的软链,然后创建新的软链,有两个选项:yes|no
  • group:定义文件/目录的属组
  • mode:定义文件/目录的权限
  • owner:定义文件/目录的属主
  • path:必选项,定义文件/目录的路径
  • src:要被链接的源文件的路径,只应用于state=link的情况
  • dest:被链接到的路径,只应用于state=link的情况
  • state:
    • directory:如果目录不存在,创建目录
    • file:即使文件不存在,也不会被创建
    • link:创建软链接
    • hard:创建硬链接
    • touch:如果文件不存在,则会创建一个新的文件,如果文件或目录已存在,则更新其最后修改时间
    • absent:删除目录、文件或者取消链接文件
[root@controller ansible]# ansible all -m file -a "path=/A/file1 state=touch"    #创建
[root@controller ansible]# ansible all -m file -a "path=/A/dir1 state=directory"    #创建目录
[root@controller ansible]# ansible all -m file -a "src=/etc/passwd dest=/A/userinfo state=link"    #创建软链接
[root@controller ansible]# ansible all -m file -a "path=/A/dir1 state=absent"    #删除文件夹
[root@controller ansible]# ansible all -m file -a "path=/A/file1 owner=ansible group=ansible mode=700 state=file"    #修改文件权限

4.7、Copy模块:

复制文件到远程主机,copy模块包含如下选项:

  • backup:在覆盖之前将原文件备份,备份文件包含时间信息。有两个选项:yes|no
  • content:用于替代"src",可以直接设定指定文件的值
  • dest:必选项。要将源文件复制到的远程主机的绝对路径,如果源文件是一个目录,那么该路径也必须是个目录
  • force:如果目标主机包含该文件,但内容不同,如果设置为yes,则强制覆盖,如果为no,则只有当目标主机的目标位置不存在该文件时,才复制。默认为yes
  • others:所有的file模块里的相关文件属性选项都可以在这里使用
  • src:要复制到远程主机的文件在本地的地址,可以是绝对路径,也可以是相对路径。如果路径是一个目录,它将递归复制。在这种情况下,如果路径使用"/"来结尾,则只复制目录里的内容,如果没有使用"/"来结尾,则包含目录在内的整个内容全部复制,类似于rsync。
  • remote_src:默认是no,即复制主控端的文件到被控端,yes表示被控端的文件复制到被控端
  • validate:测试文件的语法,如果测试不通过,则不执行,测试通过则执行
[root@controller ansible]# ansible all -m copy -a 'src=a dest=/tmp/file2 group=ansible mode=700'    #拷贝的同时,修改权限
[root@controller ansible]# ansible all -m copy -a 'content="zzd" dest=/tmp/file3'    #写入内容到文件(不会追加到文件中,而是重定向的效果)
[root@controller ansible]# ansible all -m copy -a 'content="zzd" dest=/tmp/file4 backup=yes'    #备份一份原文件
[root@controller ansible]# ansible all -m copy -a 'content="zzd" dest=/tmp/file3 force=no'    #如果文件存在,则不覆盖 
[root@controller ansible]# ansible all -m copy -a 'src=a dest=/tmp/file2 remote_src=yes'    #拷贝控制端的文件到控制端
[root@controller ansible]# ansible all -m copy -a "src=/mine/sudoers dest=/etc/sudoers validate='visudo -cf %s'"    #检查文件的合法性

4.8、group模块

goup模块请求的是groupadd, groupdel, groupmod 三个指令。

  • gid:指定组id
  • name:指定组名
  • state:创建还是删除组,选项:present|absent
  • system:是否将该组创建为系统组,默认为no
[root@controller ansible]# ansible test -m group -a 'name=test gid=1001 state=present system=yes'

4.9、user模块

user模块是请求的是useradd, userdel, usermod三个指令

  • home:指定用户的家目录,需要与createhome配合使用
  • groups:指定用户的附加组
  • group:指定用户属组
  • uid:指定用的uid
  • password:指定用户的密码
  • name:指定用户名
  • createhome:是否创建家目录 yes|no
  • system:是否为系统用户
  • comment:定义用户描述信息
  • remove:当state=absent时,remove=yes则表示连同家目录一起删除,等价于userdel -r
  • state:是创建还是删除
  • shell:指定用户的shell环境
[root@controller ansible]# ansible all -m user -a ' name=user1 uid=1002 state=present'    #创建user1用户,并设置uid为1002,
[root@controller ansible]# ansible all -m user -a ' name=user1 uid=1003 state=present'    #修改user1用户的uid为1003
[root@controller ansible]# ansible all -m user -a 'name=user1 state=absent remove=yes'    #删除user1用户,并将家一起删除
[root@controller ansible]# ansible all -m user -a "name=user2 state=present password={{ 'mypassword' |  password_hash('sha512','mysecretsalt') }}"    #创建user2用户并设置密码为mypassword

4.10、yum模块

使用yum包管理器来管理软件包,其选项有:

  • name:要进行操作的软件包的名字,也可以传递一个url或者一个本地的rpm包的路径
  • state:状态(present安装,absent卸载,latest最新版本)
[root@controller ansible]# ansible all -m yum -a 'name=* state=latest'    #更新所有软件包
[root@controller ansible]# ansible all -m yum -a 'name=httpd state=latest'    #安装httpd包    
[root@controller ansible]# ansible all -m yum -a 'name="@Development tools" state=present'    #安装Development tools包组
[root@controller ansible]# ansible all -m yum -a 'name=http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm state=present'    #下载并安装网站上的包

4.11、apt模块

使用apt包管理器来管理软件包,其选项有:

  • name:要进行操作的软件包的名字,也可以传递一个url或者一个本地的rpm包的路径
  • state:状态(present安装,absent卸载,latest最新版本)
[root@controller ansible]# ansible all -m apt -a 'name=httpd state=latest'

4.12、service模块

用于管理服务

该模块包含如下选项:

  • arguments:给命令行提供一些选项
  • enabled:是否开机启动 yes|no
  • name:必选项,服务名称
  • pattern:定义一个模式,如果通过status指令来查看服务的状态时,没有响应,就会通过ps指令在进程中根据该模式进行查找,如果匹配到,则认为该服务依然在运行
  • runlevel:运行级别
  • sleep:如果执行了restarted,在则stop和start之间沉睡几秒钟
  • state:对当前服务执行启动,停止、重启、重新加载等操作(started,stopped,restarted,reloaded)
  • daemon_reload:针对使用systemd的系统,重新加载systemd配置,yes/no
[root@controller ansible]# ansible all -m service -a "name=httpd state=started enabled=yes"
[root@controller ansible]# asnible all -m service -a "name=foo pattern=/usr/bin/foo state=started"
[root@controller ansible]# ansible all -m service -a "name=network state=restarted args=eth0"

4.13、systemd模块

  • daemon_reload:当服务配置文件发生变更时重载服务
  • name:指定服务名称
  • enabled:是否设置开机自启
  • state:管理服务状态,reloaded|restarted|started|stopped
[root@controller ansible]# ansible all -m systemd -a "name=httpd state=started enabled=yes daemon_reload=yes"

4.14、cron模块

用于管理计划任务

包含如下选项:

  • backup:对远程主机上的原任务计划内容修改之前做备份
  • cron_file:如果指定该选项,则用该文件替换远程主机上的cron.d目录下的用户的任务计划
  • day:日(1-31,,/2,……)
  • hour:小时(0-23,,/2,……)
  • minute:分钟(0-59,,/2,……)
  • month:月(1-12,,/2,……)
  • weekday:周(0-7,*,……)
  • job:要执行的任务,依赖于state=present
  • name:该任务的描述
  • special_time:指定什么时候执行,参数:reboot,yearly,annually,monthly,weekly,daily,hourly
  • state:确认该任务计划是创建还是删除,present创建,absent删除
  • user:以哪个用户的身份执行
[root@controller ansible]# ansible all -m cron -a 'name="a job for reboot" special_time=reboot job="/some/job.sh"'
[root@controller ansible]# ansible all -m cron -a 'name="yum autoupdate" weekday="2" minute=0 hour=12 user="root"'
[root@controller ansible]# ansible all -m cron -a 'backup="True" name="test" minute="0" hour="5,2" job="ls -alh > /dev/null"'
[root@controller ansible]# ansilbe all -m cron -a 'cron_file=ansible_yum-autoupdate state=absent'
[root@controller ansible]# ansible all -m cron -a 'name="testcron minute" minute="*" job="echo test >> /tmp/test"'
[root@controller ansible]# ansible all -m cron -a 'name="testcron minute" state=absent'

4.15、synchronize模块

使用rsync同步文件,其参数如下:

  • archive: 归档,相当于同时开启recursive(递归)、links、perms、times、owner、group、-D选项都为yes ,默认该项为开启
  • checksum: 跳过检测sum值,默认关闭
  • compress:是否开启压缩
  • copy_links:复制链接文件,默认为no ,注意后面还有一个links参数
  • delete: 删除不存在的文件,默认no
  • src:源地址
  • dest:目录路径
  • dest_port:默认目录主机上的端口 ,默认是22,走的ssh协议
  • dirs:传输目录不进行递归,默认为no,即进行目录递归
  • rsync_opts:rsync参数部分
  • set_remote_user:主要用于/etc/ansible/hosts中定义或默认使用的用户与rsync使用的用户不同的情况
  • mode: push或pull 模块,push模式的话,一般用于从本机向远程主机上传文件,pull 模式用于从远程主机上取文件
[root@controller ansible]# ansible all -m synchronize -a 'src=/root/ansible/ dest=/tmp/data archive=yes delete=yes'    #归档同步目录  

说明

  • archive:-a
  • compress: --compress
  • delete:--delete
  • copy_links:--links
  • dest:/data
  • src:/data/*
  • rsync_opts:-pU
  • mode:push/pull

push 在主控端执行

[root@controller ansible]# ansible mysql -m syncchronize -a 'archive=yes compress=yes dest=/data/ src=/data/ rsync_opts=-vz mode=push'
[root@controller ansible]# rsync -avz --compress /data/* 192.168.40.131:/data/

push 在被控端执行

[root@controller ansible]# ansible mysql -m syncchronize -a 'archive=yes compress=yes dest=/data/ src=/data/ rsync_opts=-vz mode=pull'
[root@controller ansible]# rsync -avz --compress 192.168.8.130:/data/* /data/

4.16、filesystem模块

在块设备上创建文件系统

常用选项:

  • dev:目标块设备
  • force:在一个已有文件系统的设备上强制创建
  • fstype:文件系统的类型
  • opts:传递给mkfs命令的选项
[root@controller ansible]# ansible test -m filesystem -a 'fstype=ext2 dev=/dev/sdb1 force=yes'
[root@controller ansible]# ansible test -m filesystem -a 'fstype=ext4 dev=/dev/sdb1 opts="-cc"'

4.17、mount模块

配置挂载点

选项:

  • boot:是否开机自动挂载
  • fstype:必选项,挂载文件的类型
  • name:必选项,挂载点
  • opts:传递给mount命令的参数
  • src:必选项,要挂载的文件
  • state:必选项
    • present:只处理fstab中的配置
    • absent:删除挂载点
    • mounted:自动创建挂载点并挂载之
    • umounted:卸载
[root@controller ansible]# ansible test -m mount -a "name=/mnt/dvd src=/dev/sr0 fstype=iso9660 opts=ro state=present"
[root@controller ansible]# ansible test -m mount -a "name=/srv/disk src='LABEL=SOME_LABEL' state=present "
[root@controller ansible]# ansible test -m mount -a "name=/home src='UUID=b3e48f45-f933-4c8e-a700-22a159ec9077' opts=noatime state=present"
[root@controller ansible]# ansible test -a 'dd if=/dev/zero of=/disk.img bs=4k count=1024'
[root@controller ansible]# ansible test -a 'losetup /dev/loop0 /disk.img'
[root@controller ansible]# ansible test -m filesystem -a 'fstype=ext4 force=yes opts=-F dev=/dev/loop0'
[root@controller ansible]# ansible test -m mount -a 'name=/mnt src=/dev/loop0 fstype=ext4 state=mounted opts=rw'

4.18、get_url 模块

该模块主要用于从http、ftp、https服务器上下载文件(类似于wget),主要有如下选项:

  • sha256sum:下载完成后进行sha256 check;
  • timeout:下载超时时间,默认10s
  • url:下载的URL
  • url_password、url_username:主要用于需要用户名密码进行验证的情况
  • use_proxy:是事使用代理,代理需事先在环境变更中定义
[root@controller ~]# ansible all -m get_url -a 'url=http://controller/passwd dest=/tmp  mode=0440'    #从控制端下载文件到被控端的/tmp目录,并修改权限

4.19、unarchive模块

用于解压文件,模块包含如下选项:

  • remote_src:默认为no,直接将主控端的压缩包解压到被控端,如果为yes,则是将被控端的压缩包解压到被控端
  • creates:指定一个文件名,当该文件存在时,则解压指令不执行
  • dest:远程主机上的一个路径,即文件解压的路径
  • group:解压后的目录或文件的属组
  • list_files:如果为yes,则会列出压缩包里的文件,默认为no,2.0版本新增的选项
  • mode:解决后文件的权限
  • src:如果copy为yes,则需要指定压缩文件的源路径
  • owner:解压后文件或目录的属主
[root@controller ~]# ansible all -m unarchive -a 'src=foo.tgz dest=/var/lib/foo'
[root@controller ~]# ansible all -m unarchive -a 'src=/tmp/foo.zip dest=/usr/local/bin copy=no'
[root@controller ~]# ansible all -m unarchive -a 'src=https://example.com/example.zip dest=/usr/local/bin remote_src=yes'

4.20、assemble模块:

用于组装文件,即将多个零散的文件,合并一个大文件

常用参数:

  • src:原文件(即零散文件)的路径
  • dest:合并后的大文件路径
  • group:合并后的大文件的属组
  • owner:合并后的大文件的属主
  • mode:合并后的大文件的权限
  • validate:与template的validate相同,指定命令验证文件
  • ignore_hidden:组装时,是否忽略隐藏文件,默认为no,该参数在2.0版本中新增
- hosts: all
  tasks:
    - name: Make a Directory in /opt
      file: path=/opt/sshkeys state=directory owner=root group=root mode=0700 
    - name: Copy SSH keys over
      copy: src=keys/{{ item }}.pub dest=/opt/sshkeys/{{ item }}.pub owner=root group=root mode=0600
      with_items: 
        - dan 
        - kate 
        - mal 
    - name: Make the root users SSH config directory
      file: path=/root/.ssh state=directory owner=root group=root mode=0700
      
    #将/opt/sshkeys目录里所有的文件合并到/root/.ssh/authorized_keys一个文件中
    - name: Build the authorized_keys file
      assemble: src=/opt/sshkeys/ dest=/root/.ssh/authorized_keys owner=root group=root mode=0700 

4.21、fetch模块:

用于将被控端的文件发送到主控端

  • flat:不生成目录结构,直接从根目录开始
[root@controller ~]# ansible all -m fetch -m fetch -a 'src=/etc/httpd/conf/httpd.conf dest=/root/ansible/ flat=yes'

4.22、stat模块:

判断被控端文件是否存在

  • path:文件路径
- hosts: servera
  tasks:
        - name: test stat
          stat:
                path: /etc/passwd
          register: zzd
        - name: debug state
          debug:
                msg: "{{ zzd  }}"

5、Playbook

5.1、Ansible Playbook简介

ansbile-playbook是一系列ansible命令的集合,利用yaml 语言编写。playbook命令根据自上而下的顺序依次执行。同时,playbook开创了很多特性,它可以允许你传输某个命令的状态到后面的指令,如你可以从一台机器的文件中抓取内容并附为变量,然后在另一台机器中使用,这使得你可以实现一些复杂的部署机制,这是ansible命令无法实现的。

playbook通过ansible-playbook命令使用,它的参数和ansible命令类似,如参数-k(–ask-pass) 和 -K (–ask-sudo) 来询问ssh密码和sudo密码,-u指定用户,这些指令也可以通过规定的单元写在playbook 。ansible-playbook的简单使用方法: ansible-playbook example-play.yml 。

5.2、Playbook基本语法

下面是一个简单的ansible-playbook示例,可以了解其构成:

[root@controller ansible]# cat user.yml
- name: create user
  hosts: all
  remote_user: root
  gather_facts: false
  vars:
    user:"test"
  tasks:
    - name: create  user
      user: name="{{ user }}"
  ignore_errors: yes    #忽略错误

配置项说明:

  • name:对该playbook实现的功能做一个概述,后面执行过程中,会打印 name变量的值
  • hosts:指定对哪些被管理机进行操作;
  • remote_user:指定在远程被管理机上执行操作时使用什么用户,如不指定,则使用ansible.cfg中配置的remote_user
  • gather_facts:指定在执行任务之前,是否先执行setup模块获取主机相关信息,如未用到,可不指定
  • vars:定义后续任务中会使用到的变量,如未用到,可不指定
  • tasks:定义具体需要执行的任务
    • name:对任务的描述,在执行过程中会打印出来。
    • user:指定调用user模块;
      • name:user模块里的一个参数,用于指定创建的用户名称

同样,如果想实现把这个新增的用户删除,只需将该playbook文件的最后一行替换为如下行再执行相应的playbook即可:

user: name="{{ user }}" state=absent remove=yes

5.3、Playbook简单示例

下面通过playbook管理一个httpd服务器来简单了解下playbook的应用:

1、创建playbook

[root@controller ansible]# vim manage_apache.yml
- name: play to setup web server
  hosts: all
  tasks:
    - name: latest httpd version installed
      yum:
        name: httpd
        state: latest
        
    - name: correct index.html is present
      copy: 
        src: files/index.html
        dest: /var/www/html/index.html
        
    - name: start httpd service
      service:
        name: httpd
        state: started
        enabled: true

2、执行playbook

[root@controller ansible]# ansible-playbook  manage_apache.yml 

PLAY [play to setup web server] *********************************************************************************************************************************************

TASK [Gathering Facts] ******************************************************************************************************************************************************
ok: [10.1.61.187]

TASK [latest httpd version installed] ***************************************************************************************************************************************
changed: [10.1.61.187]

TASK [correct index.html is present] ****************************************************************************************************************************************
changed: [10.1.61.187]

TASK [start httpd service] **************************************************************************************************************************************************
changed: [10.1.61.187]

PLAY RECAP ******************************************************************************************************************************************************************
10.1.61.187                : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  

5.4、ansible-playbook常用选项

1. 打印详细信息

  • -v:打印任务运行结果
  • -vv:打印任务运行结果以及任务的配置信息
  • -vvv:包含了远程连接的一些信息
  • -vvvv:Adds extra verbosity options to the connection plug-ins,including the users being used in the managed hosts to execute scripts, and what scripts have been executed
[root@controller ansible]# ansible-playbook  manage_apache.yml  -vv

2. 校验playbook语法

[root@controller ansible]# ansible-playbook --syntax-check  manage_apache.yml   

playbook: manage_apache.yml

3. 测试运行playbook

通过-C选项可以测试playbook的执行情况,但不会真的执行:

[root@controller ansible]# ansible-playbook -C  manage_apache.yml  

PLAY [play to setup web server] *********************************************************************************************************************************************

TASK [Gathering Facts] ******************************************************************************************************************************************************
ok: [10.1.61.187]

TASK [latest httpd version installed] ***************************************************************************************************************************************
ok: [10.1.61.187]

TASK [correct index.html is present] ****************************************************************************************************************************************
ok: [10.1.61.187]

TASK [start httpd service] **************************************************************************************************************************************************
ok: [10.1.61.187]

PLAY RECAP ******************************************************************************************************************************************************************
10.1.61.187                : ok=4    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

5.5、Multiple Plays

# This is a simple playbook with two plays

- name: first play
  hosts: web.example.com
  tasks:
    - name: first task
      yum:
        name: httpd
        status: present
    - name: second task
      service:
        name: httpd
        state: started
    
- name: second play
  hosts: db.example.com
  tasks:
    - name: first task
      yum:
        name: mariadb-server
        status: present
    - name: second task
      service:
        name: mariadb
        state: started

6. Ansible Playbook的结构及handler用法

6.1、playbook的结构说明

playbook是由一个或多个"play"组成的列表。play的主要功能就是对一组主机应用play中定义好的task。从根本上来讲一个task就是对ansible一个module的调用。而将多个play按照一定的顺序组织到一个playbook中,我们称之为编排。

playbook主要有以下四部分构成:

  • Target section: 用于定义将要执行playbook的远程主机组及远程主机组上的用户,还包括定义通过什么样的方式连接远程主机(默认ssh)
  • Variable section: 定义playbook运行时需要使用的变量
  • Task section: 定义将要在远程主机上执行的任务列表
  • Handler section: 定义task执行完成以后需要调用的任务

6.2、Target section

playbook中的每一个play的目的都是为了让某个或某些主机以某个指定的用户身份执行任务。

6.3、Playbook中的远程用户

playbook中的远程用户和ad-hoc中的使用没有区别,默认不定义,则直接使用ansible.cfg配置中的用户相关的配置。也可在playbook中定义如下:

- name: /etc/hosts is up to date
  hosts: datacenter
  remote_user: automation
  become: yes
  become_mothod: sudo
  become_user: root
  
  tasks:
    - name: server.example.com in /etc/hosts
      lineinfile:
        path: /etc/hosts
        line: '192.168.0.200 server.exmaple.com server'
        state: present

6.4、Playbook中的hosts

playbook中的hosts即inentory中的定义主机与主机组,在《Ansible Inventory》中我们讲到了如何选择主机与主机组,在这里也完全适用。

- name: start mariadb
  hosts: db,&london
  tasks:
    - name: start mariadb
      service:
        name: mariadb
        state: started

6.5、Task section

play的主体部分是任务列表。

任务列表中的各任务按次序逐个在hosts中指定的所有主机上执行,在所有主机上完成第一个任务后再开始第二个。在自上而下运行某playbook时,如果中途发生错误,则整个playbook会停止执行,由于playbook的幂等性,playbook可以被反复执行,所以即使发生了错误,在修复错误后,再执行一次即可。

定义task可以使用action: module options或module: options的格式,推荐使用后者以实现向后兼容。

tasks:
  - name: make sure apache is running
    service: 
      name: httpd
      state: started
      
  - name: disable selinux
    command: /sbin/setenforce 0  

如果命令或脚本的退出码不为零可以使用如下方式替代:

tasks:
  - name: run this command and ignore the result
    shell: /usr/bin/somecommand || /bin/true

可以使用ignore_errors来忽略错误信息:

tasks:
  - name: run this command and ignore the result
    shell: /usr/bin/somecommand
    ignore_errors: True

6.6、Handler section

  • 在Ansible Playbook中,handler事实上也是个task,只不过这个task默认并不执行,只有在被触发时才执行。
  • handler通过notify来监视某个或者某几个task,一旦task执行结果发生变化,则触发handler,执行相应操作。
  • handler会在所有的play都执行完毕之后才会执行,这样可以避免当handler监视的多个task执行结果都发生了变化之后而导致handler的重复执行(handler只需要在最后执行一次即可)。
tasks:
  - name: template configuration file
    template: 
      src: template.j2 
      dest: /etc/foo.conf
    notify:    #任务运行成功则会执行以下任务(该任务在最底下的handlers中指定)
      - restart memcached
      - restart apache
  - name: start memcached
    service:
      name: memcached
      state: started
  - name: start apache
    service
      name: httpd
      state: started
handlers:    #上面notify执行成功触发的任务(下面写的任务名需要和上面handlers的任务名一致)
  - name: restart memcached
    service:
      name: memcached
      state: restarted
  - name: restart apache
    service:
      name: httpd
      state: restarted

注:在notify中定义内容一定要和tasks中定义的 - name 内容一样,这样才能达到触发的效果,否则会不生效。

默认情况下,在一个play中,只要有task执行失败,则play终止,即使是与handler关联的task在失败的task之前运行成功了,handler也不会被执行。如果希望在这种情况下handler仍然能够执行,则需要使用如下配置:(一对一的方式)

- hosts: all
  force_handlers: yes    #如果触发handlers,无论后面的任务执行成功与否,都会执行handlers里面的任务
  tasks:
    - name: a task which always notifies its handler
      command: /bin/true
      notify: restart the database
    - name: a task which fails because the package doesn't exist
      yum:
        name: notapkg
        state: latest
      
  handlers:
    - name: restart the database
      service:
        name: mariadb
        state: restarted

如果与handler关联的task还未执行,在其前的task已经失败,整个play终止,则handler未被触发,也不会执行。

6.7、补充:handler的另外一种定义方(一对多的方式)

- hosts: all
  force_handlers: yes
  tasks:
    - name: a task which always notifies its handler
      command: /bin/true
      notify: webservice 
    - name: a task which fails because the package doesn't exist
      yum:
        name: notapkg
        state: latest
      
  handlers:
    - name: restart the database
      service:
        name: mariadb
        state: restarted
      listen: webservice
    - name: echo hello
      shell: echo hello
      listen: webservice

7. Ansible变量之自定义变量

简单说明

ansible支持变量,用于存储会在整个项目中重复使用到的一些值。以简化项目的创建与维护,降低出错的机率。

变量的定义:

  • 变量名应该由字母、数字下划数组成
  • 变量名必须以字母开头
  • ansible内置关键字不能作为变量名

7.1 在Inventory中定义变量

7.1.1、定义主机变量

内置主机变量

所谓内置变量其实就是ansible.cfg配置文件中的选项,在其前加上ansible_即成为内置变量。当然内置变拥有比ansible.cfg中选项更高的优先级,而且针对不同的主机,可以定义不同的值。

# 一般配置

ansible_host     #用于指定被管理的主机的真实IP
ansible_ssh_port     #用于指定连接到被管理主机的ssh端口号,默认是22
ansible_ssh_user     #ssh连接时默认使用的用户名

# 特定ssh连接
ansible_connection     #SSH连接的类型:local, ssh, paramiko,在ansible 1.2 之前默认是paramiko,后来智能选择,优先使用基于ControlPersist的ssh(如果支持的话)

ansible_ssh_pass     #ssh连接时的密码
ansible_ssh_private_key_file  #秘钥文件路径,如果不想使用ssh-agent管理秘钥文件时可以使用此选项

ansible_ssh_executable  #如果ssh指令不在默认路径当中,可以使用该变量来定义其路径

# 特权升级
ansible_become  #相当于ansible_sudo或者ansible_su,允许强制特权升级
ansible_become_method #设置提权方法,sudo
ansible_become_user #通过特权升级到的用户,相当于ansible_sudo_user或者ansible_su_user
ansible_become_ask_pass #提权是否需要密码,False 或者true
ansible_become_pass  # 提升特权时,如果需要密码的话,可以通过该变量指定,相当于ansible_sudo_pass或者ansible_su_pass

ansible_sudo_exec     #如果sudo命令不在默认路径,需要指定sudo命令路径

# 远程主机环境参数

ansible_shell_executable # 设置目标机上使用的shell,默认为/bin/sh

ansible_python_interpreter     #用来指定python解释器的路径,默认为/usr/bin/python 同样可以指定ruby 、perl 的路径
ansible_*_interpreter     #其他解释器路径,用法与ansible_python_interpreter类似,这里"*"可以是ruby或才perl等其他语言

下面是一个简单的示例:

# 指定了三台主机,三台主机的用密码分别是P@ssw0rd、123456、45789,指定的ssh连接的用户名分别为root、marry、bernie,ssh 端口分别为22、22、3055 ,这样在ansible命令执行的时候就不用再指令用户和密码等了

[test]
192.168.1.1 ansible_ssh_user=root ansible_ssh_pass='P@ssw0rd'
192.168.1.2 ansible_ssh_user=marry ansible_ssh_pass='123456'
192.168.1.3 ansible_ssh_user=bernie ansible_ssh_port=3055 ansible_ssh_pass='456789'

ini格式定义变量:

[webservers]
servera
serverb    ansible_ssh_port: 2222    #主机中定义变量,如果与主机组冲突,则优先级更高
[webservers:vars]    #主机组中定义变量
username: Bob
password: redhat 

yaml格式定义变量:

all:
  children:
    webservers:
      vars:    #在主机组中定义变量
         ansible_become: true
         ansible_become_user: root
         ansible_become_method: sudo
         ansible_become_ask_pass: False
        
      hosts:
        servera:    
            ansible_host: 192.168.1.1    #在主机中定义变量
            ansible_ssh_user: root 
            ansible_ssh_pass: 'P@ssw0rd'
        serverb:
            ansible_host: 192.168.1.1
            ansible_ssh_user: marry 
            ansible_ssh_pass: '123456'
        serverc:
            ansible_host: 192.168.1.3
            ansible_ssh_user: bernie 
            ansible_ssh_port: 3055 
            ansible_ssh_pass: '456789'

7.1.2、定义主机组变量

变量也可以通过组名,应用到组内的所有成员:

# test组中包含两台主机,通过对test组指定vars变更,相应的host1和host2相当于相应的指定了ntp_server和proxy变量参数值

[test]
host1
host2
[test:vars]
ntp_server=192.168.1.10
proxy=192.168.1.20

主机组变量示例:

# 下面是一个示例,指定了一个武汉组有web1、web2;随州组有web3、web4主机;又指定了一个湖北组,同时包含武汉和随州;同时为该组内的所有主机指定了2个vars变量。设定了一个组中国组,包含湖北、湖南。

[wuhan]
web1
web2

[suizhou]
web4
web3

[hubei:children]
wuhan
suizhou

[hubei:vars]
ntp_server=192.168.1.10
zabbix_server=192.168.1.10

7.2.、在Playbook中定义变量

7.2.1、变量的定义方式

变量的定义格式是成键值对出现的,键值对之间可以嵌套,最终形成一个大字典

通过vars关键字定义:

# 在playbook中定义变量,仅对当前playbook生效
- name: use vars define invrionmemnt
  hosts: test
  user: ansible
  vars: 
    http_port: 80
    server_name: localhost
    conf_file: /etc/nginx/conf/default.conf 
    
# 在tasks中定义变量,仅对当前task生效
- name: use vars define invrionmemnt
  hosts: test
  user: ansible
  tasks:
     - name: 
       debug:
          msg: "xxx"
       vars: 
         http_port: 80
         server_name: localhost
         conf_file: /etc/nginx/conf/default.conf

通过vars_files关键字引入变量文件

- hosts: all
  remote_user: root
  vars:
    favcolor: blue
  vars_files:
    - vars/external_vars.yml
    - vars/user_vars.yml

# vars/user_vars.yml示例:

users:
  bjones:
    first_name: Bob
    last_name: Jones
    home_dirs: /users/bjones
  acook:
    first_name: Anne
    last_name: Cook
    home_dirs: /users/acook

在playbook中通过host_vars和group_vars目录定义变量

下面这是一个项目的playbook目录结构。这个项目中,包含一个ansible.cfg文件,一个inventory文件,一个playbook.yml文件,一个group_vars目录和一个host_vars目录:

[root@controller ansible]#  tree /etc/ansible/playbooks/project
/etc/ansible/playbooks/project
├── ansible.cfg
├── group_vars
│   ├── datacenter1
│   ├── datacenter2
│   └── datacenters
├── host_vars
│   ├── demo1.example.com
│   ├── demo2.example.com
│   ├── demo3.example.com
│   └── demo4.example.com
├── inventory
└── playbook.yml

其中inventory文件的示例如下:

[datacenter1]
demo1.example.com
demo2.example.com

[datacenter2]
demo3.example.com
demo4.example.com

[datacenters:children]
datacenter1
datacenter2

可以看到group_vars目录中,定义了三个文件,分别以inventory文件中的三个主机组命名,所以这三个文件中定义的就分别是这三个组可以使用的变量。

[root@controller ansible]# cat datacenter1
package: httpd

[root@controller ansible]# cat datacenter2 
package: apache

[root@controller ansible]# cat datacenters 
package: httpd

在host_vars目录中,定义了三个文件,分别以inventory文件中的四个主机命名,所以这四个文件中定义的就分别是这四个主机可以使用的变量。

[root@controller ansible]# cat demo1.example.com 
package: httpd

[root@controller ansible]# cat demo2.example.com 
package: apache

[root@controller ansible]# cat demo3.example.com 
package: mariadb-server

[root@controller ansible]# cat demo4.example.com 
package: mysql-server

需要说明的是,如果主机组定义的变量与主机冲突,主机变量优先级最高

注册变量

在有些时候,可能需要将某一条任务执行的结果保存下来,以便在接下的任务中调用或者做些判断。可以通过register关键字来实现将某一任务结果保存为一个变量。

下面是个简单的例子,将whoami命令执行的结果注册到变量login:

- name: register variables
  hosts: test
  tasks:
    - name: capture output of whoami command
      command: whoami
      register: login

注册变量的应用场景:

  • 在一台远端的服务器获取一个目录下的一列表的文件,然后下载这些文件
  • 在handler执行之前,发现前面一个task发生了changed,然后执行一个指定的task
  • 获取远端服务器的ssh key的内容,构建出known_hosts文件

通过命令行设置变量(优先级最高)

---
- hosts: '{{ hosts }}'
  remote_user: '{{ user }}'
  tasks:
     - ...
   
[root@controller ansible]# ansible-playbook release.yml --extra-vars "hosts=vipers user=starbuck"    #其中长选项--extra-vars可以写成短选项-e

也可以写成类似如下方式:

--extra-vars '{"hosts":"vipers","user":"starbuck"}'
#传递一个变量
[root@controller ansible]# ansible-playbook -e password=huawei debug.yml
#传递多个变量(需要用字典)
[root@controller ansible]# ansible-playbook -e '{password:huawei,hello:user1}' debug.yml

7.2.2、使用与调试变量

我们通过以上5种方式在playbook中定义了各种变量。说到底,最终的目的,还是为了方便使用。下面我们就看一看具体如何使用这些变量

变量的引用

下面是一个变量的基本使用示例,前面的变量定义部分,直接使用的2.1.1中的变量示例:

- name: use vars define variables
  hosts: test
  vars: 
    http_port: 80
    server_name: localhost
    conf_file: /etc/nginx/conf/default.conf
    
  tasks:
    - name: print variables
      shell: echo "{{ http_port }} {{ server_name }} {{ conf_file }}"  > /tmp/text.txt

在上面通过vars_files引用了一个文件user_vars.yml,在该文件中定义了一个稍微复杂的字典变量,现在我想要获取users中bjones用户的first_name和acook用户的home_dirs,可以使用如下方法

{{ users.bjones.first_name }}
{{ users.acook.home_dirs }}

或者如下写法:

{{ users['bjones']['first_name'] }}
{{ users['acook']['home_dirs'] }}

变量的调试输出

有些时候,我们在引用变量的时候,可能需要知道变量中包含哪些信息,以方便在执行过程中,对变量做些处理。ansible提供一个debug模块用于调试变量输出:

- name: register variables
  hosts: test
  tasks:
    - name: capture output of whoami command
      command: whoami
      register: login
    - debug: var=login

执行后输出如下:

[root@controller ansible]# ansible-playbook register.yml 

PLAY [register variables] ***************************************************************************************************************************************************

TASK [Gathering Facts] ******************************************************************************************************************************************************
ok: [10.1.61.187]

TASK [capture output of whoami command] *************************************************************************************************************************************
changed: [10.1.61.187]

TASK [debug] ****************************************************************************************************************************************************************
ok: [10.1.61.187] => {
    "login": {
        "changed": true,
        "cmd": [
            "whoami"
        ],
        "delta": "0:00:00.004279",
        "end": "2019-05-24 00:41:43.710398",
        "failed": false,
        "rc": 0,
        "start": "2019-05-24 00:41:43.706119",
        "stderr": "",
        "stderr_lines": [],
        "stdout": "root",
        "stdout_lines": [
            "root"
        ]
    }
}

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

关于输出的debug部分重点说明如下:

  • login: 变量名,其值为一个字典
  • changed:ansible基于此来判断是否发生了状态改变
  • cmd:被调用的命令
  • failed:是否运行失败
  • rc:返回值,0代表正常,非0代表异常
  • stderr:如果出现异常,会在这里显示错误输出
  • stderr_lines:按行分割的错误输出
  • stdout:如果指令正常运行,则在这里输出返回结果
  • stdout:按行分割的返回结果

需要说明的是,通过register注册的变量的结果并不是一成不变的,在不确定返回值的情况下,尽量调试看看输出结果。

关于debug的更多用法说明:

调试模块,用于在调试中输出信息

常用参数:

  • msg:调试输出的消息
  • var:将某个变量传递给debug模块,debug会直接将其打印输出
  • verbosity:debug的级别
# Example that prints the loopback address and gateway for each host
- debug: msg="System {{ inventory_hostname }} has uuid {{ ansible_product_uuid }}"

- debug: msg="System {{ inventory_hostname }} has gateway {{ ansible_default_ipv4.gateway }}"
  when: ansible_default_ipv4.gateway is defined

- shell: /usr/bin/uptime
  register: result

- debug: var=result verbosity=2    #直接将上一条指令的结果作为变量传递给var,由debug打印出result的值

- name: Display all variables/facts known for a host
  debug: var=hostvars[inventory_hostname] verbosity=4

7.3、定义变量:

7.3.1、第一种方式:

变量名:值(键值对)
name:bob

7.3.2、第二章方式:

users:
    - name: marry
      uid: 1100
      comment: marry wang
      home: /home/marry
      - name: bob
      uid: 1100
      comment: bob li
      home: /home/bob

7.4、变量取值:

在playbook中取值
users:
    - name: marry
      uid: 1100
      comment: marry wang
      home: /home/marry
      - name: bob
      uid: 1100
      comment: bob li
      home: /home/bob

第一种取值方法:(使用列表的索引)
users.0.name
users.1.name
#不支持调用变量

第二种取值方法:
users[0]['name']    #name是字符串,Key
users[0][test]    #test是变量
#[]里面不打引号,代表是变量,打印号代表是字符串,索引0,1,2不需要打印号

第三章取值方法:(将前两种混合使用)
users[0].name

7.5、关于变量取值的思路:

1.要使用debug打印变量

2.查看变量的逻辑结构,如果是列表则使用索引取值,如果是字典则用key取值

7.6、变量优先级:(从低到高)

inventory( group < hosts )> group_vars > host_vars > playbook ( vars = vars_files ) > tasks > 命令行中定义的变量( -e 引入变量)

8. Ansible变量之fact

fact简介

ansible有一个模块叫setup,用于获取远程主机的相关信息,并可以将这些信息作为变量在playbook里进行调用。而setup模块获取这些信息的方法就是依赖于fact。

[root@controller ansible]#  ansible test -m setup
10.1.61.187 | SUCCESS => {
    "ansible_facts": {
            "ansible_all_ipv4_addresses": [
            "10.1.61.187"
        ],
        "ansible_all_ipv6_addresses": [
            "fe80::f816:3eff:fe4f:6611"
        ],
        "ansible_apparmor": {
            "status": "disabled"
        },
        "ansible_architecture": "x86_64",
        "ansible_bios_date": "04/01/2014",
        "ansible_bios_version": "Ubuntu-1.8.2-1ubuntu1~cloud0",
        
        ...output omitted...
}

setup获取的这些信息,都是可用于该主机的变量。

setup模块其他用法示例:

# 查看主机内存信息
[root@controller ansible]# ansible 10.212.52.252 -m setup -a 'filter=ansible_*_mb'

# 查看地接口为eth0-2的网卡信息
[root@controller ansible]# ansible 10.212.52.252 -m setup -a 'filter=ansible_eth[0-2]' 

# 将所有主机的信息输入到/tmp/facts目录下,每台主机的信息输入到主机名文件中(/etc/ansible/hosts里的主机名)
[root@controller ansible]# ansible all -m setup --tree /tmp/facts   

8.1、自定义fact

8.1.1、手动设置fact

ansible除了能获取到预定义的fact的内容,还支持手动为某个主机定制fact。称之为本地fact。本地fact默认存放于被控端的/etc/ansible/facts.d目录下,如果文件为ini格式或者json格式,ansible会自动识别。以这种形式加载的fact是key为ansible_local的特殊变量。

下面是一个简单的示例,在ansibler主控端定义一个ini格式的custom.fact文件内容如下:

[general]
package = httpd
service = httpd
state = started

然后我们编写一个playbook文件名为setup_facts.yml内容如下:

---
- name: Install remote facts
  hosts: test
  vars: 
    remote_dir: /etc/ansible/facts.d
    facts_file: custom.fact
  tasks:
    - name: Create the remote directory
      file:
        state: directory
        recurse: yes
        path: "{{ remote_dir }}"
    - name: Install the new facts
      copy:
        src: "{{ facts_file }}"
        dest: "{{ remote_dir }}"

执行该playbook,完成facts的推送:

[root@controller ansible]# ansible-playbook setup_facts.yml

此时,我们可以在被控端看到新的facts已经生成:

# ansible test -m setup        
10.1.61.187 | SUCCESS => {
    "ansible_facts": {
    
        ...output omitted...
        
        "ansible_local": {
            "custom": {
                "general": {
                    "package": "httpd",
                    "service": "httpd",
                    "state": "started"
                }
            }
        },

        ...output omitted...
  
}

我们可以写一个简单的playbook来使用这些facts:

- name: Install Apache and starts the service
  hosts: test
  tasks:
    - name: Install the required package
      yum: 
        name: "{{ ansible_facts.ansible_local.custom.general.package }}"
        state: latest
    - name: Start the service
      service: 
        name: "{{ ansible_facts.ansible_local.custom.general.service }}"
        state: "{{ ansible_facts.ansible_local.custom.general.state }}"

8.1.2、使用set_fact模块定义新的变量

set_fact模块可以自定义facts,这些自定义的facts可以通过template或者变量的方式在playbook中使用。如果你想要获取一个进程使用的内存的百分比,则必须通过set_fact来进行计算之后得出其值,并将其值在playbook中引用。

生效范围:是从定义变量开始,到playbook文件结束,即该playbook整个文件可以继承前面playbook中通过set_fact定义的变量,但前提是必须在同一台主机上运行

下面是一个set_fact模块的应用示例:

- name: set_fact example
  hosts: test
  tasks:
    - name: Calculate InnoDB buffer pool size
      set_fact: innodb_buffer_pool_size_mb="{{ ansible_memtotal_mb / 2 |int }}"
      
    - debug: var=innodb_buffer_pool_size_mb

执行playbook如下:

[root@controller ansible]# ansible-playbook set_fact_ex.yaml 

PLAY [set_fact example] *****************************************************************************************************************************************************

TASK [Gathering Facts] ******************************************************************************************************************************************************
ok: [10.1.61.187]

TASK [Calculate InnoDB buffer pool size] ************************************************************************************************************************************
ok: [10.1.61.187]

TASK [debug] ****************************************************************************************************************************************************************
ok: [10.1.61.187] => {
    "innodb_buffer_pool_size_mb": "3911.0"
}

PLAY RECAP ******************************************************************************************************************************************************************
10.1.61.187                : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

8.2、手动采集fact

通常情况下,我们在运行play的时候,ansible会先尝试ssh到被控端采集fact,如果此时,被控制端的ssh还没有完全启动,就会导致整个play执行失败。这个时候,我们可以先显式的关闭fact采集,然后在task中通过wait_for等待被控端ssh端口被正常监听,再在task中使用setup模块来手动采集fact:

- name: Deploy apps
  hosts: webservers
  gather_facts: False
  tasks:
    - name: wait for ssh to be running
      local_action: wait_for port=22 host="{{ inventory_hostname }}" search_regex=OpenSSH
    - name: gather facts
      setup:
    ......

8.3、启用fact缓存

如果在play中需要引入fact,则可以开启fact缓存。fact缓存目前支持三种存储方式,分别为JSON、memcached、redis。

8.3.1、Json文件fact缓存后端

使用JSON文件作为fact缓存后端的时候,ansible将会把采集的fact写入到控制主机的文件中。

ansible.cfg配置如下:

[defaults]
gathering = smart
#缓存时间,单位为秒
fact_caching_timeout = 86400    
fact_caching = jsonfile
#指定ansible包含fact的json文件位置,如果目录不存在,会自动创建
fact_caching_connection = /tmp/ansible_fact_cache    

选项说明:

  • gathering: 是否启用fact,有三个选项:
    • smart:默认收集facts,但在facts已有的情况下就不收集,即使用facts缓存
    • implicit:默认收集facts,要禁止收集,必须显式的申明:gather_facts: false
    • explicit:默认不收集,要收集,必须显示的申明: gather_facts: true
  • fact_cacheing_timeout:缓存时间,单位为s
  • fact_caching:缓存的方式,支持jsonfile、redis、memcached
  • fact_caching_connection:指定ansible缓存fact的连接方式,如果是jsonfile,则指定jsonfile的缓存路径

8.3.2、Redis fact缓存后端

使用redis作为fact缓存后端,需要在控制主机上安装redis服务并保持运行。需要安装python操作redis的软件包。

ansible.cfg配置如下:

[defaults]
gathering = smart
fact_caching_timeout = 86400 
fact_caching = redis
fact_caching_connection = 127.0.0.1:6379:0

8.3.3. Memcached fact缓存后端

使用memcached作为fact缓存后端,需要在控制主机上安装Memcached服务并保持运行,需要安装python操作memcached的软件包。

ansible.cfg配置如下:

[defaults]
gathering = smart
fact_caching_timeout = 86400 
fact_caching = memcached
fact_caching_connection = 127.0.0.1:11211

8.4、关闭fact

如果不想从fact中获取变量,或者说整个playbook当中都没有使用到fact变量,可以通过如下方法关闭fact以提升执行效率:

- hosts: test
  gather_facts: no

也可以在ansible.cfg中添加如下配置:

[defaults]
gathering = explicit

9. Ansible魔法变量及变量优先级

魔法变量

Ansible默认会提供一些内置的变量以实现一些特定的功能,我们称之为魔法变量。下面列举一些常用的魔法变量。

9.1、hostvars

获取某台指定的主机的相关变量。如果有一台web服务器的配置文件中需要指定db服务器的ip地址,我们假定这台db服务器的hostname为db.exmaple.com,ip地址绑定在eth0网卡上,我们可以通过如下方法在web服务器上调用db服务器的ip地址:

{{ hostvars['db.example.com'].ansible_eth0.ipv4.address }}

9.2、inventory_hostname(获取主机清单中定义的主机名)

inventory_hostname是Ansible所识别的当前正在运行task的主机的主机名。如果在inventory里定义过别名,那么这里就是那个别名,如果inventory包含如下一行:

server1 ansible_ssh_host=192.168.1.1

则inventory_hostname即为server1

利用hostvars和inventory_hostname变量,可以输出与当前主机相关联的所有变量:

- debug: var=hostvars[inventory_hostname]

与inventory_hostname相近的还有一个inventory_hostname_short,如果一台主机的inventory_hostname为server1.exmaple.com,则inventory_hostname_short的值为server1

9.3、group_names(任务在哪个主机上运行,则获取该主机所在的组)

用于标识当前正在执行task的目标主机位于的主机组。假如我们有三台主机,用来配置成一主二从的mysql服务器。inventory配置如下:

[mdb]
db1
[sdb]
db2
db3

mysql配置文件my.conf.j2示例如下:

#我们知道db1在mdb组,当db1与当前正在执行任务的主机位于同一组时,我们认为当前主机即在mdb组,所以对当前主机应用mysql master的配置
{% if 'db1' in group_names %}
[mysqld]
server-id=1
log-bin=mysql-bin
log-bin-index=mysql-bin.index
sync-binlog=1
innodb_flush_log_at_trx_commit=1
#当db1与当前主机不在同一组时,则认为当前主机不在mdb组,即应用my slave的配置
{% else %}
[mysqld]
server-id=2
relay-log=relay-log
relay-log-index=relay-log.index
read-only = yes
sync_master_info = 1
sync_relay_log = 1
sync_relay_log_info = 1
relay_log_recovery = 1
skip_slave_start    
{% endif %}

我们执行如下task:

- name: copy config file to mysql master
  template: src=my.conf.j2 dest=/etc/my.cnf

9.4、groups(获取主机清单中定义的所有组)

groups.mysql:(获取主机清单中mysql组的主机)

groups是inventory中所有主机组的列表,可用于枚举主机组中的所有主机。

假如我们有一个inventory文件定义如下:

[web]
server1
server2

在配置一台HAproxy的负载均衡器时,我们的配置文件肯定需要web主机组的所有服务器的IP,配置文件包含如下片段:

backend web-backend
{% for host in groups.web%}
    server {{host.inventory_hostname}} {{ host.ansible_default_ipv4.address }}:80
{% endfor %}

最终生成的文件如下:

backend web-backend
    server server1 192.168.1.1:80
    server server2 192.168.1.2:80

再给一个例子,在所有的dbservers组的服务器上创建一个数据库用户kate:

- name: Create a user for all db servers
  mysql_user: name=kate password=test host={{ hostvars.[item].ansible_eth0.ipv4.address }} state=present
  with_items: groups['dbservers'] 

9.5、play_hosts

当前playbook会在哪些hosts上运行

9.6、inventory_dir

主机清单所在目录

9.7、inventory_file

主机清单文件

9.8、变量优先级

  1. extra vars(命令中-e)最优先
  2. inventory 主机清单中连接变量(ansible_ssh_user等)
  3. play 中 vars、vars_files 等
  4. 剩余的在 inventory 中定义的变量
  5. 系统的 facts 变量
  6. 角色定义的默认变量(roles/rolesname/defaults/main.yml)

子组会覆盖父组,主机总是覆盖组定义的变量

10. 使用lookup生成变量

简单说明

在通常情况下,所有的配置信息都会被作为ansible的变量保存了,而且可以保存在ansible允许定义变量的各种地方,诸如vars区段,vars_files加载的文件中,以及host_vars和group_vars目录中。

但在有些时候,我们希望从诸如文本文件或者.csv文件中收集数据作为ansible的变量,或者直接获取某些命令的输出作为ansible的变量,甚至从redis或者etcd这样的键值存储中取得相应的值作为ansible的变量。这个时候,我们就需要通过ansible的lookup插件来从这些数据源中读取配置数据,传递给ansbile变量,并在playbook或者模板中使用这些数据。

ansible支持一套从不同数据源获取数据的lookup,包括file, password, pipe, env, template, csvfile, dnstxt, redis_kv, etcd等

查询lookup插件:

ansible-doc -t lookup -l
ansible-doc -t lookup plugin

10.1、file

使用file lookup可以从文本文件中获取数据,并在这些数据传递给ansible变量,在task或者jinja2模板中进行引用。下面是一个从文本文件中获取ssh公钥并复制到远程主机的示例:

- name: copy authorized_host file
  template: 
    src: authorized_keys.j2 
    dest: /home/deploy/.ssh/authrized_keys 
    owner: deploy
    group: deploy
    mode: 0600

authorized_keys.j2模板文件示例如下:

{{ lookup('file', '/users/deploy/.ssh/id_rsa.pub')}}

10.2、pipe

lines可代替pipe,不同的是,lines可自动换行,格式化显示

使用pipe lookup可以直接调用外部命令,并将命令执行的结果打印到标准输出,作为ansible变量。下面的例子通过pipe调用date指令拿到一个以时间数字组成的字串

- name: Flamingo | Get release version
  set_fact:
    flamingo_release_version: "{{ lookup('pipe', 'date +%Y%m%d%H%M%SZ') }}"

10.3、env

env lookup实际就是获取在控制主机上的某个环境变量的值。下面是一个读取控制机上$JAVA_HOME变量值的示例:

- name: get JAVA_HOME
  debug: 
     msg: "{{ lookup('env', 'JAVA_HOME')}}"

10.4、url

读取一个url的内容

- name: get url
  debug: 
    msg: "{{ lookup('url', 'http://www.example.com')}}"


- name: url lookup using authentication
  debug: 
    msg: "{{ lookup('url', 'https://some.private.site.com/file.txt', username='bob', password='hunter2') }}"

10.5、template

template lookup可以指定一个jinja2模板,然后返回这个模板中的变量被替换以后的结果。

假设我们有一个message.j2模板,内容如下:

This host runs {{ ansible_distribution }}

定义一个如下的task:

- name: print message from template
  debug: 
    msg: "{{ lookup('template', 'message.j2')}}"

输出的msg的结果如下:

This host runs CentOS

10.6、csvfile

csvfile可以从.csv文件中读取一个条目。假设我们有如下示例的名为users.csv的文件:

[root@controller ansible]# cat users.csv
username,email
lorin,lorin@test.com
john,john@example.com
sue,sue@exmaple.com

下面是一个使用csvfile lookkup提取sue的电子邮件地址的task示例:

- name: get sue's email
  debug: 
    msg: "{{ lookup('csvfile','sue file=users.csv delimiter=, col=1')}}"

可以看到,一共向插件传递了四个参数:sue, file=users.csv, delimiter=,以及col=1。说明如下:

  • 第一个参数指定一个名字,该名字必须出现在其所在行的第0列,需要说明的是,如果指定的第一个参数名字在文件中出现多次,则匹配第一次出现的结果
  • 第二个参数指定csv文件的文件名
  • 第三个参数指定csv文件的中条目的分隔符,默认是tab
  • 第四个参数指定要取得哪一列的值,0代表第1列,1代表第2列,默认是1

如果我们想要查找的用户存储在名为username的变量中,则可以使用"+"符号来连接username字串和其他的参数字串,来构建完整的参数字符串:

lookup('csvfile', username+'file=users.csv' delimiter=, col=1)

10.7、redis_kv

redis_kv lookup可以直接从redis存储中来获取一个key的value,key必须是一个字符串,如同Redis GET指令一样。需要注意的是,要使用redis_kv lookup,需要在主控端安装python的redis客户端,在centos上,软件包为python-redis。

下面是一个在playbook中调用redis lookup的task,从本地的redis中取中一个key为weather的值:

- name: lookup value in redis
  debug: 
    msg: "{{ lookup('redis_kv', 'redis://localhost:6379,weather')}}"

其中URL部分如果不指定,该模块会默认连接到redis://localhost:6379,所以实际上在上面的实例中,调用可以直接写成如下:

{{ lookup('redis_kv', 'weather')}}

10.8、etcd

etcd是一个分布式的key-value存储,通常被用于保存配置信息或者被用于实现服务发现。可以使用etcd lookup来从etcd中获取指定key的value。

我们通过如下方法往一个etcd中写入一个key:

curl -L http://127.0.0.1:2379/v2/keys/weather -XPUT -d value=sunny

定义一个调用etcd插件的task:

- name: look up value in etcd
  debug: 
    msg: "{{ lookup('etcd','http://127.0.0.1:2379,weather')}}"

默认情况下,etcd lookup会在http://127.0.0.1:2379上查找etcd服务器。但我们在执行playbook之前可以通过设置ANSIBLE_ETCD_URL环境变量来修改这个设置。

10.9、password

password lookup会随机生成一个密码,并将这个密码写入到参数指定的文件中。如下示例,创建一个名为bob的mysql用户,并随机生成该用户的密码,并将密码写入到主控端的bob-password.txt中:

- name: create deploy mysql user
  mysql_user: 
     name: bob 
     password: {{ lookup('password', 'bob-password.txt')}} 
     priv: *.*:ALL 
     state: present

10.10、dnstxt

dnstxt lookup用于获取指定域名的TXT记录。需要在主控端安装python-dns。

使用方法如下:

- name: lookup TXT record
  debug: 
    msg: "{{ lookup('dnstxt', 'aliyun.com') }}"

如果某一个主机有多个相关联的TXT记录,那么模块会把他们连在一起,并且每次调用时的连接顺序可能不同

#dnspython模块安装
wget  http://www.dnspython.org/kits/1.12.0/dnspython-1.12.0.tar.gz 

tar -zxvf dnspython-1.12.0.tar.gz

cd dnspython-1.12.0

python setup.py install

10.11、补充:

ansible2.5版本以后,query逐渐替代lookup,lookup返回的值采用逗号为分隔符,而query总是返回一个列表。

{{ lookup('pipe', 'ls files') }}

{{ query('pipe', 'ls files') }}  

{{ query('lines', 'ls files') }}

11. Ansible Playbook条件语句

简介

在有的时候play的结果依赖于变量、fact或者是前一个任务的执行结果,或者有的时候,我们会基于上一个task执行返回的结果而决定如何执行后续的task。这个时候就需要用到条件判断。

条件语句在Ansible中的使用场景:

  • 在目标主机上定义了一个硬限制,比如目标主机的最小内存必须达到多少,才能执行该task
  • 捕获一个命令的输出,根据命令输出结果的不同以触发不同的task
  • 根据不同目标主机的facts,以定义不同的task
  • 根据目标机的cpu的大小,以调优相关应用性能
  • 用于判断某个服务的配置文件是否发生变更,以确定是否需要重启服务

11.1、when关键字

11.1.1、when基本使用

在ansible中,使用条件判断的关键字就是when。

如在安装包的时候,需要指定主机的操作系统类型,或者是当操作系统的硬盘满了之后,需要清空文件等,可以使用when语句来做判断 。when关键字后面跟着的是python的表达式,在表达式中你能够使用任何的变量或者fact,当表达式的结果返回的是false,便会跳过本次的任务

下面是一个基本的用法示例:

---
- name: Install vim
  hosts: all
  tasks:
    - name: Install VIM via yum
      yum: 
        name: vim-enhanced 
        state: installed
      when: ansible_os_family =="RedHat"
      
    - name: Install VIM via apt
      apt: 
        name: vim 
        state: installed
      when: ansible_os_family =="Debian"
      
    - name: Unexpected OS family
      debug: msg="OS Family {{ ansible_os_family }} is not supported"
      when: not ansible_os_family =="RedHat" or ansible_os_family =="Debian"

11.1.2、比较运算符

在上面的示例当中,我们使用了"=="的比较运算符,在ansible中,还支持如下比较运算符:

  • ==:比较两个对象是否相等,相等则返回真。可用于比较字符串和数字
  • !=:比较两个对象是否不等,不等则为真。
  • >:比较两个对象的大小,左边的值大于右边的值,则为真
  • >=:比较两个对象的大小,左边的值大于等于右边的值,则为真

下面是一些简单的示例:

when: ansible_machine == "x86_64" 

when: max_memory <= 512

11.1.3、逻辑运算符

在Ansible中,除了比较运算符,还支持逻辑运算符:

  • and:逻辑与,当左边和右边两个表达式同时为真,则返回真
  • or:逻辑或,当左右和右边两个表达式任意一个为真,则返回真
  • not:逻辑否,对表达式取反
  • ():当一组表达式组合在一起,形成一个更大的表达式,组合内的所有表达式都是逻辑与的关系

示例:

# 逻辑或
when: ansible_distribution == "RedHat" or ansible_distribution == "Fedora"

# 逻辑与
when: ansible_distribution_version == "7.5" and ansible_kernel == "3.10.0-327.el7.x86_64"

when:
  - ansible_distribution_version == "7.5"
  - ansible_kernel == "3.10.0-327.el7.x86_64"
  
# 组合

when: => 
  ( ansible_distribution == "RedHat" and ansible_distribution_major_version == "7" )
  or
  ( ansible_distribution == "Fedora" and ansible_distribution_major_version == "28")

一个完整的例子:

# 判断register注册变量的返回结果
- name: restart httpd if postfix is running
  hosts: test
  tasks:
    - name: get postfix server status
      command: /usr/bin/systemctl is-active postfix
      ignore_errors: yes
      register: result
      
    - name: restart apache httpd based on postfix status
      service:
        name: httpd
        state: restarted
      when: result.rc == 0

11.2、条件判断与tests

在shell当中,我们可使用test命令来进行一些常用的判断操作,如下:

# 判断/test文件是否存在
test -e /test

# 判断/testdir是否存在且为一个目录
test -d /testdir

事实上,在ansible中也有类似的用法,只不过ansible没有使用linux的test命令,而是jinja2模板的tests。

下面是一个简单示例:

# 通过条件语句判断testpath的路径是否存在
- hosts: test
  vars:
    testpath: /testdir
  tasks:
    - debug:
        msg: "file exist"
      when: testpath is exists

上面的示例中,我们使用了is exists用于路径存在时返回真,也可以使用is not exists用于路径不存在时返回真。也可以在整个条件表达式的前面使用not以取反:

- hosts: test
  vars:
    testpath: /testdir1
  tasks:
    - debug:
        msg: "file not exist"
      when: not testpath is exists

在ansible中,除了能够使用exists这种tests之外,还有一些别的tests。接下来我们详细说一说。

11.2.1、判断变量

  • defined:判断变量是否已定义,已定义则返回真
  • undefined:判断变量是否未定义,未定义则返回真
  • none:判断变量的值是否为空,如果变量已定义且值为空,则返回真

示例:

- hosts: test
  gather_facts: no
  vars:
    testvar: "test"
    testvar1:
  tasks:
    - debug:
        msg: "testvar is defined"
      when: testvar is defined
    - debug:
        msg: "testvar2 is undefined"
      when: testvar2 is undefined
    - debug:
        msg: "testvar1 is none"
      when: testvar1 is none

11.2.2、判断执行结果

  • sucess或succeeded:通过任务执行结果返回的信息判断任务的执行状态,任务执行成功则返回true
  • failure或failed:任务执行失败则返回true
  • change或changed:任务执行状态为changed则返回true
  • skip或skipped:任务被跳过则返回true

示例:

- hosts: test
  gather_facts: no
  vars:
    doshell: true
  tasks:
    - shell: 'cat /testdir/aaa'
      when: doshell
      register: result
      ignore_errors: true
    - debug:
        msg: "success"
      when: result is success
      
    - debug:
        msg: "failed"
      when: result is failure
      
    - debug:
        msg: "changed"
      when: result is change
      
    - debug:
        msg: "skip"
      when: result is skip

11.2.3、判断路径

  • file:判断指定路径是否为一个文件,是则为真
  • directory:判断指定路径是否为一个目录,是则为真
  • link:判断指定路径是否为一个软链接,是则为真
  • mount:判断指定路径是否为一个挂载点,是则为真
  • exists:判断指定路径是否存在,存在则为真

特别注意:关于路径的所有判断均是判断主控端上的路径,而非被控端上的路径

示例:

- hosts: test
  gather_facts: no
  vars:
    testpath1: "/testdir/test"
    testpath2: "/testdir"
  tasks:
    - debug:
        msg: "file"
      when: testpath1 is file
    - debug:
        msg: "directory"
      when: testpath2 is directory

11.2.4、判断字符串

  • lower:判断字符串中的所有字母是否都是小写,是则为真
  • upper:判断字符串中的所有字母是否都是大写,是则为真
- hosts: test
  gather_facts: no
  vars: 
    str1: "abc"
    str2: "ABC"
  tasks:
    - debug:
        msg: "str1 is all lowercase"
      when: str1 is lower
    - debug:
        msg: "str2 is all uppercase"
      when: str2 is upper

11.2.5、判断整除

  • even:判断数值是否为偶数,是则为真
  • odd:判断数值是否为奇数,是则为真
  • divisibleby(num):判断是否可以整除指定的数值,是则为真

示例:

- hosts: test
  gather_facts: no
  vars: 
    num1: 6
    num2: 8 
    num3: 15
  tasks:
    - debug: 
        msg: "num1 is an even number"
      when: num1 is even
    - debug:
        msg: "num2 is an odd number"
      when: num2 is odd
    - debug:
        msg: "num3 can be divided exactly by"
      when: num3 is divisibleby(3)

11.2.6、其他tests

  1. version可用于对比两个版本号的大小,或者与指定的版本号进行对比,使用语法为version("版本号","比较操作符")
- hosts: test
  vars:
    ver1: 1.2
    ver2: 1.3
  tasks:
    - debug:
        msg: "ver1 is greater than ver2"
      when: ver1 is version(ver2,">")
    - debug:
        msg: "system version {{ ansible_distribution_version }} greater than 7.3"
      when: ansible_distribution_version is version("7.3","gt")
version中使用的比较运算符说明:

- 大于: >, gt
- 大于等于: >=, ge
- 小于: <, lt
- 小于等于: <=, le
- 等于: =, ==, eq
- 不等于: !=, <>, ne
  1. subset判断一个list是不是另一个list的子集
  2. superset判断一个list是不是另一个list的父集"
- hosts: test
  gather_facts: no
  vars:
    a:
      - 2
      - 5
    b: [1,2,3,4,5]
  tasks:
    - debug:
        msg: "A is a subset of B"
      when: a is subset(b)
    - debug:
        msg: "B is the parent set of A"
      when: b is superset(a)
  1. in判断一个字符串是否存在于另一个字符串中,也可用于判断某个特定的值是否存在于列表中
- hosts: test
  vars:
    supported_distros:
      - RedHat
      - CentOS
  tasks:
    - debug:
        msg: "{{ ansible_distribution }} in supported_distros"
      when: ansible_distribution in supported_distros
  1. string判断对象是否为一个字符串,是则为真
  2. number判断对象是否为一个数字,是则为真
- hosts: test
  gather_facts: no
  vars:
    var1: 1
    var2: "1"
    var3: a
  tasks:
    - debug:
        msg: "var1 is a number"
      when: var1 is number
    - debug:
        msg: "var2 is a string"
      when: var2 is string
    - debug:
        msg: "var3 is a string"
      when: var3 is string

11.3、条件判断与block

11.3.1、block

我们在前面使用when做条件判断时,如果条件成立则执行对应的任务。但这就面临一个问题,当我们要使用同一个条件判断执行多个任务的时候,就意味着我们要在某一个任务下面都写一下when语句,而且判断条件完全一样。这种方式不仅麻烦而且显得low。Ansible提供了一种更好的方式来解决这个问题,即block。

在ansible中,使用block将多个任务进行组合,当作一个整体。我们可以对这一个整体做条件判断,当条件成立时,则执行块中的所有任务:

- hosts: test
  tasks:
    - debug:
        msg: "task1 not in block"
    - block:
        - debug:
            msg: "task2 in block1"
        - debug:
            msg: "task3 in block1"
      when: 2 > 1

下面是一个稍微有用点儿的例子:

- hosts: test
  tasks:
    - name: set /etc/resolv.conf
      template: 
        src: resolv.conf.j2 
        dest: /etc/resolv.conf 
        owner: root 
        group: root 
        mode: 0644
    - block:
        - name: ensure /etc/resolvconf/resolv.conf.d/base file for ubuntu 16.04
          template: 
            src: resolv.conf.j2
            dest: /etc/resolvconf/resolv.conf.d/base
       
        - name: config dns for ubuntu 16.04
          template: 
            src: resolv.conf.j2
            dest: /etc/resolv.conf
      when: ansible_distribution == "Ubuntu" and ansible_distribution_major_version == "16" 

使用block注意事项:

  1. 可以为block定义name(ansible 2.3增加的特性)
  2. 可以直接对block使用when,但不能直接对block使用loop

11.3.2、rescue

block除了能和when一起使用之外,还能作错误处理。这个时候就需要用到rescue关键字:

- hosts: test
  tasks:
    - block:
        - shell: 'ls /testdir'
      rescue:
        - debug:
            msg: '/testdir is not exists'

在上面的例子中,当block中的任务执行失败时,则运行rescue中的任务。如果block中的任务正常执行,则rescue的任务就不会被执行。如果block中有多个任务,则任何一个任务执行失败,都会执行rescue。block中可以定义多个任务,同样rescue当中也可以定义多个任务。

11.3.3、always

当block执行失败时,rescue中的任务才会被执行;而无论block执行成功还是失败,always中的任务都会被执行:

- hosts: test
  tasks:
    - block:
        - shell: 'ls /testdir'
      rescue:
        - debug:
            msg: '/testdir is not exists'
      always:
        - debug:
            msg: 'This task always executes'

11.4、条件判断与错误处理

在上面讲block的使用方法的时候,我们说block除了可以将多个任务组合到一起,还有错误处理的功能。接下来我们继续说一说错误处理。

11.4.1、fail模块

在shell中,可能会有这样的需求:当脚本执行至某个阶段时,需要对某个条件进行判断,如果条件成立,则立即终止脚本的运行。在shell中,可以直接调用"exit"即可执行退出。事实上,在playbook中也有类似的模块可以做这件事。即fail模块。

fail模块用于终止当前playbook的执行,通常与条件语句组合使用,当满足条件时,终止当前play的运行。

选项只有一个:

  • msg:终止前打印出信息

示例:

# 使用fail模块中断playbook输出
- hosts: test
  tasks:
    - shell: echo "Just a test--error" 
      register: result
    - fail:
        msg: "Conditions established,Interrupt running playbook"
      when: "'error' in result.stdout"
    - debug:
        msg: "Inever execute,Because the playbook has stopped"

11.4.2、failed_when

事实上,当fail和when组合使用的时候,还有一个更简单的写法,即failed_when,当满足某个条件时,ansible主动触发失败。

# 如果在command_result存在错误输出,且错误输出中,包含了`FAILED`字串,即返回失败状态:
- name: this command prints FAILED when it fails
  command: /usr/bin/example-command -x -y -z
  register: command_result
  failed_when: "'FAILED' in command_result.stderr"

也可以直接通过fail模块和when条件语句,写成如下:

- name: this command prints FAILED when it fails
  command: /usr/bin/example-command -x -y -z
  register: command_result
  ignore_errors: True

- name: fail the play if the previous command did not succeed
  fail: msg="the command failed"
  when: "command_result.stderr and 'FAILED' in command_result.stderr"

ansible一旦执行返回失败,后续操作就会中止,所以failed_when通常可以用于满足某种条件时主动中止playbook运行的一种方式。

ansible默认处理错误的机制是遇到错误就停止执行。但有些时候,有些错误是计划之中的。我们希望忽略这些错误,以让playbook继续往下执行。这个时候就可以使用ignore_errors忽略错误,从而让playbook继续往下执行。

11.4.3、changed_when

当我们控制一些远程主机执行某些任务时,当任务在远程主机上成功执行,状态发生更改时,会返回changed状态响应,状态未发生更改时,会返回OK状态响应,当任务被跳过时,会返回skipped状态响应。我们可以通过changed_when来手动更改changed响应状态。示例如下:

- shell: /usr/bin/billybass --mode="take me to the river"
  register: bass_result
  changed_when: "bass_result.rc != 2"    #只有该条task执行以后,bass_result.rc的值不为2时,才会返回changed状态

# this will never report 'changed' status
- shell: wall 'beep'
  changed_when: False    #当changed_when为false时,该条task在执行以后,永远不会返回changed状态

11.5、在循环语句中使用条件语句

# 只打印大于5的值
tasks:
    - command: echo {{ item }}
      loop: [ 0, 2, 4, 6, 8, 10 ]
      when: item > 5
# 确保将mariadb-server安装到根分区且根分区的可用空间要大于300M
- name: install mariadb-server if enough space on root
  yum: 
    name: mariadb-server
    state: present
  loop: "{{ ansible_mounts }}"
  when: item.mount == "/" and item.size_available > 300000000

12. Ansible Playbook with_X循环语句

循环语句

简介

我们在编写playbook的时候,不可避免的要执行一些重复性操作,比如指安装软件包,批量创建用户,操作某个目录下的所有文件等。正如我们所说,ansible一门简单的自动化语言,所以流程控制、循环语句这些编程语言的基本元素它同样都具备。

在Ansible 2.5以前,playbook通过不同的循环语句以实现不同的循环,这些语句使用with_作为前缀。这些语法目前仍然兼容,但在未来的某个时间点,会逐步废弃。

下面列出一些较常见的with_X循环语句:

  • with_items
  • with_flattened
  • with_list
  • with_together
  • with_nested
  • with_indexed_items
  • with_sequence
  • with_random_choice
  • with_dict
  • with_subelement
  • with_file
  • with_fileglob
  • with_lines

12.1、with_items

item:ansible内置变量,指的是引用变量中所有的元素

简单的列表循环

12.1.1、场景一: 循环打印inventory中所有未分组的主机

- hosts: test
  gather_facts: no
  tasks:
    - debug:
        msg: "{{ item }}""
      with_items: "{{ groups.ungrouped }}"

12.1.2、场景二: 直接在with_items中定义被循环的列表

- hosts: test
  gather_facts: no
  tasks:
    - name: "with_items"
      debug:
        msg: "{{ item }}"
      with_items:
        - "user0"
        - "user1"
        - "user2"

也可以写成如下方式:

- hosts: test
  gather_facts: no
  tasks:
    - name: "with_items"
      debug:
        msg: "{{ item }}"
      with_items:["user0","user1","user2"]

12.1.3、场景三: 在with_items中定义更复杂的列表

- hosts: test
  gather_facts: no
  tasks:
    - name: "create directory"
      file:
        path: "/{{ item.path1 }}/{{ item.path2 }}"
      with_items:
        - { path1: a, path2: b}
        - { path1: c, path2: d}
      

12.2、with_list

与with_items一样,也是用于循环列表。区别是,如果列表的值也是列表,with_items会将第一层嵌套的列表拉平,而with_list会将值作为一个整体返回。

示例:

# 使用with_items的示例
- hosts: test
  gather_facts: no
  tasks:
    - name: "with_items"
      debug:
        msg: "{{ item }}"
      with_items:
        - [1, 2]
        - [a, b]

# 返回结果:
[root@controller ansible]# ansible-playbook with_items_test.yml 


PLAY [test] *****************************************************************************************************************************************************************

TASK [Gathering Facts] ******************************************************************************************************************************************************
ok: [10.1.61.187]
  
▽ASK [with_items] ***********************************************************************************************************************************************************
ok: [10.1.61.187] => (item=1) => {
    "msg": 1
}
ok: [10.1.61.187] => (item=2) => {
    "msg": 2
}
ok: [10.1.61.187] => (item=[3, 4]) => {
    "msg": [
        3,
        4
    ]
}
ok: [10.1.61.187] => (item=a) => {
    "msg": "a"
}
ok: [10.1.61.187] => (item=b) => {
    "msg": "b"
}
ok: [10.1.61.187] => (item=c) => {
    "msg": "c"
}

PLAY RECAP ******************************************************************************************************************************************************************
10.1.61.187                : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

# 使用with_list的示例
- hosts: test
  gather_facts: no
  tasks:
    - name: "with_items"
      debug:
        msg: "{{ item }}"
      with_list:
        - [1, 2]
        - [a, b]
        
# 返回结果:

[root@controller ansible]# ansible-playbook with_list_ex.yml 

PLAY [test] *****************************************************************************************************************************************************************

TASK [with_items] ***********************************************************************************************************************************************************
ok: [10.1.61.187] => (item=[1, 2]) => {
    "msg": [
        1,
        2
    ]
}
ok: [10.1.61.187] => (item=['a', 'b']) => {
    "msg": [
        "a",
        "b"
    ]
}

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


12.3、with_flattened

with_flattened与with_items类似,当处理复杂的多级列表嵌套时,会将所有的列表全部拉平:

- hosts: test
  gather_facts: no
  tasks:
    - name: "with_items"
      debug:
        msg: "{{ item }}"
      with_flattened:
        - [1, 2,[3,4]]
        - [a, b]

返回结果:

[root@controller ansible]# ansible-playbook with_flattened_ex.yml 

PLAY [test] *****************************************************************************************************************************************************************

TASK [with_items] ***********************************************************************************************************************************************************
ok: [10.1.61.187] => (item=1) => {
    "msg": 1
}
ok: [10.1.61.187] => (item=2) => {
    "msg": 2
}
ok: [10.1.61.187] => (item=3) => {
    "msg": 3
}
ok: [10.1.61.187] => (item=4) => {
    "msg": 4
}
ok: [10.1.61.187] => (item=a) => {
    "msg": "a"
}
ok: [10.1.61.187] => (item=b) => {
    "msg": "b"
}
ok: [10.1.61.187] => (item=c) => {
    "msg": "c"
}

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

12.4、with_together

with_together可以将两个列表中的元素对齐合并

示例如下:

- hosts: test
  remote_user: root
  vars:
    alpha: [ 'a','b']
    numbers: [ 1,2]
  tasks:
    - debug: msg="{{ item.0 }} and {{ item.1 }}"
      with_together:
         - "{{ alpha }}"
         - "{{ numbers }}"
         
         
# 输出的结果为:
ok: [10.1.61.187] => (item=['a', 1]) => {
    "item": [
        "a",
        1
    ],
    "msg": "a and 1"
}
ok: [10.1.61.187] => (item=['b', 2]) => {
    "item": [
        "b",
        2
    ],
    "msg": "b and 2"
}

可以看到第一个列表中的第一个元素a与第二个列表中的第一个元素1合并输出,第一个列表中的b与第二个列表中的第二个元素2合并输出了

上面的示例是基于两个列表的元素完全相同的结果,如果两个列表中的元素不同:

- hosts: test
  remote_user: root
  vars:
    alpha: [ 'a','b','c']
    numbers: [ 1,2]
  tasks:
    - debug: msg="{{ item.0 }} and {{ item.1 }}"
      with_together:
         - "{{ alpha }}"
         - "{{ numbers }}"


# 输出结果:

[root@controller ansible]# ansible-playbook with_together_ex.yml

PLAY [test] *****************************************************************************************************************************************************************

TASK [debug] ****************************************************************************************************************************************************************
ok: [10.1.61.187] => (item=['a', 1]) => {
    "msg": "a and 1"
}
ok: [10.1.61.187] => (item=['b', 2]) => {
    "msg": "b and 2"
}
ok: [10.1.61.187] => (item=['c', None]) => {
    "msg": "c and "
}

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

12.5、with_nested

嵌套循环

tasks: 
  - name: debug loops
    debug: msg="name is {{ item[0] }}  vaule is {{ item[1] }} "
    with_nested:
      - ['alice','bob']
      - ['a','b','c']


item[0]是循环的第一个列表的值['alice','bob']。item[1]是第二个列表的值;以上的执行输出如下:

[root@controller ansible]# ansible-playbook with_nested_ex.yml 

PLAY [with_nested test] ********************************************************************************************************************

TASK [debug loops] *************************************************************************************************************************
ok: [10.1.61.187] => (item=['alice', 'a']) => {
    "msg": "name is alice  vaule is a"
}
ok: [10.1.61.187] => (item=['alice', 'b']) => {
    "msg": "name is alice  vaule is b"
}
ok: [10.1.61.187] => (item=['alice', 'c']) => {
    "msg": "name is alice  vaule is c"
}
ok: [10.1.61.187] => (item=['bob', 'a']) => {
    "msg": "name is bob  vaule is a"
}
ok: [10.1.61.187] => (item=['bob', 'b']) => {
    "msg": "name is bob  vaule is b"
}
ok: [10.1.61.187] => (item=['bob', 'c']) => {
    "msg": "name is bob  vaule is c"
}

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

下面是一个稍微有用点儿的示例:

- hosts: test
  gather_facts: false
  tasks:
    - file: 
        state: directory
        path: "/data/{{ item[0] }}/{{ item[1] }}"
      with_nested:
        - [test1,test2]
        - [a,b,c]

with_cartesian与其功能完全一致

12.6、with_indexed_items

在循环处理列表时,为列表中的每一项添加索引(从0开始的数字索引)

简单示例:

- hosts: test
  gather_facts: false
  tasks:
    - debug:
        msg: "{{ item }}"
      with_indexed_items:
        - test1
        - test2
        - test3

执行之后,返回结果如下:

[root@controller ansible]# ansible-playbook with_indexed_items_ex.yml

PLAY [test] ********************************************************************************************************************************

TASK [debug] *******************************************************************************************************************************
ok: [10.1.61.187] => (item=[0, 'test1']) => {
    "msg": [
        0,
        "test1"
    ]
}
ok: [10.1.61.187] => (item=[1, 'test2']) => {
    "msg": [
        1,
        "test2"
    ]
}
ok: [10.1.61.187] => (item=[2, 'test3']) => {
    "msg": [
        2,
        "test3"
    ]
}

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

所以我们可以使用with_indexed_items执行如下操作:

- hosts: test
  gather_facts: false
  tasks:
    - debug:
        msg: "index is {{ item[0] }}, value is {{ item[1] }}"
      with_indexed_items:
        - test1
        - test2
        - test3

下面再看一个稍微复杂的列表结构:

- hosts: test
  gather_facts: false
  tasks:
    - debug:
        msg: "index is {{ item[0] }}, value is {{ item[1] }}"
      with_indexed_items:
        - test1
        - [test2,test3]
        - [test4,test5]

这个时候,返回的结果如下:

# ansible-playbook with_indexed_items_ex2.yml

PLAY [test] ********************************************************************************************************************************

TASK [debug] *******************************************************************************************************************************
ok: [10.1.61.187] => (item=[0, 'test1']) => {
    "msg": "index is 0, value is test1"
}
ok: [10.1.61.187] => (item=[1, 'test2']) => {
    "msg": "index is 1, value is test2"
}
ok: [10.1.61.187] => (item=[2, 'test3']) => {
    "msg": "index is 2, value is test3"
}
ok: [10.1.61.187] => (item=[3, 'test4']) => {
    "msg": "index is 3, value is test4"
}
ok: [10.1.61.187] => (item=[4, 'test5']) => {
    "msg": "index is 4, value is test5"
}

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

可以看到,其在处理更复杂列表的时候,会将列表拉平,类似于with_items。

与with_items一样,其也只会拉平第一层列表,如果存在多层列表嵌套,则更深的嵌套不会被拉平:

- hosts: test
  gather_facts: false
  tasks:
    - debug:
        msg: "index is {{ item[0] }}, value is {{ item[1] }}"
      with_indexed_items:
        - test1
        - [test2,[test3,test4]]

此时的返回结果:

[root@controller ansible]# ansible-playbook with_indexed_items_ex3.yml 

PLAY [test] ********************************************************************************************************************************

TASK [debug] *******************************************************************************************************************************
ok: [10.1.61.187] => (item=[0, 'test1']) => {
    "msg": "index is 0, value is test1"
}
ok: [10.1.61.187] => (item=[1, 'test2']) => {
    "msg": "index is 1, value is test2"
}
ok: [10.1.61.187] => (item=[2, ['test3', 'test4']]) => {
    "msg": "index is 2, value is ['test3', 'test4']"
}

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

12.7、with_sequence

用于返回一个数字序列

参数说明:

  • start:指定起始值
  • end:指定结束值
  • stride:指定步长,即从start至end,每次增加的值
  • count:生成连续的数字序列,从1开始,到count的值结束
  • format:格式化输出,类似于linux命令行中的printf格式化输出

关于format参数,更多的格式化输出参数可参考:12printf命令详解-朱双印博客

- hosts: test
  gather_facts: false
  tasks:
    # create groups
    - group: 
        name: {{ item }} 
        state: present
      with_items:
        - evens
        - odds

    
    # create some test users
    # [testuser00,testuser01,testuser02,...,testuser32]
    - user: 
        name: {{ item }} 
        state: present 
        groups: evens
      with_sequence: 
        start: 0 
        end: 32 
        stride: 4
        format=testuser%02d
      
    # create a series of directories with even numbers for some reason
    # [4,6,8,10,...,16]
    - file: 
        dest: /var/stuff/{{ item }} 
        state: directory
      with_sequence: 
          start=4 
          end=16 
          stride=2
    
    # a simpler way to use the sequence plugin
    # create 4 groups
    - group: 
        name: group{{ item }} 
        state: present
      with_sequence: count=4

12.8、with_random_choice

用于从一个列表的多个值中随机返回一个值

下面的示例,一个列表当中有四个值,连续执行playbook,每次都随机返回一个:

- hosts: test
  gather_facts: false
  tasks:
    - debug: msg={{ item }}
      with_random_choice:
         - "go through the door"
         - "drink from the goblet"
         - "press the red button"
         - "do nothing"

12.9、with_dict

循环字典

- hosts: test
  gather_facts: no
  vars:
    # 假如有如下变量内容:
    users:
      alice:
        name: Alice Appleworth
        telephone: 123-456-7890
      bob:
        name: Bob Bananarama
        telephone: 987-654-3210

  # 现在需要输出每个用户的用户名和手机号:
  tasks:
    - name: Print phone records
      debug: 
        msg: "User {{ item.key }} is {{ item.value.name }} ({{ item.value.telephone }})"
      with_dict: "{{ users }}"

输出如下:

[root@controller ansible]# ansible-playbook  with_dict_ex.yml 

PLAY [test] ******************************************************************************************************************************************************************************************************

TASK [Print phone records] ***************************************************************************************************************************************************************************************
ok: [10.1.61.187] => (item={'key': 'alice', 'value': {'name': 'Alice Appleworth', 'telephone': '123-456-7890'}}) => {
    "msg": "User alice is Alice Appleworth (123-456-7890)"
}
ok: [10.1.61.187] => (item={'key': 'bob', 'value': {'name': 'Bob Bananarama', 'telephone': '987-654-3210'}}) => {
    "msg": "User bob is Bob Bananarama (987-654-3210)"
}

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

12.10、with_subelement

with_subelement简单来讲,就是在一个复杂的列表当中,可以对这个列表变量的子元素进行遍历

下面是一个简单的示例:

- hosts: test
  gather_facts: no
  vars:
    users:
      - name: bob
        hobby: 
          - Games
          - Sports
      - name: alice
        hobby:
          - Music
  tasks:
    - debug:
        msg: "{{ item }}"
      with_subelement:
        - "{{ users }}"
        - hobby

输出结果如下:

[root@controller ansible]# ansible-playbook with_subelement_ex.yml 

PLAY [test] *****************************************************************************************************************************************************************

TASK [debug] ****************************************************************************************************************************************************************
ok: [10.1.61.187] => (item=[{'name': 'bob'}, 'Games']) => {
    "msg": [
        {
            "name": "bob"
        },
        "Games"
    ]
}
ok: [10.1.61.187] => (item=[{'name': 'bob'}, 'Sports']) => {
    "msg": [
        {
            "name": "bob"
        },
        "Sports"
    ]
}
ok: [10.1.61.187] => (item=[{'name': 'alice'}, 'Music']) => {
    "msg": [
        {
            "name": "alice"
        },
        "Music"
    ]
}

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

可以看到,其按照我们指定的变量users的子项hobby进行了组合输出。with_elementes将hobby子元素的每一项作为一个整体,将其他子元素作为整体,然后组合在一起。

假如现在需要遍历一个用户列表,并创建每个用户,而且还需要为每个用户推送特定的SSH公钥以用于实现远程登录。同时为某一个用户创建独立的mysql登录帐号并为其授权。

示例如下:

- hosts: test
  gather_facts: false
  vars:
    users:
      - name: alice
        authorized:
          - files/keys/master1.id_rsa.pub
          - files/keys/master2.id_rsa.pub
        mysql:
            password: mysql-password
            hosts:
              - "%"
              - "127.0.0.1"
              - "::1"
              - "localhost"
            privs:
              - "*.*:SELECT"
              - "DB1.*:ALL"
      - name: bob
        authorized:
          - files/keys/master3.id_rsa.pub
        mysql:
            password: other-mysql-password
            hosts:
              - "db1"
            privs:
              - "*.*:SELECT"
              - "DB2.*:ALL"
  tasks:
    - user: 
        name: "{{ item.name }}" 
        state: present 
        generate_ssh_key: yes
      with_items: "{{ users }}"
    
    - authorized_key: 
        user: "{{ item.0.name }}" 
        key: "{{ lookup('file', item.1) }}"
      with_subelements:
        - "{{ users }}"
        - authorized

    - name: Setup MySQL users
      mysql_user: 
        name: "{{ item.0.name }}" 
        password: "{{ item.0.mysql.password }}"" 
        host: "{{ item.1 }} priv={{ item.0.mysql.privs | join('/') }}"
      with_subelements:
        - "{{ users }"
        - mysql.hosts

12.11、with_file

用于循环主控端的文件列表,获取文件中的内容

注意: 循环的是主控端的文件列表,不是被控端的

- hosts: test
  gather_facts: false
  tasks:
    - debug:
        msg: {{ item }}
      with_file:
        - /etc/ansible/test1.yml
        - /etc/ansible/test2.yml

输出如下:

[root@controller ansible]# ansible-playbook with_file_ex.yml 

PLAY [test] ********************************************************************************************************************************

TASK [debug] *******************************************************************************************************************************
ok: [10.1.61.187] => (item=content: test1.yaml) => {
    "msg": "content: test1.yaml"
}
ok: [10.1.61.187] => (item=content: test2.yml) => {
    "msg": "content: test2.yml"
}

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

12.12、with_fileglob

上面with_file用于获取文件的内容,而with_fileglob则用于匹配文件名称。可以通过该关键字,在指定的目录中匹配符合模式的文件名。与with_file相同的是,**with_fileglob**操作的文件也是主控端的文件而非被控端的文件

- hosts: test
  tasks:
    - name: Make key directory     
      file: 
        path: /root/.sshkeys 
        state: directory 
        mode: 0700 
        owner: root 
        group: root 
        
    - name: Upload public keys     
      copy: 
        src: "{{ item }}"
        dest: /root/.sshkeys
        mode: 0600 
        owner: root 
        group: root  
      with_fileglob:
        - /root/.ssh/*.pub 
        
    - name: Assemble keys into authorized_keys file     
      assemble: 
        src: /root/.sshkeys 
        dest: /root/.ssh/authorized_keys
        mode: 0600 
        owner: root 
        group: root

12.13、with_lines

with_lines循环结构会让你在控制主机上执行任意命令,并对命令的输出进行逐行迭代。

假设我们有一个文件test.txt包含如下行:

Breeze Yan
Bernie Yang
jerry Qing

我们可以通过如下方法进行逐行输出:

- name: print all names
  debug: msg="{{ item }}"
  with_lines:
    - cat test.txt

12.14、do-Until循环

- action: shell /usr/bin/foo
  register: result
  until: result.stdout.find("all systems go") != -1
  retries: 5
  delay: 10

重复执行shell模块,当shell模块执行的命令输出内容包含"all systems go"的时候停止。重试5次,延迟时间10秒。retries默认值为3,delay默认值为5。任务的返回值为最后一次循环的返回结果。

13. Ansible Playbook loop循环语句

loop关键字说明

在ansible 2.5及以前的版本当中,所有的循环都是使用with_X风格。但是从2.6版本开始,官方开始推荐使用loop关键字来代替with_X风格的关键字。

在playbook中使用循环,直接使用loop关键字即可。

如下示例,启动httpd和postfix服务:

tasks:
  - name: postfix and httpd are running
    service:
      name: "{{ item }}"
      state: started
    loop:
      - postfix
      - httpd

那么在这个示例当中,其实就是使用loop代替了with_list循环。

事实上,我们可以使用loop关键字搭配一些过滤器来替换更多的、更复杂的with_X循环。

13.1、loop_control

loop_control用于在循环时,获取列表的索引

- hosts: test
  gather_facts: no
  vars:
    testlist:
      - a
      - [b,c,[e,f]]
      - d
  tasks:
    - debug:
        msg: "{{ index }}: {{ item }}"
      loop: "{{ testlist | flatten(levels=1) }}"
      loop_control:
        index_var: index

输出结果:

PLAY [localhost] ******************************************************************

TASK [debug] **********************************************************************
ok: [localhost] => (item=a) => {
    "msg": "0: a"
}
ok: [localhost] => (item=b) => {
    "msg": "1: b"
}
ok: [localhost] => (item=c) => {
    "msg": "2: c"
}
ok: [localhost] => (item=[u'e', u'f']) => {
    "msg": "3: [u'e', u'f']"
}
ok: [localhost] => (item=d) => {
    "msg": "4: d"
}

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

参数说明:

  • index_var:loop_control的选项,让我们指定一个变量,loop_control会将列表元素的索引值存放到这个指定的变量中

13.2、with_list

loop可以替代with_list,当处理嵌套列表时,列表不会被拉平

- hosts: test
  gather_facts: no
  vars:
    testlist:
      - a
      - [b,c]
      - d
  tasks:
    - debug:
        msg: "{{ item }}"
      loop: "{{ testlist }}"

13.3、with_flattened

将所有嵌套都拉平

- hosts: test
  gather_facts: no
  vars:
    testlist:
      - a
      - [b,c]
      - d
  tasks:
    - debug:
        msg: "{{ item }}"
      loop: "{{ testlist| flatten }}"
      

13.4、with_items

只拉平第一层

- hosts: test
  gather_facts: no
  vars:
    testlist:
      - a
      - [b,c]
      - d
  tasks:
    - debug:
        msg: "{{ item }}"
      loop: "{{ testlist| flatten(levels=1) }}"

13.5、with_indexed_items

通过flatten过滤器(加参数),再配合loop_control关键字,可以替代with_indexed_items,当处理多层嵌套的列表时,只有列表中的第一层会被拉平

- hosts: test
  gather_facts: no
  vars:
    testlist:
      - a
      - [b,c]
      - d
  tasks:
    - debug:
        msg: "{{ index }}: {{ item }}"
      loop: "{{ testlist | flatten(levels=1) }}"
      loop_control:
        index_var: index

参数说明:

  • loop_control: 用于控制循环的行为,比如在循环时获取到元素的索引
  • index_var:loop_control的选项,让我们指定一个变量,loop_control会将列表元素的索引值存放到这个指定的变量中

13.6、with_together

zip_longest过滤器配合list过滤器,可以替代with_together

- hosts: test
  gather_facts: no
  vars: 
    testlist1: [a,b]
    testlist2: [1,2,3]
    testlist3: [A,B,C,D]
  tasks:
    - debug:
        msg: "{{ item.0 }} -- {{ item.1 }} -- {{ item.2 }}"
      #with_together:
      #  - "{{ testlist1 }}"
      #  - "{{ testlist2 }}"
      #  - "{{ testlist3 }}"
    - debug:
        # [a,1,A],[b,2,B],['',3,C],['','',D]
        # [a,1,A],[b,2,B]
        msg: "{{ item.0 }} -- {{ item.1 }} -- {{ item.2 }}"
      loop: "{{ testlist1 | zip_longest(testlist2,testlist3) | list}}"

当多个列表使用with_together进行对齐合并时,如果多个列表的长度不同,则使用最长的列表进行对齐,由于短列表中的元素数量不够,所以使用空值与长列表中的元素进行对齐,zip_longest过滤器也会像with_together一样,对列表进行组合,但是还需要借助list过滤器,将组合后的数据列表化。

在使用zip_longest过滤器代替with_together关键字时,默认也是使用空值与长列表中的元素进行对齐,但是也可以指定其他的字符串代替空值,如下示例即使用"NONE"代替空值:

- debug:
    msg: "{{ item.0 }} - {{ item.1 }} - {{ item.2 }}"
  loop: {{ testlist1 | zip_longest(testlist2,testlist3,filevalue='NONE') | list }}

zip_longest默认使用最长的列表长度进行对齐,当有多个列表的长度不同时,如果希望使用最短的列表对齐,则可以使用zip过滤器:

- debug:
    msg: "{{ item.0 }} - {{ item.1 }} - {{ item.2 }}"
  loop: {{ testlist1 | zip(testlist2,testlist3) | list }}

13.7、with_nested/with_cartesian

可使用product过滤器配合list过滤器以替代with_nested或者with_cartesian。product过滤器也是需要将组合后的数据进行列表化,所以需要与list过滤器配合使用:

- hosts: test
  gather_facts: no
  vars: 
    testlist1: [a,b,c]
    testlist2: [1,2,3,4]
  tasks:
    - debug:
        msg: "{{item.0}} --- { item.1}"
      loop: {{ testlist1 | product(testlist2) | list}}

13.8、with_sequence

使用range过滤器配合list过滤器可以替代with_sequence:

- hosts: test
  gather_facts: no
  tasks:
    - debug:
        msg: "{{ item }}"
      loop: "{{ range(0,6,2) | list }}"

上例中表示生成数字,从0开始,到6结束,步长为2。但是需要说明的是,range函数的操作不包含结束范围,也就是说上面的循环只会生成0,2,4三个数字,而不包含6。

另外,with_sequence还有格式化的功能:

- debug:
    msg: "{{ item }}"
  with_sequence: start=2 end=6 stride=2 format="number is %0.2f"

可使用format配合loop实现:

- debug:
    msg: "{{ 'number is %0.2f' |format(item) }}"
  loop: "{{range(2,7,2) |list}}"

13.9、with_random_choice

使用random函数可以替代with_random_choice,由于random函数是随机取出列表中的一个值,并不涉及循环操作,所以并不使用loop关键字:

- hosts: test
  gather_facts: no
  vars:
    testlist: [a,b,c]
  tasks:
    - debug:
        msg: "{{ testlist | random }}"

13.10、with_dict

可使用loop配合dict2items过滤器实现with_dict功能:

- hosts: test
  gather_facts: no
  vars:
    users:
      alice: female
      bob: male
  tasks:
    - debug:
        msg: "{{ item.key }} is {{ item.value}}"
      loop: "{{users | dict2items }}"

13.11、with_subelements

可使用loop配合subelements过滤器替代with_subelements:

- hosts: test
  gather_facts: no
  vars:
    users:
      - name: bob
        gender: male
        hobby:
          - Skateboard
          - VideoGame
      - name: alice
        gender: female
        hobby:
          - Music
  tasks:
    - debug:
        msg: "{{ item.0.name }}'s hobby is {{ item.1}}"
      with_subelements:
        - "{{ users }}"
        - hobby
    - debug:
        msg: "{{ item.0.name }}'s hobby is {{ item.1}}"
      loop: "{{ users | subelements('hobby') }}"

13.12、使用zip_longest过滤器将两个列表中的元素对齐合并

- hosts: localhost
  gather_facts: no
  vars: 
    testlist1: [a,b]    
    testlist2: [1,2,3]
    testlist3: [A,B,C,D]
  tasks:
    - debug:
        msg: "['{{ item.0 }}','{{ item.1 }}' ,'{{ item.2 }}']" # a,1,A   b,2,B '',3,c, '','',D
      loop: "{{ testlist1 | zip_longest(testlist2,testlist3) | list}}"
      

输出结果如下:

TASK [debug] **********************************************************************
ok: [localhost] => (item=[u'a', 1, u'A']) => {
    "msg": [
        "a", 
        "1", 
        "A"
    ]
}
ok: [localhost] => (item=[u'b', 2, u'B']) => {
    "msg": [
        "b", 
        "2", 
        "B"
    ]
}
ok: [localhost] => (item=[None, 3, u'C']) => {
    "msg": [
        "", 
        "3", 
        "C"
    ]
}
ok: [localhost] => (item=[None, None, u'D']) => {
    "msg": [
        "", 
        "", 
        "D"
    ]
}

当多个列表进行对齐合并时,如果多个列表的长度不同,则使用最长的列表进行对齐,由于短列表中的元素数量不够,所以使用空值与长列表中的元素进行对齐,zip_longest过滤器会对列表进行组合,但是还需要借助list过滤器,将组合后的数据列表化。

在使用zip_longest过滤器时,默认使用空值与长列表中的元素进行对齐,但是也可以指定其他的字符串代替空值,如下示例即使用"NONE"代替空值:

- debug:
    msg: "{{ item.0 }} - {{ item.1 }} - {{ item.2 }}"
  loop: {{ testlist1 | zip_longest(testlist2,testlist3,filevalue='NONE') | list }}

zip_longest默认使用最长的列表长度进行对齐,当有多个列表的长度不同时,如果希望使用最短的列表对齐,则可以使用zip过滤器:

- debug:
    msg: "{{ item.0 }} - {{ item.1 }} - {{ item.2 }}"   # a,1,A b,2,B 
  loop: {{ testlist1 | zip(testlist2,testlist3) | list }}

13.13、在循环语句中注册变量

下面是一个register的变量在循环中使用的例子:

[root@controller ansible]# cat register_loop.yml 
- name: registered variable usage as a loop list
  hosts: test
  tasks:
      - name: ensure /mnt/bkspool exists
        file:
          path: /mnt/bkspool
          state: directory
          
      - name: retrieve the list of home directories
        command: ls /home
        register: home_dirs
        
      - name: Show home_dirs results
        debug:
          var: home_dirs.stdout_lines
          
      - name: add home dirs to the backup spooler
        file: 
          path: /mnt/bkspool/{{ item }}
          src: /home/{{ item }}
          state: link
          force: yes
        loop: "{{ home_dirs.stdout_lines }}"

在循环语句中注册变量:

- name: Loop Register test
  gather_facts: no
  hosts: test
  tasks:
    - name: Looping Echo Task
      shell: "echo this is my item: {{ item }}"
      loop:
        - one
        - two
      register: echo_results
      
    - name: Show echo_results variable
      debug:
        var: echo_results

执行语句,可以看到变量的返回结果为一个字典列表:

ok: [10.1.61.187] => {
    "echo_results": {
        "changed": true,
        "msg": "All items completed",
        "results": [
            {
                "ansible_loop_var": "item",
                "changed": true,
                "cmd": "echo this is my item: one",
                "delta": "0:00:00.004905",
                "end": "2019-06-10 00:23:51.814151",
                "failed": false,
                "invocation": {
                    "module_args": {
                        "_raw_params": "echo this is my item: one",
                        "_uses_shell": true,
                        "argv": null,
                        "chdir": null,
                        "creates": null,
                        "executable": null,
                        "removes": null,
                        "stdin": null,
                        "stdin_add_newline": true,
                        "strip_empty_ends": true,
                        "warn": true
                    }
                },
                "item": "one",
                "rc": 0,
                "start": "2019-06-10 00:23:51.809246",
                "stderr": "",
                "stderr_lines": [],
                "stdout": "this is my item: one",
                "stdout_lines": [
                    "this is my item: one"
                ]
            },
            {
                "ansible_loop_var": "item",
                "changed": true,
                "cmd": "echo this is my item: two",
                "delta": "0:00:00.004736",
                "end": "2019-06-10 00:23:52.008981",
                "failed": false,
                "invocation": {
                    "module_args": {
                        "_raw_params": "echo this is my item: two",
                        "_uses_shell": true,
                        "argv": null,
                        "chdir": null,
                        "creates": null,
                        "executable": null,
                        "removes": null,
                        "stdin": null,
                        "stdin_add_newline": true,
                        "strip_empty_ends": true,
                        "warn": true
                    }
                },
                "item": "two",
                "rc": 0,
                "start": "2019-06-10 00:23:52.004245",
                "stderr": "",
                "stderr_lines": [],
                "stdout": "this is my item: two",
                "stdout_lines": [
                    "this is my item: two"
                ]
            }
        ]
    }
}

14. Ansible文件管理模块及Jinja2过滤器

对于任何自动管理工具而言,对于文件的管理都是其绕不开的话题。同样,ansible也围绕文件管理提供了众多的模块。同时还提供了Jinja2模板语法来配置文件模板。

14.1、常用文件管理模块

14.1.1、file

我们在讲ansible ad-hoc的时候,已经说过file模块,在playbook中的使用也没什么不同,下面给个简单的示例:

- name: Touch a file and set permissions
  file:
    path: /path/to/file
    owner: user1
    group: group1
    mode: 0640
    state: touch

14.1.2、synchronize

synchronize模块示例:

- name: synchronize local file to remote files
  synchronize:
    src: file
    dest: /path/to/file

14.1.3、copy

同样的,我们已经介绍过copy模块,示例如下:

- name: copy a file to managed hosts
  copy:
    src: file
    dest: /path/to/file

14.1.4、fetch

fetch模块与copy模块正好相反,copy是把主控端的文件复制到被控端,而fetch则是把被控端的文件复制到主控端。并且在主控端指定的目录下,以被控端主机名的形式来组织目录结构。

- name: Use the fetch module to retrieve secure log files
  hosts: all
  user: ansible
  tasks:
    - name: Fetch the /var/log/secure log file from managed hosts
      fetch:
        src: /var/log/secure
        dest: secure-backups
        flat: no

在主控端文件存储的目录树如下:

# tree  secure-backups/
secure-backups/
└── 10.1.61.187
    └── var
        └── log
            └── secure

3 directories, 1 file

参考:https://docs.ansible.com/ansible/latest/modules/fetch_module.html#fetch-module

14.1.5、lineinfile

lineinfile是一个非常有用的模块,而且相对来说,也是用法比较复杂的模块,可直接参考《Ansible lineinfile模块》

14.1.6、stat

stat模块与linux中的stat命令一样,用来显示文件的状态信息。

- name: Verify the checksum of a file
  stat:
    path: /path/to/file
    checksum_algorithm: md5
  register: result
  
- debug:
    msg: "The checksum of the file is {{ result.stat.checksum }}"

参考: https://docs.ansible.com/ansible/latest/modules/stat_module.html#stat-module

14.1.7、blockinfile

围绕着被标记的行插入、更新、删除一个文本块。

[root@controller ansible]# cat files/test.html
<html>
  <head>
  </head>
  <body>
  </body>
</html>

[root@controller ansible]# cat blockinfile_ex.yml
---
- name: blockinfile module test
  hosts: test
  tasks:
    - name: copy test.html to dest
      copy:
        src: files/test.html
        dest: /var/www/html/test.html
    - name: add block 
      blockinfile:
        marker: "<!-- {mark} ANSIBLE MANAGED BLOCK -->"
        insertafter: "<body>"
        path: /var/www/html/test.html
        block: |
          <h1>Welcome to {{ ansible_hostname }}</h1>
          <p>Last updated on {{ ansible_date_time.iso8601 }}</p>

执行后结果如下:

[root@controller ansible]# cat test.html 
<html>
  <head>
  </head>
  <body>
<!-- BEGIN ANSIBLE MANAGED BLOCK -->
<h1>Welcome to app</h1>
<p>Last updated on 2019-05-28T15:00:03Z</p>
<!-- END ANSIBLE MANAGED BLOCK -->
  </body>
</html>

更多blockinfile用法参考:ansible.builtin.blockinfile module – Insert/update/remove a text block surrounded by marker lines — Ansible Documentation

14.2、Jinja2模板管理

Jinja2简介

Jinja2是基于python的模板引擎。那么什么是模板?

假设说现在我们需要一次性在10台主机上安装redis,这个通过playbook现在已经很容易实现。默认情况下,所有的redis安装完成之后,我们可以统一为其分发配置文件。这个时候就面临一个问题,这些redis需要监听的地址各不相同,我们也不可能为每一个redis单独写一个配置文件。因为这些配置文件中,绝大部分的配置其实都是相同的。这个时候最好的方式其实就是用一个通用的配置文件来解决所有的问题。将所有需要修改的地方使用变量替换,如下示例中redis.conf.j2文件:

daemonize yes
supervised systemd
pidfile /var/run/redis.pid
port 6379
logfile "/var/log/redis/redis.log"
dbfilename dump.rdb
dir /data/redis

maxmemory {{ (ansible_memtotal_mb /2) | int }}M

bind {{ ansible_eth0.ipv4.address }} 127.0.0.1

timeout 300
loglevel notice

databases 16
save 900 1
save 300 10
save 60 10000

rdbcompression yes

maxclients 10000
appendonly yes
appendfilename appendonly.aof
appendfsync everysec

那么此时,redis.conf.j2文件就是一个模板文件。{{ ansible_eth0.ipv4.address }}是一个fact变量,用于获取被控端ip地址以实现替换。

14.2.1、在playbook中使用jinja2

现在我们有了一个模板文件,那么在playbook中如何来使用呢?

playbook使用template模块来实现模板文件的分发,其用法与copy模块基本相同,唯一的区别是,copy模块会将原文件原封不动的复制到被控端,而template会将原文件复制到被控端,并且使用变量的值将文件中的变量替换以生成完整的配置文件。

下面是一个完整的示例:

# cat config_redis.yml 
- name: Configure Redis
  hosts: test
  tasks:
    - name: create redis group
      group:
        name: redis
        gid: 1111
    - name: create redis user
      user:
        name: redis
        uid: 1111
        group: redis
    - name: install redis
      yum:
        name: redis
        state: present
    - name: create data dir
      file:
        path: /data/redis
        state: directory
        recurse: yes
        owner: redis
        group: redis
    - name: copy redis.conf to dest
      template:
        src: templates/redis.conf.j2
        dest: /etc/redis.conf
      notify:
        - restart redis
    - name: start redis
      service:
        name: redis
        state: started
        enabled: yes
  handlers:
    - name: restart redis
      service:
        name: redis
        state: restarted

执行完成之后,我们可以看到被控端/etc/redis.conf配置文件如下:

daemonize yes
pidfile /var/run/redis.pid
port 6379
logfile "/var/log/redis/redis.log"
dbfilename dump.rdb
dir /data/redis

maxmemory 1G

bind 10.1.61.187 127.0.0.1

timeout 300
loglevel notice

databases 16
save 900 1
save 300 10
save 60 10000

rdbcompression yes

maxclients 10000
appendonly yes
appendfilename appendonly.aof
appendfsync everysec

关于template模块的更多参数说明:

  • backup:如果原目标文件存在,则先备份目标文件
  • dest:目标文件路径
  • force:是否强制覆盖,默认为yes
  • group:目标文件属组
  • mode:目标文件的权限
  • owner:目标文件属主
  • src:源模板文件路径
  • validate:在复制之前通过命令验证目标文件,如果验证通过则复制

14.2.2、Jinja2条件语句

在上面的示例中,我们直接取了被控节点的eth0网卡的ip作为其监听地址。那么假如有些机器的网卡是bond0,这种做法就会报错。这个时候我们就需要在模板文件中定义条件语句如下:

daemonize yes
pidfile /var/run/redis.pid
port 6379
logfile "/var/log/redis/redis.log"
dbfilename dump.rdb
dir /data/redis

maxmemory 1G

{% if ansible_bond0 is defined %}
bind {{ ansible_bond0.ipv4.address }} 127.0.0.1
{% elif ansible_eth0 is defined %}
bind {{ ansible_eth0.ipv4.address }} 127.0.0.1
{% else%}
bind 0.0.0.0
{% endif %}

timeout 300
loglevel notice

databases 16
save 900 1
save 300 10
save 60 10000

rdbcompression yes

maxclients 10000
appendonly yes
appendfilename appendonly.aof
appendfsync everysec

我们可以更进一步,让redis主从角色都可以使用该文件:

daemonize yes
pidfile /var/run/redis.pid
port 6379
logfile "/var/log/redis/redis.log"
dbfilename dump.rdb
dir /data/redis

maxmemory {{ (ansible_memtotal_mb /2) | int }}M

{% if ansible_bond0 is defined %}
bind {{ ansible_bond0.ipv4.address }} 127.0.0.1
{% elif ansible_bond0 is defined %}
bind {{ ansible_bond0.ipv4.address }} 127.0.0.1
{% else%}
bind 0.0.0.0
{% endif %}

{% if masterip is defined %}
slaveof {{ hostvars[groups.redismaster.0].ansible_bond0.ipv4.address }} {{ masterport|default(6379) }}
{% endif %}

{% if masterpass is defined %}
masterauth {{ masterpass }}
{% endif %}

{% if requirepass is defined %}
requirepass {{ requirepass }}
{% endif %}


#requirepass是配置在主节点的,masterauth是配置在从节点的,两边配置要一样从节点才能和主节点连接上进行主从复制。

timeout 300
loglevel notice

databases 16
save 900 1
save 300 10
save 60 10000

rdbcompression yes

maxclients 10000
appendonly yes
appendfilename appendonly.aof
appendfsync everysec

stop-writes-on-bgsave-error no

我们定义一个inventory如下:

all:
  children: 
    redis:
      children:
         redismaster:
              vars: 
                  requirepass: redhat
              hosts: 
                 servera: 
         redisslave:
              vars:
                  masterauth: redhat 
                  masterport: 6379
                  masterip: yes
              hosts:
                 serverb:
                 serverc:


测试

[root@servera ~]# redis-cli 
127.0.0.1:6379> auth redhat
127.0.0.1:6379> info
role:master

[root@serverb ~]# redis-cli 
127.0.0.1:6379> info
role:slave

14.2.3、Jinja2循环语句

定义一个inventory示例如下:

[proxy]
10.1.61.195

[webservers]
10.1.61.27
10.1.61.187

现在把proxy主机组中的主机作为代理服务器,安装nginx做反向代理,将请求转发至后面的两台webserver,即webserver组的服务器。

现在我们编写一个playbook如下:

[root@controller ansible]# cat config_nginx.conf

# 还需要在ansible.cfg中开启facts缓存
- name: gather facts
  gather_facts: Fasle
  hosts: webservers
  tasks:
    - name: gather facts
      setup:
   
- name: Configure Nginx
  hosts: proxy
  tasks:
    - name: install nginx
      yum:
        name: nginx
        state: present
    - name: copy nginx.conf to dest
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/nginx.conf
      notify:
        - restart nginx
    - name: start nginx
      service:
        name: nginx
        state: started
        enabled: yes
  handlers:
    - name: restart nginx
      service:
        name: nginx
        state: restarted

模板文件 templates/nginx.conf.j2示例如下:

[root@controller ansible]# cat nginx.conf.j2 
user nginx;
worker_processes {{ ansible_processor_vcpus }};
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;
include /usr/share/nginx/modules/*.conf;
events {
    worker_connections 65535;
    use epoll;
}
http {
    map $http_x_forwarded_for $clientRealIP {
        "" $remote_addr;
        ~^(?P<firstAddr>[0-9\.]+),?.*$ $firstAddr;
    }
    log_format  real_ip '{ "datetime": "$time_local", '
                        '"remote_addr": "$remote_addr", '
                        '"source_addr": "$clientRealIP", '
                        '"x_forwarded_for": "$http_x_forwarded_for", '
                        '"request": "$request_uri", '
                        '"status": "$status", '
                        '"request_method": "$request_method", '
                        '"request_length": "$request_length", '
                        '"body_bytes_sent": "$body_bytes_sent", '
                        '"request_time": "$request_time", '
                        '"http_referrer": "$http_referer", '
                        '"user_agent": "$http_user_agent", '
                        '"upstream_addr": "$upstream_addr", '
                        '"upstream_status": "$upstream_status", '
                        '"upstream_http_header": "$upstream_http_host",'
                        '"upstream_response_time": "$upstream_response_time", '
                        '"x-req-id": "$http_x_request_id", '
                        '"servername": "$host"'
                        ' }';
    access_log  /var/log/nginx/access.log  real_ip;
    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;
    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;
    include /etc/nginx/conf.d/*.conf;

    upstream web {
    {% for host in groups['webservers'] %}
            {% for host in groups['webservers'] %}
               {% if ansible_bond0 is defined %}
               server {{ hostvars[host].ansible_bond0.ipv4.address }}:80;
               {% elif ansible_ens33 is defined %}
               server {{ hostvars[host].ansible_ens33.ipv4.address }}:80;
               {% endif %}
            {% endfor %}
    }
    server {
        listen       80 default_server;
        server_name  _;
        location / {
            proxy_pass http://web;
        }
    }
}

for循环只能循环列表,不能循环字典,字典需要转换为列表才能循环

14.2.3、Jinja2过滤器

1. default过滤器

当指定的变量不存在时,用于设定默认值

简单示例:

"Host": "{{ db_host | default('lcoalhost') }}"

复杂一点儿的实例:

- hosts: 
  gather_facts: false
  vars:
    paths:
      - path: /tmp/test
        mode: '0400'
      - path: /tmp/foo
      - path: /tmp/bar
  tasks:
    - file:
        path: "{{ item.path }}"
        state: touch
        mode: "{{ item.mode|default(omit)}}"
      with_items: "{{ paths }}"

2. 字符串操作相关的过滤器

  • upper:将所有字符串转换为大写
  • lower:将所有字符串转换为小写
  • capitalize:将字符串的首字母大写,其他字母小写
  • reverse:将字符串倒序排列
  • first:返回字符串的第一个字符
  • last:返回字符串的最后一个字符
  • trim:将字符串开头和结尾的空格去掉
  • center(30):将字符串放在中间,并且字符串两边用空格补齐30位
  • length:返回字符串的长度,与count等价
  • list:将字符串转换为列表
  • shuffle:list将字符串转换为列表,但是顺序排列,shuffle同样将字符串转换为列表,但是会随机打乱字符串顺序

示例:

- hosts: test
  gather_facts: no
  vars: 
    teststr: "abc123ABV"
    teststr1: " abc "
    teststr2: "123456789"
    teststr3: "sfacb1335@#$%"
  tasks:
    - debug: 
        msg: "{{ teststr | upper }}"
    - debug:
        msg: "{{ teststr | lower }}"
    - debug:
        msg: "{{ teststr | capitalize }}"
    - debug:
        msg: "{{ teststr | reverse }}"
    - debug:
        msg: "{{ teststr|first }}"
    - debug: 
        msg: "{{ teststr|last }}"
    - debug:
        msg: "{{ teststr1 | trim }}"
    - debug:
        msg: "{{ teststr2 | center(30) }}"
    - debug: 
        msg: "{{ teststr2 | length }}"
    - debug:
        msg: "{{ teststr3 | list }}"
    - debug:
        msg: "{{ teststr3 | shuffle }}"

3. 数字操作相关的过滤器

  • int: 将对应的值转换为整数
  • float:将对应的值转换为浮点数
  • abs:获取绝对值
  • round:小数点四舍五入
  • random:从一个给定的范围中获取随机值

示例

- hosts: test
  gather_facts: no
  vars: 
    testnum: -1
  tasks:
    - debug: 
        msg: "{{  8+('8'|int) }}"
    - debug:
        # 默认情况下,如果无法完成数字转换则返回0
        # 这里指定如果无法完成数字转换则返回6
        msg: "{{ 'a'|int(default=6) }}" 
    - debug:
        msg: "{{ '8'|float }}"
    - debug:
        msg: "{{ 'a'|float(8.88)' }}"
    - debug:
        msg: "{{ testnum|abs }}"
    - debug: 
        msg: "{{ 12.5|round }}"
    - debug:
        msg: "{{ 3.1415926 | round(5) }}"
    - debug:
        # 从0到100中随机返回一个数字
        msg: "{{ 100|random }}"
    - debug: 
        # 从5到10中随机返回一个数字
        msg: "{{ 10|random(start=5) }}"
    - debug:
        # 从4到15中随机返回一个数字,步长为3
        # 返回的随机数只可能是:4,7,10,13中的一个
        msg: "{{ 15|random(start=4,step=3) }}"
    - debug:
        # 从0到15随机返回一个数字,步长为4
        msg: "{{ 15|random(step=4) }}"

4. 列表操作相关的过滤器

  • length: 返回列表长度
  • first:返回列表的第一个值
  • last:返回列表的最后一个值
  • min:返回列表中最小的值
  • max:返回列表中最大的值
  • sort:重新排列列表,默认为升序排列,sort(reverse=true)为降序
  • sum:返回纯数字非嵌套列表中所有数字的和
  • flatten:如果列表中包含列表,则flatten可拉平嵌套的列表,levels参数可用于指定被拉平的层级
  • join:将列表中的元素合并为一个字符串
  • random:从列表中随机返回一个元素
  • shuffle
  • upper
  • lower
  • union:将两个列表合并,如果元素有重复,则只留下一个
  • intersect:获取两个列表的交集
  • difference:获取存在于第一个列表中,但不存在于第二个列表中的元素
  • symmetric_difference:取出两个列表中各自独立的元素,如果重复则只留一个

示例:

- hosts: test
  gather_facts: false
  vars:
    testlist1: [1,2,4,6,3,5]
    testlist2: [1,[2,3,4,[5,6]]]
    testlist3: [1,2,'a','b']
    testlist4: [1,'A','b',['C','d'],'Efg']
    testlist5: ['abc',1,2,'a',3,2,'1','abc']
    testlist6: ['abc',3,'1','b','a']
  tasks:
    - debug:
        msg: "{{ testlist1 | length }}"
    - debug:
        msg: "{{ testlist1 |first }}"
    - debug: 
        msg: "{{ testlist1 | last }}"
    - debug:
        msg: "{{ testlist1 | min }}"
    - debug: 
        msg: "{{ testlist1 | max }}"
    - debug:
        msg: "{{ testlist1 | sort }}"
    - debug:
        msg: "{{ testlist1 | sort(reverse=true) }}"  
    - debug:
        msg: "{{ testlist2 | flatten }}"
    - debug:
        msg: "{{ testlist2 | flatten(levels=1) }}"
    - debug:
        msg: "{{ testlist2 | flatten | max }}"
    - debug:
        msg: "{{ testlist3 | join }}"
    - debug:
        msg: "{{ testlist3 |join(',')}}"
    - debug:
        msg: "{{ testlist3 | random }}"
    - debug:
        msg: "{{ testlist3 | shuffle }}"
    - debug:
        msg: "{{ testlist4 | upper }}"
    - debug:
        msg: "{{ testlist4 | lower }}"
    - debug:
        msg: "{{ testlist5 | union(testlist6) }}"
    - debug:
        msg: "{{ testlist5 | intersect(testlist6) }}"
    - debug:
        msg: "{{ testlist5 | difference(testlist6) }}"
    - debug:
        msg: "{{ testlist5 | symmetric_difference(testlist6) }}"

5. hash和Encoding过滤器

  • hash过滤器
- hosts: test
  gather_facts: no
  tasks:
    - debug: 
        msg: "{{ 'redhat' | hash('sha1') }}"

#结果:
ok: [servera] => {
    "msg": "3c767c41afb12ada140190ed82db3fd930e2efa3"

#相当于: 
# echo -n redhat | sha1sum 
3c767c41afb12ada140190ed82db3fd930e2efa3  -


  • password_hash过滤器
- hosts: test
  gather_facts: no
  tasks:
    - user:
        name: john
        password: "{{ password }}"
      vars:
        password: "{{ 'redhat' | password_hash('sha512') }}"
  • base64加密和解密
- hosts: test
  gather_facts: no
  tasks:
    - debug: 
        msg: "{{ 'redhat' | b64encode | b64decode }}"

b64encode: 加密
b64decode: 解密

# 相当于:
echo -n redhat | base64 -w 0  | base64 -d

6. 查找替换过滤器

  • replace过滤器
- hosts: test
  gather_facts: no
  tasks:
    - debug:
        msg: "{{ 'marvin, arthur' | replace('ar','**') }}"
  • regex_search 过滤器
- hosts: test
  gather_facts: no
  tasks:
    - debug:
        msg: "{{ 'marvin, arthur' | regex_search('ar\\S*r') }}"

\是特殊字符,需要特殊处理以后再交给正则表达式
  • regex_replace过滤器
- hosts: test
  gather_facts: no
  vars:
      str1: 'marvin, arthur'
  tasks:
    - debug: 
        msg: "{{ str1 | regex_replace('ar(\\S*)r','\\1mb') }}"

正则表达式:

\w  匹配字母数字及下划线
\W  匹配非字母数字及下划线
\s  匹配任意空白字符,等价于 [ \t\n\r\f]。
\S  匹配任意非空字符
\d  匹配任意数字,等价于 [0-9].
\D  匹配任意非数字
\A  匹配字符串开始
\Z  匹配字符串结束,如果是存在换行,只匹配到换行前的结束字符串。
\z  匹配字符串结束
\G  匹配最后匹配完成的位置。
\b  匹配一个单词边界,也就是指单词和空格间的位置。例如, 'er\b' 可以匹配"never" 中的 'er',但不能匹配 "verb" 中的 'er'。
\B  匹配非单词边界。'er\B' 能匹配 "verb" 中的 'er',但不能匹配 "never" 中的 'er'。
\n, \t, 等.  匹配一个换行符。匹配一个制表符。等
\1...\9  匹配第n个分组的内容。
\10  匹配第n个分组的内容,如果它经匹配。否则指的是八进制字符码的表达式。

7. 应用于字典变量的过滤器

  • combine 将两个字典合成一个
- hosts: servera
  gather_facts: no
  vars:
      user1:
         name: bob
         uid: 1200
      userinfo:
         home: /home/bob
         comment: "user account"
  tasks:
     - name: combine
       debug:
          msg: "{{ user1 | combine(userinfo)}}"

  • dict2items 将字典转换为列表

示例一:

tags:
  Application: payment
  Environment: dev

# 转化后:
{{ dict | dict2items }}

- key: Application
  value: payment
- key: Environment
  value: dev

示例二:

files:
  users: /etc/passwd
  groups: /etc/group

{{ files | dict2items(key_name='file', value_name='path') }}

# 转化后:
- file: users
  path: /etc/passwd
- file: groups
  path: /etc/group

  • items2dict 将列表转换为字典

示例一:

tags:
  - key: Application
    value: payment
  - key: Environment
    value: dev


{{ tags | items2dict }}

# 转化后:

Application: payment
Environment: dev

示例二:

  fruits:
  - fruit: apple
    color: red
  - fruit: pear
    color: yellow
  - fruit: grapefruit
    color: yellow
  
  {{ fruits | items2dict(key_name='fruit', value_name='color') }}
  
  # 必须指定key_name和value_name,否则会报KeyError: 'key'或者
  
  # 转化后:
        "apple": "red", 
        "grapefruit": "yellow", 
        "pear": "yellow"
  • json_query 将变量转换成json,并查询子元素
- hosts: localhost
  gather_facts: no
  vars:
    domain_definition: "{{ lookup('file','json_example.json') | from_json  }}"
  tasks:
    - name: "Display all cluster names"
      debug:
        var:  item
      loop: "{{ domain_definition | json_query('domain.cluster[*].name') }}"
  • from_json/from_yaml
{{ some_variable | from_json }}
{{ some_variable | from_yaml }}

- hosts: localhost
  gather_facts: no
  tasks:
    - name: from_json filter test
      debug:
        msg:  "{{ domain_definition }}"
  vars:
    domain_definition: "{{ lookup('file','json_example.json') | from_json  }}"

  • to_json/to_yaml/to_nice_json/to_nice_yaml
{{ some_variable | to_json }}
{{ some_variable | to_yaml }}

{{ some_variable | to_nice_json }}
{{ some_variable | to_nice_yaml }}

- name: Convert between JSON and YAML format
  vars:
     hosts:
     - name: bastion
       ip:
         - 172.25.250.254
         - 172.25.252.1
  debug:
      msg: '{{ hosts | to_json }}'
      

# 结果:
'[{"name": "bastion", "ip": ["172.25.250.254", "172.25.252.1"]}]'

示例变量:

# cat json_example.json
{
        "domain": {
            "cluster": [
                {
                    "name": "cluster1"
                },
                {
                    "name": "cluster2"
                }
            ],
            "server": [
                {
                    "name": "server11",
                    "cluster": "cluster1",
                    "port": "8080"
                },
                {
                    "name": "server12",
                    "cluster": "cluster1",
                    "port": "8090"
                },
                {
                    "name": "server21",
                    "cluster": "cluster2",
                    "port": "9080"
                },
                {
                    "name": "server22",
                    "cluster": "cluster2",
                    "port": "9090"
                }
            ],
            "library": [
                {
                    "name": "lib1",
                    "target": "cluster1"
                },
                {
                    "name": "lib2",
                    "target": "cluster2"
                }
            ]
        }
}

8. ipaddr过滤器处理网络地址

ipaddr过滤器主要用于处理网络地址, 其支持的过滤器参数如下:

  • address
  • host
  • prefix
  • size
  • network
  • netmask
  • broadcast
  • subnet
  • ipv4 and ipv6

要想使用ipaddr过滤器,需要在主控端安装python-netaddr包:

yum install -y python-netaddr

示例:

- hosts: localhost
  gather_facts: no
  vars: 
    my_ip_addr: ["192.168.0.1/24"]
  tasks:
  - name: ipaddr 
    debug:
      msg: "{{ my_ip_addr | ipaddr }}"
  - name: ipaddr(network)
    debug:
      msg: "{{ my_ip_addr | ipaddr('network') }}"
  - name: ipaddr(host)
    debug:
      msg: "{{ my_ip_addr | ipaddr('host') }}"
  - name: ipaddr(netmask)
    debug:
      msg: "{{ my_ip_addr | ipaddr('netmask') }}"
  - name: ipaddr(size)
    debug:
      msg: "{{ my_ip_addr | ipaddr('size') }}"
  - name: ipaddr(subnet)
    debug:
      msg: "{{ my_ip_addr | ipaddr('subnet') }}"
  - name: ipaddr(ipv4)
    debug:
      msg: "{{ my_ip_addr | ipaddr('ipv4') }}"
  - name: ipaddr(broadcast)
    debug:
      msg: "{{ my_ip_addr | ipaddr('broadcast') }}"

输出:

TASK [ipaddr] *********************************************************************
ok: [localhost] => {
    "msg": [
        "192.168.0.1/24"
    ]
}

TASK [ipaddr(network)] ************************************************************
ok: [localhost] => {
    "msg": [
        "192.168.0.0"
    ]
}

TASK [ipaddr(host)] ***************************************************************
ok: [localhost] => {
    "msg": [
        "192.168.0.1/24"
    ]
}

TASK [ipaddr(netmask)] ************************************************************
ok: [localhost] => {
    "msg": [
        "255.255.255.0"
    ]
}

TASK [ipaddr(size)] ***************************************************************
ok: [localhost] => {
    "msg": [
        256
    ]
}

TASK [ipaddr(subnet)] *************************************************************
ok: [localhost] => {
    "msg": [
        "192.168.0.0/24"
    ]
}

TASK [ipaddr(ipv4)] ***************************************************************
ok: [localhost] => {
    "msg": [
        "192.168.0.1/24"
    ]
}

TASK [ipaddr(broadcast)] **********************************************************
ok: [localhost] => {
    "msg": [
        "192.168.0.255"
    ]
}

9. url分割过滤器

urlspilt用于分割url,并取出相应字段

- hosts: localhost
  gather_facts: no
  vars:
    url: 'http://user:password@www.example.com:8080/xxx/index.html?query=132'
  tasks:
    - name: "get url hostname"
      debug:
        msg: "{{ url | urlsplit('hostname')  }}"
# ==> "msg": "www.example.com"

    - name: "get url netloc"
      debug:
        msg: "{{ url | urlsplit('netloc')  }}"
# ==> "user:password@www.example.com:8080"

    - name: "get url path"
      debug:
        msg: "{{ url | urlsplit('path')  }}"
# ==> "msg": "/xxx/index.html"

    - name: "get url port"
      debug:
        msg: "{{ url | urlsplit('port')  }}"
# ==> "msg": "8080"

    - name: "get url scheme"
      debug:
        msg: "{{ url | urlsplit('scheme')  }}"
# ==> "msg": "http"

    - name: "get url query"
      debug:
        msg: "{{ url | urlsplit('query') }}"
# ==> "msg": "query=132"

10. 应用于注册变量的过滤器

正常情况下,当某个task执行失败的时候,ansible会中止运行。此时我们可以通过ignore_errors来捕获异常以让task继续往下执行。然后调用debug模块打印出出错时的内容,拿来错误结果后,主动失败。

- name: Run myprog
  command: /opt/myprog
  register: result
  ignore_errors: True
  
- debug: 
    var: result

- debug: 
    msg: "Stop running the playbook if myprog failed"
  failed_when: result|failed

任务返回值过滤器:

  • failed: 如果注册变量的值是任务failed则返回True
  • changed: 如果注册变量的值是任务changed则返回True
  • success:如果注册变量的值是任务succeeded则返回True
  • skipped:如果注册变量的值是任务skipped则返回True

在ansible2.9中,该方式会被废弃,不推荐使用

11. 应用于文件路径的过滤器

  • basename:返回文件路径中的文件名部分
  • dirname:返回文件路径中的目录部分
  • expanduser:将文件路径中的~替换为用户目录
  • realpath:处理符号链接后的文件实际路径

下面是一个示例:

- name: test basename
  hosts: servera
  vars:
    homepage: ~/index.html
    linkpath: /etc/systemd/system/default.target
  tasks:
    - name: copy homepage
      copy:
        src: index.html
        dest: "{{ homepage }}"
    - debug:
        msg: "{{ homepage | basename }}"
    - debug:
        msg: "{{ homepage | dirname }}"
    - debug:
        msg: "{{ homepage | expanduser }}"
    - debug:
        msg: "{{ homepage | expanduser }}"
    - debug:
        msg: "{{ linkpath | realpath  }}"


12. 自定义过滤器

举个简单的例子,现在有一个playbook如下:

- name: test filter
  hosts: test
  vars:
    domains: ["www.example.com","example.com"]
  tasks:
    template:
      src: templates/test.conf.j2
      dest: /tmp/test.conf

templates/test.conf.j2如下:

hosts = [{{ domains | join(',') }}]

执行playbook后,在目标机上的test.conf如下:

hosts = [www.example.com,example.com]

现在如果希望目标机上的test.conf文件返回结果如下:

hosts = ["www.example.com","example.com"]

没有现成的过滤器来帮我们做这件事情。我们可以自己简单写一个surround_by_quote.py内容如下:

# 定义过滤器执行的操作
def surround_by_quote(a_list):
  return ['"%s"' % an_element for an_element in a_list]

class FilterModule(object):
  def filters(self):
    return {'surround_by_quote': surround_by_quote}

我们需要开启ansible.cfg的配置项:

filter_plugins     = /usr/share/ansible/plugins/filter

将刚刚编写的代码文件放入/usr/share/ansible/plugins/filter目录下,然后修改templates/test.conf.j2如下:

hosts = [{{ domains | join(',') |surround_by_quote }}]

再次执行playbook,最后返回结果:

hosts = ["www.example.com","example.com"]

关于jinja2更多用法参考:http://docs.jinkan.org/docs/jinja2/

15. Ansible Playbook高级用法

15.1、任务委托

在有些时候,我们希望运行与选定的主机或主机组相关联的task,但是这个task又不需要在选定的主机或主机组上执行,而需要在另一台服务器上执行。

这种特性适用于以下场景:

  • 在告警系统中启用基于主机的告警
  • 向负载均衡器中添加或移除一台主机
  • 在dns上添加或修改针对某个主机的解析
  • 在存储节点上创建一个存储以用于主机挂载
  • 使用一个外部程序来检测主机上的服务是否正常

可以使用delegate_to语句来在另一台主机上运行task:

---
- hosts: webservers
  tasks:
    - name: Take out of load balancer pool
      command: /usr/bin/take_out_of_pool {{ inventory_hostname }}
      delegate_to: 127.0.0.1

    - name: Actual steps would go here
      yum:
        name: acme-web-stack
        state: latest

    - name: Add back to load balancer pool
      command: /usr/bin/add_back_to_pool {{ inventory_hostname }}
      delegate_to: 127.0.0.1

如果delegate_to: 127.0.0.1的时候,等价于local_action

15.2、本地执行

如果希望在控制主机本地运行一个特定的任务,可以使用local_action语句。

假设我们需要配置的远程主机刚刚启动,如果我们直接运行playbook,可能会因为sshd服务尚未开始监听而导致失败,我们可以在控制主机上使用如下示例来等待被控端sshd端口监听:

tasks:
- name: test acesss url 
  local_action: command curl http://xxx

tasks:
  - name: Recursively copy files from management server to target
    local_action: command rsync -a /path/to/files {{ inventory_hostname }}:/path/to/target/

15.3、任务暂停

有些情况下,一些任务的运行需要等待一些状态的恢复,比如某一台主机或者应用刚刚重启,我们需要需要等待它上面的某个端口开启,此时就需要将正在运行的任务暂停,直到其状态满足要求。

Ansible提供了wait_for模块以实现任务暂停的需求

wait_for模块常用参数:

  • connect_timeout:在下一个任务执行之前等待连接的超时时间
  • delay:等待一个端口或者文件或者连接到指定的状态时,默认超时时间为300秒,在这等待的300s的时间里,wait_for模块会一直轮询指定的对象是否到达指定的状态,delay即为多长时间轮询一次状态。
  • host:wait_for模块等待的主机的地址,默认为127.0.0.1
  • port:wait_for模块等待的主机的端口
  • path:文件路径,只有当这个文件存在时,下一任务才开始执行,即等待该文件创建完成
  • state:等待的状态,即等待的文件或端口或者连接状态达到指定的状态时,下一个任务开始执行。当等的对象为端口时,状态有started,stoped,即端口已经监听或者端口已经关闭;当等待的对象为文件时,状态有present或者started,absent,即文件已创建或者删除;当等待的对象为一个连接时,状态有drained,即连接已建立。默认为started
  • timeout:wait_for的等待的超时时间,默认为300秒

示例:

#等待8080端口已正常监听,才开始下一个任务,直到超时
- wait_for: 
    port: 8080 
    state: started  
    
#等待8000端口正常监听,每隔10s检查一次,直至等待超时
- wait_for: 
    port: 8000 
    delay: 10 
    
#等待8000端口直至有连接建立
- wait_for: 
    host: 0.0.0.0 
    port: 8000 
    delay: 10 
    state: drained
    
#等待8000端口有连接建立,如果连接来自10.2.1.2或者10.2.1.3,则忽略。
- wait_for: 
    host: 0.0.0.0 
    port: 8000 
    state: drained 
    exclude_hosts: 10.2.1.2,10.2.1.3 
    
#等待/tmp/foo文件已创建    
- wait_for: 
    path: /tmp/foo 

#等待/tmp/foo文件已创建,而且该文件中需要包含completed字符串    
- wait_for: 
    path: /tmp/foo 
    search_regex: completed 

#等待/var/lock/file.lock被删除    
- wait_for: 
    path: /var/lock/file.lock 
    state: absent 
    
#等待指定的进程被销毁
- wait_for: 
    path: /proc/3466/status 
    state: absent 

15.4、滚动执行

默认情况下,ansible会并行的在所有选定的主机或主机组上执行每一个task,但有的时候,我们会希望能够逐台运行。最典型的例子就是对负载均衡器后面的应用服务器进行更新时。通常来讲,我们会将应用服务器逐台从负载均衡器上摘除,更新,然后再添加回去。我们可以在play中使用serial语句来告诉ansible限制并行执行play的主机数量。

下面是一个在amazon EC2的负载均衡器中移除主机,更新软件包,再添加回负载均衡的配置示例:

- name: upgrade pkgs on servers behind load balancer
  hosts: myhosts
  serial: 1
  tasks:
    - name: get the ec2 instance id and elastic load balancer id
      ec2_facts:

    - name: take the host out of the elastic load balancer id
      local_action: ec2_elb
      args:
        instance_id: "{{ ansible_ec2_instance_id }}"
        state: absent

    - name: upgrade pkgs
      apt: 
          update_cache: yes 
          upgrade: yes

    - name: put the host back n the elastic load balancer
      local_action: ec2_elb
      args:
        instance_id: "{{ ansible_ec2_instance_id }}"
        state: present
        ec2_elbs: "{{ items }}"
      with_items: ec2_elbs

在上述示例中,serial的值为1,即表示在某一个时间段内,play只在一台主机上执行。如果为2,则同时有2台主机运行play。

一般来讲,当task失败时,ansible会停止执行失败的那台主机上的任务,但是继续对其他主机执行。在负载均衡的场景中,我们会更希望ansible在所有主机执行失败之前就让play停止,否则很可能会面临所有主机都从负载均衡器上摘除并且都执行失败导致服务不可用的场景。这个时候,我们可以使用serial语句配合max_fail_percentage语句使用。max_fail_percentage表示当最大失败主机的比例达到多少时,ansible就让整个play失败。示例如下:

- name: upgrade pkgs on fservers behind load balancer
  hosts: myhosts
  serial: 1
  max_fail_percentage: 25
  tasks:
    ......

假如负载均衡后面有4台主机,并且有一台主机执行失败,这时ansible还会继续运行,要让Play停止运行,则必须超过25%,所以如果想一台失败就停止执行,我们可以将max_fail_percentage的值设为24。如果我们希望只要有执行失败,就放弃执行,我们可以将max_fail_percentage的值设为0。

15.5、只执行一次

某些时候,我们希望某个task只执行一次,即使它被绑定到了多个主机上。例如在一个负载均衡器后面有多台应用服务器,我们希望执行一个数据库迁移,只需要在一个应用服务器上执行操作即可。

可以使用run_once语句来处理:

- name: run the database migrateions
  command: /opt/run_migrateions
  run_once: true

还可以与local_action配合使用,如下:

- name: run the task locally, only once
  command: /opt/my-custom-command
  connection: local
  run_once: true

还可以与delegate_to配合使用,让这个只执行一次的任务在指定的机器上运行:

- name: run the task locally, only once
  command: /opt/my-custom-command
  run_once: true
  delegate_to: app.a1-61-105.dev.unp

15.6、设置环境变量

我们在命令行下执行某些命令的时候,这些命令可能会需要依赖环境变量。比如在安装某些包的时候,可能需要通过代理才能完成完装。或者某个脚本可能需要调用某个环境变量才能完成运行。

ansible 支持通过environment关键字来定义一些环境变量。

在如下场景中可能需要用到环境变量:

  • 运行shell的时候,需要设置path变量
  • 需要加载一些库,这些库不在系统的标准库路径当中

下面是一个简单示例:

---
- name: upload a remote file to aws s3
  hosts: test
  tasks:
    - name: install pip
      yum:
        name: python-pip
        state: installed
    
    - name: install the aws tools
      pip:
        name: awscli
        state: present
    
    - name: upload file to s3
      shell: aws s3 put-object --bucket=my-test-bucket --key={{ ansible_hostname }}/fstab --body=/etc/fstab --region=eu-west-1
      environment:
        AWS_ACCESS_KEY_ID: xxxxxx
        AWS_SECRET_ACCESS_KEY: xxxxxx

事实上,environment也可以存储在变量当中:

- hosts: all
  remote_user: root
  vars:
    proxy_env:
      http_proxy: http://proxy.example.com:8080
      https_proxy: http://proxy.bos.example.com:8080
  tasks:
    - apt: 
         name: cobbler 
         state: installed
      environment: proxy_env

15.7、交互式提示

在少数情况下,ansible任务运行的过程中需要用户输入一些数据,这些数据要么比较秘密不方便,或者数据是动态的,不同的用户有不同的需求,比如输入用户自己的账户和密码或者输入不同的版本号会触发不同的后续操作等。ansible的vars_prompt关键字就是用来处理上述这种与用户交互的情况的。

- hosts: all
  remote_user: root
  vars_prompt:
     - name: share_user
       prompt: "what is your network username?"
       private: yes

     - name: share_pass
       prompt: "what is your network password"
       private: yes
       
   tasks:
     - debug:
         var: share_user
     - debug:
         var: share_pass

vars_prompt常用选项说明:

  • private: 默认为yes,表示用户输入的值在命令行不可见
  • default:定义默认值,当用户未输入时则使用默认值
  • confirm:如果设置为yes,则会要求用户输入两次,适合输入密码的情况

16. Ansible Playbook之tags

简介

在大型项目当中,通常一个playbook会有非常多的task。而我们每次执行这个playbook时,都会将所有task运行一遍。而事实上,在实际使用过程中,我们可能只是想要执行其中的一部分任务而已,并不想把整个playbook完整跑一遍。这个时候就需要用到tags。

通过tags,我们可以给playbook中的某一些任务打上“标签”,而在执行playbook的时候,我们可以通过选定标签的方式指定只执行哪一些任务或者不执行哪一些任务。

16.1、为task打tag

下面是一个安装httpd的简单示例:

[root@controller ansible]# cat /etc/ansible/playbook/install_web.yml
- name: configure webservers 
  hosts: all
  remote_user: ansible
  
  tasks:
    - name: Install httpd
      yum: 
        name: httpd
        state: present
      tags: install_httpd
        
    - name: Cofiguration httpd
      copy: 
        src: /root/httpd.conf 
        dest: /etc/httpd/conf/httpd.conf
      tags: conf_httpd   
      notify:
        - restart httpd
        
    - name: Start httpd
      service: 
        name: httpd 
        state: started 
        enabled: no
      tags: start_httpd

  handlers:
    - name: restart httpd
      service: name=httpd state=restart

在上面的示例中,我们为两个task定义了三个tags:install_httpd、conf_httpd以及start_httpd。

16.2、使用tag

16.2.1、执行指定tag的task

有了tags之后,我们就可以只运行playbook中指定标签的task了:

[root@controller ansible]# ansible-playbook --tags="start_httpd" install_web.yml 

PLAY [configure webservers] *************************************************************************************************************************************************

TASK [Gathering Facts] ******************************************************************************************************************************************************
ok: [10.1.61.187]

TASK [Start httpd] **********************************************************************************************************************************************************
changed: [10.1.61.187]

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

也可以一次指定多个tag执行:

[root@controller ansible]# ansible-playbook --tags="conf_httpd,start_httpd" install_web.yml     

PLAY [configure webservers] *************************************************************************************************************************************************

TASK [Gathering Facts] ******************************************************************************************************************************************************
ok: [10.1.61.187]

TASK [Cofiguration httpd] ***************************************************************************************************************************************************
ok: [10.1.61.187]

TASK [Start httpd] **********************************************************************************************************************************************************
ok: [10.1.61.187]

PLAY RECAP ******************************************************************************************************************************************************************
10.1.61.187                : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

16.2.2、排除指定tag的task

通过下面的方式可以排除指定了tag的task,即除了指定tag的task不执行,其他task都执行:

[root@controller ansible]# ansible-playbook --skip-tags="install_httpd" install_web.yml                           

PLAY [configure webservers] *************************************************************************************************************************************************

TASK [Gathering Facts] ******************************************************************************************************************************************************
ok: [10.1.61.187]

TASK [Cofiguration httpd] ***************************************************************************************************************************************************
ok: [10.1.61.187]

TASK [Start httpd] **********************************************************************************************************************************************************
ok: [10.1.61.187]

PLAY RECAP ******************************************************************************************************************************************************************
10.1.61.187                : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

执行效果跟上面一样。

16.2.3、查看playbook中的所有tag

可以通过--list-tags参数列出指定的playbook中所有的tag

[root@controller ansible]# ansible-playbook --list-tags install_web.yml                          

playbook: install_web.yml

  play #1 (all): configure webservers   TAGS: []
      TASK TAGS: [conf_httpd, install_httpd, start_httpd]

16.3、打tag的几种方式

16.3.1、为一个任务指定一个标签

​​​​​​​

这种方式就是上面示例中的方法:

tags: conf_httpd

16.3.2、为一个任务指定多个标签

可以通过列表的方式为一个任务指定多个标签:

tags:
  - install_httpd
  - install_web
  
tags: ['install_httpd','install_web']

tags: install_httpd,install_web

16.3.3、为一个play指定一组标签

当为一个play指定一组标签后,该play下的所有task都会自动继承该标签,各task也可以自定义自己的标签。

- name: configure webservers 
  hosts: all
  remote_user: ansible
  tags: 
    - httpd
  tasks:
    ...

ansible内置tag

除了用户自定义tag,ansible也内置了几个tag,这些tag都包含特殊含义:

  • always:一旦某个task被打上了always的tag,则无论是playbook的完整执行,还是指定tag执行,不管你指定的tag是啥,该任务总是会被执行。除非明确指定"--skip-tags=always"选项,才不会执行该task。
  • never:该标签与always正好相反,总是不会执行,除非明确指定"--tags=never"选项。
  • tagged:在调用时使用
# 所有打了tag的任务都会被执行,包含never tag的除外,没有标签的不会被执行
ansible-playbook --tags tagged install_web.yaml 

# 所有打了tag的任务都不会被执行,包括always tag也不会被执行
ansible-playbook --skip-tags tagged install_web.yaml
  • untagged:在调用时使用
# 所有未打tag的任务都会被执行,打了always tag的也会被执行
ansibl-playbook --tags untagged install_web.yaml

# 所有未打tag的任务都不会被执行
ansibl-playbook --skip-tags untagged install_web.yaml
  • all:表示所有任务都会被执行,在默认情况下,不指定任何标签,则使用的就是该标签

17. Ansible Playbook Include

说明

在前面的学习当中,我们一直使用一个playbook文件来组织所有的task任务。但是,当我们项目越来越大,task越来越多的时候,如果还将所有的task都写到一个playbook当中,可读性就会变差,这个时候我们就需要重新来组织playbook了。

我们可以将一个大的playbook拆成若干个小的playbook文件,在主配置文件中将这些零碎的小文件引入进来,而这种方式就叫做playbook的"include"。

17.1、include

playbook的include其实就是使用include关键字

17.1.1、tasks include

1. include简单示例

下面是两个playbook示例,分别用于安装lamp和lnmp环境:

# cat lamp.yml
- hosts: test
  gather_facts: no
  tasks:
    - package: 
        name: mysql
        state: present
    - package: 
        name: php-fpm
        state: present
    - package:
        name: httpd
        state: present
        
# cat lnmp.yml
- hosts: test
  gather_facts: no
  tasks:
    - package: 
        name: mysql
        state: present
    - package:
        name: php-fpm
        state: present
    - package:
        name: nginx
        state: present

在上面的示例当中,我们可以看到lamp和lnmp中mysql和php的安装都是一样的,所以我们可以将这两个任务提取出来,放到一个单独的task文件中,然后在lnmp和lamp中引入:

# cat install_mysql_php.yml
- package: 
    name: mysql
    state: present
- package:
    name: php-fpm
    state: present
    

# cat lamp.yml
- hosts: test
  gather_facts: no
  tasks:
    - include: install_mysql_php.yml
    - package:
        name: httpd
        state: php-fpm

# cat lnmp.yml
- hosts: test
  gather_facts: no
  tasks:
    - include: install_mysql_php.yml
    - package:
        name: nginx
        state: php-fpm

2. 在include时引入变量

也可以在include的时候,传入变量:

[root@controller ansible]# cat test_include.yml
- hosts: test
  gather_facts: no
  tasks:
    - include: wordpress.yml user=timmy
    - include: wordpress.yml user=alice
    - include: wordpress.yml user=bob
  
# cat wordpress.yml
- debug:
    msg: "{{ user }}"

通过如下方式带入变量:

tasks:
 - { include: wordpress.yml, user: timmy, ssh_keys: [ 'keys/one.txt', 'keys/two.txt' ] }

再给一个例子:

- hosts: test
  gather_facts: no
  tasks: 
    - include: in.yml
      vars:
        users:
          bob:
            gender: male
          lucy:
            gender: female
            
# cat in.yml
- debug:
    msg: "{{ item.key }} is {{ item.value.gender }}"
  loop: "{{users | dict2items }}""

3. 在include中使用tag

# cat test_include.yml
- hosts: test
  gather_facts: no
  tasks:
    - include: in1.yml
      tags: t1
    - include: in2.yml
      tags: t2

# cat in1.yml
- debug:
    msg: "task1 in in1.yml"
- debug:
    msg: "task2 in in1.yml"
    
# cat in2.yml
- debug:
    msg: "task1 in in2.yml"
- debug:
    msg: "task2 in in2.yml"

在上面的示例当中,两个Include分别对应两个tag,如果我们在执行test_include.yml时,指定tag为t2,那么in2.yml中的所有任务都会被执行。所以tag是针对include的所有任务生效。

4. 在include中使用条件判断

# cat test_include.yml

- hosts: test
  gather_facts: no
  tasks:
    - include: in.yml
      when: 2 > 1

# cat in.yml
- debug:
    msg: "task in in.yml"

5. 在include中使用循环

下面是一个简单的循环示例:

[root@controller ansible]# cat test_include.yml

- hosts: test
  gather_facts: no
  tasks:
    - include: in.yml
      loop:
        - 1
        - 2

[root@controller ansible]# cat in.yml
- debug:
    msg: "task1 in in.yml"
- debug:
    msg: "task2 in in.yml"

可以看到in.yml被循环执行了两次。

我们可以稍微修改in.yml示例如下:

[root@controller ansible]# cat in.yml
- debug: 
    msg: "{{ item }} task1 in in.yml"
- debug:
    msg: "{{ item }} task2 in in.yml"

再次执行playbook的结果如下:

[root@workstation ansible]# ansible-playbook test_include.yml 

PLAY [servera] **********************************************************************************************************************************

TASK [include] **********************************************************************************************************************************
included: /etc/ansible/in.yml for servera => (item=1)
included: /etc/ansible/in.yml for servera => (item=2)

TASK [debug] ************************************************************************************************************************************
ok: [servera] => {
    "msg": "1 task1 in in.yml"
}

TASK [debug] ************************************************************************************************************************************
ok: [servera] => {
    "msg": "1 task2 in in.yml"
}

TASK [debug] ************************************************************************************************************************************
ok: [servera] => {
    "msg": "2 task1 in in.yml"
}

TASK [debug] ************************************************************************************************************************************
ok: [servera] => {
    "msg": "2 task2 in in.yml"
}

PLAY RECAP **************************************************************************************************************************************
servera                    : ok=6    changed=0    unreachable=0    failed=0 

可以看到item的值就来自test_include中的loop循环。那么这就引出了一个问题:如果正好in.yml当中也有循环时怎么办?

[root@controller ansible]# cat in.yml
- debug: 
    msg: "{{ item }} task1 in in.yml"
  loop: ['a','b','c']

再次执行test_include,结果如下:

[root@workstation ansible]# ansible-playbook test_include.yml 

PLAY [servera] **********************************************************************************************************************************

TASK [include] **********************************************************************************************************************************
included: /etc/ansible/in.yml for servera => (item=1)
included: /etc/ansible/in.yml for servera => (item=2)

TASK [debug] ************************************************************************************************************************************
 [WARNING]: The loop variable 'item' is already in use. You should set the `loop_var` value in the `loop_control` option for the task to
something else to avoid variable collisions and unexpected behavior.

ok: [servera] => (item=a) => {
    "msg": "a task1 in in.yml"
}
ok: [servera] => (item=b) => {
    "msg": "b task1 in in.yml"
}
ok: [servera] => (item=c) => {
    "msg": "c task1 in in.yml"
}

TASK [debug] ************************************************************************************************************************************
 [WARNING]: The loop variable 'item' is already in use. You should set the `loop_var` value in the `loop_control` option for the task to
something else to avoid variable collisions and unexpected behavior.

ok: [servera] => (item=a) => {
    "msg": "a task1 in in.yml"
}
ok: [servera] => (item=b) => {
    "msg": "b task1 in in.yml"
}
ok: [servera] => (item=c) => {
    "msg": "c task1 in in.yml"
}

PLAY RECAP **************************************************************************************************************************************
servera                    : ok=4    changed=0    unreachable=0    failed=0

这个时候,可以看到最终item的值来自in.yml中的循环。那如果我就想要使用test_include中的循环的值怎么办? 我们再次修改test_include.yml以及in.yml如下:

[root@controller ansible]## cat test_include.yml
- hosts: test
  gather_facts: no
  tasks:
    - include: in.yml
      loop:
        - 1
        - 2
      loop_control:
        loop_var: outer_item
        
[root@controller ansible]# cat in.yml
- debug: 
    msg: "{{outer_item }} {{ item }} task1 in in.yml"
  loop: ['a','b','c']

再次查看结果:

PLAY [servera] **********************************************************************************************************************************

TASK [include] **********************************************************************************************************************************
included: /etc/ansible/in.yml for servera
included: /etc/ansible/in.yml for servera

TASK [debug] ************************************************************************************************************************************
ok: [servera] => (item=a) => {
    "msg": "1 a task1 in in.yml"
}
ok: [servera] => (item=b) => {
    "msg": "1 b task1 in in.yml"
}
ok: [servera] => (item=c) => {
    "msg": "1 c task1 in in.yml"
}

TASK [debug] ************************************************************************************************************************************
ok: [servera] => (item=a) => {
    "msg": "2 a task1 in in.yml"
}
ok: [servera] => (item=b) => {
    "msg": "2 b task1 in in.yml"
}
ok: [servera] => (item=c) => {
    "msg": "2 c task1 in in.yml"
}

PLAY RECAP **************************************************************************************************************************************
servera                    : ok=4    changed=0    unreachable=0    failed=0 

可以看到,outer_item中的值正是外层循环中item的值。当出现这个双层循环时,可以在外层循环中使用loop_var选项指定一个变量,这个变量用于替代外层循环中的item变量,以便在内层循环中获取到外层循环的item的值,从而避免两层循环中item变量名的冲突。

17.1.2、handlers include

handlers include与tasks include大体类似,直接给例子:

# handlers1.yml内容如下:
# this might be in a file like handlers/handlers.yml
- name: restart apache
  service: 
     name: apache 
     state: restarted
  
# handlers.yml包含handlers1.yml示例:
handlers:
  - include: handlers/handlers.yml

17.1.3、playbook include

include也可以用于将一个playbook导入到另一个playbook中:

- name: this is a play at the top level of a file
  hosts: all
  remote_user: root
  tasks:
  - name: say hi
    tags: foo
    shell: echo "hi..."
- include: load_balancers.yml
- include: webservers.yml
- include: dbservers.yml

17.2、include_tasks

17.2.1、基本使用

在前面我们详细说了include的用法,然而事实上在后续的ansible版本当中,include语法可能会被弃用。而使用一些新的关键字来代替include的原始用法,include_tasks就是其中之一。

我们知道include可以用于包含tasks,handlers,playbooks等,而include_tasks则专门用于包含tasks:

[root@controller ansible]# cat include_tasks_ex.yml
- hosts:
  gather_facts: no
  tasks:
    - debug:
        msg: "task1"
    - include_tasks: in.yml
    - debug:
        msg: "task2"
        
[root@controller ansible]# cat in.yml
- debug: 
    msg: "{{ item }} task1 in in.yml"
- debug:
    msg: "{{ item }} task2 in in.yml"

执行结果如下:

PLAY [servera] **********************************************************************************************************************************

TASK [debug] ************************************************************************************************************************************
ok: [servera] => {
    "msg": "task1"
}

TASK [include_tasks] ****************************************************************************************************************************
included: /etc/ansible/in.yml for servera

TASK [debug] ************************************************************************************************************************************
ok: [servera] => {
    "msg": "task1 in in.yml"
}

TASK [debug] ************************************************************************************************************************************
ok: [servera] => {
    "msg": "task2 in in.yml"
}

TASK [debug] ************************************************************************************************************************************
ok: [servera] => {
    "msg": "task2"
}

PLAY RECAP **************************************************************************************************************************************
servera                    : ok=5    changed=0    unreachable=0    failed=0 

可以看到,当我们使用include_tasks时,include_tasks本身会被当做一个task,这个task会把include的文件的路径输出在控制台中中, 这就是include_tasks和include之间的区别。include是透明的,而include_tasks是可见的,include_tasks更像是一个任务,这个任务包含了其他的一些任务。

在ansible 2.7版本当中,include_tasks还加入了新的参数,下面是一个简单用法示例:

include_tasks:
  file: in.yml

当然这种使用方法与include_tasks: in.yml的效果完全相同。

17.2.2、在include_tasks中使用tags

在前面我们提到过,如果为include添加tags,那么tags是对include中所有任务生效的。也就是说,如果调用include对应的tag,那么include文件中的所有任务都会执行。

但是对include_tasks添加tags,则只会对include_tasks本身生效,include_tasks中所有的任务都不生效。示例如下:

[root@controller ansible]# cat include_tasks_ex.yml 
- hosts: test
  gather_facts: no
  tasks:
    - debug:
        msg: "test task1"
    - include_tasks:
        file: in.yml
      tags: t1
    - debug:
        msg: "test task3"
        
[root@controller ansible]# cat in.yml 
- debug:
    msg: "test task2"

执行结果如下:

[root@controller ansible]# ansible-playbook  include_tasks_ex.yml  --tags t1

PLAY [test] ******************************************************************************************************

TASK [include_tasks] *********************************************************************************************
included: /etc/ansible/in.yml for 10.1.61.187

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

如果想要tags对include_tasks中包含的所有任务生效,则需要使用include_tasks模块的apply参数并配合tags: always内置tag:

- hosts: test
  gather_facts: no
  tasks:
    - debug:
        msg: "test task1"
    - include_tasks:
        file: in.yml
        apply:
          tags: t1
      tags: always
    - debug:
        msg: "test task3"

执行结果:

[root@controller ansible]# ansible-playbook include_tasks_ex.yml --tags t1

PLAY [test] ******************************************************************************************************

TASK [include_tasks] *********************************************************************************************
included: /etc/ansible/in.yml for 10.1.61.187

TASK [debug] *****************************************************************************************************
ok: [10.1.61.187] => {
    "msg": "test task2"
}

PLAY RECAP *******************************************************************************************************
10.1.61.187                : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

在上一篇我们讲到tags的时候说过,如果一个任务被打上了tags: always标签,则即使我们调用其他任务的标签,该任务也会被执行。

- hosts: test
  gather_facts: no
  tasks:
    - debug:
        msg: "test task1"
    - include_tasks:
        file: in.yml
        apply:
          tags: t1,always
      tags: always
    - debug:
        msg: "test task3"

需要说明的是,在这里,tags: always标签只针对include_tasks本身生效,也就是说,如果其他任务的标签被调用,include_tasks本身会被调用,而其包含的任务不会被调用。如果要想其包含的任务也总是被调用,可修改配置如下:

17.3、import_tasks

import_tasks与include_tasks用法类似,都用于包含一个任务列表:

[root@controller ansible]# cat import_tasks_ex.yml
- hosts: test
  gather_facts: no
  tasks
    - debug: 
        msg: "test task1"
    - import_tasks: in.yml

[root@controller ansible]# cat in.yml 
- debug:
    msg: "test task2"

执行结果:

[root@controller ansible]# ansible-playbook import_tasks_ex.yml 

PLAY [test] ******************************************************************************************************

TASK [debug] *****************************************************************************************************
ok: [10.1.61.187] => {
    "msg": "test task1"
}

TASK [debug] *****************************************************************************************************
ok: [10.1.61.187] => {
    "msg": "test task2"
}

PLAY RECAP *******************************************************************************************************
10.1.61.187                : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

可以看到,import_tasks模块并不会像include_tasks模块一样,在控制台输出自身的任务信息,其相对透明。

除此之外,import_tasks和include_tasks还有如下不同:

  1. import_tasks是静态的,被import的文件在playbook被加载时就预处理了,而include_tasks是动态的,被include的文件在playbook被运行时候才开始处理。一个简单的例子:
- hosts: test
  gather_facts: no
  vars: 
    file_name: in.yml
  tasks:
    - include_tasks: {{ file_name }}
    - import_tasks: {{ file_name }}
在上面的示例中,`include_tasks`和`import_tasks`均会被执行。

再看下面的例子:
- hosts: test
  gather_facts: no
  tasks:
    - set_fact:
        file_name: in.yml
    - include_tasks: {{ file_name }}
    - import_tasks: {{ file_name }}
此时,`import_tasks`就会出错:
[root@controller ansible]# ansible-playbook include_import_tasks_ex.yml
ERROR! Error when evaluating variable in import path: {{ file_name }}.

When using static imports, ensure that any variables used in their names are defined in vars/vars_files
or extra-vars passed in from the command line. Static imports cannot use variables from facts or inventory
sources like group or host vars.

当使用静态的import时,请确保文件名中使用到的变量被定义在vars、vars_files或者extra-vars中,不支持其他的方式传入变量。

  1. 如果想要对包含的任务列表进行循环操作,则只能使用include_tasks,import_tasks不支持循环操作。也就是说,使用loop或者with_X对include文件进行循环操作时,只能配合include_tasks才能正常使用
  2. 当使用when对include文件添加条件判断时,include_tasks和import_tasks

有着本质的不同:

  • 当对include_tasks使用when时,when对应的条件只会应用于include_tasks任务本身,当执行被包含的任务时,不会对这些被包含的任务重新进行条件判断
  • 当对import_tasks使用when时,when对应的条件会被应用于被import的文件中的每一个任务,当执行被import的任务时,会对每一个被包含的任务进行同样的条件判断。

示例如下:

[root@controller ansible]# cat include_import_tasks_ex2.yml              
- hosts: test
  gather_facts: no
  tasks:
    - name: set testvar to 0
      set_fact: 
         testnum: 0
    - debug:
        msg: 'include_tasks: in1.yml'
    - include_tasks: in1.yml
      when: testnum == 0

    - name: set testvar to 0 
      set_fact: 
        testnum: 0
    - debug:
        msg: 'import_tasks: in1.yml'
    - import_tasks: in1.yml
      when: testnum == 0

执行结果:

[root@controller ansible]# ansible-playbook include_import_tasks_ex2.yml 

PLAY [test] ******************************************************************************************************

TASK [set testvar to 0] ******************************************************************************************
ok: [10.1.61.187]

TASK [debug] *****************************************************************************************************
ok: [10.1.61.187] => {
    "msg": "include_tasks: in1.yml"
}

TASK [include_tasks] *********************************************************************************************
included: /etc/ansible/in1.yml for 10.1.61.187

TASK [set_fact] **************************************************************************************************
ok: [10.1.61.187]

TASK [debug] *****************************************************************************************************
ok: [10.1.61.187] => {
    "msg": "test task2"
}

TASK [set testvar to 0] ******************************************************************************************
ok: [10.1.61.187]

TASK [debug] *****************************************************************************************************
ok: [10.1.61.187] => {
    "msg": "import_tasks: in1.yml"
}

TASK [set_fact] **************************************************************************************************
ok: [10.1.61.187]

TASK [debug] *****************************************************************************************************
skipping: [10.1.61.187]

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

17.4、在handlers中使用include_tasks及import_tasks

我们知道,handlers中执行的其实也是任务,只不过是被触发才会运行,所以如果要在handlers中引入任务,也可直接使用include_tasks和import_tasks。没有include_handlers的说法。

17.5、import_playbook

我们在前面提到过,include除了可以引用任务列表,还可以引用整个playbook,在之后的版本中,如果想要引入playbook,则需要使用import_playbook模块。在2.8版本后,使用include引用整个playbook的特性会被弃用。

示例:

[root@controller ansible]# cat import_playbook_ex.yml
- hosts: test
  gather_facts: no
  tasks:
    - debug:
        msg: "test task"
- import_playbook: inplay.yml

[root@controller ansible]# cat inplay.yml
- hosts: test
  gather_facts: no
  tasks:
    - debug:
        msg: "test task in inplay.yml"

18. Ansible Playbook Roles

18.1、角色(roles)

在Ansible中,role是将playbook分割为多个文件的主要机制。它大大简化了复杂playbook的编写,同时还使得它们非常易于复用。

18.1.1、role的基本构成

roles文件组织结构示例:

group_vas/
site.yml
webservers.yml
roles/
   common/
     files/
     templates/
     tasks/
     handlers/
     vars/
     defaults/
     meta/
   webservers/
     files/
     templates/
     tasks/
     handlers/
     vars/
     defaults/
     meta/

roles各目录的作用及可用的文件:

  • files:用于存放一些非模板文件的文件,如https证书等。
  • tempaltes:用于存放角色相关的Jinja2模板文件,当使用角色相关的模板时,如未明确指定模板路径,则默认使用此目录中的模板
  • tasks:角色所要执行的所有任务文件都存放于此,包含一个主文件main.yml,可以在主文件中通过include的方式引入其他任务文件
  • handlers:用于定义角色中需要调用 的handlers,包含一个主配置文件main.yml,可通过include引入其他的handlers文件。
  • vars:用于定义此角色用到的变量,包含一个主文件main.yml
  • meta:用于存储角色的元数据信息,这些元数据用于描述角色的相关属性,包括作者,角色的主要作用,角色的依赖关系等。默认这些信息会写入到当前目录下的main.yml文件中
  • defaults:除了vars目录,defaults目录也用于定义此角色用到的变量,与vars不同的是,defaults中定义的变量的优先级最低。

创建role的步骤如下:

  1. 创建以roles命名的目录
  2. 在roles目录中分别创建角色名称命名的目录,如websrvs等
  3. 在每个角色命名的目录中分别创建files、handlers、meta、tasks、teamplates和vars目录,用不到的目录可以创建为空目录,也可以不创建。
  4. 在playbook文件中,调用各角色

需要说明的是,以上目录并不都是必须的,如果你的roles当中并不需要用到某一个目录,也可以不用创建,比如我们将所有的变量都放到defaults中,则可以不需要vars目录,如果未用到模板文件,则不需要templates目录。

18.1.2、在playbook中使用roles

基本引用的方法:

- hosts: webservers
  roles:
     - common
     - webserver

也可以通过如下方法引用时带入变量:

---
- hosts: webservers
  roles:
    - common
    - role: foo_app_instance
      vars:
        dir: '/opt/a'
        app_port: 5000
      tags: typeA
    - role: foo_app_instance
      vars:
        dir: '/opt/b'
        app_port: 5001
      tags: typeB

还可以在引用时使用条件语句:

- hosts: webservers
  roles:
    - role: some_role 
      when: "ansible_os_family == 'RedHat'" 

通过include引入role

- hosts: webservers
  tasks:
    - name: Include the some_role role
      include_role:
        name: some_role
      when: "ansible_facts['os_family'] == 'RedHat'"

18.1.3、pre_tasks和post_tasks

如果在执行一个role时,需要在其前或其后依然要执行某些任务,我们可以使用pre_tasks及post_tasks来声明。pre_tasks是在role之前执行,而post_tasks则在role之后执行:

- name: deply webservers
  host: webservers
  vars_files:
    - secrets.yml
  pre_tasks:
    - name: update yum cache
      yum: update_cache=yes
  roles:
    - role: apache
      database_host: {{ hostvars.db.ansible_eth0.ipv4.address }}
      domains:
        - exampel.com
        - www.example.com
  post_tasks:
    - name: print something
      shell: echo "The roles have been updated!"

18.1.4、role的依赖

如果当前role在执行前需要依赖另一个role,我们可以在roles的meta目录中的main.yml中定义role的依赖关系。

示例1:

# roles/myapp/meta/main.yml
---
dependencies:
  - role: common
    vars:
      some_parameter: 3
  - role: apache
    vars:
      apache_port: 80
  - role: postgres
    vars:
      dbname: blarg
      other_parameter: 12

18.2、Ansible Galaxy

ansible-galaxy是一个工具,我们可以利用它快速的创建一个标准的roles目录结构,还可以通过它在Ansible Galaxy上下载别人写好的roles,直接拿来用。

初始化一个roles的目录结构:

[root@controller ansible]# ansible-galaxy init my_new_role

安装别人写好的roles:

[root@controller ansible]# ansible-galaxy install -p /etc/ansible/roles bennojoy.mysql

列出已安装的roles:

[root@controller ansible]# ansible-galaxy list

查看已安装的roles信息:

[root@controller ansible]# ansible-galaxy info bennojoy.mysql

卸载roles:

[root@controller ansible]# ansible-galaxy remove bennojoy.mysql

补充:通过ansible-galaxy初始化一个collection:

[root@controller ansible]# ansible-galaxy collection init ns.collection

collection集合案例:

#安装clooection
[root@controller ansible]# ansible-galaxy collection install community.mysql

[root@controller ansible]# cat mysql.yaml 
- hosts: localhost
  tasks:
     - name: install mysql
       yum:
          name: 
             - mariadb-server
             - mariadb
             - MySQL-python
          state: present
     - name: start myriqdb
       service:
          name: mariadb
          state: started
          enabled: yes
     - name: Removes anonymous user account for localhost
       community.mysql.mysql_user:
          name: ''
          host: localhost
          state: absent
     - name: Create database user with name 'bob' and password '12345' with all database privileges
       community.mysql.mysql_user:
         name: bob
         password: 12345
         priv: '*.*:ALL'
         state: present

19、Ansible Vault配置加密

简介

在使用ansible的过程中,不可避免的会存储一些敏感信息,比如在变量文件中存储帐号密码信息等。

ansible通过ansible-vault命令行工具来提供对敏感文件的加密和解密。

ansible-vault可以创建、加密、解密和查看文件。其可以加密任何ansible使用的文件,包括inventory文件,playbook中调用的变量文件等。

19.1、Ansible-vault常用操作

1、​​​​​​​创建加密文件

[root@controller ansible]# ansible-vault create file

2、编辑加密文件

[root@controller ansible]# ansible-vault edit file

3、重置密码

[root@controller ansible]# ansible-vault rekey file

4、加密已有文件

[root@controller ansible]# ansible-vault encrypt file

5、解密文件

[root@controller ansible]# ansible-vault decrypt file

6、查看文件

[root@controller ansible]# ansible-vault view file

19.2、Ansible-vault配置示例

1、创建一个user.yml的变量文件,内容如下:

username: "user1"
pwhash: "$1$GkTPu7we$ZZtdsLPIHkS.fmoVcn3v51"

2、加密上面创建的变量文件:

[root@controller ansible]# ansible-vault encrypt user.yml 
New Vault password: 
Confirm New Vault password: 
Encryption successful

3、编写playbook文件如下:

- name: create user accounts for all our servers
  hosts: test
  become: True
  remote_user: ansible
  vars_files:
    - user.yml
  tasks:
    - name: Creating user from user.yml
      user:
        name: "{{ username }}"
        password: "{{ pwhash }}"

4、执行playbook

[root@controller ansible]# ansible-playbook create_user.yml --ask-vault-pass
Vault password: 

也可以通过如下操作执行playbook:

[root@controller ansible]# echo redhat > vault-pass
[root@controller ansible]# chmod 600 vault-pass
[root@controller ansible]# ansible-playbook create_user.yml --vault-password-file=vault-pass

20、动态Inventory管理

20.1、动态主机管理模块

20.1.1、add_host

在playbook执行的过程中,动态的添加主机到指定的主机组中

常用参数:

  • groups:添加主机至指定的组
  • name:要添加的主机名或IP地址

示例:

- name: add a host to group webservers
  hosts: webservers
  tasks:
    - add_host:
        name: "{{ item }}" 
        group: webservers 
        foo=42 #添加主机到webservers组中,主机的变量foo的值为42
      loop:
        - host1
        - host2
        
    - debug:
        msg: "{{ groups.webservers }}"

20.1.2、group_by

在playbook执行的过程中,动态的创建主机组

示例:

- name: Create operating system group
  hosts: all
  tasks:
    #在playbook中设置一个新的主机组
    - group_by: 
        key: "os_{{ ansible_distribution }}"
    
- name: Run on CentOS hosts only
  hosts: os_CentOS
  tasks:
    - name: Install Apache
      yum: 
        name: httpd 
        state: latest
      
- name: Run on Ubuntu hosts only
  hosts: os_Ubuntu
  tasks:
    - name: Install Apache
      apt: 
        name: apache2 
        state: latest

20.2、动态inventory管理

20.2.1动态inventory简介

在前面我们所有的选取主机组的操作都是通过维护inventory文件来完成的。而事实上,当在大规模应用当中,如果主机达成千上万台,这个时候还手动维护inventory文件将会给运维工作带来巨大的挑战。

在这种大规模的应用场景中,通常的做法是,将所有的主机都存储在cmdb当中。当需要对某一组主机或者某一类型的主机执行相应操作时,通过cmdb将相应主机取出来,动态的生成inventory,然后交由ansible处理即可。

所以其实Ansible Inventory包含静态inventory和动态inventory两部分。而我们前面通过手动在inventory文件中维护主机列表的方式即称之为静态inventory。而动态inventory则是指通过外部脚本获取主机列表,并按照ansible所要求的格式返回给ansible指令的操作方式。

动态inventory一般都会结合cmdb或者云计算平台等获取主机信息,由于主机资源一般会动态的进行增减,而这些系统一般会智能更新。我们需要通过这些工具提供的api或者接入库查询等方式返回主机列表。

20.2.2、动态inventory脚本规约

动态inventory脚本最终返回的满足ansible输出格式的json数据。ansible对于使用什么语言来实现动态inventory没有要求。但脚本必须支持两个参数:

  • --list:用于返回所有的主机组信息,每个组所包含的主机列表hosts、子组列表children、主机变量列表vars都应该是字典形式的,而_meta则用于存放主机变量
  • --host :返回指定主机的变量列表,也可返回一个空字典

20.2.3、动态inventory脚本示例

[root@controller ansible]# cat dynamic_inventory.py

#!/usr/bin/env python
# coding: utf-8
import os
import sys
import argparse
 
try:
    import json
except ImportError:
    import simplejson as json
 
class ExampleInventory(object):
 
    def __init__(self):
        self.inventory = {}
        self.read_cli_args()
 
        # Called with `--list`.
        if self.args.list:
            self.inventory = self.inventory_groups()
        # Called with `--host [hostname]`.
        elif self.args.host:
            # Not implemented, since we return _meta info `--list`.
            self.inventory = self.inventory_hosts(self.args.host)
        # If no groups or vars are present, return empty inventory.
        else:
            self.inventory = self.empty_inventory()
 
        print json.dumps(self.inventory);
 
    # Example inventory for testing.
 
    def inventory_groups(self):
        return {
            "webserver":  # 定义webserver组
            {
                "hosts": ["10.10.0.112"],  # webserver 组内主机
                "vars": {  # 组变量
                    "ansible_ssh_pass": "123456",  # Inventory 内置变量
                    "ansible_ssh_port": "27100"
                }
            },
            "dbserver":
            {
                "hosts": ["10.10.0.109"],
                "vars": {
                    "ansible_ssh_pass": "123456",
                    "ansible_ssh_port": "27100"
                    }
            },
            '_meta': {
                'hostvars': {
                    '10.10.0.112': {
                        'host_specific_var': 'foo'
                    },
                    '10.10.0.109': {
                        'host_specific_var': 'bar'
                    }
                }
            }
        }

    def inventory_hosts(self,host):
        if host == "10.10.0.112":
            return {
              '_meta': {
                'hostvars': {
                    '10.10.0.112': {
                        'host_specific_var': 'foo'
                    }
                }
              }
            }
        elif host == "10.10.0.109":
            return {
              '_meta': {
                'hostvars': {
                    '10.10.0.109': {
                        'host_specific_var': 'bar'
                    }   
                }   
              }   
            }      
        else:
            return {'_meta': {'hostvars': {}}}
 
    # Read the command line args passed to the script.
    def read_cli_args(self):
        parser = argparse.ArgumentParser()
        parser.add_argument('--list', help="list hosts", action = 'store_true')
        parser.add_argument('--host', help="display hostvars for host",action = 'store')
        self.args = parser.parse_args()
 
# Get the inventory.
ExampleInventory()

脚本需要设置x权限,否则ansible会提示没有权限调用:

[root@controller ansible]# chmod +x ./dynamic_inventory.py

执行该脚本,返回如下:

[root@controller ansible]#  ./dynamic_inventory.py --list
{"webserver": {"hosts": ["10.10.0.112"], "vars": {"ansible_ssh_port": "27100", "ansible_ssh_pass": "123456"}}, "_meta": {"hostvars": {"10.10.0.112": {"host_specific_var": "foo"}, "10.10.0.109": {"host_specific_var": "bar"}}}, "dbserver": {"hosts": ["10.10.0.109"], "vars": {"ansible_ssh_port": "27100", "ansible_ssh_pass": "123456"}}}

[root@controller ansible]# ./dynamic_inventory.py --host 10.10.0.109
{"_meta": {"hostvars": {"10.10.0.109": {"host_specific_var": "bar"}}}}

[root@controller ansible]# ./dynamic_inventory.py --host 192.168.0.1
{"_meta": {"hostvars": {}}}

通过ansible操作示例如下:

[root@controller ansible]# ansible -i dynamic_inventory.py webserver --list-hosts
  hosts (1):
    10.10.0.112

[root@controller ansible]# ansible -i dynamic_inventory.py all --list-hosts
  hosts (2):
    10.10.0.112
    10.10.0.109

21. Ansible性能调优

简介

在当前配置管理工具大行其道的应用中,ansible凭借其轻量级、agentless等特性得以占据一席之地。然而其也并不是完美无缺,事实上,其最为人所诟病的就是在大规模服务器应用场景当中表现出的性能问题。所以本篇文档就来说一说如何通过一些配置优化来加速ansible的运行。

21.1、开启ansible性能监测

ansible附带了一组回调插件,可以通过callback_whitelist 指令在ansible.cfg文件中启用这些插件

[default]

callback_whitelist = timer, profile_tasks, profile_roles, cgroup_perf_recap

[callback_cgroup_perf_recap]
control_group=ansible_profile
  • cgroup_perf_recap可查看一个playbook消耗的cpu和内存
  • timer 可用于查看playbook执行所消耗的时间
[root@controller ansible]# yum install whatprovides cgcreate 

[root@controller ansible]# yum install -y libcgroup-tools


# 创建cgroup
[root@controller ansible]# cgcreate -a group:user -t group:user -g cpuacct,memory,pids:ansible_profile

[root@controller ansible]# ansible-playbook deploy_webservers.yml

也可通过如下指令直接执行:

[root@controller ansible]# cgexec -g cpuacct,memory,pids:ansible_profile ansible-playbook deploy_webservers.yml

21.2、任务优化

- hosts: web,db
  tasks:
  - name: install package no use loop 
    yum:
      name: 
      - httpd
      - mod_wsgi
      - mod_ssl
      - targetcli
      state: latest
    when: ansible_facts['fqdn']  == "servera.lab.example.com"

  - name: install package  use loop 
    yum:
      name: "{{ item }}""
      state: latest
      loop:
      - httpd
      - mod_wsgi
      - mod_ssl
      - targetcli
    when: ansible_facts['fqdn'] == "serverb.lab.example.com"
  • 高效复制文件到被控端
- name: copy files to target
  copy:
    src: test_files
    dest: /mnt

- name: copy files to target
  synchronize:
    src: test_files
    dest: /mnt
  • 尽可能可能的使用模板取代lineinfile

21.3、优化ssh连接

默认情况下,ansible基于ssh连接被控端,当ansible在运行playbook的时候,会建立很多的ssh连接来执行相应的task,而每个task都会创建一个新的ssh连接,ssh连接的建立毫无疑问需要额外tcp建立连接的开销。

openssh支持一个优化,叫做ssh multiplexing,也被称作ControlPersist。当启用了该特性,则多个连接到相同主机的ssh会话将会共享相同的tcp连接。这样就只有第一次连接的时候需要进行TCP三次握手。

当启用Multiplexing后:

  • 第一次ssh连接到主机的时候,openssh会创建一个主连接
  • 紧接着openssh会创建一个控制套接字,通过主连接与远程主机关联
  • 当有新的ssh尝试连接到主机时,openssh将使用控制套接字与远程主机通信,并不会创建新的tcp连接

开启该配置项:

[root@controller ansible]# cat ansible.cfg
[ssh_connection]
ssh_args = -C -o ControlMaster=auto -o ControlPersist=60s


配置项说明:

  • ControlMaster:启用ssh multiplexing,允许多个同时与远程主机连接的ssh会话使用单一网络连接。auto用于告诉ssh在主连接和控制套接字不存在的情况下,自动创建它们。第一个ssh会话建立,则与同一主机连接的其他会话重复利用此连接,从而绕过较慢的初始过程,ssh在最后一个会话关闭后,立即销毁共享的连接
  • ControlPersist:使连接在后台保持打开,而不是在上一次会话后销毁连接。其配置空闲的连接保持打开的时间长度,每个新会话将重置此空闲计时器。

21.4、pipelining

在说明pipelining之前,需要先了解下ansible是如何执行一个task的:

  1. 基于playbook中的task生成一个python脚本
  2. 将生成的python脚本复制到被控主机上
  3. 在被控主机上运行这个脚本

上面三个步骤中,后面两个步骤会产生两个ssh会话。而在pipelinin模式下,ansible执行python脚本时并不会复制它,而是通过管道传递给ssh会话。这样一来,就会让原本需要的两个ssh会话减少为一个,以降低开销,节省时间。

启用pipelining:

[root@controller ansible]# cat ansible.cfg
[defaults]
pipelining = True

需要注意的是,如果开启pipelining,则需要在被控端的/etc/sudoers文件中关闭requiretty:

[root@controller ansible]# cat /etc/sudoers.d/ansible
ansible    ALL=(ALL)       NOPASSWD:ALL
Defaults:ansible !requiretty

21.5、并发执行

默认情况下,ansible在批量连接客户端执行play时的并发数是5个,可以通过调整该值来提高并发数:

[defaults]
forks = 20

即使提高了并发个数,playbook也可能在执行一个需要较长时间的任务时导致阻塞,针对这类任务,可以使用异步的方式来运行:

- hosts: all
  tasks:
    - name: Install mlocate
      yum: 
        name: mlocate 
        state: installed
    - name: Run updatedb
      command: /usr/bin/updatedb
      async: 300
      poll: 10

当使用上面的任务控制超过forks设置的节点个数时,'install mlocate'任务会先在forks个节点上跑,完成后再继续下一批,这个并发数是由我们设置的forks选项控制的。而'Run updatedb'这个任务会一次性在所有节点上都执行,执行的超时时间为300s,然后每10s轮循一次检查它是否完成。需要说明的是,因为需要等待其执行完成,所以如果这个任务比较耗时,仍然需要等待其执行完毕后才能开始下一个任务。

但是如果该任务只是一个后台任务,比如只是在后台执行一个脚本或者启动一个服务,不需要等待其返回结果。那就不需要检查这个任务是否完成,只需要继续执行其它任务,最后再使用wait_for这个模块去检查之前的脚本或者服务是否按预期中运行了便可。这时候只需要把poll的值设置为0,便可以按上面的要求配置ansible不等待job的完成。

还有一种需求,假如一个task,它就是需要运行很长的时间,不能让它超时退出,需要一直等待这个task完成。这个时候就需要将async的值设置为0。

总结来说,大概有以下的一些场景需要使用到ansible的polling特性:

  • 某个task需要运行很长的时间,这个task很可能会达到timeout。
  • 某个task需要在大量的机器上面运行
  • 某个task是不需要等待它完成的

当然也有一些场景是不适合使用:

  • 这个任务是需要运行完后才能继续另外的任务的
  • 这个任务能很快的完成

22. Ansible调试

22.1、运行前检查

1、当我们在运行ansible-playbook时,使用--check选项时,将不会对受控主机作出任何更改,而是通过模拟运行的方式执行所有task,以用于检查playbook在运行时的状态:

[root@controller ansible]# ansible-playbook foo.yml --check

2、在运行ansible-playbook时,如果使用--diff选项配合--check选项,可以用于检查本次执行play时,相较上一次产生了哪些改变:

[root@controller ansible]# ansible-playbook foo.yml  --check --diff --limit foo.example.com

3、有些时候,我们在检测模式下运行play时,我们会希望某个play总是运行,我们可以使用always_run子句:

tasks:
  - name: this task is run even in check mode
    command: /something/to/run --even-in-check-mode
    always_run: yes
> 需要说明的是,如果一个task中同时包含when和always_run,如果when返回了false,即使alwys_run为true,任务依然会被跳过。

22.2、打印详细输出信息

参考《6. Ansible Playbook基本使用》

22.3、使用debug模块

在前面debug模块使用的比较多了,这里直接再给个示例:

tasks:
  - debug:
      var: myvariable
  - debug:
      msg: "The value of myvariable is {{ var }}"

22.4、使用assert模块

assert模块会在指定的条件不符合的时候返回错误并失败退出。

# 当目标机没有eth1网卡时则playbook会返回失败
- name: assert that eth1 interface exists
  assert:
    that: ansible_eth1 is defined

当调试playbook的时候,插入assert模块在我们设定的某些条件不成立时立刻失败,对调试很有用。

下面示例用于检查目标文件是否是一个目录,如果不是,则失败退出:

- name: stat /opt/foo
  stat: path=/opt/foo
  register: st
  
- name: assert that /opt/foo is a directory
  assert:
    that: st.stat.isdir

22.5、限制特定的task运行

22.5.1、指定任务执行

可以通过--start-at-task参数告诉Ansible从指定的task开始运行playbook,而不是从头开始运行。如果你的playbook因为某一个task中有bug而失败了,在你修复了这个bug后希望从被修复的这个task开始再次执行playbook的时候,就可以使用这个参数。

# 以下命令会从名为"install packages"的任务开始执行playbook

[root@controller ansible]# ansible-playbook playbook.yml --start-at-task="install packages"

22.5.2、分步执行

可以通过--step选项来交互式的执行playbook:

[root@controller ansible]# ansible-playbook playbook.yml --step

这样ansible在每个任务前会自动停止,并询问是否应该执行该任务。

假如有一个名为"configure ssh"的任务,playbook执行到这里会停止并询问:

Perform task: configure ssh (y/n/c):
  • "y"会执行该任务
  • "n"会跳过该任务
  • "c"则会继续执行剩余的所有任务而不再询问

22.5.3、tags

参考《17. Ansible Playbook之tags》

23. Ansible lineinfile模块

简介

之所以专门说一说这个模块,是因为lineinfile在实际使用中非常有用。

实际上,在大多数时候,我们在linux上的操作,就是针对文件的操作,通过配置管理工具对配置文件作统一的配置修改是一个非常酷的功能。

下面是官方针对该模块的说明:

lineinfile - Ensure a particular line is in a file, or replace an existing line using a back-referenced regular expression

简单讲,这个模块就是针对一个文件中行内容的操作。

下面我们详细说一说其具体可以做的事情。

23.1、修改匹配行

下面是一个简单的task示例:

# 将/etc/selinux/config中匹配到以'SELINUX='开头的行,将其替换为'SELINUX=disabled'
- name: modify selinux to disabled
  lineinfile:
    path: /etc/selinux/config
    regex: '^SELINUX='
    line: 'SELINUX=disabled'

23.2、在匹配行前或后添加内容

示例文件如下:

[root@controller ansible]# cat /etc/http.conf

Listen 127.0.0.1:80
Listen 80
Port

23.3、在匹配行前添加

在http.conf文件的Listen 80前面添加一行Listen 8080,task示例如下:

- name: add line before Listen 80
  lineinfile:
    dest: /etc/http.conf
    insertbefore: '^Listen 80'
    line: 'Listen 8080'

23.4、在匹配行后添加

在http.conf文件的Port后面添加一行just a test,task示例如下:

- name: add line before Listen 80
  lineinfile:
    dest: /etc/http.conf
    insertafter: '^Port'
    line: 'just a test'

23.5、修改文件内容及权限

示例文件:

[root@controller ansible]# cat /etc/hosts

127.0.0.1       localhost.localdomain localhost ::1       localhost6.localdomain6 localhost6
10.1.61.130     hub.dz11.com

修改/etc/hosts,将以127.0.0.1开头的行替换为 127.0.0.1 localhost,并将/etc/hosts的属主和属组都修改为root,权限改为644,如下:

- name: modify hosts
  lineinfile:
    dest: /etc/hosts
    regex: '^127\.0\.0\.1'
    line: '127.0.0.1 localhost'
    owner: root
    group: root
    mode: 0644

23.6、删除一行内容

示例原文件:

[root@controller ansible]# cat /etc/hosts

127.0.0.1       localhost.localdomain localhost ::1       localhost6.localdomain6 localhost6
10.1.61.130     hub.dz11.com

删除以10.1.61.130开头的行:

- name: delete a line
  lineinfile:
    dest: /etc/hosts
    regex: '^10\.1\.61'
    state: absent

文件存在则添加一行内容

往/etc/hosts里添加一行10.1.61.131 test.dz11.com(多次执行,不会重复添加),示例如下:

- name: add a line
  lineinfile:
    path: /etc/hosts
    line: '10.1.61.131 test.dz11.com'

23.7、如果有匹配的行则修改该行,如果不匹配则添加

示例原文件/tmp/test.txt内容如下:

# %wheel   ALL=(ALL)   ALL

下面的示例task中,匹配以%wheel开头的行,匹配到,则执行替换,未匹配,则添加。因为原文件中,没有以%wheel开头的行,所以会添加一行:

- name: add or modify a line
  lineinfile: 
    path: /tmp/test.txt
    regex: '^%wheel'
    line: '%wheel  ALL=(ALL)       NOPASSWD: ALL'

修改后的文件如下:

[root@controller ansible]# cat /tmp/text.txt

# %wheel   ALL=(ALL)   ALL
%wheel  ALL=(ALL)       NOPASSWD: ALL

23.8、参数backrefs,backup说明

  • backup: 是否备份原文件,默认为no
  • backrefs:
    • 当backrefs为no时,如果regex没有匹配到行,则添加一行,如果Regx匹配到行,则修改该行
    • 当backrefs为yes时,如果regex没有匹配到行,则保持原文件不变,如果regex匹配到行,则修改该行
    • backrefs默认为no,所以上面那个示例中,我们没有配置backrefs,而默认没有匹配,则修改。

下面我们看一看backrefs为yes时匹配到行的示例:

示例原文件:

[root@controller ansible]# cat /tmp/testfile

# %wheel   ALL=(ALL)   ALL
%wheel  ALL=(ALL)       NOPASSWD: ALL
#?bar

task示例:

- name: test backrefs
 lineinfile:
     backup: yes
     state: present
     path: /tmp/testfile
     regexp: '^#\?bar'
     backrefs: yes
     line: 'bar'

修改后的文件:

[root@controller ansible]# cat /tmp/testfile

# %wheel   ALL=(ALL)   ALL
%wheel  ALL=(ALL)       NOPASSWD: ALL
bar

23.9、使用validate验证文件是否正确修改

在一些场景下,我们修改完文件后,需要对文件做一下测试,用以检查文件修改之后,是否能正常运行。如http.conf、nginx.conf等,一旦改错,而不加以测试,可能会直接导致http服务挂掉。

可以使用validate关键字,在修改完成以后,对文件执行检测:

- name: test validate
  lineinfile:
      dest: /etc/sudoers
      state: present
      regexp: '^%ADMIN ALL='
      line: '%ADMIN ALL=(ALL)'
      validate: 'visudo -cf %s'
  tags:
    - testsudo

参考:ansible 下lineinfile详细使用_散人的技术博客_51CTO博客

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值