07.Jenkins集成之Ansible要点

在前面的章节中,通过Jenkins内置的插件简单的实现了部署项目到虚拟机和容器当中。虽然实现了应用代码的部署,但是在稍显复杂的场景中仅凭Jenkins内置的插件完成CD工作还是很难满足需求的,可能你会遇到如下问题:

  • 在大批量主机上部署代码,替换配置文件等操作单靠内置的插件实现复杂困难;
  • 在项目数量持续增加的情况下,添加/修改项目的配置、项目配置维护也逐渐增加了难度

基于上面列出的以及其它可能发生的影响工作效率的问题,就需要用到我们之前介绍的自动化运维工具Ansible来解决了。不过在正式使用ansible 重构前面章节部署服务的脚本之前,首先来学习一下在后面章节中即将用到的与ansible相关的基础知识。

本节主要涉及到的内容如下:

  • Ansible inventory
  • Ansible 变量
  • Ansible 条件判断
  • Ansible 循环
  • Ansible Template

Ansible inventory

Inventory,也就是被管理的主机清单。在大规模的配置管理工作中,Ansible可同时针对基础架构中的多个系统工作,它通过选择Ansible inventory中列出的主机清单来完成此操作。

默认情况下该清单保存在本地服务器的/etc/ansible/hosts文件中。它是一个静态的INI格式的文件,当然我们还可以在运行ansible或ansible-playbook命令的时候用-i参数临时指定该文件。如果-i参数指定的是一个目录,则ansible执行时会指定该目录下的多个inventory文件。

主机和组(Host & Groups)

Inventory 文件的定义可以采用多种格式。不过我们这里主要介绍在/etc/ansible/hosts文件中使用INI格式,如下所示:

mail.example.com

[webservers]
foo.example.com
bar.example.com

[dbservers]
one.example.com
two.example.com
bar.example.com

说明:

  • mail.example.com 为没有被分组的主机,没有被分组的主机一般都写到inventory清单文件的开头。

  • 方括号”[]“中是组名,用于对主机进行分类,便于对不同主机进行区别管理。

  • 一个主机可以属于不同的组,比如上面的bar.example.com主机同时属于webserver组和dbserver组。这时属于两个组的变量都可以为这台主机所用,至于变量的优先级关系将于以后的章节中讨论。

  • 这里的主机可以是一个ip,也可以是一个FQDN形式的域名,只要ansible主机能与之正常通信即可,更详细的配置在后面讨论。

看一下如何列出目标组里的主机清单:

ansilbe <group_name>|host --list-host

默认组

Ansible有两个默认组:allungrouped

  • all包含每个主机。
  • ungrouped包含的主机只属于all组且不属于其他任何一组

下面写法等同于匹配inventory清单文件中的所有机器:

all
*

端口

如果有主机的SSH端口不是标准的22端口,可在主机名之后加上端口号,用冒号分隔。

例如,端口号不是默认设置时,可以做如下配置:

badwolf.example.com:5309

别名

上面提到过,主机可以是FQDN形式,也可以是ip形式,假设有一些静态IP地址,希望设置一些别名,而且不在系统的host文件中设置,又或者是通过隧道在连接,那么可以设置如下形式:

test_ansible ansible_ssh_port=5555 ansible_ssh_host=192.168.177.43

说明:

  • 在上面的例子中,通过 test_ansible 别名,执行ansible时会ssh连接192.168.177.43 主机的5555端口。这是通过inventory 文件的特性功能设置的变量。

匹配

如果要添加大量格式类似的主机,不用列出每个主机名:在看一组相似的hostname或者ip , 可简写如下:

[webservers]
www[01:50].example.com

说明:

  • 方括号”[]“中01:50 也可写为1:50 效果相同,表示指定了从www1到www50,webservers组共计50台主机;

如果你不习惯ini格式,也可以使用yaml格式,如下:

 webservers:
    hosts:
      www[01:50].example.com:

还可以定义字母范围的简写模式:

[databases]
db-[a:f].example.com
  • 表示databases组有db-a到db-f共6台主机。

如下写法分别表示一个或多个groups。多组之间以冒号分隔表示或的关系。这意味着一个主机可以同时存在多个组:

webservers
webservers:dbservers

也可以排除一个特定组,如下示例中,所有执行命令的机器必须隶属 webservers 组但同时不在 dbservers组:

webservers:!dbservers

