Ansible - Playbook
Playbook简介
palybook
是由一个或多个paly
组成的列表。play
的主要功能在于将事先归并为一组的主机装扮成事先通过ansible
中的 task
定义好的角色。从根本上来讲,所谓 task 无非是调用 ansible
的一个 module
。将多个 play
组织在一个 playbook
中,即可以让它们联同起来按事先编排好的机制同唱一台大戏。
一个playbook
由以下几个部分组成:
- Inventory
- Modules
- Ad Hoc Commands
- Playbooks
- tasks:即调用模块完成的操作
- variables:变量
- templates:模板
- handlers:触发器,由某子任务触发执行操作
- roles:角色
Inventory
ansible
的主要功能在于批量主机操作,为了便捷地使用其中的部分主机,可以在inventory file
中将其分组命名。默认的inventory file
为/etc/ansible/hosts
inventory
文件遵循INI文件风格,中括号中的字符为组名。可以将同一个主机同时归并到多个不同的组中;此外,当如若目标主机使用了非默认的SSH端口,还可以在主机名称之后使用冒号加端口号来标明。
[webservers]
www1.magedu.com:2222
www2.magedu.com
[dbservers]
db1.magedu.com
db2.magedu.com
db3.magedu.com
如果主机名称遵循相似的命名模式,还可以使用列表的方式标识各主机,例如:
[webservers]
www[01:50].example.com
[databases]
db-[a:f].example.com
在 inventory file
中也可以设置 变量 ,方便后期 ansible
执行调用。
node2 ansible_ssh_host=192.168.118.15 ansible_ssh_user=root http_port=80
node3 ansible_ssh_host=192.168.118.16 ansible_ssh_user=root http_port=8080
上面配置中 http_port
就是自定义的变量。当然在组中也可以设置该组所有主机相同的变量:
[webservers]
www1.magedu.com
www2.magedu.com
[webservers:vars]
ntp_server=ntp.magedu.com
nfs_server=nfs.magedu.com
注意:这里 webservers:vars
中 vars
是固定关键字写法。
Inventory 常用参数
这里只是列举常用参数:
ansible_ssh_host: 被管理主机及ip
ansible_ssh_port: 被管理主机端口
ansible_ssh_pass: 如果没有设置密钥认证,则需要填写密码
ansible_sudo_pass: linux sudo 切换用户时密码
ansible_shell_type: shell 类型 bash 还是 xsh
Modules
ansible modules
有很多,之后会专门写一篇关于 modules
总结的文档。
Ad Hoc Commands
什么是 ad-hoc
?
当我们需要敲一些命令去快速的查看或者完成一项工作时,而不需要持久的存储这些命令,这样的命令就叫做 ad-hoc
ansible
就提供了两种方式去完成任务,一是 ad-hoc
命令,另一种是 ansible playbook
,前者可以解决一些简单的任务,后者解决较复杂的任务。
比如,当需要马上查看主机端口时,使用 ad-hoc 就是很高效的,
如下:ansible [hostname] -m shell -a 'netstat -ntplu'
Playbook
playbook
核心元素:
- hosts:执行的远程主机列表
- tasks:任务集
- variables:内置变量或自定义变量在 playbook中使用
- notify 和 handlers 结合使用,由特定的条件触发操作,满足条件执行,否则不执行
- tags:标签,指定某条件执行,用于选择运行 playbook 中的部分代码
Playbook 语法
playbook 使用 yaml 语法格式,后缀为 yaml 也可以是 yml 要求及格式如下:
- 在单一一个playbook文件中,可以连续三个连子号(---)区分多个play。还有选择性的连续三个点好(...)用来表示play的结尾,也可省略。
- 次行开始正常写playbook的内容,一般都会写上描述该playbook的功能。
- 使用#号注释代码。
- 缩进必须统一,不能空格和tab混用。
- 缩进的级别也必须是一致的,同样的缩进代表同样的级别,程序判别配置的级别是通过缩进结合换行实现的。
- YAML文件内容和Linux系统大小写判断方式保持一致,是区分大小写的,k/v的值均需大小写敏感
- k/v的值可同行写也可以换行写。同行使用:分隔。
- v可以是个字符串,也可以是一个列表
- 一个完整的代码块功能需要最少元素包括 name: task
下面通过一个安装维护 httpd 服务来逐步引出 playbook中知识点
通过一个简单的示例查看 playbook
语法及格式:
首先在 /etc/ansible/hosts
中定义主机及主机组
[root@localhost ~]#cat /etc/ansible/hosts
[test_hosts]
node2 ansible_ssh_host=192.168.118.15 ansible_ssh_user=root
node3 ansible_ssh_host=192.168.118.16 ansible_ssh_user=root
编写 playbook 脚本:
[root@localhost ~]#cat test.yml
- hosts: test_hosts
remote_user: root
tasks:
- name: "echo hello hukey."
debug:
msg: "hello, hukey."
前三行基本是固定格式:
hosts: [hostname | groupname]
要执行任务的主机或主机组remote_user: [username]
在主机组中执行任务的用户名tasks
: 所有需要执行的任务集
示例1:在 test_hosts
主机上安装 httpd
服务 然后启动服务。
- hosts: test_hosts
remote_user: root
tasks:
- name: install httpd
yum: name=httpd state=latest
- name: start httpd
systemd: name=httpd enabled=yes state=started
上面的这个 playbook
就是安装 httpd 并启动服务,前三行是固定模式,在 tasks
中,每一个以 name
开头的就是一个小任务。所以 tasks
中就是由一个一个小任务组成的。
Playbook 中的变量
如果要在 playbook 中使用变量,则需要 vars
关键字来定义:
- hosts: test_hosts
remote_user: root
vars:
- package: httpd
- service: httpd
tasks:
- name: install {{ package }}
yum: name={{ package }} state=latest
- name: start {{ service }}
systemd: name={{ service }} enabled=yes state=started
这里定义了两个变量 package=httpd
和 service=httpd
在引用变量时,用两个大括号:{{ variable_name }}
执行 Playbook
编写好 playbook 后,就需要通过 ansible-playbook 执行,常用参数如下:
--check or -C #只检测可能会发生的改变,但不真正执行操作
--syntax-check # 语法检查,不执行
--list-hosts #列出运行任务的主机
--list-tags #列出playbook文件中定义所有的tags
--list-tasks #列出playbook文件中定义的所以任务集
--limit #主机列表 只针对主机列表中的某个主机或者某个组执行
-f #指定并发数,默认为5个
-t #指定tags运行,运行某一个或者多个tags。(前提playbook中有定义tags)
-v #显示过程 -vv -vvv更详细
通常,直接使用 ansible-playbook apache.yml
执行:
[root@localhost ~]#ansible-playbook apache.yml
PLAY [test_hosts] ***********************************************************
TASK [Gathering Facts] ******************************************************
ok: [node3]
ok: [node2]
TASK [install httpd] ********************************************************
changed: [node3]
changed: [node2]
TASK [start httpd] **********************************************************
changed: [node3]
changed: [node2]
PLAY RECAP ******************************************************************
node2: ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node3: ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
执行 playbook 时
- 发现主机是否存在或连通
- 开始执行tasks 中的子任务
上面的 playbook中只有2个子任务,通过执行过程可以看出,ansible 在执行 playbook 时,是按照任务为中心的思想来执行的, 也就是 第一个任务,在所有的主机上执行完毕,然后在将第二个任务在所有主机上执行。其中 ok 表示每一步执行的是否成功,而 changed 则表示执行该playbook 被管理主机是否发生了更改,如果被管理主机没有发生更改则不会有 changed,再次执行该playbook:
[root@localhost ~]#ansible-playbook apache.yml
PLAY [test_hosts] *************************************************************
TASK [Gathering Facts] ********************************************************
ok: [node3]
ok: [node2]
TASK [install httpd] ********************************************************
ok: [node3]
ok: [node2]
TASK [start httpd] *******************************************************
ok: [node3]
ok: [node2]
PLAY RECAP **************************************************************
node2: ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node3: ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
因为第一次已经安装启动了, 再次执行 playbook
则不发生任何更改。
需求又来了, 要修改主机的 httpd 端口为 8080
思路:ansible 主机需要有一份 修改好端口的配置文件,将配置文件推送到被管理主机,然后重启服务。
修改如下:
- hosts: test_hosts
remote_user: root
vars:
- package: httpd
- service: httpd
tasks:
- name: install {{ package }}
yum: name={{ package }} state=latest
- name: copy configuration file
copy: src=/root/conf/httpd.conf dest=/etc/httpd/conf/httpd.conf
- name: restart {{ service }}
systemd: name={{ service }} enabled=yes state=restarted
copy: src=/root/conf/httpd.conf dest=/etc/httpd/conf/httpd.conf
/root/conf/httpd.conf
为 ansible 主机本地路径,在该配置文件中修改端口为: 8080 覆盖到被管理主机的 /etc/httpd/conf/httpd.conf
对比刚开始的 playbook 做了改动,加入了 -name: copy configration file
并将 start httpd
修改为 restart
执行如下:
[root@localhost ~]#ansible-playbook apache.yml
PLAY [test_hosts] **********************************************************
TASK [Gathering Facts] *****************************************************
ok: [node2]
ok: [node3]
TASK [install httpd] *******************************************************
ok: [node3]
ok: [node2]
TASK [copy configuration file] *********************************************
changed: [node3]
changed: [node2]
TASK [restart httpd] *******************************************************
changed: [node2]
changed: [node3]
PLAY RECAP *****************************************************************
node2: ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node3: ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
OK,执行没有任何问题,以后每次修改端口都可以直接在本地修改,然后执行下 playbook 推送再重启就好了,但是上面的写法并不完美。就算配置文件没有修改依然会重启服务,这是没有必要的。有没有一种方法,当修改了配置文件才重启服务, 如果没有修改则不重启呢?这就需要用到 notify + handlers
处理机制。
notify + handlers
Handlers 和 notify 结合使用,由特定条件触发的操作,满足条件才执行,否则不执行。这里需要满足的条件是,当配置文件修改,playbook 如下:
- hosts: test_hosts
remote_user: root
vars:
- package: httpd
- service: httpd
tasks:
- name: install {{ package }}
yum: name={{ package }} state=latest
- name: copy configuration file
copy: src=/root/conf/httpd.conf dest=/etc/httpd/conf/httpd.conf
notify:
- restart httpd
- name: start {{ service }}
systemd: name={{ service }} enabled=yes state=started
handlers:
- name: restart httpd
systemd: name=httpd state=restarted
在 copy
下一行加入了 notify
然后在最后添加 handlers 注意格式!
修改配置文件:/root/conf/httpd.conf Listen:8000
然后执行 playbook
[root@localhost ~]#ansible-playbook apache.yml
PLAY [test_hosts] **********************************************************
TASK [Gathering Facts] *****************************************************
ok: [node2]
ok: [node3]
TASK [install httpd] *******************************************************
ok: [node3]
ok: [node2]
TASK [copy configuration file] *********************************************
changed: [node2]
changed: [node3]
TASK [start httpd] *********************************************************
ok: [node2]
ok: [node3]
RUNNING HANDLER [restart httpd] ********************************************
changed: [node2]
changed: [node3]
PLAY RECAP *****************************************************************
node2: ok=5 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node3: ok=5 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
配置文件修改,在执行子任务时触发了 notify 定义触发器,执行了 restart
,再次执行 playbook 试试:
[root@localhost ~]#ansible-playbook apache.yml
PLAY [test_hosts] **********************************************************
TASK [Gathering Facts] *****************************************************
ok: [node2]
ok: [node3]
TASK [install httpd] *******************************************************
ok: [node3]
ok: [node2]
TASK [copy configuration file] *********************************************
ok: [node2]
ok: [node3]
TASK [start httpd] *********************************************************
ok: [node3]
ok: [node2]
PLAY RECAP *****************************************************************
node2: ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node3: ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
再次执行时,配置文件并没有发生任何变化,所以这里没有触发 restart httpd
这样比上面 每次执行都重启要智能很多了吧?但是依然不完美。每次执行 playbook 都会执行一些没必要的子任务,比如:TASK [install httpd]
和 TASK [start httpd]
这两个子任务在安装执行之后一般不会在用到了, 但是删除了又不能保证 playbook 的完整性,这时候就需要 tags 出马了。
Tags
tags 便签的意思,也就是为子任务打一个标签,然后就可以通过 ansible-playbook
执行打标签的部分,其他子任务不予执行,如下:
- hosts: test_hosts
remote_user: root
vars:
- package: httpd
- service: httpd
tasks:
- name: install {{ package }}
yum: name={{ package }} state=latest
- name: copy configuration file
copy: src=/root/conf/httpd.conf dest=/etc/httpd/conf/httpd.conf
notify:
- restart httpd
tags:
- updateConf
- name: start {{ service }}
systemd: name={{ service }} enabled=yes state=started
handlers:
- name: restart httpd
systemd: name=httpd state=restarted
在 [name: copy configuration file]
子任务最后加入了 tags: updateConf
,然后修改配置文件 /root/conf/httpd.conf
端口为 9090
通过 --list-tags
查看标签:
[root@localhost ~]#ansible-playbook apache.yml --list-tags
playbook: apache.yml
play #1 (test_hosts): test_hosts TAGS: []
TASK TAGS: [updateConf]
通过 ansible-playbook apache.yml -t updateConf
执行打标签的子任务:
[root@localhost ~]#ansible-playbook apache.yml -t updateConf
PLAY [test_hosts] **********************************************************
TASK [Gathering Facts] *****************************************************
ok: [node2]
ok: [node3]
TASK [copy configuration file] *********************************************
changed: [node3]
changed: [node2]
RUNNING HANDLER [restart httpd] ********************************************
changed: [node2]
changed: [node3]
PLAY RECAP *****************************************************************
node2: ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node3: ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
可以看到,所有子任务中,只是执行了 TASK [copy configuration file]
子任务,这样的 playbook 就趋近完美了。可以再次执行,再次执行时由于httpd.conf 没有发生修改,所以不会触发 restart httpd
,如下:
[root@localhost ~]#ansible-playbook apache.yml -t updateConf
PLAY [test_hosts] **********************************************************
TASK [Gathering Facts] *****************************************************
ok: [node2]
ok: [node3]
TASK [copy configuration file] *********************************************
ok: [node2]
ok: [node3]
PLAY RECAP *****************************************************************
node2: ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node3: ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
总结一下:
通过上面的安装、维护 httpd 服务编写的 playbook 中,使用到了:
- 变量:通过
vars
关键字定义变量; - notify + handlers 子任务触发器,触发执行,不触发则不执行;
- tags:标签,使用
ansible-playbook xxx.yml -t [tag_name]
只执行打标签的子任务,其他任务不予执行。
除了上面,还有一些功能也是非常使用的。
条件判断
很多文中这里都是 流程控制,我觉得用 条件判断 来定义更简单易懂。
playbook 中使用 when
关键字来进行条件判断,这里的 when
相当于 shell 中的 if
,它是 jinja2 的语法。
有这样一个需求:如果主机的 IP 为: 192.168.118.15 则打印它的主机名,实现如下:
有这样一个需求:如果主机的 IP 为: 192.168.118.15 则打印它的主机名,实现如下: |
- hosts: all
remote_user: root
tasks:
- name: IP -> Host
debug: msg={{ ansible_fqdn }}
when: ansible_all_ipv4_addresses[0] == '192.168.118.15'
关键字: when
定义条件判断, 当 ip 为:192.168.118.15
的时候打印 主机的 fqdn 否则跳过,执行如下:
[root@localhost ~]#ansible-playbook hosts.yml
PLAY [all] *****************************************************************
TASK [Gathering Facts] *****************************************************
ok: [node3]
ok: [node2]
TASK [IP -> Host] **********************************************************
ok: [node2] => {
"msg": "node2.super.com"
}
skipping: [node3]
PLAY RECAP *****************************************************************
node2: ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node3: ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
定义了两台主机,其中 node3 不满足条件, 则跳过 skip
循环
在playbook中使用循环的场景还是很多的, 比如安装一些 程序包,处理一堆文件,创建一批用户等。
playbook 中还有循环功能,关键字: item
和 with_items
需求:为 test_hosts 主机组创建一批新用户 |
- hosts: test_hosts
remote_user: root
tasks:
- name: add users
user: name={{ item }}
with_items:
- user10
- user20
- user30
item
为固定变量关键字,循环内容为 with_items
中的值,执行结果如下:
[root@localhost ~]#ansible-playbook adduser.yml
PLAY [test_hosts] **********************************************************
TASK [Gathering Facts] *****************************************************
ok: [node3]
ok: [node2]
TASK [add users] ***********************************************************
changed: [node3] => (item=user10)
changed: [node2] => (item=user10)
changed: [node3] => (item=user20)
changed: [node2] => (item=user20)
changed: [node2] => (item=user30)
changed: [node3] => (item=user30)
PLAY RECAP *****************************************************************
node2: ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node3: ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
reigster
在 playbook 中 task 之间 可以通过 register 接收结果并传递
- hosts: node2
remote_user: root
tasks:
- name: register hostname
shell: "hostname"
register: info
- name: display vars
debug: msg="{{ info.stdout }}"
执行结果:
[root@localhost ~]#ansible-playbook host.yml
PLAY [node2] ***************************************************************
TASK [Gathering Facts] *****************************************************
ok: [node2]
TASK [register hostname] ***************************************************
changed: [node2]
TASK [display vars] ********************************************************
ok: [node2] => {
"msg": "node2.super.com"
}
PLAY RECAP *****************************************************************
node2: ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
通过上面的示例,可以观察出 子任务1通过 register获取到执行结果,在子任务2中通过 变量名 info
打印出来。这样的方式可以通过执行命令来判断结果是否是想要的结果。