playbook可以理解为ansible的剧本,按剧本中的设定,指定的主机完成一系列规定的操作。
ansible的ad-hoc模式,适用于单个ansible模块操作;当远程主机需要进行大量复杂的操作时,这时候就需要将大量ansible模块操作进行集合成playbook。
这种关系就像,shell命令和shell脚本。
playbook适用yaml语法编写,其文件后缀名为.yaml。其基本语法如下
文件的第一行应该以 "---" (三个连字符)开始,表明YMAL文件的开始。
# 后的内容表示注释。
- 用于表示数据之间的关系是属于同一列表,-后需要空一格。
:用于表示数据之间的关系是键值对,:后需要空一格。
通过缩进不同(通常是两个空格)表示不同层结构,同一个列表中的元素应该保持相同的缩进。否则会被当做错误处理。
举例如下
- hosts: web
remote_user: root
gather_facts: no #整个playbook当中都没有使用到fact变量,可以关闭fact以提升执行效率
tasks:
- name: install nginx
yum: name=nginx state=present
- name: copy nginx.conf
copy: src=/tmp/nginx.conf dest=/etc/nginx/nginx.conf backup=yes
notify: reload #当此步骤执行后通知给名为reload的handlers
tags: reloadnginx #给此步骤打标签
- name: start nginx service
service: name=nginx state=started
tags: startnginx #给此步骤打标签
handlers: #注意,前面没有-,与hosts、tasks等相同缩进
- name: reload
service: name=nginx state=restarted
使用ansible-playbook命令运行playbook文件,得到输出内容为JSON格式。并且由不同颜色组成,便于识别。一般而言
执行有三个步骤:1、收集facts 2、执行tasks 3、报告结果
在playbook中定义为变量引用,其格式为{{变量名}},可引用的来源有如下几种
当使用ansible-playbook时,第一步会将远程主机的信息收集存放在facts集合中,其中的变量可以直接在playbook中引用{{}}。
当运行ansible-playbook命令时,可以使用—extra-vars或-e为playbook中的变量传递值,级别最高,例如
ansible-playbook test.yml -e "hosts=www user=mageedu"
使用register元素把任务的输出定义为某个变量的值,供后面任务引用,例如
tasks:
- shell: /usr/bin/foo
register: foo_result
当给一个主机应用角色的时候可以传递变量,然后在角色内使用这些变量,示例如下:
- hosts: webservers
roles:
- common
- { role: foo_app, dir: '/web/htdocs/a.com', port: 8080 }
在inventor文件中定义变量
#test组中包含两台主机,通过对test组指定vars,相当于为host1和host2相指定了ntp_server和proxy变量参数值
[test]
host1
host2
[test:vars]
ntp_server=192.168.1.10
proxy=192.168.1.20
# 下面是一个示例,指定了一个武汉组有web1、web2;随州组有web3、web4主机;又指定了一个湖北组,同时包含武汉和随州;同时为该组内的所有主机指定了2个vars变量。
[wuhan]
web1
web2
[suizhou]
web4
web3
[hubei:children]
wuhan
suizhou
[hubei:vars]
ntp_server=192.168.1.10
zabbix_server=192.168.1.10
在playbook文件中使用vars为整个playbook定义变量
- hosts: webservers
vars:
- role: foo_app
port: 8080
使用ansible的魔法变量
具体参见https://www.cnblogs.com/breezey/p/9275763.html
template模块实现模板文件的分发,其用法与copy模块基本相同,唯一的区别是,copy模块会将原文件原封不动的复制到远程主机,而template会将原文件复制到远程主机,并且使用变量的值将文件中的变量替换以生成完整的配置文件。
template使用了Jinjia2语言编写原文件模版,templates文件必须存放于playbook剧本同级templates目录下,且命名为 .j2 结尾 。
Jinjia2语言拥有如下形式:
模板文件之if判断
部分Jinjia2模板内容
......
{% if ansible_eth0.ipv4.address %}
bind {{ ansible_eth0.ipv4.address }} 127.0.0.1
#如果网卡eth0存在ip地址则以上面的bind为模板
{% elif ansible_bond0.ipv4.address %}
bind {{ ansible_bond0.ipv4.address }} 127.0.0.1
#如果网卡eth0不存在地址网卡bond0存在ip地址则以上面的bind为模板
{% else%}
bind 0.0.0.0
#如果都不存在则以上面的bind为模板
{% endif %}
.....
模板文件之for循环
inventory文件内容
....
[webserver]
192.168.80.111
192.168.80.121
....
部分Jinjia2模板内容
....
upstream web {
{% for host in groups['webserver'] %}
server {{ hostvars[host]['ansible_eth0']['ipv4']['address'] }};
{% endfor %}
}
....
playbook中的某个task的执行依赖某个条件,这个条件可以是变量或前一个任务的执行结果等;这时候就需要进行条件判断,playbook中的关于task执行前的条件判断使用关键字when。
举例
- 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" fail=yes
when: not ansible_os_family =="RedHat" or ansible_os_family =="Debian"
tasks中有三个任务task,当远程主机是RedHat系统则执行第一个,当远程主机是Debian则执行第二个,当都不是时候执行第三个。
条件表达式中可以使用逻辑运算and,or,not,()
条件表达式为判断变量
- hosts: test
gather_facts: no
vars:
testvar: "test"
testvar1:
tasks:
- debug:
msg: "testvar is defined"
when: testvar is defined
- debug:
msg: "testvar2 is undefined"
when: testvar2 is undefined
- debug:
msg: "testvar1 is none"
when: testvar1 is none
条件表达式为判断前面任务执行结果
- hosts: test
gather_facts: no
vars:
doshell: true
tasks:
- shell: 'cat /testdir/aaa'
when: doshell
register: result
ignore_errors: true
- debug:
msg: "success"
when: result is success
- debug:
msg: "failed"
when: result is failure
- debug:
msg: "changed"
when: result is change
- debug:
msg: "skip"
when: result is skip
条件表达式为判断路径
- hosts: test
gather_facts: no
vars:
testpath1: "/testdir/test"
testpath2: "/testdir"
tasks:
- debug:
msg: "file"
when: testpath1 is file
- debug:
msg: "directory"
when: testpath2 is directory
注意
:关于路径的所有判断均是判断主控端上的路径,而非被控端上的路径
条件表达式为判断字符串
- hosts: test
vars:
supported_distros:
- RedHat
- CentOS
tasks:
- debug:
msg: "{{ ansible_distribution }} in supported_distros"
when: ansible_distribution in supported_distros
条件判断之block
当我们要使用同一个条件判断执行多个任务的时候,可以对多个任务进行绑定为同一block整天进行判断。
举例
- hosts: test
tasks:
- debug:
msg: "task1 not in block"
- block:
- debug:
msg: "task2 in block1"
- debug:
msg: "task3 in block1"
when: 2 > 1
rescue
- hosts: test
tasks:
- block:
- shell: 'ls /testdir'
rescue:
- debug:
msg: '/testdir is not exists'
在上面的例子中,当block中的任务执行失败时,则运行rescue中的任务。如果block中的任务正常执行,则rescue的任务就不会被执行。如果block中有多个任务,则任何一个任务执行失败,都会执行rescue。block中可以定义多个任务,同样rescue当中也可以定义多个任务
always
当block执行失败时,rescue中的任务才会被执行;而无论block执行成功还是失败,always中的任务都会被执行
- hosts: test
tasks:
- block:
- shell: 'ls /testdir'
rescue:
- debug:
msg: '/testdir is not exists'
always:
- debug:
msg: 'This task always executes'
条件判断之错误处理
fail模块
在shell中,可能会有这样的需求:当脚本执行至某个阶段时,需要对某个条件进行判断,如果条件成立,则立即终止脚本的运行。在shell中,可以直接调用"exit"即可执行退出。事实上,在playbook中也有类似的模块可以做这件事。即fail模块。
fail模块用于终止当前playbook的执行,通常与条件语句组合使用,当满足条件时,终止当前play的运行。
选项只有一个:
- 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"
failed_when
事实上,当fail和when组合使用的时候,还有一个更简单的写法,即failed_when,当满足某个条件时,ansible主动触发失败
#如果在command_result存在错误输出,且错误输出中,包含了`FAILED`字串,即返回失败状态:
- name: this command prints FAILED when it fails
command: /usr/bin/example-command -x -y -z
register: command_result
failed_when: "'FAILED' in command_result.stderr"
也可以直接通过fail模块和when条件语句,写成如下
- name: this command prints FAILED when it fails
command: /usr/bin/example-command -x -y -z
register: command_result
ignore_errors: True
- name: fail the play if the previous command did not succeed
fail: msg="the command failed"
when: " command_result.stderr and 'FAILED' in command_result.stderr"
ansible一旦执行返回失败,后续操作就会中止,所以failed_when通常可以用于满足某种条件时主动中止playbook运行的一种方式。
ansible默认处理错误的机制是遇到错误就停止执行。但有些时候,有些错误是计划之中的。我们希望忽略这些错误,以让playbook继续往下执行。这个时候就可以使用ignore_errors忽略错误,从而让playbook继续往下执行
loop关键字
启动httpd和postfilx服务
tasks:
- name: postfix and httpd are running
service:
name: "{{ item }}"
state: started
loop:
- postfix
- httpd
也可以将loop循环的列表提前赋值给一个变量,然后在循环语句中调用
#cat test_services.yml
test_services:
- postfix
- httpd
# cat install_pkgs.yml
- name: start services
hosts: test
vars_files:
- test_services.yml
tasks:
- name: postfix and httpd are running
service:
name: "{{ item }}"
state: started
loop: "{{ test_services }}"
下面是一个循环更复杂类型数据的示例
# cat test_loop.yml
- name: test loop
hosts: test
tasks:
- name: add www group
group:
name: www
- name: add several users
user:
name: "{{ item.name }}"
state: present
groups: "{{ item.groups }}"
loop:
- { name: 'testuser1', groups: 'wheel' }
- { name: 'testuser2', groups: 'www' }
在循环语句中注册变量
- 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
with_items 单词循环
- hosts: test
vars:
data:
- user0
- user1
- user2
tasks:
- name: "with_items"
debug:
msg: "{{ item }}"
with_items: "{{ data }}"
with_nested 嵌套循环
tasks:
- name: debug loops
debug: msg="name is {{ item[0] }} vaule is {{ item[1] }} num is {{ item[2] }}"
with_nested:
- ['alice','bob']
- ['a','b','c']
- ['1','2','3']
#item[0]是循环的第一个列表的值['alice','bob']。item[1]是第二个列表的值;item[2]则是第三个列表的值
#部分执行结果
TASK [debug loops] ***********************************************************************************************
ok: [10.1.61.187] => (item=['alice', 'a', '1']) => {
"msg": "name is alice vaule is a num is 1"
}
ok: [10.1.61.187] => (item=['alice', 'a', '2']) => {
"msg": "name is alice vaule is a num is 2"
}
ok: [10.1.61.187] => (item=['alice', 'a', '3']) => {
"msg": "name is alice vaule is a num is 3"
}
ok: [10.1.61.187] => (item=['alice', 'b', '1']) => {
"msg": "name is alice vaule is b num is 1"
}
with_dict 循环字典
# 假如有如下变量内容:
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 }}"
with_fileglob 循环指定目录中的文件
- hosts: test
tasks:
- name: Make key directory
file:
path: /root/.sshkeys
state: directory
mode: 0700
owner: root
group: root
- name: Upload public keys
copy:
src: "{{ item }}"
dest: /root/.sshkeys
mode: 0600
owner: root
group: root
with_fileglob:
- /root/.ssh/*.pub
- name: Assemble keys into authorized_keys file
assemble:
src: /root/.sshkeys
dest: /root/.ssh/authorized_keys
mode: 0600
owner: root
group: root
with_lines 循环一个文件中的所有行
with_lines循环结构会让你在控制主机上执行任意命令,并对命令的输出进行逐行迭代。假设我们有一个 文件test.txt包含如下行
Breeze Yan
Bernie Yang
jerry Qing
我们可以通过如下方法进行逐行输出
- name: print all names
debug: msg="{{ item }}"
with_lines:
- cat test.txt
do-until循环
- action: shell /usr/bin/foo
register: result
until: result.stdout.find("all systems go") != -1
retries: 5 #重试次数
delay: 10 #每次循环之间的间隔,默认为5秒
任务委托
在有些时候,我们希望运行一个与选定的主机有关联的task,但是这个task又不需要在选定的主机上执行,而需要在另一台服务器上执行
这就是任务委托。
可以使用delegate_to语句来在另一台主机上运行task
- name: enable alerts for web servers
hosts: webservers
tasks:
- name: enable alerts
nagios: action=enable_alerts service=web host="{{ inventory_hostname }}"
delegate_to: nagios.example.com
注意
以上面为例,如果hosts有多个远程主机,那么每个远程主机都会委托一次,导致enable alerts操作执行多次。如果只需要一个主机执行就可以的话,可配合run_once使用
任务暂停
有些情况下,一些任务的运行需要等待一些状态的恢复,比如某一台主机或者应用刚刚重启,我们需要需要等待它上面的某个端口开启,此时就需要将正在运行的任务暂停,直到其状态满足要求。
Ansible提供了wait_for模块以实现任务暂停的需求
wait_for模块常用参数:
#等待8080端口已正常监听,才开始下一个任务,直到超时
- wait_for:
port: 8080
state: started
#等待8000端口正常监听,每隔10s检查一次,直至等待超时
- wait_for:
port: 8000
delay: 10
#等待8000端口直至有连接建立
- wait_for:
host: 0.0.0.0
port: 8000
delay: 10
state: drained
#等待8000端口有连接建立,如果连接来自10.2.1.2或者10.2.1.3,则忽略。
- wait_for:
host: 0.0.0.0
port: 8000
state: drained
exclude_hosts: 10.2.1.2,10.2.1.3
#等待/tmp/foo文件已创建
- wait_for:
path: /tmp/foo
#等待/tmp/foo文件已创建,而且该文件中需要包含completed字符串
- wait_for:
path: /tmp/foo
search_regex: completed
#等待/var/lock/file.lock被删除
- wait_for:
path: /var/lock/file.lock
state: absent
#等待指定的进程被销毁
- wait_for:
path: /proc/3466/status
state: absent
#等待openssh启动,10s检查一次
- wait_for:
port: 22
host: "{{ ansible_ssh_host | default(inventory_hostname) }}" search_regex: OpenSSH
delay: 10
注意
以上面为例,如果hosts有多个远程主机,那么每个远程主机都会wait_for一次。如果只需要一个主机wait_for就足够,可配合run_once使用。
滚动执行
默认情况下,ansible会并行的在所有选定的主机或主机组上执行每一个task,但有的时候,我们会希望能够逐台运行。最典型的例子就是对负载均衡器后面的应用服务器进行更新时。通常来讲,我们会将应用服务器逐台从负载均衡器上摘除,更新,然后再添加回去。我们可以在play中使用serial语句来告诉ansible限制并行执行play的主机数量。
下面是一个在amazon EC2的负载均衡器中移除主机,更新软件包,再添加回负载均衡的配置示例:
- name: upgrade pkgs on servers behind load balancer
hosts: myhosts
serial: 1
tasks:
- name: get the ec2 instance id and elastic load balancer id
ec2_facts:
- name: take the host out of the elastic load balancer id
local_action: ec2_elb
args:
instance_id: "{{ ansible_ec2_instance_id }}"
state: absent
- name: upgrade pkgs
apt:
update_cache: yes
upgrade: yes
- name: put the host back n the elastic load balancer
local_action: ec2_elb
args:
instance_id: "{{ ansible_ec2_instance_id }}"
state: present
ec2_elbs: "{{ items }}"
with_items: ec2_elbs
在上述示例中,serial的值为1,即表示在某一个时间段内,play只在一台主机上执行。如果为2,则同时有2台主机运行play。
一般来讲,当task失败时,ansible会停止执行失败的那台主机上的任务,但是继续对其他 主机执行。在负载均衡的场景中,我们会更希望ansible在所有主机执行失败之前就让play停止,否则很可能会面临所有主机都从负载均衡器上摘除并且都执行失败导致服务不可用的场景。这个时候,我们可以使用serial语句配合max_fail_percentage语句使用。max_fail_percentage表示当最大失败主机的比例达到多少时,ansible就让整个play失败。示例如下:
- name: upgrade pkgs on fservers behind load balancer
hosts: myhosts
serial: 1
max_fail_percentage: 25
tasks:
......
假如负载均衡后面有4台主机,并且有一台主机执行失败,这时ansible还会继续运行,要让Play停止运行,则必须超过25%,所以如果想一台失败就停止执行,我们可以将max_fail_percentage的值设为24。如果我们希望只要有执行失败,就放弃执行,我们可以将max_fail_percentage的值设为0
调试模块
调试模块,用于在调试中输出信息
常用参数:
msg:调试输出的消息
var:将某个任务执行的输出作为变量传递给debug模块,debug会直接将其打印输出
verbosity:debug的级别(默认是0级,全部显示)
例子:
- name: Print debug infomation eg1
hosts: test2
gather_facts: F
vars:
user: jingyong
tasks:
- name: Command run line
shell: date
register: result
- name: Show debug info
debug: var=result verbosity=0
#程序是将命令date返回信息使用debug模块打印出来。
#返回结果如下:
PLAY [Print debug infomation eg] ***********************************************
TASK [Show debug info] *********************************************************
ok: [192.168.0.1] ={
"result": {
"changed": true,
"cmd": "date",
"delta": "0:00:00.002400",
"end": "2016-08-27 13:42:16.502629",
"rc": 0,
"start": "2016-08-27 13:42:16.500229",
"stderr": "",
"stdout": "2016年 08月 27日 星期六 13:42:16 CST",
"stdout_lines": [
"2016年 08月 27日 星期六 13:42:16 CST"
],
"warnings": []
}
}
PLAY RECAP *********************************************************************
192.168.0.1 : ok=2changed=1unreachable=0failed=0
可以看到debug不光输出了date命令结果,还返回了很多相关调试信息,只需要date返回值,可以使用变量属性过滤 如:result.stdout 就是命令的返回值。
程序改成:
- name: Print debug infomation eg
hosts: test2
gather_facts: F
tasks:
- name: Command run line
shell: date
register: result
- name: Show debug info
debug: var=result.stdout verbosity=0
#运行结果:
PLAY [Print debug infomation eg] ***********************************************
TASK [Command run line] ********************************************************
changed: [192.168.0.1]
TASK [Show debug info] *********************************************************
ok: [192.168.0.1] ={
"result.stdout": "2002年 01月 12日 星期六 03:16:26 CST"
}
PLAY RECAP *********************************************************************
192.168.0.1 : ok=3changed=1unreachable=0failed=0
备份老的war包
- hosts: "vcs-{{ csport }}-{{ project }}"
user: root
vars:
war_path: /data/apps/{{ project }}.war
backup_ahead_path: "/data/backup/{{ project }}"
backup_link_path: "/data/backup/{{ project }}_laster"
tasks:
- name: 1/6 Get war time 获取老war的时间作为版本号
shell: stat -c %y {{ war_path }}|awk -F. '{print $1}'|awk '{print $1"_"$2}'|awk -F":" '{print $1"_"$2}'
register: war_time
- name: 2/6 Create backup dir 以老war包的时间创建备份目录
file: dest={{ backup_ahead_path }}_{{ war_time.stdout }} state=directory
- name: 3/6 Backup 将老的war包复制到备份目录下
shell: /bin/cp -ar {{ war_path }} {{ backup_ahead_path }}_{{ war_time.stdout }}
- name: 4/6 Delete old war 删除老的war包
file: path={{ war_path }} state=absent
- name: 5/6 Delete old soft link 删除老的链接文件 laster
file: path={{ backup_link_path }} state=absent
- name: 6/6 Create new soft link 以新的备份目录创建链接文件 laster
file: src={{ backup_ahead_path }}_{{ war_time.stdout }} dest={{ backup_link_path }} state=link
通过nginx更新tomcat的war包
- hosts: "vcs-{{ csport }}-{{ project }}"
user: root
serial: 1
vars:
package: "{{ project }}.war"
storage_path: /data/upload
war_path: /data/apps
webapp_path: /data/app/tomcat8_{{ csport }}/webapps/ROOT
tasks:
- name: 1/11 Create project_path 保证tomcat存在项目目录
file: dest={{ webapp_path }} state=directory
- name: 2/11 Copy war 将war包复制到tomcat上存放war包的目录
copy: src={{ storage_path }}/{{ package }} dest={{ war_path }}
- name: 3/11 nginx upstream down nginx修改配置将tomcat服务器标记为服务不可用
replace:
dest: /data/app/nginx/nginx/conf/nginx.conf
regexp: '(server {{ ansible_eth0.ipv4.address }}:{{ csport}});'
replace: '\1 down;'
delegate_to: 127.0.0.1
- name: 4/11 nginx restart nginx重启新配置生效
shell: /data/app/nginx/nginx/sbin/nginx -s reload
delegate_to: 127.0.0.1 ##将此任务委托给新节点执行
- name: 5/11 Stop tomcat 停止tomcat
shell: /bin/bash -c "/data/app/tomcat8_{{ csport }}/bin/catalina.sh stop"
ignore_errors: true
- name: wait process stop 等待tomcat进程关闭,超时时间15s
wait_for: port={{ csport }} state=stopped timeout=15
- name: 6/11 Kill process 强制杀tomcat进程
shell: ps -ef|grep tomcat8_{{ csport }}|grep -v grep |awk '{print $2}'|xargs kill -9
ignore_errors: true
- name: 7/11 Remove the old ROOT dir 删除老的项目目录
file: path={{ webapp_path }} state=absent
- name: sleep 10s
command: sleep 10s
- name: 8/11 Start tomcat 启动tomcat
shell: /bin/bash -c "nohup /data/app/tomcat8_{{ csport }}/bin/catalina.sh start &"
- name: wait process start
wait_for: port={{ csport }} state=started timeout=15
- name: 9/11 healthcheck 健康状态检查新war
shell: curl -I -o /dev/null -s -w %{http_code}"\n" "{{ ansible_eth0.ipv4.address }}:{{ csport }}/1.html"
register: httpcode
- debug:
msg: httpcode is {{ httpcode.stdout }}
- name: 10/11 nginx upstream up 将tomcat标记为生效
replace:
dest: /data/app/nginx/nginx/conf/nginx.conf
regexp: '(server {{ ansible_eth0.ipv4.address }}:{{ csport}}) down;'
replace: '\1;'
delegate_to: 127.0.0.1
failed_when: httpcode.stdout !="200" 健康状态检查失败则终止
- name: 11/11 nginx restart
shell: /data/app/nginx/nginx/sbin/nginx -s reload
delegate_to: 127.0.0.1
```