ansible系列之四:playbook书写方法以及简单实例

由于playbook是使用yaml格式书写的,先来学习下。

yaml格式

以一个简单的playbook为例,说明yaml的基本语法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---
- hosts: localhost
tasks:
- name: exec date command
command: /bin/date
register: ret
notify:
- print out
#- name: copy fstab to /tmp
# copy: src=/etc/fstab dest=/tmp
- name: wait for sshd
wait_for: port=22 delay=10
handlers:
- name: print out
debug: var=ret
  1. yaml文件以---开头,以表明这是一个yaml文件,就像xml文件在开头使用<?xml version="1.0" encoding="utf-8"?>宣称它是xml文件一样。但即使没有使用---开头,也不会有什么影响。

  2. yaml中使用”#”作为注释符,可以注释整行,也可以注释行内从”#”开始的内容。

  3. yaml中的字符串通常不用加任何引号,即使它包含了某些特殊字符。但有些情况下,必须加引号,最常见的是在引用变量的时候。具体见后文。

  4. 关于布尔值的书写格式,即true/false的表达方式。其实playbook中的布尔值类型非常灵活,可分为两种情况:

  5. 模块的参数: 这时布尔值作为字符串被ansible解析。接受yes/on/1/true/no/off/0/false,这时被ansible解析。例如上面示例中的update_cache=yes

  6. 非模块的参数: 这时布尔值被yaml解释器解析,完全遵循yaml语法。接受不区分大小写的true/yes/on/y/false/no/off/n。例如上面的gpgcheck=noenabled=True

    建议遵循ansible的官方规范,模块的布尔参数采用yes/no,非模块的布尔参数采用True/False。

列表与字典

在python中,列表使用 [] 来表示,例如 [ "zhangsan","lisi","wangsu" ]。在yaml中,是使用 -来表示,如下:

1
2
3
- zhangsan
- lisi
- wangwu

而在python中,字典是使用 {} 来表示的。例如:{name:"zhangsan", age:18},这就是一个字典,表示 key-value的关系。而在yaml就不需要加大括号,但每一项都需要分行写。

有如下例子:

1
2
3
4
5
- name: "zhangsan"
age:18

- name: "lisi"
age: 88

翻译成python的写法就是这个样子的:[ {name:"zhangsan", age:18} , {name:"lisi", age:88}] ,表示这个列表里面有2个元素,每个元素里面又都是一个字典。

具体对应在ansible playbook中,列表所描述的是局部环境,它不一定要有名称,不一定要从同一个属性开始,只要使用”- “,它就表示圈定一个范围,范围内的项都属于该列表。例如:

1
2
3
4
5
6
7
8
9
10
---
- name: list1 # 列表1,同时给了个名称
hosts: localhost # 指出了hosts是列表1的一个对象
remote_user: root # 列表1的属性
tasks: # 还是列表1的属性

- hosts: 192.168.100.65 # 列表2,但是没有为列表命名,而是直入主题
remote_user: root
sudo: yes
tasks:

唯一要注意的是,每一个playbook中必须包含"hosts"和"tasks"项。更严格地说,是每个play的顶级列表必须包含这两项。就像上面的例子中,就表示该playbook中包含了两个play,每个play的顶级列表都包含了hosts和tasks。其实绝大多数情况下,一个playbook中都只定义一个play,所以只有一个顶级列表项。顶级列表的各项,其实可以将其看作是ansible-playbook运行时的选项

playbook的内容

先来看一下简单实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@localhost yaml]# cat test.yaml
---
- hosts: localhost
tasks:
- name: exec date command
command: /bin/date
register: ret
notify:
- print out
- name: wait for sshd
wait_for: port=22 delay=10
handlers:
- name: print out
debug: var=ret

运行的过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
[root@localhost yaml]# ansible-playbook test.yaml

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

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

TASK [exec date command] ***************************************************************************************************************************
changed: [localhost]

TASK [wait for sshd] *******************************************************************************************************************************
ok: [localhost]

