1、Ansible搭建(基于CentOS 7.9)
1.1、在控制节点和被控节点获取epel源
Copy [root@controller ~]# wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo
1.2、安装Ansible
Copy [root@controller ~]# yum clean all #清除缓存
[root@controller ~]# yum makecache #建立缓存
[root@controller ~]# yum install -y ansible #开始安装
[root@controller ~]# ansibel --version #查看Ansible的版本及信息
2、理论
控制端使用ssh服务连接被控端
控制端默认使用root用户控制被控端,工作目录在用户家目录
3、基础配置
3.1、Ansible发送指令的原理
Ansible底层为Python编写,因此Ansible的各种指令在执行时都会转换成Python脚本
下发Ansible命令时,先转换成Python脚本 然后临时保存到主控端的~/.ansible/tmp目录
执行Ansible命令时,使用ssh服务将主控端~/.ansible/tmp目录下的Python脚本发送到被控端,被控端执行脚本
执行完成后,将~/.ansible/tmp下的Python脚本删除
3.2、Ansible配置文件
优先级 :
Copy 最高:定义变量为ANSIBLE_CONFIG = /tmp/ansible.cfg
Copy 第二:当前目录下的ansible.cfg(最优)
Copy 第三:当前用户家目录下的 ~/.ansible.cfg
Copy 最后:默认配置文件/etc/ansible/ansible.cfg
Copy [root@controller ~]# vim /etc/ansible/ansible.cfg #默认配置文件(一般不用)
自己创建一个目录,方便以后打包分享给别人
[root@controller ~]# mkdir ansible
[root@controller ~]# cp /etc/ansible/ansible.cfg ansible/ #将ansible默认配置文件拷贝过来
[root@controller ansible]# vi ansible.cfg #修改主机清单文件所在路径为当前目录下的inventory
inventory = ./inventory #以配置文件所在目录的相对路经
[root@controller ansible]# cp /etc/ansible/hosts inventory 将默认主机清单文件拷贝到当前目录并重命名为inventory(如果之前没有编写主机清单,则不用拷贝,直接新建一个名为inventory的文件进行编写即可)
3.3、配置文件参数说明
Copy [defaults]
inventory = ./inventory #主机清单文件所在位置
#library = /usr/share/my_modules/
#module_utils = /usr/share/my_module_utils/
#remote_tmp = ~/.ansible/tmp #被控端临时存储目录
#local_tmp = ~/.ansible/tmp #主控端临时存储目录
#plugin_filters_cfg = /etc/ansible/plugin_filters.yml
#forks = 5 #并发数(一次性连接的被控端)
#poll_interval = 15 #探测任务完成情况的时间
#sudo_user = root
#ask_sudo_pass = True
#ask_pass = True #相当于命令行中的-k,密码验证,默认是false
#transport = smart
#remote_port = 22 #被控端ssh服务的端口号
#module_lang = C
#module_set_locale = False
remote_user = ansible #远程主机的连接用户
[privilege_escalation]
become=True #是否提权
become_method=sudo #使用什么提权
become_user=root #提权到什么用户
become_ask_pass=False #提权需不需要密码
module_name = command #如果不指定模块,默认使用command模块
host_key_checking = False #是否进行ssh密钥验证(如果为False则自动接收被控端的公钥)
3.4、编写主机清单
在主机清单中写入被控端 IP 地址或本地hosts表中解析的主机名(可直接写IP地址)
Copy 配置本地hosts文件解析主机名
[root@controller ~]# vim /etc/hosts
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.91.11 servera
192.168.91.12 serverb
192.168.91.13 serverc
192.168.91.14 serverd
192.168.91.15 servere
192.168.91.16 serverf
编写Ansible主机清单文件
[root@controller ~]# vim /etc/ansible/hosts
serverf
[a]
servera
serverb
[b]
serverc
[c]
serverd
servere
[d:children] #d组包含b和c组(带有children标签的组,只能写组,不能写主机)
b
c
[root@controller ansible]# ansible-inventory -i file --list -y --output file.yaml #将ini格式转换成yaml格式的主机清单
[root@controller ansible]# ansible all -i file --list-hosts
解释:
-i:指定主机清单文件,当前为file
3.5、配置ssh免密及提权
Copy 被控端创建普通用户
[root@controller ansible]# ansible all -m user -a 'name=ansible state=present' -k
[root@controller ansible]# ansible all -m shell -a 'echo 1 | passwd --stdin ansible' -u root -k
生成公私钥并将公钥发送到各个被控端,默认存放在~/.ssh目录
[root@controller ansible]# ssh-keygen
[root@controller ansible]# ssh-copy-id ansible@servera
[root@controller ansible]# ssh-copy-id ansible@serverb
[root@controller ansible]# ssh-copy-id ansible@serverc
[root@controller ansible]# vim ansible.cfg
remote_user = ansible
解释:
remote_user:指定被控端使用的用户,此处指定为ansible用户
免密制作已完成,接下来配置提权
[root@controller ansible]# ansible all -m shell -a 'echo "ansible ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/ansible' -u root -k
[root@controller ansible]# vim ansible.cfg
[privilege_escalation]
become=True #是否提权
become_method=sudo #使用什么提权
become_user=root #提权到什么用户
become_ask_pass=False #提权需不需要密码
验证免密及提权是否配置成功:
[root@controller ansible]# ansible all -m shell -a 'ls /root'
3.6、Ansible操作对象
Copy 操作对象:(all和ungrouped属于ansible内置组)
[root@controller ~]# ansible all -m ping #对所有主机进行操作
[root@controller ansible]# ansible ungrouped -m ping #对不属于任何组的主机进行操作
[root@controller ~]# ansible servera -m ping #对某台主机进行操作
[root@controller ~]# ansible servera,serverb -m ping #对多台主机进行操作
[root@controller ~]# ansible a -m ping #对a组进行操作
[root@controller ~]# ansible 'server*' -m ping
3.7、Ad-hoc指令
一个ad-hoc命令的执行,需要按以下格式进行执行:(不加模块名的话,默认使用command模块)
语法格式:
ansible 主机或组 -m 模块名 -a '模块参数' ansible参数
查看主机清单中的主机
[root@controller ~]# ansible all --list-hosts
解释:
all:表示主机清单中所有的主机
[root@controller ~]# ansible all -m ping
[root@controller ~]# ansible servera -m shell -a 'ls' -k -u root
解释:
-m:指定模块
-a:指定模块参数(需要执行的命令)
-k:使用密码连接(默认使用ssh免密连接)
-u:指定被控端操作的用户
[root@controller ansible]# ansible all -m user -a 'name=user1 state=present'
解释:
user:用户模块
name=user1:指定创建用户名为user1
state=present:执行状态为创建
[root@controller ansible]# ansible all -m shell -a pwd -k #查看当前被控端使用的用户,pwd最准确
查看模块帮助
[root@controller ansible]# ansible-doc -l #查看ansible当前支持的所有模块
[root@controller ansible]# ansible-doc -l user #查看user模块的帮助
4、常用模块
4.1、shell模块
shell模块包含如下选项:
creates:指定一个文件名,如果该文件存在,则该命令不执行
removes:指定一个文件名,如果该文件不存在,则该命令执行
chdir:在执行指令之前,先切换到该指定的目录,默认工作目录是远程主机的家目录。
Copy [root@controller ansible]# ansible all -m shell -a 'creates=/tmp/file1 pwd' #如果被控端的/tmp/file1存在,则不运行pwd命令
[root@controller ansible]# ansible all -m shell -a 'removes=/tmp/file1 pwd' #如果被控端的/tmp/file1不存在,则运行pwd命令
[root@controller ansible]# ansible all -m shell -a 'chdir=/root ls' #修改工作目录
4.2、command模块
该模块通过-a跟上要执行的命令可以直接执行,不过命令里如果有带有如下字符部分则执行不成功 “ "", "|", "&" ;
Copy [root@controller ~]# ansible all -a 'ls /root'
4.3、raw模块:
用法和shell模块一样,也可以执行任意命令,就像在本机执行一样;和command、shell模块不同的是其没有chdir、creates、removes参数
4.4、script模块:
将管理端的shell 在被管理主机上执行,其原理是先将 shell 复制到远程主机,再在远程主机上执行,原理类似于raw模块。
Copy #要执行的脚本文件script.sh内容如下:
#/bin/bash
ifconfig
df -hT
# 执行ansible指令:
ansible 10.212.52.252 -m script -a 'script.sh'
4.5、authorized_keys模块
用于向被控端推送公钥,通常用于在ansible第一次连接被控端时向其推送ansible主控端的管理公钥。
常用选项:
path:默认情况下,会将公钥推送至被控端用户家目录的.ssh/authorized_keys文件中,可通过该配置项自定义该路径
state:是推送还是删除,present|absent
Copy [root@controller ansible]# ansible-galaxy collection install ansible.posix #安装相关模块,需要连接网络
[root@controller ansible]# ansible all -m authorized_key -a 'user=root key="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8IhPtFKeUKjUX/Q4g+UZittMD/9swAYd9dgv0PHhbro4MlSOVbHAldkg6+0kt04qcjUMLm6I2fM4ozbcN+/l0gpGYPJ/qPRS5ghHYg58iYSie+plmrmipmVG724WNpZaHd0aL8mVN0EF3ngqFgLEegvPbK0P8ZynHFyYJFT8vhqf/mRwlxqIoGuImyztpqmwXnTnEH4zvX46BtiQcCItFJIlyCFaEiCDeuKht4FBOfDVMiW9qkZ98811JQUJD+ndW3PW3T9tirXaic2iquq/xjWiUbW6jftgK/ExIt2syOgOXjvlrmCknbMfJcxjQfX6Yk0sEZWS4POiWep+z9CH9 root@controller" state=present'
4.6、file模块
file模块主要用于远程主机上的文件操作,file模块包含如下选项:
force:需要在两种情况下强制创建软链接,一种是源文件不存在但之后会建立的情况下;另一种是目标软链接已存在,需要先取消之前的软链,然后创建新的软链,有两个选项:yes|no
src:要被链接的源文件的路径,只应用于state=link的情况
dest:被链接到的路径,只应用于state=link的情况
state:
touch:如果文件不存在,则会创建一个新的文件,如果文件或目录已存在,则更新其最后修改时间
Copy [root@controller ansible]# ansible all -m file -a "path=/A/file1 state=touch" #创建
[root@controller ansible]# ansible all -m file -a "path=/A/dir1 state=directory" #创建目录
[root@controller ansible]# ansible all -m file -a "src=/etc/passwd dest=/A/userinfo state=link" #创建软链接
[root@controller ansible]# ansible all -m file -a "path=/A/dir1 state=absent" #删除文件夹
[root@controller ansible]# ansible all -m file -a "path=/A/file1 owner=ansible group=ansible mode=700 state=file" #修改文件权限
4.7、Copy模块:
复制文件到远程主机,copy模块包含如下选项:
backup:在覆盖之前将原文件备份,备份文件包含时间信息。有两个选项:yes|no
content:用于替代"src",可以直接设定指定文件的值
dest:必选项。要将源文件复制到的远程主机的绝对路径,如果源文件是一个目录,那么该路径也必须是个目录
force:如果目标主机包含该文件,但内容不同,如果设置为yes,则强制覆盖,如果为no,则只有当目标主机的目标位置不存在该文件时,才复制。默认为yes
others:所有的file模块里的相关文件属性选项都可以在这里使用
src:要复制到远程主机的文件在本地的地址,可以是绝对路径,也可以是相对路径。如果路径是一个目录,它将递归复制。在这种情况下,如果路径使用"/"来结尾,则只复制目录里的内容,如果没有使用"/"来结尾,则包含目录在内的整个内容全部复制,类似于rsync。
remote_src:默认是no,即复制主控端的文件到被控端,yes表示被控端的文件复制到被控端
validate:测试文件的语法,如果测试不通过,则不执行,测试通过则执行
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'" #检查文件的合法性
4.8、group模块
goup模块请求的是groupadd, groupdel, groupmod 三个指令。
state:创建还是删除组,选项:present|absent
Copy [root@controller ansible]# ansible test -m group -a 'name=test gid=1001 state=present system=yes'
4.9、user模块
user模块是请求的是useradd, userdel, usermod三个指令
home:指定用户的家目录,需要与createhome配合使用
createhome:是否创建家目录 yes|no
remove:当state=absent时,remove=yes则表示连同家目录一起删除,等价于userdel -r
Copy [root@controller ansible]# ansible all -m user -a ' name=user1 uid=1002 state=present' #创建user1用户,并设置uid为1002,
[root@controller ansible]# ansible all -m user -a ' name=user1 uid=1003 state=present' #修改user1用户的uid为1003
[root@controller ansible]# ansible all -m user -a 'name=user1 state=absent remove=yes' #删除user1用户,并将家一起删除
[root@controller ansible]# ansible all -m user -a "name=user2 state=present password={{ 'mypassword' | password_hash('sha512','mysecretsalt') }}" #创建user2用户并设置密码为mypassword
4.10、yum模块
使用yum包管理器来管理软件包,其选项有:
name:要进行操作的软件包的名字,也可以传递一个url或者一个本地的rpm包的路径
state:状态(present安装,absent卸载,latest最新版本)
Copy [root@controller ansible]# ansible all -m yum -a 'name=* state=latest' #更新所有软件包
[root@controller ansible]# ansible all -m yum -a 'name=httpd state=latest' #安装httpd包
[root@controller ansible]# ansible all -m yum -a 'name="@Development tools" state=present' #安装Development tools包组
[root@controller ansible]# ansible all -m yum -a 'name=http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm state=present' #下载并安装网站上的包
4.11、apt模块
使用apt包管理器来管理软件包,其选项有:
name:要进行操作的软件包的名字,也可以传递一个url或者一个本地的rpm包的路径
state:状态(present安装,absent卸载,latest最新版本)
Copy [root@controller ansible]# ansible all -m apt -a 'name=httpd state=latest'
4.12、service模块
用于管理服务
该模块包含如下选项:
pattern:定义一个模式,如果通过status指令来查看服务的状态时,没有响应,就会通过ps指令在进程中根据该模式进行查找,如果匹配到,则认为该服务依然在运行
sleep:如果执行了restarted,在则stop和start之间沉睡几秒钟
state:对当前服务执行启动,停止、重启、重新加载等操作(started,stopped,restarted,reloaded)
daemon_reload:针对使用systemd的系统,重新加载systemd配置,yes/no
Copy [root@controller ansible]# ansible all -m service -a "name=httpd state=started enabled=yes"
[root@controller ansible]# asnible all -m service -a "name=foo pattern=/usr/bin/foo state=started"
[root@controller ansible]# ansible all -m service -a "name=network state=restarted args=eth0"
4.13、systemd模块
daemon_reload:当服务配置文件发生变更时重载服务
state:管理服务状态,reloaded|restarted|started|stopped
Copy [root@controller ansible]# ansible all -m systemd -a "name=httpd state=started enabled=yes daemon_reload=yes"
4.14、cron模块
用于管理计划任务
包含如下选项:
backup:对远程主机上的原任务计划内容修改之前做备份
cron_file:如果指定该选项,则用该文件替换远程主机上的cron.d目录下的用户的任务计划
job:要执行的任务,依赖于state=present
special_time:指定什么时候执行,参数:reboot,yearly,annually,monthly,weekly,daily,hourly
state:确认该任务计划是创建还是删除,present创建,absent删除
Copy [root@controller ansible]# ansible all -m cron -a 'name="a job for reboot" special_time=reboot job="/some/job.sh"'
[root@controller ansible]# ansible all -m cron -a 'name="yum autoupdate" weekday="2" minute=0 hour=12 user="root"'
[root@controller ansible]# ansible all -m cron -a 'backup="True" name="test" minute="0" hour="5,2" job="ls -alh > /dev/null"'
[root@controller ansible]# ansilbe all -m cron -a 'cron_file=ansible_yum-autoupdate state=absent'
[root@controller ansible]# ansible all -m cron -a 'name="testcron minute" minute="*" job="echo test >> /tmp/test"'
[root@controller ansible]# ansible all -m cron -a 'name="testcron minute" state=absent'
4.15、synchronize模块
使用rsync同步文件,其参数如下:
archive: 归档,相当于同时开启recursive(递归)、links、perms、times、owner、group、-D选项都为yes ,默认该项为开启
copy_links:复制链接文件,默认为no ,注意后面还有一个links参数
dest_port:默认目录主机上的端口 ,默认是22,走的ssh协议
dirs:传输目录不进行递归,默认为no,即进行目录递归
set_remote_user:主要用于/etc/ansible/hosts中定义或默认使用的用户与rsync使用的用户不同的情况
mode: push或pull 模块,push模式的话,一般用于从本机向远程主机上传文件,pull 模式用于从远程主机上取文件
Copy [root@controller ansible]# ansible all -m synchronize -a 'src=/root/ansible/ dest=/tmp/data archive=yes delete=yes' #归档同步目录
说明
mode:push/pull push 在主控端执行
Copy [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 在被控端执行
Copy [root@controller ansible]# ansible mysql -m syncchronize -a 'archive=yes compress=yes dest=/data/ src=/data/ rsync_opts=-vz mode=pull'
[root@controller ansible]# rsync -avz --compress 192.168.8.130:/data/* /data/
4.16、filesystem模块
在块设备上创建文件系统
常用选项:
Copy [root@controller ansible]# ansible test -m filesystem -a 'fstype=ext2 dev=/dev/sdb1 force=yes'
[root@controller ansible]# ansible test -m filesystem -a 'fstype=ext4 dev=/dev/sdb1 opts="-cc"'
4.17、mount模块
配置挂载点
选项:
Copy [root@controller ansible]# ansible test -m mount -a "name=/mnt/dvd src=/dev/sr0 fstype=iso9660 opts=ro state=present"
[root@controller ansible]# ansible test -m mount -a "name=/srv/disk src='LABEL=SOME_LABEL' state=present "
[root@controller ansible]# ansible test -m mount -a "name=/home src='UUID=b3e48f45-f933-4c8e-a700-22a159ec9077' opts=noatime state=present"
[root@controller ansible]# ansible test -a 'dd if=/dev/zero of=/disk.img bs=4k count=1024'
[root@controller ansible]# ansible test -a 'losetup /dev/loop0 /disk.img'
[root@controller ansible]# ansible test -m filesystem -a 'fstype=ext4 force=yes opts=-F dev=/dev/loop0'
[root@controller ansible]# ansible test -m mount -a 'name=/mnt src=/dev/loop0 fstype=ext4 state=mounted opts=rw'
4.18、get_url 模块
该模块主要用于从http、ftp、https服务器上下载文件(类似于wget),主要有如下选项:
sha256sum:下载完成后进行sha256 check;
url_password、url_username:主要用于需要用户名密码进行验证的情况
use_proxy:是事使用代理,代理需事先在环境变更中定义
Copy [root@controller ~]# ansible all -m get_url -a 'url=http://controller/passwd dest=/tmp mode=0440' #从控制端下载文件到被控端的/tmp目录,并修改权限
4.19、unarchive模块
用于解压文件,模块包含如下选项:
remote_src:默认为no,直接将主控端的压缩包解压到被控端,如果为yes,则是将被控端的压缩包解压到被控端
creates:指定一个文件名,当该文件存在时,则解压指令不执行
list_files:如果为yes,则会列出压缩包里的文件,默认为no,2.0版本新增的选项
src:如果copy为yes,则需要指定压缩文件的源路径
Copy [root@controller ~]# ansible all -m unarchive -a 'src=foo.tgz dest=/var/lib/foo'
[root@controller ~]# ansible all -m unarchive -a 'src=/tmp/foo.zip dest=/usr/local/bin copy=no'
[root@controller ~]# ansible all -m unarchive -a 'src=https://example.com/example.zip dest=/usr/local/bin remote_src=yes'
4.20、assemble模块:
用于组装文件,即将多个零散的文件,合并一个大文件
常用参数:
validate:与template的validate相同,指定命令验证文件
ignore_hidden:组装时,是否忽略隐藏文件,默认为no,该参数在2.0版本中新增
Copy - 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模块:
用于将被控端的文件发送到主控端
Copy [root@controller ~]# ansible all -m fetch -m fetch -a 'src=/etc/httpd/conf/httpd.conf dest=/root/ansible/ flat=yes'
4.22、stat模块:
判断被控端文件是否存在
Copy path:文件路径
- hosts: servera
tasks:
- name: test stat
stat:
path: /etc/passwd
register: zzd
- name: debug state
debug:
msg: "{{ zzd }}"
5、Playbook
5.1、Ansible Playbook简介
ansbile-playbook是一系列ansible命令的集合,利用yaml 语言编写。playbook命令根据自上而下的顺序依次执行。同时,playbook开创了很多特性,它可以允许你传输某个命令的状态到后面的指令,如你可以从一台机器的文件中抓取内容并附为变量,然后在另一台机器中使用,这使得你可以实现一些复杂的部署机制,这是ansible命令无法实现的。
playbook通过ansible-playbook命令使用,它的参数和ansible命令类似,如参数-k(–ask-pass) 和 -K (–ask-sudo) 来询问ssh密码和sudo密码,-u指定用户,这些指令也可以通过规定的单元写在playbook 。ansible-playbook的简单使用方法: ansible-playbook example-play.yml 。
5.2、Playbook基本语法
下面是一个简单的ansible-playbook示例,可以了解其构成:
Copy [root@controller ansible]# cat user.yml
- name: create user
hosts: all
remote_user: root
gather_facts: false
vars:
user:"test"
tasks:
- name: create user
user: name="{{ user }}"
ignore_errors: yes #忽略错误
配置项说明:
name:对该playbook实现的功能做一个概述,后面执行过程中,会打印 name变量的值
remote_user:指定在远程被管理机上执行操作时使用什么用户,如不指定,则使用ansible.cfg中配置的remote_user
gather_facts:指定在执行任务之前,是否先执行setup模块获取主机相关信息,如未用到,可不指定
vars:定义后续任务中会使用到的变量,如未用到,可不指定
name:user模块里的一个参数,用于指定创建的用户名称
同样,如果想实现把这个新增的用户删除,只需将该playbook文件的最后一行替换为如下行再执行相应的playbook即可:
Copy user: name="{{ user }}" state=absent remove=yes
5.3、Playbook简单示例
下面通过playbook管理一个httpd服务器来简单了解下playbook的应用:
1、创建playbook
Copy
[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
Copy
[root@controller ansible]# ansible-playbook manage_apache.yml
PLAY [play to setup web server] *********************************************************************************************************************************************
TASK [Gathering Facts] ******************************************************************************************************************************************************
ok: [10.1.61.187]
TASK [latest httpd version installed] ***************************************************************************************************************************************
changed: [10.1.61.187]
TASK [correct index.html is present] ****************************************************************************************************************************************
changed: [10.1.61.187]
TASK [start httpd service] **************************************************************************************************************************************************
changed: [10.1.61.187]
PLAY RECAP ******************************************************************************************************************************************************************
10.1.61.187 : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
5.4、ansible-playbook常用选项
1. 打印详细信息
-vvvv:Adds extra verbosity options to the connection plug-ins,including the users being used in the managed hosts to execute scripts, and what scripts have been executed
Copy [root@controller ansible]# ansible-playbook manage_apache.yml -vv
2. 校验playbook语法
Copy [root@controller ansible]# ansible-playbook --syntax-check manage_apache.yml
playbook: manage_apache.yml
3. 测试运行playbook
通过-C选项可以测试playbook的执行情况,但不会真的执行:
Copy [root@controller ansible]# ansible-playbook -C manage_apache.yml
PLAY [play to setup web server] *********************************************************************************************************************************************
TASK [Gathering Facts] ******************************************************************************************************************************************************
ok: [10.1.61.187]
TASK [latest httpd version installed] ***************************************************************************************************************************************
ok: [10.1.61.187]
TASK [correct index.html is present] ****************************************************************************************************************************************
ok: [10.1.61.187]
TASK [start httpd service] **************************************************************************************************************************************************
ok: [10.1.61.187]
PLAY RECAP ******************************************************************************************************************************************************************
10.1.61.187 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
5.5、Multiple Plays
Copy # This is a simple playbook with two plays
- name: first play
hosts: web.example.com
tasks:
- name: first task
yum:
name: httpd
status: present
- name: second task
service:
name: httpd
state: started
- name: second play
hosts: db.example.com
tasks:
- name: first task
yum:
name: mariadb-server
status: present
- name: second task
service:
name: mariadb
state: started
6. Ansible Playbook的结构及handler用法
6.1、playbook的结构说明 playbook是由一个或多个"play"组成的列表。play的主要功能就是对一组主机应用play中定义好的task。从根本上来讲一个task就是对ansible一个module的调用。而将多个play按照一定的顺序组织到一个playbook中,我们称之为编排。
playbook主要有以下四部分构成:
Target section: 用于定义将要执行playbook的远程主机组及远程主机组上的用户,还包括定义通过什么样的方式连接远程主机(默认ssh)
Variable section: 定义playbook运行时需要使用的变量
Task section: 定义将要在远程主机上执行的任务列表
Handler section: 定义task执行完成以后需要调用的任务
playbook中的每一个play的目的都是为了让某个或某些主机以某个指定的用户身份执行任务。
6.3、Playbook中的远程用户
playbook中的远程用户和ad-hoc中的使用没有区别,默认不定义,则直接使用ansible.cfg配置中的用户相关的配置。也可在playbook中定义如下:
Copy - name: /etc/hosts is up to date
hosts: datacenter
remote_user: automation
become: yes
become_mothod: sudo
become_user: root
tasks:
- name: server.example.com in /etc/hosts
lineinfile:
path: /etc/hosts
line: '192.168.0.200 server.exmaple.com server'
state: present
6.4、Playbook中的hosts
playbook中的hosts即inentory中的定义主机与主机组,在《Ansible Inventory》中我们讲到了如何选择主机与主机组,在这里也完全适用。
Copy - name: start mariadb
hosts: db,&london
tasks:
- name: start mariadb
service:
name: mariadb
state: started
6.5、Task section
play的主体部分是任务列表。
任务列表中的各任务按次序逐个在hosts中指定的所有主机上执行,在所有主机上完成第一个任务后再开始第二个。在自上而下运行某playbook时,如果中途发生错误,则整个playbook会停止执行,由于playbook的幂等性,playbook可以被反复执行,所以即使发生了错误,在修复错误后,再执行一次即可。
定义task可以使用action: module options或module: options的格式,推荐使用后者以实现向后兼容。
Copy tasks:
- name: make sure apache is running
service:
name: httpd
state: started
- name: disable selinux
command: /sbin/setenforce 0
如果命令或脚本的退出码不为零可以使用如下方式替代:
Copy
tasks:
- name: run this command and ignore the result
shell: /usr/bin/somecommand || /bin/true
可以使用ignore_errors来忽略错误信息:
Copy tasks:
- name: run this command and ignore the result
shell: /usr/bin/somecommand
ignore_errors: True
6.6、Handler section
在Ansible Playbook中,handler事实上也是个task,只不过这个task默认并不执行,只有在被触发时才执行。
handler通过notify来监视某个或者某几个task,一旦task执行结果发生变化,则触发handler,执行相应操作。
handler会在所有的play都执行完毕之后才会执行,这样可以避免当handler监视的多个task执行结果都发生了变化之后而导致handler的重复执行(handler只需要在最后执行一次即可)。
Copy 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仍然能够执行,则需要使用如下配置:(一对一的方式)
Copy - hosts: all
force_handlers: yes #如果触发handlers,无论后面的任务执行成功与否,都会执行handlers里面的任务
tasks:
- name: a task which always notifies its handler
command: /bin/true
notify: restart the database
- name: a task which fails because the package doesn't exist
yum:
name: notapkg
state: latest
handlers:
- name: restart the database
service:
name: mariadb
state: restarted
如果与handler关联的task还未执行,在其前的task已经失败,整个play终止,则handler未被触发,也不会执行。
6.7、补充:handler的另外一种定义方(一对多的方式)
Copy - hosts: all
force_handlers: yes
tasks:
- name: a task which always notifies its handler
command: /bin/true
notify: webservice
- name: a task which fails because the package doesn't exist
yum:
name: notapkg
state: latest
handlers:
- name: restart the database
service:
name: mariadb
state: restarted
listen: webservice
- name: echo hello
shell: echo hello
listen: webservice
7. Ansible变量之自定义变量
简单说明 ansible支持变量,用于存储会在整个项目中重复使用到的一些值。以简化项目的创建与维护,降低出错的机率。
变量的定义:
7.1 在Inventory中定义变量
7.1.1、定义主机变量
内置主机变量
所谓内置变量其实就是ansible.cfg配置文件中的选项,在其前加上ansible_即成为内置变量。当然内置变拥有比ansible.cfg中选项更高的优先级,而且针对不同的主机,可以定义不同的值。
Copy # 一般配置
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等其他语言
下面是一个简单的示例:
Copy # 指定了三台主机,三台主机的用密码分别是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格式定义变量:
Copy [webservers]
servera
serverb ansible_ssh_port: 2222 #主机中定义变量,如果与主机组冲突,则优先级更高
[webservers:vars] #主机组中定义变量
username: Bob
password: redhat
yaml格式定义变量:
Copy all:
children:
webservers:
vars: #在主机组中定义变量
ansible_become: true
ansible_become_user: root
ansible_become_method: sudo
ansible_become_ask_pass: False
hosts:
servera:
ansible_host: 192.168.1.1 #在主机中定义变量
ansible_ssh_user: root
ansible_ssh_pass: 'P@ssw0rd'
serverb:
ansible_host: 192.168.1.1
ansible_ssh_user: marry
ansible_ssh_pass: '123456'
serverc:
ansible_host: 192.168.1.3
ansible_ssh_user: bernie
ansible_ssh_port: 3055
ansible_ssh_pass: '456789'
7.1.2、定义主机组变量
变量也可以通过组名,应用到组内的所有成员:
Copy # test组中包含两台主机,通过对test组指定vars变更,相应的host1和host2相当于相应的指定了ntp_server和proxy变量参数值
[test]
host1
host2
[test:vars]
ntp_server=192.168.1.10
proxy=192.168.1.20
主机组变量示例:
Copy # 下面是一个示例,指定了一个武汉组有web1、web2;随州组有web3、web4主机;又指定了一个湖北组,同时包含武汉和随州;同时为该组内的所有主机指定了2个vars变量。设定了一个组中国组,包含湖北、湖南。
[wuhan]
web1
web2
[suizhou]
web4
web3
[hubei:children]
wuhan
suizhou
[hubei:vars]
ntp_server=192.168.1.10
zabbix_server=192.168.1.10
7.2.、在Playbook中定义变量
7.2.1、变量的定义方式
变量的定义格式是成键值对出现的,键值对之间可以嵌套,最终形成一个大字典
通过vars关键字定义:
Copy # 在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关键字引入变量文件
Copy - 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目录:
Copy [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文件的示例如下:
Copy [datacenter1]
demo1.example.com
demo2.example.com
[datacenter2]
demo3.example.com
demo4.example.com
[datacenters:children]
datacenter1
datacenter2
可以看到group_vars目录中,定义了三个文件,分别以inventory文件中的三个主机组命名,所以这三个文件中定义的就分别是这三个组可以使用的变量。
Copy
[root@controller ansible]# cat datacenter1
package: httpd
[root@controller ansible]# cat datacenter2
package: apache
[root@controller ansible]# cat datacenters
package: httpd
在host_vars目录中,定义了三个文件,分别以inventory文件中的四个主机命名,所以这四个文件中定义的就分别是这四个主机可以使用的变量。
Copy
[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:
Copy - name: register variables
hosts: test
tasks:
- name: capture output of whoami command
command: whoami
register: login
注册变量的应用场景:
在一台远端的服务器获取一个目录下的一列表的文件,然后下载这些文件
在handler执行之前,发现前面一个task发生了changed,然后执行一个指定的task
获取远端服务器的ssh key的内容,构建出known_hosts文件
通过命令行设置变量(优先级最高)
Copy ---
- hosts: '{{ hosts }}'
remote_user: '{{ user }}'
tasks:
- ...
[root@controller ansible]# ansible-playbook release.yml --extra-vars "hosts=vipers user=starbuck" #其中长选项--extra-vars可以写成短选项-e
也可以写成类似如下方式:
Copy --extra-vars '{"hosts":"vipers","user":"starbuck"}'
Copy #传递一个变量
[root@controller ansible]# ansible-playbook -e password=huawei debug.yml
#传递多个变量(需要用字典)
[root@controller ansible]# ansible-playbook -e '{password:huawei,hello:user1}' debug.yml
7.2.2、使用与调试变量
我们通过以上5种方式在playbook中定义了各种变量。说到底,最终的目的,还是为了方便使用。下面我们就看一看具体如何使用这些变量
变量的引用
下面是一个变量的基本使用示例,前面的变量定义部分,直接使用的2.1.1中的变量示例:
Copy - 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,可以使用如下方法
Copy {{ users.bjones.first_name }}
{{ users.acook.home_dirs }}
或者如下写法:
{{ users['bjones']['first_name'] }}
{{ users['acook']['home_dirs'] }}
变量的调试输出
有些时候,我们在引用变量的时候,可能需要知道变量中包含哪些信息,以方便在执行过程中,对变量做些处理。ansible提供一个debug模块用于调试变量输出:
Copy - name: register variables
hosts: test
tasks:
- name: capture output of whoami command
command: whoami
register: login
- debug: var=login
执行后输出如下:
Copy
[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部分重点说明如下:
changed:ansible基于此来判断是否发生了状态改变
stdout:如果指令正常运行,则在这里输出返回结果
需要说明的是,通过register注册的变量的结果并不是一成不变的,在不确定返回值的情况下,尽量调试看看输出结果。 关于debug的更多用法说明: 调试模块,用于在调试中输出信息
常用参数:
var:将某个变量传递给debug模块,debug会直接将其打印输出
Copy # Example that prints the loopback address and gateway for each host
- debug: msg="System {{ inventory_hostname }} has uuid {{ ansible_product_uuid }}"
- debug: msg="System {{ inventory_hostname }} has gateway {{ ansible_default_ipv4.gateway }}"
when: ansible_default_ipv4.gateway is defined
- shell: /usr/bin/uptime
register: result
- debug: var=result verbosity=2 #直接将上一条指令的结果作为变量传递给var,由debug打印出result的值
- name: Display all variables/facts known for a host
debug: var=hostvars[inventory_hostname] verbosity=4
7.3、定义变量:
7.3.1、第一种方式:
7.3.2、第二章方式:
Copy users:
- name: marry
uid: 1100
comment: marry wang
home: /home/marry
- name: bob
uid: 1100
comment: bob li
home: /home/bob
7.4、变量取值:
Copy # 在playbook中取值
users:
- name: marry
uid: 1100
comment: marry wang
home: /home/marry
- name: bob
uid: 1100
comment: bob li
home: /home/bob
#第一种取值方法:(使用列表的索引)
users.0.name
users.1.name
#不支持调用变量
#第二种取值方法:
users[0]['name'] #name是字符串,Key
users[0][test] #test是变量
#[]里面不打引号,代表是变量,打印号代表是字符串,索引0,1,2不需要打印号
#第三章取值方法:(将前两种混合使用)
users[0].name
7.5、关于变量取值的思路:
查看变量的逻辑结构,如果是列表则使用索引取值,如果是字典则用key取值
7.6、变量优先级:(从低到高)
inventory( group < hosts )> group_vars > host_vars > playbook ( vars = vars_files ) > tasks > 命令行中定义的变量( -e 引入变量)
8. Ansible变量之fact
fact简介 ansible有一个模块叫setup,用于获取远程主机的相关信息,并可以将这些信息作为变量在playbook里进行调用。而setup模块获取这些信息的方法就是依赖于fact。
Copy [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模块其他用法示例:
Copy # 查看主机内存信息
[root@controller ansible]# ansible 10.212.52.252 -m setup -a 'filter=ansible_*_mb'
# 查看地接口为eth0-2的网卡信息
[root@controller ansible]# ansible 10.212.52.252 -m setup -a 'filter=ansible_eth[0-2]'
# 将所有主机的信息输入到/tmp/facts目录下,每台主机的信息输入到主机名文件中(/etc/ansible/hosts里的主机名)
[root@controller ansible]# ansible all -m setup --tree /tmp/facts
8.1、自定义fact
8.1.1、手动设置fact
ansible除了能获取到预定义的fact的内容,还支持手动为某个主机定制fact。称之为本地fact。本地fact默认存放于被控端的/etc/ansible/facts.d目录下,如果文件为ini格式或者json格式,ansible会自动识别。以这种形式加载的fact是key为ansible_local的特殊变量。
下面是一个简单的示例,在ansibler主控端定义一个ini格式的custom.fact文件内容如下:
Copy [general]
package = httpd
service = httpd
state = started
然后我们编写一个playbook文件名为setup_facts.yml内容如下:
Copy ---
- 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的推送:
Copy [root@controller ansible]# ansible-playbook setup_facts.yml
此时,我们可以在被控端看到新的facts已经生成:
Copy # 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:
Copy - name: Install Apache and starts the service
hosts: test
tasks:
- name: Install the required package
yum:
name: "{{ ansible_facts.ansible_local.custom.general.package }}"
state: latest
- name: Start the service
service:
name: "{{ ansible_facts.ansible_local.custom.general.service }}"
state: "{{ ansible_facts.ansible_local.custom.general.state }}"
8.1.2、使用set_fact模块定义新的变量
set_fact模块可以自定义facts,这些自定义的facts可以通过template或者变量的方式在playbook中使用。如果你想要获取一个进程使用的内存的百分比,则必须通过set_fact来进行计算之后得出其值,并将其值在playbook中引用。
生效范围:是从定义变量开始,到playbook文件结束,即该playbook整个文件可以继承前面playbook中通过set_fact定义的变量,但前提是必须在同一台主机上运行
下面是一个set_fact模块的应用示例:
Copy - 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如下:
Copy
[root@controller ansible]# ansible-playbook set_fact_ex.yaml
PLAY [set_fact example] *****************************************************************************************************************************************************
TASK [Gathering Facts] ******************************************************************************************************************************************************
ok: [10.1.61.187]
TASK [Calculate InnoDB buffer pool size] ************************************************************************************************************************************
ok: [10.1.61.187]
TASK [debug] ****************************************************************************************************************************************************************
ok: [10.1.61.187] => {
"innodb_buffer_pool_size_mb": "3911.0"
}
PLAY RECAP ******************************************************************************************************************************************************************
10.1.61.187 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
8.2、手动采集fact
通常情况下,我们在运行play的时候,ansible会先尝试ssh到被控端采集fact,如果此时,被控制端的ssh还没有完全启动,就会导致整个play执行失败。这个时候,我们可以先显式的关闭fact采集,然后在task中通过wait_for等待被控端ssh端口被正常监听,再在task中使用setup模块来手动采集fact:
Copy - name: Deploy apps
hosts: webservers
gather_facts: False
tasks:
- name: wait for ssh to be running
local_action: wait_for port=22 host="{{ inventory_hostname }}" search_regex=OpenSSH
- name: gather facts
setup:
......
8.3、启用fact缓存
如果在play中需要引入fact,则可以开启fact缓存。fact缓存目前支持三种存储方式,分别为JSON、memcached、redis。
8.3.1、Json文件fact缓存后端
使用JSON文件作为fact缓存后端的时候,ansible将会把采集的fact写入到控制主机的文件中。
ansible.cfg配置如下:
Copy [defaults]
gathering = smart
#缓存时间,单位为秒
fact_caching_timeout = 86400
fact_caching = jsonfile
#指定ansible包含fact的json文件位置,如果目录不存在,会自动创建
fact_caching_connection = /tmp/ansible_fact_cache
选项说明:
gathering: 是否启用fact,有三个选项:
smart:默认收集facts,但在facts已有的情况下就不收集,即使用facts缓存
implicit:默认收集facts,要禁止收集,必须显式的申明:gather_facts: false
explicit:默认不收集,要收集,必须显示的申明: gather_facts: true
fact_cacheing_timeout:缓存时间,单位为s
fact_caching:缓存的方式,支持jsonfile、redis、memcached
fact_caching_connection:指定ansible缓存fact的连接方式,如果是jsonfile,则指定jsonfile的缓存路径
8.3.2、Redis fact缓存后端
使用redis作为fact缓存后端,需要在控制主机上安装redis服务并保持运行。需要安装python操作redis的软件包。
ansible.cfg配置如下:
Copy [defaults]
gathering = smart
fact_caching_timeout = 86400
fact_caching = redis
fact_caching_connection = 127.0.0.1:6379:0
8.3.3. Memcached fact缓存后端
使用memcached作为fact缓存后端,需要在控制主机上安装Memcached服务并保持运行,需要安装python操作memcached的软件包。
ansible.cfg配置如下:
Copy
[defaults]
gathering = smart
fact_caching_timeout = 86400
fact_caching = memcached
fact_caching_connection = 127.0.0.1:11211
8.4、关闭fact
如果不想从fact中获取变量,或者说整个playbook当中都没有使用到fact变量,可以通过如下方法关闭fact以提升执行效率:
Copy - hosts: test
gather_facts: no
也可以在ansible.cfg中添加如下配置:
Copy [defaults]
gathering = explicit
9. Ansible魔法变量及变量优先级
魔法变量 Ansible默认会提供一些内置的变量以实现一些特定的功能,我们称之为魔法变量。下面列举一些常用的魔法变量。
9.1、hostvars
获取某台指定的主机的相关变量。如果有一台web服务器的配置文件中需要指定db服务器的ip地址,我们假定这台db服务器的hostname为db.exmaple.com,ip地址绑定在eth0网卡上,我们可以通过如下方法在web服务器上调用db服务器的ip地址:
Copy {{ hostvars['db.example.com'].ansible_eth0.ipv4.address }}
9.2、inventory_hostname(获取主机清单中定义的主机名)
inventory_hostname是Ansible所识别的当前正在运行task的主机的主机名。如果在inventory里定义过别名,那么这里就是那个别名,如果inventory包含如下一行:
Copy server1 ansible_ssh_host=192.168.1.1
则inventory_hostname即为server1
利用hostvars和inventory_hostname变量,可以输出与当前主机相关联的所有变量:
Copy - debug: var=hostvars[inventory_hostname]
与inventory_hostname相近的还有一个inventory_hostname_short,如果一台主机的inventory_hostname为server1.exmaple.com,则inventory_hostname_short的值为server1
9.3、group_names(任务在哪个主机上运行,则获取该主机所在的组)
用于标识当前正在执行task的目标主机位于的主机组。假如我们有三台主机,用来配置成一主二从的mysql服务器。inventory配置如下:
Copy [mdb]
db1
[sdb]
db2
db3
mysql配置文件my.conf.j2示例如下:
Copy #我们知道db1在mdb组,当db1与当前正在执行任务的主机位于同一组时,我们认为当前主机即在mdb组,所以对当前主机应用mysql master的配置
<div data-gb-custom-block data-tag="if" data-0='db1'>
[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的配置
<div data-gb-custom-block data-tag="else"></div>
[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
</div>
我们执行如下task:
Copy - name: copy config file to mysql master
template: src=my.conf.j2 dest=/etc/my.cnf
9.4、groups(获取主机清单中定义的所有组)
groups.mysql:(获取主机清单中mysql组的主机)
groups是inventory中所有主机组的列表,可用于枚举主机组中的所有主机。
假如我们有一个inventory文件定义如下:
Copy [web]
server1
server2
在配置一台HAproxy的负载均衡器时,我们的配置文件肯定需要web主机组的所有服务器的IP,配置文件包含如下片段:
Copy backend web-backend
<div data-gb-custom-block data-tag="for">
server {{host.inventory_hostname}} {{ host.ansible_default_ipv4.address }}:80
</div>
最终生成的文件如下:
Copy backend web-backend
server server1 192.168.1.1:80
server server2 192.168.1.2:80
再给一个例子,在所有的dbservers组的服务器上创建一个数据库用户kate:
Copy - name: Create a user for all db servers
mysql_user: name=kate password=test host={{ hostvars.[item].ansible_eth0.ipv4.address }} state=present
with_items: groups['dbservers']
9.5、play_hosts
当前playbook会在哪些hosts上运行
9.6、inventory_dir
主机清单所在目录
9.7、inventory_file
主机清单文件
9.8、变量优先级
inventory 主机清单中连接变量(ansible_ssh_user等)
角色定义的默认变量(roles/rolesname/defaults/main.yml)
子组会覆盖父组,主机总是覆盖组定义的变量
10. 使用lookup生成变量
简单说明 在通常情况下,所有的配置信息都会被作为ansible的变量保存了,而且可以保存在ansible允许定义变量的各种地方,诸如vars区段,vars_files加载的文件中,以及host_vars和group_vars目录中。
但在有些时候,我们希望从诸如文本文件或者.csv文件中收集数据作为ansible的变量,或者直接获取某些命令的输出作为ansible的变量,甚至从redis或者etcd这样的键值存储中取得相应的值作为ansible的变量。这个时候,我们就需要通过ansible的lookup插件来从这些数据源中读取配置数据,传递给ansbile变量,并在playbook或者模板中使用这些数据。
ansible支持一套从不同数据源获取数据的lookup,包括file, password, pipe, env, template, csvfile, dnstxt, redis_kv, etcd等
查询lookup插件:
Copy ansible-doc -t lookup -l
ansible-doc -t lookup plugin
10.1、file
使用file lookup可以从文本文件中获取数据,并在这些数据传递给ansible变量,在task或者jinja2模板中进行引用。下面是一个从文本文件中获取ssh公钥并复制到远程主机的示例:
Copy - 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模板文件示例如下:
Copy {{ lookup('file', '/users/deploy/.ssh/id_rsa.pub')}}
10.2、pipe
lines可代替pipe,不同的是,lines可自动换行,格式化显示
使用pipe lookup可以直接调用外部命令,并将命令执行的结果打印到标准输出,作为ansible变量。下面的例子通过pipe调用date指令拿到一个以时间数字组成的字串
Copy - name: Flamingo | Get release version
set_fact:
flamingo_release_version: "{{ lookup('pipe', 'date +%Y%m%d%H%M%SZ') }}"
10.3、env
env lookup实际就是获取在控制主机上的某个环境变量的值。下面是一个读取控制机上$JAVA_HOME变量值的示例:
Copy - name: get JAVA_HOME
debug:
msg: "{{ lookup('env', 'JAVA_HOME')}}"
10.4、url
读取一个url的内容
Copy - name: get url
debug:
msg: "{{ lookup('url', 'http://www.example.com')}}"
- name: url lookup using authentication
debug:
msg: "{{ lookup('url', 'https://some.private.site.com/file.txt', username='bob', password='hunter2') }}"
10.5、template
template lookup可以指定一个jinja2模板,然后返回这个模板中的变量被替换以后的结果。
假设我们有一个message.j2模板,内容如下:
Copy This host runs {{ ansible_distribution }}
定义一个如下的task:
Copy - name: print message from template
debug:
msg: "{{ lookup('template', 'message.j2')}}"
输出的msg的结果如下:
Copy This host runs CentOS
10.6、csvfile
csvfile可以从.csv文件中读取一个条目。假设我们有如下示例的名为users.csv的文件:
Copy [root@controller ansible]# cat users.csv
username,email
lorin,lorin@test.com
john,john@example.com
sue,sue@exmaple.com
下面是一个使用csvfile lookkup提取sue的电子邮件地址的task示例:
Copy - name: get sue's email
debug:
msg: "{{ lookup('csvfile','sue file=users.csv delimiter=, col=1')}}"
可以看到,一共向插件传递了四个参数:sue, file=users.csv, delimiter=,以及col=1。说明如下:
第一个参数指定一个名字,该名字必须出现在其所在行的第0列,需要说明的是,如果指定的第一个参数名字在文件中出现多次,则匹配第一次出现的结果
第三个参数指定csv文件的中条目的分隔符,默认是tab
第四个参数指定要取得哪一列的值,0代表第1列,1代表第2列,默认是1
如果我们想要查找的用户存储在名为username的变量中,则可以使用"+"符号来连接username字串和其他的参数字串,来构建完整的参数字符串:
Copy lookup('csvfile', username+'file=users.csv' delimiter=, col=1)
10.7、redis_kv
redis_kv lookup可以直接从redis存储中来获取一个key的value,key必须是一个字符串,如同Redis GET指令一样。需要注意的是,要使用redis_kv lookup,需要在主控端安装python的redis客户端,在centos上,软件包为python-redis。
下面是一个在playbook中调用redis lookup的task,从本地的redis中取中一个key为weather的值:
Copy - name: lookup value in redis
debug:
msg: "{{ lookup('redis_kv', 'redis://localhost:6379,weather')}}"
其中URL部分如果不指定,该模块会默认连接到redis://localhost:6379,所以实际上在上面的实例中,调用可以直接写成如下:
Copy {{ lookup('redis_kv', 'weather')}}
10.8、etcd
etcd是一个分布式的key-value存储,通常被用于保存配置信息或者被用于实现服务发现。可以使用etcd lookup来从etcd中获取指定key的value。
我们通过如下方法往一个etcd中写入一个key:
Copy curl -L http://127.0.0.1:2379/v2/keys/weather -XPUT -d value=sunny
定义一个调用etcd插件的task:
Copy - name: look up value in etcd
debug:
msg: "{{ lookup('etcd','http://127.0.0.1:2379,weather')}}"
默认情况下,etcd lookup会在http://127.0.0.1:2379上查找etcd服务器。但我们在执行playbook之前可以通过设置ANSIBLE_ETCD_URL环境变量来修改这个设置。
10.9、password
password lookup会随机生成一个密码,并将这个密码写入到参数指定的文件中。如下示例,创建一个名为bob的mysql用户,并随机生成该用户的密码,并将密码写入到主控端的bob-password.txt中:
Copy - name: create deploy mysql user
mysql_user:
name: bob
password: {{ lookup('password', 'bob-password.txt')}}
priv: *.*:ALL
state: present
10.10、dnstxt
dnstxt lookup用于获取指定域名的TXT记录。需要在主控端安装python-dns。
使用方法如下:
Copy - name: lookup TXT record
debug:
msg: "{{ lookup('dnstxt', 'aliyun.com') }}"
如果某一个主机有多个相关联的TXT记录,那么模块会把他们连在一起,并且每次调用时的连接顺序可能不同
Copy #dnspython模块安装
wget http://www.dnspython.org/kits/1.12.0/dnspython-1.12.0.tar.gz
tar -zxvf dnspython-1.12.0.tar.gz
cd dnspython-1.12.0
python setup.py install
10.11、补充:
ansible2.5版本以后,query逐渐替代lookup,lookup返回的值采用逗号为分隔符,而query总是返回一个列表。
Copy {{ lookup('pipe', 'ls files') }}
{{ query('pipe', 'ls files') }}
{{ query('lines', 'ls files') }}
11. Ansible Playbook条件语句
简介 在有的时候play的结果依赖于变量、fact或者是前一个任务的执行结果,或者有的时候,我们会基于上一个task执行返回的结果而决定如何执行后续的task。这个时候就需要用到条件判断。
条件语句在Ansible中的使用场景:
在目标主机上定义了一个硬限制,比如目标主机的最小内存必须达到多少,才能执行该task
捕获一个命令的输出,根据命令输出结果的不同以触发不同的task
根据不同目标主机的facts,以定义不同的task
用于判断某个服务的配置文件是否发生变更,以确定是否需要重启服务
11.1、when关键字
11.1.1、when基本使用
在ansible中,使用条件判断的关键字就是when。
如在安装包的时候,需要指定主机的操作系统类型,或者是当操作系统的硬盘满了之后,需要清空文件等,可以使用when语句来做判断 。when关键字后面跟着的是python的表达式,在表达式中你能够使用任何的变量或者fact,当表达式的结果返回的是false,便会跳过本次的任务
下面是一个基本的用法示例:
Copy ---
- name: Install vim
hosts: all
tasks:
- name: Install VIM via yum
yum:
name: vim-enhanced
state: installed
when: ansible_os_family =="RedHat"
- name: Install VIM via apt
apt:
name: vim
state: installed
when: ansible_os_family =="Debian"
- name: Unexpected OS family
debug: msg="OS Family {{ ansible_os_family }} is not supported"
when: not ansible_os_family =="RedHat" or ansible_os_family =="Debian"
11.1.2、比较运算符
在上面的示例当中,我们使用了"=="的比较运算符,在ansible中,还支持如下比较运算符:
==:比较两个对象是否相等,相等则返回真。可用于比较字符串和数字
:比较两个对象的大小,左边的值大于右边的值,则为真
=:比较两个对象的大小,左边的值大于等于右边的值,则为真 下面是一些简单的示例:
Copy when: ansible_machine == "x86_64"
when: max_memory <= 512
11.1.3、逻辑运算符
在Ansible中,除了比较运算符,还支持逻辑运算符:
and:逻辑与,当左边和右边两个表达式同时为真,则返回真
or:逻辑或,当左右和右边两个表达式任意一个为真,则返回真
():当一组表达式组合在一起,形成一个更大的表达式,组合内的所有表达式都是逻辑与的关系
示例:
Copy # 逻辑或
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")
一个完整的例子:
Copy # 判断register注册变量的返回结果
- name: restart httpd if postfix is running
hosts: test
tasks:
- name: get postfix server status
command: /usr/bin/systemctl is-active postfix
ignore_errors: yes
register: result
- name: restart apache httpd based on postfix status
service:
name: httpd
state: restarted
when: result.rc == 0
11.2、条件判断与tests
在shell当中,我们可使用test命令来进行一些常用的判断操作,如下:
Copy # 判断/test文件是否存在
test -e /test
# 判断/testdir是否存在且为一个目录
test -d /testdir
事实上,在ansible中也有类似的用法,只不过ansible没有使用linux的test命令,而是jinja2模板的tests。
下面是一个简单示例:
Copy # 通过条件语句判断testpath的路径是否存在
- hosts: test
vars:
testpath: /testdir
tasks:
- debug:
msg: "file exist"
when: testpath is exists
上面的示例中,我们使用了is exists用于路径存在时返回真,也可以使用is not exists用于路径不存在时返回真。也可以在整个条件表达式的前面使用not以取反:
Copy - hosts: test
vars:
testpath: /testdir1
tasks:
- debug:
msg: "file not exist"
when: not testpath is exists
在ansible中,除了能够使用exists这种tests之外,还有一些别的tests。接下来我们详细说一说。
11.2.1、判断变量
defined:判断变量是否已定义,已定义则返回真
undefined:判断变量是否未定义,未定义则返回真
none:判断变量的值是否为空,如果变量已定义且值为空,则返回真 示例:
Copy
- hosts: test
gather_facts: no
vars:
testvar: "test"
testvar1:
tasks:
- debug:
msg: "testvar is defined"
when: testvar is defined
- debug:
msg: "testvar2 is undefined"
when: testvar2 is undefined
- debug:
msg: "testvar1 is none"
when: testvar1 is none
11.2.2、判断执行结果
sucess或succeeded:通过任务执行结果返回的信息判断任务的执行状态,任务执行成功则返回true
failure或failed:任务执行失败则返回true
change或changed:任务执行状态为changed则返回true
skip或skipped:任务被跳过则返回true 示例:
Copy - hosts: test
gather_facts: no
vars:
doshell: true
tasks:
- shell: 'cat /testdir/aaa'
when: doshell
register: result
ignore_errors: true
- debug:
msg: "success"
when: result is success
- debug:
msg: "failed"
when: result is failure
- debug:
msg: "changed"
when: result is change
- debug:
msg: "skip"
when: result is skip
11.2.3、判断路径
directory:判断指定路径是否为一个目录,是则为真
mount:判断指定路径是否为一个挂载点,是则为真
exists:判断指定路径是否存在,存在则为真 特别注意:关于路径的所有判断均是判断主控端上的路径,而非被控端上的路径
示例:
Copy - hosts: test
gather_facts: no
vars:
testpath1: "/testdir/test"
testpath2: "/testdir"
tasks:
- debug:
msg: "file"
when: testpath1 is file
- debug:
msg: "directory"
when: testpath2 is directory
11.2.4、判断字符串
lower:判断字符串中的所有字母是否都是小写,是则为真
upper:判断字符串中的所有字母是否都是大写,是则为真
Copy - hosts: test
gather_facts: no
vars:
str1: "abc"
str2: "ABC"
tasks:
- debug:
msg: "str1 is all lowercase"
when: str1 is lower
- debug:
msg: "str2 is all uppercase"
when: str2 is upper
11.2.5、判断整除
divisibleby(num):判断是否可以整除指定的数值,是则为真 示例:
Copy - hosts: test
gather_facts: no
vars:
num1: 6
num2: 8
num3: 15
tasks:
- debug:
msg: "num1 is an even number"
when: num1 is even
- debug:
msg: "num2 is an odd number"
when: num2 is odd
- debug:
msg: "num3 can be divided exactly by"
when: num3 is divisibleby(3)
11.2.6、其他tests
version可用于对比两个版本号的大小,或者与指定的版本号进行对比,使用语法为version("版本号","比较操作符")
Copy - 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中使用的比较运算符说明:
Copy - 大于: >, gt
- 大于等于: >=, ge
- 小于: <, lt
- 小于等于: <=, le
- 等于: =, ==, eq
- 不等于: !=, <>, ne
subset判断一个list是不是另一个list的子集
superset判断一个list是不是另一个list的父集"
Copy - 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)
in判断一个字符串是否存在于另一个字符串中,也可用于判断某个特定的值是否存在于列表中
Copy - hosts: test
vars:
supported_distros:
- RedHat
- CentOS
tasks:
- debug:
msg: "{{ ansible_distribution }} in supported_distros"
when: ansible_distribution in supported_distros
Copy - hosts: test
gather_facts: no
vars:
var1: 1
var2: "1"
var3: a
tasks:
- debug:
msg: "var1 is a number"
when: var1 is number
- debug:
msg: "var2 is a string"
when: var2 is string
- debug:
msg: "var3 is a string"
when: var3 is string
11.3、条件判断与block
11.3.1、block
我们在前面使用when做条件判断时,如果条件成立则执行对应的任务。但这就面临一个问题,当我们要使用同一个条件判断执行多个任务的时候,就意味着我们要在某一个任务下面都写一下when语句,而且判断条件完全一样。这种方式不仅麻烦而且显得low。Ansible提供了一种更好的方式来解决这个问题,即block。
在ansible中,使用block将多个任务进行组合,当作一个整体。我们可以对这一个整体做条件判断,当条件成立时,则执行块中的所有任务:
Copy
- hosts: test
tasks:
- debug:
msg: "task1 not in block"
- block:
- debug:
msg: "task2 in block1"
- debug:
msg: "task3 in block1"
when: 2 > 1
下面是一个稍微有用点儿的例子:
Copy - 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定义name(ansible 2.3增加的特性)
可以直接对block使用when,但不能直接对block使用loop
11.3.2、rescue
block除了能和when一起使用之外,还能作错误处理。这个时候就需要用到rescue关键字:
Copy - hosts: test
tasks:
- block:
- shell: 'ls /testdir'
rescue:
- debug:
msg: '/testdir is not exists'
在上面的例子中,当block中的任务执行失败时,则运行rescue中的任务。如果block中的任务正常执行,则rescue的任务就不会被执行。如果block中有多个任务,则任何一个任务执行失败,都会执行rescue。block中可以定义多个任务,同样rescue当中也可以定义多个任务。
11.3.3、always
当block执行失败时,rescue中的任务才会被执行;而无论block执行成功还是失败,always中的任务都会被执行:
Copy - hosts: test
tasks:
- block:
- shell: 'ls /testdir'
rescue:
- debug:
msg: '/testdir is not exists'
always:
- debug:
msg: 'This task always executes'
11.4、条件判断与错误处理
在上面讲block的使用方法的时候,我们说block除了可以将多个任务组合到一起,还有错误处理的功能。接下来我们继续说一说错误处理。
11.4.1、fail模块
在shell中,可能会有这样的需求:当脚本执行至某个阶段时,需要对某个条件进行判断,如果条件成立,则立即终止脚本的运行。在shell中,可以直接调用"exit"即可执行退出。事实上,在playbook中也有类似的模块可以做这件事。即fail模块。
fail模块用于终止当前playbook的执行,通常与条件语句组合使用,当满足条件时,终止当前play的运行。
选项只有一个:
Copy # 使用fail模块中断playbook输出
- hosts: test
tasks:
- shell: echo "Just a test--error"
register: result
- fail:
msg: "Conditions established,Interrupt running playbook"
when: "'error' in result.stdout"
- debug:
msg: "Inever execute,Because the playbook has stopped"
11.4.2、failed_when
事实上,当fail和when组合使用的时候,还有一个更简单的写法,即failed_when,当满足某个条件时,ansible主动触发失败。
Copy # 如果在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条件语句,写成如下:
Copy - name: this command prints FAILED when it fails
command: /usr/bin/example-command -x -y -z
register: command_result
ignore_errors: True
- name: fail the play if the previous command did not succeed
fail: msg="the command failed"
when: "command_result.stderr and 'FAILED' in command_result.stderr"
ansible一旦执行返回失败,后续操作就会中止,所以failed_when通常可以用于满足某种条件时主动中止playbook运行的一种方式。
ansible默认处理错误的机制是遇到错误就停止执行。但有些时候,有些错误是计划之中的。我们希望忽略这些错误,以让playbook继续往下执行。这个时候就可以使用ignore_errors忽略错误,从而让playbook继续往下执行。
11.4.3、changed_when
当我们控制一些远程主机执行某些任务时,当任务在远程主机上成功执行,状态发生更改时,会返回changed状态响应,状态未发生更改时,会返回OK状态响应,当任务被跳过时,会返回skipped状态响应。我们可以通过changed_when来手动更改changed响应状态。示例如下:
Copy
- shell: /usr/bin/billybass --mode="take me to the river"
register: bass_result
changed_when: "bass_result.rc != 2" #只有该条task执行以后,bass_result.rc的值不为2时,才会返回changed状态
# this will never report 'changed' status
- shell: wall 'beep'
changed_when: False #当changed_when为false时,该条task在执行以后,永远不会返回changed状态
11.5、在循环语句中使用条件语句
Copy # 只打印大于5的值
tasks:
- command: echo {{ item }}
loop: [ 0, 2, 4, 6, 8, 10 ]
when: item > 5
Copy # 确保将mariadb-server安装到根分区且根分区的可用空间要大于300M
- name: install mariadb-server if enough space on root
yum:
name: mariadb-server
state: present
loop: "{{ ansible_mounts }}"
when: item.mount == "/" and item.size_available > 300000000
12. Ansible Playbook with_X循环语句
循环语句
简介 我们在编写playbook的时候,不可避免的要执行一些重复性操作,比如指安装软件包,批量创建用户,操作某个目录下的所有文件等。正如我们所说,ansible一门简单的自动化语言,所以流程控制、循环语句这些编程语言的基本元素它同样都具备。
在Ansible 2.5以前,playbook通过不同的循环语句以实现不同的循环,这些语句使用with_作为前缀。这些语法目前仍然兼容,但在未来的某个时间点,会逐步废弃。
下面列出一些较常见的with_X循环语句:
12.1、with_items
item:ansible内置变量,指的是引用变量中所有的元素
简单的列表循环
12.1.1、场景一: 循环打印inventory中所有未分组的主机
Copy - hosts: test
gather_facts: no
tasks:
- debug:
msg: "{{ item }}""
with_items: "{{ groups.ungrouped }}"
12.1.2、场景二: 直接在with_items中定义被循环的列表
Copy - hosts: test
gather_facts: no
tasks:
- name: "with_items"
debug:
msg: "{{ item }}"
with_items:
- "user0"
- "user1"
- "user2"
也可以写成如下方式:
Copy - hosts: test
gather_facts: no
tasks:
- name: "with_items"
debug:
msg: "{{ item }}"
with_items:["user0","user1","user2"]
12.1.3、场景三: 在with_items中定义更复杂的列表
Copy - hosts: test
gather_facts: no
tasks:
- name: "create directory"
file:
path: "/{{ item.path1 }}/{{ item.path2 }}"
with_items:
- { path1: a, path2: b}
- { path1: c, path2: d}
12.2、with_list
与with_items一样,也是用于循环列表。区别是,如果列表的值也是列表,with_items会将第一层嵌套的列表拉平,而with_list会将值作为一个整体返回。
示例:
Copy # 使用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
Copy # 使用with_list的示例
- hosts: test
gather_facts: no
tasks:
- name: "with_items"
debug:
msg: "{{ item }}"
with_list:
- [1, 2]
- [a, b]
# 返回结果:
[root@controller ansible]# ansible-playbook with_list_ex.yml
PLAY [test] *****************************************************************************************************************************************************************
TASK [with_items] ***********************************************************************************************************************************************************
ok: [10.1.61.187] => (item=[1, 2]) => {
"msg": [
1,
2
]
}
ok: [10.1.61.187] => (item=['a', 'b']) => {
"msg": [
"a",
"b"
]
}
PLAY RECAP ******************************************************************************************************************************************************************
10.1.61.187 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
12.3、with_flattened
with_flattened与with_items类似,当处理复杂的多级列表嵌套时,会将所有的列表全部拉平:
Copy - hosts: test
gather_facts: no
tasks:
- name: "with_items"
debug:
msg: "{{ item }}"
with_flattened:
- [1, 2,[3,4]]
- [a, b]
返回结果:
Copy [root@controller ansible]# ansible-playbook with_flattened_ex.yml
PLAY [test] *****************************************************************************************************************************************************************
TASK [with_items] ***********************************************************************************************************************************************************
ok: [10.1.61.187] => (item=1) => {
"msg": 1
}
ok: [10.1.61.187] => (item=2) => {
"msg": 2
}
ok: [10.1.61.187] => (item=3) => {
"msg": 3
}
ok: [10.1.61.187] => (item=4) => {
"msg": 4
}
ok: [10.1.61.187] => (item=a) => {
"msg": "a"
}
ok: [10.1.61.187] => (item=b) => {
"msg": "b"
}
ok: [10.1.61.187] => (item=c) => {
"msg": "c"
}
PLAY RECAP ******************************************************************************************************************************************************************
10.1.61.187 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
12.4、with_together
with_together可以将两个列表中的元素对齐合并
示例如下:
Copy - 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合并输出了
上面的示例是基于两个列表的元素完全相同的结果,如果两个列表中的元素不同:
Copy - hosts: test
remote_user: root
vars:
alpha: [ 'a','b','c']
numbers: [ 1,2]
tasks:
- debug: msg="{{ item.0 }} and {{ item.1 }}"
with_together:
- "{{ alpha }}"
- "{{ numbers }}"
# 输出结果:
[root@controller ansible]# ansible-playbook with_together_ex.yml
PLAY [test] *****************************************************************************************************************************************************************
TASK [debug] ****************************************************************************************************************************************************************
ok: [10.1.61.187] => (item=['a', 1]) => {
"msg": "a and 1"
}
ok: [10.1.61.187] => (item=['b', 2]) => {
"msg": "b and 2"
}
ok: [10.1.61.187] => (item=['c', None]) => {
"msg": "c and "
}
PLAY RECAP ******************************************************************************************************************************************************************
10.1.61.187 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
12.5、with_nested
嵌套循环
Copy 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]是第二个列表的值;以上的执行输出如下:
Copy [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
下面是一个稍微有用点儿的示例:
Copy - hosts: test
gather_facts: false
tasks:
- file:
state: directory
path: "/data/{{ item[0] }}/{{ item[1] }}"
with_nested:
- [test1,test2]
- [a,b,c]
with_cartesian与其功能完全一致
12.6、with_indexed_items
在循环处理列表时,为列表中的每一项添加索引(从0开始的数字索引)
简单示例:
Copy - hosts: test
gather_facts: false
tasks:
- debug:
msg: "{{ item }}"
with_indexed_items:
- test1
- test2
- test3
执行之后,返回结果如下:
Copy [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执行如下操作:
Copy - hosts: test
gather_facts: false
tasks:
- debug:
msg: "index is {{ item[0] }}, value is {{ item[1] }}"
with_indexed_items:
- test1
- test2
- test3
下面再看一个稍微复杂的列表结构:
Copy - hosts: test
gather_facts: false
tasks:
- debug:
msg: "index is {{ item[0] }}, value is {{ item[1] }}"
with_indexed_items:
- test1
- [test2,test3]
- [test4,test5]
这个时候,返回的结果如下:
Copy # 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一样,其也只会拉平第一层列表,如果存在多层列表嵌套,则更深的嵌套不会被拉平:
Copy - hosts: test
gather_facts: false
tasks:
- debug:
msg: "index is {{ item[0] }}, value is {{ item[1] }}"
with_indexed_items:
- test1
- [test2,[test3,test4]]
此时的返回结果:
Copy [root@controller ansible]# ansible-playbook with_indexed_items_ex3.yml
PLAY [test] ********************************************************************************************************************************
TASK [debug] *******************************************************************************************************************************
ok: [10.1.61.187] => (item=[0, 'test1']) => {
"msg": "index is 0, value is test1"
}
ok: [10.1.61.187] => (item=[1, 'test2']) => {
"msg": "index is 1, value is test2"
}
ok: [10.1.61.187] => (item=[2, ['test3', 'test4']]) => {
"msg": "index is 2, value is ['test3', 'test4']"
}
PLAY RECAP *********************************************************************************************************************************
10.1.61.187 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
12.7、with_sequence
用于返回一个数字序列
参数说明:
stride:指定步长,即从start至end,每次增加的值
count:生成连续的数字序列,从1开始,到count的值结束
format:格式化输出,类似于linux命令行中的printf格式化输出
关于format参数,更多的格式化输出参数可参考:12printf命令详解-朱双印博客
Copy - 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,每次都随机返回一个:
Copy - hosts: test
gather_facts: false
tasks:
- debug: msg={{ item }}
with_random_choice:
- "go through the door"
- "drink from the goblet"
- "press the red button"
- "do nothing"
12.9、with_dict
循环字典
Copy - 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 }}"
输出如下:
Copy [root@controller ansible]# ansible-playbook with_dict_ex.yml
PLAY [test] ******************************************************************************************************************************************************************************************************
TASK [Print phone records] ***************************************************************************************************************************************************************************************
ok: [10.1.61.187] => (item={'key': 'alice', 'value': {'name': 'Alice Appleworth', 'telephone': '123-456-7890'}}) => {
"msg": "User alice is Alice Appleworth (123-456-7890)"
}
ok: [10.1.61.187] => (item={'key': 'bob', 'value': {'name': 'Bob Bananarama', 'telephone': '987-654-3210'}}) => {
"msg": "User bob is Bob Bananarama (987-654-3210)"
}
PLAY RECAP *******************************************************************************************************************************************************************************************************
10.1.61.187 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
12.10、with_subelement
with_subelement简单来讲,就是在一个复杂的列表当中,可以对这个列表变量的子元素进行遍历
下面是一个简单的示例:
Copy
- hosts: test
gather_facts: no
vars:
users:
- name: bob
hobby:
- Games
- Sports
- name: alice
hobby:
- Music
tasks:
- debug:
msg: "{{ item }}"
with_subelement:
- "{{ users }}"
- hobby
输出结果如下:
Copy [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登录帐号并为其授权。
示例如下:
Copy - hosts: test
gather_facts: false
vars:
users:
- name: alice
authorized:
- files/keys/master1.id_rsa.pub
- files/keys/master2.id_rsa.pub
mysql:
password: mysql-password
hosts:
- "%"
- "127.0.0.1"
- "::1"
- "localhost"
privs:
- "*.*:SELECT"
- "DB1.*:ALL"
- name: bob
authorized:
- files/keys/master3.id_rsa.pub
mysql:
password: other-mysql-password
hosts:
- "db1"
privs:
- "*.*:SELECT"
- "DB2.*:ALL"
tasks:
- user:
name: "{{ item.name }}"
state: present
generate_ssh_key: yes
with_items: "{{ users }}"
- authorized_key:
user: "{{ item.0.name }}"
key: "{{ lookup('file', item.1) }}"
with_subelements:
- "{{ users }}"
- authorized
- name: Setup MySQL users
mysql_user:
name: "{{ item.0.name }}"
password: "{{ item.0.mysql.password }}""
host: "{{ item.1 }} priv={{ item.0.mysql.privs | join('/') }}"
with_subelements:
- "{{ users }"
- mysql.hosts
12.11、with_file
用于循环主控端的文件列表,获取文件中的内容
注意: 循环的是主控端的文件列表,不是被控端的
Copy - hosts: test
gather_facts: false
tasks:
- debug:
msg: {{ item }}
with_file:
- /etc/ansible/test1.yml
- /etc/ansible/test2.yml
输出如下:
Copy [root@controller ansible]# ansible-playbook with_file_ex.yml
PLAY [test] ********************************************************************************************************************************
TASK [debug] *******************************************************************************************************************************
ok: [10.1.61.187] => (item=content: test1.yaml) => {
"msg": "content: test1.yaml"
}
ok: [10.1.61.187] => (item=content: test2.yml) => {
"msg": "content: test2.yml"
}
PLAY RECAP *********************************************************************************************************************************
10.1.61.187 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
12.12、with_fileglob
上面with_file用于获取文件的内容,而with_fileglob则用于匹配文件名称。可以通过该关键字,在指定的目录中匹配符合模式的文件名。与with_file相同的是,with_fileglob 操作的文件也是主控端的文件而非被控端的文件
Copy - hosts: test
tasks:
- name: Make key directory
file:
path: /root/.sshkeys
state: directory
mode: 0700
owner: root
group: root
- name: Upload public keys
copy:
src: "{{ item }}"
dest: /root/.sshkeys
mode: 0600
owner: root
group: root
with_fileglob:
- /root/.ssh/*.pub
- name: Assemble keys into authorized_keys file
assemble:
src: /root/.sshkeys
dest: /root/.ssh/authorized_keys
mode: 0600
owner: root
group: root
12.13、with_lines
with_lines循环结构会让你在控制主机上执行任意命令,并对命令的输出进行逐行迭代。
假设我们有一个文件test.txt包含如下行:
Copy Breeze Yan
Bernie Yang
jerry Qing
我们可以通过如下方法进行逐行输出:
Copy - name: print all names
debug: msg="{{ item }}"
with_lines:
- cat test.txt
12.14、do-Until循环
Copy - action: shell /usr/bin/foo
register: result
until: result.stdout.find("all systems go") != -1
retries: 5
delay: 10
重复执行shell模块,当shell模块执行的命令输出内容包含"all systems go"的时候停止。重试5次,延迟时间10秒。retries默认值为3,delay默认值为5。任务的返回值为最后一次循环的返回结果。
13. Ansible Playbook loop循环语句
loop关键字说明 在ansible 2.5及以前的版本当中,所有的循环都是使用with_X风格。但是从2.6版本开始,官方开始推荐使用loop关键字来代替with_X风格的关键字。
在playbook中使用循环,直接使用loop关键字即可。
如下示例,启动httpd和postfix服务:
Copy tasks:
- name: postfix and httpd are running
service:
name: "{{ item }}"
state: started
loop:
- postfix
- httpd
那么在这个示例当中,其实就是使用loop代替了with_list循环。
事实上,我们可以使用loop关键字搭配一些过滤器来替换更多的、更复杂的with_X循环。
13.1、loop_control
loop_control用于在循环时,获取列表的索引
Copy - 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
输出结果:
Copy PLAY [localhost] ******************************************************************
TASK [debug] **********************************************************************
ok: [localhost] => (item=a) => {
"msg": "0: a"
}
ok: [localhost] => (item=b) => {
"msg": "1: b"
}
ok: [localhost] => (item=c) => {
"msg": "2: c"
}
ok: [localhost] => (item=[u'e', u'f']) => {
"msg": "3: [u'e', u'f']"
}
ok: [localhost] => (item=d) => {
"msg": "4: d"
}
PLAY RECAP ************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
参数说明:
index_var:loop_control的选项,让我们指定一个变量,loop_control会将列表元素的索引值存放到这个指定的变量中
13.2、with_list
loop可以替代with_list,当处理嵌套列表时,列表不会被拉平
Copy - hosts: test
gather_facts: no
vars:
testlist:
- a
- [b,c]
- d
tasks:
- debug:
msg: "{{ item }}"
loop: "{{ testlist }}"
13.3、with_flattened
将所有嵌套都拉平
Copy - hosts: test
gather_facts: no
vars:
testlist:
- a
- [b,c]
- d
tasks:
- debug:
msg: "{{ item }}"
loop: "{{ testlist| flatten }}"
13.4、with_items
只拉平第一层
- hosts: test
gather_facts: no
vars:
testlist:
- a
- [b,c]
- d
tasks:
- debug:
msg: "{{ item }}"
loop: "{{ testlist| flatten(levels=1) }}"
13.5、with_indexed_items
通过flatten过滤器(加参数),再配合loop_control关键字,可以替代with_indexed_items,当处理多层嵌套的列表时,只有列表中的第一层会被拉平
Copy - hosts: test
gather_facts: no
vars:
testlist:
- a
- [b,c]
- d
tasks:
- debug:
msg: "{{ index }}: {{ item }}"
loop: "{{ testlist | flatten(levels=1) }}"
loop_control:
index_var: index
参数说明:
loop_control: 用于控制循环的行为,比如在循环时获取到元素的索引
index_var:loop_control的选项,让我们指定一个变量,loop_control会将列表元素的索引值存放到这个指定的变量中
13.6、with_together
zip_longest过滤器配合list过滤器,可以替代with_together
Copy - 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"代替空值:
Copy - debug:
msg: "{{ item.0 }} - {{ item.1 }} - {{ item.2 }}"
loop: {{ testlist1 | zip_longest(testlist2,testlist3,filevalue='NONE') | list }}
zip_longest默认使用最长的列表长度进行对齐,当有多个列表的长度不同时,如果希望使用最短的列表对齐,则可以使用zip过滤器:
Copy - debug:
msg: "{{ item.0 }} - {{ item.1 }} - {{ item.2 }}"
loop: {{ testlist1 | zip(testlist2,testlist3) | list }}
13.7、with_nested/with_cartesian
可使用product过滤器配合list过滤器以替代with_nested或者with_cartesian。product过滤器也是需要将组合后的数据进行列表化,所以需要与list过滤器配合使用:
Copy - hosts: test
gather_facts: no
vars:
testlist1: [a,b,c]
testlist2: [1,2,3,4]
tasks:
- debug:
msg: "{{item.0}} --- { item.1}"
loop: {{ testlist1 | product(testlist2) | list}}
13.8、with_sequence
使用range过滤器配合list过滤器可以替代with_sequence:
Copy - 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还有格式化的功能:
Copy - debug:
msg: "{{ item }}"
with_sequence: start=2 end=6 stride=2 format="number is %0.2f"
可使用format配合loop实现:
Copy - debug:
msg: "{{ 'number is %0.2f' |format(item) }}"
loop: "{{range(2,7,2) |list}}"
13.9、with_random_choice
使用random函数可以替代with_random_choice,由于random函数是随机取出列表中的一个值,并不涉及循环操作,所以并不使用loop关键字:
Copy - hosts: test
gather_facts: no
vars:
testlist: [a,b,c]
tasks:
- debug:
msg: "{{ testlist | random }}"
13.10、with_dict
可使用loop配合dict2items过滤器实现with_dict功能:
Copy - hosts: test
gather_facts: no
vars:
users:
alice: female
bob: male
tasks:
- debug:
msg: "{{ item.key }} is {{ item.value}}"
loop: "{{users | dict2items }}"
13.11、with_subelements
可使用loop配合subelements过滤器替代with_subelements:
Copy - hosts: test
gather_facts: no
vars:
users:
- name: bob
gender: male
hobby:
- Skateboard
- VideoGame
- name: alice
gender: female
hobby:
- Music
tasks:
- debug:
msg: "{{ item.0.name }}'s hobby is {{ item.1}}"
with_subelements:
- "{{ users }}"
- hobby
- debug:
msg: "{{ item.0.name }}'s hobby is {{ item.1}}"
loop: "{{ users | subelements('hobby') }}"
13.12、使用zip_longest过滤器将两个列表中的元素对齐合并
Copy - 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}}"
输出结果如下:
Copy 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"代替空值:
Copy - debug:
msg: "{{ item.0 }} - {{ item.1 }} - {{ item.2 }}"
loop: {{ testlist1 | zip_longest(testlist2,testlist3,filevalue='NONE') | list }}
zip_longest默认使用最长的列表长度进行对齐,当有多个列表的长度不同时,如果希望使用最短的列表对齐,则可以使用zip过滤器:
Copy - debug:
msg: "{{ item.0 }} - {{ item.1 }} - {{ item.2 }}" # a,1,A b,2,B
loop: {{ testlist1 | zip(testlist2,testlist3) | list }}
13.13、在循环语句中注册变量
下面是一个register的变量在循环中使用的例子:
Copy [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 }}"
在循环语句中注册变量:
Copy - 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
执行语句,可以看到变量的返回结果为一个字典列表:
Copy ok: [10.1.61.187] => {
"echo_results": {
"changed": true,
"msg": "All items completed",
"results": [
{
"ansible_loop_var": "item",
"changed": true,
"cmd": "echo this is my item: one",
"delta": "0:00:00.004905",
"end": "2019-06-10 00:23:51.814151",
"failed": false,
"invocation": {
"module_args": {
"_raw_params": "echo this is my item: one",
"_uses_shell": true,
"argv": null,
"chdir": null,
"creates": null,
"executable": null,
"removes": null,
"stdin": null,
"stdin_add_newline": true,
"strip_empty_ends": true,
"warn": true
}
},
"item": "one",
"rc": 0,
"start": "2019-06-10 00:23:51.809246",
"stderr": "",
"stderr_lines": [],
"stdout": "this is my item: one",
"stdout_lines": [
"this is my item: one"
]
},
{
"ansible_loop_var": "item",
"changed": true,
"cmd": "echo this is my item: two",
"delta": "0:00:00.004736",
"end": "2019-06-10 00:23:52.008981",
"failed": false,
"invocation": {
"module_args": {
"_raw_params": "echo this is my item: two",
"_uses_shell": true,
"argv": null,
"chdir": null,
"creates": null,
"executable": null,
"removes": null,
"stdin": null,
"stdin_add_newline": true,
"strip_empty_ends": true,
"warn": true
}
},
"item": "two",
"rc": 0,
"start": "2019-06-10 00:23:52.004245",
"stderr": "",
"stderr_lines": [],
"stdout": "this is my item: two",
"stdout_lines": [
"this is my item: two"
]
}
]
}
}
14. Ansible文件管理模块及Jinja2过滤器
对于任何自动管理工具而言,对于文件的管理都是其绕不开的话题。同样,ansible也围绕文件管理提供了众多的模块。同时还提供了Jinja2模板语法来配置文件模板。
14.1、常用文件管理模块
14.1.1、file
我们在讲ansible ad-hoc的时候,已经说过file模块,在playbook中的使用也没什么不同,下面给个简单的示例:
Copy - name: Touch a file and set permissions
file:
path: /path/to/file
owner: user1
group: group1
mode: 0640
state: touch
14.1.2、synchronize
synchronize模块示例:
Copy - name: synchronize local file to remote files
synchronize:
src: file
dest: /path/to/file
14.1.3、copy
同样的,我们已经介绍过copy模块,示例如下:
Copy - name: copy a file to managed hosts
copy:
src: file
dest: /path/to/file
14.1.4、fetch
fetch模块与copy模块正好相反,copy是把主控端的文件复制到被控端,而fetch则是把被控端的文件复制到主控端。并且在主控端指定的目录下,以被控端主机名的形式来组织目录结构。
Copy - 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
在主控端文件存储的目录树如下:
Copy # tree secure-backups/
secure-backups/
└── 10.1.61.187
└── var
└── log
└── secure
3 directories, 1 file
参考:https://docs.ansible.com/ansible/latest/modules/fetch_module.html#fetch-module
14.1.5、lineinfile
lineinfile是一个非常有用的模块,而且相对来说,也是用法比较复杂的模块,可直接参考《Ansible lineinfile模块》
14.1.6、stat
stat模块与linux中的stat命令一样,用来显示文件的状态信息。
Copy - name: Verify the checksum of a file
stat:
path: /path/to/file
checksum_algorithm: md5
register: result
- debug:
msg: "The checksum of the file is {{ result.stat.checksum }}"
参考: https://docs.ansible.com/ansible/latest/modules/stat_module.html#stat-module
14.1.7、blockinfile
围绕着被标记的行插入、更新、删除一个文本块。
Copy [root@controller ansible]# cat files/test.html
<html>
<head>
</head>
<body>
</body>
</html>
[root@controller ansible]# cat blockinfile_ex.yml
---
- name: blockinfile module test
hosts: test
tasks:
- name: copy test.html to dest
copy:
src: files/test.html
dest: /var/www/html/test.html
- name: add block
blockinfile:
marker: "<!-- {mark} ANSIBLE MANAGED BLOCK -->"
insertafter: "<body>"
path: /var/www/html/test.html
block: |
<h1>Welcome to {{ ansible_hostname }}</h1>
<p>Last updated on {{ ansible_date_time.iso8601 }}</p>
执行后结果如下:
Copy [root@controller ansible]# cat test.html
<html>
<head>
</head>
<body>
<!-- BEGIN ANSIBLE MANAGED BLOCK -->
<h1>Welcome to app</h1>
<p>Last updated on 2019-05-28T15:00:03Z</p>
<!-- END ANSIBLE MANAGED BLOCK -->
</body>
</html>
更多blockinfile用法参考:ansible.builtin.blockinfile module – Insert/update/remove a text block surrounded by marker lines — Ansible Documentation
14.2、Jinja2模板管理
Jinja2简介 Jinja2是基于python的模板引擎。那么什么是模板?
假设说现在我们需要一次性在10台主机上安装redis,这个通过playbook现在已经很容易实现。默认情况下,所有的redis安装完成之后,我们可以统一为其分发配置文件。这个时候就面临一个问题,这些redis需要监听的地址各不相同,我们也不可能为每一个redis单独写一个配置文件。因为这些配置文件中,绝大部分的配置其实都是相同的。这个时候最好的方式其实就是用一个通用的配置文件来解决所有的问题。将所有需要修改的地方使用变量替换,如下示例中redis.conf.j2文件:
Copy daemonize yes
supervised systemd
pidfile /var/run/redis.pid
port 6379
logfile "/var/log/redis/redis.log"
dbfilename dump.rdb
dir /data/redis
maxmemory {{ (ansible_memtotal_mb /2) | int }}M
bind {{ ansible_eth0.ipv4.address }} 127.0.0.1
timeout 300
loglevel notice
databases 16
save 900 1
save 300 10
save 60 10000
rdbcompression yes
maxclients 10000
appendonly yes
appendfilename appendonly.aof
appendfsync everysec
那么此时,redis.conf.j2文件就是一个模板文件。{{ ansible_eth0.ipv4.address }}是一个fact变量,用于获取被控端ip地址以实现替换。
14.2.1、在playbook中使用jinja2
现在我们有了一个模板文件,那么在playbook中如何来使用呢?
playbook使用template模块来实现模板文件的分发,其用法与copy模块基本相同,唯一的区别是,copy模块会将原文件原封不动的复制到被控端,而template会将原文件复制到被控端,并且使用变量的值将文件中的变量替换以生成完整的配置文件。
下面是一个完整的示例:
Copy # 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配置文件如下:
Copy daemonize yes
pidfile /var/run/redis.pid
port 6379
logfile "/var/log/redis/redis.log"
dbfilename _dump_.rdb
dir /data/redis
maxmemory 1G
bind 10.1.61.187 127.0.0.1
timeout 300
loglevel notice
databases 16
save 900 1
save 300 10
save 60 10000
rdbcompression yes
maxclients 10000
appendonly yes
appendfilename appendonly.aof
appendfsync everysec
关于template模块的更多参数说明:
backup:如果原目标文件存在,则先备份目标文件
validate:在复制之前通过命令验证目标文件,如果验证通过则复制
14.2.2、Jinja2条件语句
在上面的示例中,我们直接取了被控节点的eth0网卡的ip作为其监听地址。那么假如有些机器的网卡是bond0,这种做法就会报错。这个时候我们就需要在模板文件中定义条件语句如下:
Copy daemonize yes
pidfile /var/run/redis.pid
port 6379
logfile "/var/log/redis/redis.log"
dbfilename dump.rdb
dir /data/redis
maxmemory 1G
<div data-gb-custom-block data-tag="if" data-0='0' data-1='0' data-2='0' data-3='0' data-4='0' data-5='0' data-6='0' data-7='0' data-8='0' data-9='0' data-10='0' data-11='0' data-12='0'>
bind {{ ansible_bond0.ipv4.address }} 127.0.0.1
<div data-gb-custom-block data-tag="elif" data-0='0' data-1='0' data-2='0' data-3='0' data-4='0' data-5='0' data-6='0' data-7='0' data-8='0' data-9='0' data-10='0' data-11='0'></div>
bind {{ ansible_eth0.ipv4.address }} 127.0.0.1
<div data-gb-custom-block data-tag="else"></div>
bind 0.0.0.0
</div>
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主从角色都可以使用该文件:
Copy 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
<div data-gb-custom-block data-tag="if" data-0='0' data-1='0' data-2='0' data-3='0' data-4='0' data-5='0' data-6='0' data-7='0' data-8='0' data-9='0' data-10='0' data-11='0' data-12='0'>
bind {{ ansible_bond0.ipv4.address }} 127.0.0.1
<div data-gb-custom-block data-tag="elif" data-0='0' data-1='0' data-2='0' data-3='0' data-4='0' data-5='0' data-6='0' data-7='0' data-8='0' data-9='0' data-10='0' data-11='0' data-12='0'></div>
bind {{ ansible_bond0.ipv4.address }} 127.0.0.1
<div data-gb-custom-block data-tag="else"></div>
bind 0.0.0.0
</div>
<div data-gb-custom-block data-tag="if">
slaveof {{ hostvars[groups.redismaster.0].ansible_bond0.ipv4.address }} {{ masterport|default(6379) }}
</div>
<div data-gb-custom-block data-tag="if">
masterauth {{ masterpass }}
</div>
<div data-gb-custom-block data-tag="if">
requirepass {{ requirepass }}
</div>
#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如下:
Copy all:
children:
redis:
children:
redismaster:
vars:
requirepass: redhat
hosts:
servera:
redisslave:
vars:
masterauth: redhat
masterport: 6379
masterip: yes
hosts:
serverb:
serverc:
测试
Copy [root@servera ~]# redis-cli
127.0.0.1:6379> auth redhat
127.0.0.1:6379> info
role:master
[root@serverb ~]# redis-cli
127.0.0.1:6379> info
role:slave
14.2.3、Jinja2循环语句
定义一个inventory示例如下:
Copy [proxy]
10.1.61.195
[webservers]
10.1.61.27
10.1.61.187
现在把proxy主机组中的主机作为代理服务器,安装nginx做反向代理,将请求转发至后面的两台webserver,即webserver组的服务器。
现在我们编写一个playbook如下:
Copy [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示例如下:
Copy [root@controller ansible]# cat nginx.conf.j2
user nginx;
worker_processes {{ ansible_processor_vcpus }};
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 65535;
use epoll;
}
http {
map $http_x_forwarded_for $clientRealIP {
"" $remote_addr;
~^(?P<firstAddr>[0-9\.]+),?.*$ $firstAddr;
}
log_format real_ip '{ "datetime": "$time_local", '
'"remote_addr": "$remote_addr", '
'"source_addr": "$clientRealIP", '
'"x_forwarded_for": "$http_x_forwarded_for", '
'"request": "$request_uri", '
'"status": "$status", '
'"request_method": "$request_method", '
'"request_length": "$request_length", '
'"body_bytes_sent": "$body_bytes_sent", '
'"request_time": "$request_time", '
'"http_referrer": "$http_referer", '
'"user_agent": "$http_user_agent", '
'"upstream_addr": "$upstream_addr", '
'"upstream_status": "$upstream_status", '
'"upstream_http_header": "$upstream_http_host",'
'"upstream_response_time": "$upstream_response_time", '
'"x-req-id": "$http_x_request_id", '
'"servername": "$host"'
' }';
access_log /var/log/nginx/access.log real_ip;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
include /etc/nginx/conf.d/*.conf;
upstream web {
<div data-gb-custom-block data-tag="for" data-0='webservers' data-1='webservers'></div>
<div data-gb-custom-block data-tag="for" data-0='webservers' data-1='webservers'>
<div data-gb-custom-block data-tag="if" data-0='0' data-1='0' data-2='0' data-3='0' data-4='0' data-5='0' data-6='0' data-7='0' data-8='0' data-9='0' data-10='0' data-11='0' data-12='0'>
server {{ hostvars[host].ansible_bond0.ipv4.address }}:80;
<div data-gb-custom-block data-tag="elif" data-0='33' data-1='33' data-2='33' data-3='33' data-4='33' data-5='33' data-6='3'></div>
server {{ hostvars[host].ansible_ens33.ipv4.address }}:80;
</div>
</div>
}
server {
listen 80 default_server;
server_name _;
location / {
proxy_pass http://web;
}
}
}
for循环只能循环列表,不能循环字典,字典需要转换为列表才能循环
14.2.3、Jinja2过滤器
当指定的变量不存在时,用于设定默认值
简单示例:
Copy "Host": "{{ db_host | default('lcoalhost') }}"
复杂一点儿的实例:
Copy - 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 }}"
capitalize:将字符串的首字母大写,其他字母小写
center(30):将字符串放在中间,并且字符串两边用空格补齐30位
shuffle:list将字符串转换为列表,但是顺序排列,shuffle同样将字符串转换为列表,但是会随机打乱字符串顺序 示例:
Copy - 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 }}"
Copy - 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) }}"
sort:重新排列列表,默认为升序排列,sort(reverse=true)为降序
flatten:如果列表中包含列表,则flatten可拉平嵌套的列表,levels参数可用于指定被拉平的层级
union:将两个列表合并,如果元素有重复,则只留下一个
difference:获取存在于第一个列表中,但不存在于第二个列表中的元素
symmetric_difference:取出两个列表中各自独立的元素,如果重复则只留一个 示例:
Copy - 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) }}"
Copy - hosts: test
gather_facts: no
tasks:
- debug:
msg: "{{ 'redhat' | hash('sha1') }}"
#结果:
ok: [servera] => {
"msg": "3c767c41afb12ada140190ed82db3fd930e2efa3"
#相当于:
# echo -n redhat | sha1sum
3c767c41afb12ada140190ed82db3fd930e2efa3 -
Copy - hosts: test
gather_facts: no
tasks:
- user:
name: john
password: "{{ password }}"
vars:
password: "{{ 'redhat' | password_hash('sha512') }}"
Copy - hosts: test
gather_facts: no
tasks:
- debug:
msg: "{{ 'redhat' | b64encode | b64decode }}"
b64encode: 加密
b64decode: 解密
# 相当于:
echo -n redhat | base64 -w 0 | base64 -d
Copy - hosts: test
gather_facts: no
tasks:
- debug:
msg: "{{ 'marvin, arthur' | replace('ar','**') }}"
Copy - hosts: test
gather_facts: no
tasks:
- debug:
msg: "{{ 'marvin, arthur' | regex_search('ar\\S*r') }}"
\是特殊字符,需要特殊处理以后再交给正则表达式
Copy - hosts: test
gather_facts: no
vars:
str1: 'marvin, arthur'
tasks:
- debug:
msg: "{{ str1 | regex_replace('ar(\\S*)r','\\1mb') }}"
正则表达式:
Copy \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个分组的内容,如果它经匹配。否则指的是八进制字符码的表达式。
Copy - 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)}}"
Copy tags:
Application: payment
Environment: dev
# 转化后:
{{ dict | dict2items }}
- key: Application
value: payment
- key: Environment
value: dev
示例二:
Copy 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
Copy tags:
- key: Application
value: payment
- key: Environment
value: dev
{{ tags | items2dict }}
# 转化后:
Application: payment
Environment: dev
示例二:
Copy fruits:
- fruit: apple
color: red
- fruit: pear
color: yellow
- fruit: grapefruit
color: yellow
{{ fruits | items2dict(key_name='fruit', value_name='color') }}
# 必须指定key_name和value_name,否则会报KeyError: 'key'或者
# 转化后:
"apple": "red",
"grapefruit": "yellow",
"pear": "yellow"
json_query 将变量转换成json,并查询子元素
Copy - 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') }}"
Copy {{ some_variable | from_json }}
{{ some_variable | from_yaml }}
- hosts: localhost
gather_facts: no
tasks:
- name: from_json filter test
debug:
msg: "{{ domain_definition }}"
vars:
domain_definition: "{{ lookup('file','json_example.json') | from_json }}"
to_json/to_yaml/to_nice_json/to_nice_yaml
Copy {{ 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"]}]'
示例变量:
Copy # 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"
}
]
}
}
ipaddr过滤器主要用于处理网络地址, 其支持的过滤器参数如下:
ipv4 and ipv6 要想使用ipaddr过滤器,需要在主控端安装python-netaddr包:
Copy yum install -y python-netaddr
示例:
Copy - 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') }}"
输出:
Copy 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"
]
}
urlspilt用于分割url,并取出相应字段
Copy - hosts: localhost
gather_facts: no
vars:
url: 'http://user:password@www.example.com:8080/xxx/index.html?query=132'
tasks:
- name: "get url hostname"
debug:
msg: "{{ url | urlsplit('hostname') }}"
# ==> "msg": "www.example.com"
- name: "get url netloc"
debug:
msg: "{{ url | urlsplit('netloc') }}"
# ==> "user:password@www.example.com:8080"
- name: "get url path"
debug:
msg: "{{ url | urlsplit('path') }}"
# ==> "msg": "/xxx/index.html"
- name: "get url port"
debug:
msg: "{{ url | urlsplit('port') }}"
# ==> "msg": "8080"
- name: "get url scheme"
debug:
msg: "{{ url | urlsplit('scheme') }}"
# ==> "msg": "http"
- name: "get url query"
debug:
msg: "{{ url | urlsplit('query') }}"
# ==> "msg": "query=132"
正常情况下,当某个task执行失败的时候,ansible会中止运行。此时我们可以通过ignore_errors来捕获异常以让task继续往下执行。然后调用debug模块打印出出错时的内容,拿来错误结果后,主动失败。
Copy - name: Run myprog
command: /opt/myprog
register: result
ignore_errors: True
- debug:
var: result
- debug:
msg: "Stop running the playbook if myprog failed"
failed_when: result|failed
任务返回值过滤器:
failed: 如果注册变量的值是任务failed则返回True
changed: 如果注册变量的值是任务changed则返回True
success:如果注册变量的值是任务succeeded则返回True
skipped:如果注册变量的值是任务skipped则返回True 在ansible2.9中,该方式会被废弃,不推荐使用
expanduser:将文件路径中的~替换为用户目录
realpath:处理符号链接后的文件实际路径 下面是一个示例:
Copy - 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 }}"
举个简单的例子,现在有一个playbook如下:
Copy - 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如下:
Copy hosts = [{{ domains | join(',') }}]
执行playbook后,在目标机上的test.conf如下:
Copy hosts = [www.example.com,example.com]
现在如果希望目标机上的test.conf文件返回结果如下:
Copy hosts = ["www.example.com","example.com"]
没有现成的过滤器来帮我们做这件事情。我们可以自己简单写一个surround_by_quote.py内容如下:
Copy # 定义过滤器执行的操作
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如下:
Copy hosts = [{{ domains | join(',') |surround_by_quote }}]
再次执行playbook,最后返回结果:
Copy hosts = ["www.example.com","example.com"]
关于jinja2更多用法参考:http://docs.jinkan.org/docs/jinja2/
15. Ansible Playbook高级用法
15.1、任务委托 在有些时候,我们希望运行与选定的主机或主机组相关联的task,但是这个task又不需要在选定的主机或主机组上执行,而需要在另一台服务器上执行。
这种特性适用于以下场景:
可以使用delegate_to语句来在另一台主机上运行task:
Copy ---
- hosts: webservers
tasks:
- name: Take out of load balancer pool
command: /usr/bin/take_out_of_pool {{ inventory_hostname }}
delegate_to: 127.0.0.1
- name: Actual steps would go here
yum:
name: acme-web-stack
state: latest
- name: Add back to load balancer pool
command: /usr/bin/add_back_to_pool {{ inventory_hostname }}
delegate_to: 127.0.0.1
如果delegate_to: 127.0.0.1的时候,等价于local_action
15.2、本地执行
如果希望在控制主机本地运行一个特定的任务,可以使用local_action语句。
假设我们需要配置的远程主机刚刚启动,如果我们直接运行playbook,可能会因为sshd服务尚未开始监听而导致失败,我们可以在控制主机上使用如下示例来等待被控端sshd端口监听:
Copy tasks:
- name: test acesss url
local_action: command curl http://xxx
tasks:
- name: Recursively copy files from management server to target
local_action: command rsync -a /path/to/files {{ inventory_hostname }}:/path/to/target/
15.3、任务暂停 有些情况下,一些任务的运行需要等待一些状态的恢复,比如某一台主机或者应用刚刚重启,我们需要需要等待它上面的某个端口开启,此时就需要将正在运行的任务暂停,直到其状态满足要求。
Ansible提供了wait_for模块以实现任务暂停的需求
wait_for模块常用参数:
connect_timeout:在下一个任务执行之前等待连接的超时时间
delay:等待一个端口或者文件或者连接到指定的状态时,默认超时时间为300秒,在这等待的300s的时间里,wait_for模块会一直轮询指定的对象是否到达指定的状态,delay即为多长时间轮询一次状态。
host:wait_for模块等待的主机的地址,默认为127.0.0.1
path:文件路径,只有当这个文件存在时,下一任务才开始执行,即等待该文件创建完成
state:等待的状态,即等待的文件或端口或者连接状态达到指定的状态时,下一个任务开始执行。当等的对象为端口时,状态有started,stoped,即端口已经监听或者端口已经关闭;当等待的对象为文件时,状态有present或者started,absent,即文件已创建或者删除;当等待的对象为一个连接时,状态有drained,即连接已建立。默认为started
timeout:wait_for的等待的超时时间,默认为300秒 示例:
Copy #等待8080端口已正常监听,才开始下一个任务,直到超时
- wait_for:
port: 8080
state: started
#等待8000端口正常监听,每隔10s检查一次,直至等待超时
- wait_for:
port: 8000
delay: 10
#等待8000端口直至有连接建立
- wait_for:
host: 0.0.0.0
port: 8000
delay: 10
state: drained
#等待8000端口有连接建立,如果连接来自10.2.1.2或者10.2.1.3,则忽略。
- wait_for:
host: 0.0.0.0
port: 8000
state: drained
exclude_hosts: 10.2.1.2,10.2.1.3
#等待/tmp/foo文件已创建
- wait_for:
path: /tmp/foo
#等待/tmp/foo文件已创建,而且该文件中需要包含completed字符串
- wait_for:
path: /tmp/foo
search_regex: completed
#等待/var/lock/file.lock被删除
- wait_for:
path: /var/lock/file.lock
state: absent
#等待指定的进程被销毁
- wait_for:
path: /proc/3466/status
state: absent
15.4、滚动执行 默认情况下,ansible会并行的在所有选定的主机或主机组上执行每一个task,但有的时候,我们会希望能够逐台运行。最典型的例子就是对负载均衡器后面的应用服务器进行更新时。通常来讲,我们会将应用服务器逐台从负载均衡器上摘除,更新,然后再添加回去。我们可以在play中使用serial语句来告诉ansible限制并行执行play的主机数量。
下面是一个在amazon EC2的负载均衡器中移除主机,更新软件包,再添加回负载均衡的配置示例:
Copy
- 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失败。示例如下:
Copy - name: upgrade pkgs on fservers behind load balancer
hosts: myhosts
serial: 1
max_fail_percentage: 25
tasks:
......
假如负载均衡后面有4台主机,并且有一台主机执行失败,这时ansible还会继续运行,要让Play停止运行,则必须超过25%,所以如果想一台失败就停止执行,我们可以将max_fail_percentage的值设为24。如果我们希望只要有执行失败,就放弃执行,我们可以将max_fail_percentage的值设为0。
15.5、只执行一次
某些时候,我们希望某个task只执行一次,即使它被绑定到了多个主机上。例如在一个负载均衡器后面有多台应用服务器,我们希望执行一个数据库迁移,只需要在一个应用服务器上执行操作即可。
可以使用run_once语句来处理:
Copy - name: run the database migrateions
command: /opt/run_migrateions
run_once: true
还可以与local_action配合使用,如下:
Copy - name: run the task locally, only once
command: /opt/my-custom-command
connection: local
run_once: true
还可以与delegate_to配合使用,让这个只执行一次的任务在指定的机器上运行:
Copy - name: run the task locally, only once
command: /opt/my-custom-command
run_once: true
delegate_to: app.a1-61-105.dev.unp
15.6、设置环境变量
我们在命令行下执行某些命令的时候,这些命令可能会需要依赖环境变量。比如在安装某些包的时候,可能需要通过代理才能完成完装。或者某个脚本可能需要调用某个环境变量才能完成运行。
ansible 支持通过environment关键字来定义一些环境变量。
在如下场景中可能需要用到环境变量:
需要加载一些库,这些库不在系统的标准库路径当中 下面是一个简单示例:
Copy ---
- 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也可以存储在变量当中:
Copy - hosts: all
remote_user: root
vars:
proxy_env:
http_proxy: http://proxy.example.com:8080
https_proxy: http://proxy.bos.example.com:8080
tasks:
- apt:
name: cobbler
state: installed
environment: proxy_env
15.7、交互式提示
在少数情况下,ansible任务运行的过程中需要用户输入一些数据,这些数据要么比较秘密不方便,或者数据是动态的,不同的用户有不同的需求,比如输入用户自己的账户和密码或者输入不同的版本号会触发不同的后续操作等。ansible的vars_prompt关键字就是用来处理上述这种与用户交互的情况的。
Copy - hosts: all
remote_user: root
vars_prompt:
- name: share_user
prompt: "what is your network username?"
private: yes
- name: share_pass
prompt: "what is your network password"
private: yes
tasks:
- debug:
var: share_user
- debug:
var: share_pass
vars_prompt常用选项说明:
private: 默认为yes,表示用户输入的值在命令行不可见
default:定义默认值,当用户未输入时则使用默认值
confirm:如果设置为yes,则会要求用户输入两次,适合输入密码的情况
16. Ansible Playbook之tags
在大型项目当中,通常一个playbook会有非常多的task。而我们每次执行这个playbook时,都会将所有task运行一遍。而事实上,在实际使用过程中,我们可能只是想要执行其中的一部分任务而已,并不想把整个playbook完整跑一遍。这个时候就需要用到tags。
通过tags,我们可以给playbook中的某一些任务打上“标签”,而在执行playbook的时候,我们可以通过选定标签的方式指定只执行哪一些任务或者不执行哪一些任务。
16.1、为task打tag
下面是一个安装httpd的简单示例:
Copy [root@controller ansible]# cat /etc/ansible/playbook/install_web.yml
- name: configure webservers
hosts: all
remote_user: ansible
tasks:
- name: Install httpd
yum:
name: httpd
state: present
tags: install_httpd
- name: Cofiguration httpd
copy:
src: /root/httpd.conf
dest: /etc/httpd/conf/httpd.conf
tags: conf_httpd
notify:
- restart httpd
- name: Start httpd
service:
name: httpd
state: started
enabled: no
tags: start_httpd
handlers:
- name: restart httpd
service: name=httpd state=restart
在上面的示例中,我们为两个task定义了三个tags:install_httpd、conf_httpd以及start_httpd。
16.2、使用tag
16.2.1、执行指定tag的task
有了tags之后,我们就可以只运行playbook中指定标签的task了:
Copy [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执行:
Copy [root@controller ansible]# ansible-playbook --tags="conf_httpd,start_httpd" install_web.yml
PLAY [configure webservers] *************************************************************************************************************************************************
TASK [Gathering Facts] ******************************************************************************************************************************************************
ok: [10.1.61.187]
TASK [Cofiguration httpd] ***************************************************************************************************************************************************
ok: [10.1.61.187]
TASK [Start httpd] **********************************************************************************************************************************************************
ok: [10.1.61.187]
PLAY RECAP ******************************************************************************************************************************************************************
10.1.61.187 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
16.2.2、排除指定tag的task
通过下面的方式可以排除指定了tag的task,即除了指定tag的task不执行,其他task都执行:
Copy [root@controller ansible]# ansible-playbook --skip-tags="install_httpd" install_web.yml
PLAY [configure webservers] *************************************************************************************************************************************************
TASK [Gathering Facts] ******************************************************************************************************************************************************
ok: [10.1.61.187]
TASK [Cofiguration httpd] ***************************************************************************************************************************************************
ok: [10.1.61.187]
TASK [Start httpd] **********************************************************************************************************************************************************
ok: [10.1.61.187]
PLAY RECAP ******************************************************************************************************************************************************************
10.1.61.187 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
执行效果跟上面一样。
16.2.3、查看playbook中的所有tag
可以通过--list-tags参数列出指定的playbook中所有的tag
Copy [root@controller ansible]# ansible-playbook --list-tags install_web.yml
playbook: install_web.yml
play #1 (all): configure webservers TAGS: []
TASK TAGS: [conf_httpd, install_httpd, start_httpd]
16.3、打tag的几种方式
16.3.1、为一个任务指定一个标签
这种方式就是上面示例中的方法:
16.3.2、为一个任务指定多个标签
可以通过列表的方式为一个任务指定多个标签:
Copy tags:
- install_httpd
- install_web
tags: ['install_httpd','install_web']
tags: install_httpd,install_web
16.3.3、为一个play指定一组标签
当为一个play指定一组标签后,该play下的所有task都会自动继承该标签,各task也可以自定义自己的标签。
Copy - name: configure webservers
hosts: all
remote_user: ansible
tags:
- httpd
tasks:
...
ansible内置tag
除了用户自定义tag,ansible也内置了几个tag,这些tag都包含特殊含义:
always:一旦某个task被打上了always的tag,则无论是playbook的完整执行,还是指定tag执行,不管你指定的tag是啥,该任务总是会被执行。除非明确指定"--skip-tags=always"选项,才不会执行该task。
never:该标签与always正好相反,总是不会执行,除非明确指定"--tags=never"选项。
Copy # 所有打了tag的任务都会被执行,包含never tag的除外,没有标签的不会被执行
ansible-playbook --tags tagged install_web.yaml
# 所有打了tag的任务都不会被执行,包括always tag也不会被执行
ansible-playbook --skip-tags tagged install_web.yaml
Copy # 所有未打tag的任务都会被执行,打了always tag的也会被执行
ansibl-playbook --tags untagged install_web.yaml
# 所有未打tag的任务都不会被执行
ansibl-playbook --skip-tags untagged install_web.yaml
all:表示所有任务都会被执行,在默认情况下,不指定任何标签,则使用的就是该标签
17. Ansible Playbook Include
说明 在前面的学习当中,我们一直使用一个playbook文件来组织所有的task任务。但是,当我们项目越来越大,task越来越多的时候,如果还将所有的task都写到一个playbook当中,可读性就会变差,这个时候我们就需要重新来组织playbook了。
我们可以将一个大的playbook拆成若干个小的playbook文件,在主配置文件中将这些零碎的小文件引入进来,而这种方式就叫做playbook的"include"。
17.1、include
playbook的include其实就是使用include关键字
17.1.1、tasks include
下面是两个playbook示例,分别用于安装lamp和lnmp环境:
Copy # 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中引入:
Copy # 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
也可以在include的时候,传入变量:
Copy [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 }}"
通过如下方式带入变量:
Copy tasks:
- { include: wordpress.yml, user: timmy, ssh_keys: [ 'keys/one.txt', 'keys/two.txt' ] }
再给一个例子:
Copy - 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的所有任务生效。
Copy # 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"
下面是一个简单的循环示例:
Copy [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示例如下:
Copy [root@controller ansible]# cat in.yml
- debug:
msg: "{{ item }} task1 in in.yml"
- debug:
msg: "{{ item }} task2 in in.yml"
再次执行playbook的结果如下:
Copy [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当中也有循环时怎么办?
Copy [root@controller ansible]# cat in.yml
- debug:
msg: "{{ item }} task1 in in.yml"
loop: ['a','b','c']
再次执行test_include,结果如下:
Copy [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=0s
这个时候,可以看到最终item的值来自in.yml中的循环。那如果我就想要使用test_include中的循环的值怎么办? 我们再次修改test_include.yml以及in.yml如下:
Copy [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']
再次查看结果:
Copy PLAY [servera] **********************************************************************************************************************************
TASK [include] **********************************************************************************************************************************
included: /etc/ansible/in.yml for servera
included: /etc/ansible/in.yml for servera
TASK [debug] ************************************************************************************************************************************
ok: [servera] => (item=a) => {
"msg": "1 a task1 in in.yml"
}
ok: [servera] => (item=b) => {
"msg": "1 b task1 in in.yml"
}
ok: [servera] => (item=c) => {
"msg": "1 c task1 in in.yml"
}
TASK [debug] ************************************************************************************************************************************
ok: [servera] => (item=a) => {
"msg": "2 a task1 in in.yml"
}
ok: [servera] => (item=b) => {
"msg": "2 b task1 in in.yml"
}
ok: [servera] => (item=c) => {
"msg": "2 c task1 in in.yml"
}
PLAY RECAP **************************************************************************************************************************************
servera : ok=4 changed=0 unreachable=0 failed=0
可以看到,outer_item中的值正是外层循环中item的值。当出现这个双层循环时,可以在外层循环中使用loop_var选项指定一个变量,这个变量用于替代外层循环中的item变量,以便在内层循环中获取到外层循环的item的值,从而避免两层循环中item变量名的冲突。
17.1.2、handlers include
handlers include与tasks include大体类似,直接给例子:
Copy # handlers1.yml内容如下:
# this might be in a file like handlers/handlers.yml
- name: restart apache
service:
name: apache
state: restarted
# handlers.yml包含handlers1.yml示例:
handlers:
- include: handlers/handlers.yml
17.1.3、playbook include
include也可以用于将一个playbook导入到另一个playbook中:
- name: this is a play at the top level of a file
hosts: all
remote_user: root
tasks:
- name: say hi
tags: foo
shell: echo "hi..."
- include: load_balancers.yml
- include: webservers.yml
- include: dbservers.yml
17.2、include_tasks
17.2.1、基本使用
在前面我们详细说了include的用法,然而事实上在后续的ansible版本当中,include语法可能会被弃用。而使用一些新的关键字来代替include的原始用法,include_tasks就是其中之一。
我们知道include可以用于包含tasks,handlers,playbooks等,而include_tasks则专门用于包含tasks:
Copy [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"
执行结果如下:
Copy
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还加入了新的参数,下面是一个简单用法示例:
Copy include_tasks:
file: in.yml
当然这种使用方法与include_tasks: in.yml的效果完全相同。
17.2.2、在include_tasks中使用tags
在前面我们提到过,如果为include添加tags,那么tags是对include中所有任务生效的。也就是说,如果调用include对应的tag,那么include文件中的所有任务都会执行。
但是对include_tasks添加tags,则只会对include_tasks本身生效,include_tasks中所有的任务都不生效。示例如下:
Copy [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"
执行结果如下:
Copy [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"
执行结果:
Copy [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标签,则即使我们调用其他任务的标签,该任务也会被执行。
Copy - hosts: test
gather_facts: no
tasks:
- debug:
msg: "test task1"
- include_tasks:
file: in.yml
apply:
tags: t1,always
tags: always
- debug:
msg: "test task3"
需要说明的是,在这里,tags: always标签只针对include_tasks本身生效,也就是说,如果其他任务的标签被调用,include_tasks本身会被调用,而其包含的任务不会被调用。如果要想其包含的任务也总是被调用,可修改配置如下:
17.3、import_tasks
import_tasks与include_tasks用法类似,都用于包含一个任务列表:
Copy
[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"
执行结果:
Copy [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还有如下不同:
import_tasks是静态的,被import的文件在playbook被加载时就预处理了,而include_tasks是动态的,被include的文件在playbook被运行时候才开始处理。一个简单的例子:
Copy - hosts: test
gather_facts: no
vars:
file_name: in.yml
tasks:
- include_tasks: {{ file_name }}
- import_tasks: {{ file_name }}
在上面的示例中,include_tasks
和import_tasks
均会被执行。
再看下面的例子:
Copy - hosts: test
gather_facts: no
tasks:
- set_fact:
file_name: in.yml
- include_tasks: {{ file_name }}
- import_tasks: {{ file_name }}
此时,import_tasks
就会出错:
Copy [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中,不支持其他的方式传入变量。
如果想要对包含的任务列表进行循环操作,则只能使用include_tasks,import_tasks不支持循环操作。也就是说,使用loop或者with_X对include文件进行循环操作时,只能配合include_tasks才能正常使用
当使用when对include文件添加条件判断时,include_tasks和import_tasks
有着本质的不同:
当对include_tasks使用when时,when对应的条件只会应用于include_tasks任务本身,当执行被包含的任务时,不会对这些被包含的任务重新进行条件判断
当对import_tasks使用when时,when对应的条件会被应用于被import的文件中的每一个任务,当执行被import的任务时,会对每一个被包含的任务进行同样的条件判断。
示例如下:
Copy [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
执行结果:
Copy [root@controller ansible]# ansible-playbook include_import_tasks_ex2.yml
PLAY [test] ******************************************************************************************************
TASK [set testvar to 0] ******************************************************************************************
ok: [10.1.61.187]
TASK [debug] *****************************************************************************************************
ok: [10.1.61.187] => {
"msg": "include_tasks: in1.yml"
}
TASK [include_tasks] *********************************************************************************************
included: /etc/ansible/in1.yml for 10.1.61.187
TASK [set_fact] **************************************************************************************************
ok: [10.1.61.187]
TASK [debug] *****************************************************************************************************
ok: [10.1.61.187] => {
"msg": "test task2"
}
TASK [set testvar to 0] ******************************************************************************************
ok: [10.1.61.187]
TASK [debug] *****************************************************************************************************
ok: [10.1.61.187] => {
"msg": "import_tasks: in1.yml"
}
TASK [set_fact] **************************************************************************************************
ok: [10.1.61.187]
TASK [debug] *****************************************************************************************************
skipping: [10.1.61.187]
PLAY RECAP *******************************************************************************************************
10.1.61.187 : ok=8 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
17.4、在handlers中使用include_tasks及import_tasks
我们知道,handlers中执行的其实也是任务,只不过是被触发才会运行,所以如果要在handlers中引入任务,也可直接使用include_tasks和import_tasks。没有include_handlers的说法。
17.5、import_playbook
我们在前面提到过,include除了可以引用任务列表,还可以引用整个playbook,在之后的版本中,如果想要引入playbook,则需要使用import_playbook模块。在2.8版本后,使用include引用整个playbook的特性会被弃用。
示例:
Copy
[root@controller ansible]# cat import_playbook_ex.yml
- hosts: test
gather_facts: no
tasks:
- debug:
msg: "test task"
- import_playbook: inplay.yml
[root@controller ansible]# cat inplay.yml
- hosts: test
gather_facts: no
tasks:
- debug:
msg: "test task in inplay.yml"
18. Ansible Playbook Roles
18.1、角色(roles) 在Ansible中,role是将playbook分割为多个文件的主要机制。它大大简化了复杂playbook的编写,同时还使得它们非常易于复用。
18.1.1、role的基本构成 roles文件组织结构示例:
Copy group_vas/
site.yml
webservers.yml
roles/
common/
files/
templates/
tasks/
handlers/
vars/
defaults/
meta/
webservers/
files/
templates/
tasks/
handlers/
vars/
defaults/
meta/
roles各目录的作用及可用的文件:
files:用于存放一些非模板文件的文件,如https证书等。
tempaltes:用于存放角色相关的Jinja2模板文件,当使用角色相关的模板时,如未明确指定模板路径,则默认使用此目录中的模板
tasks:角色所要执行的所有任务文件都存放于此,包含一个主文件main.yml,可以在主文件中通过include的方式引入其他任务文件
handlers:用于定义角色中需要调用 的handlers,包含一个主配置文件main.yml,可通过include引入其他的handlers文件。
vars:用于定义此角色用到的变量,包含一个主文件main.yml
meta:用于存储角色的元数据信息,这些元数据用于描述角色的相关属性,包括作者,角色的主要作用,角色的依赖关系等。默认这些信息会写入到当前目录下的main.yml文件中
defaults:除了vars目录,defaults目录也用于定义此角色用到的变量,与vars不同的是,defaults中定义的变量的优先级最低。
创建role的步骤如下:
在roles目录中分别创建角色名称命名的目录,如websrvs等
在每个角色命名的目录中分别创建files、handlers、meta、tasks、teamplates和vars目录,用不到的目录可以创建为空目录,也可以不创建。
需要说明的是,以上目录并不都是必须的,如果你的roles当中并不需要用到某一个目录,也可以不用创建,比如我们将所有的变量都放到defaults中,则可以不需要vars目录,如果未用到模板文件,则不需要templates目录。
18.1.2、在playbook中使用roles
基本引用的方法:
Copy - hosts: webservers
roles:
- common
- webserver
也可以通过如下方法引用时带入变量:
Copy
---
- 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
还可以在引用时使用条件语句:
Copy
- hosts: webservers
roles:
- role: some_role
when: "ansible_os_family == 'RedHat'"
通过include引入role
Copy - hosts: webservers
tasks:
- name: Include the some_role role
include_role:
name: some_role
when: "ansible_facts['os_family'] == 'RedHat'"
18.1.3、pre_tasks和post_tasks
如果在执行一个role时,需要在其前或其后依然要执行某些任务,我们可以使用pre_tasks及post_tasks来声明。pre_tasks是在role之前执行,而post_tasks则在role之后执行:
Copy - name: deply webservers
host: webservers
vars_files:
- secrets.yml
pre_tasks:
- name: update yum cache
yum: update_cache=yes
roles:
- role: apache
database_host: {{ hostvars.db.ansible_eth0.ipv4.address }}
domains:
- exampel.com
- www.example.com
post_tasks:
- name: print something
shell: echo "The roles have been updated!"
18.1.4、role的依赖
如果当前role在执行前需要依赖另一个role,我们可以在roles的meta目录中的main.yml中定义role的依赖关系。
示例1:
Copy # roles/myapp/meta/main.yml
---
dependencies:
- role: common
vars:
some_parameter: 3
- role: apache
vars:
apache_port: 80
- role: postgres
vars:
dbname: blarg
other_parameter: 12
18.2、Ansible Galaxy
ansible-galaxy是一个工具,我们可以利用它快速的创建一个标准的roles目录结构,还可以通过它在Ansible Galaxy上下载别人写好的roles,直接拿来用。
初始化一个roles的目录结构:
Copy [root@controller ansible]# ansible-galaxy init my_new_role
安装别人写好的roles:
Copy [root@controller ansible]# ansible-galaxy install -p /etc/ansible/roles bennojoy.mysql
列出已安装的roles:
Copy [root@controller ansible]# ansible-galaxy list
查看已安装的roles信息:
Copy [root@controller ansible]# ansible-galaxy info bennojoy.mysql
卸载roles:
Copy [root@controller ansible]# ansible-galaxy remove bennojoy.mysql
补充:通过ansible-galaxy初始化一个collection:
Copy [root@controller ansible]# ansible-galaxy collection init ns.collection
collection集合案例:
Copy #安装clooection
[root@controller ansible]# ansible-galaxy collection install community.mysql
[root@controller ansible]# cat mysql.yaml
- hosts: localhost
tasks:
- name: install mysql
yum:
name:
- mariadb-server
- mariadb
- MySQL-python
state: present
- name: start myriqdb
service:
name: mariadb
state: started
enabled: yes
- name: Removes anonymous user account for localhost
community.mysql.mysql_user:
name: ''
host: localhost
state: absent
- name: Create database user with name 'bob' and password '12345' with all database privileges
community.mysql.mysql_user:
name: bob
password: 12345
priv: '*.*:ALL'
state: present
19、Ansible Vault配置加密
简介 在使用ansible的过程中,不可避免的会存储一些敏感信息,比如在变量文件中存储帐号密码信息等。
ansible通过ansible-vault命令行工具来提供对敏感文件的加密和解密。
ansible-vault可以创建、加密、解密和查看文件。其可以加密任何ansible使用的文件,包括inventory文件,playbook中调用的变量文件等。
19.1、Ansible-vault常用操作
1、创建加密文件
Copy [root@controller ansible]# ansible-vault create file
2、编辑加密文件
Copy [root@controller ansible]# ansible-vault edit file
3、重置密码
Copy [root@controller ansible]# ansible-vault rekey file
4、加密已有文件
Copy [root@controller ansible]# ansible-vault encrypt file
5、解密文件
Copy [root@controller ansible]# ansible-vault decrypt file
6、查看文件
Copy [root@controller ansible]# ansible-vault view file
19.2、Ansible-vault配置示例
1、创建一个user.yml的变量文件,内容如下:
Copy username: "user1"
pwhash: "$1$GkTPu7we$ZZtdsLPIHkS.fmoVcn3v51"
2、加密上面创建的变量文件:
Copy [root@controller ansible]# ansible-vault encrypt user.yml
New Vault password:
Confirm New Vault password:
Encryption successful
3、编写playbook文件如下:
Copy - 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
Copy [root@controller ansible]# ansible-playbook create_user.yml --ask-vault-pass
Vault password:
也可以通过如下操作执行playbook:
Copy [root@controller ansible]# echo redhat > vault-pass
[root@controller ansible]# chmod 600 vault-pass
[root@controller ansible]# ansible-playbook create_user.yml --vault-password-file=vault-pass
20、动态Inventory管理
20.1、动态主机管理模块
20.1.1、add_host
在playbook执行的过程中,动态的添加主机到指定的主机组中
常用参数:
Copy - name: add a host to group webservers
hosts: webservers
tasks:
- add_host:
name: "{{ item }}"
group: webservers
foo=42 #添加主机到webservers组中,主机的变量foo的值为42
loop:
- host1
- host2
- debug:
msg: "{{ groups.webservers }}"
20.1.2、group_by
在playbook执行的过程中,动态的创建主机组
示例:
Copy - name: Create operating system group
hosts: all
tasks:
#在playbook中设置一个新的主机组
- group_by:
key: "os_{{ ansible_distribution }}"
- name: Run on CentOS hosts only
hosts: os_CentOS
tasks:
- name: Install Apache
yum:
name: httpd
state: latest
- name: Run on Ubuntu hosts only
hosts: os_Ubuntu
tasks:
- name: Install Apache
apt:
name: apache2
state: latest
20.2、动态inventory管理
20.2.1动态inventory简介
在前面我们所有的选取主机组的操作都是通过维护inventory文件来完成的。而事实上,当在大规模应用当中,如果主机达成千上万台,这个时候还手动维护inventory文件将会给运维工作带来巨大的挑战。
在这种大规模的应用场景中,通常的做法是,将所有的主机都存储在cmdb当中。当需要对某一组主机或者某一类型的主机执行相应操作时,通过cmdb将相应主机取出来,动态的生成inventory,然后交由ansible处理即可。
所以其实Ansible Inventory包含静态inventory和动态inventory两部分。而我们前面通过手动在inventory文件中维护主机列表的方式即称之为静态inventory。而动态inventory则是指通过外部脚本获取主机列表,并按照ansible所要求的格式返回给ansible指令的操作方式。
动态inventory一般都会结合cmdb或者云计算平台等获取主机信息,由于主机资源一般会动态的进行增减,而这些系统一般会智能更新。我们需要通过这些工具提供的api或者接入库查询等方式返回主机列表。
20.2.2、动态inventory脚本规约
动态inventory脚本最终返回的满足ansible输出格式的json数据。ansible对于使用什么语言来实现动态inventory没有要求。但脚本必须支持两个参数:
--list:用于返回所有的主机组信息,每个组所包含的主机列表hosts、子组列表children、主机变量列表vars都应该是字典形式的,而_meta则用于存放主机变量
--host :返回指定主机的变量列表,也可返回一个空字典
20.2.3、动态inventory脚本示例
[root@controller ansible]# cat dynamic_inventory.py
Copy #!/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会提示没有权限调用:
Copy [root@controller ansible]# chmod +x ./dynamic_inventory.py
执行该脚本,返回如下:
Copy [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操作示例如下:
Copy [root@controller ansible]# ansible -i dynamic_inventory.py webserver --list-hosts
hosts (1):
10.10.0.112
[root@controller ansible]# ansible -i dynamic_inventory.py all --list-hosts
hosts (2):
10.10.0.112
10.10.0.109
21. Ansible性能调优
简介
在当前配置管理工具大行其道的应用中,ansible凭借其轻量级、agentless等特性得以占据一席之地。然而其也并不是完美无缺,事实上,其最为人所诟病的就是在大规模服务器应用场景当中表现出的性能问题。所以本篇文档就来说一说如何通过一些配置优化来加速ansible的运行。
21.1、开启ansible性能监测
ansible附带了一组回调插件,可以通过callback_whitelist 指令在ansible.cfg文件中启用这些插件
Copy [default]
callback_whitelist = timer, profile_tasks, profile_roles, cgroup_perf_recap
[callback_cgroup_perf_recap]
control_group=ansible_profile
cgroup_perf_recap可查看一个playbook消耗的cpu和内存
timer 可用于查看playbook执行所消耗的时间
Copy [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
也可通过如下指令直接执行:
Copy [root@controller ansible]# cgexec -g cpuacct,memory,pids:ansible_profile ansible-playbook deploy_webservers.yml
21.2、任务优化
Copy - 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"
高效复制文件到被控端
Copy - name: copy files to target
copy:
src: test_files
dest: /mnt
- name: copy files to target
synchronize:
src: test_files
dest: /mnt
21.3、优化ssh连接
默认情况下,ansible基于ssh连接被控端,当ansible在运行playbook的时候,会建立很多的ssh连接来执行相应的task,而每个task都会创建一个新的ssh连接,ssh连接的建立毫无疑问需要额外tcp建立连接的开销。
openssh支持一个优化,叫做ssh multiplexing,也被称作ControlPersist。当启用了该特性,则多个连接到相同主机的ssh会话将会共享相同的tcp连接。这样就只有第一次连接的时候需要进行TCP三次握手。
当启用Multiplexing后:
第一次ssh连接到主机的时候,openssh会创建一个主连接
紧接着openssh会创建一个控制套接字,通过主连接与远程主机关联
当有新的ssh尝试连接到主机时,openssh将使用控制套接字与远程主机通信,并不会创建新的tcp连接 开启该配置项:
Copy [root@controller ansible]# cat ansible.cfg
[ssh_connection]
ssh_args = -C -o ControlMaster=auto -o ControlPersist=60s
配置项说明:
ControlMaster:启用ssh multiplexing,允许多个同时与远程主机连接的ssh会话使用单一网络连接。auto用于告诉ssh在主连接和控制套接字不存在的情况下,自动创建它们。第一个ssh会话建立,则与同一主机连接的其他会话重复利用此连接,从而绕过较慢的初始过程,ssh在最后一个会话关闭后,立即销毁共享的连接
ControlPersist:使连接在后台保持打开,而不是在上一次会话后销毁连接。其配置空闲的连接保持打开的时间长度,每个新会话将重置此空闲计时器。
21.4、pipelining
在说明pipelining之前,需要先了解下ansible是如何执行一个task的:
基于playbook中的task生成一个python脚本
在被控主机上运行这个脚本 上面三个步骤中,后面两个步骤会产生两个ssh会话。而在pipelinin模式下,ansible执行python脚本时并不会复制它,而是通过管道传递给ssh会话。这样一来,就会让原本需要的两个ssh会话减少为一个,以降低开销,节省时间。
启用pipelining:
Copy [root@controller ansible]# cat ansible.cfg
[defaults]
pipelining = True
需要注意的是,如果开启pipelining,则需要在被控端的/etc/sudoers文件中关闭requiretty:
Copy [root@controller ansible]# cat /etc/sudoers.d/ansible
ansible ALL=(ALL) NOPASSWD:ALL
Defaults:ansible !requiretty
21.5、并发执行
默认情况下,ansible在批量连接客户端执行play时的并发数是5个,可以通过调整该值来提高并发数:
Copy [defaults]
forks = 20
即使提高了并发个数,playbook也可能在执行一个需要较长时间的任务时导致阻塞,针对这类任务,可以使用异步的方式来运行:
Copy - hosts: all
tasks:
- name: Install mlocate
yum:
name: mlocate
state: installed
- name: Run updatedb
command: /usr/bin/updatedb
async: 300
poll: 10
当使用上面的任务控制超过forks设置的节点个数时,'install mlocate'任务会先在forks个节点上跑,完成后再继续下一批,这个并发数是由我们设置的forks选项控制的。而'Run updatedb'这个任务会一次性在所有节点上都执行,执行的超时时间为300s,然后每10s轮循一次检查它是否完成。需要说明的是,因为需要等待其执行完成,所以如果这个任务比较耗时,仍然需要等待其执行完毕后才能开始下一个任务。
但是如果该任务只是一个后台任务,比如只是在后台执行一个脚本或者启动一个服务,不需要等待其返回结果。那就不需要检查这个任务是否完成,只需要继续执行其它任务,最后再使用wait_for这个模块去检查之前的脚本或者服务是否按预期中运行了便可。这时候只需要把poll的值设置为0,便可以按上面的要求配置ansible不等待job的完成。
还有一种需求,假如一个task,它就是需要运行很长的时间,不能让它超时退出,需要一直等待这个task完成。这个时候就需要将async的值设置为0。
总结来说,大概有以下的一些场景需要使用到ansible的polling特性:
某个task需要运行很长的时间,这个task很可能会达到timeout。
当然也有一些场景是不适合使用:
22. Ansible调试
22.1、运行前检查
1、当我们在运行ansible-playbook时,使用--check选项时,将不会对受控主机作出任何更改,而是通过模拟运行的方式执行所有task,以用于检查playbook在运行时的状态:
Copy [root@controller ansible]# ansible-playbook foo.yml --check
2、在运行ansible-playbook时,如果使用--diff选项配合--check选项,可以用于检查本次执行play时,相较上一次产生了哪些改变:
Copy [root@controller ansible]# ansible-playbook foo.yml --check --diff --limit foo.example.com
3、有些时候,我们在检测模式下运行play时,我们会希望某个play总是运行,我们可以使用always_run子句:
Copy tasks:
- name: this task is run even in check mode
command: /something/to/run --even-in-check-mode
always_run: yes
需要说明的是,如果一个task中同时包含when和always_run,如果when返回了false,即使alwys_run为true,任务依然会被跳过。 22.2、打印详细输出信息 参考《6. Ansible Playbook基本使用》
22.3、使用debug模块
在前面debug模块使用的比较多了,这里直接再给个示例:
Copy tasks:
- debug:
var: myvariable
- debug:
msg: "The value of myvariable is {{ var }}"
22.4、使用assert模块
assert模块会在指定的条件不符合的时候返回错误并失败退出。
Copy # 当目标机没有eth1网卡时则playbook会返回失败
- name: assert that eth1 interface exists
assert:
that: ansible_eth1 is defined
当调试playbook的时候,插入assert模块在我们设定的某些条件不成立时立刻失败,对调试很有用。
下面示例用于检查目标文件是否是一个目录,如果不是,则失败退出:
Copy - name: stat /opt/foo
stat: path=/opt/foo
register: st
- name: assert that /opt/foo is a directory
assert:
that: st.stat.isdir
22.5、限制特定的task运行
22.5.1、指定任务执行
可以通过--start-at-task参数告诉Ansible从指定的task开始运行playbook,而不是从头开始运行。如果你的playbook因为某一个task中有bug而失败了,在你修复了这个bug后希望从被修复的这个task开始再次执行playbook的时候,就可以使用这个参数。
Copy # 以下命令会从名为"install packages"的任务开始执行playbook
[root@controller ansible]# ansible-playbook playbook.yml --start-at-task="install packages"
22.5.2、分步执行
可以通过--step选项来交互式的执行playbook:
Copy [root@controller ansible]# ansible-playbook playbook.yml --step
这样ansible在每个任务前会自动停止,并询问是否应该执行该任务。
假如有一个名为"configure ssh"的任务,playbook执行到这里会停止并询问:
Copy Perform task: configure ssh (y/n/c):
22.5.3、tags
参考《17. Ansible Playbook之tags》
23. Ansible lineinfile模块
简介 之所以专门说一说这个模块,是因为lineinfile在实际使用中非常有用。
实际上,在大多数时候,我们在linux上的操作,就是针对文件的操作,通过配置管理工具对配置文件作统一的配置修改是一个非常酷的功能。
下面是官方针对该模块的说明:
Copy lineinfile - Ensure a particular line is in a file, or replace an existing line using a back-referenced regular expression
简单讲,这个模块就是针对一个文件中行内容的操作。
下面我们详细说一说其具体可以做的事情。
23.1、修改匹配行
下面是一个简单的task示例:
Copy # 将/etc/selinux/config中匹配到以'SELINUX='开头的行,将其替换为'SELINUX=disabled'
- name: modify selinux to disabled
lineinfile:
path: /etc/selinux/config
regex: '^SELINUX='
line: 'SELINUX=disabled'
23.2、在匹配行前或后添加内容
示例文件如下:
Copy [root@controller ansible]# cat /etc/http.conf
Listen 127.0.0.1:80
Listen 80
Port
23.3、在匹配行前添加
在http.conf文件的Listen 80前面添加一行Listen 8080,task示例如下:
Copy - name: add line before Listen 80
lineinfile:
dest: /etc/http.conf
insertbefore: '^Listen 80'
line: 'Listen 8080'
23.4、在匹配行后添加
在http.conf文件的Port后面添加一行just a test,task示例如下:
Copy - name: add line before Listen 80
lineinfile:
dest: /etc/http.conf
insertafter: '^Port'
line: 'just a test'
23.5、修改文件内容及权限
示例文件:
Copy [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,如下:
Copy - name: modify hosts
lineinfile:
dest: /etc/hosts
regex: '^127\.0\.0\.1'
line: '127.0.0.1 localhost'
owner: root
group: root
mode: 0644
```yaml
## 23.6、删除一行内容
示例原文件:
```yaml
[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开头的行:
Copy - name: delete a line
lineinfile:
dest: /etc/hosts
regex: '^10\.1\.61'
state: absent
文件存在则添加一行内容
往/etc/hosts里添加一行10.1.61.131 test.dz11.com(多次执行,不会重复添加),示例如下:
Copy - name: add a line
lineinfile:
path: /etc/hosts
line: '10.1.61.131 test.dz11.com'
23.7、如果有匹配的行则修改该行,如果不匹配则添加
示例原文件/tmp/test.txt内容如下:
Copy # %wheel ALL=(ALL) ALL
下面的示例task中,匹配以%wheel开头的行,匹配到,则执行替换,未匹配,则添加。因为原文件中,没有以%wheel开头的行,所以会添加一行:
Copy - name: add or modify a line
lineinfile:
path: /tmp/test.txt
regex: '^%wheel'
line: '%wheel ALL=(ALL) NOPASSWD: ALL'
修改后的文件如下:
Copy [root@controller ansible]# cat /tmp/text.txt
# %wheel ALL=(ALL) ALL
%wheel ALL=(ALL) NOPASSWD: ALL
23.8、参数backrefs,backup说明
backrefs:
当backrefs为no时,如果regex没有匹配到行,则添加一行,如果Regx匹配到行,则修改该行
当backrefs为yes时,如果regex没有匹配到行,则保持原文件不变,如果regex匹配到行,则修改该行
backrefs默认为no,所以上面那个示例中,我们没有配置backrefs,而默认没有匹配,则修改。 下面我们看一看backrefs为yes时匹配到行的示例:
示例原文件:
Copy [root@controller ansible]# cat /tmp/testfile
# %wheel ALL=(ALL) ALL
%wheel ALL=(ALL) NOPASSWD: ALL
#?bar
task示例:
Copy - name: test backrefs
lineinfile:
backup: yes
state: present
path: /tmp/testfile
regexp: '^#\?bar'
backrefs: yes
line: 'bar'
修改后的文件:
Copy [root@controller ansible]# cat /tmp/testfile
# %wheel ALL=(ALL) ALL
%wheel ALL=(ALL) NOPASSWD: ALL
bar
23.9、使用validate验证文件是否正确修改
在一些场景下,我们修改完文件后,需要对文件做一下测试,用以检查文件修改之后,是否能正常运行。如http.conf、nginx.conf等,一旦改错,而不加以测试,可能会直接导致http服务挂掉。
可以使用validate关键字,在修改完成以后,对文件执行检测:
Copy - name: test validate
lineinfile:
dest: /etc/sudoers
state: present
regexp: '^%ADMIN ALL='
line: '%ADMIN ALL=(ALL)'
validate: 'visudo -cf %s'
tags:
- testsudo