使用Ansible进行实际尝试时遇到的困扰点

首先

这篇文章是AP通讯公司2018年圣诞日历的第二天的文章。

最近,我正在業務中進行在AWS上進行伺服器替換,並且致力於使用Ansible進行構建自動化的工作。之前我已經稍微熟悉Ansible了,但很少有適用於工作並全力以赴使用的機會,這次是我第一次有這樣的機會。然而,當我試著寫下並運行時,它並沒有按照我預想的那樣運行,讓我感到有些困惑。

大致上是像公式文件和书籍中所写的内容,但这些内容往往容易被忽略或者是一些较为冷门的情况,希望对即将使用或长时间未使用该内容的人们能有所帮助。

顺便提一下,Ansible 的版本是 2.6.3。如果已经通过更新等方式解决了问题,请忽略这句话。另外,如果有任何问题、补充或建议,请留下评论。感激不尽。

变量编程

设定与魔术变量同名的变量

Ansible中有一个称为“魔术变量”的东西。
这些是一些很方便的变量,它们自动填充了在清单等中编写的与主机相关的信息。
魔术变量共有以下四种。

    • hostvars

 

    • groups

 

    • group_names

 

    inventory_hostname

在提供的链接文档中也提到了不要使用这些变量名。特别是像groups这样的短变量名可能会被忽略或无意识地使用。顺便说一下,即使在Playbook或者***_vars中使用了魔术变量,也不一定会在使用该变量的任务中报错。因为无论将什么值放入变量中,都会被魔术变量的值覆盖并执行。所以任务通常会报错并停止。但是在最糟糕的情况下,任务可能会以与预期不同的值继续进行,导致意想不到的结果。

举个例子,当你写了这样的Playbook时,

- hosts: all
  gather_facts: false
  vars:
  - groups: hogehoge
  tasks:
  - name: test vars
    debug:
      msg: "groups is {{ groups }}"

我打算的值是groups是hogehoge,但是…

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

TASK [test vars] *******************************************************************************************************
ok: [192.168.33.100] => {
    "msg": "groups is {'all': ['192.168.33.100'], 'ungrouped': [], 'vagrant': ['192.168.33.100']}"
}

PLAY RECAP *************************************************************************************************************
192.168.33.100             : ok=1    changed=0    unreachable=0    failed=0

当将值设置为 hogehoge 的时候,魔术变量会被覆盖并进行处理。

逃避方案

如果要设置变量,则需要避免使用魔术变量,因此需要在文档等中了解魔术变量的含义。
虽然这不仅限于此问题,但在审核之前进行检查或在测试环境中进行操作确认似乎是个好主意。
顺便说一句,ansible-lint没有特别检测到错误等情况。

2018/12/2 补充记录

我收到了同事@akira6592提供的补充信息。
在文章中,我介绍了包括groups在内的4个魔术变量,
但是在文档的以下页面中列出了所有的魔术变量。
https://docs.ansible.com/ansible/latest/reference_appendices/special_variables.html
这似乎是从Ansible 2.7版本的文档中新增的页面。
真是比我想象的要多啊!
虽然这次写法强调了注意事项,但从本质上来说,它们是非常方便的,所以让我们大力利用吧。

使用不能作为变量的符号(如连字符)被错误地使用

在Ansible中,可用于变量的字符和符号仅限于以下三种。

    • アルファベット(大文字・小文字可) ※1文字目はアルファベットのみ可

 

    • 数字

 

    アンダースコア _

在Ansible的变量名中,只能使用下划线作为符号,不能使用连字符 -。

逃避方法

根据变量名,似乎只有一种选择,那就是去掉连字符。幸运的是,可以使用下划线,在蛇形命名法中替换为var1_var2,也可以使用驼峰命名法,如Var1Var2,并将大写和小写分别识别,这样改写应该是很容易的。

顺便说一句

如果设置了带有连字符的变量(例如:var1-var2),在Playbook内会显示以下错误。

