Ansible是什么?

Ansible是一个自动化管理远程系统的工具,它主要有两个功能:

  • 自动化管理远程节点
  • 控制远程节点,让它们满足预期的状态

主要由3个组件构成:

  1. 控制节点: 用于安装和运行ansible。
  2. 被控制节点: ansible管理的节点。
  3. 清单(Inventory):按逻辑组织的被控制节点列表,它描述了ansible管理的对象。

Ansible的安装配置步骤:

  1. 使用Homebrew安装: brew install ansible

  2. 创建被管理节点的清单hosts.yaml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    all:
    children:
    master:
    hosts:
    master-node:
    ansible_host: 52.118.108.104
    ansible_user: root
    nodes:
    hosts:
    node-1:
    ansible_host: 150.238.65.205
    ansible_user: root
  3. 检验清单是否正确。-i用于指定清单文件,all和master是被管理节点的模式(pattern)。

    1
    2
    3
    4
    5
    6
    7
    $ ansible -i hosts.yaml all --list-host
    hosts (2):
    master-node
    node-1
    $ ansible -i hosts.yaml master --list-host
    hosts (1):
    master-node
  4. 配置SSH免密码登录,让控制节点可以远程连接到被管理节点。

  5. 检验SSH是否配置正确

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    $ansible -i hosts.yaml all -m ping
    master-node | SUCCESS => {
    "ansible_facts": {
    "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
    }
    node-1 | SUCCESS => {
    "ansible_facts": {
    "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false,
    "ping": "pong"
    }

如何写一个Ansible清单?

  • Inventory支持多种文件格式,最常用的两种是INI和YAML。
  • 可以对清单中的节点分组(group),系统有两个默认分组 allungrouped
  • 可以使用children来嵌套分组。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    all:
    children:
    master:
    hosts:
    master-node:
    ansible_host: 52.118.108.104
    ansible_user: root
    nodes:
    hosts:
    node-1:
    ansible_host: 150.238.65.205
    ansible_user: root
    prod:
    children:
    master:
    nodes:
    1
    2
    3
    4
    $ ansible -i hosts.yaml prod --list-hosts
    hosts (2):
    master-node
    node-1
  • 可以使用类似于python切片的语法来指定被控制节点的范围
    1
    2
    3
    pod1:
    hosts:
    9.1.1.[1:10:2]
    1
    2
    3
    4
    5
    6
    7
    $ ansible -i range.yaml pod1 --list-hosts
    hosts (5):
    9.1.1.1
    9.1.1.3
    9.1.1.5
    9.1.1.7
    9.1.1.9

如何使用变量?

  • 变量名和python的命名规则相同,由字母数字下划线组成且不能以数字开头,不能是python或者ansible的关键字。
  • 使用yaml风格定义变量
  • 变量作用范围为global,play,host
  • 可以在task中使用register将输出赋值给变量

如何在清单中添加变量?

  1. 给一个节点添加变量,可以直接在节点下添加键值对,如上述hosts.yaml中的ansible_hostansible_user
  2. 给一个group添加变量,可以在group下面添加vars字段。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    all:
    children:
    master:
    hosts:
    master-node:
    ansible_host: 52.118.108.104
    ansible_user: root
    nodes:
    hosts:
    node-1:
    ansible_host: 150.238.65.205
    ansible_user: root
    vars:
    ntp_server: 9.1.1.1
    prod:
    children:
    master:
    nodes:
    1
    2
    3
    4
    5
    6
    $ ansible -i hosts.yaml master -a "echo {{ ntp_server }}"
    master-node | FAILED | rc=-1 >>
    The task includes an option with an undefined variable. The error was: 'ntp_server' is undefined. 'ntp_server' is undefined
    $ ansible -i hosts.yaml nodes -a "echo {{ ntp_server }}"
    node-1 | CHANGED | rc=0 >>
    9.1.1.1
  3. 也可以在清单文件同目录下创建group_vars/{{group_name}}文件来给group定义变量。
    1
    2
    3
    4
    5
    6
    $ cat group_vars/master
    ---
    ftp_server: 9.1.1.1
    $ ansible -i hosts.yaml master -a "echo {{ ftp_server }}"
    master-node | CHANGED | rc=0 >>
    9.1.1.1

如何使用模式来匹配目标主机?

模式(Pattern)可以通过通配符或者正则表达式来指定一个主机、IP、或者清单组(group)。

  • all或者*匹配所有主机
  • 主机或者group名匹配一个主机或者一个group
  • 可以用:或者,指定多个主机或group
  • group1:!group2 匹配在group1中且不在group2中的主机
  • group1:&group2 匹配既在group1又在group2中的主机

如何如何执行临时命令?

可以使用下面的命令来执行临时命令:

1
$ ansible [pattern] -m [module] -a "[module options]"

如果不指定-m [module],ansible会使用默认的command模块,module options就是要执行的命令。

1
2
3
$ ansible -i hosts.yaml master -a 'ls $HOME'
master-node | CHANGED | rc=0 >>
snap

使用file模块来创建一个目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ ansible -i hosts.yaml master -m ansible.builtin.file -a "dest=/root/projects mode=755 owner=root group=root state=directory"
master-node | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": true,
"gid": 0,
"group": "root",
"mode": "0755",
"owner": "root",
"path": "/root/projects",
"size": 4096,
"state": "directory",
"uid": 0
}
$ ansible -i hosts.yaml master -a 'ls $HOME' -u root
master-node | CHANGED | rc=0 >>
projects
snap

jinja2是什么?

jinja2是一种模板语言,通常用于web后端渲染HTML页面,但它可以处理所有格式的文本文件。

Template + data = HTML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Webpage</title>
</head>
<body>
<ul id="navigation">
{% for item in navigation %}
<li><a href="{{ item.href }}">{{ item.caption }}</a></li>
{% endfor %}
</ul>

<h1>My Webpage</h1>
{{ a_variable }}

{# a comment #}
</body>
</html>

jinja的三种语句:

  • {% ... %} for Statements
  • {{ ... }} for Expressions to print to the template output
  • {# ... #} for Comments not included in the template output

jinja的过滤器filters:

1
{{ name|striptags|title }}

过滤器相当于一个把变量name当做参数的纯函数组合,上述过滤器就相当于函数调用title(striptags(name))

Ansible playbooks是什么?

前面介绍的临时命令并不能满足复杂的部署任务,利用可重复使用、配置简单的ansible playbook就可以实现复杂的多机部署任务。利用playbook,用户可以:

  • 声明配置。
  • 编排在多机上执行的任务的步骤及顺序。
  • 同步或者异步地执行这些任务。

Ansible playbook由一个或者多个play组成,并按照从上到下的顺序执行,每个play至少包含两个部分:

  • 用pattern来指定被管理的机器。
  • 要执行的任务(task)。

Ansible playbook有如下特点:

  • 在匹配的主机上,默认按顺序执行定义的任务,一次执行一个。
  • 每个任务用指定的参数来执行一个ansible模块(module)。
  • 如果任务在某个主机上失败,ansible则不会在该主机上执行接下来的所有任务。
  • 大多数的ansible模块会检查机器的状态是否已经满足了module定义的状态,如果已经满足,则退出任务的执行。所以ansible模块的执行是幂等的。
  • task可以用block组织起来,然后利用rescuealways做异常处理。类似于try...except...finally

Ansible playbook的相关命令:

  • $ ansible-playbook playbook.yaml -f 10: Fork出10个子进程来执行playbook中定义的任务。
  • $ ansible-lint playbook.yaml: 检查playbook的定义。

例如,用ansible playbook来列出主机上家目录下的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
### playbook.yaml
- name: List files of master nodes
hosts: master
tasks:
- name: List files
ansible.builtin.find:
paths: /root
register: out
- name: Print the List
ansible.builtin.debug:
var: out
- name: List files of nodes
hosts: nodes
tasks:
- name: List files
ansible.builtin.find:
paths: /root
register: out
- name: Print the List
ansible.builtin.debug:
var: out
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
$ ansible-playbook -i hosts.yaml playbook.yaml -f 10

PLAY [List files of master nodes]

TASK [Gathering Facts]
ok: [master-node]

TASK [List files]
ok: [master-node]

TASK [Print the List]
ok: [master-node] => {
"out": {
"changed": false,
"examined": 9,
"failed": false,
"files": [
{
"atime": 1668424993.3145647,
"ctime": 1668424993.3145647,
"dev": 64513,
"gid": 0,
"gr_name": "root",
"inode": 33538,
"isblk": false,
"ischr": false,
"isdir": false,
"isfifo": false,
"isgid": false,
"islnk": false,
"isreg": true,
"issock": false,
"isuid": false,
"mode": "0644",
"mtime": 1668424993.3145647,
"nlink": 1,
"path": "/root/a.txt",
"pw_name": "root",
"rgrp": true,
"roth": true,
"rusr": true,
"size": 0,
"uid": 0,
"wgrp": false,
"woth": false,
"wusr": true,
"xgrp": false,
"xoth": false,
"xusr": false
}
],
"matched": 1,
"msg": "All paths examined",
"skipped_paths": {}
}
}

PLAY [List files of nodes]

TASK [Gathering Facts]

TASK [List files]
ok: [node-1]

TASK [Print the List]
ok: [node-1] => {
"out": {
"changed": false,
"examined": 8,
"failed": false,
"files": [],
"matched": 0,
"msg": "All paths examined",
"skipped_paths": {}
}
}

PLAY RECAP
master-node : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node-1 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

如何在playbook中使用过滤器?

  1. 处理未定义的变量
  • {{ some_var | default('admin', true) }}: 给some_var设置默认值为’admin’,true表示some_var是false或者空字符串的时候也使用默认值。
  • {{ mode | default(omit) }}: 如果mode没有定义就忽略它,直接使用系统的默认值。
  • {{ some_var | mandatory }}: 如果some_var没有定义,则ansible会执行失败。
  1. 根据变量的值是true/false/null来返回不同的结果, 如{{ enabled | ternary('no shutdown', 'shutdown', omit) }}
  2. 使用过滤器type_debug, dict2itemsitems2dict来获取或改变数据类型。
    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
    # playbook filters.yaml
    - name: Example for list to dict
    hosts: local
    gather_facts: no
    vars:
    colors:
    - fruit: apple
    color: red
    - fruit: pear
    color: yellow
    - fruit: grapefruit
    color: yellow
    tasks:
    - name: Print dict
    ansible.builtin.debug:
    msg: "{{ colors | items2dict(key_name='fruit', value_name='color') | string }}" # "{'apple': 'red', 'pear': 'yellow', 'grapefruit': 'yellow'}"
    - name: Example for dict to list
    hosts: local
    gather_facts: no
    vars:
    fruits:
    apple: red
    pear: yellow
    grapefruit: yellow
    tasks:
    - name: Print list
    ansible.builtin.debug:
    msg: "{{ fruits | dict2items(key_name='fruit', value_name='color') | string }}" # "[{'fruit': 'apple', 'color': 'red'}, {'fruit': 'pear', 'color': 'yellow'}, {'fruit': 'grapefruit', 'color': 'yellow'}]"
  3. 强制类型转换。{{ some_var | bool }}, {{ some_var | to_json }}, {{ some_var | to_nice_yaml(indent=4) }}
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    # playbook: filters.yaml
    - name: Example for to_json, to_nice_json and from_json
    hosts: local
    gather_facts: no
    vars:
    fruits:
    apple: red
    pear: yellow
    grapefruit: yellow
    tasks:
    - name: Print to_json
    ansible.builtin.debug:
    msg: "{{ fruits | to_json}}"
    - name: Print to_nice_json
    ansible.builtin.debug:
    msg: "{{ fruits | to_nice_json}}"
    - name: Print from_json
    ansible.builtin.debug:
    msg: "{{ fruits | to_nice_json | from_json}}"
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    ok: [localhost] => {
    "msg": "{\"apple\": \"red\", \"pear\": \"yellow\", \"grapefruit\": \"yellow\"}"
    }
    ok: [localhost] => {
    "msg": "{\n \"apple\": \"red\",\n \"grapefruit\": \"yellow\",\n \"pear\": \"yellow\"\n}"
    }
    ok: [localhost] => {
    "msg": {
    "apple": "red",
    "grapefruit": "yellow",
    "pear": "yellow"
    }
    }
  4. 使用zip, zip_longest合并列表。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # filters.yaml
    - name: Example for zip, zip_longest
    hosts: local
    gather_facts: no
    vars:
    indexes: [0, 1, 2, 3]
    tasks:
    - name: Print list with zip.
    debug:
    msg: "{{ indexes | zip(['a', 'b', 'c', 'd']) | string }}" # "[(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd')]"
    - name: Print list with zip_longest.
    debug:
    msg: "{{ indexes | zip_longest(['a'], fillvalue='x') | string }}" # "[(0, 'a'), (1, 'x'), (2, 'x'), (3, 'x')]"
  5. 使用subelements合并对象本身及其子元素。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # filters.yaml
    - name: Example for zip, zip_longest
    hosts: local
    gather_facts: no
    vars:
    users:
    - name: Han Meimei
    roles: ['developer', 'tester', 'sre']
    - name: Li lei
    roles: ['developer']
    tasks:
    - name: Print combined msg.
    debug:
    msg: "{{ item.0.name }} is a {{ item.1 }}."
    loop: "{{ users | subelements('roles') }}"
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ok: [localhost] => (item=[{'name': 'Han Meimei', 'roles': ['developer', 'tester', 'sre']}, 'developer']) => {
    "msg": "Han Meimei is a developer."
    }
    ok: [localhost] => (item=[{'name': 'Han Meimei', 'roles': ['developer', 'tester', 'sre']}, 'tester']) => {
    "msg": "Han Meimei is a tester."
    }
    ok: [localhost] => (item=[{'name': 'Han Meimei', 'roles': ['developer', 'tester', 'sre']}, 'sre']) => {
    "msg": "Han Meimei is a sre."
    }
    ok: [localhost] => (item=[{'name': 'Li lei', 'roles': ['developer']}, 'developer']) => {
    "msg": "Li lei is a developer."
    }
  6. 使用combine来合并字典。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #filters.yaml
    - name: Example for combine
    hosts: local
    gather_facts: no
    vars:
    user:
    name: Han Meimei
    roles: ['developer', 'tester', 'sre']
    scores:
    math: 100
    tasks:
    - name: Print combined user.
    debug:
    msg: "{{ user | combine({ 'roles': ['designer', 'sre'], 'scores': { 'english': 80 } }, list_merge='append_rp', recursive=True) | from_yaml }}"
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    ok: [localhost] => {
    "msg": {
    "name": "Han Meimei",
    "roles": [
    "developer",
    "tester",
    "designer",
    "sre"
    ],
    "scores": {
    "english": 80,
    "math": 100
    }
    }
    }
  7. 使用map从列表或字典中抽取数据。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #filters.yaml
    - name: Example for map
    hosts: local
    gather_facts: no
    tasks:
    - name: Print list.
    debug:
    msg: "{{ [0,2] | map('extract', ['x','y','z']) | list | string }}" # "['x', 'z']"
    - name: Print dict.
    debug:
    msg: "{{ ['x','y'] | map('extract', {'x': 42, 'y': 31}) | list | string }}" # "[42, 31]"
  8. 使用ansible.builtin.permutationsansible.builtin.combinations来获取列表的排列与组合。
  9. 使用product来获取两个列表的笛卡尔积。
  10. 使用json_query来获取json的子元素。
    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
    #filters.yaml
    - name: Example for json_query
    hosts: local
    gather_facts: no
    vars:
    "domain_definition": {
    "domain": {
    "cluster": [
    {
    "name": "cluster1"
    },
    {
    "name": "cluster2"
    }
    ],
    "server": [
    {
    "name": "server11",
    "cluster": "cluster1",
    "port": "8080"
    },
    {
    "name": "server12",
    "cluster": "cluster1",
    "port": "8090"
    },
    {
    "name": "server21",
    "cluster": "cluster2",
    "port": "9080"
    },
    {
    "name": "server22",
    "cluster": "cluster2",
    "port": "9090"
    }
    ],
    "library": [
    {
    "name": "lib1",
    "target": "cluster1"
    },
    {
    "name": "lib2",
    "target": "cluster2"
    }
    ]
    }
    }
    tasks:
    - debug:
    msg: "{{ domain_definition | json_query('domain.cluster[*].name') | string }}" # "['cluster1', 'cluster2']"
    - debug:
    msg: "{{ domain_definition | json_query('domain.server[?cluster==`cluster1`].port') | string }}" # "['8080', '8090']"
    - debug:
    msg: "{{ domain_definition | json_query('domain.server[?cluster==`cluster2`].{name: name, port: port}') | string }}" # "[{'name': 'server21', 'port': '9080'}, {'name': 'server22', 'port': '9090'}]"
    - debug:
    msg: "{{ domain_definition | json_query('domain.server[?starts_with(name, `server1`)].port') | string }}" # "['8080', '8090']"
    - debug:
    msg: "{{ domain_definition | json_query('domain.server[?contains(name, `server1`)].port') | string }}" # "['8080', '8090']"
  11. random生成随机数。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #filters.yaml
    - name: Example for random
    hosts: local
    gather_facts: no
    tasks:
    - debug:
    msg: "{{ ['a','b','c'] | random }}" # "msg": "a"
    - debug:
    msg: "{{ 60 | random }}" # "msg": "39"
    - debug:
    msg: "{{ 101 | random(step=10) }}" # "msg": "80"
    - debug:
    msg: "{{ 101 | random(start=1, step=10) }}" # "msg": "21"
    - debug:
    msg: "{{ 60 | random(seed=inventory_hostname) }}" # "msg": "42"
  12. shuffle可以打乱列表。 {{ ['a','b','c'] | shuffle(seed=inventory_hostname) }}
  13. max, min, flatten处理列表。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #filters.yaml
    - name: Example for list management
    hosts: local
    gather_facts: no
    vars:
    nums: [1, 2, 3]
    nested_nums: [1, 2, 3, [4, 5]]
    dict_list: [{'val': 1}, {'val': 2}]
    tasks:
    - debug:
    msg: "{{ nums | min }}" # "1"
    - debug:
    msg: "{{ nums | max }}" # "3"
    - debug:
    msg: "{{ dict_list | max(attribute='val') | string }}" # "{'val': 2}"
    - debug:
    msg: "{{ nested_nums | flatten(levels=1) | string }}" # "[1, 2, 3, 4, 5]"
  14. 使用unique, union, intersect, difference, symmetric_difference给列表做集合运算。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #filters.yaml
    - name: Example for sets
    hosts: local
    gather_facts: no
    vars:
    list1: [1, 2, 3, 4, 5, 5, 4]
    list2: [1, 2, 6, 7, 8]
    tasks:
    - debug:
    msg: "{{ list1 | unique | string }}" # "[1, 2, 3, 4, 5]"
    - debug:
    msg: "{{ list1 | union(list2) | string }}" # "[1, 2, 3, 4, 5, 6, 7, 8]"
    - debug:
    msg: "{{ list1 | intersect(list2) | string }}" # "[1, 2]"
    - debug:
    msg: "{{ list1 | difference(list2) | string }}" # "[3, 4, 5]"
    - debug:
    msg: "{{ list1 | symmetric_difference(list2) | string }}" # "[3, 4, 5, 6, 7, 8]"
  15. 使用ipaddr, ipv4, ipv6处理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
    #filters.yaml
    - name: Example for IP address
    hosts: local
    gather_facts: no
    vars:
    s: "hello9.128.1.1world"
    invalid_ip: "9.1.1.314"
    ip: "127.0.0.1"
    cidr: "192.0.2.1/24"
    tasks:
    - debug:
    msg: "{{ s | ansible.utils.ipaddr }}" # false
    - debug:
    msg: "{{ s | ansible.utils.ipv4 }}" # false
    - debug:
    msg: "{{ s | ansible.utils.ipv6 }}" # false
    - debug:
    msg: "{{ s | ansible.utils.ipaddr('address') }}" # false
    - debug:
    msg: "{{ invalid_ip | ansible.utils.ipaddr }}" # false
    - debug:
    msg: "{{ cidr | ansible.utils.ipaddr('address') }}" # "192.0.2.1"
    - debug:
    msg: "{{ ip | ansible.utils.ipv4 }}" # "127.0.0.1"
    - debug:
    msg: "{{ ip | ansible.utils.ipaddr('address') }}" # "127.0.0.1"
  16. 使用hash, checksum, password_hash加密字符串和密码。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # playbook filters.yaml
    - name: Example for hash, checksum, password_hash
    hosts: local
    gather_facts: no
    vars:
    pwd: "pass4root"
    tasks:
    - debug:
    msg: "{{ pwd | hash('sha1') }}" # "0754b1729f43eb5e37007610e28a47bd60531b97"
    - debug:
    msg: "{{ pwd | hash('md5') }}" # "f2feffd19d83fdac11a2e29c3e4d7fc6"
    - debug:
    msg: "{{ pwd | checksum }}" # "0754b1729f43eb5e37007610e28a47bd60531b97"
    - debug:
    msg: "{{ pwd | password_hash('sha512') }}" # "$6$rounds=656000$p3CTKl4HE5zC297h$/7MyWoqFwsug5n0aFaGh3CK2utVkxiuJW5wc04obh/NPPN2YiHe.XHlh4E/QXm4zNkR4zUg4zQEwzSCX03cLt/"
    - debug:
    msg: "{{ pwd | password_hash('sha256', 'mysecretsalt') }}" # "$5$rounds=535000$mysecretsalt$UXdCeKTnnbG1O6cMAPkfEve5t17.eExAsSfxGYmd7H/"
  17. 使用comment, urlencode, urlsplit处理字符串。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    # playbook filters.yaml
    - name: Example for `comment`, `urlencode`, `urlsplit`
    hosts: local
    gather_facts: no
    vars:
    s: "hello world"
    url: "http://user:password@www.example.com:9000/dir/index.html?query=term#fragment"
    crn: "crn:v2:bluemix:uuid:a/accountid"
    tasks:
    - debug:
    msg: "{{ s | comment(decoration='# ', prefix='######', postfix='######') }}" # "######\n# hello world\n######"
    - debug:
    msg: "{{ s | comment('xml') }}" # "<!--\n -\n - hello world\n -\n-->"
    - debug:
    msg: "{{ crn | urlencode }}" # "crn%3Av2%3Abluemix%3Auuid%3Aa/accountid"
    - debug:
    msg: "{{ url | urlsplit('hostname') }}" # "www.example.com"
    - debug:
    msg: "{{ url | urlsplit | string }}" # "{'fragment': 'fragment', 'hostname': 'www.example.com', 'netloc': 'user:password@www.example.com:9000', 'password': 'password', 'path': '/dir/index.html', 'port': 9000, 'query': 'query=term', 'scheme': 'http', 'username': 'user'}"
  18. 使用regex_search, regex_findall, regex_replace, regex_escape处理字符串。
    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
    # playbook filters.yaml
    - name: Example for `regex_search`, `regex_findall`, `regex_replace`, `regex_escape`
    hosts: local
    gather_facts: no

    tasks:
    - debug:
    msg: "{{ 'server1/database42' | regex_search('database[0-9]+') }}" # "database42"
    - debug:
    msg: "{{ 'foo\nBAR' | regex_search('^bar', multiline=True, ignorecase=True) }}" # "BAR"
    - debug:
    msg: "{{ 'server1/database42' | regex_search('server([0-9]+)/database([0-9]+)', '\\1', '\\2') | string }}" # "['1', '42']"
    - debug:
    msg: "{{ '21/42' | regex_search('(?P<dividend>[0-9]+)/(?P<divisor>[0-9]+)', '\\g<dividend>', '\\g<divisor>') | string }}" # "['21', '42']"
    - debug:
    msg: "{{ 'ansible' | regex_search('foobar') }}" # ''
    - debug:
    msg: "{{ 'ansible' | regex_search('foobar') == '' }}" # False
    - debug:
    msg: "{{ 'ansible' | regex_search('foobar') is none }}" # True
    - debug:
    msg: "{{ 'CAR\ntar\nfoo\nbar\n' | regex_findall('^.ar$', multiline=True, ignorecase=True) | string }}" # "['CAR', 'tar', 'bar']"
    - debug:
    msg: "{{ 'localhost:80' | regex_replace('^(?P<host>.+):(?P<port>\\d+)$', '\\g<host>, \\g<port>') }}" # 'localhost, 80'
    - debug:
    msg: "{{ '^f.*o(.*)$' | regex_escape() }}" # "\\^f\\.\\*o\\(\\.\\*\\)\\$"

  19. 使用basename, dirname, expanduser, expandvars, realpath, relpath, splitext, first, last, path_join处理文件路径。
    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
    # playbook filters.yaml
    - name: Example for `basename`, `dirname`, `expanduser`, `expandvars`, `realpath`, `relpath`, `splitext`, `first`, `last`, `path_join`
    hosts: local
    gather_facts: no
    vars:
    path: ~/sen/a.txt
    tasks:
    - debug:
    msg: "{{ path | basename }}" # "a.txt"
    - debug:
    msg: "{{ path | dirname }}" # "~/sen"
    - debug:
    msg: "{{ path | expanduser }}" # "/Users/senwang/sen/a.txt"
    - debug:
    msg: "{{ path | expandvars }}" # "~/sen/a.txt"
    - debug:
    msg: "{{ path | realpath }}" # "/Users/senwang/tools/ansible/~/sen/a.txt"
    - debug:
    msg: "{{ path | relpath('/etc') }}" # "../Users/senwang/tools/ansible/~/sen/a.txt"
    - debug:
    msg: "{{ path | splitext }}" # "('~/sen/a', '.txt')"
    - debug:
    msg: "{{ path | splitext | first }}" # "~/sen/a"
    - debug:
    msg: "{{ ('/etc', 'dir', 'subdir', 'a.txt') | path_join }}" # "/etc/dir/subdir/a.txt"
  20. 使用join, split, b64decode, b64encode处理字符串。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # playbook filters.yaml
    - name: Example for `join`, `split`, `b64decode`, `b64encode`
    hosts: local
    gather_facts: no
    vars:
    types: ['int', 'string', 'list', 'dict']
    tasks:
    - debug:
    msg: "{{ types | join(',') }}" # "int,string,list,dict"
    - debug:
    msg: "{{ types | join(',') | split(',') | string }}" # "['int', 'string', 'list', 'dict']"
    - debug:
    msg: "{{ 'hello world' | b64encode | b64decode }}" # "hello world"
  21. 使用to_uuid处理UUID。
    1
    2
    3
    4
    5
    6
    - name: Example for `to_uuid`
    hosts: local
    gather_facts: no
    tasks:
    - debug:
    msg: "{{ 'string' | to_uuid }}" # "c7e8b99c-6668-5b4d-bdf9-26f4c0da60e5"
  22. 使用to_datetime, strftime处理时间和日期。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # playbook filters.yaml
    - name: Example for `to_datetime`, `strftime`
    hosts: local
    gather_facts: no
    tasks:
    - debug:
    msg: "{{ (('2016-08-14 20:00:12' | to_datetime) - ('2015-12-25' | to_datetime('%Y-%m-%d'))).total_seconds() }}" # "20203212.0"
    - debug:
    msg: '{{ (("2016-08-14 20:00:12" | to_datetime) - ("2016-08-14 18:00:00" | to_datetime)).seconds }}' # "7212"
    - debug:
    msg: '{{ (("2016-08-14 20:00:12" | to_datetime) - ("2015-12-25" | to_datetime("%Y-%m-%d"))).days }}' # "233"
    - debug:
    msg: "{{ '%H:%M:%S' | strftime }}" # "19:42:01"
    - debug:
    msg: "{{ '%Y-%m-%d %H:%M:%S' | strftime(0) }}" # "1970-01-01 08:00:00"
    - debug:
    msg: "{{ '%Y-%m-%d' | strftime(1441357287) }}" # "2015-09-04"

如何在受控节点上以其他用户的身份执行命令?

可以在play或者task上通过以下配置切换用户身份来执行play或者task。

become: 设置为yes启用用户切换。
become_user: 指定要切换的用户名,如果不指定默认为root用户。
become_method: 指定become要使用的插件,如su, sudo等。

也可以在清单中为group或者节点设置become

  • ansible_become
  • ansible_become_user
  • ansible_become_password
  • ansible_become_method

如何在playbook中使用循环

  1. 使用loop遍历简单列表。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # playbook.yaml
    - name: List users
    hosts: local
    gather_facts: no
    tasks:
    - name: Show item.
    debug:
    msg: "{{ item }}"
    loop:
    - user1
    - user2
  2. 使用loop遍历字典列表。
    1
    2
    3
    4
    5
    6
    7
    8
    - name: Add several users
    ansible.builtin.user:
    name: "{{ item.name }}"
    state: present
    groups: "{{ item.groups }}"
    loop:
    - { name: 'testuser1', groups: 'wheel' }
    - { name: 'testuser2', groups: 'root' }
  3. 使用loopdict2items遍历字典。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    - name: Example for loop dict
    hosts: local
    gather_facts: no
    tasks:
    - name: Using dict2items
    ansible.builtin.debug:
    msg: "{{ item.key }} - {{ item.value }}"
    loop: "{{ tag_data | dict2items }}"
    vars:
    tag_data:
    Environment: dev
    Application: payment
    1
    2
    3
    4
    5
    6
    ok: [localhost] => (item={'key': 'Environment', 'value': 'dev'}) => {
    "msg": "Environment - dev"
    }
    ok: [localhost] => (item={'key': 'Application', 'value': 'payment'}) => {
    "msg": "Application - payment"
    }
  4. 使用register将loop的结果保存到变量。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    - name: Example for loop register
    hosts: local
    gather_facts: no
    tasks:
    - name: Register loop output as a variable
    ansible.builtin.shell: "echo {{ item }}"
    loop:
    - "one"
    - "two"
    register: echo
    - name: Print echo
    debug:
    var: echo
    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
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    ok: [localhost] => {
    "echo": {
    "changed": true,
    "msg": "All items completed",
    "results": [
    {
    "ansible_facts": {
    "discovered_interpreter_python": "/opt/homebrew/bin/python3.10"
    },
    "ansible_loop_var": "item",
    "changed": true,
    "cmd": "echo one",
    "delta": "0:00:00.007707",
    "end": "2022-11-20 20:24:53.768577",
    "failed": false,
    "invocation": {
    "module_args": {
    "_raw_params": "echo one",
    "_uses_shell": true,
    "argv": null,
    "chdir": null,
    "creates": null,
    "executable": null,
    "removes": null,
    "stdin": null,
    "stdin_add_newline": true,
    "strip_empty_ends": true,
    "warn": false
    }
    },
    "item": "one",
    "msg": "",
    "rc": 0,
    "start": "2022-11-20 20:24:53.760870",
    "stderr": "",
    "stderr_lines": [],
    "stdout": "one",
    "stdout_lines": [
    "one"
    ]
    },
    {
    "ansible_loop_var": "item",
    "changed": true,
    "cmd": "echo two",
    "delta": "0:00:00.007317",
    "end": "2022-11-20 20:24:53.973643",
    "failed": false,
    "invocation": {
    "module_args": {
    "_raw_params": "echo two",
    "_uses_shell": true,
    "argv": null,
    "chdir": null,
    "creates": null,
    "executable": null,
    "removes": null,
    "stdin": null,
    "stdin_add_newline": true,
    "strip_empty_ends": true,
    "warn": false
    }
    },
    "item": "two",
    "msg": "",
    "rc": 0,
    "start": "2022-11-20 20:24:53.966326",
    "stderr": "",
    "stderr_lines": [],
    "stdout": "two",
    "stdout_lines": [
    "two"
    ]
    }
    ],
    "skipped": false,
    "warnings": [
    "Platform darwin on host localhost is using the discovered Python interpreter at /opt/homebrew/bin/python3.10, but future installation of another Python interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-core/2.13/reference_appendices/interpreter_discovery.html for more information."
    ]
    }
    }
  5. 使用until重试任务直到满足特定条件。
    1
    2
    3
    4
    5
    6
    - name: Retry a task until a certain condition is met
    ansible.builtin.shell: /usr/bin/foo
    register: result
    until: result.stdout.find("all systems go") != -1
    retries: 5
    delay: 10
  6. 使用loop遍历清单。query返回一个列表,lookup默认返回字符串,所以需要指定wantlist=True
    1
    2
    loop: "{{ query('inventory_hostnames', 'all') }}"
    loop: "{{ lookup('inventory_hostnames', 'all', wantlist=True) }}"
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    - name: Example for loop register
    hosts: local
    gather_facts: no
    tasks:
    - name: Show all the hosts in the inventory
    ansible.builtin.debug:
    msg: "{{ item }}"
    loop: "{{ groups['all'] }}"

    - name: Show all the hosts in the current play
    ansible.builtin.debug:
    msg: "{{ item }}"
    loop: "{{ ansible_play_batch }}"
    - name: Show all the hosts in the inventory
    ansible.builtin.debug:
    msg: "{{ item }}"
    loop: "{{ query('inventory_hostnames', 'all') }}"
    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
    TASK [Show all the hosts in the inventory] 
    ok: [localhost] => (item=master-node) => {
    "msg": "master-node"
    }
    ok: [localhost] => (item=node-1) => {
    "msg": "node-1"
    }
    ok: [localhost] => (item=localhost) => {
    "msg": "localhost"
    }

    TASK [Show all the hosts in the current play]
    ok: [localhost] => (item=localhost) => {
    "msg": "localhost"
    }

    TASK [Show all the hosts in the inventory]
    ok: [localhost] => (item=master-node) => {
    "msg": "master-node"
    }
    ok: [localhost] => (item=node-1) => {
    "msg": "node-1"
    }
    ok: [localhost] => (item=localhost) => {
    "msg": "localhost"
    }
  7. 使用loop_control
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    - name: Example for loop register
    hosts: local
    gather_facts: no
    tasks:
    - name: Show all the hosts in the inventory
    ansible.builtin.debug:
    msg: "{{ it }}"
    loop: "{{ groups['all'] }}"
    loop_control:
    loop_var: it
    index_var: i
    label: "{{ i }}"
    pause: 3

如何在playbook中使用条件判断语句?

  • 可以使用when为task指定执行条件。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    - name: Example for when
    hosts: all
    gather_facts: no
    tasks:
    - name: list all files/dirs in $HOME directory.
    command: ls ~
    register: result
    - name: print result
    debug:
    var: result.stdout_lines
    when: result.stdout_lines | length > 5
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    TASK [list all files/dirs in $HOME directory.]
    changed: [localhost]
    changed: [node-1]
    changed: [master-node]

    skipping: [master-node]
    skipping: [node-1]
    ok: [localhost] => {
    "result.stdout_lines": [
    "Desktop",
    "Documents",
    "Downloads",
    "Library",
    "Movies",
    "Music",
    ...
    ]
    }
  • 也可以使用when来根据前一个任务的状态做不同的处理。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    - name: Example for when
    hosts: all
    gather_facts: no
    tasks:
    - name: list all files/dirs in $HOME directory.
    command: ls ~
    register: result
    - name: print result
    debug:
    var: result.stdout_lines
    when: result.stdout_lines | length > 5
    register: r
    - name: print succeeded
    debug:
    msg: "Task on {{ inventory_hostname }} succeeded."
    when: r is succeeded
    - name: print failed
    debug:
    msg: "Task on {{ inventory_hostname }} failed."
    when: r is failed
    - name: print skipped
    debug:
    msg: "Task on {{ inventory_hostname }} skipped."
    when: r is skipped
    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
    TASK [list all files/dirs in $HOME directory.]
    changed: [localhost]
    changed: [node-1]
    changed: [master-node]

    TASK [print result]
    skipping: [master-node]
    ok: [localhost] => {
    "result.stdout_lines": [
    "Desktop",
    "Documents",
    "Downloads",
    "Library",
    "Movies",
    "Music",
    "Pictures",
    "Public",
    "data",
    "go",
    "projects",
    "sdk",
    "tmp",
    "tools",
    "vpc"
    ]
    }
    skipping: [node-1]

    ok: [master-node] => {
    "msg": "Task on master-node succeeded."
    }
    ok: [node-1] => {
    "msg": "Task on node-1 succeeded."
    }
    ok: [localhost] => {
    "msg": "Task on localhost succeeded."
    }

    TASK [print failed]
    skipping: [master-node]
    skipping: [node-1]
    skipping: [localhost]

    TASK [print skipped]
    skipping: [localhost]
    ok: [master-node] => {
    "msg": "Task on master-node skipped."
    }
    ok: [node-1] => {
    "msg": "Task on node-1 skipped."
    }
  • whenimport_tasks: tasks.yaml一起使用时,tasks.yaml中的task也会判断when后面的条件。
  • wheninclude_tasks: tasks.yaml一起使用时,tasks.yaml中的task不会去做when判断。
  • 常用的用于条件测试的facts:
    fact description
    ansible_facts['distribution'] 操作系统发行版名称,如Redhat
    ansible_facts['distribution_major_version'] 操作系统发行版主版本号,如16
    ansible_facts['os_family'] 操作系统发行版家族,如RedhatDebian

如何写条件判断语句中测试?

  • 语法: variable is test_name 例如: result is failed
  • 可以用match, search, regex测试字符创。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    - name: Example for string test
    hosts: local
    connection: localhost
    gather_facts: no
    vars:
    url: "https://example.com/users/foo/resources/bar"
    tasks:
    - debug:
    msg: "url not match: false." # "url not match: false."
    when: url is not match("example.com/users/.*/resources") # 必须从字符串开始就匹配
    - debug:
    msg: "url match: true." # "url match: true."
    when: url is match("https://example.com/users/.*/resources")
    - debug:
    msg: "url search: true." # "url search: true."
    when: url is search("users/.*/resources/.*") # 不要求从字符串开始就匹配
    - debug:
    msg: "url regex: true" # "url regex: true"
    when: url is regex("example\.com/\w+/foo")
  • vault_encrypted测试变量是否vault的加密值。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    $ ansible-vault encrypt_string --vault-id dev --ask-vault-password --output password.yaml "pass4root" --name passwd
    $ cat password.yaml
    passwd: !vault |
    $ANSIBLE_VAULT;1.1;AES256
    62343061653433353330663564333233356439306535663837346631393737646538626164366664
    6532303034383336366530326438396536343232366131390a313863383336323838643863376663
    61383561323338353031363861633032616135316638393139616161306538333563363038653665
    3137616362386362380a643061326663396436393430343830653836636134626463613863613732
    6539%
    1
    2
    3
    4
    5
    6
    7
    - name: Example for vault_encrypted
    hosts: local
    gather_facts: no
    vars_files: password.yaml
    tasks:
    - debug:
    msg: '{{ (passwd is vault_encrypted) | ternary("Vault encrypted", "Not vault encrypted") }}' # "Vault encrypted"
  • truthyfalsy来判断布尔值。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    - name: Example for truthy and falsy
    hosts: local
    gather_facts: no
    tasks:
    - debug:
    msg: "{{ item }} is Truthy"
    when: item is truthy(convert_bool=True)
    loop: ["", 0, {}, [], "hello world", 1, {"key": "value"}, [1]]
    - debug:
    msg: "{{ item }} is Falsy"
    when: item is falsy(convert_bool=True)
    loop: ["", 0, {}, [], "hello world", 1, {"key": "value"}, [1]]
    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
    ok: [localhost] => (item=hello world) => {
    "msg": "hello world is Truthy"
    }
    ok: [localhost] => (item=1) => {
    "msg": "1 is Truthy"
    }
    ok: [localhost] => (item={'key': 'value'}) => {
    "msg": "{'key': 'value'} is Truthy"
    }
    ok: [localhost] => (item=[1]) => {
    "msg": "[1] is Truthy"
    }

    ok: [localhost] => (item=) => {
    "msg": " is Falsy"
    }
    ok: [localhost] => (item=0) => {
    "msg": "0 is Falsy"
    }
    ok: [localhost] => (item={}) => {
    "msg": "{} is Falsy"
    }
    ok: [localhost] => (item=[]) => {
    "msg": "[] is Falsy"
    }
  • 使用version比较版本号。可用的操作符包括<, lt, <=, le, >, gt, >=, ge, ==, =, eq, !=, <>, ne
    1
    2
    3
    4
    5
    6
    - name: Example for version
    hosts: master
    tasks:
    - debug:
    msg: "OS version {{ ansible_facts['distribution_version'] }} >= 1.0" # "OS version 22.04 >= 1.0"
    when: ansible_facts['distribution_version'] is version('1.0', '>=')
  • 使用supersetsubset来比较集合。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    - name: Example for superset and subset
    hosts: local
    gather_facts: no
    vars:
    a: [1,2,3,4,5]
    b: [2,3]
    tasks:
    - debug:
    msg: "A includes B" # "A includes B"
    when: a is superset(b)

    - debug:
    msg: "B is included in A" # "B is included in A"
    when: b is subset(a)
  • 使用allany判断列表里的值是否全部为truthy。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    - name: Example for all and any
    hosts: local
    gather_facts: no
    vars:
    mylist:
    - 1
    - "{{ 3 == 3 }}"
    - True
    myotherlist:
    - False
    - True
    tasks:

    - debug:
    msg: "all are true!" # "all are true!"
    when: mylist is all

    - debug:
    msg: "at least one is true" # "at least one is true"
    when: myotherlist is any
  • 使用directory, file, link, exists, abs, same_file, mount来判断路径。
  • 使用human_readablehuman_to_bytes转换字节数。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    - name: Example for human_readable and human_to_bytes
    hosts: local
    gather_facts: no
    tasks:
    - name: "Human Readable"
    assert: # "All assertions passed"
    that:
    - '"1.00 Bytes" == 1|human_readable'
    - '"1.00 bits" == 1|human_readable(isbits=True)'
    - '"10.00 KB" == 10240|human_readable'
    - '"97.66 MB" == 102400000|human_readable'
    - '"0.10 GB" == 102400000|human_readable(unit="G")'
    - '"0.10 Gb" == 102400000|human_readable(isbits=True, unit="G")'
    - name: "Human to Bytes"
    assert: # "All assertions passed"
    that:
    - "{{'0'|human_to_bytes}} == 0"
    - "{{'0.1'|human_to_bytes}} == 0"
    - "{{'0.9'|human_to_bytes}} == 1"
    - "{{'1'|human_to_bytes}} == 1"
    - "{{'10.00 KB'|human_to_bytes}} == 10240"
    - "{{ '11 MB'|human_to_bytes}} == 11534336"
    - "{{ '1.1 GB'|human_to_bytes}} == 1181116006"
    - "{{'10.00 Kb'|human_to_bytes(isbits=True)}} == 10240"
  • 使用string, mapping, iterable, sequence, integer, number, float判断数据类型。

如何获取用户输入?

Ansible可以在play中用vars_prompt获取用户输入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- name: Example for vars_prompt
hosts: local
gather_facts: no
vars_prompt:
- name: username
prompt: What's your user name?
private: false
- name: password
prompt: what's your password?
tasks:
- debug:
msg: Username is {{ username }}
- debug:
msg: Password is {{ password }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ ansible-playbook -i hosts.yaml playbook.yaml
What's your user name?: Sen Wang
what's your password?:

PLAY [Example for vars_prompt]

TASK [debug]
ok: [localhost] => {
"msg": "Username is Sen Wang"
}

TASK [debug]
ok: [localhost] => {
"msg": "Password is 123456"
}

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

什么是fact和magic variable?

fact:

  • Ansible facts是保存受控节点的系统信息的变量。在playbook中可以使用ansible_facts引用。
  • 可以在play中使用gather_facts: false来禁止搜集受控节点信息并赋值给ansible_facts
  • 可以在/etc/ansible/facts.d/x.factini文件中设置自定义fact。然后用ansible_local.x.section.var来引用。

Ansible magic variables是ansible的内置变量,用于保存ansible自身的一些信息。

  • hostvars: 保存所有受控节点的信息。
  • groups: 保存清单中所有的group信息。
  • group_names: 保存当前节点所在的所有组的信息。
  • inventory_hostname: 保存当前节点的主机名。
  • ansible_play_hosts: 保存当前play中所有active的主机名。
  • ansible_play_batch: 保存当前play中正在运行的一批主机名,主机的个数和serial一致。
  • ansible_playbook_python: 保存用于调用ansible命令行的python可执行文件路径。
  • inventory_dir: 保存清单文件所在目录的路径。
  • playbook_dir: 保存playbook所在目录的路径。
  • inventory_file: 保存清单文件的路径。
  • role_path: 只能在role中引用,保存当前role的路径。
  • ansible_check_mode: 是否在命令行中使用--check启用了check mode。
1
2
3
4
5
6
7
8
9
- name: Example for magic variables.
hosts: local
gather_facts: false
tasks:
- set_fact:
magic_var_list: "{{ [hostvars['master-node'].ansible_forks, groups, group_names, inventory_hostname, ansible_play_hosts, ansible_play_batch, ansible_playbook_python, inventory_dir, inventory_file, playbook_dir, ansible_check_mode] }}"
- debug:
msg: "{{ item }}"
loop: "{{ magic_var_list }}"
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
62
63
64
65
66
67
68
69
70
$ ansible-playbook -i hosts.yaml playbook.yaml

PLAY [Example for magic variables.]

TASK [set_fact]
ok: [localhost]

TASK [debug]
ok: [localhost] => (item=5) => {
"msg": 5
}
ok: [localhost] => (item={'all': ['master-node', 'node-1', 'localhost'], 'ungrouped': [], 'master': ['master-node'], 'nodes': ['node-1'], 'prod': ['master-node', 'node-1'], 'local': ['localhost']}) => {
"msg": {
"all": [
"master-node",
"node-1",
"localhost"
],
"local": [
"localhost"
],
"master": [
"master-node"
],
"nodes": [
"node-1"
],
"prod": [
"master-node",
"node-1"
],
"ungrouped": []
}
}
ok: [localhost] => (item=['local']) => {
"msg": [
"local"
]
}
ok: [localhost] => (item=localhost) => {
"msg": "localhost"
}
ok: [localhost] => (item=['localhost']) => {
"msg": [
"localhost"
]
}
ok: [localhost] => (item=['localhost']) => {
"msg": [
"localhost"
]
}
ok: [localhost] => (item=/opt/homebrew/Cellar/ansible/6.2.0/libexec/bin/python3.10) => {
"msg": "/opt/homebrew/Cellar/ansible/6.2.0/libexec/bin/python3.10"
}
ok: [localhost] => (item=/Users/senwang/tools/ansible) => {
"msg": "/Users/senwang/tools/ansible"
}
ok: [localhost] => (item=/Users/senwang/tools/ansible/hosts.yaml) => {
"msg": "/Users/senwang/tools/ansible/hosts.yaml"
}
ok: [localhost] => (item=/Users/senwang/tools/ansible) => {
"msg": "/Users/senwang/tools/ansible"
}
ok: [localhost] => (item=False) => {
"msg": false
}

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

如何使用role?

role将变量、task、文件、handlers及其他的ansible元素以一定的目录结构组织起来,从而到达可重用及分享的目的。

role的目录结构如下:

  • tasks/main.yml: role执行的task都放在该目录下。
  • handlers/main.yml: 存放role的handlers。
  • library/my_module.py: 存放自定义的module。
  • defaults/main.yml: 存放默认变量。
  • vars/main.yml: 存放其他变量。
  • files/main.yml: 存放文件。
  • templates/main.yml: 存放templates。
  • meta/main.yml: 存放role的metadata,包括role的依赖以及Galaxy的metadata。role所依赖的其他role会在这里指定,并且优先于role定义的task执行。
  • meta/argument_specs.yml: 如果role中有这个文件,那么在执行role时会先跑一个task来检查role的参数。

role必须包含至少一个上述的目录。

Ansible查找role的路径优先级如下:

  • collection
  • 和playbook同目录下的roles目录
  • roles_path指定的目录。默认值为:~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles
  • playbook同目录

用以下的方式引用role:

  • play level使用roles静态引入
  • tasks level使用include_role动态引入
  • tasks level使用import_role静态引入

引入role之后,play执行的顺序:

  • play中定义的pre_tasks。
  • pre_tasks notify的handlers。
  • 定义在roles中的role按顺序执行。
  • play定义的tasks。
  • role和task notify的handlers。
  • play中定义的post_tasks。
  • post_tasks notify的handlers。

如何理解动态的include和静态的import?

playbook可以使用include动态地引入task,rolevariable,也可使用import静态的引入playbook,roletask
所谓静态,指的是ansible在运行开始就把被import的playbook/task/role引入playbook中,其task跟普通的task类似。而用include动态引入task/role,则是在执行到include的时候,用一个task将被引入的task/role引入进来。这样导致了import和include有如下区别。

include import
类型 动态 静态
处理时机 运行时遇到的时候处理 解析playbook的时候预处理
是否可以引入playbook 只能引入task/role 可以引入playbook
task的选项 只作用于该task 作用于被引入的所有task
是否可以用于loop 可以 不可以
–list-tags/–list-tasks 被引入的task/tag不可见 可以列出
notify handler 被引入的handler/task只能被整体执行 可以单独notify被引入的某一个task/handler

如何在受控节点之外的节点上运行task?

在task中使用delegate_to可以将task委派给其他的节点执行。

1
2
3
4
5
6
7
8
9
10
11
- name: Example for delegate_to
hosts: master
gather_facts: no
tasks:
- name: list all files/dirs in $HOME directory.
command: 'ls'
register: result
delegate_to: node-1
- name: print result
ansible.builtin.debug:
var: result.stdout_lines # ["file_on_node_1", "snap"]

也可以使用local_action在控制节点上运行task。

1
2
3
4
5
6
7
8
9
10
- name: Example for delegate_to
hosts: master
gather_facts: no
tasks:
- name: list all files/dirs in $HOME directory.
local_action: command ls ~
register: result
- name: print result
debug:
var: result.stdout_lines # ["Desktop", "Documents", "Downloads", "Library"...]

ansible默认收集的是当前受控节点的facts,如果希望收集delegate_to节点的facts可以用delegate_facts: true设置。

如何使用handler?

  • handler是一种特殊的task,它只有被别的task使用notify通知且通知的task的状态为changed时才会运行。
  • 可以使用listen让多个handler监听同一个notify。
  • handler通常只在一组task或者role之后运行一次,即使它被多次notify。
  • 可以在task中使用meta: flush_handlers提前运行handler。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    - name: Example for handler
    hosts: local
    gather_facts: no
    vars:
    file_name: "a.txt"
    tasks:
    - name: Create a new file.
    file:
    path: "~/{{ file_name }}"
    state: touch
    notify:
    - Print file created
    handlers:
    - name: Print file created
    debug:
    msg: "File {{ file_name }} created." # File a.txt created.
    listen: Print message

如何处理错误?

  • 一个task在某个节点执行失败后(状态为failed),后续的task也不会在该节点上执行了。 可以使用ignore_errors: true来继续执行后续的task。
  • 使用ignore_unreachable: true来忽略网络错误。
  • 使用meta: clear_host_error来清空被标记为unreachable的节点,后续的task才能重新尝试去连接这些主机。
  • handler在task失败后就不会被触发,可以在play上指定force_handlers: True来强制运行handler。
  • 使用failed_whenchanged_when来定义task是成功还是失败。
  • 使用any_errors_fatal: true在第一个失败的任务后中断所有任务和play的执行。
  • max_fail_percentage: 30指定在30%的主机执行任务失败后终止后续任务和play的执行。

如何给受控节点设置环境变量?

可以在play,block,task级别使用environment给受控节点设置环境变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- name: Example for environment
hosts: master
gather_facts: no
tasks:
- name: print environment variable
command: echo $key
environment:
key: value
register: result
notify: Print result
handlers:
- name: Print result
debug:
var: result.stdout_lines # ["value"]

如何使用ansible模块?

Ansible module是最终跑在受控节点上的ansible组件,负责完成任务,维护受控节点状态并最终给控制节点返回结果。

  • 可以用$ ansible-doc -l查看所有的模块。
  • 可以在play, block, task level用module_defaults给ansible模块指定默认参数。
    1
    2
    3
    4
    5
    6
    7
    - hosts: local
    module_defaults:
    ansible.builtin.file:
    owner: root
    group: root
    mode: 0755
    ...
  • 可以在/etc/ansible/plugin_filters.yml文件中禁止ansible加载指定的模块。

什么是Ansible插件?

Ansible 插件(plugin)是对Ansible核心功能的增强,plugin可以分为如下几类:

  • Action plugins
  • Become plugins
  • Cache plugins
  • Callback plugins
  • Cliconf plugins
  • Connection plugins
  • Docs fragments
  • Filter plugins
  • Httpapi plugins
  • Inventory plugins
  • Lookup plugins
  • Modules
  • Module utilities
  • Netconf plugins
  • Shell plugins
  • Strategy plugins
  • Terminal plugins
  • Test plugins
  • Vars plugins

可以在这里获得所有插件的说明。

Collection是什么?

Collection是Ansible的分发格式,也就是所谓的包,用户可以把自己的role,playbook,module,plugin以collection的形式放在分发服务器上,供其他用户安装使用。可以在这里找到官方仓库的collection列表。
要使用一个collection,用户需要:

  1. 使用命令$ ansible-galaxy collection list查看已经安装的collection。
  2. 使用命令$ ansible-galaxy collection install space_name.collection_name安装一个collection。
  3. 安装完成之后就可以在playbook的task中以space_name.collection_name.module_name来使用了。