今日课程推荐https://edu.51cto.com/course/23377.html ,Ansible与Ansible-playbook应用实战视频课程

1、playbook简介与文件格式

playbook字面意思,即剧本,现实中由演员按照剧本表演,在Ansible中,这次由计算机进行表演,由计算机安装、部署应用,提供对外服务,以及组织计算机处理各种各样的事情。

playbook文件由YMAL语言编写。YMAL格式是类似于JSON的文件格式,便于人理解和阅读,同时便于书写。首先学习了解一下YMAL的格式,对后面书写playbook很有帮助。以下为playbook常用到的YMAL格式规则。

 文件的第一行应该以“---” (三个连字符)开始,表明YMAL文件的开始。
 在同一行中,#之后的内容表示注释,类似于shell,python和ruby。
 YMAL中的列表元素以“-”开头然后紧跟着一个空格,后面为元素内容。
 同一个列表中的元素应该保持相同的缩进。否则会被当作错误处理。
 play中hosts,variables,roles,tasks等对象的表示方法都是键值中间以“:”分隔表示,“:”后面还要增加一个空格。

首先看下面这个例子:

- apple
- banana
- orange

等价于JSON的下面这个格式:

[
 “apple”,
 “banana”,
 “orange”
]

playbook文件是通过ansible-playbook命令进行解析的,ansbile-playbook命令会根据自上而下的顺序依次执行playbook文件中的内容。

2、playbook的组成

playbook是由一个或多个“play”组成的列表。play的主要功能在于,将事先合并为一组的主机组合成事先通过Ansible定义好的角色。将多个play组织在一个playbook中就可以让它们联同起来按事先编排好的机制完成一系列复杂的任务。

playbooks主要有以下四部分构成,分别如下。

 Target部分: 定义将要执行 playbook 的远程主机组。
 Variable部分: 定义playbook运行时需要使用的变量。
 Task部分: 定义将要在远程主机上执行的任务列表。
 Handler部分: 定义task 执行完成以后需要调用的任务。

下面介绍下构成playbook的四个组成部分。

(1)Hosts和Users

  • playbook中的每一个play的目的都是为了让某个或某些远程主机以某个指定的用户身份执行任务。
  • hosts:用于指定要执行任务的远程主机,每个playbook都必须指定hosts,hosts也可以使用通配符格式。主机或主机组在inventory清单(hosts文件)中指定,可以使用系统默认的/etc/ansible/hosts,也可以自己编辑,在运行的时候加上-i选项,可指定自定义主机清单的位置。
  • remote_user:用于指定在远程主机上执行任务的用户。可以指定任意用户,也可以使用sudo,但是用户必须要有执行相应任务的权限。

(2)任务列表

play的主体部分是task list。

task list中的各任务按次序逐个在hosts中指定的所有远程主机上执行,即在所有远程主机上完成第一个任务后再开始第二个。在运行自上而下某playbook时,如果中途发生错误,则所有已执行任务都将回滚,因此在更正playbook后需要重新执行一次。

task的目的是使用指定的参数执行模块,而在模块参数中可以使用变量。模块执行一个命令,即使执行一次或多次, 其结果是一样的,这意味着playbook多次执行是安全的,因为其结果均一致。tasks包含name和要执行的模块,name是可选的,只是为了便于用户阅读,建议加上去,模块是必需的,同时也要给予模块相应的参数。

定义tasks推荐使用module: options”的格式,例如:

service: name=httpd state=running

(3)handlers

用于当关注的资源发生变化时采取一定的操作。handlers是和“notify”配合使用的。
“notify”这个动作可用于在每个play的最后被触发,这样可以避免多次有改变发生时,每次都执行指定的操作,通过“notify”,仅在所有的变化发生完成后一次性地执行指定操作。
在notify中列出的操作称为handler,也就是说notify用来调用handler中定义的操作。
注意:在notify中定义的内容一定要和handlers中定义的“ - name”内容一样,这样才能达到触发的效果,否则会不生效。

