ansible入门指南 - playbook

发布时间 2023-08-18 16:23:29作者: Chinor

playbook

ansible playbook 提供了一种可重用的方式, 用来管理机器的目标状态. 官方提供了一些playbook的例子可供学习
playbook的功能:

  • 声明配置
  • 编排执行步骤
  • 同步或异步执行任务

ansible playbook推荐使用模块的全名, 例如使用 ansible.builtin.yum 而不是 yum, 因为模块的名称可能会重复

以下是官方playbook示例, 示例分为两个play
第一个play会在webservers主机上执行两个task, 把httpd服务通过yum更新到最新版本, 然后根据 /srv/httpd.j2模板生成 /etc/httpd.conf配置文件
第二个play会在daatbases主机执行两个task, 把postgresql升级到最新版, 然后启动postgresql服务

---
- name: Update web servers
  hosts: webservers
  remote_user: root

  tasks:
  - name: Ensure apache is at the latest version
    ansible.builtin.yum:
      name: httpd
      state: latest
  - name: Write the apache config file
    ansible.builtin.template:
      src: /srv/httpd.j2
      dest: /etc/httpd.conf

- name: Update db servers
  hosts: databases
  remote_user: root

  tasks:
  - name: Ensure postgresql is at the latest version
    ansible.builtin.yum:
      name: postgresql
      state: latest
  - name: Ensure that postgresql is started
    ansible.builtin.service:
      name: postgresql
      state: started

ansible playbook的执行具有幂等性, 即一个playbook无论执行多少次, 达到的结果都是一样的. 例如使用yum模块安装httpd服务,ansible会检查服务的状态, 如果httpd已经在服务器上安装好了, 那么这个task便不会执行.

ansible playbook校验

写好的ansible playbook可以使用工具进行语法检查, 此处以 ansible-lint为例

root@localhost ~ ansible-lint playbook.yaml
WARNING  Listing 3 violation(s) that are fatal
name[casing]: All names should start with an uppercase letter.
playbook.yaml:5 Task/Handler: ansible_facts.eno1.ipv4.address

yaml[comments]: Missing starting space in comment
playbook.yaml:7

yaml[empty-lines]: Too many blank lines (1 > 0)
playbook.yaml:9

Read documentation for instructions on how to ignore specific rule violations.

                Rule Violation Summary
 count tag               profile  rule associated tags
     1 yaml[comments]    basic    formatting, yaml
     1 yaml[empty-lines] basic    formatting, yaml
     1 name[casing]      moderate idiom

Failed: 3 failure(s), 0 warning(s) on 1 files. Last profile that met the validation criteria was 'min'.

更多工具可以参考 https://docs.ansible.com/ansible/latest/community/other_tools_and_programs.html#validate-playbook-tools

模板

ansible 使用 jinja2 渲染模板
官网给了一个使用模板的示例

# hostname.yaml
---
- name: Write hostname
  hosts: all
  gather_facts: false
  tasks:
  - name: write hostname using jinja2
    ansible.builtin.template:
       src: templates/test.j2
       dest: /tmp/hostname

# templates/test.j2
My name is {{ ansible_facts['hostname'] }}

创建上面两个文件, 然后运行 ansible-playbook hostname.yaml -i localhost,. 运行完成后, 会在目标主机创建文件 /tmp/hostname, 文件内容是 My name is 主机名, 即 template 模块把 templates/test.j2模板渲染为 /tmp/hostname文件, 然后上传到被控制主机上.

ansible_facts 的内容可以通过 ansible -i localhost, -m setup all 查看

过滤器

过滤器可以用来修改变量, 例如提取字符串, 数字取整等等. 过滤器有三种, jinja2的过滤器, python过滤器, ansible内置过滤器, 本篇大致介绍ansible内置过滤器, 具体的可以参考官方文档.

设置默认值

如果some_variable不存在, 设置默认值为5

{{ some_variable | default(5) }}

如果值为空字符串或者false, 但是值确实是存在的. 必须设置default的第二个参数为true才能使默认值生效.
下面的例子中, 会从环境变量中读取 MY_USER的值. 如果 MY_USER的值不存在, 会返回一个空值, 必须设置default的第二个参数为 true才能使默认值 admin生效.

{{ lookup('env', 'MY_USER') | default('admin', true) }}

default可以把模板的值变为可选值, 例如下面的例子

- name: Touch files with an optional mode
  ansible.builtin.file:
    dest: "{{ item.path }}"
    state: touch
    mode: "{{ item.mode | default(omit) }}"
  loop:
    - path: /tmp/foo
    - path: /tmp/bar
    - path: /tmp/baz
      mode: "0444"

运行改playbook会创建三个文件夹, 前两个文件夹由于没有设置mode, 系统会根据设置的umask创建文件, 最后一个文件会根据 mode 创建文件

设置必须的值

如果ansible配置 DEFAULT_UNDEFINED_VAR_BEHAVIOR 被设为 false, 那就会允许未定义的变量. 可以通过 mandatory设置变量为必须的.

