Ansible 剧本playbook

概念

Playbooks are automation blueprints, in YAML format, that Ansible uses to deploy and configure nodes in an inventory.

playbook 是自动化的蓝图,使用YAML 格式,用来配置和部署清单中的节点。

  • Executing tasks with elevated privileges or as a different user.
  • Using loops to repeat tasks for items in a list.
  • Delegating playbooks to execute tasks on different machines.
  • Running conditional tasks and evaluating conditions with playbook tests.
  • Using blocks to group sets of tasks.

playbook 、play、task 的关系

简单总结一下playbook、play和task的关系:
1.playbook中可以定义一个或多个play
2.每个play中可以定义一个或多个task

- 其中还可以定义两类特殊的task:pre_tasks和post_tasks  
- pre_tasks表示执行执行普通任务之前执行的任务列表  
- post_tasks表示普通任务执行完之后执行的任务列表  

3.每个play都需要通过hosts指令指定要执行该play的目标主机
4.每个play都可以设置一些该play的环境控制行为,比如定义play级别的变量

Yaml 语法

YAML的基本语法规则如下:
(1).使用缩进表示层级关系
(2).缩进时不允许使用Tab键,只允许使用空格
(3).缩进的空格数目不重要,只要相同层级的元素左对齐即可
(4).yaml文件以”—“作为文档的开始,以表明这是一个yaml文件

- 即使没有使用`---`开头,也不会有什么影响  

(5).# 表示注释,从这个字符一直到行尾,都会被解析器忽略
(6).字符串不用加引号,但在可能产生歧义时,需加引号(单双引号皆可),比如引用变量时
(7).布尔值非常灵活,不分区大小写的true/false、yes/no、on/off、y/n、0和1都允许

YAML支持三种数据结构:
(1).对象:key/value格式,也称为哈希结构、字典结构或关联数组
(2).数组:也称为列表
(3).标量(scalars):单个值

可以去找一些在线YAML转换JSON网站,比如 http://yaml-online-parser.appspot.com 通过在线转换可以验证或查看自己所写的YAML是否出错以及哪里出错。

对象

一组键值对,使用冒号隔开key和value。注意,冒号后必须至少一个空格

name: task1
等价于json:{
  "name": "task1"
}


数组

---
- Shell
- Perl
- Python
等价于json:

["Shell","Perl","Python"]

对象与数组结合

---
languages:
  - Shell
  - Perl
  - Python

字典
---
person1:
  name: task1
  age: 18
  gender: male

person2:
  name: task2
  age: 19
  gender: female

复合结构

---
- person1:
  name: task1
  age: 18
  langs:
    - Perl
    - Ruby
    - Shell

- person2:
  name: task2
  age: 19
  langs:
    - Python
    - Javascript

字符串续行

字符串可以写成多行,从第二行开始,必须至少有一个单空格缩进。换行符会被转为空格。

str: hello
  world
  hello world

等价于json:

{
  "str": "hello world hello world"
}

也可以使用 > 换行,它类似于上面的多层缩进写法。此外,还可以使用|在换行时保留换行符。

this: |
  Foo
  Bar
that: >
  Foo
  Bar

等价于json:

{‘that’: ‘Foo Bar’, ‘this’: ‘Foo\nBar\n’}



playbook 的组成

再来解释一下这个playbook文件的含义。

playbook中,每个play都需要放在数组中,所以在playbook的顶层使用列表的方式- xxx:来表示这是一个play(此处是- hosts:)。

每个play都必须包含hoststasks指令。

hosts指令用来指定要执行该play的目标主机,可以是主机名,也可以是主机组,还支持其它方式来更灵活的指定目标主机。具体的规则后文再做介绍。

tasks指令用来指定这个play中包含的任务,可以是一个或多个任务,任务也需要放在play的数组中,所以tasks指令内使用- xxx:的方式来表示每一个任务(此处是- copy:)。