(4)tags
tags用于让用户选择运行或略过playbook中的部分代码。Ansible具有幂等性,因此会自动跳过没有变化的部分;但是当一个playbook任务比较多时,一个一个的判断每个部分是否发生了变化,也需要很长时间。因此,如果确定某些部分没有发生变化,就可以通过tags跳过这些代码片断。

3、Playbook执行结果解析

使用ansible-playbook运行playbook文件,输出的内容为JSON格式。并且由不同颜色组成,便于识别。一般而言,输出内容中,每个颜色表示的含义如下。
 绿色代表执行成功,但系统保持原样。
 黄色代表系统状态发生改变,也就是执行的操作生效。
 红色代表执行失败,会显示错误信息。

下面是一个简单的playbook文件:

- name: create user
  hosts: 172.16.213.231
  user: root
  gather_facts: false
  vars:
    user1: testuser
  tasks:
   - name: start createuser
     user: name="{{user1}}"

上面的playbook 实现的功能是新增一个用户,每个参数含义如下。
 name参数对该playbook实现的功能做一个概述,后面执行过程中,会输出name的值。
 hosts参数指定了对哪些主机进行操作。
 user参数指定了使用什么用户登录到远程主机进行操作。
 gather_facts参数指定了在执行task任务前,是否先执行setup模块获取主机相关信息,此参数默认值为true,表示开启,如果我们在task中要使用facts信息时,就需要开启此功能。否则可以设置为false。设置为false可以加快playbook的执行速度。
 vars参数,指定了变量,这里指字一个user1变量,其值为testuser,需要注意的是,变量值一定要用引号括起来。
 tasks指定了一个任务,其下面的name参数同样是对任务的描述,在执行过程中会打印出来。user是一个模块,user后面的name是user模块里的一个参数,而增加的用户名调用了上面user1变量的值。

3、playbook中tasks语法使用

在playbook中,task部分是整个任务的核心,我们前面介绍的ansible的常用模块,例如commands模块、shell模块、file模块、cron模块、user模块等,在playbook中仍然可用,每个模块所使用的参数以及含义跟命令行模式下也完全一样,只不过写法不同而已,下面通过几个例子来看看playbook中常见功能模块的写法。

(1)、playbook示例

下面是一个playbook示例,test.yml文件内容如下:

- hosts: hadoophosts
  remote_user: root
  tasks:
   - name: create hadoop user
     user: name=hadoop state=present
   - name: create hadoop directory and chmod/chown
     file: path=/opt/hadoop state=directory mode=0755 owner=hadoop group=hadoop
   - name: synchronize hadoop program
     synchronize: src=/data/hadoop/ dest=/opt/hadoop
   - name: Setting environment variables
     shell: echo "export JAVA_HOME=/usr/jdk" >> /etc/profile

这个playbook文件中,使用了user、file、synchronize和shell模块,文件开始定义了一个主机组hadoophosts,然后设置root用户在远程主机上执行操作,接着,就是task任务的开始,“- name”是描述性信息,用来标识任务执行内容和进度,第一个task用来创建一个hadoop用户,使用了user模块,注意,上面的user表示ansible的user模块,而user后面的name、state是user模块的参数,这些参数含义上面已经做过介绍,这里就不在重复了

下面还有file模块、synchronize模块以及shell模块,它们的写法跟user模块类似,也不再过多介绍。

从此文件可以看出,通过playbook模式编写的文件更加简洁、易懂,只要设置好了任务的运行策略、顺序,每次需要用到这个操作的话,直接执行就可以了。执行的方式如下:

[root@server239 ansible]# ansible-playbook  test.yml
除了前面已经介绍过的ansible模块,还有一些模块在playbook中也经常用到,下面再介绍一些常用的playbook模块。

(2)、unarchive模块

unarchive模块用来实现解压缩,也就是将压缩文件解压分发到远程不同节点上。只需记住如下几个参数即可:

 src: 源文件路径,这个源文件在管理机上。
 dest: 指定远程主机的文件路径。
 mode:设置远程主机上文件权限。

看下面这个例子:

- hosts: 172.16.213.231
  remote_user: root
  gather_facts: false
  tasks:
   - name: unarchive spark files
     unarchive: src=/src/spark.tar.gz dest=/opt

这个操作是将管理机上的/src/spark.tar.gz文件传输到远程主机上后进行解压缩,并将解压缩后的文件放到远程主机的/opt目录下。注意,这个例子中我们设置了gather_facts选项为false,这是因为下面的操作中,没有用到facts信息。

(3)、lineinfile、replace模块

在自动化运维中,对文件进行内容替换是一个非常常见的场景,比如修改、删除、添加操作系统的某些参数等,Ansible中虽然提供了shell模块结合sed命令来达到替换的效果,但经常会遇到需要转义的问题,并且考虑到可读性性和可维护性等多方面因素,使用Ansible自带的替换模块是一个不错的选择。Ansible常用的替换模块为replace和lineinfile。

replace模块可以根据指定的正则表达式替换远程主机下某个文件中的内容,常用的参数有如下几个:

 path:要操作的远程主机上文件的路径。
 regexp:正则表达式,指定替换规则。
 replace:指定最终要替换的字符串。
 backup:是否在修改文件之前对文件进行备份,yes是进行备份。

看下面这个例子:

- hosts: 172.16.213.231
  remote_user: root
  tasks:
   - name: modify selinux
     replace: path=/etc/selinux/config regexp="enforcing" replace=disabled backup=yes

这个操作是对远程主机上/etc/selinux/config文件中的enforcing字符串进行替换,替换为disabled,替换前进行备份。其实就是关闭远程主机上selinux服务。

最后,再介绍一下lineinfile,此模块也可以实现replace的功能,但lineinfile功能更加强大,支持的参数也比较多,常用参数含义如下:

 path:操作的远程主机上的文件路径
 regexp:正则表达式,要替换的内容规则
 line:指定替换后的文本内容
 state:当设置为absent代表删除匹配的行
 insertafter:insertafter参数可以将文本插入到“指定的行”之后
 insertbefore:insertbefore参数可以将文本插入到“指定的行”之前
 backup:进行替换操作前是否进行备份

下面来看一个基于lineinfile的playbook任务:

- hosts: 172.16.213.231
  remote_user: root
  tasks:
   - lineinfile: dest=/etc/profile insertafter='ulimit(.*)' line="ulimit -c unlimited"
   - lineinfile: dest=/etc/profile line="export JAVA_HOME=/usr/jdk"
   - lineinfile: dest=/etc/selinux/config regexp='SELINUX=(.*)' line='SELINUX=disabled'
   - lineinfile: dest=/etc/resolv.conf regexp='search(.*)' state=absent

这个playbook任务中,调用了四次lineinfile替换操作,第一次是在/etc/profile文件中找到以ulimit开头的行,并在后面添加一行内容"ulimit -c unlimited",第二次是在/etc/profile文件的最后添加一个JAVA_HOME路径,第三次是修改/etc/selinux/config文件中以“SELINUX=”开头的行,将其替换为“SELINUX=disabled”,其实就是关闭selinux,最后一个操作是在/etc/resolv.conf文件找查找以search开头的行,然后将其删除掉。

(4)、register、set_fact、debug模块

ansible中定义变量的方式有很多种,可以将模块的执行结果注册为变量,也可以在roles中的文件内定义变量,还可以使用内置变量等,而register、set_fact都可用来注册一个变量。

使用register选项,可以将当前task的输出结果赋值给一个变量,看下面这个例子:

- hosts: 172.16.213.231
  remote_user: root
  tasks:
   - name: ps command
     shell: hostname
     register: host_result
   - debug: var=host_result

此例子是将在远程主机上执行的shell命令“hostname”的输出结果赋值给变量host_result,然后再将变量引用并使用debug模块输出。输出结果是json格式的。注意,此例子最后还使用了debug模块,此模块用于在调试中输出信息.

下面是上面playbook的debug输出结果:

