黑帽联盟

标题: ansible笔记(42):角色 [打印本页]

作者: yun    时间: 2019-9-11 15:39
标题: ansible笔记(42):角色
本帖最后由 yun 于 2019-9-11 17:18 编辑

ansible是一个系列文章,我们会尽量以通俗易懂的方式总结ansible的相关知识点。
ansible系列博文直达链接:ansible轻松入门系列
"ansible系列"中的每篇文章都建立在前文的基础之上,所以,请按照顺序阅读这些文章,否则有可能在阅读中遇到障碍。


在介绍角色之前,我先讲讲自己的经历。

我经常需要安装配置各种服务,比如,nginx、redis、mysql...等等,于是我决定,分别为这些服务编写对应的剧本,以便在需要的时候,能够根据已经编写好的playbook,快速的安装配置对应的软件,于是,我们编写了nginx.yml、redis.yml、mysql.yml等等playbook,当有一台新主机A需要安装nginx和redis时,我只需要将nginx.yml和redis.yml中的目标主机IP修改成A主机的IP,然后执行这两个playbook即可,有没有觉得很方便?一开始我是觉得挺方便的,但是很快,我发现了一些很明显的问题。

第一个问题就是,今天我需要在A主机上安装redis,于是我把redis.yml的目标主机修改成了A主机的IP,但是明天,我又需要在B主机上安装redis,于是我又把redis.yml的目标主机修改成了B主机的IP,每次安装配置都需要修改redis.yml,这样做似乎是理所应当的, 但是仔细想想,能不能有更好的办法呢?能不能让redis.yml中的配置过程与目标主机的耦合度降到最低呢?因为我有可能需要在任意一台主机上安装配置redis,所以每次都因为目标主机的缘故修改redis.yml文件,不仅仅会觉得麻烦,逻辑的界线似乎也不是特别清晰,好吧,一时半会儿我也没有想到什么好的解决办法,于是我决定先把这个问题记录下来。

刚开始的时候,我编写的nginx.yml和redis.yml都很简单,因为初次编写它们的时候,我考虑到的情况并不是很多,甚至都没有使用到任何模板文件,也没有使用什么变量,总之就是很简单的写了几个任务,但是经过不断的实践,我决定优化刚开始编写的playbook,让它适用于更多的情况,慢慢的,我开始编写默认的模板文件,开始将一些默认配置提取到变量文件中,之前如果想要配置nginx,只靠nginx.yml一个文件就够了,但是现在,我已经有了nginx.yml、nginx.conf.j2、nginx.defaultvars.yml三个文件,同理,redis.yml也慢慢扩展成了几个文件,其他服务对应的文件也变得越来越多,总之,我按照自己的规范,井然有序的工作着。

直到有一天,公司来了一个名叫狗蛋的新同事,他也是做运维的,我俩一见如故,很快便开始交换自己的运维经验,我告诉他,我已经写好了一些playbook,以便针对不同的服务进行方便快捷的安装配置,狗蛋听我这么说,立马接话道:"英雄所见略同,我也是这么干的,而且我有自己的一套规范。",狗蛋告诉我,他在编写处理nginx的playbook时,会创建一个名叫nginx的文件夹,然后再在nginx的目录中创建一些子目录,比如,task目录、temp目录、default目录等等,然后将一些安装配置的任务剧本放在task目录中,将一些会用到的模板文件放到temp目录中,将一些默认设置变量文件放在default目录中,这样就显得非常简洁明了了,比如,当你需要找到nginx的相关模板文件时,只要去nginx的temp目录查找就行,同理,他在编写redis的playbook时,也会创建一个redis目录,然后在redis目录中创建task、temp、default等目录,然后将各个相关文件放到对应的目录中,所有服务都是按照同样的标准编写的,这样比较规范,方便自己编写,也方便在以后找到对应的各种文件,听了狗蛋的描述,我觉得他的方法似乎更加规范,于是我决定,向狗蛋看齐,学习他的那一套规范,并且把狗蛋的规范写成文档,以便公司以后的新同事可以按照文档规范的进行操作,这样所有人生产出的内容都是按照统一的规范来的,不仅风格统一,而且也方便其他人阅读自己编写的内容。

