ansible入门到精通再到放弃

介绍

注:本文demo使用ansible2.9稳定版

ansible是一种自动化运维工具,基于paramiko开发的,并且基于模块化工作,Ansible是一种集成IT系统的配置管理、应用部署、执行特定任务的开源平台,它是基于python语言,由[]Paramiko和PyYAML两个关键模块构建。集合了众多运维工具的优点,实现了批量系统配置、批量程序部署、批量运行命令等功能.ansible是基于模块工作的,本身没有批量部署的能力.真正具有批量部署的是ansible所运行的模块,ansible只是提供一种框架.ansible不需要在远程主机上安装client/agents,因为它们是基于ssh来和远程主机通讯的.

ansible被定义为配置管理工具,配置管理工具通常具有以下功能:

  • 确保所依赖的软件包已经被安装
  • 配置文件包含正确的内容和正确的权限
  • 相关服务被正确运行

基本架构

ansible系统由控制主机和被管理主机组成,控制主机不支持windows平台

  • 核心: ansible
  • Core Modules: ansible自带的模块
  • Custom Modules: 核心模块功能不足时,用户可以添加扩展模块
  • Plugins: 通过插件来实现记录日志,发送邮件或其他功能
  • Playbooks: 剧本,YAML格式文件,多个任务定义在一个文件中,定义主机需要调用哪些模块来完成的功能
  • Connectior Plugins: ansible基于连接插件连接到各个主机上,默认是使用ssh
  • Host Inventory: 记录由Ansible管理的主机信息,包括端口、密码、ip等

特点

部署简单, 只需要在控制主机上部署ansible环境,被控制端上只要求安装ssh和python 2.5以上版本,这个对于类unix系统来说相当与无需配置.

  1. no angents: 被管控节点无需安装agent
  2. no server: 无服务端,使用是直接调用命名
  3. modules in any languages: 基于模块工作, 可以使用任意语言开发模块
  4. 易读的语法: 基于yaml语法编写playbook
  5. 基于推送模式: 不同于puppet的拉取模式,直接由调用者控制变更在服务器上发生的时间
  6. 模块是幂等性的:定义的任务已存在则不会做任何事情,意味着在同一台服务器上多次执行同一个playbook是安全的

任务执行模式

Ansible任务执行模式分为以下两种:

  • ad-hoc模式(点对点模块)
    使用单个模块,支持批量执行单条命令,相当与在bash中执行一句shell命令
  • playbook模式(剧本模式)
    ansible主要的管理方式,通过多个task的集合完成一类功能,可以理解为多个ad-hoc的配置文件

执行流程

  1. 读取 ansible.cfg
  2. 通过规则过滤 inventory 定义的主机
  3. 加载 task 对应模块文件
  4. 通过 core 将模块或命令打包成 python 脚本
  5. 将临时脚本传输至远程服务器
  6. 对应执行目录:~/.ansible/tmp/xxx/xxx.py
  7. 给文件加执行权限
  8. 执行并返回结果
  9. 删除临时文件并退出

安装配置

yum 安装

yum install ansible

pip 安装

pip install ansible

配置文件查找顺序

  1. 检查环境变量ANSIBLE_CONFIG指向的路径文件
  2. ~/.ansible.cfg
  3. /etc/ansible/ansible.cfg

常用配置参数

inventory = /etc/ansible/hosts      #这个参数表示资源清单inventory文件的位置
library = /usr/share/ansible        #指向存放Ansible模块的目录,支持多个目录方式,只要用冒号(:)隔开就可以
forks = 5       #并发连接数,默认为5
sudo_user = root        #设置默认执行命令的用户
remote_port = 22        #指定连接被管节点的管理端口,默认为22端口,建议修改,能够更加安全
host_key_checking = False       #设置是否检查SSH主机的密钥,值为True/False。关闭后第一次连接不会提示配置实例
timeout = 60        #设置SSH连接的超时时间,单位为秒
log_path = /var/log/ansible.log     #指定一个存储ansible日志的文件(默认不记录日志)

定义主机列表(Inventory)

ansible的主要功用在于批量主机操作,为了便捷地使用其中的部分主机,可以在inventory file中将其分组命名。默认的inventory file为/etc/ansible/hosts。
inventory file可以有多个,且也可以通过Dynamic Inventory来动态生成。

Inventory文件格式:

  • inventory文件遵循INI文件风格,中括号中的字符为组名。可以将同一个主机同时归并到多个不同的组中;此外,当如若目标主机使用了非默认的SSH端口,还可以在主机名称之后使用冒号加端口号来标明。
ntp.com

[webservers]
www1.com:2222
www2.com

[dbservers]
db1.com
db2.com
db3.com
  • 如果主机名称遵循相似的命名模式,还可以使用列表的方式标识各主机,例如:
[webservers]
www[01:50].example.com

[databases]
db-[a:f].example.com
  • 主机变量: 可以在inventory中定义主机时为其添加主机变量以便于在playbook中使用。例如:
[webservers]
www1.com http_port=80 maxRequestsPerChild=808
www2.com http_port=8080 maxRequestsPerChild=909
  • 组变量
[webservers]
www1.com
www2.com

[webservers:vars]
ntp_server=ntp.com
nfs_server=nfs.com

inventory其他的参数

  • ansible基于ssh连接inventory中指定的远程主机时,还可以通过参数指定其交互方式;这些参数如下所示:
ansible_ssh_host # 远程主机
ansible_ssh_port # 指定远程主机ssh端口
ansible_ssh_user # ssh连接远程主机的用户,默认root
ansible_ssh_pass # 连接远程主机使用的密码,在文件中明文,建议使用--ask-pass或者使用SSH keys
ansible_sudo_pass # sudo密码, 建议使用--ask-sudo-pass
ansible_connection # 指定连接类型: local, ssh(sshpass), paramiko
ansible_ssh_private_key_file # ssh 连接使用的私钥
ansible_shell_type # 指定连接对端的shell类型, 默认sh,支持csh,fish
ansible_python_interpreter # 指定对端使用的python编译器的路径

基于 ad-hoc 模式运行

ansible通过ssh实现配置管理、应用部署、任务执行等功能,因此,需要事先配置ansible端能基于密钥认证的方式联系各被管理节点。

ansible命令使用语法:

ansible <host-pattern> [-f forks] [-m module_name] [-a args]
# -m module:默认为command

例如:

  • 执行 ping 模块检查网络是否可达
# ansible all -m ping
192.168.57.22 | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}
192.168.57.11 | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}
  • 使用 command 模块远程执行命令:
# ansible test -m command -a 'grep root /etc/passwd' 
192.168.57.22 | SUCCESS | rc=0 >>
root:x:0:0:root:/root:/bin/bash
operator:x:11:0:operator:/root:/sbin/nologin

常用模块

  • 可以通过 ansible-doc -l 列出所有可用的 module
  • ansible -s 可以查看指定 module 的用法,或者参看官方帮助文档

ping:主机连通性测试

ansible all -m ping

command :在远程主机执行命令,不支持管道

ansible all -m command -a 'id'

shell:在远程主机调用shell执行命令,支持管道

ansible all -m shell -a 'echo "/tmp/test.txt" | xargs rm -f'

copy:复制文件到远程主机,支持设定内容和修改权限

ansible all -m copy -a 'src=/testdir/copytest dest=/testdir/ backup=yes'

file:创建文件,创建链接文件,删除文件等

ansible all -m file -a 'path=/testdir/testfile state=touch'	# 创建文件
ansible all -m file -a 'path=/testdir/testdir state=directory'	# 创建目录
ansible all -m file -a 'path=/testdir/testlink state=link src=/testdir/testfile"	# 创建软链接
ansible all -m file -a 'path=/testdir/testlink state=hard src=/testdir/testfile"	# 创建硬链接

fetch:从远程复制文件到本地

ansible all -m fetch -a "src=/etc/fstab dest=/testdir/ansible"

cron:管理cron计划任务

ansible all -m cron -a "minute=0 hour=0 day=* month=* weekday=* name='Ntpdate server for sync time' job='ntpdate 172.25.70.250'"	# 添加任务
ansible all -m cron -a "name='Ntpdate server for sync time' state=absent"	# 删除任务

yum:安装软件包

ansible all -m yum -a "name=openssl state=present"	# 安装软件 state 参数 present/latest
ansible all -m yum -a "name=openssl state=absent"	# 卸载软件

service:管理服务

ansible web -m service -a "name=httpd state=started"	# 启动httpd服务
ansible web -m service -a "name=httpd state=reloaded"	# 重载httpd服务
ansible web -m service -a "name=httpd state=restarted"	# 重启httpd服务
ansible web -m service -a "name=httpd state=stopped"	# 停止httpd服务
ansible web -m service -a "name=httpd enabled=yes"		# 开启开机自启
ansible web -m service -a "name=httpd enabled=no"		# 关闭开机自启

user:管理用户账号

ansible all -m user -a "name=dba groups=admins,dbagroup append=yes home=/home/dba shell=/bash/shell state=present"	# 创建账号
ansible all -m user -a "name=dba remove=yes state=absent"	# 删除用户并删除加目录
ansible all -m user -a "name=dba password=$6$GD8Q update_password=always"	# 更新用户密码,password 后面接的是加密以后的密码

对密码加密可以使用 python 的 crypt 和 passlib,passlib 需要安装 pip install passlib

进入到python的交互式里面
第一种:
import crypt
crypt.crypt("密码")
第二种:
from passlib.hash import sha512_crypt
sha512_crypt.hash("密码")

group:管理用户组

ansible all -m group -a "name=testgroup state=present" # 创建组
ansible all -m group -a "name=testgroup state=absent" # 删除组

script:将本地的脚本在远端服务器运行

ansible all -m script -a 'chdir=/opt /opt/test.sh'	# 执行前进入指定目录
ansible all -m script -a 'creates=/opt/a.file /opt/test.sh'	# 若/opt/a.file存在时,不执行 test.sh 脚本
ansible all -m script -a 'removes=/opt/a.file /opt/test.sh'	# 若/opt/a.file不存在时,不执行test.sh脚本