gather_facts是一个play级别的指令设置,它是一个负责收集目标主机信息的任务,由setup模块提供。默认情况下,每个play都会先执行这个特殊的任务,收集完信息之后才开始执行其它任务。但是,收集目标主机信息的效率很低,如果能够确保playbook中不会使用到所收集的信息,可以显式指定gather_facts: no来禁止这个默认执行的收集任务,这对效率的提升是非常可观的。

此外每个play和每个task都可以使用name指令来命名,也建议尽量为每个play和每个task都命名,且名称具有唯一性。

playbook 模块参数传递方式

在刚才的示例中,copy模块的参数传递方式如下:

  tasks: 
    - name: copy /etc/passwd to /tmp
      copy: src=/etc/passwd dest=/tmp

这是标准的yaml语法,参数部分src=/etc/passwd dest=/tmp是一个字符串,当作copy对应的值。

根据前面介绍的yaml语法,还可以换行书写。有以下几种方式:

---
- name: first play
  hosts: nginx
  gather_facts: false
  tasks: 
    - copy: 
        src=/etc/passwd dest=/tmp

    - copy: 
        src=/etc/passwd
        dest=/tmp

    - copy: >
        src=/etc/passwd
        dest=/tmp

    - copy: |
        src=/etc/passwd
        dest=/tmp

除此之外,Ansible还提供了另外两种传递参数的方式:
(1).将参数和参数值写成key: value的方式
(2).使用args参数声明接下来的是参数

---
- name: first play
  hosts: nginx
  gather_facts: false
  tasks: 
    - name: copy1
      copy: 
        src: /etc/passwd
        dest: /tmp

    - name: copy2
      copy: 
      args:
        src: /etc/passwd
        dest: /tmp

指定执行的主机

每一个play都包含hosts指令,它用来指示在解析inventory之后选择哪些主机执行该play中的tasks。

hosts指令通过pattern的方式来筛选节点,pattern的指定方式有以下几种规则:
1.直接指定inventory中定义的主机名

- hosts: localhost  

2.直接指定inventory中的主机组名

- hosts: nginx、hosts: all  

3.使用组名时,可以使用数值索引的方式表示组中的第几个主机

- hosts: nginx[1]:mysql[0]  

4.可使用冒号隔开多个pattern

- hosts: nginx:localhost  

5.可以使用范围表示法

- hosts: 192.168.200.3[0:3]  
- hosts: web[A:D]  

6.可以使用通配符*

- hosts: *.example.com  
- hosts: *  

7.可以使用正则表达式,需使用~开头

- hosts: ~(web|db)\.example\.com  

此外:
1.所有pattern选中的主机都是包含性的,第一个pattern选中的主机会添加到下一个pattern的范围内,直到最后一个pattern筛选完,于是取得了所有pattern匹配的主机
2.pattern前面加一个&符号表示取交集

- pattern1:&pattern2 要求同时存在于pattern1和pattern2中的主机  

3.pattern前面加一个!符号表示排除

- pattern1:!pattern2 要求出现在pattern1中但未出现在pattern2中  

Ansible 模块

https://docs.ansible.com/ansible/latest/module_plugin_guide/index.html

模块的概念

Modules (also referred to as “task plugins” or “library plugins”) are discrete units of code that can be used from the command line or in a playbook task. Ansible executes each module, usually on the remote managed node, and collects return values. In Ansible 2.10 and later, most modules are hosted in collections.

模块(也称为“任务插件”或“库插件”)是可以从命令行或在剧本任务中使用的离散代码单元。Ansible通常在远程托管节点上执行每个模块,并收集返回值。在Ansible 2.10及更高版本中,大多数模块都托管在集合中。

Ansible中提供的模块(Module),每个模块对应一个功能。通常来说,执行一个任务的本质就是执行一个模块。

ansible 模块的帮助

ansible-doc -h

-l, –list List available plugins 查看可用的模块
-s, –snippet Show playbook snippet for specified plugin(s) 获取指定模块的信息