RUNNING HANDLER [print out] ************************************************************************************************************************
ok: [localhost] => {
"ret": {
"changed": true,
"cmd": [
"/bin/date"
],
"delta": "0:00:00.184998",
"end": "2020-01-08 06:56:53.917607",
"failed": false,
"rc": 0,
"start": "2020-01-08 06:56:53.732609",
"stderr": "",
"stderr_lines": [],
"stdout": "Wed Jan 8 06:56:53 EST 2020",
"stdout_lines": [
"Wed Jan 8 06:56:53 EST 2020"
]
}
}

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

hosts内容

对于playbook中的每一个play,使用hosts选项可以定义要执行这些任务的主机或主机组。具体可以设置为IP或者是/etc/ansible/hosts定义的分组,有以下几种指定主机和主机组的方式:

  • all*:表示inventory中的所有主机。
  • ::取并集。例如”host1:host2:group1”表示2台主机加一个主机组。
  • :&:取交集。例如”group1:&group2”表示两个主机组中都有的主机。
  • :!:排除。例如”group1:!host1”表示group1中排除host1主机的剩余主机。
  • 通配符:例如”web*.baidu.com”。
  • 数字范围:例如”web[0-5].baidu.com”。
  • 字母范围:例如”web[a-d].baidu.com”。
  • 正则表达式:以”~”开头。例如”~web\d.baidu.com”。

tasks任务

每个play都包含一个hosts和一个tasks,hosts定义的是inventory中待控制的主机,tasks下定义的是一系列task任务列表,比如调用各个模块。这些task按顺序一次执行一个,直到所有被筛选出来的主机都执行了这个task之后才会移动到下一个task上进行同样的操作。

当某一台被控主机执行某个任务出错或失败时,它将会被移除出任务轮询列表。也就是说,对于某主机来说,某任务执行失败,后续的所有任务都不会再去执行。当然,这不会影响其他的主机执行任务(除非主机的任务之间有依赖关系)。

tasks的特性如下:

  • 可以为每个task加上name项,但是也不是必须。
  • 可以执行一个或者多个任务,每调用的一个模块都称为一个action。

由于一个task失败会影响到所有的tasks,所以在shell中,如果要忽略非0状态码继续执行任务,可以使用以下两种方式:

1
2
3
tasks: 
- name: ignore non_zero return code
shell: /usr/sbin/ntpdate ntp1.aliyun.com || /bin/true

或者

1
2
3
4
tasks: 
- name: another way to ignore the non_zero return code
shell: /usr/sbin/ntpdate ntp1.aliyun.com
ignore_errors: true

notify和handler

ansible中几乎所有的模块都具有幂等性,这意味着被控主机的状态是否发生改变是能被捕捉的,即每个任务的changed=true或changed=false。ansible在捕捉到changed=true时,就可以触发notify组件(如果定义了该组件)。notify是一个组件,并非一个模块,它可以直接定义action,其主要目的是调用handler。

tag标签

可以为playbook中的每个任务都打上标签,标签的主要作用是可以在ansible-playbook中设置只执行哪些被打上tag的任务或忽略被打上tag的任务。

1
2
3
--list-tags           # list all available tags
-t TAGS, --tags=TAGS # only run plays and tasks tagged with these values
--skip-tags=SKIP_TAGS # only run plays and tasks whose tags do not match these values

条件控制

在ansible中,只有when可以实现条件判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
tasks:

# 逻辑或
- command: /sbin/shutdown -h now
when: (ansible_distribution == "CentOS" and ansible_distribution_major_version == "6") or
(ansible_distribution == "Debian" and ansible_distribution_major_version == "7")

# 逻辑与
- command: /sbin/shutdown -t now
when:
- ansible_distribution == "CentOS"
- ansible_distribution_major_version == "6"

# 取反
- command: /sbin/shutdown -t now
when: not ansible_distribution == "CentOS"

注意两点:

  • when判断的对象是task,所以和task在同一列表层次。它的判断结果决定它所在task是否执行,而不是它下面的task是否执行。

  • when中引用变量的时候不需要加大括号符号。