setup:该模块主要用于收集信息,是通过调用facts组件来实现的,以变量形式存储主机上的信息

ansible all -m setup
ansible all -m setup -a 'filter=ansible_memory_mb'	# 过滤结果

基于 playbook 执行

playbook是由一个或多个“play”组成的列表。play的主要功能在于将事先归并为一组的主机装扮成事先通过ansible中的task定义好的角色。从根本上来讲,所谓task无非是调用ansible的一个module。将多个play组织在一个playbook中,即可以让它们联同起来按事先编排的机制同唱一台大戏。

下面是一个简单示例:

- hosts: master
  user: test
  remote_user: root
  vars:
    motd_warning: 'WARNING: Use by master ONLY'
  tasks:
    - name: setup a MOTD
      copy: dest=/etc/motd content="{{ motd_warning }}"
      notify: say something
  handlers:
    - name: say something
      command: echo "copy OK"

playbooks的组成部分

playbooks是使用yaml语法格式,通过上面的示例可以看出一个play可以包含如下内容:

  • Target section: 定义要运行playbook的远程主机组

  • hosts: hosts用于指定要执行指定任务的主机,其可以是一个或多个由冒号分隔主机组,all 表示所有

  • user: 指定远程主机上的执行任务的用户,还可以指定sudo用户等

  • Variable section: 定义playbook运行时使用的变量

  • vars:定义变量

  • Task section: 定义要在远程主机上运行的任务列表

  • name: 每个任务都有name,建议描述任务执行步骤,未通过name会用执行结果作为name

  • 'module:options': 调用的module和传入的参数args

  • Handler section: 定义task完成后需要调用的任务

  • notify: 在Task Section在每个play的最后触发,调用在hendler中定义的操作

  • handler: 也是task的列表

playbooks是使用yaml语法格式,通过上面的示例可以看出一个play可以包含如下内容:

  • hosts:主机组,后面定义的task将作用于该主机组的所有主机,其可以是一个或多个由冒号分隔主机组,all 表示所有
  • vars:变量定义,在后面的task中可以引用
  • remote-user:连接参数,例如remote-user,become,become-user等等,这些参数将会覆盖ansible.cfg配置文件里的参数
  • tasks:任务,可以看作很多modules的集合,这些modules可以使用vars定义的变量
  • handlers:触发才会执行的task,很多情况下,当其他task被执行并且状态有改变后,我们希望会触发一些任务,那些被触发的任务可以写在这里,在 tasks 中使用 notify 调用

对应变量还可以直接引用变量文件:

vars:
  ...
vars_files:
- /vars/external_vars.yml

执行过程:

[root@localhost ansible]# ansible-playbook set_motd.yaml 

PLAY [master] **********************************************

TASK [Gathering Facts] **********************************************
ok: [192.168.57.11]

TASK [setup a MOTD] **********************************************
changed: [192.168.57.11]

RUNNING HANDLER [say something] **********************************************
changed: [192.168.57.11]

PLAY RECAP **********************************************
192.168.57.11              : ok=3    changed=2    unreachable=0    failed=0   

# cat /etc/motd 
WARNING: Use by master ONLY

变量引用

前面我们介绍了下变量的定义/引用方式和fact变量,那么在playbook中我们如何使用这些变量呢?

变量通常会在模版、条件判断语句、新的变量定义等处能用到。

使用变量的方法很简单,只需要将变量写在两个大括号内并且前后都有空格即可,同时我们必须将这个大括号用双引号引起来,如果变量穿插在字符串内使用,双引号也要将字符串部分引起来。

示例如下:

- hosts: app_servers
  vars:
    app_path: "{{ base_path }}/22"

如果一个变量定义比较复杂,例如列表、字典或fact(json格式),我们可以通过如下方式访问:

列表变量访问:

{{ foo[0] }}

字典变量访问:

{{ foo[name] }} 

{{ foo.name }}

json格式访问变量访问:

{{ansible_eth0["ipv4"]["address"] }}

{{ansible_eth0.ipv4.address }}

这里说一个小技巧,在我们排错过程中很多情况我们要debug一些变量。此时,可以使用debug模块输出变量。
debug模块有两种使用方式,vars和msg :

---
- hosts: node1
  gather_facts: false
  vars:
    - name: weimeng
    - age: 26
  tasks:
    - name: Use var debug
      debug:
        var: name,age
    - name: Use msg debug
      debug:
        msg: "my name is {{ name }},and my age is {{ age }}"

输入如下:

PLAY [node1] *******************************************************************

TASK [Use var debug] ***********************************************************
ok: [node1] => {
    "name,age": "(u'weimeng', 26)"
}

TASK [Use msg debug] ***********************************************************
ok: [node1] => {
    "msg": "my name is weimeng,and my age is 26"
}

PLAY RECAP *********************************************************************
node1                      : ok=2    changed=0    unreachable=0    failed=0

条件语句

ansible条件语句不是很多,比较常用的就是when语句和循环语句。

当满足一定的条件时,我们想要跳过某个task,这时候when语句出场了。当when语句的参数为true时,才会执行这个task,否则反之。

yum模块的name可以以列表的形式指定多个安装包,但是很多其他模块是不支持列表的,例如file的path,copy的src,等等;或者说我们想迭代的将一个列表元素传递给某个模块处理,如果有多少个元素写多个task就很麻烦。此时我们可以使用ansible的循环语句loop(ansible 2.5以后),在2.5版本之前可以使用with_,loop类似于旧版本的with_list语句。

when语句
ansible的when语句用于判断是否执行这个task,例如

tasks:
  - name: "shut down Debian flavored systems"
    command: /sbin/shutdown -t now
    when: ansible_os_family == "Debian"

示例中如果系统的类型是“Debian”才会执行/sbin/shutdown -t now命令。

条件语句也可以使用“and”和“or”:

tasks:
  - name: "shut down CentOS 6 and Debian 7 systems"
    command: /sbin/shutdown -t now
    when: (ansible_distribution == "CentOS" and ansible_distribution_major_version == "6") or
          (ansible_distribution == "Debian" and ansible_distribution_major_version == "7")

条件也可以写成列表的形式,这种形式和and语句起到一样的效果:

tasks:
  - name: "shut down CentOS 6 systems"
    command: /sbin/shutdown -t now
    when:
      - ansible_distribution == "CentOS"
      - ansible_distribution_major_version == "6"

register变量条件语句
通过对某个task的执行结果是否成功,决定另外一个task是否要执行:

tasks:
  - command: /bin/false
    register: result
    ignore_errors: True

  - command: /bin/something
    when: result is failed

  # In older versions of ansible use ``success``, now both are valid but succeeded uses the correct tense.
  - command: /bin/something_else
    when: result is succeeded

  - command: /bin/still/something_else
    when: result is skipped

变量是否被定义语句:

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 undefined

循环语句

上面说到loop类似于旧版本的with_list语句,也就是说loop会将列表的元素逐个传递给上面的module,从而达到重复执行的目的。

最简单的形式:

---
tasks:
  - command: echo { item }
    loop: [ 0, 2, 4, 6, 8, 10 ]

loop与when结合使用:

---
tasks:
- command: echo { item }
loop: [ 0, 2, 4, 6, 8, 10 ]
when: item > 5

通常loop语句会结合各式各样的filter去使用,例如“ loop: “{ { ['alice', 'bob'] |product(['clientdb', 'employeedb', 'providerdb'])|list }}””,这个例子和with_nested语句起到一样的效果。也就是说旧版本的with_ + lookup() 所能实现的,新版本的loop+filter同样能实现。

执行顺序

一般playbook里的task执行顺序和python一样,由上至下,定义的顺序即执行的顺序。同样的,使用include和import导入playbook或tasks也会安照导入顺序执行。

per_tasks和post_tasks

当playbook中有使用roles导入task和自定义tasks时,我们会发现ansible总会先执行roles导入的task,然后执行自定义的tasks,例如:

- hosts: localhost
  gather_facts: no
  vars:
    - ff: 1
    - gg: 2
  tasks:
    - debug:
        var: ff
  roles:
    - role: role_B 

输出结果:

PLAY [localhost] ***************************************************************

TASK [role_B : debug] **********************************************************
ok: [localhost] => {
    "a": 2
}

TASK [debug] *******************************************************************
ok: [localhost] => {
    "ff": 1
}

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0

从上面示例发现,虽然我们将tasks定义在了前面,但是tasks任务还是在roles任务之后执行。此时我们可以使用pre_task和post_task来强制指定执行顺序,例如:

---
- hosts: localhost
  gather_facts: no
  vars:
    - ff: 1
    - gg: 2
  pre_tasks:
    - import_role:
        name: role_A
      vars:
        age: 23
  roles:
    - role: role_B
  tasks:
    - debug:
        var: ff
  post_tasks:
    - debug:
        var: gg

总结下playbook里任务的执行顺序:

  • per_task包含的play
  • per_task中handlers任务
  • roles语句中role列表依次执行,每个role优先执行依赖的play及任务
  • tasks语句中的所有play
  • 前两步所触发的handlers
  • post_tasks包含的play
  • post_tasks触发的handlers

handlers

在部署应用时,通常的步骤是安装软件包->更改配置文件->初始化数据库->启动(重启)服务;升级的步骤一般是:升级软件包->更改配置文件->初始化数据库->重启服务。我们发现不管是新部署还是升级,最后一步都是要重新加载程序的,也就是说当我们升级了软件或者更改了配置文件都需要重启一下应用。

为了实现触发服务重启,ansible使用handlers方法定义重启的动作,handlers并不是每次执行playbook都会触发,而是某些指定资源状态改变时才会触发指定的handlers(这里使用“资源”一词借鉴于puppet)。

示例如下:

- name: template configuration file
  template:
    src: template.j2
    dest: /etc/foo.conf
  notify:
     - restart memcached
     - restart apache

上面的示例中,当/etc/foo.conf文件内容有改动时(返回changed),会触发重启memcached和apache服务,如果文件内容没有变化时(返回ok),则不会触发handlers。