TASK [debug] **********************************************************
ok: [172.16.213.231] => {
    "host_result": {
        "changed": true,
        "cmd": "hostname",
        "delta": "0:00:00.007228",
        "end": "2020-04-01 04:42:34.254587",
        "failed": false,
        "rc": 0,
        "start": "2020-04-01 04:42:34.247359",
        "stderr": "",
        "stderr_lines": [],
        "stdout": "server231.localdomain",
        "stdout_lines": [
            "server231.localdomain"
        ]
    }
}

可以看出,此输出是一段json格式的数据,最顶端的key为host_result,大括号内还有多个二级key,我们想要的结果是输出远程主机的主机名即可,不需要这些额外的二级key信息,如何实现这个要求呢,如果想要输出json数据的某二级key项,可以使用"key.dict"或"key['dict']"的方式引用即可。从上面输出可以看到,我们需要的二级key是stdout项,所以要仅仅输出此项内容,可以将变量引用改为host_result.stdout即可,也就是将上面的playbook任务改成如下内容:

- hosts: 172.16.213.231
  remote_user: root
  tasks:
   - name: hostname command
     shell: hostname
     register: host_result
   - debug: var=host_result.stdout
   - debug: 'msg="output: {{host_result.stdout}}"'

在这个playbook中,我们又增加了一个debug参数,debug模块常用的参数有两个,分别是msg和var,它们都可以引用变量输出信息,但有一点小区别,msg可以输出自定义信息,并且变量需要双大括号包含起来,而var参数只能输出变量,并且不需要双大括号。

修改后的playbook执行debug输出结果如下:

TASK [debug] ********************************************************
ok: [172.16.213.231] => {
    "host_result.stdout": "server231.localdomain"
}

TASK [debug] ********************************************************
ok: [172.16.213.231] => {
    "msg": "output: server231.localdomain"
}

从输出可知,这个才是我们想要的结果。

set_fact和register的功能很类似,它也可以将task输出赋值给变量。set_fact更像shell中变量的赋值方式,可以将某个变量的值赋值给另一个变量,也可以将字符串赋值给变量。看下面这个例子:


- hosts: 172.16.213.231
  remote_user: root
  tasks:
   - name: hostname command
     shell: hostname
     register: host_result
   - set_fact: var1="{{host_result.stdout}}"
   - set_fact: var2="This is a string"
   - debug: msg="{{var1}}  {{var2}}"

这个例子是将hostname的输出结果赋值给host_result变量,然后通过set_fact将host_result变量赋值给var1变量,接着又将一个字符串赋值给var2变量,最后,通过debug模块输出这些变量信息。注意这些模块的使用方式和书写格式。

这个playbook的输出结果为:

TASK [debug] **********************************************************
ok: [172.16.213.231] => {
    "msg": "server231.localdomain  This is a string"
}

(5)、delegate_to、connection、和local_action模块

ansible默认只会对远程主机执行操作,但有时候如果需要在管理机本机上执行一些操作,该如何实现呢,这个实现的方法有很多,可以通过delegate_to(任务委派)来实现,也可以通过connection:local方法,还可以通过local_action关键字来实现。

下面来看一个例子,说明它们的用法。

- hosts: 172.16.213.231
  remote_user: root
  gather_facts: true
  tasks:
   - name: connection
     shell: echo "connection . {{inventory_hostname}} $(hostname) ." >> /tmp/local.log
     connection: local
   - name: delegate_to
     shell: echo "delegate_to . {{inventory_hostname}} $(hostname) ." >> /tmp/local.log
     delegate_to: localhost
   - name: local_action
     local_action: shell echo "local_action. {{inventory_hostname}} $(hostname)" >> /tmp/local.log

这个例子中,依次使用了connection、delegate_to和local_action三种方式,还使用了一个变量{{inventory_hostname}},这是ansible的一个内置变量,它用来获取远程主机的主机名,说到主机名,其实就是用到了facts信息,所以,需要设置gather_facts选项为true,另外,$(hostname)是shell里面的变量,也是用来获取主机名,此例子实现的功能是将远程主机的主机名依次输出到管理机的/tmp/local.log文件中。