{{ variable | mandatory }}

三元表达式

判断前面的表达式的结果, 如果为true, 则返回第一个参数, false则返回第二个参数, 如果为null返回第三个参数(ansible>2.8特性)

{{ (status == 'needs_restart') | ternary('restart', 'continue') }}

判断数据类型

{{ myvar | type_debug }}

字典转列表

{{ dict | dict2items }}

转换前

tags:
  Application: payment
  Environment: dev

转换后

- key: Application
  value: payment
- key: Environment
  value: dev

dict2item 过滤器可以指定转换后 keyvalue的名字

{{ files | dict2items(key_name='file', value_name='path') }}

转换前

files:
  users: /etc/passwd
  groups: /etc/group

转换后

- file: users
  path: /etc/passwd
- file: groups
  path: /etc/group

列表转字典

{{ tags | items2dict }}

转换前

tags:
  - key: Application
    value: payment
  - key: Environment
    value: dev

转换后

Application: payment
Environment: dev

类似地, 如果字典的key和value名称可以通过下面的方式指定

{{ tags | items2dict(key_name='fruit', value_name='color') }}

数据类型转换

可以用如下的方式实现json和yaml的互转

{{ some_variable | to_json }}
{{ some_variable | to_yaml }}
{{ some_variable | to_nice_json }}
{{ some_variable | to_nice_yaml }}

列表合并

用zip过滤器把两个列表合并, 和python的zip函数类似

- name: Give me list combo of two lists
  ansible.builtin.debug:
    msg: "{{ [1,2,3,4,5,6] | zip(['a','b','c','d','e','f']) | list }}"

# => [[1, "a"], [2, "b"], [3, "c"], [4, "d"], [5, "e"], [6, "f"]]

- name: Give me shortest combo of two lists
  ansible.builtin.debug:
    msg: "{{ [1,2,3] | zip(['a','b','c','d','e','f']) | list }}"

# => [[1, "a"], [2, "b"], [3, "c"]]

字典合并

{{ {'a':1, 'b':2} | combine({'b':3}) }}

合并后的结果为

{'a':1, 'b':3}

combine还可以接收两个参数, recursive嵌套的元素是否会被合并,默认为false. list_merge 合并选项, 默认为replace, 即相同key会被覆盖, 可选值还有 keep, append, prepend, append_rp, prepend_rp

笛卡尔积

product 可以计算两个数组的笛卡尔积

- name: Generate multiple hostnames
  ansible.builtin.debug:
    msg: "{{ ['foo', 'bar'] | product(['com']) | map('join', '.') | join(',') }}"

结果

{ "msg": "foo.com,bar.com" }

提取json数据的值

- name: Display all cluster names
  ansible.builtin.debug:
    var: item
  loop: "{{ domain_definition | community.general.json_query('domain.cluster[*].name') }}"

随机数据

# 随机mac地址
"{{ '52:54:00' | community.general.random_mac }}"
# => '52:54:00:ef:1c:03'
# 指定种子生成mac地址
"{{ '52:54:00' | community.general.random_mac(seed=inventory_hostname) }}"
# 列表随机选一个
"{{ ['a','b','c'] | random }}"
# => 'c'
# 指定范围内随机选
"{{ 60 | random }} * * * * root /script/from/cron"
# => '21 * * * * root /script/from/cron'
{{ 101 | random(step=10) }}
{{ 101 | random(start=1, step=10) }}

# 打乱列表
{{ ['a','b','c'] | shuffle }}
{{ ['a','b','c'] | shuffle(seed=inventory_hostname) }}

列表操作

# 取最小/最大值
{{ list1 | min }}
{{ [{'val': 1}, {'val': 2}] | min(attribute='val') }}
{{ [{'val': 1}, {'val': 2}] | max(attribute='val') }}

# 多层嵌套列表转单层
{{ [3, [4, 2] ] | flatten }}
# => [3, 4, 2]

# 指定层级
{{ [3, [4, [2]] ] | flatten(levels=1) }}
# => [3, 4, [2]]

# 保留空值
{{ [3, None, [4, [2]] ] | flatten(levels=1, skip_nulls=False) }}
# => [3, None, 4, [2]]

# 列表去重
# list1: [1, 2, 5, 1, 3, 4, 10]
{{ list1 | unique }}
# => [1, 2, 5, 3, 4, 10]

# 两个列表并集去重
{{ list1 | union(list2) }}

# 两个列表取交集
{{ list1 | intersect(list2) }}

# 两个列表取差集(在list1中且不在list2中)
# list1: [1, 2, 5, 1, 3, 4, 10]
# list2: [1, 2, 3, 4, 5, 11, 99]
{{ list1 | difference(list2) }}
# => [10]

# 两个列表的对称差
# list1: [1, 2, 5, 1, 3, 4, 10]
# list2: [1, 2, 3, 4, 5, 11, 99]
{{ list1 | symmetric_difference(list2) }}
# => [10, 11, 99]

数学运算

