02. Ansible - Playbook

Playbook

在之前使用 Ansible 的时候都是采用命令行的方式执行,这样的方式存在几个问题:

  1. 如果某个操作需要多次执行,如何保存命令。
  2. 生产中的操作往往不是一个模块能完成的,如何整合这些命令。

对于上面的需求,Playbook(剧本)的作用就在于能够通过声明配置的方式,对操作流程进行有序的编排,并支持同步或者异步的方式发起任务。

Playbook 采用 YAML 语言编写,由一个或多个 play 组成。同时,每个 play 又由一个或者多个 task(任务)组成。

基础示例

以一个添加组合用户为例,使用命令行操作:

# 先添加用户组
ansible '192.168.2.201' -b -m group -a 'gid=10001 name=ops state=present'

# 再添加用户
ansible '192.168.2.201' -b -m user -a 'uid=10001 name=opuser01 password="123456" group="ops" create_home=no shell=/sbin/nologin state=present'

这样就需要使用两条命令来完成,改写成 playbook:

---
- hosts: 192.168.2.202
  tasks:
    - name: "1. Add group"
      group: gid=10001 name=ops state=present
    - name: "2. Add user"
      user: uid=10001 name=opuser01 password="123456" group="ops" create_home=no shell=/sbin/nologin state=presen

只需要将 -a 的参数作为模块名称的 Key 的 Value 即可。写完后可以检测是否正常:

ansible-playbook -b 01.add-group-user.yaml --check

通过 --check 或者 -C 参数,只会检测,不会真实执行。

ansible-playbook -b 01.add-group-user.yaml

如图所示:

可以看到执行过程有 1 个 Play,3 个 TASK。其中 Gathering Facts 任务的作用在于收集远程客户端的信息,类似于 setup 模块的用途。属于默认的 TASK,如果主机多,会非常耗时,不过如果不需要用到它返回的 facts 变量,可以关掉它。剩下的 2 个 TASK 就是用户自己定义操作。

-a 参数的值直接放到模块后面,虽然书写简单,但是不便于维护。建议按照 YAML 格式继续拆解:

---
- hosts: 192.168.2.202
  tasks:
    - name: "1. Add group"
      group: 
        gid: 10001
        name: ops
        state: present
    - name: "2. Add user"
      user: 
        uid: 10001
        name: opuser01
        password: 123456
        group: ops
        create_home: no
        shell: /sbin/nologin
        state: present

变量定义

在 Playbook 中创建变量的方式主要包含以下几种:

  • 在 Playbook 文件中,通过 vars 定义。
  • 在 Playbook 文件中,通过 vars_files 引入外部变量文件。
  • 在 Playbook 所在目录中创建 group_vars(能自动识别)目录,在下面为不同组设置对应的变量文件。
  • ansible 内置的变量,比如 facts 的变量。
  • 在 Playbook 文件中,通过 register 将返回注册成变量。

变量定义(vars)

通过在 Playbook 中定义变量可以直接被后面使用:

---
- hosts: 192.168.2.201
  vars:
    source_file: /tmp/demo.sh
    remote_dir: /tmp
  tasks:
    - name: "Send file"
      copy:
        src: "{{ source_file }}"
        dest: "{{ remote_dir }}/abc.sh"
        mode: 0755

可以使用 {{}} 调用变量,但是需要注意,如果变量在内容的开头,需要使用引号括起来,否则可能会报错。

变量定义(vars_files)

通过引入外部定义变量文件然后使用里面的变量。

vars.yaml

source_file: /tmp/demo.sh
remote_dir: /tmp

Playbook:

---
- hosts: 192.168.2.201
  vars_files: 
    - "./vars.yaml"
  tasks:
    - name: "Send file"
      copy:
        src: "{{ source_file }}"
        dest: "{{ remote_dir }}/aaa.sh"
        mode: 0755

变量定义(group_vars)

通过在 Playbook 目录创建 group_vars 目录来定义变量:

mkdir -p group_vars/{all,master,client,linux}