ansible -h

-a MODULE_ARGS, –args MODULE_ARGS
module arguments

-e EXTRA_VARS, –extra-vars EXTRA_VARS
set additional variables as key=value or YAML/JSON, if
filename prepend with @

使用ansible-doc -l 列举模块

以copy 模块举例

详细用法为 ansible-doc copy

查看模块参数 ansible-doc -s copy

如下删除本机文件命令示例:

ansible localhost -m file -a ‘path=/tmp/passwd state=absent’

通过Ansible删除本地文件/tmp/passwd。需要使用的模块是file模块,file模块的主要作用是创建或删除文件/目录。

ansible很多模块供了一个state参数,它是一个非常重要的参数。它的值一般都会包含present和absent两种状态值(并非一定),不同模块的present和absent状态表示的含义不同,但通常来说,present状态表示肯定、存在、会、成功等含义,absent则相反,表示否定、不存在、不会、失败等含义。

参数前有“=”是强制参数如file 模块的path 参数

ansible 调试模块debug

debug模块的用法非常简单,就两个常用的参数:msg参数和var参数。这两个参数是互斥的,所以只能使用其中一个。msg参数可以输出字符串,也可以输出变量的值,var参数只能输出变量的值。

设置变量str 输出变量值。

注意上面示例中的msg="hello {{str}}",Ansible的字符串是可以不用引号去包围的,例如msg=hello是允许的,但如果字符串中包含了特殊符号,则可能需要使用引号去包围,例如此处的示例出现了会产生歧义的空格。此外,要区分变量名和普通的字符串,需要在变量名上加一点标注:{{}}包围Ansible的变量,这其实是Jinja2模板(如果不知道,先别管这是什么)的语法。其实不难理解,它的用法和Shell下引用变量使用$符号或${}是一样的,例如echo "hello ${var}"

Ansible 配置文件

安装目录如下(yum安装):
  配置文件目录:/etc/ansible/
  执行文件目录:/usr/bin/
  Lib库依赖目录:/usr/lib/pythonX.X/site-packages/ansible/
  Help文档目录:/usr/share/doc/ansible-X.X.X/
  Man文档目录:/usr/share/man/man1/

ansible配置文件查找顺序

  ansible与我们其他的服务在这一点上有很大不同,这里的配置文件查找是从多个地方找的,顺序如下:

  1. 检查环境变量ANSIBLE_CONFIG指向的路径文件(export ANSIBLE_CONFIG=/etc/ansible.cfg);
  2. ~/.ansible.cfg,检查当前目录下的ansible.cfg配置文件;
  3. /etc/ansible.cfg检查etc目录的配置文件。

Ansible include/import

使用include还是import?

将各类文件分类存放后,最终需要在某个入口文件去汇集引入这些外部文件。加载这些外部文件通常可以使用include指令、include_xxx指令和import_xxx指令,其中xxx表示内容类型。

在早期Ansible版本,组织文件的方式均使用include指令,但随着版本的更迭,Ansible对这方面做了更为细致的区分。虽然目前仍然支持include,但早已纳入废弃的计划,所以现在不要再使用include指令,在后文中我也不会使用include指令。

对于playbook(或play)或task,可以使用include_xxximport_xxx指令:

  • (1).include_tasks和import_tasks用于引入外部任务文件;
  • (2).import_playbook用于引入playbook文件;
  • (3).include可用于引入几乎所有内容文件,但建议不要使用它;

对于handler,因为它本身也是task,所以它也能使用include_tasksimport_tasks来引入,但是这并不是想象中那么简单,后文再细说。

对于variable,使用include_vars(这是核心模块提供的功能)或其它组织方式(如vars_files),没有对应的import_vars

对于后文要介绍的Role,使用include_roleimport_roleroles指令。