# 对数(e为底)
{{ 8 | log }}
# 对数(10为底)
{{ 8 | log(10) }}

# 指数
{{ 8 | pow(5) }}

# 根号
{{ 8 | root }}

网络相关过滤器

# 是否是合法IP
{{ myvar | ansible.netcommon.ipaddr }}
{{ myvar | ansible.netcommon.ipv4 }}
{{ myvar | ansible.netcommon.ipv6 }}

# CIDR 提取IP地址
{{ '192.0.2.1/24' | ansible.netcommon.ipaddr('address') }}

哈希/加密密码

{{ 'test1' | hash('sha1') }}
# => "b444ac06613fc8d63795be9ad0beaf55011936ac"
{{ 'passwordsaresecret' | password_hash('sha512') }}
# => "$6$UIv3676O/ilZzWEE$ktEfFF19NQPF2zyxqxGkAceTnbEgpEKuGBtk6MlU4v2ZorWaVQUMyurgmHCh2Fr4wpmQ/Y.AlXMJkRnIS4RfH/"
{{ 'secretpassword' | password_hash('sha256', 'mysecretsalt') }}
# => "$5$mysecretsalt$ReKNyDYjkKNqRVwouShhsEqZ3VOE8eoVO4exihOfvG4"
# 指定salt和rounds保证幂等性, 即每次生成的密码哈希相同
{{ 'secretpassword' | password_hash('sha256', 'mysecretsalt', rounds=5001) }}
# => "$5$rounds=5001$mysecretsalt$wXcTWWXbfcR8er5IVf7NuquLvnUA6s8/qdtOhAZ.xN."

操作字符串

注释

# 字符串加上备注
{{ "Plain style (default)" | comment }}
# 处理完成后变成 # Plain style (default)
# 备注支持多种风格
{{ "C style" | comment('c') }}
{{ "C block style" | comment('cblock') }}
{{ "Erlang style" | comment('erlang') }}
{{ "XML style" | comment('xml') }}
# 可以自定义注释符号
{{ "My Special Case" | comment(decoration="! ") }}
# 自定义注释的前缀后缀
{{ "Custom style" | comment('plain', prefix='#######\n#', postfix='#\n#######\n   ###\n    #') }}

# 为了让注释可读性更好, 可以在`ansible.cfg`中定义变量, 然后在playbook中作为注释添加到文件中
# 例如在ansible.cfg中添加如下变量
ansible_managed = This file is managed by Ansible.%n
  template: {file}
  date: %Y-%m-%d %H:%M:%S
  user: {uid}
  host: {host}
# 然后在playbook中使用comment过滤器
{{ ansible_managed | comment }}
# 可以产生如下效果
#
# This file is managed by Ansible.
#
# template: /home/ansible/env/dev/ansible_managed/roles/role1/templates/test.j2
# date: 2015-09-10 11:02:58
# user: ansible
# host: myhost
#

URL编码

{{ 'Trollhättan' | urlencode }}
# => 'Trollh%C3%A4ttan'
# 提取URL中的内容
{{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit('hostname') }}
# => 'www.acme.com'

{{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit('netloc') }}
# => 'user:password@www.acme.com:9000'

{{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit('username') }}
# => 'user'

{{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit('password') }}
# => 'password'

{{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit('path') }}
# => '/dir/index.html'

{{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit('port') }}
# => '9000'

{{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit('scheme') }}
# => 'http'

{{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit('query') }}
# => 'query=term'

{{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit('fragment') }}
# => 'fragment'

{{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit }}
# =>
#   {
#       "fragment": "fragment",
#       "hostname": "www.acme.com",
#       "netloc": "user:password@www.acme.com:9000",
#       "password": "password",
#       "path": "/dir/index.html",
#       "port": 9000,
#       "query": "query=term",
#       "scheme": "http",
#       "username": "user"
#   }

正则表达式提取字符串

# Extracts the database name from a string
{{ 'server1/database42' | regex_search('database[0-9]+') }}
# => 'database42'

# Example for a case insensitive search in multiline mode
{{ 'foo\nBAR' | regex_search('^bar', multiline=True, ignorecase=True) }}
# => 'BAR'

# Example for a case insensitive search in multiline mode using inline regex flags
{{ 'foo\nBAR' | regex_search('(?im)^bar') }}
# => 'BAR'

# Extracts server and database id from a string
{{ 'server1/database42' | regex_search('server([0-9]+)/database([0-9]+)', '\\1', '\\2') }}
# => ['1', '42']

# Extracts dividend and divisor from a division
{{ '21/42' | regex_search('(?P<dividend>[0-9]+)/(?P<divisor>[0-9]+)', '\\g<dividend>', '\\g<divisor>') }}
# => ['21', '42']

regex_search 找不到的时候会返回空字符串'', 但是在和操作符连用的使用返回none

=> False

{{ 'ansible' | regex_search('foobar') is none }}

=> True

提取所有子串