我和狗蛋商量好了统一的规范以后,很快便形成了具体的文档,我俩自豪的把规范发布到了公司的wiki系统中,并且邀功似的向技术总监展示了我俩编写的"规范",总监打开规范文档,还没有仔细看两眼就笑了笑说:"你俩不要重复造轮子了,ansible官方早就制定了一套统一的规范,全世界使用ansible的老铁都是按照官方的规范来操作的,如果公司按照你俩制定的规范来,岂不是反而不符合大多数人的规范吗?",我和狗蛋本来是想嘚瑟一下,谁知道最后反而被总监教育了一番,总监告诉我们,ansible官方制定的那一套规范被称之为"角色(Roles)",让我俩赶紧去了解一下。

我和狗蛋真是后知后觉,原来官方早就有"角色"这种东西,角色是一种解决问题的思想,也是一种通用的规范,比如,我们可以把nginx的相关配置过程抽象成一个nginx角色,以便下次需要进行同样的配置时,调用这个角色,同理,redis的配置过程也可以抽象成一个redis角色,不过,无论是哪种角色,都是按照ansible官方规范好的目录结构进行创建的,那么此处,我们就来了解一下ansible官方定义的这个"规范",也就是所谓的"角色"。

为了让你更直观的认识角色的标准结构,我提前创建了一个示例角色,示例角色的目录结构如下:
1.png
如你所见,我创建的示例角色的角色名为demorole,demorole目录就代表了这个角色,此目录中包含了defaults 、files 、handlers 、meta  、tasks 、templates 、vars等子目录,而且在defaults 、handlers 、meta  、tasks 、vars等目录中,还都有一个名为"main.yml"的文件,那么这样的目录结构代表了什么含义呢?我们一起来了解一下,在角色中,上述目录结构的作用如下:
注:此处先进行大致介绍,以便你有一个大概的印象,之后会有对应的示例,所以,如果有疑问请先保留。

tasks目录:角色需要执行的主任务文件放置在此目录中,默认的主任务文件名为main.yml,当调用角色时,默认会执行main.yml文件中的任务,你也可以将其他需要执行的任务文件通过include的方式包含在tasks/main.yml文件中。
handlers目录:当角色需要调用handlers时,默认会在此目录中的main.yml文件中查找对应的handler
defaults目录:角色会使用到的变量可以写入到此目录中的main.yml文件中,通常,defaults/main.yml文件中的变量都用于设置默认值,以便在你没有设置对应变量值时,变量有默认的值可以使用,定义在defaults/main.yml文件中的变量的优先级是最低的。
vars目录:角色会使用到的变量可以写入到此目录中的main.yml文件中,看到这里你肯定会有疑问,vars/main.yml文件和defaults/main.yml文件的区别在哪里呢?区别就是,defaults/main.yml文件中的变量的优先级是最低的,而vars/main.yml文件中的变量的优先级非常高,如果你只是想提供一个默认的配置,那么你可以把对应的变量定义在defaults/main.yml中,如果你想要确保别人在调用角色时,使用的值就是你指定的值,则可以将变量定义在vars/main.yml中,因为定义在vars/main.yml文件中的变量的优先级非常高,所以其值比较难以覆盖。
meta目录:如果你想要赋予这个角色一些元数据,则可以将元数据写入到meta/main.yml文件中,这些元数据用于描述角色的相关属性,比如 作者信息、角色主要作用等等,你也可以在meta/main.yml文件中定义这个角色依赖于哪些其他角色,或者改变角色的默认调用设定,在之后会有一些实际的示例,此处不用纠结。
templates目录: 角色相关的模板文件可以放置在此目录中,当使用角色相关的模板时,如果没有指定路径,会默认从此目录中查找对应名称的模板文件。
files目录:角色可能会用到的一些其他文件可以放置在此目录中,比如,当你定义nginx角色时,需要配置https,那么相关的证书文件即可放置在此目录中。

当然,上述目录并不全是必须的,也就是说,如果你的角色并没有相关的模板文件,那么角色目录中并不用包含templates目录,同理,其他目录也一样,一般情况下,都至少会有一个tasks目录。