ERROR! Invalid variable name in vars specified for Play: 'var1-var2' is not a valid variable name

如果在Playbook之外的地方,例如host_vars、group_vars等中进行了记述,那么不会出现上述错误,而会被处理成未定义的变量。

实际运行时会出现以下错误。test.yml
– hosts: all
gather_facts: false
tasks:
– name: debug1
debug:
var: var1-var2
– name: debug2
debug:
msg: “var is {{ var1-var2 }}”

运行结果1
$ ansible-playbook -i inventory.ini test.yml

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

TASK [debug1] **********************************************************************************************************
ok: [192.168.33.100] => {
“var1-var2”: “变量未定义!”
}

TASK [debug2] **********************************************************************************************************
fatal: [192.168.33.100]: FAILED! => {“msg”: “此任务包含一个未定义变量的选项。错误信息为:’var1’未定义\n\n错误出现在’/path-to-playbook/test.yml’的第7行,第5列,但具体的语法问题可能出现在文件中的其他位置。\n\n问题可能出现在以下行之间:\n\n var: var1-var2\n – name: debug2\n ^ 此处\n”}

PLAY RECAP *************************************************************************************************************
192.168.33.100 : ok=1 changed=0 unreachable=0 failed=1

debug1任务中的变量var1-var2被视为变量未定义!
debug2任务中出现了“’var1’未定义”的错误消息,让人感到疑惑。
如果将值赋给var1会发生什么呢?

运行结果2
$ ansible-playbook -i inventory.ini test.yml -e var1=test1

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

TASK [debug1] **********************************************************************************************************
ok: [192.168.33.100] => {
“var1-var2”: “变量未定义!”
}

TASK [debug2] **********************************************************************************************************
fatal: [192.168.33.100]: FAILED! => {“msg”: “此任务包含一个未定义变量的选项。错误信息为:’var2’未定义\n\n错误出现在’/path-to-playbook/test.yml’的第7行,第5列,但具体的语法问题可能出现在文件中的其他位置。\n\n问题可能出现在以下行之间:\n\n var: var1-var2\n – name: debug2\n ^ 此处\n”}

PLAY RECAP *************************************************************************************************************
192.168.33.100 : ok=1 changed=0 unreachable=0 failed=1

现在var2被定义为未定义。如果给var2赋值会发生什么呢?

运行结果3
$ ansible-playbook -i inventory.ini test.yml -e var1=test1 -e var2=test2

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