when也可以应用在role上,如下:

1
2
3
- hosts: webservers
roles:
- { role: debian_stock_config, when: ansible_os_family == 'Debian' }

循环控制

ansible中的循环都是借助迭代来实现的。基本都是以”with_”开头。可以参考 https://ansible-tran.readthedocs.io/en/latest/docs/playbooks_loops.html 官方文档,由国文翻译的。

with_items迭代列表

ansibel支持迭代功能。例如,有一大堆要输出的命令、一大堆要安装的软件包、一大堆要copy的文件等等。如:

1
2
3
4
5
6
7
8
9
10
---
- hosts: localhost
tasks:
- shell: grep -Rl "www\.example\.com" "{{item}}"
with_items:
- file1
- file2
- file3
register: match_file
- debug: msg="{% for i in match_file.results %} {{i.stdout}} {% endfor %}"

with_lines迭代行

with_lines很好用,可以将命令行的输出结果按行迭代。

例如,find一堆文件出来,copy走。

1
2
3
4
5
6
---
- hosts: localhost
tasks:
- copy: src="{{item}}" dest=/tmp/yaml
with_lines:
- find /tmp -type f -name "*.yml"

对文件列表使用循环

with_fileglob 可以以非递归的方式来模式匹配单个目录中的文件.如下面所示:

1
2
3
4
5
6
7
8
9
10
11
12
---
- hosts: all

tasks:

# first ensure our target directory exists
- file: dest=/etc/fooapp state=directory