也可以指定两个组的交集,如下示例表示,执行命令有机器需要同时隶属于webservers和nosql组。

webservers:&nosql

也可以组合更复杂的条件:

webservers:dbservers:&nosql:!phoenix

上面这个例子表示”webservers” 和 “dbservers” 两个组中隶属于 “nosql” 组并且不属于”phoenix” 组的机器才执行命令。

也可以使用变量,如果希望通过传参指定group,需要通过-e参数传参实现,但这种用法不常用:

webservers:!{{excluded}}:&{{required}}

也可以不必严格定义groups,单个的hostname,IP , groups都支持通配符:

*.example.com
*.com

Ansible也同时也支持通配和groups的混合使用:

one*.com:dbservers

在高级语法中,你也可以在group中选择对应编号的server或者一部分servers:

webservers[0]

webservers[0-25]

说明:

  • webservers[0]表示匹配 webservers1 组的第 1 个主机。
  • webservers1[0:25] 表示匹配 webservers1组的第 1 个到第 26 个主机。

大部分人都在匹配时使用正则表达式,只需要以 ‘~’ 开头即可:

~(web|db).*\.example\.com

也可以通过 –limit 标记来添加排除条件,/usr/bin/ansible or /usr/bin/ansible-playbook都支持:

ansible-playbook site.yml --limit datacenter2

如果你想从文件读取hosts,文件名以@为前缀即可。

比如下面示例用于将site.yaml中指定的主机列表在list.txt中存在的主机列出来:

ansible-playbook site.yml --limit @list.txt --list-hosts

Ansible 变量

在使用shell编写脚本时,多多少少的肯定会用到变量,同样的,使用playbook执行任务时,也会用到变量。使用变量可以使编写的playbook更加灵活,减少代码量,同时,Ansible使用变量来帮助处理系统之间的差异。

使用变量之前,先看一下如何定义有效变量:变量名称应为字母、数字和下划线组成。变量名应始终以字母开头,ansible内置的关键字不能作为变量名。

定义与使用变量有多种方式,这里简单介绍三种常用的:

在play中定义与使用变量

要在play或者playbook中使用变量,是在ansible中使用变量最常用的方式,可以借助varsvars_files关键字,并以key:value形式存在,比如:

- hosts: web
  vars:                         
      app_path: /usr/share/nginx/html
  tasks:
  - name: copy file
    copy:
      src: /root/aa.html
      dest: "{{ app_path }}"

上面示例使用vars定义变量,在dest中引用变量。上面定义了一个变量,也可以定义多个变量,如下:

vars:                           
  app_path: /usr/share/nginx/html
  db_config_path: /usr/share/nginx/config

#也可以使用yaml语法格式的变量设置
vars:                           
- app_path: /usr/share/nginx/html
- db_config_path: /usr/share/nginx/config

也可以使用类似”属性”的方式定义,比如:

vars:
  nginx:
    app_path: /usr/share/nginx/html
    db_config_path: /usr/share/nginx/config
tasks:
- name: copy file
  copy:
    src: /root/aa.html
    dest: "{{ nginx.app_path }}"
- name: copy file
  copy:
    src: /root/db.config
    dest: "{{ nginx['db_config_path'] }}"

需要注意的是,引用变量时:

  • 当key与value中间分隔符使用的冒号,如果value的值开头是变量,那么需要使用双引号引起来,如果变量没有在开头,可以不用引起来;

  • 当key与value中间分隔符使用的等号,不论value的值开头是不是变量,都不必用引号引起来:

例如:

这样的语法会报错:

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

这样做就可以了:

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

var_files

在play中也可以是使用vars_files关键字定义文件形式的变量,比如:

---
- hosts: all
  vars_files:
  - /vars/external_vars.yml
  tasks:
  - name: test
    command: /bin/echo "{{ somevar }}"

var_files文件内容格式为key:value 键值对形式。

---
somevar: somevalue
password: magic
ip: ['ip1','ip2']

include_vars

在play中引用变量时,也可以使用include_vars参数,include_vars可以动态的在需要的时候引用文件中定义的变量。

示例:

$ cat test_var.yaml 
---
name: test_CI
version: v1

$ cat test_vars.yaml
---
- hosts: ansible
  gather_facts: no
  tasks:
  - include_vars:
      file: /root/test_var.yaml
  - debug:
      msg: "{{version}}"