# Returns a list of all IPv4 addresses in the string
{{ 'Some DNS servers are 8.8.8.8 and 8.8.4.4' | regex_findall('\\b(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\\b') }}
# => ['8.8.8.8', '8.8.4.4']

# Returns all lines that end with "ar"
{{ 'CAR\ntar\nfoo\nbar\n' | regex_findall('^.ar$', multiline=True, ignorecase=True) }}
# => ['CAR', 'tar', 'bar']

# Returns all lines that end with "ar" using inline regex flags for multiline and ignorecase
{{ 'CAR\ntar\nfoo\nbar\n' | regex_findall('(?im)^.ar$') }}
# => ['CAR', 'tar', 'bar']

替换匹配的字符串

# Convert "ansible" to "able"
{{ 'ansible' | regex_replace('^a.*i(.*)$', 'a\\1') }}
# => 'able'

# Convert "foobar" to "bar"
{{ 'foobar' | regex_replace('^f.*o(.*)$', '\\1') }}
# => 'bar'

# Convert "localhost:80" to "localhost, 80" using named groups
{{ 'localhost:80' | regex_replace('^(?P<host>.+):(?P<port>\\d+)$', '\\g<host>, \\g<port>') }}
# => 'localhost, 80'

# Convert "localhost:80" to "localhost"
{{ 'localhost:80' | regex_replace(':80') }}
# => 'localhost'

# Comment all lines that end with "ar"
{{ 'CAR\ntar\nfoo\nbar\n' | regex_replace('^(.ar)$', '#\\1', multiline=True, ignorecase=True) }}
# => '#CAR\n#tar\nfoo\n#bar\n'

# Comment all lines that end with "ar" using inline regex flags for multiline and ignorecase
{{ 'CAR\ntar\nfoo\nbar\n' | regex_replace('(?im)^(.ar)$', '#\\1') }}
# => '#CAR\n#tar\nfoo\n#bar\n'
# 列表拼成字符串, 同python的join
{{ list | join(" ") }}
# 字符串根据逗号分割, 同python的split
{{ csv_string | split(",") }}
# base64编码解码
{{ encoded | b64decode }}
{{ decoded | b64encode }}

文件名相关过滤器

# 提取路径的文件名
{{ path | basename }}
# 提取目录
{{ path | dirname }}
# 获取文件全路径
{{ path | dirname }}
# 分割文件名和后缀, 如果path=nginx.conf, 返回['nginx','.conf']
{{ path | splitext }}
# 返回文件名部分
{{ path | splitext | first }}
# 返回后缀部分
{{ path | splitext | last }}
# 路径组合
{{ ('/etc', path, 'subdir', file) | path_join }}

时间/日期过滤器

# Get total amount of seconds between two dates. Default date format is %Y-%m-%d %H:%M:%S but you can pass your own format
{{ (("2016-08-14 20:00:12" | to_datetime) - ("2015-12-25" | to_datetime('%Y-%m-%d'))).total_seconds()  }}

# Get remaining seconds after delta has been calculated. NOTE: This does NOT convert years, days, hours, and so on to seconds. For that, use total_seconds()
{{ (("2016-08-14 20:00:12" | to_datetime) - ("2016-08-14 18:00:00" | to_datetime)).seconds  }}
# This expression evaluates to "12" and not "132". Delta is 2 hours, 12 seconds

# get amount of days between two dates. This returns only number of days and discards remaining hours, minutes, and seconds
{{ (("2016-08-14 20:00:12" | to_datetime) - ("2015-12-25" | to_datetime('%Y-%m-%d'))).days  }}

# Display year-month-day
{{ '%Y-%m-%d' | strftime }}
# => "2021-03-19"

# Display hour:min:sec
{{ '%H:%M:%S' | strftime }}
# => "21:51:04"

# Use ansible_date_time.epoch fact
{{ '%Y-%m-%d %H:%M:%S' | strftime(ansible_date_time.epoch) }}
# => "2021-03-19 21:54:09"

# Use arbitrary epoch value
{{ '%Y-%m-%d' | strftime(0) }}          # => 1970-01-01
{{ '%Y-%m-%d' | strftime(1441357287) }} # => 2015-09-04

{{ '%H:%M:%S' | strftime }}           # time now in local timezone
{{ '%H:%M:%S' | strftime(utc=True) }} # time now in UTC

测试

jinja的测试用来评估模板的表达式是否正确. 测试返回True和False.
测试when的条件是否满足

vars:
  url: "https://example.com/users/foo/resources/bar"

tasks:
    - debug:
        msg: "matched pattern 1"
      when: url is match("https://example.com/users/.*/resources")

    - debug:
        msg: "matched pattern 2"
      when: url is search("users/.*/resources/.*")

    - debug:
        msg: "matched pattern 3"
      when: url is search("users")

    - debug:
        msg: "matched pattern 4"
      when: url is regex("example\.com/\w+/foo")

检查变量是否被vault加密过