ansible执行过程中并不会立即触发handlers动作,而是以play为单位,一个play执行完后最后才会触发handlers。

这样设计也是很合理的,试想在一个play内,如果触发一次就执行一次handlers,那么除了最后一次的重启,前面触发的重启都是无用功。

当然如果我们想要立即触发,也是可以的,在play定义“- meta: flush_handlers”即可。

另外需要注意的一点是,handlers触发的执行顺序是按照定义顺序执行,而不是按照notify指定的顺序执行。

playbook安装配置apache实战

  1. 编写playbook:install_httpd.yaml
---
- hosts: slave
  vars:
    http_port: 8080
  user: root
  tasks:
  - name: ensure apache is at the latest version
    yum: name=httpd state=latest
  - name: write the apache config file
    template:
      src: template/httpd.j2
      dest: /etc/httpd/conf/httpd.conf
    notify:
    - restart apache
  - name: ensure apache is running
    service: name=httpd state=started
  handlers:
    - name: restart apache
      service:
        name: httpd
        state: restarted
  • 首先使用 yum 模块安装最新 httpd
  • 然后使用 template 模块将 jinja2 模板文件 template/httpd.j2 种 {{ http_port }} 替换成 vars 中定义的变量,并上传替换目标服务器的 /etc/httpd/conf/httpd.conf 文件
  • 再使用 service 模块启动 httpd
  • 最后执行 notify 重启 httpd

执行结果:

[root@localhost ansible]# ansible-playbook install_httpd.yaml 

PLAY [slave] *******************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************
ok: [192.168.57.22]

TASK [ensure apache is at the latest version] **********************************************************************************
changed: [192.168.57.22]

TASK [write the apache config file] ********************************************************************************************
changed: [192.168.57.22]

TASK [ensure apache is running] ************************************************************************************************
changed: [192.168.57.22]

RUNNING HANDLER [restart apache] ***********************************************************************************************
changed: [192.168.57.22]

PLAY RECAP *********************************************************************************************************************
192.168.57.22              : ok=5    changed=4    unreachable=0    failed=0   

[root@localhost ansible]# ansible slave -m shell -a 'ss -lntp | grep 8080'
192.168.57.22 | SUCCESS | rc=0 >>
LISTEN     0      128         :::8080                    :::*                   users:(("httpd",pid=29187,fd=4),("httpd",pid=29185,fd=4),("httpd",pid=29184,fd=4),("httpd",pid=29183,fd=4),("httpd",pid=29182,fd=4),("httpd",pid=29181,fd=4))

roles

ansilbe自1.2版本引入的新特性,用于层次性、结构化地组织playbook。roles能够根据层次型结构自动装载变量文件、tasks以及handlers等。要使用roles只需要在playbook中使用include指令即可。简单来讲,roles就是通过分别将变量、文件、任务、模块及处理器放置于单独的目录中,并可以便捷地include它们的一种机制。角色一般用于基于主机构建服务的场景中,但也可以是用于构建守护进程等场景中。

roles 目录结构

webservers.yml
roles/
   httpd/ # 角色项目名称
     files/ # 用来存放由copy模块或script模块调用 src 参数的默认根目录
     templates/ # 用来存放jinjia2模板,template模块会自动在此目录中寻找jinjia2模板文件
     tasks/ # 此目录应当包含一个main.yml文件,用于定义此角色的任务列表,此文件可以使用include包含其它的位于此目录的task文件。
       main.yml
     handlers/ # 此目录应当包含一个main.yml文件,用于定义此角色中触发条件时执行的动作。
       main.yml
     vars/ # 此目录应当包含一个main.yml文件,用于定义此角色用到的变量。
       main.yml
     defaults/ # 此目录应当包含一个main.yml文件,用于为当前角色设定默认变量,优先级最低。
       main.yml
     meta/ # 此目录应当包含一个main.yml文件,用于定义此角色的特殊设定及其依赖关系。
       main.yml
     tests/ # 用于存放测试role本身功能的playbook和主机定义文件,在开发测试阶段比较常用。
       inventory/
       test.yml
  • files、handlers、meta、tasks、templates、defaults、test和vars,用不到的目录可以创建为空目录,也可以不创建

在 playbook webservers.yml 中,可以这样使用roles:

---
- hosts: webservers
  roles:
    - common
    - httpd

通常创建一个role的方法有两种:

  • 命令mkdir和touch行手动创建
  • 使用ansible-galaxy自动初始化一个role

role的引用与执行

比较常用的方法,我们可以使用「roles:」语句引用role :

---
- hosts: node1
  roles:
     - role_A

或者

---
- hosts: node1
  roles:
     - name: role_A
     - name: role_A

或者

---
- hosts: node1
  roles:
     - role: role_A
     - role: role_A

或者使用绝对路径:

---
# playbooks/test.yaml
- hosts: node1
  roles:
    - role: /root/lab-ansible/roles/role_A

引入的同时添加变量参数:

---
# playbooks/test.yaml
- hosts: node1
  roles:
    - role: role_A
      vars:
        name: Maurice
        age: 100

引入的同时添加tag参数:

---
# playbooks/test.yaml
- hosts: node1
  roles:
    - role: role_B
      tags:
        - tag_one
        - tag_two
    # 上面等价于
    - { role: role_B, tags:['tag_one','tag_two'] }

根据需求,我们在playbook中引用不同的role,引用后的效果也很好理解:ansible会把role所包含的任务、变量、handlers、依赖等加载到playbook中,顺次执行。

检索路径

上面介绍了使用「roles」语句的引用方法,那么ansible去哪找这些role呢?
在不使用绝对路径的情况下,ansible检索role的默认路径有:

  • 执行ansible-playbook命令时所在的当前目录
  • playbook文件所在的目录及playbook文件所在目录的roles目录
  • 当前系统用户下的~/.ansible/roles目录
  • /usr/share/ansible/roles目录
  • ansible.cfg 中「roles_path」指定的目录,默认值为/etc/ansible/roles目录

注:centos7 下经简单测试,发现/etc/ansible/roles加载优先级高于playbook当前目录

include和import引用

在后来版本(ansible>=2.4)中,ansible引入了「import_role」(静态)和「include_role」(动态)方法:

---
# playbooks/test.yaml
- hosts: node1
  tasks:
    - include_role:
        name: role_A
      vars:
        name: maurice
        age: 100
    - import_role:
        name: role_B

比较于「roles」语句,「import_role」和「include_role」的优点如下:

  • 可以在task之间穿插导入某些role,这点是「roles」没有的特性。
  • 更加灵活,可以使用「when」语句等判断是否导入。

include和import区别

ansible 目前有 import_tasks、include_tasks、import_playbook、include_playbook、import_role、include_role

import 和 include 区别相近:
区别一

  • import_tasks(Static)方法会在playbooks解析阶段将父task变量和子task变量全部读取并加载
  • include_tasks(Dynamic)方法则是在执行play之前才会加载自己变量

**区别二 **
​- include_tasks方法调用的文件名称可以加变量

  • import_tasks方法调用的文件名称不可以有变量

具体参考:https://www.cnblogs.com/mauricewei/p/10054041.html

也正是因为「include_task」是动态导入,当我们给「include_role」导入的role打tag时,实际并不会执行该role的task。

举个例子,当我们使用include导入role_A,使用import导入role_B时:

---
# playbooks/test.yaml
- hosts: node1
  tasks:
    - include_role:
        name: role_A
      tags: maurice
    - import_role:
        name: role_B
      tags: maurice

role_A内容如下:

---
# tasks file for role_A

- debug:
    msg: "age"

- debug:
    msg: "maurice"

执行结果显示,role_A虽然被引用,但里面的task并没有执行:

PLAY [node1] *************************************************************

TASK [Gathering Facts] *************************************************************
ok: [node1]

TASK [include_role : role_A] *************************************************************

TASK [role_B : debug] *************************************************************
ok: [node1] => {
    "msg": "I'm just role_B"
}

PLAY RECAP *************************************************************
node1                      : ok=2    changed=0    unreachable=0    failed=0

重复引用

不添加其他变量、tag等的情况下,一个playbook中对同一个role引入多次时,实际ansible只会执行一次。

例如:

---
# playbooks/test.yaml
- hosts: node1
  roles:
    - role_A
    - role_A

当然,我们也可以让其运行多次,方法如下:

  • 引用role的同时定义不同的变量
  • 在meta/main.yaml中添加「allow_duplicates: true」(这个特性实测未生效……)

roles的依赖

role的依赖指role_A可以引入其他的role,例如role_B。

roles的依赖知识点总结如下:

  • 配置的路径在role_A的meta/main.yaml
  • 引入role列表的方式只能使用类似「roles」语句的方法,只需将「roles」换为「dependencies」语句,不能用新版本的include和import语句
  • 在meta/main.yaml文件内可以引入多个role,且必须以列表的形式引入
  • 在引入role的同时,可以添加变量,方法同「roles」语句

多层依赖

被引入的role总是优先执行,即便是同一个 role 被引入了多次(遵循3.3重复引用规则)。

举个例子:

role「car」引入了role「wheel」:

---
# roles/car/meta/main.yml
dependencies:
- role: wheel
  vars:
     n: 1
- role: wheel
  vars:
     n: 2
- role: wheel
  vars:
     n: 3
- role: wheel
  vars:
     n: 4

同时,role「wheel」引入了role「tire」和「brake」:

---
# roles/wheel/meta/main.yml
dependencies:
- role: tire
- role: brake

最终的执行结果为:

tire(n=1)
brake(n=1)
wheel(n=1)
tire(n=2)
brake(n=2)
wheel(n=2)
...
car

插件和模块

我们可以为一个role自定义模块和插件,这部分属于高级特性,简单了解即可。

模块
给某个role自定义模块,需要在这个role的目录下创建「library」目录,然后将自定义模块放在该目录下。

插件
自定义插件的方法与模块类似,需要创建的目录为「filter_plugins」。

Ansible Galaxy

