ansible入门指南 - playbook
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'.
模板
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
过滤器可以指定转换后 key
和 value
的名字
{{ 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"
条件语句的中变量不需要双花括号
when
和 loop
联合使用
tasks:
- name: Run with items greater than 5
ansible.builtin.command: echo {{ item }}
loop: [ 0, 2, 4, 6, 8, 10 ]
when: item > 5
when
和 role
结合使用
- 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
- 定义handlerslibrary/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: true
到meta/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有两个保留的标签名称
always
和never
如果给任务加了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 |
---|---|---|
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