vars:
  variable: !vault |
    $ANSIBLE_VAULT;1.2;AES256;dev
    61323931353866666336306139373937316366366138656131323863373866376666353364373761
    3539633234313836346435323766306164626134376564330a373530313635343535343133316133
    36643666306434616266376434363239346433643238336464643566386135356334303736353136
    6565633133366366360a326566323363363936613664616364623437336130623133343530333739
    3039

tasks:
  - debug:
      msg: '{{ (variable is vault_encrypted) | ternary("Vault encrypted", "Not vault encrypted") }}'

检查上一个task是否成功

tasks:

  - shell: /usr/bin/foo
    register: result
    ignore_errors: True

  - debug:
      msg: "it failed"
    when: result is failed

lookups

lookups插件用来从外部数据源检索数据. 数据源可以是文件 / 数据库 / API 或者其他服务.
可以执行以下命令查看支持的lookup插件

ansible-doc -l -t lookup

loops

ansible 提供了 loop, with_<lookup>until 关键字来循环执行任务.

举个例子, 使用playbook来循环创建多个用户

- name: Add several users
  ansible.builtin.user:
    name: "{{ item }}"
    state: present
    groups: "wheel"
  loop:
     - testuser1
     - testuser2
# 用户列表也可以保存在列表中, 然后使用下面的方式使用loop
# loop: "{{ somelist }}"

有些模块可以直接接受列表作为参数, 这种情况就没必要使用loop. 例如yum模块

如果遍历的是字典列表, 可以用下面的方式使用item获取值

- name: Add several users
  ansible.builtin.user:
    name: "{{ item.name }}"
    state: present
    groups: "{{ item.groups }}"
  loop:
    - { name: 'testuser1', groups: 'wheel' }
    - { name: 'testuser2', groups: 'root' }

如果遍历字典类型, 可以使用 dict2items 过滤器, 把字典转为列表再做处理

- name: Using dict2items
  ansible.builtin.debug:
    msg: "{{ item.key }} - {{ item.value }}"
  loop: "{{ tag_data | dict2items }}"
  vars:
    tag_data:
      Environment: dev
      Application: payment

loop 的参数必须是列表, lookup的返回值默认是逗号分割的字符串, 如果需要返回字符串需要加wantlist=True. ansible2.5新增了query函数返回字符串.

以下的两种loop方式等价

loop: "{{ query('inventory_hostnames', 'all') }}"

loop: "{{ lookup('inventory_hostnames', 'all', wantlist=True) }}"

loop_control 可以控制loop中的动作
例如使用pause控制在loop中间暂停多少秒

- name: Create servers, pause 3s before creating next
  community.digitalocean.digital_ocean:
    name: "{{ item }}"
    state: present
  loop:
    - server1
    - server2
  loop_control:
    pause: 3

为每次loop添加索引变量

- name: Count our fruit
  ansible.builtin.debug:
    msg: "{{ item }} with index {{ my_idx }}"
  loop:
    - apple
    - banana
    - pear
  loop_control:
    index_var: my_idx

loop 还可以用 with_* 替代, 更多文档参考https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_loops.html#migrating-from-with-x-to-loop

控制task运行机器

默认情况下ansible在匹配hosts的机器上运行, 有时候可能也需要委派其他机器运行, 例如更新webserver时, 需要临时从负载均衡上移除节点, 需要控制机去执行, 这时候可以用委派功能
下面是一个委派运行任务的例子

---
- hosts: webservers
  serial: 5

  tasks:
    - name: Take out of load balancer pool
      ansible.builtin.command: /usr/bin/take_out_of_pool {{ inventory_hostname }}
      delegate_to: 127.0.0.1

    - name: Actual steps would go here
      ansible.builtin.yum:
        name: acme-web-stack
        state: latest

    - name: Add back to load balancer pool
      ansible.builtin.command: /usr/bin/add_back_to_pool {{ inventory_hostname }}
      delegate_to: 127.0.0.1

如果是本地运行, 可以使用下面的方式简写

  tasks:
    - name: Take out of load balancer pool
      local_action: ansible.builtin.command /usr/bin/take_out_of_pool {{ inventory_hostname }}

或者使用下面的方式添加更多的参数

  tasks:
    - name: Send summary mail
      local_action:
        module: community.general.mail
        subject: "Summary Mail"
        to: "{{ mail_recipient }}"
        body: "{{ mail_body }}"
      run_once: True

使用委派的时候, ansible_host 之类的参数会指向被委派任务的机器, inventory_hostname 是原来执行任务的机器

由于ansible是并行运行的, 当localhost被委派多个任务的时候, 可能会产生冲突, 比如同时写入同一个文件, 可以使用下面的方式避免 run_once+loop

- name: "handle concurrency with a loop on the hosts with `run_once: true`"
  lineinfile: "<options here>"
  run_once: true
  loop: '{{ ansible_play_hosts_all }}'

条件运行

只有条件满足才会运行task, 例如下面的例子:只在SELinux开启的机器上安装MySQL

tasks:
  - name: Configure SELinux to start mysql on any port
    ansible.posix.seboolean:
      name: mysql_connect_any
      state: true
      persistent: true
    when: ansible_selinux.status == "enabled"