看完上述描述,你可能还是有一些小疑惑,不如我们来动手写一个简单的角色,这样就比较容易理解了。
注:当前用于测试的ansible版本为2.7.0,以下所有示例基于此版本进行。

为了熟悉角色的使用,我们一起来纯手动的编写一个用于测试的角色吧,这个角色的名字就叫"testrole"。
首先,我们创建一个名为"testrole"的目录,这个目录就代表了"testrole"角色,执行如下命令即可:
  1. # mkdir testrole
复制代码
角色目录创建完毕,我决定先赋予testrole角色一个简单的功能,即输出"hello role"这句话,没错,通过debug模块可以输出信息,那么我们需要编写一个debug任务,之前提到过,调用角色时,角色会默认执行tasks/main.yml中的任务,那么我们就把debug任务写在tasks/main.yml文件中吧,首先,在testrole目录中创建tasks子目录,在tasks子目录中创建一个名为main.yml的文件。
  1. # mkdir tasks
  2. # touch tasks/main.yml
复制代码
在tasks/main.yml文件中写入如下内容
  1. - debug:
  2.     msg: "hello role!"
复制代码
如你所见,直接将我们需要执行的任务写在tasks/main.yml文件中即可。

我们并不需要创建handlers、defaults等目录,因为目前我们编写的角色非常简单,用不到这些目录结构。

我们的第一个测试角色编写完毕了~,就是这么简单,角色编写完毕后,就需要调用对应的角色了,那么怎样才能调用角色呢?其实也很简单,我们只需要在testrole的同级目录中编写一个简单的剧本即可,此例中,调用角色的剧本文件名为test.yml,它与testrole目录处于同级目录中,如下:
  1. # ls
  2. testrole  test.yml
复制代码
我们需要使用test.yml文件来调用testrole角色,test.yml文件的内容如下:
  1. # cat test.yml
  2. - hosts: test70
  3.   roles:
  4.   - testrole
复制代码
如上例所示,调用对应的角色时,需要使用roles关键字进行调用,使用"- hosts"指定目标主机,上例表示,在目标主机test70上执行testrole角色对应的任务,但是由于testrole并没有什么其他操作,只是输出了一句话,所以并不会对目标主机有什么实际动作。

那么我们执行test.yml,看看这个测试角色能不能正常被调用,执行后结果如下:
  1. # ansible-playbook test.yml

  2. PLAY [test70] *************************************************

  3. TASK [Gathering Facts] *****************************************
  4. ok: [test70]

  5. TASK [testrole : debug] ****************************************
  6. ok: [test70] => {
  7.     "msg": "hello role!"
  8. }

  9. PLAY RECAP *************************************************
  10. test70                     : ok=2    changed=0    unreachable=0    failed=0
复制代码
如我们所愿,角色被正常调用了,"hello role!"输出了,没错,我们已经学会使用角色了。

看到此处,你会不会觉得我把简单的问题复杂化了,如果你只是单纯的调用一个debug任务输出一句话,那么是有点复杂化了,但是如果你的配置过程慢慢变得丰富,文件越来越多,结构越来越复杂,那么使用角色会是更好的选择,它能让你的文件结构符合统一的标准,让任何一个懂得这个标准的人快速的阅读你的代码,并且为以后的扩展留出很大的空间,而且,通过刚才的调用过程,你应该已经明白了,我可以将testrole目录移至到任何ansible主机中进行调用,testrole目录中包含了这个角色所需的所有文件,它是一个独立的的结构,说到独立,它在逻辑上也是独立的,因为这个角色的配置过程与目标主机是分开的,虽然我们调用角色时,需要编写一个playbook指定对应的目标主机,但是我们并没有修改角色目录中的任何文件,这正好解决了本文开头提出的问题。

刚才在调用角色时,我刻意的将test.yml文件写在了testrole目录的同级目录中,也就是说,调用testrole角色时,test.yml会从同级目录中查找与testrole角色同名的目录,其实,不仅仅是同级目录,还有一些其他的目录,在调用角色时,test.yml也会去查找,这些目录就是:
同级目录中的roles目录中。
当前系统用户的家目录中的.ansible/roles目录,即 ~/.ansible/roles目录中。
也就是说,只要testrole目录处于上述三个目录中的任何一个目录中,都可以使用上述方法正常的调用。

