很显然,我们可以将众多 ansible 命令放在 Shell 脚本中执行,以实现批量部署操作。比如:
#!/bin/sh ansible host-01 -m ping ansbile host-01 -m copy -a "src=/etc/hosts dest=/tmp/hosts" ansible host-01 -m shell -a "/sbin/reboot"
但是,如果我们的需求更加复杂呢?比如需要根据远程主机的环境、当前状态、发行版版本等等结果来执行不同的命令呢?很显然在 Shell 脚本中使用 ansible 命令不是最佳方案。那我们应该怎么办呢?
Ansible 提供了脚本化的功能,将任务编写到脚本中,运行该脚本以执行多个任务,这种脚本被称为 Playbook。使用 Playbook 描述 Ansible 要执行的系列操作,脚本为YAML文件,以yml或yaml为后缀。它替代在Shell脚本中挨个命令执行的方式。
--- - hosts: web vars: http_port: 80 max_clients: 200 remote_user: root # 任务列表 tasks: - name: ensure apache is at the latest version yum: pkg=httpd state=latest - name: Write the configuration file template: src=templates/httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf notify: - restart apache - name: Write the default index.html file template: src=templates/index.html.j2 dest=/var/www/html/index.html - name: ensure a pache is running service: name=httpd state=started # 回调处理 handlers: - name: restart apache service: name=httpd state=restarted
使用 ansible-playbook 命令执行 Playbook 脚本:
#!/bin/sh # 执行 Playbook 的基本方法。 ansible-playbook deploy.yml # 查看输出的细节 ansible-playbook playbook.yml --verbose # 查看该脚本影响哪些主机( hosts ) ansible-playbook playbook.yml --list-hosts # 并行执行脚本 ansible-playbook playbook.yml -f 10
查看更多命令帮助:
#!/bin/sh man ansible-playbook ansible-playbook -h
使用 YAML 语法编写 Playbook 脚本,这里不介绍 YAML 的语法,已经有很多优秀文章。
通常 Playbook 由三部分组成:
在哪些机器上以哪个用户执行执行:相关的指令有 hosts、user 等等;
hosts 指定主机组名;
vars 定义参数,可以后面的参数中引用;
remote_user 定义执行的用户;
指定主机与用户,对于开始的部分(hosts、user、vars):
1)定义要操作的主机及用户;
2)同时定义两个变量;
3)还可以使用 become、become_method、become_user 来以其他用户执行(需要 --ask-become-pass 指定密码);
执行哪些任务:使用 tasks 指令;
tasks 指定要执行的任务,每个任务都称为“动作(Action)”;
指定任务列表,对于 tasks 部分,基本语法如下:
tasks: - name: ensure apache is at the latest version yum: pkg=httpd state=latest # 但是 name 可选,因此: tasks: - yum: pkg=httpd state=latest
1)依次执行,如果在 tasks 中的动作发生错误,则 Playbook 会终止执行。需要调整 Playbook 以重新执行;
2)每个动作都是对模块的一次调用,只是参数和变量不同;参数可以 key=value 形式传入,参数可以写在多行中,或者以 YMAL 形式传入;
3)建议为每个动作都加上 name 指令,以指示动作的内容;否则显示当前动作内容,在命令中难以辨识。
4)对于每个动作,所调用的模块会先检查是否需要执行:如果执行成功,则返回“changed”;如果不需要执行,则返回“ok”。检查判断由模块负责实现。
--- - hosts: webservers user: root vars: http_port: 80 max_clients : 200 tasks: # 模块参数写在单行 - name: ensure apache is at the latest version yum: pkg=httpd state=latest # 模块参数写在多行 - name: write the apache config file template: src=/srv/httpd.j2 dest=/etc/httpd.conf notify: - restart apache # 模块参数以 YAML 形式传入 - name: ensure apache is running service: name: httpd state: started
问题:在执行多个不同的任务后,我们可能需要执行某些特定操作。比如,在某个任务中修改 Apache 配置文件,在另个任务中安装 Apache 插件,这两个任务都需要重启 Apache 服务。
方案:像上面的这样场景,重启 Apache 就可以设计成“回调通知”(是软件编程的概念),即执行某些操作后触发某个事件。在Ansible中,通过使用 handlers 指令实现。
--- - hosts: web # 任务列表 tasks: - name: Write the configuration file template: src=templates/httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf # 发出通知 notify: - restart-apache # 回调处理 handlers: - name: restart-apache service: name=httpd state=restarted
但多次触发只执行一次,并按照声明的顺序执行。
需要使用 notify 指令调用:
在 handlers 中的动作需要在 tasks 中调用,即使用 notify 指令;
需要任务真正执行(changed):
只有在 tasks 中的动作真正执行后(changed),才会执行 handlers 中的动作;
对应 handlers 只会执行一次:
在多次通知(notify)下,特定 handlers 只会执行一次.因为 handler 是在 tasks 执行结束后才开始执行的。就是说,在 tasks 中的多个动作可以通知 handler 中的同一个动作,但是 handler 里的这个动作只会执行一次。
依照定义顺序执行:
鉴于此,在handler中的动作是按照定义的顺序执行的,与收到的通知顺序是无关的。
在 Playbook 中,也可以逻辑控制语句:
when - 类似于编程语言中的 if 关键字
with_x - 类似于编程语言中的 while 关键字
block - 类似于编程语言中的代码块。可以把几个任务组成一个代码块,以便于针对一组操作的异常进行处理等操作。
使用条件判断语句”when“:
tasks : - name: "shutdown Debian flavored systems " when: ansible_os_family == "Debian" command: /sbin/shutdown -t now
也可以更具动作的执行结果来执行任务:
tasks: - command: /bin/false register: result ignore_errors: True - command: /bin/something when: result|failed - command: /bin/something when: result|success - command: /bin/something when: result|skipped
远程主机中的系统变量也可以作为when的条件,用"|int"还可以转换返回值的类型:
- hosts: web tasks: - debug: msg="only on Red Hat 7 , derivatives , and later" when : ansible_os_family == "RedHat" and ansible_lsb.major_release|int >= 6
还可以使用条件表达式:
vars: epic: true tasks: - shell: echo "epic !" when: epic - shell: echo "Tnot epic" when: not epic - shell: echo "epic is defined" when: epic is defined - shell: echo "epic is not defined" when: epic is not defined
数值表达式:
tasks : - command: echo { { item }} with_items: [ 0, 2 , 4 , 6, 8 , 10 ] when: item > 5 # 注意,当when和with_items一起使用时,when是针对每个条目进行判断的。
与include一起用:
- include: tasks/sometasks.yml when: "reticulating splines ’ in output"
与role一起使用:
- hosts: webservers roles : - { role: debian_stock_config, when: ansible_os_family == 'Debian' }
使用循环(Loop):
vars: somelist: ["testuserl", "testuser2"] tasks: - name: "批量添加用户" user: name={ { item }} state=present groups=wheel with_items: - testuser0 - testuser1 - name: "从变量中获取要添加的用户" user: name={ { item }} state=present groups=wheel with_items: "{ { somelist }}" # with_items不仅支持列表,还支持字典 - name: "演示一个更加复杂的循环参数" user: name={ { item.name }} state=present groups={ { item.groups }} with_items: - { name: 'testuser0', groups: 'root' } - { name: 'testuserl', groups: 'wheel' }
嵌套循环:
--- - hosts: all tasks: - name: "嵌套循环" debug: msg="first { { item.0 }} { { item.1 }}" with_nested: - ['alice', 'bob'] - ['db0', 'db1', 'db2'] - name: "嵌套循环的另一种取值形式" debug: msg="first { { item['0'] }} { { item['1'] }}" with_nested: - ['alice', 'bob'] - ['db0', 'db1', 'db2']
循环哈希表:
--- - hosts: all vars: users: alice: name: Alice tel: 1234567890 bob: name: Bob tel: 0987654321 tasks: - name: "嵌套循环的另一种取值形式" debug: msg="User { { item.key }} Name { { item.value.name }} Tel { { item.value.tel }}" with_dict: "{ { users }}"
对文件列表使用循环。可以使用with_fileglob可以以非递归的方式来模拟匹配单个目录中的文件:
tasks : # first ensure our target directory exists - file : dest=/etc/fooapp state=directory # copy each file over that matches the given pattern - copy : src={ { item}} dest=/etc/fooapp/ owner=root mode=600 with_fileglob: - /playbooks/files/fooapp/*
使用block创建块,来包含一段动作,然后根据条件执行一段语句:
tasks : - block : - yum: name={ { item }} state=installed with_items: - httpd - memcached - template: src=templates/src.j2 dest=/etc/foo.conf - service: name=bar state=started enabled=True rescue : - debug: msg= "I caught an error" - command: /bin/false - debug : msg="I also 口ever execute :-(" always: - debug: msg="this a l ways executes" when: ansible_distribution == "CentOS" become: true become_user: root
组装成块后,处理异常会更加方便。示例中的rescue和always用于异常处理。
重用Playbook,解决重复编写Playbook的问题:
include - 重用单个Playbook脚本,使用起来简单 、直接。 role - 重用实现特定功能的Playbook文件夹,使用方法稍复杂、功能强大。Ansible还为role创建了一个共享平台 Ansible Galaxy, role是Ansible最为推荐的重用和分享Playbook 的方式。
使用include语句。下面是tasks/firewall_httpd_default.yml文件的内容:
--- # possibly saved as tasks/firewall_httpd_default.yml - name : insert firewalld rule for httpd firewalld : port=80/tcp permarent=true state=enabled immediate=yes
使用include引用上述文件:
tasks: - include: tasks/firewall_httpd_default.yml
在被引入的文件中传入参数:
--- # possibly saved as tasks/firewall_httpd_default.yml - name : insert firewalld rule for httpd firewalld : port={ { port }}/tcp permarent=true state=enabled immediate=yes
上述文件中定义了一个引入文件,使用该引入文件需要传入一个port参数。那如何传入参数呢?如下:
tasks: - include : tasks/firewall.yml port=80 - include: tasks/firewall.yml port=3260 - include : tasks/firewall.yml port=423
也可以传入字典参数:
tasks: - include : tasks/firewall.yml vars: wp_user: timmy ssh_keys : - keys/one . txt - keys/two.txt
或者也可以简写成一条:
tasks: - { include : wordpress.yml, wp_user: timmy, ssh_keys: ['keys/one.txt', 'keys/two.txt']}
当然,如果参数已经在Playbook中定义了,就不需要手动传入。
关于include要注意的事情:
在Ansible 1.9及之前的版本中,不能调用include里面的 handler 的,不过基于 Ansible 2.0+则可以调用 include 里面的 handler。所以在使用的时候要注意所安装的 Ansible 版本。 Ansible 允许全局(或者叫 Plays )加 include。然而这种使用方式并不推荐,因为它不支持嵌入 include,而且很多 Playbook 的参数也无法使用。 越来越强大而不稳定的 include - 为了使 include 功能更加强大,在每个新出的 Ansible 中都会添加一些新的功能。例如,在2.0 中添加了 include 动态名字的YAML,然而这样的用法有很多的限制,不够成熟,可能在更新版本的 Ansible 申又被去掉了,学习和维护成本很高。所以在需要使用更灵活的重用机制时,建议用下面介绍的 role。
使用role语句。它类似于编程语言中的“ Package”,可以重用一组文件,形成完整的功能。例如,安装和配置Apache时,既需要用tasks实现软件包的安装和模板的复制,也需要httpd.conf和index.html的模板文件,以及handler实现重启功能。这些文件都可以放在一个role里面,以供不同的Playbook 文件重用。
提倡在Ansible Playbook中使用 role,并且提供了一个分享 role 的平台 Ansible Galaxy。在 Galaxy 上可以找到别人写好的 role
如何定义一个role?通过遵循特定的目录结构来实现对 role 的定义。下面的目录结构定义了一个role(名字为 myrole):
roles/ common/ # this hierarchy represents a "role" tasks/ # main.yml # <-- tasks file can include smaller files if warranted handlers/ # main.yml # <-- handlers file templates/ # <-- files for use with the template resource ntp.conf.j2 # <------- templates end in .j2 files/ # bar.txt # <-- files for use with the copy resource foo.sh # <-- script files for use with the script resource vars/ # main.yml # <-- variables associated with this role defaults/ # main.yml # <-- default lower priority variables for this role meta/ # main.yml # <-- role dependencies library/ # roles can also include custom modules module_utils/ # roles can also include custom module_utils lookup_plugins/ # or other types of plugins, like lookup in this case
在Playbook文件site.yaml中调用了它:
--- - hosts: webservers roles: - myrole
在role中,不需要包含上述的所有目录,根据需要加入相应的目录即可:
如果 roles 文件 role//tasks/main.yml 存在,则文件中列出的任务都将被添加到 Play 中。 如果文件 roles/ /handlers/main.yml 存在,则文件中列出 handler 都将被添加到 Play 中。 如果文件 role/ /vars/main.yml 存在,则文件中列出的变量都将被添加到 Play 中。 如果文件 role/ /defaults/main.yml 存在,则文件中列出的变量都会被添加到 Play 中。 如果文件 role/ /meta/main.yml 存在,则文件中列出的所有依赖的 role 都将被添加到Play 中。 此外,下面的文件不需要绝对或者是相对路径,等同于放在同一个目录下,可以直接使用即可。c opy 或者 scrip t 使用 roles/ /files/下的文件;templae使用 roles/ /temp l ates 下的文件;include 使用 roles/ /tasks 下的文件;
在写 role 的时候,一般都要包含 role 人口文件 roles/x/tasks/main.yml 。其他的文件和目录可以根据需求选择是否加入。
如何定义一个带有参数的role?直接定义即可。对于目录结构如下的role:
main.yml roles/ myrole/ tasks/ main.yml
其中,main.yml的内容如下,使用{ { var_name }}定义的变量就可以了:
- name: use param debug: msg="{ { param }}"
如何使用这个带有参数的role呢?如下:
--- - hosts : webservers roles: - { role: myrole, param: 'Call some_role for the 1st time' } { role: myrole, param: 'Call some role for the 2nd time' }
或者:
--- - hosts: webservers roles: - role: myrole param: 'Call xxxxxxxxxx' - role: myrole param: 'other'
如何为role指定默认参数?对于如下的目录结构:
main.yml roles/ myrole/ tasks/ main.yml defaults/ main.yml
在 roles/myrole/defaults/main.yrnl 中,使用YAML字典定义语法定义的 param 的值如下:
param: "I am the default value"
如上定义了 param 参数的默认值。
role 也可以与 when 一起使用:
--- - hosts: webservers roles: - { role : myrole, when: "ansible_os_family == 'RedHat'" } - role : my role when: "ansible os family == 'RedHat'"
如果roles和tasks同时出现,则优先级为:pre_tasks > role > tasks > post_tasks
通过为任务使用 tags 属性,可以实现执行部分任务。
例如,文件 example.yml 标记了两个标签 packages 和 configuration:
tasks: - yum: name={ { item }} state=installed with_items: - httpd tags: - packages - name: copy httpd.conf template: src=templates/httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf tags: - configuration - name: copy index.html template: src=templates/index.html.j2 dest=/var/www/html/index.html tags: - configuration
对于如下的执行方式:
#!/bin/sh # 如果不加任何 tag 参数,那么会执行所有标签对应的任务 ansible-playbook example.yml # 利用关键字 tags 指定需要执行的部分任务 ansible-playbook example.yml --tags "packages" # 利用关键字skip-tags指定不执行对应的任务 ansible-playbook example.yml --skip-tags "configuration"
如果把标签的名字定义为 always,并且没有明确指定不执行 always 标签,那么 always 标签所对应的任务就始终会被执行
命令行中利用"--tags tagged"来执行所有标记了标签的任务,无论标记的标签的名字是什么
命令行中利用"--tags untagged"来执行所有没有标签的任务
命令行中利用"--tags all"来执行所有任务
在 include 中使用标签:
- include: foo.yml tags: [web, foo]
在 role 中使用标签:
roles: - { role : webserver, port : 5000 , tags: [ 'web', 'foo' ]}
--- # 安装 apache 的 Play - hosts: web remote user: root tasks: - name: ensure apache is at the latest version yum: pkg=httpd state=latest # 安装 MySQL Server 的 Play - hosts: lb remote user: root tasks: - name: ensure mysqld is at the latest version yum: pkg=mariadb state=latest
如上示例是某个 Playbook 脚本,单个 Playbook 通常就是可以被 Ansible 执行的 YAML 文件。上面的 Playbook 分别对两组主机进行了不同的操作,对每组主机的操作就称为一个 Play 。
Setting the Environment (and Working With Proxies)
Yum module fails when installing via URL if a yum http_proxy is required #18979
可以在 tasks 中设置,可以在 Play 中设置,还可以使用变量:
- hosts: all remote_user: root # 在 play 中设置 environment: http_proxy: http://proxy.example.com:8080 # here we make a variable named "proxy_env" that is a dictionary vars: proxy_env: http_proxy: http://proxy.example.com:8080 tasks: # 直接定义 - name: Install cobbler package: name: cobbler state: present environment: http_proxy: http://proxy.example.com:8080 # 使用变量 - name: Install cobbler package: name: cobbler state: present environment: "{ { proxy_env }}"
某些语言环境的管理工具(NVM)需要使用环境变量,可以通过 environment 进行设置,参考 Setting the Environment (and Working With Proxies) 示例。
Ansible: Conditionally define variables in vars file if a certain condition is met
当条件不同时,我们需要为变量赋予不同参数:
test: var1: "{% if my_group_var %}value{% else %}other_value{% endif %}" var2: "{ {'value' if (my_group_var) else 'other_value'}}"
还可以根据条件引入不同的文件:
- include_vars: test_environment_vars.yml when: global_platform == "test" - include_vars: staging_environment_vars.yml when: global_platform == "staging" - include_vars: prod_environment_vars.yml when: - global_platform != "test" - global_platform != "staging"
Override hosts variable of Ansible playbook from the command line
在 Playbook 中的 hosts 参数用于指定目标主机,因此可以这样:
- hosts: "{ { variable_host | default('web') }}"
然后在命令行中使用 variable_host 参数传入主机名称(或者主机组名):
ansible-playbook server.yml --extra-vars "variable_host=x.x.x.x"
Cache Plugins
jsonfile – JSON formatted files
使用 Cache 插件即可,比如我们想缓存到文件中:
ANSIBLE_CACHE_PLUGIN=jsonfile ANSIBLE_CACHE_PLUGIN_CONNECTION=/cache/saving/path ansible-playbook -i inventory.txt zabbix-agent.yaml
WikiNotes/使用 Playbook 脚本
ansible笔记(13):tags的用法
Why is ansible notify not working?