条件语句的中变量不需要双花括号

whenloop 联合使用

tasks:
    - name: Run with items greater than 5
      ansible.builtin.command: echo {{ item }}
      loop: [ 0, 2, 4, 6, 8, 10 ]
      when: item > 5

whenrole结合使用

- hosts: webservers
  roles:
     - role: debian_stock_config
       when: ansible_facts['os_family'] == 'Debian'

block

block可以把多个task分为一组, 这样同样的参数可以对多个task生效

 tasks:
   - name: Install, configure, and start Apache
     block:
       - name: Install httpd and memcached
         ansible.builtin.yum:
           name:
           - httpd
           - memcached
           state: present

       - name: Apply the foo config template
         ansible.builtin.template:
           src: templates/src.j2
           dest: /etc/foo.conf

       - name: Start service bar and enable it
         ansible.builtin.service:
           name: bar
           state: started
           enabled: True
     when: ansible_facts['distribution'] == 'CentOS'
     become: true
     become_user: root
     ignore_errors: true

block的异常处理

- name: Attempt and graceful roll back demo
  block:
    - name: Print a message
      ansible.builtin.debug:
        msg: 'I execute normally'

    - name: Force a failure
      ansible.builtin.command: /bin/false

    - name: Never print this
      ansible.builtin.debug:
        msg: 'I never execute, due to the above task failing, :-('
  rescue:
    - name: Print when errors
      ansible.builtin.debug:
        msg: 'I caught an error'

    - name: Force a failure in middle of recovery! >:-)
      ansible.builtin.command: /bin/false

    - name: Never print this
      ansible.builtin.debug:
        msg: 'I also never execute :-('
  always:
    - name: Always do this
      ansible.builtin.debug:
        msg: "This always executes"

handlers

handlers用于在变更发生以后, 再执行的命令

---
- name: Verify apache installation
  hosts: webservers
  vars:
    http_port: 80
    max_clients: 200
  remote_user: root
  tasks:
    - name: Ensure apache is at the latest version
      ansible.builtin.yum:
        name: httpd
        state: latest

    - name: Write the apache config file
      ansible.builtin.template:
        src: /srv/httpd.j2
        dest: /etc/httpd.conf
      notify:
      - Restart apache

    - name: Ensure apache is running
      ansible.builtin.service:
        name: httpd
        state: started

  handlers:
    - name: Restart apache
      ansible.builtin.service:
        name: httpd
        state: restarted

handlers也可以被task指定触发

tasks:
- name: Template configuration file
  ansible.builtin.template:
    src: template.j2
    dest: /etc/foo.conf
  notify:
    - Restart apache
    - Restart memcached

handlers:
  - name: Restart memcached
    ansible.builtin.service:
      name: memcached
      state: restarted

  - name: Restart apache
    ansible.builtin.service:
      name: apache
      state: restarted

listen可以指定监听哪个notify

tasks:
  - name: Restart everything
    command: echo "this task will restart the web services"
    notify: "restart web services"

handlers:
  - name: Restart memcached
    service:
      name: memcached
      state: restarted
    listen: "restart web services"

  - name: Restart apache
    service:
      name: apache
      state: restarted
    listen: "restart web services"

环境变量

task运行的节点上可以设置环境变量

- hosts: all
  remote_user: root
  tasks:

    - name: Install cobbler
      ansible.builtin.package:
        name: cobbler
        state: present
      environment:
        http_proxy: http://proxy.example.com:8080

重用ansible组件

playbook可以是一个包含变量, play, 和任务的文件, 也可以把变量, 任务等分配到不同的文件中, 便于在其他的任务中重用

ansible可重用的组件包括 variable, tasks, playbooks, roles.

通过 import_playbook导入playbook

- import_playbook: webservers.yml
- import_playbook: databases.yml

import_playbook的值可以使用变量

- import_playbook: "/path/to/{{ import_from_extra_var }}"
- import_playbook: "{{ import_from_vars }}"
  vars:
    import_from_vars: /path/to/one_playbook.yml

静态重用 import_* 和 动态重用 include_*
动态重用 include引入的任务会不会运行取决于上级的tasks. 只要include一触发, task, role会自动运行
静态重用 import会把tasks, playbook静态添加到playbook中. 在playbook运行前完成import. 所以如果import的文件名如果使用了一个变量, 那么这个变量必须是在运行时已经定下来了, 而不是运行playbook过程中生成的.

include 和 import对比

``Include_* Import_*
Type of re-use Dynamic
When processed At runtime, when encountered
Task or play All includes are tasks
Task options Apply only to include task itself
Calling from loops Executed once for each loop item
Using --list-tags Tags within includes not listed
Using --list-tasks Tasks within includes not listed
Notifying handlers Cannot trigger handlers within includes
Using --start-at-task Cannot start at tasks within includes
Using inventory variables Can include_*{{inventory_var}}
With playbooks No include_playbook
With variables files Can include variables files