Ansible Galaxy是一个免费的roles仓库,里面有很多社区里现成稳定的role,在生产中的部署项目中,可以直接下载下来使用。

Ansible Galaxy客户端是「ansible-galaxy」,「ansible-galaxy」命令行可以用来初始化一个role,也可以用来从Ansible Galaxy下载role。

关于Ansible Galaxy的详细介绍,可以参考官网:https://galaxy.ansible.com/docs/

roles安装配置apache实战

通过ansible roles安装配置httpd服务,此处的roles使用默认的路径/etc/ansible/roles

  1. 创建目录
$ cd /etc/ansible/roles/
$ mkdir -p httpd/{handlers,tasks,templates,vars}
$ cd httpd/
  1. 变量文件准备vars/main.yml
PORT: 8088        #指定httpd监听的端口
USERNAME: www     #指定httpd运行用户
GROUPNAME: www    #指定httpd运行组
  1. 配置文件模板准备templates/httpd.conf.j2
$ cp /etc/httpd/conf/httpd.conf templates/httpd.conf.j2
# 进行一些修改,调用vars/main.yml定义的变量
$ vim templates/httpd.conf.j2
Listen {{ PORT }} 
User {{ USERNAME }}
Group {{ GROUPNAME }}
  1. 任务剧本编写,创建用户、创建组、安装软件、配置、启动等
# 创建组的task
$ vim tasks/group.yml
- name: Create a Startup Group
  group: name=www gid=60 system=yes

# 创建用户的task
$ vim tasks/user.yml
- name: Create Startup Users
  user: name=www uid=60 system=yes shell=/sbin/nologin

# 安装软件的task
$ vim tasks/install.yml
- name: Install Package Httpd
  yum: name=httpd state=latest

# 配置软件的task
$ vim tasks/config.yml
- name: Copy Httpd Template File
  template: src=httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf
  notify: Restart Httpd

# 启动软件的task
$ vim tasks/start.yml
- name: Start Httpd Service
  service: name=httpd state=started enabled=yes

# 编写main.yml,将上面的这些task引入进来
$ vim tasks/main.yml
- include: group.yml
- include: user.yml
- include: install.yml
- include: config.yml
- include: start.ym
  1. 编写重启 httpd 的handlers,handlers/main.yml
# 这里的名字name需要和task中的notify保持一致
- name: Restart Httpd
  service: name=httpd state=restarted
  1. 编写主的httpd_roles.yml文件调用httpd角色
$ cd ..
$ vim httpd_roles.yml
---
- hosts: all
  remote_user: root
  roles:
    - role: httpd        #指定角色名称
  1. 整体的一个目录结构查看
$ cd /etc/ansible/roles
$ tree .
.
├── httpd
│   ├── handlers
│   │   └── main.yml
│   ├── tasks
│   │   ├── config.yml
│   │   ├── group.yml
│   │   ├── install.yml
│   │   ├── main.yml
│   │   ├── start.yml
│   │   └── user.yml
│   ├── templates
│   │   └── httpd.conf.j2
│   └── vars
│       └── main.yml
└── httpd_roles.yml

5 directories, 10 files
  1. 执行 playbook
$ ansible-playbook -C httpd_roles.yml

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

TASK [Gathering Facts] **************************************************************************************
ok: [192.168.1.33]
ok: [192.168.1.32]
ok: [192.168.1.31]
ok: [192.168.1.36]

TASK [httpd : Create a Startup Group] ***********************************************************************
changed: [192.168.1.31]
changed: [192.168.1.33]
changed: [192.168.1.36]
changed: [192.168.1.32]

TASK [httpd : Create Startup Users] *************************************************************************
changed: [192.168.1.33]
changed: [192.168.1.32]
changed: [192.168.1.31]
changed: [192.168.1.36]

TASK [httpd : Install Package Httpd] ************************************************************************
changed: [192.168.1.33]
changed: [192.168.1.32]
changed: [192.168.1.31]
changed: [192.168.1.36]

TASK [httpd : Copy Httpd Template File] *********************************************************************
changed: [192.168.1.33]
changed: [192.168.1.36]
changed: [192.168.1.32]
changed: [192.168.1.31]

TASK [httpd : Start Httpd Service] **************************************************************************
changed: [192.168.1.36]
changed: [192.168.1.31]
changed: [192.168.1.32]
changed: [192.168.1.33]

RUNNING HANDLER [httpd : Restart Httpd] *********************************************************************
changed: [192.168.1.36]
changed: [192.168.1.33]
changed: [192.168.1.32]
changed: [192.168.1.31]

PLAY RECAP **************************************************************************************************
192.168.1.31               : ok=7    changed=6    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.1.32               : ok=7    changed=6    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.1.33               : ok=7    changed=6    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.1.36               : ok=7    changed=6    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
  • -C 仅测试语法
  1. 正式执行playbook
$ ansible-playbook httpd_roles.yml 

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

TASK [Gathering Facts] **************************************************************************************
ok: [192.168.1.33]
ok: [192.168.1.32]
ok: [192.168.1.31]
ok: [192.168.1.36]

TASK [httpd : Create a Startup Group] ***********************************************************************
changed: [192.168.1.31]
changed: [192.168.1.33]
changed: [192.168.1.36]
changed: [192.168.1.32]

TASK [httpd : Create Startup Users] *************************************************************************
changed: [192.168.1.33]
changed: [192.168.1.32]
changed: [192.168.1.31]
changed: [192.168.1.36]

TASK [httpd : Install Package Httpd] ************************************************************************
changed: [192.168.1.33]
changed: [192.168.1.32]
changed: [192.168.1.31]
changed: [192.168.1.36]

TASK [httpd : Copy Httpd Template File] *********************************************************************
changed: [192.168.1.33]
changed: [192.168.1.36]
changed: [192.168.1.32]
changed: [192.168.1.31]

TASK [httpd : Start Httpd Service] **************************************************************************
changed: [192.168.1.36]
changed: [192.168.1.31]
changed: [192.168.1.32]
changed: [192.168.1.33]

RUNNING HANDLER [httpd : Restart Httpd] *********************************************************************
changed: [192.168.1.36]
changed: [192.168.1.33]
changed: [192.168.1.32]
changed: [192.168.1.31]

PLAY RECAP **************************************************************************************************
192.168.1.31               : ok=7    changed=6    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.1.32               : ok=7    changed=6    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.1.33               : ok=7    changed=6    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.1.36               : ok=7    changed=6    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

[root@ansible roles]# ansible-playbook httpd_roles.yml 

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

TASK [Gathering Facts] **************************************************************************************
ok: [192.168.1.32]
ok: [192.168.1.33]
ok: [192.168.1.31]
ok: [192.168.1.36]

TASK [httpd : Create a Startup Group] ***********************************************************************
changed: [192.168.1.32]
changed: [192.168.1.31]
changed: [192.168.1.33]
changed: [192.168.1.36]

TASK [httpd : Create Startup Users] *************************************************************************
changed: [192.168.1.31]
changed: [192.168.1.33]
changed: [192.168.1.32]
changed: [192.168.1.36]

TASK [httpd : Install Package Httpd] ************************************************************************
changed: [192.168.1.31]
changed: [192.168.1.33]
changed: [192.168.1.32]
changed: [192.168.1.36]

TASK [httpd : Copy Httpd Template File] *********************************************************************
changed: [192.168.1.33]
changed: [192.168.1.32]
changed: [192.168.1.31]
changed: [192.168.1.36]

TASK [httpd : Start Httpd Service] **************************************************************************
fatal: [192.168.1.36]: FAILED! => {"changed": false, "msg": "httpd: Syntax error on line 56 of /etc/httpd/conf/httpd.conf: Include directory '/etc/httpd/conf.modules.d' not found\n"}
changed: [192.168.1.33]
changed: [192.168.1.32]
changed: [192.168.1.31]

RUNNING HANDLER [httpd : Restart Httpd] *********************************************************************
changed: [192.168.1.33]
changed: [192.168.1.32]
changed: [192.168.1.31]

PLAY RECAP **************************************************************************************************
192.168.1.31               : ok=7    changed=6    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.1.32               : ok=7    changed=6    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.1.33               : ok=7    changed=6    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.1.36               : ok=5    changed=4    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

ansible roles总结

  1. 编写任务(task)的时候,里面不需要写需要执行的主机,单纯的写某个任务是干什么的即可,装软件的就是装软件的,启动的就是启动的。单独做某一件事即可,最后通过main.yml将这些单独的任务安装执行顺序include进来即可,这样方便维护且一目了然。
  2. 定义变量时候直接安装k:v格式将变量写在vars/main.yml文件即可,然后task或者template直接调用即可,会自动去vars/main.yml文件里面去找。
  3. 定义handlers时候,直接在handlers/main.yml文件中写需要做什么事情即可,多可的话可以全部写在该文件里面,也可以像task那样分开来写,通过include引入一样的可以。在task调用notify时直接写与handlers名字对应即可(二者必须高度一直)。
  4. 模板文件一样放在templates目录下即可,task调用的时后直接写文件名字即可,会自动去到templates里面找。注意:如果是一个角色调用另外一个角色的单个task时后,那么task中如果有些模板或者文件,就得写绝对路径了。

jinja2模板