你也可修改ansible的配置文件,设置自己的角色搜索目录,编辑/etc/ansible/ansible.cfg配置文件,设置roles_path选项,此项默认是注释掉的,将注释符去掉,当你想要设置多个路径时,多个路径之间用冒号隔开,示例如下
  1. roles_path    = /etc/ansible/roles:/opt:/testdir
复制代码
即使你的角色目录不处于上述目录中的任何一个,也可以使用绝对路径的方式,调用对应的角色,示例如下:
  1. - hosts: test70
  2.   roles:
  3.   - "/testdir/ansible/testrole/"
复制代码
如上例所示,我直接使用了testrole的绝对路径,调用了testrole角色,其实,上述写法不是特别正规,标准的语法应该是如下模样:
  1. - hosts: test70
  2.   roles:
  3.   - role: "/testdir/ansible/testrole/"
复制代码
没错,在roles关键字中使用role关键字指定角色对应的绝对路径,也可以直接调用角色,即使不使用绝对路径,也可以使用同样的语法指定角色名,如下:
  1. - hosts: test70
  2.   roles:
  3.   - role: testrole
复制代码
除了上述调用角色的语法,还有一些其他的语法也可以调用角色,不过后文中再行总结,先往下聊。

上述示例中,我们没有使用任何变量,那么我们来尝试一下在角色中使用变量。
我们将tasks/main.yml中的内容改为如下内容:
  1. - debug:
  2.     msg: "hello {{ testvar }} !"
复制代码
如上例所示,我们在输出的信息中使用了testvar变量,那么,我们在调用这个角色时,则需要传入对应的变量,否则就会报错,调用上例角色的示例如下:
  1. - hosts: test70
  2.   roles:
  3.   - role: testrole
  4.     vars:
  5.       testvar: "www.cnblackhat.com"
复制代码
如上例所示,我们在调用角色时,传入了对应的testvar变量,以便对应的任务可以使用这个变量。
执行上例playbook,最终输出的信息为"hello www.cnblackhat.com !"

其实,我们也可以为testvar变量设置默认值,这样即使在调用角色时没有传入任何参数,也有默认的值可以使用,同时也不会在调用时因为没有传入对应变量而报错,所以,我们需要在testrole目录中创建一个defaults目录,并且创建defaults/main.yml文件,defaults/main.yml文件内容如下:
  1. # cat testrole/defaults/main.yml
  2. testvar: "role"
复制代码
如你所见,我们在defaults/main.yml文件中定义了testvar变量,默认值为"role"
此刻,我们调用testrole时,即使不传入testvar变量,也可以正常的进行调用了,如果不传入testvar变量,则默认所使用"role"作为变量值。

需要注意的是,在默认情况下,角色中的的变量是全局可访问的。
这样说可能不容易理解,不如来看一个小示例,如下
2.png
如上图所示,我定义了两个示例角色,这两个示例角色中都使用了名为testvar的变量,而且在这两个角色中,testvar变量都有各自的默认值,在testrole角色中,testvar的默认值为"test",在demorole角色中,testvar的默认值为"demo",在test.yml文件中,我调用了这两个角色,在调用testrole角色时,我传入了testvar变量,其值为zsythink,但是在调用demorole角色时,没有传入testvar变量,按照正常的理解,当执行test.yml文件时,testrole应该使用"zsythink"作为testvar变量的值,demorole应该使用默认值"demo"作为testvar变量的值,那么我们来执行一下test.yml,看看结果与我们想象的是否相同,执行结果如下
  1. # ansible-playbook test.yml

  2. PLAY [test70] *************************************************

  3. TASK [Gathering Facts] *****************************************
  4. ok: [test70]

  5. TASK [testrole : debug] *****************************************
  6. ok: [test70] => {
  7.     "msg": "hello zsythink!"
  8. }

  9. TASK [demorole : debug] **************************************
  10. ok: [test70] => {
  11.     "msg": "hello zsythink!"
  12. }

  13. PLAY RECAP *************************************************
  14. test70                     : ok=3    changed=0    unreachable=0    failed=0
