learn-tech/专栏/Jenkins持续交付和持续部署/07.Jenkins集成之Ansible要点.md
2024-10-16 06:37:41 +08:00

34 KiB
Raw Blame History

                        因收到Google相关通知网站将会择期关闭。相关通知内容
                        
                        
                        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有两个默认组all和ungrouped。

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到www50webservers组共计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单个的hostnameIP 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中使用变量最常用的方式可以借助vars和vars_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中可以用如下比较运算符

== :等于 = 不等于 >:大于 <:小于 >=:大于等于 <=:小于等于

上面的逻辑运算符除了可以在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_Xwith_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' }

用loop和flatten过滤器将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替换为loop和flatten过滤器。

  • 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替换为loop和zip过滤器。

  • 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和任一dictsort或dict2items过滤器。

  • 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复制前是否检验需要复制目的地的路径
  • destJinja2格式化模板的文件要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脚本进行持续部署操作。