# 执行结果
$ ansible-playbook test_vars.yaml 
......
TASK [debug] ***************************************************************************************
ok: [192.168.177.43] => {
    "msg": "v1"
}
......

也可以将file文件内所有的变量赋值给一个变量名,如下:

$ cat test_vars.yaml
---
- hosts: ansible
  gather_facts: no
  tasks:
  - include_vars:
      file: /root/test_var.yaml
      name: test_include_vars
  - debug:
      msg: "{{test_include_vars}}------>{{ test_include_vars.version }}"
~

执行结果:

TASK [debug] ***************************************************************************************
ok: [192.168.177.43] => {
    "msg": "{u'version': u'v1', u'name': u'test_CI'}------>v1"
}

注册变量register

变量的另一个主要用法是运行命令并将该命令执行的结果作为注册变量。这也是用的比较多的一种使用变量的方式。

可以将ansible中执行的任务的值保存在变量中,该变量可以被其它任务引用。

一个简单的语法示例(将执行任务结果通过debug模块输出出来):

$ cat template-debug.yaml

- hosts: ansible
  tasks:
  - name: exec command
    shell: hostname
    register: exec_result

  - name: Show debug info
    debug:
      msg: "{{ exec_result }}"

#执行结果
$ ansible-playbook template-debug.yaml 

......

TASK [Show debug info] *****************************************************************************
ok: [192.168.177.43] => {
    "msg": {
        "changed": true, 
        "cmd": "hostname", 
        "delta": "0:00:00.115138", 
        "end": "2020-02-26 15:51:54.825096", 
        "failed": false, 
        "rc": 0, 
        "start": "2020-02-26 15:51:54.709958", 
        "stderr": "", 
        "stderr_lines": [], 
        "stdout": "ansible", 
        "stdout_lines": [
            "ansible"
        ]
    }
}

PLAY RECAP *****************************************************************************************
192.168.177.43             : ok=3    changed=1    unreachable=0    failed=0   

其中:

register参数将执行的结果赋给变量exec_result,然后debug模块引用了变量。

根据结果可得到,该任务的执行结果返回的是一个字典,也就是该变量的值是一个字典,而该字典下的stdoutkey的值才是该命令真正的执行结果,其中的changed、cmd、stderr、stdout_lines等key作为该任务执行结果的附加key,所以可以通过dict[‘key’]的方式获取具体的值。比如:

  - name: Show debug info
      debug:
        msg: "{{ exec_result.stdout }}"

  # 执行结果

  TASK [Show debug info] *****************************************************************************
  ok: [192.168.177.43] => {
      "msg": "ansible"
  }

rc 可以理解为任务执行的返回码,0表示成功。

上面stdout的值如果是列表(或已经是列表),则可以在任务循环中使用它。

- name: 列表循环
  hosts: all
  tasks:
    - name: 列出目录所有文件和目录
      command: ls /home
      register: home_dirs

    - name: 添加软连接
      file:
        path: /mnt/bkspool/{{ item }}
        src: /home/{{ item }}
        state: link
      loop: "{{ home_dirs.stdout_lines }}"

上面的示例将/home目录的下的文件列出来以后给home_dirs变量,并使用loop循环列出,执行下一步任务。

在命令行定义变量

除了上面的定义变量方法外,还可以在执行playbook的同时通过-e参数传递变量,比如:

$ cat release.yml
---
- hosts: ansible
  gather_facts: no
  tasks:
  - name: test
    debug:
      msg: "version: {{ version}}, other :{{ other_variable }}"

#执行结果
$ ansible-playbook release.yml --extra-vars "version=1.23.45 other_variable=foo"

.....
TASK [test] ****************************************************************************************
ok: [192.168.177.43] => {
    "msg": "version: 1.23.45, other :foo"
}

或者使用json方法:

$ ansible-playbook release.yml --extra-vars '{"version":"1.23.45","other_variable":"foo"}'

$ ansible-playbook arcade.yml --extra-vars '{"pacman":"mrs","ghosts":["inky","pinky","clyde","sue"]}'

使用json文件方式:

ansible-playbook release.yml --extra-vars "@some_file.json"

有关变量的使用就先简单的介绍到这里。

Ansible 条件判断

在大多数语言中,条件表达都使用if/else语句,而在ansible中,条件表达主要用于template模板,本小节内容则主要介绍一下在playbook中经常使用的条件语句:”when”。

when

首先以一个简单的示例演示一下如何使用when:

循环列表,当num大于5时打印数字:

---
- hosts: ansible
  gather_facts: false
  tasks:
  - name: tes
    debug: 
      msg: " {{ item }}"
    with_items: [0,2,4,6,8,10]
    when: item > 5

with_items:表示循环,下面会说到。

执行结果如下:

$ ansible-playbook test-when.yaml 
.....

ok: [192.168.177.43] => (item=6) => {
    "msg": " 6"
}
ok: [192.168.177.43] => (item=8) => {
    "msg": " 8"
}
ok: [192.168.177.43] => (item=10) => {
    "msg": " 10"
}
......

上面的例子使用了”>“比较运算符,在ansible中可以用如下比较运算符:

  • == :等于
  • != : 不等于
  • &gt;:大于
  • &lt;:小于
  • &gt;=:大于等于
  • &lt;=:小于等于

上面的逻辑运算符除了可以在playbook中使用外,也可以在template的jinja2模板中使用。

除了上面的比较运算符之外,ansible还支持逻辑运算符,如下:

  • and:和
  • or:或者
  • not:否,也可以称之为取反
  • ():组合,可以将一些列条件组合在一起

在实际工作中,when语句除了单独使用外,与注册变量(register)组合使用也比较常见,比如:

可以将任务执行的结果传递给register指定的变量名,并通过when语句去做条件判断,如下示例。

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

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

  - command: /bin/something_else
    when: result is succeeded

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

说明:

上面ignore_errors参数表示,如果该命令执行错误(比如命令不存在)导致报错时忽略错误,这样即便这个任务执行失败了也不会影响playbook内其他任务的运行。

下面三个命令表示根据register指定的变量结果去判断要不要执行当前任务。

上面的示例可以说只是演示了一下when与register结合使用的语法,那么下面就以实际的例子来演示一下:

例1:

可以使用”stdout”值访问register变量的字符串内容。

---
- hosts: ansible
  gather_facts: false
  tasks:
  - shell: cat /etc/motd
    register: motd_contents

  - shell: echo "motd contains the word hi"
    when: motd_contents.stdout.find('hi') != -1

说明:

上一节介绍register变量时提到过register指定的变量实际内容是一个字典,该字典有多个key,上面的示例就是用了stdout这个key,该key的值就是实际执行命令的结果,然后用find方法搜索指定的值,最后通过when语句判断。

例2:

使用”stdout”值列出register变量的字符串内容。

例如:检查register变量的字符串内容是否为空:


- name: check
  hosts: all
  tasks:
      - name: list contents of directory
        command: ls mydir
        register: contents

      - name: check_wether_empty
        debug:
          msg: "Directory is empty"
        when: contents.stdout == ""

或者判断某个分支是否存在。

- command: chdir=/path/to/project  git branch
  register: git_branches

- command: /path/to/project/git-clean.sh
  when: "('release-v2'  in git_branches.stdout)"

when与playbook内设置的其它变量也可以一起使用,如下示例:

vars:
  epic: true

tasks:
    - shell: echo "This certainly is epic!"
      when: epic

tasks:
    - shell: echo "This certainly isn't epic!"
      when: not epic

tasks:
    - shell: echo "I've got '{{ foo }}'"
      when: foo is defined

    - fail: msg="Bailing out. this play requires 'bar'"
      when: bar is undefined

其中,defined和undefined用来判断该变量是否被定义。

exists 类似与linux系统中的”test -e”命令,用来判断ansible控制节点路径或者文件是否存在。

比如,判断ansible主机 homed 目录是否存在,可以写成这样。

---
- hosts: ansible
  gather_facts: no
  vars:
    path: /homed
  tasks:
  - debug:
      msg: "file exists"
    when: path is exists

同样的,也可以使用is not exists来判断目录不存在,这各比较简单,这里就不再演示了。

如果要判断远程主机目录是否存在怎么办?可以通过判断命令执行结果来作为条件,比如:

 - name: 判断目录是否存在
      shell: ls {{ dest_dict }}
      register: dict_exist
      ignore_errors: true

    - name: 创建相关目录
      file: dest="{{ item }}" state=directory  mode=755
      with_items:
        - "{{ dest_dict }}"
      when: dict_exist is failure

defined/undefind/none

defined和undefind用来判断变量是否定义,none用来判断变量值是否为空,若为空则返回真

示例如下:

---
- hosts: ansible
  gather_facts: no
  vars:
    name: "web"
    version: ""
  tasks:
  - debug:
      msg: "变量已定义"
    when: name is defined
  - debug:
      msg: "变量没有定义"
    when: version1 is undefined
  - debug:
      msg: "变量定义了,但为空"
    when: version is none

这里就不演示效果了,有兴趣大家可以自己尝试一下。有关条件判断的内容的就先到这里

Ansible 循环

日常工作中可能会遇到比如复制很多文件到远程主机、按序执行多个启动服务的命令或者遍历某个任务的执行结果等情况,在playbook中,可通过循环完成此类型的任务。

loop

一个简单的循环,比如创建多个用户:

通常写是这样的:

- name: add user testuser1
  user:
    name: "testuser1"
    state: present
    groups: "wheel"

- name: add user testuser2
  user:
    name: "testuser2"
    state: present
    groups: "wheel"

但是如果使用了循环,也可以是这样的:

- name: add several users
  user:
    name: "{{ item }}"
    state: present
    groups: "wheel"
  loop:
     - testuser1
     - testuser2

如果在yaml文件或vars中定义了列表,也可以是这样:

loop: "{{ somelist }}"

在变一下,如果遍历的项目类型不简单的字符串列表,例如字典,也可以是这样:

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

register with loop

在前面提到过,register与loop一起使用后,放置在变量中的数据结构将包含一个results属性,该属性是来自模块的执行结果的列表。

例如:

- shell: "echo {{ item }}"
  loop:
    - "one"
    - "two"
  register: res

随后,执行的结果会放在变量中

- shell: echo "{{ item }}"
  loop:
    - one
    - two
  register: res
  changed_when: res.stdout != "one"

也可以将任务执行的结果通过loop进行循环

- name: list dirs
  shell: ls /dir
  reigister: list_dict

- name: test loop
  debug:
    msg: "{{ item }}"
  loop:  list_dict.stdout_lines

with_x 与loop

随着Ansible 2.5的发布,官方建议执行循环的方法使用loop关键字而不是with_X(with_X不是表示某一个关键字,而是一系列关键字的集合)样式的循环。

在许多情况下,loop使用过滤器更好地表达语法,而不是使用query,进行更复杂的使用lookup

以下示例将展示将许多常见with_样式循环转换为loop和filters。

with_list

with_list 每个嵌套在大列表的元素都被当做一个整体存放在item变量中,不会“拉平”嵌套的列表,只会循环的处理列表(最外层)中的每一项

- name: with_list
  debug:
    msg: "{{ item }}"
  with_list:
    - one
    - two
    - [b,c]

- name: with_list -> loop
  debug:
    msg: "{{ item }}"
  loop:
    - one
    - two

- name: with_list_loop
  vars:
    testlist:
    - a
    - [b,c]
    - d
  tasks:
  - debug:
      msg: "{{item}}"
    loop: "{{testlist}}"

with_items

示例如下:

拉平”嵌套的列表,如果大列表中存在小列表,那么这些小列表会被”展开拉平”,列表里面的元素也会被循环打印输出。

依次将abcd四个txt文件和 shell目录拷贝到目标文件夹下

- name: copy file
      copy: src=/root/{{ item }} dest=/root/test/{{ item }}
      with_items:
      - a.txt
      - b.txt
      - ['c.txt','d.txt']
      - shell   (目录)

也可以这样写
- name: copy file
      copy: src=/root/{{ item }} dest=/root/test/{{ item }}
      with_items: ['a.txt','b.txt','shell']

with_items也可以用于循环字典变量,如下

- name: add users
  user: name={{ item }} state=present groups=wheel
  with_items:    
  - testuser1
  - testuser2

- name: add users
  user: name={{ item.name }} state=present groups={{ item.groups }}
  with_items:
  - { name: 'testuser1', groups: 'test1' }
  - { name: 'testuser2', groups: 'test2' }

loopflatten过滤器将with_items替换

- name: with_items
  debug:
    msg: "{{ item }}"
  with_items: "{{ items }}"

- name: with_items -> loop
  debug:
    msg: "{{ item }}"
  loop: "{{ items|flatten(levels=1) }}"

- name: with_item_list
  vars:
    testlist:
    - a
    - [b,c]
    - d
  tasks:
  - debug:
      msg: "{{item}}"
    loop: "{{testlist | flatten(levels=1)}}"