以下的例子可以阐明include和import的区别

# 创建restarts.yml文件
- name: Restart apache
  ansible.builtin.service:
    name: apache
    state: restarted

- name: Restart mysql
  ansible.builtin.service:
    name: mysql
    state: restarted

使用include的方式重启服务, 只需要调用handler的名字就行

- name: Trigger an included (dynamic) handler
  hosts: localhost
  handlers:
    - name: Restart services
      include_tasks: restarts.yml
  tasks:
    - command: "true"
      notify: Restart services

使用import的方式重启服务, 需要分别调用 restarts.yaml的两个task

- name: Trigger an imported (static) handler
  hosts: localhost
  handlers:
    - name: Restart services
      import_tasks: restarts.yml
  tasks:
    - command: "true"
      notify: Restart apache
    - command: "true"
      notify: Restart mysql

Role

Role是task, vars, handlers等文件的集合. 把文件按照Role的方式组织, 可以更方便地重用和与其他人分享.
Role一般包含以下文件

  • tasks/main.yml - 需要执行的task列表
  • handlers/main.yml - 定义handlers
  • library/my_module.py - 自定义的模块
  • defaults/main.yml - 设置变量的默认值, 优先级最低, 会被覆盖
  • vars/main.yml - 其他变量
  • files/main.yml - 运行ansible task依赖的文件, 例如需要拷贝到被控主机的文件
  • templates/main.yml - 模板文件
  • meta/main.yml - role的元数据, 包括依赖 / 平台等信息.

ansible会从下面的地方检索role

  • playbook同目录下的 roles文件夹中
  • 配置的role_path中, 默认是 ~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles
  • playbook文件所在目录

role也可以在playbook中通过如下方式定义

---
- hosts: webservers
  roles:
    - role: '/path/to/my/roles/common'
Using roles

使用role的三种方式

  • 在playbook的roles下面
  • 使用 include_role
  • 使用 import_role

role默认情况下只会运行一次, 如果需要运行role多次, 可以通过如下的方式

---
- hosts: webservers
  roles:
    - { role: foo, message: "first" }
    - { role: foo, message: "second" }
---
# 或者下面的另一种写法
---
- hosts: webservers
  roles:
    - role: foo
      message: "first"
    - role: foo
      message: "second"

可以添加属性 allow_duplicates: truemeta/main.yml 文件中, 这样同名的role也可以运行多次

module_default

如果需要多次调用同样的module, 并且传递的参数也相同, 可以用 module_defaults 指定默认的参数

- hosts: localhost
  module_defaults:
    ansible.builtin.file:
      owner: root
      group: root
      mode: 0755

交互式输入: prompts

---
- hosts: all
  vars_prompt:

    - name: username
      prompt: What is your username?
      private: false

    - name: password
      prompt: What is your password?

  tasks:

    - name: Print a message
      ansible.builtin.debug:
        msg: 'Logging in as {{ username }}'

输入的密码加密

vars_prompt:

  - name: my_password2
    prompt: Enter password2
    private: true
    encrypt: sha512_crypt
    confirm: true
    salt_size: 7
    unsafe: true 
    # 如果输入的值包含 `{} 和 %`等模板关键字, 会导致异常, 使用unsafe就可以接收这些特殊字符

ansible中的变量

ansible变量名只能包含数字,字符和下划线
变量可以直接在playbook中声明, 也可以在执行命令的时候通过如下方式导入

ansible-playbook release.yml --extra-vars "version=1.23.45 other_variable=foo"
ansible-playbook release.yml --extra-vars '{"version":"1.23.45","other_variable":"foo"}'
# 从文件引入变量
ansible-playbook release.yml --extra-vars "@some_file.json"
ansible-playbook release.yml --extra-vars "@some_file.yaml"

如果在不同的地方,定义了同一个变量, 那么哪里定义的会生效? , 变量的优先级可以参考 https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_variables.html#id42

playbook验证

验证playbook一般有两种方式, check模式和diff模式.
check模式不会再远程机器上产生任何变动, 支持check模式的模块会报告这次执行会产生什么结果. 不支持check模式的模块不会报告, 也不会执行
diff模式会报告前后的对比
这两种模式可以结合使用

使用下面的方式, 以check模式运行playbook

ansible-playbook foo.yml --check

check模式例子

- hosts: all
  gather_facts: false
  tasks:
    - name: This task will always make changes to the system
      ansible.builtin.command: echo --even-in-check-mode
      check_mode: false

    - name: This task will never make changes to the system
      ansible.builtin.lineinfile:
        line: "important config"
        dest: /tmp/myconfig.conf
        state: present
      register: changes_to_important_config
      check_mode: true

这个例子中, 在每个tasks中加了 check_mode 参数. 每个task中定义的check_mode优先级更高, 如果 ansible-playbook 执行时, 无论有没有加 --check参数, 都以具体的task定义为准

diff模式的例子