# copy each file over that matches the given pattern
- copy: src={{ item }} dest=/etc/fooapp/ owner=root mode=600
with_fileglob:
- /playbooks/files/fooapp/*

随机元素

random_choice功能可以用来随机获取一些值.它并不是负载均衡器(已经有相关的模块了).它有时可以用作一个简化版的负载均衡器,比如作为条件判断:

1
2
3
4
5
6
7
8
9
---
- hosts: localhost
tasks:
- debug: msg={{ item }}
with_random_choice:
- "go through the door"
- "drink from the goblet"
- "press the red button"
- "do nothing"

block特性

在ansible中,可以使用”block”关键字将多个任务整合成一个”块”,这个”块”将被当做一个整体,我们可以对这个”块”添加判断条件,当条件成立时,则执行这个块中的所有任务。rescue:只有脚本报错时才执行,always:无论结果如何都执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
---
- hosts: localhost
tasks:
- block:
- debug: msg='I execute normally'
- command: /bin/false
- debug: msg='I never execute, due to the above task failing'
rescue:
- debug: msg='I caught an error'
- command: /bin/false
- debug: msg='I also never execute :-('
always:
- debug: msg="this always executes"

变量

ansible有很多类型的变量,需要掌握之。

facts变量

ansible playbook是在运行时是可以主要可以各主机的基础信息的,即默认是会运行setup模块可以获取这些信息。可以使用gather_facts: no设置不去收集信息。

有如下例子:

1
2
3
4
5
6
7
8
[root@localhost yaml]# cat setup.yaml
---
- hosts: localhost
tasks:
- debug: var=ansible_eth0.ipv4.address
- debug: var=ansible_default_ipv4.address
- debug: var=user
- debug: var=hosts

作用是获取机器的IP信息,运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[root@localhost yaml]# ansible-playbook setup.yaml -e "user=root hosts=localhost"
[WARNING]: Found variable using reserved name: hosts


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

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

TASK [debug] ***************************************************************************************************************************************
ok: [localhost] => {
"ansible_eth0.ipv4.address": "192.168.1.60"
}

TASK [debug] ***************************************************************************************************************************************
ok: [localhost] => {
"ansible_default_ipv4.address": "192.168.1.60"
}

TASK [debug] ***************************************************************************************************************************************
ok: [localhost] => {
"user": "root"
}

TASK [debug] ***************************************************************************************************************************************
ok: [localhost] => {
"hosts": "localhost"
}

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

register注册变量

使用register选项,可以将当前task的输出结果赋值给一个变量。例如,下面的示例中将echo的结果”haha”赋值给say_hi变量。注意,模块的输出结果是json格式的,所以,引用变量时要指定引用的对象。

1
2
3
4
5
6
---
- hosts: localhost
tasks:
- shell: echo haha
register: say_hi
- debug: var=say_hi.stdout

set_fact定义变量

set_fact和register的功能很相似,也是将值赋值给变量。它更像shell中变量的赋值方式,可以将某个变量的值赋值给另一个变量,也可以将字符串赋值给变量。

1
2
3
4
5
6
7
8
---
- hosts: 192.168.100.65
tasks:
- shell: echo haha
register: say_hi
- set_fact: var1="{{say_hi.stdout}}"
- set_fact: var2="your name is"
- debug: msg="{{var2}} {{var1}}"

vars定义变量

可以在play或task层次使用vars定义字典型变量。如果同名,则task层次的变量覆盖play层次的变量。

1
2
3
4
5
6
7
8
9
10
11
---
- hosts: localhost
gather_facts: no
vars:
key1: value1
key2: value2
tasks:
- debug: msg="key1 = {{ key1 }} key2 = {{ key2 }}"
vars:
key2: value 333
- debug: msg="key1 = {{ key1 }} key2 = {{ key2 }}"

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@localhost yaml]# ansible-playbook test_var.yaml

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

TASK [debug] *****************************************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "key1 = value1 key2 = value 333"
}

TASK [debug] *****************************************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "key1 = value1 key2 = value2"
}

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

vars_files定义变量

和vars一样,只不过它是将变量以字典格式定义在独立的文件中,且vars_files不能定义在task层次,只能定义在play层次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@localhost yaml]# cat var_file.yaml
---
key1: value1
key2: value2
[root@localhost yaml]# cat test_var.yaml
---
- hosts: localhost
gather_facts: no
vars_files:
- var_file.yaml
tasks:
- debug: msg="key1 = {{ key1 }} key2 = {{ key2 }}"
vars:
key2: value 333
- debug: msg="key1 = {{ key1 }} key2 = {{ key2 }}"

上面var_file.yaml使用的是相对路径,基于playbook所在的路径。例如该playbook为/tmp/x.yml,则var_file.yaml也应该在/tmp下。当然,完全可以使用绝对路径。

命令行传递变量

ansible和ansible-playbook命令的”-e”选项都可以传递变量,传递的方式有两种:-e key=value-e @var_file。注意,当key=value方式传递变量时,如果变量中包含特殊字符,必须防止其被shell解析。如下案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@localhost yaml]# ansible-playbook test_var.yaml -e "key1=value111111"

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

TASK [debug] ***************************************************************************************************************************************
ok: [localhost] => {
"msg": "key1 = value111111 key2 = value 333"
}

TASK [debug] ***************************************************************************************************************************************
ok: [localhost] => {
"msg": "key1 = value111111 key2 = value2"
}

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

roles中的变量

由于role是整合playbook的,它有默认的文件组织结构。其中有一个目录vars,其内的main.yml用于定义变量。还有defaults目录内的main.yml则是定义role默认变量的,默认变量的优先级最低。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
shell> tree /yaml
/yaml
├── roles
└── nginx
├── defaults
└── main.yml
├── files
├── handlers
├── meta
├── tasks
├── templates
└── vars
└── main.yml
└── site.yml

main.yml中变量定义方式也是字典格式。

主机与主机组变量

在inventory文件中可以为主机和主机组定义变量,不仅包括内置变量赋值,还包括自定义变量赋值。例如以下inventory文件。

1
2
3
4
5
6
7
8
9
10
192.168.100.65 ansible_ssh_port=22 var1=1
[centos7]
192.168.100.63
192.168.100.64
192.168.100.65 var1=2
[centos7:vars]
var1=2.2
var2=3
[all:vars]
var2=4

其中ansible_ssh_port是主机内置变量,为其赋值22,这类变量是设置类变量,不能被引用。此外还在多处为主机192.168.100.65进行了赋值。其中[centos7:vars][all:vars]表示为主机组赋值,前者是为centos7这个组赋值,后者是为所有组赋值。

以下是执行语句:

1
2
3
shell> ansible 192.168.100.65 -i /tmp/hosts -m shell -a 'echo "{{var1}} {{var2}}"'
192.168.100.65 | SUCCESS | rc=0 >>
2 3

从结果可知,主机变量优先级高于主机组变量,给定的主机组变量优先级高于all特殊组。

除了在inventory文件中定义主机、主机组变量,还可以将其定义在host_vars和group_vars目录下的独立的文件中,但要求这些host_vars或group_vars这两个目录和inventory文件或playbook文件在同一个目录下,且变量的文件以对应的主机名或主机组名命名。

例如,inventory文件路径为/etc/ansible/hosts,playbook文件路径为/tmp/x.yml,则主机192.168.100.65和主机组centos7的变量文件路径可以为以下几种:

  • /etc/ansible/host_vars/192.168.100.65
  • /etc/ansible/group_vars/centos7
  • /tmp/host_vars/192.168.100.65
  • /tmp/group_vars/centos7

实例

以下实例实现了一个halo搭建的blog系统。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# cat halo.yaml
---
- hosts: 192.168.1.61
tasks:
- name: install java
yum: name=java-1.8.0-openjdk state=present

- name: get halo
get_url: url={{ item }} dest=/root
with_items:
- http://halo.ryanc.cc/release/halo-latest.jar
- http://halo.ryanc.cc/config/halo.service
- http://halo.ryanc.cc/config/application-template.yaml

- name: modify halo config file
shell: |
[ -d ~/.halo/ ] || mkdir -p ~/.halo/
if [ -f /root/application-template.yaml ];then
sed -i 's/8090/8080/' /root/application-template.yaml
mv /root/application-template.yaml ~/.halo/application.yaml
fi

- name: modify halo service file
shell: |
if [ -f /root/halo.service ];then
sed -i 's#JAR_PATH#/root/halo-latest.jar#' /root/halo.service
fi
[ -f /etc/systemd/system/halo.service ] || mv /root/halo.service /etc/systemd/system/halo.service

- name: add halo service
systemd:
daemon_reload: yes
name: halo
state: started
enabled: yes

- name: Include clean yaml
include: uninstall_halo.yaml
vars:
halo_path: ~/.halo/
tags: clean

# cat uninstall_halo.yaml
---
- name: stop halo service
systemd: name=halo state=stopped

- name: uninstall java
yum: name=java-1.8.0-openjdk state=absent

- name: delete file
shell: |
rm -f ~/halo-latest.jar
rm -f /etc/systemd/system/halo.service
rm -rf ~/.halo
register: result
args:
warn: no

- name: show debug info
debug: var=result
  • 安装halo:ansible-playbook halo.yaml --skip-tags clean
  • 删除halo:ansible-playbook halo.yaml -t clean

playbook和play的关系

一个playbook中可以包含多个play。每个play都至少包含有tasks和hosts这两项,还可以包含其他非必须项,如vars,vars_files,remote_user等。tasks中可以通过模块调用定义一系列的action。只不过,绝大多数时候,一个playbook都只定义一个play。

所以,大致关系为:

  • playbook: [play1,play2,play3]
  • play: [hosts,tasks,vars,remote_user…]
  • tasks: [module1,module2,…]

模块的参数一般来说是key=value格式的,有3种传递的方式:

  • 直接写在模块后,此时要求使用”key=value”格式。这是让ansible内部去解析字符串。因为可分行写,所以有多种写法。
  • 写成字典型,即”key: value”。此时要求多层缩进。这是让yaml去解析字典。
  • 使用内置属性args,然后多层缩进定义参数列表。这是让ansible明确指定用yaml来解析。
  • 本文作者: wumingx
  • 本文链接: https://www.wumingx.com/linux/ansible-playbook.html
  • 本文主题: ansible系列之四:playbook书写方法以及简单实例
  • 版权声明: 本站所有文章除特别声明外,转载请注明出处!如有侵权,请联系我删除。
0%