#flatten过滤器可以替代with_flattened,当处理多层嵌套的列表时,列表中所有的嵌套层级都会被拉平

with_indexed_items

循环列表时为每一个元素添加索引

- name: with_indexed_items
  debug:
    msg: "{{ item.0 }} - {{ item.1 }}"
  with_indexed_items: "{{ items }}"

示例如下,打印出列表内的元素的索引以及对应的值

$ cat test_with_index.yaml 
---
- hosts: ansible
  tasks:
  - name: with_indexed_items
    debug:
      msg: "{{ item.0 }} - {{ item.1 }}"
    with_indexed_items:
    - ['a','b','c','d']

#打印结果
ok: [192.168.177.43] => (item=[0, u'a']) => {
    "msg": "0 - a"
}
ok: [192.168.177.43] => (item=[1, u'b']) => {
    "msg": "1 - b"
}
ok: [192.168.177.43] => (item=[2, u'c']) => {
    "msg": "2 - c"
}
ok: [192.168.177.43] => (item=[3, u'd']) => {
    "msg": "3 - d"
}

将with_indexed_items关键字替换为loop关键字,flatten过滤器(加参数),再配合loop_control关键字,可以替代with_indexed_items。当处理多层嵌套的列表时,只有列表中的第一层会被拉平

- name: with_indexed_items -> loop
  debug:
    msg: "{{ index }} - {{ item }}"
  loop: "{{ items|flatten(levels=1) }}"
  loop_control:
    index_var: index

说明:

  • “loop_control”关键字可以用于控制循环的行为,比如循环时获取元素的索引。
  • “index_var”是loop_control的一个设置选项,它的作用是指定一个变量,loop_control会将循环获取到的索引值放到这个变量中。当循环到这个索引值时,就能根据这个索引获取到对应的值。

with_flattened

with_flattened与使用with_items的效果是一样的。

with_flattened替换为loopflatten过滤器。

- name: with_flattened
  debug:
    msg: "{{ item }}"
  with_flattened: "{{ items }}"

- name: with_flattened -> loop
  debug:
    msg: "{{ item }}"
  loop: "{{ items|flatten }}"

---
- name: test_with_flattened
  vars:
    testlist:
    - a
    - [b,c]
    - d
  tasks:
  - debug:
      msg: "{{item}}"
    loop: "{{testlist | flatten}}"

with_together

该关键字会将多个列表进行组合,如果多个列表的长度不同,则使用最长的列表长度进行对齐,短列表中元素数量不够时,使用none值与长列表中的元素对其。类似于python下的zip函数

如下示例

---
- hosts: ansible
  tasks:
  - name: with_together
    debug:
      msg: "{{ item.0 }} - {{ item.1 }}"
    with_together:
    - ['a','b','c','d']
    - [1,2,3,4,5,6,7]

执行结果