ansible使用的是jinja2模板引擎。当template模块对模板文件进行渲染时,使用的就是jinja2模板引擎;当在playbook中引用变量时,会将变量用双括号”{{ }}”括起,这也是jinja2的语法,在jinja2中,使用”{{ }}”装载变量,除了”{{ }}”,还有一些其他的jinja2基本语法,如下:

  • {{ }} :用来装载表达式,比如变量、运算表达式、比较表达式等。
  • {% %} :用来装载控制语句,比如 if 控制结构,for循环控制结构。
  • {# #} :用来装载注释,模板文件被渲染后,注释不会包含在最终生成的文件中。

变量 {

变量:

# 变量
# testvar = '123456'

# 模板内容
test jinja2 variable
test {{ testvar }} test

# 生成内容
test jinja2 variable
test {{ testvar }} test

比较表达式:

模板内容
jinja2 test
{{ 1 == 1 }}
{{ 2 != 2 }}
{{ 2 > 1 }}
{{ 2 >= 1 }}
{{ 2 < 1 }}
{{ 2 <= 1 }}
 
生成内容
jinja2 test
True
False
True
True
False
False

逻辑运算:

模板内容
jinja2 test
{{ (2 > 1) or (1 > 2) }}
{{ (2 > 1) and (1 > 2) }}
 
{{ not true }}
{{ not True }}
{{ not false }}
{{ not False }}
 
 
生成内容
jinja2 test
True
False
 
False
False
True
True

算数运算:

模板内容
jinja2 test
{{ 3 + 2 }}
{{ 3 - 4 }}
{{ 3 * 5 }}
{{ 2 ** 3 }}
{{ 7 / 5 }}
{{ 7 // 5 }}
{{ 17 % 5 }}
 
生成内容
jinja2 test
5
-1
15
8
1.4
1
2

成员运算:

模板内容
jinja2 test
{{ 1 in [1,2,3,4] }}
{{ 1 not in [1,2,3,4] }}
 
生成内容
jinja2 test
True
False

在上述成员运算的示例中,in 运算符后面对应的是一个”列表”,一些基础的数据类型,都可以包含在”{{ }}”中,jinja2本身就是基于python的模板引擎,所以,python的基础数据类型都可以包含在”{{ }}”中,相关示例如下:

模板内容
jinja2 test
### str
{{ 'testString' }}
{{ "testString" }}
### num
{{ 15 }}
{{ 18.8 }}
### list
{{ ['Aa','Bb','Cc','Dd'] }}
{{ ['Aa','Bb','Cc','Dd'].1 }}
{{ ['Aa','Bb','Cc','Dd'][1] }}
### tuple
{{ ('Aa','Bb','Cc','Dd') }}
{{ ('Aa','Bb','Cc','Dd').0 }}
{{ ('Aa','Bb','Cc','Dd')[0] }}
### dic
{{ {'name':'bob','age':18} }}
{{ {'name':'bob','age':18}.name }}
{{ {'name':'bob','age':18}['name'] }}
### Boolean
{{ True }}
{{ true }}
{{ False }}
{{ false }}
 
 
生成内容
jinja2 test
### str
testString
testString
### num
15
18.8
### list
['Aa', 'Bb', 'Cc', 'Dd']
Bb
Bb
### tuple
('Aa', 'Bb', 'Cc', 'Dd')
Aa
Aa
### dic
{'age': 18, 'name': 'bob'}
bob
bob
### Boolean
True
True
False
False

从上述示例模板文件可以看出,字符串、数值、列表、元组、字典、布尔值等数据类型均可在”{{ }}”使用,但是,通常我们不会像上述示例那样使用它们,因为通常我们会通过变量将对应的数据传入,而不是将数据直接写在”{{ }}”中,即使直接将数据写在”{{ }}”中,也会配合其他表达式或者函数进行处理,所以,我们可以把上述模板文件的内容改为如下内容进行测试:

jinja2 test
{{ teststr }}
{{ testnum }}
{{ testlist[1] }}
{{ testlist1[1] }}
{{ testdic['name'] }}

此处我们使用如下playbook对上述模板文件进行测试,而不是使用ad-hoc命令,原因稍后解释,测试playbook如下:

$ cat temptest.yml
---
- hosts: test70
 remote_user: root
 gather_facts: no
 vars:
   teststr: 'tstr'
   testnum: 18
   testlist: ['aA','bB','cC']
   testlist1:
   - AA
   - BB
   - CC
   testdic:
     name: bob
     age: 18
 tasks:
 - template:
     src: /testdir/ansible/test.j2
     dest: /opt/test

运行上例playbook以后,最终生成的文件如下

# cat test
jinja2 test
tstr
18
bB
BB
bob

刚才之所以用playbook进行测试,而不是使用ad-hoc命令,是因为在使用ad-hoc命令时,把列表、数字、字典等数据类型当做参数传入后,这些参数会被默认当做字符串进行处理,所以上例没有通过ad-hoc命令直接进行测试,而是通过playbook的方式渲染模板。

除了变量和各种常用的运算符,过滤器也可以直接在”{{ }}”中使用,与前文示例中的用法没有任何区别,示例如下:

模板内容
jinja2 test
{{ 'abc' | upper }}
 
 
生成内容
jinja2 test
ABC
  • 过滤器很多很强大,后面单独一节讲

当然,jinja2的tests自然也能够在”{{ }}”中使用,与前文中的用法也是一样的,如下:

模板内容
jinja2 test
{{ testvar1 is defined }}
{{ testvar1 is undefined }}
{{ '/opt' is exists }}
{{ '/opt' is file }}
{{ '/opt' is directory }}
 
执行命令时传入变量
# ansible test70 -m template -e "testvar1=1 testvar2=2" -a "src=test.j2 dest=/opt/test"
 
生成内容
jinja2 test
True
False
True
False
True

说了”过滤器”和”tests”,怎么能缺了”lookup”,示例如下:

模板内容
jinja2 test
 
{{ lookup('file','/testdir/testfile') }}
 
{{ lookup('env','PATH') }}
 
test jinja2
 
 
ansible主机中的testfile内容如下
# cat /testdir/testfile
testfile in ansible
These are for testing purposes only
 
 
生成内容
jinja2 test
 
testfile in ansible
These are for testing purposes only
 
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
 
test jinja2

注释

在jinja2中,使用{##}包含注释信息,如下:

模板内容
jinja2 test
{#这是一行注释信息#}
jinja2 test
{#
这是多行注释信息,
模板被渲染以后,
最终的文件中不会包含这些信息
#}
jinja2 test
 
 
生成内容
jinja2 test
jinja2 test
jinja2 test

控制语句

在jinja2中,使用{%%}对控制语句进行包含,比如 if 控制语句、for 循环控制语句等都需要包含在 {%%} 中。

if

if,格式如下:

{% if 条件 %}
...
...
...
{% endif %}

模板:

# cat test.j2
jinja2 test
 
{% if testnum > 3 %}
greater than 3
{% endif %}

除了 if 结构,当然还有 if…else… 结构:

{% if 条件 %}
...
{% else %}
...
{% endif %}

与其他语言一样,也有 if…else if… 的语法结构,如下:

{% if 条件一 %}
...
{% elif 条件二 %}
...
{% elif 条件N %}
...
{% endif %}

或者结合在一起使用,语法如下

{% if 条件一 %}
...
{% elif 条件N %}
...
{% else %}
...
{% endif %}

还有”if”表达式,利用”if”表达式,可以实现类似三元运算的效果:

{{ 'a' if 2>1 else 'b' }}

在前文的示例中,都是在playbook中定义变量,然后在模板文件中使用变量,其实也可以直接在模板文件中定义变量:

{% set teststr='abc' %}
{{ teststr }}

for

for循环的基本语法:

{% for 迭代变量 in 可迭代对象 %}
{{ 迭代变量 }}
{% endfor %}

先看一个简单的for循环示例,如下

{% for i in [3,1,7,8,2] %}
{{ i }}
{% endfor %}

最终生成的文件内容如下:

3
1
7
8
2

从生成的内容可以看出,每次循环后都会自动换行,如果不想要换行,则可以使用如下语法

{% for i in [3,1,7,8,2] -%}
{{ i }}
{%- endfor %}

for的结束控制符 %} 之前添加了减号 -,在endfor的开始控制符 {% 之后添加到了减号 -

渲染上述模板,最终生成内容:

31782

如上所示,列表中的每一项都没有换行,而是连在了一起显示,如果你觉得这样显示有些”拥挤”,则可以稍微改进一下上述模板,如下:

{% for i in [3,1,7,8,2] -%}
{{ i }}{{ ' ' }}
{%- endfor %}

生成内容:

3 1 7 8 2

其实,还有更加简洁的写法,就是将上述模板内容修改为如下内容:

{% for i in [3,1,7,8,2] -%}
{{ i~' ' }}
{%- endfor %}

如上例所示,直接在迭代变量的后面使用了波浪符””,并且用波浪符将迭代变量和空格字符串连在一起,渲染上述模板内容,最终生成内容的效果与刚才示例中的效果是相同的,在jinja2中,波浪符””就是字符串连接符,它会把所有的操作数转换为字符串,并且连接它们。

“for”除了能够循环操作列表,也能够循环操作字典,示例如下:

{% for key,val in {'name':'bob','age':18}.iteritems() %}
{{ key ~ ':' ~ val }}
{% endfor %}

如上所示,在循环操作字典时,先使用iteritems函数对字典进行处理,然后使用key和val两个变量作为迭代变量,分别用于存放字典中键值对的”键”和”值”,所以,直接输出两个变量的值即可,key和val是我随意起的变量名,你可以自己定义这两个迭代变量的名称,而且,上例中的iteritems函数也可以替换成items函数,但是推荐使用iteritems函数,上例最终生成内容如下:

age:18
name:bob

在使用for循环时,有一些内置的特殊变量可以使用,比如,如果想要知道当前循环操作为整个循环的第几次操作,则可以借助”loop.index”特殊变量,示例如下:

{% for i in [3,1,7,8,2] %}
{{ i ~ '----' ~ loop.index }}
{% endfor %}

最终生成文件内容如下:

3----1
1----2
7----3
8----4
2----5

除了内置特殊变量”loop.index”,还有一些其他的内置变量,它们的作用如下(此处先简单的进行介绍,之后会给出示例):

loop.index   当前循环操作为整个循环的第几次循环,序号从1开始
loop.index0   当前循环操作为整个循环的第几次循环,序号从0开始
loop.revindex  当前循环操作距离整个循环结束还有几次,序号到1结束
loop.revindex0 当前循环操作距离整个循环结束还有几次,序号到0结束
loop.first    当操作可迭代对象中的第一个元素时,此变量的值为true
loop.last    当操作可迭代对象中的最后一个元素时,此变量的值为true
loop.length   可迭代对象的长度
loop.depth   当使用递归的循环时,当前迭代所在的递归中的层级,层级序号从1开始
loop.depth0   当使用递归的循环时,当前迭代所在的递归中的层级,层级序号从0开始
loop.cycle()  这是一个辅助函数,通过这个函数我们可以在指定的一些值中进行轮询取值,具体参考之后的示例

如果只是想单纯的对一段内容循环的生成指定的次数,则可以借助range函数完成,比如,循环3次

{% for i in range(3) %}
something
...
{% endfor %}

当然,range函数可以指定起始数字、结束数字、步长等,默认的起始数字为0,

{% for i in range(1,4,2) %}
{{i}}
{% endfor %}

上例表示从1开始,到4结束(不包括4),步长为2,也就是说只有1和3会输出。

默认情况下,模板中的for循环不能像其他语言中的 for循环那样使用break或者continue跳出循环,但是可以在”for”循环中添加”if”过滤条件,以便符合条件时,循环才执行真正的操作,示例如下:

{% for i in [7,1,5,3,9] if i > 3 %}
{{ i }}
{% endfor %}

上述 示例表示只有列表中的数字大于3时,才输出列表中的元素,刚才在介绍if表达式时,我们说过,if表达式可以和其他控制语句结合使用,就是这个意思,上例的语法就是 “if内联表达式” 和 “for循环控制结构” 结合在一起的使用方式。

for循环中使用if判断控制语句进行判断不是也可以实现上述语法的效果吗?比如,使用如下示例的写法。

{% for i in [7,1,5,3,9] %}
  {% if i>3 %}
    {{ i }}
  {%endif%}
{% endfor %}

没错,如果仅仅是为了根据条件进行过滤,上述两种写法并没有什么不同,但是,如果需要在循环中使用到loop.index这种计数变量时,两种写法则会有所区别,具体区别渲染如下模板内容后则会很明显的看出来:

{% for i in [7,1,5,3,9] if i>3 %}
{{ i ~'----'~ loop.index }}
{% endfor %}
 
{% for i in [7,1,5,3,9] %}
{% if i>3 %}
{{ i ~'----'~ loop.index}}
{% endif %}
{% endfor %}

生成内容:

7----1
5----2
9----3
 
7----1
5----3
9----5

从上述结果可以看出,当使用if内联表达式时,如果不满足对应条件,则不会进入当次迭代,所以loop.index也不会进行计算,而当使用if控制语句进行判断时,其实已经进入了当次迭代,loop.index也已经进行了计算。

当for循环中使用了if内联表达式时,还可以与else控制语句结合使用,示例如下:

{% for i in [7,1,5,3,9] if i>10 %}
{{ i }}
{%else%}
no one is greater than 10
{% endfor %}

如上例所示,for循环中存在if内联表达式,if对应的条件为i > 10,即元素的值必须大于10,才回执行一次迭代操作,而for循环中还有一个else控制语句,else控制语句之后也有一行文本,那么上例是什么意思呢?上述示例表示,如果列表中的元素大于10,则进入当次迭代,输出”i”的值,if对应的条件成立时,else块后的内容不执行,如果列表中没有任何一个元素大于10,即任何一个元素都不满足条件,则渲染else块后面的内容。

其实,当for循环中没有使用if内联表达式时,也可以使用else块,示例如下

{% for u in userlist %}
  {{ u.name }}
{%else%}
  no one
{% endfor %}

上例中,只有userlist列表为空时,才会渲染else块后的内容。

所以,综上所述,如果因序列为空或者有条件过滤了序列中的所有项目而没有执行循环时,可以使用else渲染一个用于替换的块。

默认情况下,模板中的for循环无法使用break和continue,不过jinja2支持一些扩展,如果在ansible中启用这些扩展,则可以让模板中的for循环支持break和continue,方法如下:

如果想要开启对应的扩展支持,需要修改ansible的配置文件/etc/ansible/ansible.cfg,默认情况下未启用jinja2的扩展,如果想
要启用jinja2扩展,则需要设置jinja2_extension选项,这个设置项默认情况下是注释的,我的默认设置如下

#jinja2_extensions = jinja2.ext.do,jinja2.ext.i18n

把注释符去掉,默认已经有两个扩展了,如果想要支持break和continue,则需要添加一个loopcontrols扩展,最终配置如下

jinja2_extensions = jinja2.ext.do,jinja2.ext.i18n,jinja2.ext.loopcontrols

完成上述配置步骤即可在for循环中使用break和continue控制语句,与其他语言一样,break表示结束整个循环,continue表示结束当次循环,示例如下:

{% for i in [7,1,5,3,9] %}
  {% if loop.index is even %}
    {%continue%}
  {%endif%}
  {{ i ~'----'~ loop.index }}
{% endfor %}

break的示例如下:

{% for i in [7,1,5,3,9] %}
  {% if loop.index > 3 %}
    {%break%}
  {%endif%}
  {{i ~'---'~ loop.index}}
{% endfor %}

如果想要在jinja2中修改列表中的内容,则需要借助jinja2的另一个扩展,这个扩展的名字就是”do”。默认就有这个扩展,它的名字是jinja2.ext.do,通过do扩展修改列表的示例如下:

{% set testlist=[3,5] %}
 
{% for i in testlist  %}
  {{i}}
{% endfor %}
 
{%do testlist.append(7)%}
 
{% for i in testlist  %}
  {{i}}
{% endfor %}

如上例所示,先定义了一个列表,然后遍历了这个列表,使用 do 在列表的末尾添加了一个元素,数字7,然后又遍历了它。

转义

前文中已经总结了jinja2模板的一些基础用法,比如,变量和表达式被包含在”{{ }}”中,控制语句被包含在”{% %}”, 注释被包含在”{# #}”中,也就是说,在模板文件中,一旦遇到”{{ }}”、”{% %}”或者”{# #}”,jinja2模板引擎就会进行相应的处理,最终生成的文件内容中并不会包含”{{ }}”或者”{% %}”这些字符,但是如果,最终生成的文件中就是需要有”{{ }}”这一类的字符,该怎么办呢?有如下几种方法。

最简单的方法就是直接在”{{ }}”中使用引号将这类符号引起,当做纯粹的字符串进行处理,示例模板内容如下:

{{  '{{' }}
{{  '}}' }}
{{ '{{ test string }}' }}
{{ '{% test string %}' }}
{{ '{# test string #}' }}

上述模板最终生成的内容如下:

{{
}}
{{ test string }}
{% test string %}
{# test string #}

但是如果有较多这样的符号都需要保持原样(不被jinja2解析),那么使用上述方法可能比较麻烦,因为行多量大,大段的符号需要注意就比较烦人,所以,如果有较大的段落时,可以借助”{% raw %}”块,来实现刚才的需求,示例如下:

{% raw %}
  {{ test }}
  {% test %}
  {# test #}
  {% if %}
  {% for %}
{% endraw %}

如上例所示,”{% raw %}”与”{% endraw %}”之间的所有”{{ }}”、”{% %}”或者”{# #}”都不会被jinja2解析,上例模板被渲染后,raw块中的符号都会保持原样,最终生成内容如下:

{{ test }}
{% test %}
{# test #}
{% if %}
{% for %}

包含

在jinja2中,也可以像其他语言一样使用”include”对其他文件进行包含,比如,有两个模板文件,test.j2和test1.j2,想要在test.j2中包含test1.j2,则可以使用如下方法

$ cat test.j2
test...................
test...................
{% include 'test1.j2' %}
 
test...................
 
$ cat test1.j2
test1.j2 start
{% for i in range(3) %}
{{i}}
{% endfor %}
test1.j2 end

如果在test.j2中定义了一个变量,那么在被包含的test1.j2中可以使用这个在test.j2中的变量吗?示例如下:

# cat test.j2
{% set varintest='var in test.j2' %}
test...................
test...................
{% include 'test1.j2' %}
 
test...................
 
# cat test1.j2
test1.j2 start
{{ varintest }}
test1.j2 end

如上例所示,在test.j2中定义了varintest变量,然后在test1.j2中引用了这个变量,那么渲染test.j2模板,最终结果如下

test...................
test...................
test1.j2 start
var in test.j2
test1.j2 end
test...................

由此可见,被包含的文件在默认情况下是可以使用test.j2中定义的变量的,这是因为在默认情况下,使用”include”时,会导入当前环境的上下文,通俗点说就是,如果你在外部文件中定义了变量,通过include包含了文件以后,被包含文件中可以使用之前外部文件中定义的变量。

当然,如果不想让被包含文件能够使用到外部文件中定义的变量,则可以使用”without context”显式的设置”include”,当”include”中存在”without context”时,表示不导入对应的上下文,示例如下:

$ cat test.j2
{% set varintest='var in test.j2' %}
test...................
test...................
{% include 'test1.j2' without context %}
 
test...................
 
$ cat test1.j2
test1.j2 start
{{ varintest }}
test1.j2 end

如上例所示,在test.j2中包含了test1.j2文件,在包含时使用了”without context”,同时,在test1.j2中调用了test.j2中定义的变量,此时如果渲染test.j2文件,则会报错,这是因为显式的设置了不导入上下文,所以无法在test1.j2中使用test.j2中定义的变量,按照上例渲染test.j2文件,会出现如下错误:

# ansible test70 -m template -a "src=test.j2 dest=/opt/test"
test70 | FAILED! => {
    "changed": false,
    "msg": "AnsibleError: Unexpected templating type error occurred on ({% set varintest='var in test.j2' %}\ntest...................\ntest...................\n{% include 'test1.j2' without context %}\n\ntest...................\n): argument of type 'NoneType' is not iterable"
}

注意:如果在”include”时设置了”without context”,那么在被包含的文件中使用for循环时,不能让使用range()函数,也就是说,下例中的test.j2文件无法被正常渲染

# cat test.j2
test...................
test...................
{% include 'test1.j2' without context %}
 
test...................
 
# cat test1.j2
test1.j2 start
{% for i in range(3) %}
{{i}}
{% endfor %}
test1.j2 end

在ansible中渲染上例中的test.j2文件,会报错,报错信息中同样包含”argument of type ‘NoneType’ is not iterable”。

也可以显式的指定”with context”,表示导入上下文,示例如下:

cat test.j2
test...................
test...................
{% include 'test1.j2' with context %}
 
test...................

如上例所示,在使用”include”时,显式指定了”with context”,表示导入对应的上下文,当然,在默认情况下,即使不使用”with context”,”include”也会导入对应的上下文,所以,如下两种写法是等效的。

{% include 'test1.j2' %}
{% include 'test1.j2' with context %}

默认情况下,如果指定包含的文件不存在,则会报错,示例如下:

# cat test.j2
test...................
test...................
{% include 'test1.j2' with context %}
 
test...................
{% include 'test2.j2' with context %}

如上例所示,在test.j2中指定包含了两个文件,test1.j2和test2.j2,但是并没有编写所谓的test2.j2,所以,当渲染test.j2模板时,会报如下错误:

# ansible test70 -m template -a "src=test.j2 dest=/opt/test"
test70 | FAILED! => {
    "changed": false,
    "msg": "TemplateNotFound: test2.j2"
}

那么有没有一种方法,能够在指定包含的文件不存在时,自动忽略包含对应的文件呢?答案是肯定的,使用”ignore missing”标记皆可,示例如下:

# cat test.j2
test...................
test...................
{% include 'test1.j2' with context %}
 
test...................
{% include 'test2.j2' ignore missing with context %}

如上例所示,虽然test2.j2文件不存在,但是渲染test.j2文件时不会报错,因为使用”ignore missing”后,如果需要包含的文件不存在,会自动忽略对应的文件,不会报错。

导入

说完了”{% include %}”,再来聊聊”{% import %}”,include的作用是在模板中包含另一个模板文件,而import的作用是在一个文件中导入其他文件中的宏。具体方法不表。

继承

除了”包含”和”导入”的能力,jinja2模板引擎还有一个非常棒的能力,就是”继承”,”继承”可以更加灵活的生成模板文件。

可以先定义一个父模板,然后在父模板中定义一些”块”,不同的内容放在不同的块中,之后再定义一个子模板,这个子模板继承自刚才定义的父模板,可以在子模板中写一些内容,这些内容可以覆盖父模板中对应的内容,这样说可能不太容易理解,先来看个小例子。

$ cat test.j2
something in test.j2...
something in test.j2...
{% block test %}
Some of the options that might be replaced
{% endblock %}
something in test.j2...
something in test.j2...

如上例所示,test.j2就是刚才描述的”父模板”文件,这个文件中并没有太多内容,只是有一些文本,以及一个”块”,这个块通过”{% block %}”和”{% endblock %}”定义,块的名字为”test”,test块中有一行文本,可以直接渲染这个文件,渲染后的结果如下

something in test.j2...
something in test.j2...
Some of the options that might be replaced
something in test.j2...
something in test.j2...

直接渲染这个父模板,父模板中的块并没有对父模板有任何影响,现在,定义一个子模板文件,并且指明这个子模板继承自这个父模板,示例如下:

$ cat test1.j2
{% extends 'test.j2' %}
 
{% block test %}
aaaaaaaaaaaaaa
11111111111111
{% endblock %}

如上所示,”{% extends ‘test.j2’ %}”表示当前文件继承自test.j2文件,test.j2文件中的内容都会被继承过来,而test1.j2文件中同样有一个test块,test块中有两行文本,那么渲染test1.j2文件,得到的结果如下:

something in test.j2...
something in test.j2...
aaaaaaaaaaaaaa
11111111111111
something in test.j2...
something in test.j2...

从上述结果可以看出 ,最终生成的内容中,子模板中的test块中的内容覆盖了父模板中的test块的内容。

这就是继承的使用方法,其实很简单,也可以在父模板的块中不写任何内容,而是靠子模板去填充对应的内容,示例如下:

$ cat test.j2
something in test.j2...
something in test.j2...
{% block test %}
{% endblock %}
something in test.j2...
something in test.j2...
 
# cat test1.j2
{% extends 'test.j2' %}
 
{% block test %}
aaaaaaaaaaaaaa
11111111111111
{% endblock %}

其实上例与之前的示例并没有什么区别,只是上例中父模板的块中没有默认的内容,而之前的示例中父模板的块中有默认的内容而已。

父模板中也可以存在多个不同名称的block,以便将不同的内容从逻辑上分开,放置在不同的块中。

过滤器

相比较于 python 原生的 Jinja2 模版,ansible 扩展了很多过滤器和测试变量,同时也添加了一个新的插件「lookups」

数据格式化过滤器

过滤器「to_json」「to_yaml」,将变量转换为json和yaml格式

{{ some_variable | to_json }}
{{ some_variable | to_yaml }}

过滤器「to_nice_json」「to_nice_yaml」,将变量转换为更加友好的json和yaml格式

{{ some_variable | to_nice_json }}
{{ some_variable | to_nice_yaml }}

也可以自定义缩进的大小

{{ some_variable | to_nice_json(indent=2) }}
{{ some_variable | to_nice_yaml(indent=8) }}

过滤器「from_json」「from_yaml」,从已经格式化好了的变量读取数据:

{{ some_variable | from_json }}
{{ some_variable | from_yaml }}

「from_json」示例,从file.json文件读取json数据:

tasks:
  - shell: cat /some/path/to/file.json
    register: result
  - set_fact:
      myvar: "{{ result.stdout | from_json }}"

过滤器「from_yaml_all」,用来解析YAML多文档文件

tasks:
  - shell: cat /some/path/to/multidoc-file.yaml
    register: result
 - debug:
     msg: '{{ item }}'
  loop: '{{ result.stdout | from_yaml_all | list }}'

YAML多文档文件指一个文件中包含多个yaml数据文档,例如:

---
part_one: one
...

---
part_two: two
...

变量强制定义过滤器

当引用一个未被定义的变量时,ansible默认会报错,当然可以通过更改ansible.cfg配置项的方式关闭这种机制(即设置[defaults]字段下的error_on_undefined_vars = False)。

在关闭这个机制的情况下,如果想让ansible强制检查某个变量是否定义,可以使用「mandatory」过滤器,写法如下:

{{ variable | mandatory }}

此时,如果变量「variable」被定义了,则引用,否则会报错:

fatal: [node1]: FAILED! => {"msg": "Mandatory variable 'aaa' not defined."}

变量默认值过滤器

「default」过滤器可以为未定义变量设置默认值,类似于roles/defaults/main.yaml里定义的变量,优先级最低(变量优先级参考ansible基础-变量)。

示例如下:

{{ some_variable | default(5) }}

另外,如果想将变量参数是false、False和空(None)视为未定义,则必须给defaults过滤器第二参数位置加上「true」:

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

上面示例中表示:从环境变量中查找「MU_USER」变量,如果变量值为false、False、空(None)、未定义则将其设置为「admin」。否则引用之前被定义的参数。

可删除参数过滤器

过滤器「omit」:在使用模块的时候,有些参数的存在与否可以取决于变量是否被定义:

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

上面示例表示:变量中如果定义了变量「mode」,file模块则使用mode参数,否则就不使用。

执行结果为「/tmp/foo」和「/tmp/bar」文件使用默认权限,「/tmp/baz」文件使用「0444」权限。

列表过滤器

过滤器「min」,获取最小值元素

{{ list1 | min }}

过滤器「max」,获取最大值元素

{{ [3, 4, 2] | max }}

过滤器「flatten」,扁平化列表元素

{{ [3, [4, 2] ] | flatten }}

转换结果为:

[3, 4, 2 ]

过滤器「flatten」,并且指定级别

{{ [3, [4, [2]] ] | flatten(levels=1) }}

过滤器「unique」,给列表元素去重

{{ list1 | unique }}

过滤器「union」,合并两个列表后去重

{{ list1 | union(list2) }}

过滤器「intersect」,取两个列表相同的元素

{{ list1 | intersect(list2) }}

过滤器「difference」,去掉list1中与list2相同的元素,返回list1中剩余的元素

{{ list1 | difference(list2) }}

过滤器「symmetric_difference」,去掉list1与list2相同的元素,返回list1和list2剩余元素的集合

{{ list1 | symmetric_difference(list2) }}

随机Mac地址数过滤器

过滤器「random_mac」,在一个MAC地址前缀的基础上,随机生成mac地址。

"{{ '52:54:00' | random_mac }}"
# => '52:54:00:ef:1c:03'

注:如果给出的MAC地址前缀格式有问题,ansible会报错。

随机数过滤器

过滤器「random」,用于生成随机数,操作对象可以是一个列表也可以是一个数字。

从列表里随机获取一个数值:

"{{ ['a','b','c'] | random }}"
# => 'c'

从数字0到60之间获取一个随机数:

"{{ 60 | random }} * * * * root /script/from/cron"
# => '21 * * * * root /script/from/cron'

从数字10到100之间获取一个随机数,间隔设置为10:

{{ 101 | random(1, 10) }}
# => 31
{{ 101 | random(start=1, step=10) }}
# => 51

添加「seed」参数可以根据指定变量获取一个随机数,用于满足幂等性需求:

"{{ 60 | random(seed=inventory_hostname) }} * * * * root /script/from/cron"

打乱列表顺序过滤器

过滤器「shuffle」,用于给一个列表重新排序,每次排序随机:

{{ ['a','b','c'] | shuffle }}
# => ['c','a','b']
{{ ['a','b','c'] | shuffle }}
# => ['b','c','a']

添加「seed」参数可以根据指定变量获取一个随机排序,用于满足幂等性要求,此时,每次执行playbook获取到的列表顺序是固定的:

{{ ['a','b','c'] | shuffle(seed=inventory_hostname) }}
# => ['b','a','c']

Json数据查询过滤器

过滤器「json_query」,用于从json数据变量中摘取出一部分数据:

例如,下面是一个完整的json格式的变量

domain_definition:
    domain:
        cluster:
            - name: "cluster1"
            - name: "cluster2"
        server:
            - name: "server11"
              cluster: "cluster1"
              port: "8080"
            - name: "server12"
              cluster: "cluster1"
              port: "8090"
            - name: "server21"
              cluster: "cluster2"
              port: "9080"
            - name: "server22"
              cluster: "cluster2"
              port: "9090"
        library:
            - name: "lib1"
              target: "cluster1"
            - name: "lib2"

从这个变量中摘取出所有的「name」:

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

摘取cluster1的port:

- name: "Display all ports from cluster1"
  debug:
    var: item
  loop: "{{ domain_definition | json_query(server_name_cluster1_query) }}"
  vars:
    server_name_cluster1_query: "domain.server[?cluster=='cluster1'].port"

摘取cluster2的name和port:

- name: "Display all server ports and names from cluster1"
  debug:
    var: item
  loop: "{{ domain_definition | json_query(server_name_cluster1_query) }}"
  vars:
    server_name_cluster1_query: "domain.server[?cluster=='cluster2'].{name: name, port: port}"

IP地址过滤器

过滤器「ipaddr」,用于测试是否为IP地址格式:

{{ myvar | ipaddr }}

「ipaddr」过滤器也可以用于摘取出一个IP地址的指定信息:

例如,从一个CIDR摘取出IP地址信息:

{{ '192.0.2.1/24' | ipaddr('address') }}

输出结果为:

"msg": "192.0.2.1"

过滤器「ipv4」「ipv6」,用于检测ipv4和ipv6协议的IP地址:

{{ myvar | ipv4 }}
{{ myvar | ipv6 }}

哈希值过滤器

过滤器「hash」,用于获取字符串的hash值。

获取字符串的sha1哈希值:

{{ 'test1' | hash('sha1') }}

获取字符串的md5哈希值:

{{ 'test2' | hash('blowfish') }}

过滤器「checksum」,用于获取字符串的checksum:

{{ 'test2' | checksum }}

过滤器「password_hash」,用于获取一个密码的哈希值。

获取sha512密码哈希值,结果随机:

{{ 'passwordsaresecret' | password_hash('sha512') }}

获取sha256密码哈希值,并加盐处理,结果满足幂等性:

{{ 'secretpassword' | password_hash('sha256', 'mysecretsalt') }}

根据主机名获取一个满足幂等原则的sha256密码,写法如下:

{{ 'secretpassword' | password_hash('sha512', 65534 | random(seed=inventory_hostname) | string) }}

一些hash类型也允许提供「rounds」参数:

{{ 'secretpassword' | password_hash('sha256', 'mysecretsalt', rounds=10000) }}

注:关于哈希加盐和rounds请自行Google。

注释过滤器

过滤器「comment」,可以实现注释字符串的功能,默认为「#」注释。

{{ "Plain style (default)" | comment }}

结果为:

#
# Plain style (default)
#

也可以为 C (//...), C block (/.../), Erlang (%...) 和XML ()做注释,分别为:

{{ "C style" | comment('c') }}
{{ "C block style" | comment('cblock') }}
{{ "Erlang style" | comment('erlang') }}
{{ "XML style" | comment('xml') }}

使用「decoration」参数可以人为指定注释符号:

{{ "My Special Case" | comment(decoration="! ") }}

输出结果为:

!
! My Special Case
!

为了美观,也可以定制格式:

{{ "Custom style" | comment('plain', prefix='#######\n#', postfix='#\n#######\n   ###\n    #') }}

输出结果为:

#######
#
# Custom style
#
#######
   ###
    #

解析url过滤器

过滤器「urlsplit」,用于分解一个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"
#   }

正则过滤器

过滤器「regex_search」,用于对一个字符串的正则匹配查找

# search for "foo" in "foobar"
{{ 'foobar' | regex_search('(foo)') }}

# will return empty if it cannot find a match
{{ 'ansible' | regex_search('(foobar)') }}

# case insensitive search in multiline mode
{{ 'foo\nBAR' | regex_search("^bar", multiline=True, ignorecase=True) }}

过滤器「regex_findall」,用于对所有事件进行查找:

# Return 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') }}

过滤器「regex_replace」,用于对一个字符串进行文本替换:

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

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

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

# convert "localhost:80" to "localhost"
{{ 'localhost:80' | regex_replace(':80') }}

# add "https://" prefix to each item in a list
{{ hosts | map('regex_replace', '^(.*)$', 'https://\\1') | list }}

过滤器「regex_escape」,用于转义特殊字符串:

# convert '^f.*o(.*)$' to '\^f\.\*o\(\.\*\)\$'
{{ '^f.*o(.*)$' | regex_escape() }}

base64过滤器

过滤器「b64decode」「b64encode」,Base64编码与解码

{{ encoded | b64decode }}
{{ decoded | b64encode }}

格式化时间数据过滤器

# Display year-month-day
{{ '%Y-%m-%d' | strftime }}

# Display hour:min:sec
{{ '%H:%M:%S' | strftime }}

# Use ansible_date_time.epoch fact
{{ '%Y-%m-%d %H:%M:%S' | strftime(ansible_date_time.epoch) }}

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

日期相关对象

过滤器「to_datetime」,获取到的是日期对象

# 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, etc 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  }}

目录相关过滤器

过滤器「basename」,获取一个文件的绝对路径,例如将「foo.txt」转换为「/etc/asdf/foo.txt」

{{ path | basename }}

过滤器「dirname」,获取一个文件或目录的上级目录

{{ path | dirname }}

例如:

「/etc/httpd/conf」将获取到「/etc/httpd」

「/etc/httpd/conf/」将获取到「/etc/httpd/conf」

「/etc/httpd/conf/httpd.conf」将获取到「/etc/httpd/conf」

过滤器「realpath」,获取一个链接文件的真实文件路径,默认是绝对路径

{{ path | realpath }}

获取「/etc」的相对路径

{{ path | relpath('/etc') }}

uuid 过滤器

过滤器「to_uuid」,根据一个字符串生成一个UUID

{{ hostname | to_uuid }}

其他过滤器

过滤器「quote」,给字符串添加引号,在shell模块内使用

- shell: echo {{ string_value | quote }}

过滤器「ternary」,根据前面语句的真与假选择一个字符串

{{ (name == "John") | ternary('Mr','Ms') }}

上面示例表示:如果变量「name」的值为「John」则表达式返回字符串「Mr」,否则返回字符串「Ms」。

过滤器「join」,将列表转换成字符串,可以指定连接符

{{ list | join(" ") }}

过滤器「splittext」,拆分字符串,将文件的位置提取出来作为一个单独的元素

# with path == 'nginx.conf' the return would be ('nginx', '.conf')
{{ path | splitext }}

过滤器「type_debug」,用于debug出数据类型

{{ myvar | type_debug }}

加密

众所周知,ansible是很火的一个自动化部署工具,在ansible控制节点内,存放着当前环境服务的所有服务的配置信息,其中自然也包括一些敏感的信息,例如明文密码、IP地址等等。

从安全角度来讲,这些敏感数据的文件不应该以明文的形式存在。此时就用到了ansible加密的特性。

ansible通过命令行「ansible-vault」给你目标文件/字符串进行加密。在执行playbook时,通过指定相应参数来给目标文件解密,从而实现ansible vault的功能。

ansible可以加密任何部署相关的文件数据,例如:

  • 主机/组变量等所有的变量文件
  • tasks、hanlders等所有的playbook文件
  • 命令行导入的文件(eg : -e @file.yaml ,-e @file.json)
  • copy,template的模块里src参数所使用的文件,甚至是二进制文件。
  • playbook里用到的某个字符串参数也可以加密(Ansible>=2.3)

具体加密方法不表,参考官方文档去。

速度优化

1. SSH Multiplexing

ansible运行playbook时会启动很多ssh连接来执行复制文件,运行命令这样的操作.openssh支持这样一个优化,叫做ssh Multiplexing,当使用这个ssh Multiplexing的时候,多个连接到相同主机的ssh回话会共享相同的TCP连接,这样就只有第一次连接的时候需要进行TCP三次握手.

ansible会默认使用ssh Multiplexing特性,一般不需要更改配置,相关的配置项为:

[ssh_connection]
control_master = auto # 套接字不存在的情况下自动创建
control_path = $HOME/.ansible/cp/ansible-ssh-%h-%p-%r # 连接套接字存放的位置
control_persist = 60s  # 60s没有ssh连接就关闭主连接

2. pipelining

ansible执行过程中,他会基于调用的模块生成一个python脚本,然后将python脚本复制到主机上,最后执行脚本.ansible支持一个优化,叫做pipelining,在这个模式下ansible执行脚本时并不会去复制它,而是通过管道传递给ssh会话,这会让ansible的ssh会话从2个减少到1个,从而节省时间.

pipelining默认是关闭的, 因为他需要确认被管理主机上的/etc/sudoers文件中的requiretty没有启用, 格式如下:

Defaults: <username> !requiretty

ansible开启pipelining方法, 修改ansible.cfg配置文件:

[defaults]
pipelining = True

3. fact缓存

ansible playbook会默认先收集fact信息,如果不需要fact数据可以在playbook中禁用fact采集:

- name: not need facts
  hosts: myhosts
  gather_facts: False
  tasks:
    ...

也可以 ansible.cfg 全局禁用fact采集:

[defaults]
gathering = explicit

另一种解决方案就是使用fact缓存,目前ansible支持下面几种fact缓存:

  • JSON文件
  • Redis
  • memcached

JSON文件做fact缓存示例
ansible把采集到的fact写入控制主机的json文件中,如果文件已经存在,那么ansible不会再去主机上采集fact
启用JSON文件缓存,修改ansible.cfg文件:

[defaults]
gathering = smart

# 设置超时时间
face_caching_timeout = 86400

# 使用JSON文件做为缓存后端
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_fact_cache

4. 并发数

ansible默认并发数是5,可以用下面两种方法修改并发数:

  • 环境变量方式
export ANSIBLE_FORKS=20
  • 设置ansible.cfg
[defaults]
forks = 20
posted @ 2021-04-08 15:14  leffss  阅读(776)  评论(0编辑  收藏  举报