复制代码
如你所见,结果与我预想的并不相同,无论是testrole还是demorole,都使用了"zsythink"作为了testvar的变量值,出现上述状况的原因我们刚才已经提到过,原因是:在默认情况下,角色中的变量是全局可访问的,上例中,当将testvar变量的值设置为"zsythink"时,就表示将testrole和demorole中的testvar变量的值都设置成了"zsythink",所以最终输出信息时,两个角色的testvar变量都使用了相同的值。

如果想要解决上述问题,则可以将变量的访问域变成角色所私有的,如果想要将变量变成角色私有的,则需要设置/etc/ansible/ansible.cfg文件,将private_role_vars的值设置为yes,默认情况下,"private_role_vars = yes"是被注释掉的,将前面的注释符去掉皆可,设置完成后,再次执行上例中的test.yml文件,输出结果如下:
  1. # ansible-playbook test.yml

  2. PLAY [test70] *************************************************

  3. TASK [Gathering Facts] *****************************************
  4. ok: [test70]

  5. TASK [testrole : debug] *****************************************
  6. ok: [test70] => {
  7.     "msg": "hello zsythink!"
  8. }

  9. TASK [demorole : debug] **************************************
  10. ok: [test70] => {
  11.     "msg": "hello demo!"
  12. }

  13. PLAY RECAP *************************************************
  14. test70                     : ok=3    changed=0    unreachable=0    failed=0
复制代码
默认情况下,我们无法多次调用同一个角色,也就是说,如下playbook只会调用一次testrole角色:
  1. # cat test.yml
  2. - hosts: test70
  3.   roles:
  4.   - role: testrole
  5.   - role: testrole
复制代码
执行上例playbook会发现,testrole的debug模块只输出了一次,如果想要多次调用同一个角色,有两种方法,如下:
方法一:设置角色的allow_duplicates属性 ,让其支持重复的调用。
方法二:调用角色时,传入的参数值不同。

方法一需要为角色设置allow_duplicates属性,而此属性需要设置在meta/main.yml文件中,所以我们需要在testrole中创建meta/main.yml文件,写入如下内容:
  1. # cat testrole/meta/main.yml
  2. allow_duplicates: true
复制代码
如上例所示,我们将allow_duplicates属性设置为true,表示可以重复调用同一个角色。
属性设置完毕后,执行如下playbook尝试两次调用同一个角色,是完全可以正常执行的。
  1. # cat test.yml
  2. - hosts: test70
  3.   roles:
  4.   - role: testrole
  5.   - role: testrole
复制代码
说完方法一,现在来说说方法二,当调用角色需要传参时,如果参数的值不同,则可以连续调用多次
下例中,两次调用了testrole角色,两次调用都传入了testvar变量,但是testvar变量的值不同。
  1. # cat test.yml
  2. - hosts: test70
  3.   roles:
  4.   - role: testrole
  5.     vars:
  6.       testvar: "zsythink"
  7.   - role: testrole
  8.     vars:
  9.       testvar: "zsythink.net"
复制代码
使用上例的方法,也可以对同一角色调用多次。

在上述示例中,我们已经学会了如何在defaults/main.yml文件中定义变量的默认值,不过我们还没有使用过vars/main.yml文件,也没有在其中定义过任何变量,之前提到过,定义在vars/main.yml文件中的变量优先级比较高,难以被覆盖,那我们就来动手试试,看看定义在这个文件中的变量的优先级到底有多高,为了使效果更加明显,我们在defaults/main.yml文件和vars/main.yml文件中同时定义testvar变量,并为其赋值不同的值,如下:
  1. # cat testrole/defaults/main.yml
  2. testvar: "test"
  3. # cat testrole/vars/main.yml
  4. testvar: "testvar_in_vars_directory"