ok: [192.168.177.43] => (item=[u'a', 1]) => {
    "msg": "a - 1"
}
ok: [192.168.177.43] => (item=[u'b', 2]) => {
    "msg": "b - 2"
}
ok: [192.168.177.43] => (item=[u'c', 3]) => {
    "msg": "c - 3"
}
ok: [192.168.177.43] => (item=[u'd', 4]) => {
    "msg": "d - 4"
}
ok: [192.168.177.43] => (item=[None, 5]) => {
    "msg": " - 5"
}
ok: [192.168.177.43] => (item=[None, 6]) => {
    "msg": " - 6"
}
ok: [192.168.177.43] => (item=[None, 7]) => {
    "msg": " - 7"

将with_together替换为loopzip过滤器。

- name: with_together -> loop
  debug:
    msg: "{{ item.0 }} - {{ item.1 }}"
  loop: "{{ list_one|zip(list_two)|list }}"

zip根据最短的列表长度,对列表进行组合

With_dict

循环字典,比如

users:
  alice:
    name: Alice Appleworth
    telephone: 123-456-7890
  bob:              #key
    name: Bob Bananarama    #value1
    telephone: 987-654-3210   #value2

tasks:
  - name: Print records
    debug: msg="User {{ item.key }} is {{ item.value.name }} ({{ item.value.telephone }})"
    with_dict: "{{ users }}"

with_dict可以通过取代loop和任一dictsortdict2items过滤器。

- name: with_dict
  debug:
    msg: "{{ item.key }} - {{ item.value }}"
  with_dict: "{{ dictionary }}"

- name: with_dict -> loop (option 1)
  debug:
    msg: "{{ item.key }} - {{ item.value }}"
  loop: "{{ dictionary|dict2items }}"

- name: with_dict -> loop (option 2)
  debug:
    msg: "{{ item.0 }} - {{ item.1 }}"
  loop: "{{ dictionary|dictsort }}"

- name: with_dict -> loop (option 2)
  debug:
    msg: "{{ item.0 }} - {{ item.1 }}"
  loop: "{{  lookup('dict',dict_name) }}"

with_random_choice

with_random_choice只需使用random过滤器即可替换,而无需使用loop

- name: with_random_choice
  debug:
    msg: "{{ item }}"
  with_random_choice: "{{ my_list }}"

- name: with_random_choice -> loop (No loop is needed here)
  debug:
    msg: "{{ my_list|random }}"
  tags: random

有关循环的内容就介绍到这里,上面列举的基本都是工作中能用到的示例,对于一种需求,实现方法有很多,选择最适合的一种即可。

Ansible Template

使用ansible编写playbook的时候,替换配置文件内容的操作时有发生,而template模块无疑是完成这一操作的最好利器。Ansible template模块使用Jinja2模板来启用动态表达式和访问变量。使用文件(roles/templates/xxx.j2)引用Jinja2模版实现配置文件内容的拼接与替换。

Jinja2是基于python的模板引擎,相比较于python原生的Jinja2模版,ansible使用过滤器、插件、测试变量等能够使部署代码更加简洁高效。

template模块的使用方法和copy模块基本相同,本次template章节主要讲一下template使用方法以及经常使用的jinja2基本语法。

template使用语法

前面提到template模块的使用方法大致与copy模块相同,所以使用语法也差不多

ansible <group_name> -m template -a "src=<src_path>.j2 dest=<dest_path> option=..."

其中option表示其它参数,常用的如下:

* backup:在覆盖之前将原文件备份,备份文件包含时间戳信息
* follow:是否遵循目的机器中的文件系统链接
* force:是否强制执行
* group:设置文件/目录的所属组
* mode:设置文件权限,模式实际上是八进制数字(如0644),少了前面的零可能会有意想不到的结果。从版本1.8开始,可以将模式指定为符号模式(例如u+rwx或u=rw,g=r,o=r)
* newline_sequence(2.4+):   指定要用于模板文件的换行符
* owner:设置文件/目录的所属用户
* src: Jinja2格式化模板的文件位置(必须)
* trim_blocks:设置为True,则块之后的第一个换行符被移除
* unsafe_writes:是否以不安全的方式进行,可能导致数据损坏
* validate:复制前是否检验需要复制目的地的路径
* dest:Jinja2格式化模板的文件要copy的目的地址(必须)
.....

在看一些常用的例子:

# 把nginx.conf.j2文件经过填写参数后,复制到远程节点的nginx配置文件目录下
$ ansible web -m template -a "src=/root/nginx.conf.j2 dest=/usr/share/nginx/conf/nginx.conf owner=nginx group=nginx mode=0644"

# 把sshd_config.j2文件经过填写参数后,复制到远程节点的ssh配置文件目录下
$ ansible web -m template -a "src=/etc/ssh/sshd_config.j2 dest=/etc/ssh/sshd_config owner=root group=root mode=0600 backup=yes"

在Jinja2中使用变量

定义变量后,也可以使用Jinja2模板系统在playbook中使用它们。需要将变量用双括号”{{ }}“引起来,除了双括号装载变量,还有其它表达式:

  • {{ }}:用来装载变量,当然也可以用来装载其它python类型的数据,比如,列表、字符串、表达式,或者ansible的过滤器等
  • {% %}:用来装载控制语句,比如for循环,if语句等
  • {# #}:用来装载注释,当文件被渲染后,被注释的内容不会包含在文件之内

下面一个简单的Jinja2模板:

My amp goes to {{ max_amp_value }}

此表达式提供了变量替换的最基本形式。

可以在playbook中使用相同的语法。例如:

template: src=foo.cfg.j2 dest={{ remote_install_path }}/foo.cfg

$ cat foo.cfg.j2
{% set test_list=['test','test1','test2'] %}
{% for i in list  %}
    {{i}}
{% endfor %}

在template内部,可以自动访问主机范围内的所有变量。

Jinja2 使用语法

Jinja2的语法是由variables(变量)和statement(语句)组成,如下:

  • variables: 可以输出数据

    {{ my_variable }}
    {{ some_dudes_name | capitalize }}
    
  • statements: 可以用来创建条件和循环等等

    #  简单的if else elif endif
    {% if my_conditional %}
    ...
      {% elif my_conditionalN %}
    ...
        {% else %}
    {% endif %}
    
    # for 循环
    {% for item in all_items %}
      {{ item }}
    {% endfor %}
    
    
    {# COMMENT #} 表示注释
    

从variables的第二个示例可知,Jinja2 支持使用带过滤器的 Unix 型管道操作符,也可以称之为过滤器,ansible有很多的内置过滤器可供使用。

下面以一个示例将上面的语法做个简单的说明:

$ cat /root/template-test.yaml 
---
- hosts: ansible
  gather_facts: no
  tasks:
  - template:
      src: /tmp/ansible/test.j2
      dest: /opt/test
    vars:
      test_list: ['test','test1','test2']
      test_list1: ['one','two','three']
      test_dict: {'key1':'value1'}
      test_dict1: {'key1':{'key2':'value2'}}

$ cat /tmp/ansible/test.j2 

{% for i in test_list  %}
    {{i}}
{% endfor %}

{% for i in test_list1 %} 
    {% if i=='one' %}
value1 --> {{i}}
    {% elif loop.index == 2 %}
value2 -->{{i}}
    {% else %}
value3 -->{{i}}
    {% endif %}
{% endfor %}

{% for key,value in test_dict.iteritems() %}
-------------->{{value}}
{% endfor %}

{{ test_dict1['key1']['key2'] }}

$ ansible-playbook template-test.yaml
或者通过ad-hoc命令执行
$ ansible ansible -m template -a "src=test.j2 dest=/opt/test"

执行结果

$ cat /opt/test

    test
    test1
    test2

    value1 --> one

    value2 -->two

    value3 -->three

-------------->value1

value2

上面的示例为了简单,将所有的变量放到了playbook的yaml文件中,当然,该变量也可以放到模板文件中,如

{% set test_list=['test','test1','test2'] %}
{% for i in test_list  %}
    {{i}}
{% endfor %}

效果与上面的一样

去掉换行符

对于上面的结果发现,每条结果后会自动换行,如果不想换行,可以在结束控制符”%}“之前添加一个“-”,如下所示

{% for i in test_list1 -%}
{{ i }}
{%- endfor %}

字符串拼接

在jinja2中,使用波浪符”~“作为字符串连接符,它会把所有的操作数转换为字符串,并且连接它们。

{% for key,val in {'test1':'v1','num':18}.iteritems() %}
{{ key ~ ':' ~ val }}
{% endfor %}

转义

在对配置文件进行操作的时候,难免遇见设置特殊字符,比如”空格”,”#“,单引号双引号之类的字符,如果想要保留这些特殊字符,就需要对这些字符进行转义。

使用转义操作,简单的可以使用单引号” ‘ “ ,进行纯粹的字符串处理,如

$ cat /tmp/ansible/test.j2 

{% for i in [8,1,5,3,9] %}
        '{{ i }}'
        {{ '{# i #}' }}
        {{ '{{ i }}' }}
{% endfor %}

$ cat template-test.yaml
---
- hosts: ansible 
  gather_facts: no
  tasks:
  - name: copy
    template:
      src: /tmp/ansible/test.j2
      dest: /opt/test

执行结果如下

    '8'
    {# i #}
    {{ i }}
    '1'
    {# i #}
    {{ i }}
    '5'
    {# i #}
    {{ i }}
 ......

jinja2 可以使用"{% raw %}"块保持”{% raw %}“与”{% endraw %}“之间的所有”{{ }}“、”{% %}“或者”{# #}“都不会被jinja2解析,比如

# cat test.j2

{% raw %}
    {% for i in [8,1,5,3,9] %}

    {% {{ i }} %}
    {# {{ i }} #}
    {% {{ i }} %}
  {% endfor %}
{% endraw %}

执行结果为

  {% for i in [8,1,5,3,9] %}

    {% {{ i }} %}
    {# {{ i }} #}
    {% {{ i }} %}
  {% endfor %}

有关Ansible的内容就介绍到这里,上面介绍这些内容在使用ansible时应该是经常用到的一些知识点了,足够我们编写持续部署的脚本,在下一节中我们就来看一下如何使用ansible来编写playbook脚本进行持续部署操作。