TASK [debug1] **********************************************************************************************************
fatal: [192.168.33.100]: FAILED! => {“msg”: “({{var1-var2}})上发生了意外的模板类型错误:不支持的运算数类型:’str’和’str'”}

PLAY RECAP *************************************************************************************************************
192.168.33.100 : ok=0 changed=0 unreachable=0 failed=1

这次在之前通过的debug1任务中出现了错误。奇怪。。。

嗯,这就是说要直接停止在Ansible变量中使用连字符了。当我看到类似于“実行結果1”的错误消息时,我没有意识到是由于连字符的问题,结果花了相当长时间陷入困境。

当将变量设置为ansible_become: true时,将无法返回登录用户。

(Translation: When the variable ansible_become is set to true, it is not possible to return to the login user.)

Ansible 中有类似 “become” 和 “become_user” 的配置项,既可以在 Playbook 中编写,也可以在变量中编写。
如果想要针对每个 Playbook 进行不同的配置,也可以在变量中编写以达到复用的目的。
根据使用案例的不同,Ansible 提供了多种不同的编写方式,这也是 Ansible 的优点之一。
但是,有时候即使我们认为两种写法是相同的,而以变量的形式编写时,行为也会略有不同,这让我感到困惑。

如果写一份如下的Playbook的话,

- hosts: all
  gather_facts: false
  become: true
  tasks:
  - name: root user
    command: whoami
  - name: login user
    command: whoami
    become: false

当运行后,我们可以看到第二个任务“登录用户”按照预期在vagrant用户下执行。

$ ansible-playbook -i inventory.ini whoami.yml -v
Using /etc/ansible/ansible.cfg as config file

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

TASK [root user] *******************************************************************************************************
changed: [192.168.33.100] => {"changed": true, "cmd": ["whoami"], "delta": "0:00:00.004528", "end": "2018-12-01 04:57:34.454376", "rc": 0, "start": "2018-12-01 04:57:34.449848", "stderr": "", "stderr_lines": [], "stdout": "root", "stdout_lines": ["root"]}

TASK [login user] ******************************************************************************************************
changed: [192.168.33.100] => {"changed": true, "cmd": ["whoami"], "delta": "0:00:00.004071", "end": "2018-12-01 04:57:35.172791", "rc": 0, "start": "2018-12-01 04:57:35.168720", "stderr": "", "stderr_lines": [], "stdout": "vagrant", "stdout_lines": ["vagrant"]}

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

但是,如果将ansible_become: true设置为变量,

$ ansible-playbook -i inventory.ini whoami.yml -v -e ansible_become=true
Using /etc/ansible/ansible.cfg as config file

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

TASK [root user] *******************************************************************************************************
changed: [192.168.33.100] => {"changed": true, "cmd": ["whoami"], "delta": "0:00:00.013255", "end": "2018-12-01 04:57:49.792209", "rc": 0, "start": "2018-12-01 04:57:49.778954", "stderr": "", "stderr_lines": [], "stdout": "root", "stdout_lines": ["root"]}

TASK [login user] ******************************************************************************************************
changed: [192.168.33.100] => {"changed": true, "cmd": ["whoami"], "delta": "0:00:00.005359", "end": "2018-12-01 04:57:50.544723", "rc": 0, "start": "2018-12-01 04:57:50.539364", "stderr": "", "stderr_lines": [], "stdout": "root", "stdout_lines": ["root"]}

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

第二个任务“login user”中的代码”become: false”没有生效,导致以root用户身份运行。

避免的方法

除非明确决定将所有任务以become_user(默认为root)身份运行,否则最好不要在变量中设置ansible_become: true,而是在Playbook中进行控制。

由于以下所述的模块系统中是否存在”become”变得非常重要,所以我们在这个案例中将”become”写在变量上,并且没有被解除,结果导致了困境。

模块编辑

在使用fetch模块时可能会发生MemoryError错误。

在文件传输模块中,常常使用copy模块,copy模块用于在Ansible执行机器 ===> 远程机器之间传输文件。
而fetch模块则相反,用于在Ansible执行机器 <=== 远程机器上获取文件。
然而,经常会出现以下错误情况。
虽然描述了很多内容,但最后可发现发生了MemoryError。

TASK [test fetch by root] **********************************************************************************************
fatal: [192.168.33.100]: FAILED! => {"changed": false, "module_stderr": "Shared connection to 192.168.33.100 closed.\r\n", "module_stdout": "Traceback (most recent call last):\r\n  File \"/tmp/ansible_U2m0xP/ansible_module_slurp.py\", line 87, in <module>\r\n    main()\r\n  File \"/tmp/ansible_U2m0xP/ansible_module_slurp.py\", line 83, in main\r\n    module.exit_json(content=data, source=source, encoding='base64')\r\n  File \"/tmp/ansible_U2m0xP/ansible_modlib.zip/ansible/module_utils/basic.py\", line 2364, in exit_json\r\n  File \"/tmp/ansible_U2m0xP/ansible_modlib.zip/ansible/module_utils/basic.py\", line 2358, in _return_formatted\r\n  File \"/tmp/ansible_U2m0xP/ansible_modlib.zip/ansible/module_utils/basic.py\", line 2312, in jsonify\r\n  File \"/tmp/ansible_U2m0xP/ansible_modlib.zip/ansible/module_utils/basic.py\", line 795, in jsonify\r\n  File \"/usr/lib64/python2.7/json/__init__.py\", line 250, in dumps\r\n    sort_keys=sort_keys, **kw).encode(obj)\r\n  File \"/usr/lib64/python2.7/json/encoder.py\", line 210, in encode\r\n    return ''.join(chunks)\r\nMemoryError\r\n", "msg": "MODULE FAILURE", "rc": 1}

查看fetch模块的文档,我发现它写得很详细,而且也与情况相符。

在使用become运行fetch时,将使用slurp模块来获取文件内容以确定远程校验和。这将有效地使传输的大小翻倍,并且根据文件大小,可能会消耗远程或本地主机上的所有可用内存,导致MemoryError。因此,建议尽可能在不使用become的情况下运行该模块。

粗略总结一下,fetch模块似乎使用了一个叫做slurp模块来确认目标文件的校验和。这个slurp模块会在内存中获取Base64的校验和,因此它至少会消耗目标文件大小的两倍的RAM。而且如果使用了become: true,这个内存消耗会再增加一倍(总共至少四倍)。因此,有时会因为内存不足而发生MemoryError的情况。

而且,即使可以传输,也需要相当长的时间。
从虚拟机中使用rsync获取100MB左右的文件只需要大约1秒,但使用fetch则需要几分钟。
这是因为校验和需要花费这么长的时间吗?

逃避方案

有几种可以考虑的避免策略。

become: falseにする

validate_checksumをfalseにする
別のモジュールを使う

成为:关于如何将其设置为false,
根据所需获取的文件,可能会由于登录用户权限不足而出现问题。
就我个人而言,对于只使用一次的代码,我不想花费太多时间去考虑,所以选择了一种不太好的方法,即将其复制到/tmp/目录下进行获取,当获取完成后再删除。

我将validate_checksum设置为false,虽然无法保证文件的一致性,但我觉得应该没问题就试着使用了。但不知怎么回事,出现了相同的错误。也许选项没有被识别?我查看了源代码,但只有ansible-doc显示的文档信息,没找到实际内容在哪里。

可以使用作为另一个模块的synchronize模块来进行文件操作。该模块是像copy、fetch这样的文件传输模块之一,类似于在Linux系统中熟知的rsync,可以递归地复制目录。该模块有两种模式, 默认是push模式,像copy模块一样将数据从Ansible执行机器传输到远程机器,但是如果使用pull模式,则可以像fetch模块一样从远程机器中获取数据。通过使用pull模式,可以替代fetch模块。

- hosts: all
  gather_facts: false
  become: true
  tasks:
  - name: sync
    synchronize:
      mode: pull
      src: /etc/sysconfig/ # リモートファイルパス
      dest: files          # 格納先(相対パスでも絶対パスでも可)

但是同步模块并不是在任何情况下都可以使用的。

目前,同步操作仅限于通过无需密码的sudo提升权限。这是因为rsync本身正在连接到远程机器,而rsync并没有提供一种传递sudo凭证的方式。

有一种方法可以省略输入密码,那就是仅使用sudo。
在Ansible中,可以使用多种方法作为become_method,在Linux系统中除了默认的sudo,还可以使用su。
但是,在只能使用su而无法使用sudo的环境中,与fetch模块类似,只能获取当前登录用户有权限访问的文件。

概述

无论是Ansible还是其他任何工具,都可以说一点是,在阅读工具的概述之后,“这样写应该可以使用?”这样的想法可以捉到,但实际运行时并不总是按照预期进行,这种情况相当常见。

另外,如果将Ansible视为配置管理工具,
尽管在构建过程中发送文件的次数可能较多,但获取文件的次数则相对较少,因此我认为像fetch模块这样的功能可能是相对较少使用的方式。
但是如果将其视为运维自动化工具来看,我认为有足够的机会使用它。

如果我能够稍微帮助其他用户,让他们从我的兴趣点中受益,我会感到非常高兴。

bannerAds