- hosts: all
  gather_facts: false
  tasks:
    - name: This task will always make changes to the system
      ansible.builtin.command: echo --even-in-check-mode

    - name: This task will never make changes to the system
      ansible.builtin.lineinfile:
        line: "important config"
        dest: /tmp/myconfig.conf
        state: present
      register: changes_to_important_config

将上面的内容保存为play.yaml, 然后运行 ansible-playbook -i localhost, play.yaml --diff --check. --diff打印变化的内容, --check跳过任务
运行的结果如下

PLAY [all] ******************************************************************************************************************************************************************************************************

TASK [This task will always make changes to the system] *********************************************************************************************************************************************************
skipping: [localhost]

TASK [This task will never make changes to the system] **********************************************************************************************************************************************************
--- before: /tmp/myconfig.conf (content)
+++ after: /tmp/myconfig.conf (content)
@@ -0,0 +1 @@
+important config

changed: [localhost]

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

check_mode属性类似, 可以添加 diff: false属性关闭 diff模式

运行时切换用户

ansible可以使用 become在执行任务的时候, 切换到root权限或者其他用户权限执行任务
例如下面的例子以nobody用户的身份执行任务

- name: Run a command as nobody
  command: somecommand
  become: true
  become_method: su
  become_user: nobody
  become_flags: '-s /bin/sh'

标签

标签功能用于给一部分的task打标签, 这样可以执行运行一个playbook中的部分task, 而不用大幅修改playbook.

示例

tasks:
- name: Install the servers
  ansible.builtin.yum:
    name:
    - httpd
    - memcached
    state: present
  tags:
  - packages
  - webservers

- name: Configure the service
  ansible.builtin.template:
    src: templates/src.j2
    dest: /etc/foo.conf
  tags:
  - configuration

运行的时候加上 --tags configuration 即可只运行 Configure the service任务

使用block给任务分组的时候, 可以使用tags来给整个block加标签, 这样就不用加到每个task上. tags也可以加到play/import上, 但是不能直接应用到include导入的task上, 可以通过如下方式使用 apply把标签加到include导入的task上

- name: Apply the db tag to the include and to all tasks in db.yml
  include_tasks:
    file: db.yml
    # adds 'db' tag to tasks within db.yml
    apply:
      tags: db
  # adds 'db' tag to this 'include_tasks' itself
  tags: db

ansible有两个保留的标签名称 alwaysnever
如果给任务加了 alwasy标签, 运行指定标签任务的时候, 除非显式声明 --skip-tags always, 否则一定会执行
如果给任务加了 never标签, 除非显式声明 --tags never, 否则不会执行

ansible-playbook 与标签相关的参数
-tags all - 运行所有任务, 无论有什么标签
--tags [tag1, tag2] - 只运行标记有tag1或tag2的任务
--skip-tags [tag3, tag4] - 运行没有tag3或tag4标签的任务
--tags tagged - 只运行有标签的任务
--tags untagged - 只运行没有标签的任务

其他标签相关参数

# 列出所有标签
ansible-playbook example.yml --list-tags
# 列除出指定标签的任务
ansible-playbook example.yml --tags "configuration,packages" --list-tasks

调试器

ansible调试器可以在运行task时调试, 打印变量, 修改变量等

例子

- name: Execute a command
  ansible.builtin.command: "false"
  debugger: on_failed

debugger的参数有 always, never, on_failed, on_unreachable, on_skipped

debugger提供的调试命令

Command Shortcut Action
print p 打印任务相关信息, 可以是变量等
task.args[key ] = value no shortcut 更新模块参数
task_vars[key ] = value no shortcut 更新模块变量, 更新完成后需要执行 update_task
update_task u 使用更新后的变量重新创建task
redo r 重新运行task
continue c 继续执行下一个task
quit q 退出调试

异步执行

在一个任务运行中的时候, 可能需要消耗很多时间, 但是我们想运行第二个任务, 这时候可以使用异步执行功能
异步执行适合机器安装/更新软件类的任务

Ad-Hoc 运行异步任务

ansible all -B 3600 -P 0 -a "sleep 1000" -i localhost,
# `-B` 参数表示异步执行, 3600秒后超时退出. `-P`指定定期拉去运行结果的时间间隔. -P的值越小, 拉取结果就越频繁, 对CPU的消耗也越高
# 执行完上面的命令后会返回一个job_id
# 使用async_status模块, 检查job的运行结果, 传如上面的job_id作为参数
ansible all -m async_status -a "jid=j645205036430.1929724" -i localhost,

Playbook运行异步任务

---
- hosts: all
  remote_user: root
  tasks:
  - name: Simulate long running op (15 sec), wait for up to 45 sec, poll every 5 sec
    ansible.builtin.command: /bin/sleep 15
    async: 45
    poll: 5

poll指定5, 表示每5秒拉一次结果. 如果设置为0, 表示这个任务开始运行后, 不等待运行结果, 立即运行下一个任务.
如果运行的任务有锁, 比如yum在安装软件时, 不能立即执行安装下一个软件. 需要等安装的yum锁释放才行. 这时候就不能用poll=0