复制代码
同时,我们在调用testrole时,仍然传入testvar变量,看看testvar变量到底会使用哪个值作为最终的值,示例如下:
  1. # cat test.yml
  2. - hosts: test70
  3.   roles:
  4.   - role: testrole
  5.     vars:
  6.       testvar: "zsythink"
复制代码
执行上例playbook,最终结果如下:
  1. # ansible-playbook test.yml

  2. PLAY [test70] *************************************************

  3. TASK [Gathering Facts] *****************************************
  4. ok: [test70]

  5. TASK [testrole : debug] ****************************************
  6. ok: [test70] => {
  7.     "msg": "hello testvar_in_vars_directory!"
  8. }

  9. PLAY RECAP *************************************************
  10. test70                     : ok=2    changed=0    unreachable=0    failed=0
复制代码
从上述信息可以看出,即使在调用角色的时候传入对应的变量,也无法覆盖定义在vars/main.yml文件中的值,那么我们可以利用这个特性,将你想要确保使用的值定义在vars/main.yml中,以便别人在调用角色时,使用的值就是你定义的值,当然,如果你强烈推荐的值别人压根不想使用,也是有办法灵活的进行覆盖的,比如在调用playbook时使用"-e"选项传入参数,示例如下:
  1. # ansible-playbook -e testvar='usethis' test.yml

  2. PLAY [test70] *************************************************

  3. TASK [Gathering Facts] *****************************************
  4. ok: [test70]

  5. TASK [testrole : debug] ****************************************
  6. ok: [test70] => {
  7.     "msg": "hello usethis!"
  8. }

  9. PLAY RECAP *************************************************
  10. test70                     : ok=2    changed=0    unreachable=0    failed=0
复制代码
除了使用"-e"传入的变量的优先级,其他变量(包括主机变量)的优先级均低于vars/main.yml中变量的优先级。

假设现在testrole需要使用一些模板,那么也可以直接将模板文件放到templates目录中。
比如,testrole中需要使用一个名为test.conf.j2的模板文件,那么我们就将test.conf.j2文件放置在testrole/templates/目录中,test.conf.j2文件内容如下
  1. # cat testrole/templates/test.conf.j2
  2. something in template;
  3. {{ template_var }}
复制代码
模板文件中使用到了 template_var变量,我们可以为 template_var变量定义一个默认变量
  1. # cat testrole/defaults/main.yml
  2. testvar: "test"
  3. template_var: "template"
复制代码
然后在testrole中,直接使用这个模板文件
  1. # cat testrole/tasks/main.yml
  2. - debug:
  3.     msg: "hello {{ testvar }}!"
  4. - template:
  5.     src: test.conf.j2
  6.     dest: /opt/test.conf
复制代码
如上例所示,我们在使用template任务时,src直接指定了对应的模板文件的名称,并没有指定任何路径,这代表角色会默认去templates子目录中查找对应的文件。

如果你想要在角色中使用一些handlers以便进行触发,则可以直接将对应的handler任务写入到handlers/main.yml文件中,示例如下:
  1. # cat testrole/handlers/main.yml
  2. - name: test_handler
  3.   debug:
  4.     msg: "this is a test handler"
复制代码
我直接在handlers/main.yml文件中写入了一个名为"test_handler"任务,以便随时进行触发。
为了能够更加简单的触发对应的handler,我直接将tasks/main.yml中的debug任务的状态强行设置为"changed",示例如下:
注:前文已经总结了changed_when的用法,此处不再赘述。
  1. # cat testrole/tasks/main.yml
  2. - debug:
  3.     msg: "hello testrole!"
  4.   changed_when: true
  5.   notify: test_handler
复制代码
如上例所示,当需要notify对应handler时,直接写入handler对应的名称即可,角色会自动去handlers/main.yml文件中查找对应的handler。

经过上述描述,你肯定已经对"角色"有了初步的认识,不过我们还有很多关于角色的话题没有聊,比如使用ansible-galaxy命令快速创建角色、下载角色、管理角色,比如其他引用角色的方法,等等,这些就放在后面的文章中进行总结吧,这篇文章先写到这里,希望能够对你有所帮助~






欢迎光临 黑帽联盟 (https://bbs.cnblackhat.com/) Powered by Discuz! X2.5