目录
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.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.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.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、分步执行
23. Ansible lineinfile模块
简介
23.1、修改匹配行
23.2、在匹配行前或后添加内容
23.3、在匹配行前添加
23.4、在匹配行后添加
23.5、修改文件内容及权限
23.6、删除一行内容
23.7、如果有匹配的行则修改该行,如果不匹配则添加
23.8、参数backrefs,backup说明
23.9、使用validate验证文件是否正确修改
[root@controller ~]# wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo
[root@controller ~]# yum clean all #清除缓存
[root@controller ~]# yum makecache #建立缓存
[root@controller ~]# yum install -y ansible #开始安装
[root@controller ~]# ansibel --version #查看Ansible的版本及信息
控制端使用ssh服务连接被控端
控制端默认使用root用户控制被控端,工作目录在用户家目录
Ansible底层为Python编写,因此Ansible的各种指令在执行时都会转换成Python脚本
1、下发Ansible命令时,先转换成Python脚本 然后临时保存到主控端的~/.ansible/tmp目录
2、执行Ansible命令时,使用ssh服务将主控端~/.ansible/tmp目录下的Python脚本发送到被控端,被控端执行脚本
3、执行完成后,将~/.ansible/tmp下的Python脚本删除
优先级:
最高:定义变量为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的文件进行编写即可)
[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则自动接收被控端的公钥)
在主机清单中写入被控端 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
被控端创建普通用户
[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'
操作对象:(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
一个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模块的帮助
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' #修改工作目录
[root@controller ~]# ansible all -a 'ls /root'
#要执行的脚本文件script.sh内容如下:
#/bin/bash
ifconfig
df -hT
# 执行ansible指令:
ansible 10.212.52.252 -m script -a 'script.sh'
用于向被控端推送公钥,通常用于在ansible第一次连接被控端时向其推送ansible主控端的管理公钥。
常用选项:
[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'
file模块主要用于远程主机上的文件操作,file模块包含如下选项:
[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" #修改文件权限
复制文件到远程主机,copy模块包含如下选项:
[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'" #检查文件的合法性
goup模块请求的是groupadd, groupdel, groupmod 三个指令。
[root@controller ansible]# ansible test -m group -a 'name=test gid=1001 state=present system=yes'
user模块是请求的是useradd, userdel, usermod三个指令
[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
使用yum包管理器来管理软件包,其选项有:
[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' #下载并安装网站上的包
使用apt包管理器来管理软件包,其选项有:
[root@controller ansible]# ansible all -m apt -a 'name=httpd state=latest'
用于管理服务
该模块包含如下选项:
[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"
[root@controller ansible]# ansible all -m systemd -a "name=httpd state=started enabled=yes daemon_reload=yes"
用于管理计划任务
包含如下选项:
[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'
使用rsync同步文件,其参数如下:
[root@controller ansible]# ansible all -m synchronize -a 'src=/root/ansible/ dest=/tmp/data archive=yes delete=yes' #归档同步目录
说明
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/
在块设备上创建文件系统
常用选项:
[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"'
配置挂载点
选项:
[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'
该模块主要用于从http、ftp、https服务器上下载文件(类似于wget),主要有如下选项:
[root@controller ~]# ansible all -m get_url -a 'url=http://controller/passwd dest=/tmp mode=0440' #从控制端下载文件到被控端的/tmp目录,并修改权限
用于解压文件,模块包含如下选项:
[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'
用于组装文件,即将多个零散的文件,合并一个大文件
常用参数:
- 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模块:
用于将被控端的文件发送到主控端
[root@controller ~]# ansible all -m fetch -m fetch -a 'src=/etc/httpd/conf/httpd.conf dest=/root/ansible/ flat=yes'
4.22、stat模块:
判断被控端文件是否存在
- hosts: servera
tasks:
- name: test stat
stat:
path: /etc/passwd
register: zzd
- name: debug state
debug:
msg: "{{ zzd }}"
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 。
下面是一个简单的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 #忽略错误
配置项说明:
同样,如果想实现把这个新增的用户删除,只需将该playbook文件的最后一行替换为如下行再执行相应的playbook即可:
user: name="{{ user }}" state=absent remove=yes
下面通过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
1. 打印详细信息
[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
# 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
playbook是由一个或多个"play"组成的列表。play的主要功能就是对一组主机应用play中定义好的task。从根本上来讲一个task就是对ansible一个module的调用。而将多个play按照一定的顺序组织到一个playbook中,我们称之为编排。
playbook主要有以下四部分构成:
playbook中的每一个play的目的都是为了让某个或某些主机以某个指定的用户身份执行任务。
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
playbook中的hosts即inentory中的定义主机与主机组,在《Ansible Inventory》中我们讲到了如何选择主机与主机组,在这里也完全适用。
- name: start mariadb
hosts: db,&london
tasks:
- name: start mariadb
service:
name: mariadb
state: started
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
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未被触发,也不会执行。
- 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
ansible支持变量,用于存储会在整个项目中重复使用到的一些值。以简化项目的创建与维护,降低出错的机率。
变量的定义:
内置主机变量
所谓内置变量其实就是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'
变量也可以通过组名,应用到组内的所有成员:
# 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
变量的定义格式是成键值对出现的,键值对之间可以嵌套,最终形成一个大字典
通过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
注册变量的应用场景:
通过命令行设置变量(优先级最高)
---
- 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
我们通过以上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部分重点说明如下:
需要说明的是,通过register注册的变量的结果并不是一成不变的,在不确定返回值的情况下,尽量调试看看输出结果。
关于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
变量名:值(键值对)
name:bob
users:
- name: marry
uid: 1100
comment: marry wang
home: /home/marry
- name: bob
uid: 1100
comment: bob li
home: /home/bob
在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
1.要使用debug打印变量
2.查看变量的逻辑结构,如果是列表则使用索引取值,如果是字典则用key取值
inventory( group < hosts )> group_vars > host_vars > playbook ( vars = vars_files ) > tasks > 命令行中定义的变量( -e 引入变量)
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
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 }}"
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
通常情况下,我们在运行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:
......
如果在play中需要引入fact,则可以开启fact缓存。fact缓存目前支持三种存储方式,分别为JSON、memcached、redis。
使用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
选项说明:
使用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
使用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
如果不想从fact中获取变量,或者说整个playbook当中都没有使用到fact变量,可以通过如下方法关闭fact以提升执行效率:
- hosts: test
gather_facts: no
也可以在ansible.cfg中添加如下配置:
[defaults]
gathering = explicit
Ansible默认会提供一些内置的变量以实现一些特定的功能,我们称之为魔法变量。下面列举一些常用的魔法变量。
获取某台指定的主机的相关变量。如果有一台web服务器的配置文件中需要指定db服务器的ip地址,我们假定这台db服务器的hostname为db.exmaple.com,ip地址绑定在eth0网卡上,我们可以通过如下方法在web服务器上调用db服务器的ip地址:
{{ hostvars['db.example.com'].ansible_eth0.ipv4.address }}
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
用于标识当前正在执行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
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']
当前playbook会在哪些hosts上运行
主机清单所在目录
主机清单文件
子组会覆盖父组,主机总是覆盖组定义的变量
在通常情况下,所有的配置信息都会被作为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
使用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')}}
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') }}"
env lookup实际就是获取在控制主机上的某个环境变量的值。下面是一个读取控制机上$JAVA_HOME变量值的示例:
- name: get JAVA_HOME
debug:
msg: "{{ lookup('env', 'JAVA_HOME')}}"
读取一个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') }}"
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
csvfile可以从.csv文件中读取一个条目。假设我们有如下示例的名为users.csv的文件:
[root@controller ansible]# cat users.csv
username,email
lorin,[email protected]
john,[email protected]
sue,[email protected]
下面是一个使用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。说明如下:
如果我们想要查找的用户存储在名为username的变量中,则可以使用"+"符号来连接username字串和其他的参数字串,来构建完整的参数字符串:
lookup('csvfile', username+'file=users.csv' delimiter=, col=1)
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')}}
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环境变量来修改这个设置。
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
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
ansible2.5版本以后,query逐渐替代lookup,lookup返回的值采用逗号为分隔符,而query总是返回一个列表。
{{ lookup('pipe', 'ls files') }}
{{ query('pipe', 'ls files') }}
{{ query('lines', 'ls files') }}
在有的时候play的结果依赖于变量、fact或者是前一个任务的执行结果,或者有的时候,我们会基于上一个task执行返回的结果而决定如何执行后续的task。这个时候就需要用到条件判断。
条件语句在Ansible中的使用场景:
在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"
在上面的示例当中,我们使用了"=="的比较运算符,在ansible中,还支持如下比较运算符:
下面是一些简单的示例:
when: ansible_machine == "x86_64"
when: max_memory <= 512
在Ansible中,除了比较运算符,还支持逻辑运算符:
示例:
# 逻辑或
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
在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。接下来我们详细说一说。
示例:
- 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
示例:
- 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
特别注意:关于路径的所有判断均是判断主控端上的路径,而非被控端上的路径
示例:
- 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
- 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
示例:
- 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)
- 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
- 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)
- hosts: test
vars:
supported_distros:
- RedHat
- CentOS
tasks:
- debug:
msg: "{{ ansible_distribution }} in supported_distros"
when: ansible_distribution in supported_distros
- 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
我们在前面使用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注意事项:
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当中也可以定义多个任务。
当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'
在上面讲block的使用方法的时候,我们说block除了可以将多个任务组合到一起,还有错误处理的功能。接下来我们继续说一说错误处理。
在shell中,可能会有这样的需求:当脚本执行至某个阶段时,需要对某个条件进行判断,如果条件成立,则立即终止脚本的运行。在shell中,可以直接调用"exit"即可执行退出。事实上,在playbook中也有类似的模块可以做这件事。即fail模块。
fail模块用于终止当前playbook的执行,通常与条件语句组合使用,当满足条件时,终止当前play的运行。
选项只有一个:
示例:
# 使用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"
事实上,当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继续往下执行。
当我们控制一些远程主机执行某些任务时,当任务在远程主机上成功执行,状态发生更改时,会返回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状态
# 只打印大于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
我们在编写playbook的时候,不可避免的要执行一些重复性操作,比如指安装软件包,批量创建用户,操作某个目录下的所有文件等。正如我们所说,ansible一门简单的自动化语言,所以流程控制、循环语句这些编程语言的基本元素它同样都具备。
在Ansible 2.5以前,playbook通过不同的循环语句以实现不同的循环,这些语句使用with_作为前缀。这些语法目前仍然兼容,但在未来的某个时间点,会逐步废弃。
下面列出一些较常见的with_X循环语句:
item:ansible内置变量,指的是引用变量中所有的元素
简单的列表循环
- hosts: test
gather_facts: no
tasks:
- debug:
msg: "{{ item }}""
with_items: "{{ groups.ungrouped }}"
- 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"]
- 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}
与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
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
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
嵌套循环
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与其功能完全一致
在循环处理列表时,为列表中的每一项添加索引(从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
用于返回一个数字序列
参数说明:
关于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"
循环字典
- 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
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
用于循环主控端的文件列表,获取文件中的内容
注意: 循环的是主控端的文件列表,不是被控端的
- 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
上面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
with_lines循环结构会让你在控制主机上执行任意命令,并对命令的输出进行逐行迭代。
假设我们有一个文件test.txt包含如下行:
Breeze Yan
Bernie Yang
jerry Qing
我们可以通过如下方法进行逐行输出:
- name: print all names
debug: msg="{{ item }}"
with_lines:
- cat test.txt
- 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。任务的返回值为最后一次循环的返回结果。
在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循环。
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
参数说明:
loop可以替代with_list,当处理嵌套列表时,列表不会被拉平
- hosts: test
gather_facts: no
vars:
testlist:
- a
- [b,c]
- d
tasks:
- debug:
msg: "{{ item }}"
loop: "{{ testlist }}"
将所有嵌套都拉平
- hosts: test
gather_facts: no
vars:
testlist:
- a
- [b,c]
- d
tasks:
- debug:
msg: "{{ item }}"
loop: "{{ testlist| flatten }}"
只拉平第一层
- hosts: test
gather_facts: no
vars:
testlist:
- a
- [b,c]
- d
tasks:
- debug:
msg: "{{ item }}"
loop: "{{ testlist| flatten(levels=1) }}"
通过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
参数说明:
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 }}
可使用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}}
使用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}}"
使用random函数可以替代with_random_choice,由于random函数是随机取出列表中的一个值,并不涉及循环操作,所以并不使用loop关键字:
- hosts: test
gather_facts: no
vars:
testlist: [a,b,c]
tasks:
- debug:
msg: "{{ testlist | random }}"
可使用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 }}"
可使用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') }}"
- 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 }}
下面是一个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"
]
}
]
}
}
对于任何自动管理工具而言,对于文件的管理都是其绕不开的话题。同样,ansible也围绕文件管理提供了众多的模块。同时还提供了Jinja2模板语法来配置文件模板。
我们在讲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
synchronize模块示例:
- name: synchronize local file to remote files
synchronize:
src: file
dest: /path/to/file
同样的,我们已经介绍过copy模块,示例如下:
- name: copy a file to managed hosts
copy:
src: file
dest: /path/to/file
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
lineinfile是一个非常有用的模块,而且相对来说,也是用法比较复杂的模块,可直接参考《Ansible lineinfile模块》
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
围绕着被标记的行插入、更新、删除一个文本块。
[root@controller ansible]# cat files/test.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: ""
insertafter: ""
path: /var/www/html/test.html
block: |
Welcome to {{ ansible_hostname }}
Last updated on {{ ansible_date_time.iso8601 }}
执行后结果如下:
[root@controller ansible]# cat test.html
Welcome to app
Last updated on 2019-05-28T15:00:03Z
更多blockinfile用法参考:ansible.builtin.blockinfile module – Insert/update/remove a text block surrounded by marker lines — Ansible Documentation
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地址以实现替换。
现在我们有了一个模板文件,那么在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模块的更多参数说明:
在上面的示例中,我们直接取了被控节点的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
定义一个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[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循环只能循环列表,不能循环字典,字典需要转换为列表才能循环
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. 字符串操作相关的过滤器
示例:
- 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. 数字操作相关的过滤器
示例
- 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. 列表操作相关的过滤器
示例:
- 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过滤器
- hosts: test
gather_facts: no
tasks:
- debug:
msg: "{{ 'redhat' | hash('sha1') }}"
#结果:
ok: [servera] => {
"msg": "3c767c41afb12ada140190ed82db3fd930e2efa3"
#相当于:
# echo -n redhat | sha1sum
3c767c41afb12ada140190ed82db3fd930e2efa3 -
- hosts: test
gather_facts: no
tasks:
- user:
name: john
password: "{{ password }}"
vars:
password: "{{ 'redhat' | password_hash('sha512') }}"
- hosts: test
gather_facts: no
tasks:
- debug:
msg: "{{ 'redhat' | b64encode | b64decode }}"
b64encode: 加密
b64decode: 解密
# 相当于:
echo -n redhat | base64 -w 0 | base64 -d
6. 查找替换过滤器
- hosts: test
gather_facts: no
tasks:
- debug:
msg: "{{ 'marvin, arthur' | replace('ar','**') }}"
- hosts: test
gather_facts: no
tasks:
- debug:
msg: "{{ 'marvin, arthur' | regex_search('ar\\S*r') }}"
\是特殊字符,需要特殊处理以后再交给正则表达式
- 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. 应用于字典变量的过滤器
- 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)}}"
示例一:
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
示例一:
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"
- 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') }}"
{{ 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 }}"
{{ 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过滤器主要用于处理网络地址, 其支持的过滤器参数如下:
要想使用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:[email protected]: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:[email protected]: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
任务返回值过滤器:
在ansible2.9中,该方式会被废弃,不推荐使用
11. 应用于文件路径的过滤器
下面是一个示例:
- 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/
在有些时候,我们希望运行与选定的主机或主机组相关联的task,但是这个task又不需要在选定的主机或主机组上执行,而需要在另一台服务器上执行。
这种特性适用于以下场景:
可以使用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
如果希望在控制主机本地运行一个特定的任务,可以使用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/
有些情况下,一些任务的运行需要等待一些状态的恢复,比如某一台主机或者应用刚刚重启,我们需要需要等待它上面的某个端口开启,此时就需要将正在运行的任务暂停,直到其状态满足要求。
Ansible提供了wait_for模块以实现任务暂停的需求
wait_for模块常用参数:
示例:
#等待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
默认情况下,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。
某些时候,我们希望某个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
我们在命令行下执行某些命令的时候,这些命令可能会需要依赖环境变量。比如在安装某些包的时候,可能需要通过代理才能完成完装。或者某个脚本可能需要调用某个环境变量才能完成运行。
ansible 支持通过environment关键字来定义一些环境变量。
在如下场景中可能需要用到环境变量:
下面是一个简单示例:
---
- 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
在少数情况下,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常用选项说明:
在大型项目当中,通常一个playbook会有非常多的task。而我们每次执行这个playbook时,都会将所有task运行一遍。而事实上,在实际使用过程中,我们可能只是想要执行其中的一部分任务而已,并不想把整个playbook完整跑一遍。这个时候就需要用到tags。
通过tags,我们可以给playbook中的某一些任务打上“标签”,而在执行playbook的时候,我们可以通过选定标签的方式指定只执行哪一些任务或者不执行哪一些任务。
下面是一个安装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。
有了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
通过下面的方式可以排除指定了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
执行效果跟上面一样。
可以通过--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]
这种方式就是上面示例中的方法:
tags: conf_httpd
可以通过列表的方式为一个任务指定多个标签:
tags:
- install_httpd
- install_web
tags: ['install_httpd','install_web']
tags: install_httpd,install_web
当为一个play指定一组标签后,该play下的所有task都会自动继承该标签,各task也可以自定义自己的标签。
- name: configure webservers
hosts: all
remote_user: ansible
tags:
- httpd
tasks:
...
ansible内置tag
除了用户自定义tag,ansible也内置了几个tag,这些tag都包含特殊含义:
# 所有打了tag的任务都会被执行,包含never tag的除外,没有标签的不会被执行
ansible-playbook --tags tagged install_web.yaml
# 所有打了tag的任务都不会被执行,包括always tag也不会被执行
ansible-playbook --skip-tags tagged install_web.yaml
# 所有未打tag的任务都会被执行,打了always tag的也会被执行
ansibl-playbook --tags untagged install_web.yaml
# 所有未打tag的任务都不会被执行
ansibl-playbook --skip-tags untagged install_web.yaml
在前面的学习当中,我们一直使用一个playbook文件来组织所有的task任务。但是,当我们项目越来越大,task越来越多的时候,如果还将所有的task都写到一个playbook当中,可读性就会变差,这个时候我们就需要重新来组织playbook了。
我们可以将一个大的playbook拆成若干个小的playbook文件,在主配置文件中将这些零碎的小文件引入进来,而这种方式就叫做playbook的"include"。
playbook的include其实就是使用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变量名的冲突。
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
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
在前面我们详细说了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的效果完全相同。
在前面我们提到过,如果为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本身会被调用,而其包含的任务不会被调用。如果要想其包含的任务也总是被调用,可修改配置如下:
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还有如下不同:
- 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中,不支持其他的方式传入变量。
有着本质的不同:
示例如下:
[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
我们知道,handlers中执行的其实也是任务,只不过是被触发才会运行,所以如果要在handlers中引入任务,也可直接使用include_tasks和import_tasks。没有include_handlers的说法。
我们在前面提到过,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"
在Ansible中,role是将playbook分割为多个文件的主要机制。它大大简化了复杂playbook的编写,同时还使得它们非常易于复用。
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各目录的作用及可用的文件:
创建role的步骤如下:
需要说明的是,以上目录并不都是必须的,如果你的roles当中并不需要用到某一个目录,也可以不用创建,比如我们将所有的变量都放到defaults中,则可以不需要vars目录,如果未用到模板文件,则不需要templates目录。
基本引用的方法:
- 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'"
如果在执行一个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!"
如果当前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
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
在使用ansible的过程中,不可避免的会存储一些敏感信息,比如在变量文件中存储帐号密码信息等。
ansible通过ansible-vault命令行工具来提供对敏感文件的加密和解密。
ansible-vault可以创建、加密、解密和查看文件。其可以加密任何ansible使用的文件,包括inventory文件,playbook中调用的变量文件等。
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
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
在playbook执行的过程中,动态的添加主机到指定的主机组中
常用参数:
示例:
- 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 }}"
在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
在前面我们所有的选取主机组的操作都是通过维护inventory文件来完成的。而事实上,当在大规模应用当中,如果主机达成千上万台,这个时候还手动维护inventory文件将会给运维工作带来巨大的挑战。
在这种大规模的应用场景中,通常的做法是,将所有的主机都存储在cmdb当中。当需要对某一组主机或者某一类型的主机执行相应操作时,通过cmdb将相应主机取出来,动态的生成inventory,然后交由ansible处理即可。
所以其实Ansible Inventory包含静态inventory和动态inventory两部分。而我们前面通过手动在inventory文件中维护主机列表的方式即称之为静态inventory。而动态inventory则是指通过外部脚本获取主机列表,并按照ansible所要求的格式返回给ansible指令的操作方式。
动态inventory一般都会结合cmdb或者云计算平台等获取主机信息,由于主机资源一般会动态的进行增减,而这些系统一般会智能更新。我们需要通过这些工具提供的api或者接入库查询等方式返回主机列表。
动态inventory脚本最终返回的满足ansible输出格式的json数据。ansible对于使用什么语言来实现动态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
在当前配置管理工具大行其道的应用中,ansible凭借其轻量级、agentless等特性得以占据一席之地。然而其也并不是完美无缺,事实上,其最为人所诟病的就是在大规模服务器应用场景当中表现出的性能问题。所以本篇文档就来说一说如何通过一些配置优化来加速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
[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
- 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
默认情况下,ansible基于ssh连接被控端,当ansible在运行playbook的时候,会建立很多的ssh连接来执行相应的task,而每个task都会创建一个新的ssh连接,ssh连接的建立毫无疑问需要额外tcp建立连接的开销。
openssh支持一个优化,叫做ssh multiplexing,也被称作ControlPersist。当启用了该特性,则多个连接到相同主机的ssh会话将会共享相同的tcp连接。这样就只有第一次连接的时候需要进行TCP三次握手。
当启用Multiplexing后:
开启该配置项:
[root@controller ansible]# cat ansible.cfg
[ssh_connection]
ssh_args = -C -o ControlMaster=auto -o ControlPersist=60s
配置项说明:
在说明pipelining之前,需要先了解下ansible是如何执行一个task的:
上面三个步骤中,后面两个步骤会产生两个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
默认情况下,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特性:
当然也有一些场景是不适合使用:
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,任务依然会被跳过。
参考《6. Ansible Playbook基本使用》
在前面debug模块使用的比较多了,这里直接再给个示例:
tasks:
- debug:
var: myvariable
- debug:
msg: "The value of myvariable is {{ var }}"
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
可以通过--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"
可以通过--step选项来交互式的执行playbook:
[root@controller ansible]# ansible-playbook playbook.yml --step
这样ansible在每个任务前会自动停止,并询问是否应该执行该任务。
假如有一个名为"configure ssh"的任务,playbook执行到这里会停止并询问:
Perform task: configure ssh (y/n/c):
参考《17. Ansible Playbook之tags》
之所以专门说一说这个模块,是因为lineinfile在实际使用中非常有用。
实际上,在大多数时候,我们在linux上的操作,就是针对文件的操作,通过配置管理工具对配置文件作统一的配置修改是一个非常酷的功能。
下面是官方针对该模块的说明:
lineinfile - Ensure a particular line is in a file, or replace an existing line using a back-referenced regular expression
简单讲,这个模块就是针对一个文件中行内容的操作。
下面我们详细说一说其具体可以做的事情。
下面是一个简单的task示例:
# 将/etc/selinux/config中匹配到以'SELINUX='开头的行,将其替换为'SELINUX=disabled'
- name: modify selinux to disabled
lineinfile:
path: /etc/selinux/config
regex: '^SELINUX='
line: 'SELINUX=disabled'
示例文件如下:
[root@controller ansible]# cat /etc/http.conf
Listen 127.0.0.1:80
Listen 80
Port
在http.conf文件的Listen 80前面添加一行Listen 8080,task示例如下:
- name: add line before Listen 80
lineinfile:
dest: /etc/http.conf
insertbefore: '^Listen 80'
line: 'Listen 8080'
在http.conf文件的Port后面添加一行just a test,task示例如下:
- name: add line before Listen 80
lineinfile:
dest: /etc/http.conf
insertafter: '^Port'
line: 'just a test'
示例文件:
[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
示例原文件:
[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'
示例原文件/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
下面我们看一看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
在一些场景下,我们修改完文件后,需要对文件做一下测试,用以检查文件修改之后,是否能正常运行。如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博客