PlayBook语法实例
playbook是由一个或多个play组成的列表,play的主要功能在于将事先归并为一组的主机装扮成事先通过Ansible中的tasks定义好的角色(play的内容被称为tasks,即任务),从根本上来讲所谓tasks无非是调用Ansible的一个module,将多个play组织在一个playbook中即可以让它们联同起来按事先编排的机制一同工作.
下面来看一个基础的playbook,其中只包含一个tasks任务:
代码语言:javascript复制---
- hosts: web_server #指定执行的主机组
vars: #声明了2个变量
http_port: 80
max_clients: 200
remote_user: root #使用root权限执行
tasks:
- name: ensure apache is at the latest version
yum: pkg=httpd state=latest
- name: write the apache config file #将apache配置文件拷贝到指定主机
template: src=/etc/httpd/conf/httpd.conf dest=/etc/httpd.conf
notify: #当上面拷贝工作完成以后,执行消息发送
- restart apache
- name: ensure apache is running #设置开机自启动
service: name=httpd state=started
handlers:
- name: restart apache #接收消息,并执行重启apache服务
service: name=httpd state=restarted
第一行中,文件开头为---,这是YAML将文件解释为正确的文档的要求,YAML允许多个文档存在于一个文件中,每个文档由 --- 符号分割,但Ansible只需要一个文件存在一个文档即可,因此这里需要存在于文件的开始行第一行.
YAML对空格非常敏感,并使用空格来将不同的信息分组在一起,在整个文件中应该只使用空格而不使用制表符,并且必须使用一致的间距,才能正确读取文件,相同缩进级别的项目被视为同级元素.
以 - 开头的项目被视为列表项目.作为散列或字典操作,它具有key:value格式的项,YAML文档基本上定义了一个分层的树结构,其中位于左侧是包含的元素.YAML文件扩展名通常为.yaml或者.yml,接下来,我们将分别讲解 playbook 语言的多个特性.
PlayBook构成部分
◆Hosts(主机)与Users(用户)◆
我们可以为playbook中的每一个play,个别的选择操作的目标机器是哪些,以哪个用户身份去完成要执行的步骤(called tasks)
代码语言:javascript复制---
- hosts: web_servers
remote_user: root
tasks:
- name: test connection
remote_user: yourname
sudo: yes
playbook中的每一个play的目的都是为了让某个或某些主机以某个指定的用户身份执行任务.
命 令 参 数 | 参 数 解 释 |
---|---|
hosts | 指定要执行指定任务的主机 |
remote_user | 指定远程主机上的执行任务的用户,此处的意思是以root身份执行 |
user | 与remote_user相同 |
sudo | 如果设置为yes执行该任务组的用户在执行任务的时候,获取root权限 |
sudo_user | 指定使用那个用户授权执行 |
connection | 通过什么方式连接到远程主机,默认为ssh |
gather_facts | setup模块默认自动执行 |
◆Tasks 和 Action◆
每一个play包含了一个tasks列表(任务列表),任务列表中的各任务按次序逐个在hosts中指定的所有主机上执行即在所有主机上完成第一个任务后再开始第二个,在自上而下运行某playbook时如果中途发生错误,所有已执行任务都将回滚,因此在更正playbook后重新执行即可.
每一个tasks必须有一个名称name,这样在运行playbook时,从其输出的任务执行信息中可以很好的辨别出是属于哪一个tasks的,如果没有定义name,action的值将会用作输出信息中标记特定的tasks.tasks的目的是使用指定的参数执行模块,而在模块参数中可以使用变量.模块执行是幂等的,这意味着多次执行是安全的,因为其结果均一致.
每个tassk都应该有其name用于playbook的执行结果输出,建议其内容尽可能清晰地描述任务执行步骤,如果未提供name则action的结果将用于输出.
下面是一种基本的taska的定义,service moudle使用key=value格式的参数,这也是大多数module使用的参数格式:
代码语言:javascript复制tasks:
- name: make sure apache is running
service: name=httpd state=running
[在众多模块中,只有command和shell模块仅需要给定一个列表而无需使用“key=value”格式如下]
tasks:
- name: disable selinux
command: /sbin/setenforce 0
[使用command module和shell module时,我们需要关心返回码信息,如果有一条命令,它的成功执行的返回码不是0我们可以这样做]
tasks:
- name: run this command and ignore the result
shell: /usr/bin/somecommand || /bin/true
[或者使用ignore_errors来忽略错误信息]
tasks:
- name: run this command and ignore the result
shell: /usr/bin/somecommand
ignore_errors: True
[如果action行看起来太长,可以使用space(空格)或者indent(缩进)隔开连续的一行]
tasks:
- name: Copy ansible inventory file to client
copy: src=/etc/ansible/hosts dest=/etc/ansible/hosts owner=root group=root mode=0644
◆Handlers 发生改变后执行◆
上面我们曾提到过,module具有“幂等”性,所以当远程主机被人改动时,可以重放playbooks达到恢复的目的.playbooks本身可以识别这种改动,并且有一个基本的event system(事件系统),可以响应这种改动.
当发生改动时notify这个actions会在playbook的每一个tasks结束时被触发,而且即使有多个不同的tasks通知改动的发生,notify actions只会被触发一次.这样可以避免多次有改变发生时每次都执行指定的操作,取而代之仅在所有的变化发生完成后一次性地执行指定操作.
下面一个小例子,当一个文件的内容被改动时,重启两个services进程.
代码语言:javascript复制- name: template configuration file
template: src=template.conf dest=/etc/foo.conf
notify:
- restart memcached
- restart apache
handlers也是一些tasks的列表,通过名字来引用,它们和一般的tasks并没有什么区别handlers是由通知者进行notify,如果没有被notify,handlers不会执行.不管有多少个通知者进行了notify,等到play中的所有tasks执行完成之后,handlers也只会被执行一次.
下面是一个 handlers 的示例:
代码语言:javascript复制handlers:
- name: restart memcached
service: name=memcached state=restarted
- name: restart apache
service: name=apache state=restarted
Handlers最佳的应用场景是用来重启服务,或者触发系统重启操作,除此以外很少用到了.
◆几个常用小实例◆
1.通过playbook添加用户,给远程主机添加用户lyshark.
代码语言:javascript复制---
- name: create user
hosts: all
user: root
gather_facts: False #在执行命令前是否去获取setup信息
vars:
- user: lyshark
tasks:
- name: create user
user: name={{ user }}
2.通过playbook删除用户,删除远程主机lyshark的账号.
代码语言:javascript复制---
- name: create user
hosts: all
user: root
gather_facts: False
vars:
- user: lyshark
tasks:
- name: create user
user: name={{ user }} state=absent remove=yes
3.安装 httpd 服务,将本地准备好的配置文件 copy 过去,并且启动服务.
代码语言:javascript复制---
- hosts: all
remote_user: root
gather_facts: False
tasks:
- name: install apache on CentOS 7
yum: name=httpd state=present
- name: copy httpd conf
copy: src=/etc/httpd/conf/httpd.conf dest=/etc/httpd/conf/httpd.conf
- name: start apache service
service: name=httpd state=started
4.通过playbook 安装apache修改端口,并带有vars变量.
代码语言:javascript复制---
- hosts: all
remote_user: root
gather_facts: False
vars:
src_http_dir: /etc/httpd/conf
dest_http_dir: /etc/httpd/conf
tasks:
- name: install apache on CentOS 7
yum: name=httpd state=present
- name: copy httpd conf
copy: src={{ src_http_dir }}/httpd.conf dest={{ dest_http_dir }}/httpd.conf
notify:
- systemctl restart httpd
- name: start apache service
service: name=httpd state=restarted
handlers: #当httpd.conf文件发生改变时,就触发handler进行服务重启.
- name: restart apache
service: name=httpd state=restarted
PlayBook常用模块
Playbook的模块与在Ansible命令行下使用的模块有一些不同.这主要是因为在playbook中会使用到一些facts变量和一些通过setup模块从远程主机上获取到的变量,有些模块没法在命令行下运行,就是因为它们需要这些变量.而且即使那些可以在命令行下工作的模块也可以通过playbook的模块获取一些更高级的功能.
◆template模块◆
在实际应用中,我们的配置文件有些地方可能会根据远程主机的配置的不同而有稍许的不同,template可以使用变量来接收远程主机上setup收集到的facts信息,针对不同配置的主机,定制配置文件,用法大致与copy模块相同.
命 令 参 数 | 参 数 解 释 |
---|---|
attributes | 文件或目录的属性 |
backup | 如果原目标文件存在,则先备份目标文件 |
block_end_string | 标记块结束的字符串 |
block_start_string | 标记块的开始的字符串 |
dest | 目标文件路径 |
follow | 是否遵循目标中的文件链接 |
force | 是否强制覆盖,默认为yes |
group | 目标文件或目录的所属组 |
owner | 目标文件或目录的所属主 |
mode | 目标文件的权限 |
newline_sequence | 指定用于模板文件的换行符序列 |
src | 源模板文件路径 |
trim_blocks | 如果这设置为True,则删除块后的第一个换行符 |
validate | 在复制之前通过命令验证目标文件,如果验证通过则复制 |
variable_end_string | 标记打印语句结束的字符串 |
variable_start_string | 标记打印语句开头的字符串 |
[官方简单示例]
- template: src=/mytemplates/foo.j2 dest=/etc/file.conf owner=bin group=wheel mode=0644
- template: src=/mytemplates/foo.j2 dest=/etc/file.conf owner=bin group=wheel mode="u=rw,g=r,o=r"
- template: src=/mine/sudoers dest=/etc/sudoers validate='visudo -cf %s'
[playbook的引用该模板配置文件的方法示例]
- name: Setup BIND
host: all
tasks:
- name: configure BIND
template: src=/etc/httpd/conf/httpd.conf dest=/etc/httpd/conf/httpd.conf owner=root group=root mode=0644
[或者是下面这样写,作用是相同的]
- template:
src: /etc/httpd/conf/httpd.conf
dest: /etc/file.conf
owner: root
group: root
mode: 0644
[创建DOS样式文本文件]
- template:
src: config.ini.j2
dest: /share/windows/config.ini
newline_sequence: 'rn'
◆set_fact模块◆
set_fact模块可以自定义facts,这些自定义的facts可以通过template或者变量的方式在playbook中使用,如果你想要获取一个进程使用的内存的百分比,则必须通过set_fact来进行计算之后得出其值,并将其值在playbook中引用.
代码语言:javascript复制[root@localhost ~]# echo "# Configure the buffer pool" >> /etc/my.cnf
[root@localhost ~]# echo "innodb_buffer_pool_size = {{ innodb_buffer_pool_size_mb|int }}M" >> /etc/my.cnf
[root@localhost ~]# vim set_fact.yml
---
- name: Configure Mariadb
hosts: all
tasks:
- name: install Mariadb
yum: name=mariadb-server state=installed
- name: Calculate InnoDB buffer pool size
set_fact: innodb_buffer_pool_size_mb={{ ansible_memtotal_mb / 2 }}
- name: Configure Mariadb
template: src=/etc/my.cnf dest=/etc/my.cnf owner=root group=root mode=0644
- name: Start Mariadb
service: name=mariadb state=started enabled=yes
handlers:
- name: restart Mariadb
service: name=mariadb state=restarted
◆pause模块◆
在playbook执行的过程中暂停一定时间或者提示用户进行某些操作,要为每个主机暂停、等待、休眠,可以使用wait_for模块,如果您想提前暂停而不是设置为过期,或者您需要完全中止剧本运行.
命 令 参 数 | 参 数 解 释 |
---|---|
echo | 控制键入时是否显示键盘输入 |
minutes | 暂停多少分钟 |
seconds | 暂停多少秒 |
prompt | 打印一串信息提示用户操作 |
[暂停5分钟以建立应用程序缓存]
- pause:
minutes: 5
[有用的提醒,提醒您注意更新后的内容]
- pause:
prompt: "Make sure org.foo.FooOverload exception is not present"
[暂停以后获得一些信息]
- pause:
prompt: "Enter a secret"
echo: no
◆wait_for模块◆
wait_for模块是在playbook的执行过程中,等待某些操作完成以后再进行后续操作.
命 令 参 数 | 参 数 解 释 |
---|---|
active_connection_states | 被计为活动连接的TCP连接状态列表 |
connect_timeout | 在下一个任务执行之前等待连接的超时时间 |
delay | 等待一个端口或者文件或者连接到指定的状态 |
exclude_hosts | 在查找状态的活动TCP连接时要忽略的主机或IP的列表drained |
host | wait_for模块等待的主机的地址,默认为127.0.0.1 |
msg | 这会覆盖正常的错误消息,使其不符合所需的条件 |
port | wait_for模块等待的主机的端口 |
path | 文件路径,只有当这个文件存在时,下一任务才开始执行,即等待该文件创建完成 |
search_regex | 可以用来匹配文件或套接字连接中的字符串,默认为多行正则表达式 |
sleep | 检查之间睡眠的秒数,在2.3之前,这被硬编码为1秒 |
state | 等待的状态,状态有started,stoped,present或started,absent |
timeout | wait_for的等待的超时时间,默认为300秒 |
- name: create task
hosts: all
tasks:
[等待8080端口已正常监听,才开始下一个任务,直到超时]
- wait_for: port=8080 state=started
[等待8000端口正常监听,每隔10s检查一次,直至等待超时]
- wait_for: port=8081 delay=10
[等待8000端口直至有连接建立]
- wait_for: host=0.0.0.0 port=8000 delay=10 state=drained
[等待8000端口有连接建立,如果连接来自192.168.8.66或者192.168.8.8则忽略]
- wait_for: host=0.0.0.0 port=8000 state=drained exclude_hosts=192.168.8.66,192.168.8.8
[等待/tmp/foo文件已创建]
- wait_for: path=/tmp/foo
[等待/tmp/foo文件已创建,而且该文件中需要包含completed字符串]
- wait_for: path=/tmp/foo search_regex=completed
[等待/var/lock/file.lock被删除]
- wait_for: path=/var/lock/file.lock state=absent
[等待指定的进程被销毁]
- wait_for: path=/proc/3466/status state=absent
[等待openssh启动,10s检查一次]
- local_action: wait_for port=22 host="{{ ansible_ssh_host | default(inventory_hostname) }}" search_regex=OpenSSH delay=10
◆assemble模块◆
assemble模块用于组装文件,即将多个零散的文件(称为碎片),合并一个目标文件.
命 令 参 数 | 参 数 解 释 |
---|---|
attributes | 文件或目录应的属性 |
backup | 创建一个备份文件(如果yes),包括时间戳信息 |
decrypt | 控制使用保管库对源文件进行自动解密 |
delimiter | 分隔文件内容的分隔符 |
dest | 使用所有源文件的连接创建的文件,合并后的大文件路径 |
group | 合并后的大文件的所属组 |
owner | 合并后的大文件的所属主 |
ignore_hidden | 组装时,是否忽略隐藏文件,默认为no |
mode | 合并后的大文件的权限,指定文件权限 |
regexp | 在regex匹配文件名时汇编文件 |
src | 源文件(即零散文件)的路径 |
validate | 与template的validate相同,指定命令验证文件 |
[指定源文件(即零散文件)的路径,合并一个目标文件someapp.conf]
- assemble:
src: /etc/someapp/fragments
dest: /etc/someapp/someapp.conf
[指定一个分隔符,将在每个片段之间插入]
- assemble:
src: /etc/someapp/fragments
dest: /etc/someapp/someapp.conf
delimiter: '### START FRAGMENT ###'
[在SSHD传递验证后,将新的sshd_config文件复制到目标地址]
- assemble:
src: /etc/ssh/conf.d/
dest: /etc/ssh/sshd_config
validate: '/usr/sbin/sshd -t -f %s'
◆add_host模块◆
add_host模块使用变量在清单中创建新的主机组,以便在以后的相同剧本中使用.获取变量以便我们可以更充分地定义新主机,add_host模块在playbook执行的过程中,动态的添加主机到指定的主机组中.
代码语言:javascript复制[添加主机到webservers组中,主机的变量foo的值为42]
- name: add host to group 'just_created' with variable foo=42
add_host:
name: "{{ ip_from_ec2 }}"
groups: just_created
foo: 42
[将主机添加到多个组]
- name: add host to multiple groups
add_host:
hostname: "{{ new_ip }}"
groups:
- group1
- group2
[向主机添加一个非本地端口的主机]
- name: add a host with a non-standard port local to your machines
add_host:
name: "{{ new_ip }}:{{ new_port }}"
[添加一个通过隧道到达的主机别名]
- name: add a host alias that we reach through a tunnel (Ansible <= 1.9)
add_host:
hostname: "{{ new_ip }}"
ansible_ssh_host: "{{ inventory_hostname }}"
ansible_ssh_port: "{{ new_port }}"
[添加一个通过隧道到达的主机别名]
- name: add a host alias that we reach through a tunnel (Ansible >= 2.0)
add_host:
hostname: "{{ new_ip }}"
ansible_host: "{{ inventory_hostname }}"
ansible_port: "{{ new_port }}"
◆group_by模块◆
group_by模块在playbook执行的过程中,动态的创建主机组.
代码语言:javascript复制[创建主机组]
- group_by:
key: machine_{{ ansible_machine }}
[创建类似kvm-host的主机组]
- group_by:
key: virt_{{ ansible_virtualization_type }}_{{ ansible_virtualization_role }}
[创建嵌套主机组]
- group_by:
key: el{{ ansible_distribution_major_version }}-{{ ansible_architecture }}
parents:
- el{{ ansible_distribution_major_version }}
◆debug模块◆
debug模块在执行过程中打印语句,可用于调试变量或表达式中输出信息.
代码语言:javascript复制[为每个主机打印IP地址和网关]
- debug:
msg: "System {{ inventory_hostname }} has uuid {{ ansible_product_uuid }}"
- debug:
msg: "System {{ inventory_hostname }} has gateway {{ ansible_default_ipv4.gateway }}"
when: ansible_default_ipv4.gateway is defined
- shell: /usr/bin/uptime
register: result
- debug:
var: result [直接将上一条指令的结果作为变量传递给var,由debug打印出result的值]
verbosity: 2
- name: Display all variables/facts known for a host
debug:
var: hostvars[inventory_hostname]
verbosity: 4
◆fail模块◆
fail模块用于终止当前playbook的执行,通常与条件语句组合使用,当满足条件时,终止当前play的运行,也可以直接由failed_when取代.
代码语言:javascript复制[执行失败钱打印出自定义信息]
- fail:
msg: "The system may not be provisioned according to the CMDB status."
when: cmdb_status != "to-be-staged"
PlayBook条件判断
在有的时候play的结果依赖于变量,fact或者是前一个任务的执行结果,从而需要使用到条件语句.
◆when◆
有的时候在特定的主机需要跳过特定的步骤,例如在安装包的时候,需要指定主机的操作系统类型,或者是当操作系统的硬盘满了之后,需要清空文件等,可以使用when语句来做判断.when关键字后面跟着的是python的表达式,在表达式中你能够使用任何的变量或者fact,当表达式的结果返回的是false,便会跳过本次的任务.
when基本用法
代码语言:javascript复制---
- name: Install VIM
hosts: all tasks:
- name:Install VIM via yum
yum: name=vim-enhanced state=installed
when: ansible_os_family =="RedHat"
- name:Install VIM via apt
apt: name=vim state=installed
when: ansible_os_family =="Debian"
- name: Unexpected OS family
debug: msg="OS Family {{ ansible_os_family }} is not supported" fail=yes
when: not ansible_os_family =="RedHat" or ansible_os_family =="Debian"
条件语句还有一种用法,它还可以让你当达到一定的条件的时候暂停下来,等待你的输入确认.一般情况下,当ansible遭遇到error时,它会直接结束运行.那其实你可以当遭遇到不是预期的情况的时候给使用pause模块,这样可以让用户自己决定是否继续运行任务:
代码语言:javascript复制- name: pause for unexpected conditions
pause: prompt="Unexpected OS"
when: ansible_os_family !="RedHat"
在when中使用jinja2的语法
代码语言:javascript复制tasks:
- command: /bin/false
register: result # 将命令执行的结果传递给result变量
ignore_errors: True # 忽略错误
- command: /bin/something
when: result|failed # 如果注册变量的值 是任务failed则返回true
- command: /bin/something_else
when: result|success # 如果注册变量的值是任务success则返回true
- command: /bin/still/something_else
when: result|skipped # 如果注册变量的值是任务skipped则返回true
- command: /bin/foo
when: result|changed # 如果注册变量的值是任务changed则返回true
- hosts: all
user: root
vars:
epic: true
tasks:
- shell: echo "This certainly is epic!"
when: epic
- shell: echo "This certainly is not epic!"
when: not epic
如果变量不存在,则可以通过jinja2的'defined'命令跳过
代码语言:javascript复制tasks:
- shell: echo "I've got '{{ foo }}' and am not afraid to use it!"
when: foo is defined
- fail: msg="Bailing out. this play requires 'bar'"
when: bar is not defined
when在循环语句中的使用方法
代码语言:javascript复制tasks:
- command: echo {{ item }}
with_items: [ 0, 2, 4, 6, 8, 10 ]
when: item > 56 #在include和roles中使用when:
# 在include中使用的示例:- include: tasks/sometasks.yml
when: "'reticulating splines' in output"
# 在roles中使用的示例:- hosts: webservers
roles:
- { role: debian_stock_config, when: ansible_os_family == 'Debian' }
◆条件导入◆
有些时候,你也许想在一个Playbook中以不同的方式做事,比如说在debian和centos上安装apache,apache的包名不同,除了when语句,还可以使用下面的示例来解决:
代码语言:javascript复制---
- hosts: all
remote_user: root
vars_files:
- "vars/common.yml"
- [ "vars/{{ ansible_os_family }}.yml", "vars/os_defaults.yml" ]
tasks:
- name: make sure apache is running
service: name={{ apache }} state=running
很多不同的yml文件只是包含键和值,如下:
代码语言:javascript复制[root@localhost ~]# for vars/CentOS.yml
apache: httpd
somethingelse: 42
如果操作系统是“CentOS”,Ansible导入的第一个文件将是“vars/CentOS.yml”,紧接着是“/var/os_defaults.yml”,如果这个文件不存在.而且在列表中没有找到,就会报错.在Debian系统中,最先查看的将是“vars/Debian.yml”而不是“vars/CentOS.yml”,如果没找到,则寻找默认文件“vars/os_defaults.yml”.
◆with_first_found◆
有些时候,我们想基于不同的操作系统,选择不同的配置文件,及配置文件的存放路径,可以借助with_first_found来解决:
代码语言:javascript复制- name: template a file
template: src={{ item }} dest=/etc/myapp/foo.conf
with_first_found:
- files:
- {{ ansible_distribution }}.conf
- default.conf
paths:
- search_location_one/somedir/
- /opt/other_location/somedir/
◆failed_when◆
failed_when其实是ansible的一种错误处理机制,是由fail模块使用了when条件语句的组合效果.示例如下:
代码语言:javascript复制- name: this command prints FAILED when it fails
command: /usr/bin/example-command -x -y -z
register: command_result
failed_when: "'FAILED' in command_result.stderr"
我们也可以直接通过fail模块和when条件语句,写成如下:
代码语言:javascript复制- name: this command prints FAILED when it fails
command: /usr/bin/example-command -x -y -z
register: command_result
ignore_errors: True
- name: fail the play if the previous command did not succeed
fail: msg="the command failed"
when: "'FAILED' in command_result.stderr"
◆changed_when◆
当我们控制一些远程主机执行某些任务时,当任务在远程主机上成功执行,状态发生更改时,会返回changed状态响应,状态未发生更改时,会返回OK状态响应,当任务被跳过时,会返回skipped状态响应.我们可以通过changed_when来手动更改changed响应状态,示例如下:
代码语言:javascript复制- shell: /usr/bin/billybass --mode="take me to the river"
register: bass_result
changed_when: "bass_result.rc != 2" # 只有该条task执行以后,bass_result.rc的值不为2时,才会返回changed状态
# 永远不会报告“改变”的状态
- shell: wall 'beep'
changed_when: False # 当changed_when为false时,该条task在执行以后,永远不会返回changed状态
PlayBook循环语句
在使用Ansible做自动化运维的时候,免不了的要重复执行某些操作,如:添加几个用户,创建几个MySQL用户并为之赋予权限,操作某个目录下所有文件等等.好在playbook支持循环语句,可以使得某些需求很容易而且很规范的实现.
◆with_items◆
with_items是playbooks中最基本也是最常用的循环语句.
代码语言:javascript复制tasks:
- name:Secure config files
file: path=/etc/{{ item }} mode=0600 owner=root group=root
with_items:
- my.cnf
- shadow
- fstab
# 或
with_items:"{{ somelist }}"
上面的例子说明在/etc下创建权限级别为0600,属主属组都是root三个文件,分别为my.cnf、shadow、fstab. 使用with_items迭代循环的变量可以是个单纯的列表,也可以是一个较为复杂的数据结果,如字典类型:
代码语言:javascript复制tasks:
- name: add several users
user: name={{ item.name }} state=present groups={{ item.groups }}
with_items:
- { name: 'testuser1', groups: 'root' }
- { name: 'testuser2', groups: 'root' }
◆with_nested嵌套循环◆
代码语言:javascript复制tasks:
- name: give users access to multiple databases
mysql_user: name={{ item[0] }} priv={{ item[1] }}.*:ALL append_privs=yes password=foo
with_nested:
- [ 'alice', 'bob' ]
- [ 'clientdb', 'employeedb', 'providerdb' ]
item[0]是循环的第一个列表的值['alice','bob']. item[1]是第二个列表的值,表示循环创建alice和bob两个用户,并且为其赋予在三个数据库上的所有权限.
也可以将用户列表事先赋值给一个变量:
代码语言:javascript复制tasks:
- name: here, 'users' contains the above list of employees
mysql_user: name={{ item[0] }} priv={{ item[1] }}.*:ALL append_privs=yes password=foo
with_nested:
- "{{users}}"
- [ 'clientdb', 'employeedb', 'providerdb' ]
◆with_dict◆
with_dict可以遍历更复杂的数据结构,假如有如下变量内容:
代码语言:javascript复制users:
alice:
name: Alice Appleworth
telephone: 123-456-7890
bob:
name: Bob Bananarama
telephone: 987-654-3210
现在需要输出每个用户的用户名和手机号:
代码语言:javascript复制tasks:
- name: Print phone records
debug: msg="User {{ item.key }} is {{ item.value.name }} ({{ item.value.telephone }})"
with_dict: "{{ users }}"
◆with_fileglob文件匹配遍历◆
可以指定一个目录,使用with_fileglob可以循环这个目录中的所有文件,示例如下:
代码语言:javascript复制tasks:
- name:Make key directory
file: path=/root/.sshkeys ensure=directory mode=0700 owner=root group=root
- name:Upload public keys
copy: src={{ item }} dest=/root/.sshkeys mode=0600 owner=root group=root
with_fileglob:
- keys/*.pub
- name:Assemble keys into authorized_keys file
assemble: src=/root/.sshkeys dest=/root/.ssh/authorized_keysmode=0600 owner=root group=root
◆with_subelement遍历子元素◆
假如现在需要遍历一个用户列表,并创建每个用户,而且还需要为每个用户配置以特定的SSH key登录,变量文件内容如下:
代码语言:javascript复制users:
- name: alice
authorized:
- /tmp/alice/onekey.pub
- /tmp/alice/twokey.pub
mysql:
password: mysql-password
hosts:
- "%"
- "127.0.0.1"
- "::1"
- "localhost"
privs:
- "*.*:SELECT"
- "DB1.*:ALL"
- name: bob
authorized:
- /tmp/bob/id_rsa.pub
mysql:
password: other-mysql-password
hosts:
- "db1"
privs:
- "*.*:SELECT"
- "DB2.*:ALL"
[playbook中定义如下:]
- user: name={{ item.name }} state=present generate_ssh_key=yes
with_items: "`users`"
- authorized_key: "user={{ item.0.name }} key='{{ lookup('file', item.1) }}'"
with_subelements:
- users
- authorized
[也可以遍历嵌套的子列表:]
- name: Setup MySQL users
mysql_user: name={{ item.0.name }} password={{ item.0.mysql.password }} host={{ item.1 }} priv={{ item.0.mysql.privs | join('/') }}
with_subelements:
- users
- mysql.hosts
◆with_sequence循环整数序列◆
with_sequence可以生成一个自增的整数序列,可以指定起始值和结束值,也可以指定增长步长. 参数以key=value的形式指定,format指定输出的格式.数字可以是十进制、十六进制、八进制:
代码语言:javascript复制- hosts: all
tasks:
# create groups
- group: name=evens state=present
- group: name=odds state=present
# create some test users
- user: name={{ item }} state=present groups=evens
with_sequence: start=0 end=32 format=testuserd
# create a series of directories with even numbers for some reason
- file: dest=/var/stuff/{{ item }} state=directory
with_sequence: start=4 end=16 stride=2 # stride用于指定步长
# a simpler way to use the sequence plugin
# create 4 groups
- group: name=group{{ item }} state=present
with_sequence: count=4
◆with_random_choice随机选择◆
从列表中随机取一个值:
代码语言:javascript复制- debug: msg={{ item }}
with_random_choice:
- "go through the door"
- "drink from the goblet"
- "press the red button"
- "do nothing"
◆do-Util循环◆
重复执行shell模块,当shell模块执行的命令输出内容包含"all systems go"的时候停止,重试5次,延迟时间10秒.retries默认值为3,delay默认值为5,任务的返回值为最后一次循环的返回结果.
代码语言:javascript复制- action: shell /usr/bin/foo
register: result
until: result.stdout.find("all systems go") != -1
retries: 5
delay: 10
◆循环注册变量◆
在循环中使用register时,保存的结果中包含results关键字,该关键字保存模块执行结果的列表.
代码语言:javascript复制---
- hosts: all
tasks:
- shell: echo "{{ item }}"
with_items:
- one
- two
register: echo
变量内容如下:
代码语言:javascript复制TASK [command] *******************************************************
skipping: [192.168.10.20] => (item=one)
skipping: [192.168.10.20] => (item=two)
PLAY RECAP ***********************************************************
192.168.10.20 : ok=1 changed=0 unreachable=0 failed=0
遍历注册变量的结果:
代码语言:javascript复制- name: Fail if return code is not 0
fail:
msg: "The command ({{ item.cmd }}) did not have a 0 return code"
when: item.rc != 0
with_items: "`echo`.`results`"
◆with_together遍历数据并行集合◆
代码语言:javascript复制- hosts: all
remote_user: root
vars:
alpha: [ 'a','b','c','d']
numbers: [ 1,2,3,4 ]
tasks:
- debug: msg="{{ item.0 }} and {{ item.1 }}"
with_together:
- "{{ alpha }}"
- "{{ numbers }}"
输出执行结果如下:
代码语言:javascript复制ok: [192.168.10.20] => (item=[u'a', 1]) => {
"changed": false,
"item": [
"a",
1
],
"msg": "a and 1"
}
ok: [192.168.10.20] => (item=[u'b', 2]) => {
"changed": false,
"item": [
"b",
2
],
"msg": "b and 2"
}
ok: [192.168.10.20] => (item=[u'c', 3]) => {
"changed": false,
"item": [
"c",
3
],
"msg": "c and 3"
}
ok: [192.168.10.20] => (item=[u'd', 4]) => {
"changed": false,
"item": [
"d",
4
],
"msg": "d and 4"
}
PlayBook部署实验
Ansible的PlayBook文件格式为YAML语言,所以希望读者在编写PlayBook前对YAML语法有一定的了解,否则在运行PlayBook的时候经常碰到语法错误提示,这里我们通过介绍批量部署apache服务为例,介绍一下apache.yaml这个PlayBook的具体应用写法,如果你对YAML语言没有了解的话,请自行去百度学习.
1.首先在当前目录下创建一个目录,用来保存与apache相关的配置文件和程序,这里要注意的是,我们应该在本机搭建一个环境,调试好后再进行部署,调试环节此处略过.
代码语言:javascript复制[root@localhost ~]# mkdir playbook
[root@localhost ~]# ll
total 0
drwxr-xr-x. 2 root root 6 Dec 2 06:43 playbook
[root@localhost playbook]# cp -a /etc/httpd/conf/httpd.conf ./
[root@localhost playbook]# cp -a /var/www/html/index.html ./
[root@localhost playbook]# ls -l
total 16
-rw-r--r--. 1 root root 11755 Dec 2 06:46 httpd.conf
-rw-r--r--. 1 root root 15 Dec 2 06:44 index.html
2.通过编写一个apache.yaml剧本,来实现批量部署的案例,这里是Yum安装所以很简单,首先我们先来看一下这个PlayBook的代码:
代码语言:javascript复制[root@localhost playbook]# cat apache.yaml
1 --- #表示该文件是YAML文件,非必需
2 - hosts: all #playbook针对的主机,all表示所有
3 tasks: #表示一个集合
4 - name: Install httpd #说明信息,安装httpd服务
5 yum: name=httpd state=present #通过YUM模块安装信息
6
7 - name: Copy httpd.conf #说明信息,复制配置文件
8 template: src=./httpd.conf dest=/etc/httpd/conf/httpd.conf owner=root group=root mode=0755
9
10 - name: Copy index.html #说明文件,复制网页目录
11 template: src=./index.html dest=/var/www/html/index.html owner=root group=root mode=0755
12 notify: #发送消息,当上一条语句执行完毕后,执行下面的模块
13 - Restart Apache Server
14
15 handlers: #消息发送到这里,指定主机执行模块
16 - name: Restart Apache Server #与上面的notify内容相对应
17 service: name=httpd state=restarted #重启httpd服务
3.当我们配置完成YAML剧本以后,再来配置一个Inventory的hosts主机文件,也就是指定要执行此操作的主机列表,此处我们定义以下主机列表:
代码语言:javascript复制[root@localhost playbook]# cat hosts
[apache] #指定Apache这个组,组中有成员192.168.10.20/30两个IP
192.168.10.20
192.168.10.30
#[test]
#192.168.10.1..100 #也可以这样指定一个范围
#192.168.10.1[0:100] #也可以这样写
[apache:vars] #指定我们使用的Python解释器路径
ansible_python_interpreter=/usr/bin/python2.7
4.接下来我们对apache.yaml使用 --syntax-check
命令参数,检查一下PlayBook语法是否正确:
[root@localhost playbook]# ansible-playbook apache.yaml --syntax-check
playbook: apache.yaml
5.紧接着使用--list-task
参数显示apache.yaml,PlayBook文件中所有的task名称如下所示:
[root@localhost playbook]# ansible-playbook apache.yaml --list-task
playbook: apache.yaml
play #1 (all): all TAGS: []
tasks:
Install httpd TAGS: []
Copy httpd.conf TAGS: []
Copy index.html TAGS: []
5.紧接着使用--list-hosts
参数显示apache.yaml,PlayBook文件中所有的task名称如下所示:
[root@localhost playbook]# ansible-playbook apache.yaml --list-hosts
playbook: apache.yaml
play #1 (all): all TAGS: []
pattern: [u'all']
hosts (2):
192.168.10.20
192.168.10.30
6.确认过以后,直接使用下面的命令一键部署,我们写好的PlayBook剧本,此时我们等它一会.
代码语言:javascript复制[root@localhost playbook]# ansible-playbook -i hosts apache.yaml
PLAY RECAP ******************************************************************************
192.168.10.20 : ok=5 changed=4 unreachable=0 failed=0
192.168.10.30 : ok=5 changed=4 unreachable=0 failed=0
当然playbook还支持交互式地执行 task 我们可以指定 -step 参数即可,apache.yaml 是一个相对简单的 Playbook 文件,在我们的实际工作中可能会遇到各种复杂的需求,但 Playbook 的灵活性非常强大,下面我们来看几个常用参数,如下:
代码语言:javascript复制---
- hosts: 192168.10.10:192.168.10.100 #指定一个主机执行范围
patterns
remote_user: root #指定远程SSH认证用户
sudo: yes #启用Sudo操作
sudo_user: lyshark #指定Sudo的用户
gather_facts: no #设置facts信息收集
accelerate: no #设置accelerate模式
accelerate_port: 5099 #设置accelerate端口
max_fail_percentage: 30 #设置失败百分比
connection: local #设置远程连接方式
serial: 15 #设置变量
Ansible 的 playbook 写法很丰富,功能很强大,只有掌握了 playbook 每一个参数之后,我们才能写出强大而且灵活性很高的 Playbook ,这也是我们在工作中接触和使用最多的地方.