既然某类内容文件既可以使用include_xxx引入,也可以使用import_xxx引入,对于我们来说,就有必要去搞清楚它们有什么区别。本文最后我会详细解释它们,现在我先把结论写在这:

  • (1).include_xxx指令是在遇到它的时候才加载文件并解析执行,所以它是动态解析的;
  • (2).import_xxx是在解析playbook的时候解析的,也就是说在执行playbook之前就已经解析好了,所以它也称为静态加载。

include和import 引入 举例

用示例来解释会非常简单。假设,两个playbook文件pb1.yml和pb2.yml。

pb1.yml文件内容如下:

---
- name: play1
  hosts: localhost
  gather_facts: false
  tasks:
    - name: task1 in play1
      debug:
        msg: "task1 in play1"

  # - include_tasks: pb2.yml
    - import_tasks: pb2.yml

pb2.yml文件内容如下:

- name: task2 in play1
  debug: 
    msg: "task2 in play1"

- name: task3 in play1
  debug: 
    msg: "task3 in play1"

执行pb1.yml:

$ ansible-playbook pb1.yml

上面是在pb1.yml文件中通过import_tasks引入了额外的任务文件pb2.yml,对于此处来说,将import_tasks替换成include_tasks也能正确工作,不会有任何影响。

但如果是在循环中(比如loop),则只能使用include_tasks而不能再使用import_tasks

Ansible sshd 配置

知识点:notify、handlers

---
- name: modify sshd_config
  hosts: new
  gather_facts: false
  tasks:
    # 1. 备份/etc/ssh/sshd_config文件
    - name: backup sshd config
      shell: 
        /usr/bin/cp -f {{path}} {{path}}.bak
      vars: 
        - path: /etc/ssh/sshd_config

    # 2. 设置PermitRootLogin no
    - name: disable root login
      lineinfile: 
        path: "/etc/ssh/sshd_config"
        line: "PermitRootLogin no"
        insertafter: "^#PermitRootLogin"
        regexp: "^PermitRootLogin"
      notify: "restart sshd"

    # 3. 设置PasswordAuthentication no
    - name: disable password auth
      lineinfile: 
        path: "/etc/ssh/sshd_config"
        line: "PasswordAuthentication no"
        regexp: "^PasswordAuthentication yes"
      notify: "restart sshd"

  handlers: 
    - name: "restart sshd"
      service: 
        name: sshd
        state: restarted

这里使用了shell模块中的creates参数,它表示如果其指定的文件/tmp/only_once.txt存在,则不执行shell命令,只有该文件不存在时才执行。这就是保证幂等性的一种体现:既然不能保证多次执行shell命令的结果不变,那就保证只执行一次。

Ansible提供了notify指令和handlers功能。如果在某个task中定义了notify指令,当Ansible在监控到该任务changed=1时,会触发该notify指令所定义的handler,然后去执行handler(不是触发后立即执行,稍后会解释)。所谓handler,其实就是task,无论在写法上还是作用上它和task都没有区别,唯一的区别在于handler是被触发而被动执行的,不像普通task一样会按流程正常执行。

唯一需要注意的是,notify和handlers中任务的名称必须一致。比如notify: "restart nginx",那么handlers中必须得有一个任务设置了name: restart nginx

此外,在上面的示例中,两个lineinfile任务都设置了相同的notify,但Ansible不会多次去重启sshd,而是在最后重启一次。实际上,Ansible在执行完某个任务之后并不会立即去执行对应的handler,而是在当前play中所有普通任务都执行完后再去执行handler,这样的好处是可以多次触发notify,但最后只执行一次对应的handler,从而避免多次重启。

Ansilbe shell模块颁发密钥

shell 模块分发密钥

---
- name: configure ssh connection
  hosts: nodes
  gather_facts: false
  connection: local
  tasks:
    - name: configure ssh connection
      shell: |
        ssh-keyscan {{inventory_hostname}} >>~/.ssh/known_hosts
        sshpass -p'Thtf#997' ssh-copy-id root@{{inventory_hostname}}