在指定的组下面创建对应的变量配置文件:group_vars/master/vars.yaml

dir_name: /tmp

Playbook:

---
- hosts: master
  tasks:
    - name: "测试变量"
      debug: 
        msg: "变量的值为:{{ dir_name }}"

变量定义(facts)

Ansible 在执行 Playbook 的时候有 Gathering Facts 的操作,该操作用于手机远程机器的配置信息。这些信息也正是 setup 模块输出的信息。在 Playbook 中,可以利用这一点,动态获取到客户端的配置信息,以此作为变量使用。

ansible master -m setup

其返回的内容其实际是一个 JSON,在 Playbook 能够对该 JSON 进行解析,其中主要一些值如下:

{
    "ansible_facts": {
        "ansible_all_ipv4_addresses": [], // IPV4 列表
        "ansible_all_ipv6_addresses": [], // IPV6 列表
        "ansible_default_ipv4": {
            "address": "192.168.2.201",  // 默认 IPV4 地址
            "interface": "ens33",  // 默认网卡名称
        }, 
        "ansible_distribution": "CentOS", // 系统
        "ansible_distribution_version": "7.9", // 系统版本
        "ansible_dns": {
            "nameservers": [] // DNS 地址
        }, 
        "ansible_hostname": "node-01", // 主机名
        "ansible_kernel": "3.10.0-1160.el7.x86_64", // 内核版本
        "ansible_memfree_mb": 2779, // 空闲内存
        "ansible_memtotal_mb": 3770, // 总内存
        "ansible_processor_cores": 2, // CPU 核心数
        "ansible_processor_vcpus": 4, // 总 CPU 数
    }, 
}

Playbook:

---
- hosts: 192.168.2.201
  tasks:
    - name: "测试 facts 变量"
      debug: 
        msg: "主机名称:{{ ansible_hostname }}
              IPV4地址:{{ ansible_default_ipv4.address }}
              系统版本:{{ ansible_distribution }} {{ ansible_distribution_version }}"

如果在 Playbook 中不需要使用 facts 变量,可以配置 gather_facts: false 去掉 Gathering Facts 这个 TASK。

---
- hosts: 192.168.2.201
  gather_facts: false
  tasks:
  	...

变量定义(register)

有时候想要把命令的输出结果当成变量让后面的 TASK 使用,就可以使用 register:

---
- hosts: 192.168.2.201
  tasks:
    - name: "1. Get time"
      shell: "date +%F"
      register: time_now
    - name: "2. Use time"
      debug:
        msg: "{{ time_now }}"

但是这样写存在一个问题,输出的结果不是命令执行的结果:

{
    "msg": {
        "changed": true, 
        "cmd": "date +%F", 
        "delta": "0:00:00.002554", 
        "end": "2023-03-18 21:11:30.448565", 
        "failed": false, 
        "rc": 0, 
        "start": "2023-03-18 21:11:30.446011", 
        "stderr": "", 
        "stderr_lines": [], 
        "stdout": "2023-03-18", 
        "stdout_lines": [
            "2023-03-18"
        ]
    }
}

真正想要的内容其实是 stdout 里面的内容,所以第二步的变量得变成:{{ time_now.stdout }}

除此之外,还有几个值是很总要的:

  • rc:如果是 0,则表示命令执行正确,否则错误
  • stderr:如果出错,这里会有错误信息
  • stdout:真正输出的内容

应用示例(登录提示)

/etc/motd 文件中可以定义一些内容,用户在 ssh 成功后会在终端输出这些内容。通过这个功能,可以在初始化的时候将这个文件嵌入一些系统相关信息,让用户登录服务器的时候能够第一时间看到。

创建 motd 的模板文件:motd.j2

######################################################################################
主机名称:{{ ansible_hostname }}
IPV4地址:{{ ansible_default_ipv4.address }}
系统信息:{{ ansible_distribution }} {{ ansible_distribution_version }}
内核版本:{{ ansible_kernel }}
内存容量:{{ ansible_memtotal_mb }}
######################################################################################

Playbook:

---
- hosts: 192.168.2.201
  tasks:
    - name: "Send file"
      template:
        src: ./motd.j2
        dest: /etc/motd
        backup: yes

执行后登录指定机器查看:

特别注意:

templates 模块的用法和 copy 几乎是一模一样,但是 template 能够处理模板文件中的变量,而 copy 不行。

流程控制(处理 handler)

在之前使用的时候会存在一个问题,上一步不管是否执行,只要没有报错,下一步都会执行。这在某些场景是没必要的。

比如更新 nginx 配置,然后重新加载 nginx,如果第一步更新配置文件,发现文件内容并没有变,文件是不会被分发过去的,但是第二步重新加载 nginx 还是会执行。这有时候并不符合用户的预期设想。为了解决这个问题,就需要用到 handler。

在没使用 handler 之前的 Playbook:

---
- hosts: 192.168.2.201
  gather_facts: false
  tasks:
    - name: "Send file"
      copy:
        src: /tmp/demo.sh
        dest: /tmp/abc.sh
        backup: yes
    - name: "Debug"
      debug:
        msg: "到执行啦"

无论如何都会执行 Debug,此时修改使用 handler:

---
- hosts: 192.168.2.201
  gather_facts: false
  tasks:
    - name: "Send file"
      copy:
        src: /tmp/demo.sh
        dest: /tmp/abc.sh
        backup: yes
      notify:
        - Debug
  handlers:
    - name: "Debug"
      debug:
        msg: "到执行啦"

此时再执行,就不会执行 Debug,因为加了 notify 对当前 TASK 的内容进行了监控,如果有变化,才会执行 handlers 中对应的 TASK。

此时的 Debug 就不再是 TASK,而是 Handler 了。

流程控制(判断 when)

when 一般配合 facts 变量或者 register 变量一起使用,通过对 facts 变量进行判断,进而确定是否执行。

when 的常用配置方式:

when: ( ansible_hostname == "node-01" )
when: ( ansible_hostname is match("node.*|n9e.*") )
when: ( ansible_hostname is not match("node.*|n9e.*") )

使用示例:

---
- hosts: all
  tasks:
    - name: "Debug"
      debug:
        msg: "主机 {{ ansible_hostname }} 执行啦"
      when: ( ansible_hostname == "node-01" )

如图所示:

流程控制(循环 with-items)

with-items 用于列表的循环,用于重复执行某个 TASK。

Playbook:

---
- hosts: 192.168.2.201
  tasks:
    - name: "Debug"
      debug:
        msg: "开始学习:{{ item }}"
      with_items:
        - "Java"
        - "Python"
        - "Golang"

如图所示:

在模块中使用 item 变量可以调用到 with_items 列表中定义的值。


有的时候每次循环传递值可能不止一个,此时的 Playbook 就需要改成:

---
- hosts: 192.168.2.201
  tasks:
    - name: "Debug"
      debug:
        msg: "{{ item.language }} 已经学习 {{ item.years }} 年啦!"
      with_items:
        - { language: "Java", years: 3 }
        - { language: "Python", years: 5 }
        - { language: "Golang", years: 8 }

如图所示:

一般情况下 with_items 已经够用了,如果想要更强大的循环功能,可以使用 loops

标签设置(tags)

给某些 TASK 打上标签,一般用于调试的时候执行或跳过执行标签的 TASK。

Playbook:

---
- hosts: 192.168.2.201
  tasks:
    - name: "Debug1"
      debug:
        msg: "步骤1"
      tags:
        - step1
        - step
    - name: "Debug2"
      debug:
        msg: "步骤2"
      tags:
        - step2
        - step

在执行的时候就可以通过 -t 或者 --tags 指定运行的标签,也可以使用 --skip-tags 跳过某些标签。

ansible-playbook -b 11.tags.yaml -t step1,step2
ansible-playbook -b 11.tags.yaml --skip-tags step1

忽略错误(ignore errors)

在检测语法文件语法的时候,比如 register 设置变量的时候,如果使用 --check 有时会报错,比如:

---
- hosts: 192.168.2.201
  tasks:
    - name: "Get time"
      shell: "date +%F"
      register: "time_now"
    - name: "Use time"
      debug:
        msg: "当期时间:{{ time_now.stdout }}"

此时如果执行的时候 -C 或者 --check 检测文件,就会提示因为 time_now 变量没有生成,所有获取 stdout 出错。

为了顺利检测,可以加上忽略错误:

---
- hosts: 192.168.2.201
  tasks:
    - name: "Get time"
      shell: "date +%F"
      register: "time_now"
    - name: "Use time"
      debug:
        msg: "当期时间:{{ time_now.stdout }}"
      ignore_errors: yes

此时再测试,虽然还是会报错,但是不会影响接下来的流程。

剧本嵌套(include)

在日常使用中,某些剧本是很复杂的,如果都写在一个 yaml 中,会不好维护,所有可以将某些 TASK 拆除出来。然后嵌套回去。

step1.yaml

- name: "Step 1"
  debug:
    msg: "This is step 1"

step2.yaml

- name: "Step 2"
  debug:
    msg: "This is step 2"

main.yaml

---
- hosts: 192.168.2.201
  tasks:
    - include_tasks: step1.yaml
    - include_tasks: step2.yaml

如图所示:

include_tasks 能满足一定的需求,但是如果某些 task 中需要对主机进行筛选,那就意味着需要写很多 when 对主机进行判断,不是很方便。可以通过 role 的方式解决。

角色(roles)

roles 与其说是一种配置,不如说是一种目录规范,其作用在于让剧本的内容更细的进行更细的分门别类。

基本的目录结构:

demo/
├── basic            # 自定义,但需要有意义,而且 role 中会指定该目录
│   ├── files        # 存放没有变量的文件,例如安装配置文件,安装包等
│   ├── handlers
│   │   └── main.yml # 用于存放 handler
│   ├── tasks
│   │   └── main.yml # 存放该 role 下的 tasks
│   └── templates    # 放置使用了变量的模板,推荐以 .j2 作为后缀
├── hosts            # 如果有需要,还可以单独弄一个主机清单,执行的时候通过 -i 指定该文件
└── top.yml          # 入口文件

测试 roles 项目:

demo/basic/files/demo.sh

#!/bin/bash
echo $date

demo/basic/templates/demo.j2

主机名:{{ ansible_hostname }}

demo/basic/handlers/main.yml

- name: "Handler1"
  debug:
    msg: "This is handler 1"
- name: "Handler2"
  debug:
    msg: "This is handler 2"

demo/basic/tasks/main.yml

- name: "Send normal file"
  copy:
    src: demo.sh
    dest: /tmp/a.sh
    mode: 0755
    backup: yes
  notify:
    - Handler1
- name: "Send template file"
  template:
    src: demo.j2
    dest: /tmp/
    backup: yes
  notify:
    - Handler2

这里文件和模板都不需要目录名称,ansible 会自己去对应的目录下面找。


demo/top.yml

---
- hosts: 192.168.2.201
  roles:
    - role: basic

对于复杂的需求,一般都会有多个 hosts 或者 role。结果如图所示:

安全设置(vault)

有些时候某些配置文件中可能包含了密码等敏感内容,如果直接明文写,可能会有风险,此时就需要用到 vault 给这个文件加密。

config.yml

password: 123456

vault.yaml

---
- hosts: 192.168.2.201
  vars_files:
    - "config.yaml"
  tasks:
    - name: echo password
      debug:
        msg: "Password is {{ password }}"

给文件加密:

ansible-vault encrypt config.yaml

此时会让输入密码,按照提示配置就行。完成之后查看 config.yaml 内容就是加密的了。

这时候执行 Playbook 会出现如下错误提示:

ERROR! Attempting to decrypt but no vault secrets found

如果想要使用它,可以跟上询问密码的参数:

ansible-playbook -b vault.yaml --ask-vault-pass

此时就能正常使用了:

posted @ 2023-03-19 00:36  Dy1an  阅读(95)  评论(0编辑  收藏  举报