最近我们团队在推进运维自动化用Ansible做配置管理和应用部署大大提升了运维效率以前部署一个应用要手动登录每台服务器操作又慢又容易出错现在,用Ansible写个Playbook一键就能部署到所有服务器又快又稳效率提升了很多。

但是Ansible虽然好用坑也不少前几天就遇到了一个奇怪的Bug排查到深夜才解决过程很曲折也很有启发今天想记录一下这次深夜Bug排查的经过包括问题现象排查过程根本原因以及解决方案和经验教训希望能帮大家在使用Ansible的时候,少踩坑遇到类似问题能快速定位和解决。

一、问题现象

事情是这样的前几天我们要上线一个新版本的应用按照惯例我用AnsiblePlaybook来部署这个Playbook已经用了很久了之前,部署过很多次都很顺利没有出过问题,所以我也没太在意像往常一样执行了部署命令准备喝杯咖啡等部署完成。

结果没想到Playbook执行到一半就报错了错误信息很奇怪是在执行一个template模块的任务时报错说"AnsibleError: template error while templating string: unexpected '}'expected ']'"大概是模板语法错误的意思我一看就有点懵了,因为这个模板文件和Playbook都很久没改过了之前,一直用得好好的怎么突然就报模板语法错误了呢?

我以为是我不小心改坏了什么文件就去看了一下那个模板文件和Playbook的Git提交记录发现最近确实没有人改过这些文件最近一次修改还是几个月前,而且之前,部署都正常这就更奇怪了文件没改过之前,一直正常怎么今天突然就报错了呢?

我又试了几次重新执行Playbook结果还是在同一个地方报错同样的错误信息看来不是偶发问题是必现的这时候已经是,晚上8点多了本来以为很快就能部署完下班结果遇到这个奇怪的问题看来今晚要加班了没办法只能开始排查这个问题。

二、排查过程

排查的过程很曲折走了很多弯路花了好几个小时才找到根本原因这里记录一下排查的过程。

第一步:检查模板文件语法

首先,我怀疑是模板文件的语法有问题,虽然之前,一直正常,但是可能有什么隐藏的语法问题之前,没触发今天触发了,于是我仔细检查了那个报错的模板文件的语法这个模板文件是一个应用的配置文件模板用Jinja2语法里面有一些变量和条件判断,比如{{ app_name }}{% if env == 'prod' %}...{% endif %}等等。

我逐行检查了模板文件的语法看有没有不匹配的括号花括号百分号等等检查了很久没有发现语法问题所有的{{ }}{% %}都是配对的没有不匹配的情况,而且这个模板之前,一直正常使用说明语法应该没问题那为什么会报模板语法错误呢?

我又用Ansible的--check模式和-vvv详细输出执行了一下想看更详细的错误信息结果详细输出显示错误确实是在模板渲染的时候,发生的,但是具体是哪一行哪一列出问题没有显示得很清楚只显示了模板错误的大致信息这让排查更困难了。

第二步:检查变量是否有问题

模板语法没问题那可能是变量的问题,比如某个变量的值里面包含了特殊字符导致模板渲染出错,因为Jinja2模板在渲染的时候,会对变量的值也进行二次渲染(如果变量值里有{{ }}这样的模板语法会被再次,渲染)如果变量值里有不合法的模板语法就会导致渲染出错。

于是我开始检查模板里用到的所有变量的值看有没有哪个变量的值里包含了特殊字符,或者不合法的模板语法我把模板里用到的变量都列了出来,然后逐个检查它们的值这些变量有些是在Playbook里定义的有些是在Inventory里定义的有些是从外部变量文件加载的。

检查了很久终于发现了一个可疑的变量有一个变量叫app_config它的值是一个JSON字符串里面包含了应用的一些配置这个JSON字符串里有一个字段的值是一个正则表达式里面包含了{}字符,比如"pattern": "^\\d{4}-\\d{2}-\\d{2}$"这里面的{4}{2}看起来有点像Jinja2的模板语法{{ }}虽然不是完全一样,但是会不会是这个导致模板渲染出错呢?

我觉得,很有可能,因为Jinja2在渲染模板的时候,如果变量值里有{字符可能会被误判为模板语法的开始导致解析错误特别是,如果有不匹配的{}就会报"unexpected '}'"这样的错误和我们遇到的错误信息很像,于是我把这个变量的值临时改成一个简单的字符串没有特殊字符,然后重新执行Playbook结果,居然成功了没有报错!

这说明问题确实出在这个变量上这个变量的值里的特殊字符导致了模板渲染错误,但是为什么之前,一直正常今天才出问题呢?我又去看了一下这个变量的定义发现这个变量是从一个外部变量文件加载的而这个变量文件最近被人改过加了那个正则表达式的配置,所以之前,没有这个特殊字符模板渲染正常现在加了这个正则表达式里面有{}字符就导致模板渲染出错了终于找到问题的直接原因了!

第三步:深入研究根本原因

找到直接原因之后,我又深入研究了一下为什么变量值里的{}会导致模板渲染错误,因为按道理Jinja2应该只会渲染模板文件里的{{ }}{% %}不会对变量值里的内容进行二次渲染啊为什么变量值里的{会导致问题呢?

研究了很久查了Ansible的文档和Jinja2的文档还看了一些社区的讨论终于搞清楚了根本原因原来Ansible有一个特性叫"unsafe"或者说Ansible默认会对变量进行递归模板渲染也就是,不仅模板文件里的{{ }}会被渲染变量值里,如果有{{ }}这样的模板语法也会被再次,渲染这是Ansible的一个设计为了支持变量嵌套,比如变量A的值是{{ variableB }}渲染变量A的时候,会把variableB的值也渲染出来这样很灵活,但是也带来了问题,如果变量值里有{}这样的字符,但是不是合法的模板语法就会导致渲染错误。

而且更坑的是Ansible的模板渲染对{字符的处理比较特殊,如果变量值里有单独的{或者}不是成对的{{ }}或者{% %}有时候会被误判为模板语法的一部分导致解析错误特别是当{后面跟着数字,或者其他字符的时候,更容易出问题我们遇到的那个正则表达式\\d{4}里面的{4}就被Ansible的模板引擎误判了以为是模板语法的开始,但是又找不到匹配的}}或者%}就报了"unexpected '}'"的错误。

而且这个问题,还有一个坑就是它不是必现的取决于变量值里的具体内容和位置有些{不会触发问题有些会,所以之前,我们的变量里也有一些{字符,但是没有触发问题这次加了那个正则表达式正好触发了这个问题,所以才突然报错这也是为什么之前,一直正常今天才出问题的原因。

搞清楚根本原因之后,已经是深夜11点多了花了好几个小时终于找到问题的根源了接下来就是解决问题。

三、解决方案

找到根本原因之后,我研究了几种解决方案最后选择了最合适的一种这里分享一下几种可能的解决方案以及它们的优缺点。

方案1:用unsafe标记变量禁止二次渲染

Ansible提供了一个unsafe的机制可以标记某个变量为unsafe这样Ansible就不会对这个变量的值进行二次模板渲染会原样输出这样就能避免变量值里的特殊字符导致模板渲染错误这是Ansible官方推荐的处理包含特殊字符的变量的方式。

使用方法是在定义变量的时候,用!unsafe标记,比如:

app_config: !unsafe |
  {"pattern": "^\\d{4}-\\d{2}-\\d{2}$"}

或者在Playbook里用set_fact模块设置变量的时候,加上unsafe: yes参数:

- name: Set app config as unsafe
  set_fact:
    app_config: "{{ lookup('file', 'app_config.json') }}"
    unsafe: yes

这样app_config变量就被标记为unsafeAnsible不会对它的值进行二次渲染会原样输出到模板里这样就不会,因为变量值里的{}导致模板渲染错误了。

这个方案的优点是是Ansible官方推荐的方式比较规范也比较彻底能完全避免二次渲染的问题缺点是需要修改变量的定义,而且,如果变量里确实需要嵌套其他变量的话,用了unsafe之后,就不能嵌套了会原样输出,所以要根据实际情况选择。

方案2:用过滤器禁止渲染,或者转义特殊字符

另一种方案是在模板里使用变量的时候,加上过滤器禁止渲染,或者转义特殊字符,比如用| to_json过滤器把变量转成JSON字符串这样特殊字符会被转义不会导致模板渲染错误,或者用| string过滤器把变量转成字符串,但是这个可能还是会被二次渲染不一定能解决问题。

比如在模板里:

{{ app_config | to_json }}

这样appconfig会被转成JSON字符串特殊字符会被转义不会导致模板渲染错误这个方案的优点是不用修改变量定义只需要改模板里的引用比较方便缺点是可能会改变变量的输出格式,比如tojson会加上引号等等,如果需要原样输出的话,可能不合适,而且也不是所有情况都能用过滤器解决。

方案3:把特殊字符转义,或者用原始字符串

第三种方案是把变量值里的特殊字符转义,比如把{转义成{{ '{' }}这样Jinja2会把它当普通字符输出不会误判为模板语法,但是这个方案比较麻烦需要修改变量值里的每个特殊字符,而且,如果变量值是动态生成的,或者从外部加载的不好转义,所以不太推荐。

我们选择的方案

最后我们选择了方案1用unsafe标记那个包含特殊字符的变量,因为那个变量是一个JSON配置字符串里面可能会有各种特殊字符(正则表达式等等)用unsafe标记之后Ansible会原样输出不会二次渲染能彻底避免这类问题,而且这个变量不需要嵌套其他变量,所以用unsafe没有问题修改之后,重新执行Playbook果然成功了没有报错部署顺利完成这时候已经是深夜12点多了终于解决了问题可以下班了。

四、经验和教训

这次深夜Bug排查,虽然过程很曲折花了很多时间,但是也让我学到了很多对Ansible的理解也更深入了这里总结一些经验和教训分享给大家。

教训1:Ansible会对变量进行二次渲染要注意特殊字符

这是最重要的教训Ansible默认会对变量进行递归二次模板渲染也就是变量值里,如果有{{ }}{% %}这样的模板语法会被再次,渲染这是Ansible的一个特性很灵活,但是也很容易踩坑,如果变量值里有特殊字符(比如{}%等等)可能会导致模板渲染错误,或者意外的渲染结果,所以在使用Ansible的时候,一定要注意变量值里的特殊字符特别是包含JSONXML正则表达式代码等等内容的变量要特别小心。

对于包含特殊字符的变量建议用unsafe标记禁止二次渲染,或者用适当的过滤器处理避免意外的渲染问题这是Ansible的一个常见坑很多人都踩过希望大家能注意避免。

教训2:报错信息可能有误导性要深入排查根本原因

第二个教训是Ansible的报错信息有时候可能有误导性,比如我们这次遇到的错误信息是"template error while templating string: unexpected '}'"看起来像是模板文件的语法错误,但是实际上,根本原因是变量值里的特殊字符导致的二次渲染错误,如果只看报错信息去检查模板文件的语法可能永远找不到问题,所以遇到问题的时候,不要只看表面的报错信息要深入排查根本原因多想一步可能是什么其他原因导致的这个错误。

而且Ansible的报错信息有时候不够详细不会告诉你具体是哪个变量哪个位置出问题需要自己逐个排查这时候要有耐心用排除法逐个变量检查,或者临时修改变量值看问题是否消失来定位问题变量不要急躁慢慢排查总能找到根本原因。

教训3:变量文件的变更也要审查不能忽视

第三个教训是变量文件的变更也要审查不能忽视我们这次的问题直接原因是有人在变量文件里加了一个正则表达式的配置导致变量值里有了特殊字符,但是这个变更没有经过严格的审查也没有测试就直接合并了导致部署的时候,才发现问题,如果在代码审查的时候,能注意到这个变量值里有特殊字符可能会导致Ansible模板渲染问题提前处理就不会导致深夜排查问题了。

所以,不管是代码变更还是配置变更变量文件变更都要经过严格的审查和测试不能,因为是配置文件就忽视很多问题都是由配置变更引起的,而且配置问题往往比较隐蔽不容易发现等到线上出问题才发现就晚了,所以一定要重视配置变更的审查和测试。

教训4:要有回滚预案部署前做好准备

第四个教训是部署前要有回滚预案做好准备这次我们部署遇到问题,虽然花了很多时间排查,但是好在没有影响线上业务,因为我们是先部署测试环境再部署生产环境在测试环境就发现了问题没有影响生产,而且我们也有回滚预案,如果部署出问题能快速回滚到上一个版本,所以没有造成大的影响。

但是这次也提醒我们部署前一定要做好准备有回滚预案要先在测试环境验证没问题再部署生产环境不要直接在生产环境部署避免出问题影响线上业务,而且部署的时候,要有人值守出了问题能及时处理不要部署完就,不管了出了问题没人处理就麻烦了。

教训5:自动化工具虽好也要理解其原理和坑

第五个教训是自动化工具虽好也要理解其原理和坑Ansible这样的自动化工具确实能大大提升效率,但是它们也有自己的原理和坑,如果只会用不理解原理遇到问题就很难排查,所以在使用自动化工具的时候,不仅要会用还要理解其基本原理和常见的坑这样遇到问题才能快速定位和解决而不是束手无策。

而且自动化工具不是银弹不能解决所有问题也可能引入新的问题,所以在推进自动化的,同时也要保持警惕做好测试和审查避免自动化工具本身的问题导致线上故障自动化是为了提升效率和稳定性不是为了引入新的问题,所以一定要用好自动化工具理解其原理和坑避免踩坑。

五、写在最后

以上就是这次Ansible自动化踩坑深夜Bug排查的全部记录包括问题现象排查过程根本原因解决方案以及经验和教训希望能帮大家在使用Ansible的时候,少踩坑遇到类似问题能快速定位和解决。

这次排查,虽然花了很多时间到深夜才解决,但是也让我对Ansible的理解更深入了学到了很多宝贵的经验这些经验在以后的工作中会很有帮助能让我更好地使用Ansible避免类似的问题,所以,虽然过程辛苦,但是也很有价值。

运维自动化是趋势能大大提升效率和稳定性,但是在推进自动化的过程中也会遇到各种坑和问题这是正常的,只要我们认真排查总结经验就能不断进步把自动化做得更好更稳希望大家都能在自动化的道路上少踩坑多收获提升运维效率和系统稳定性。

最后用一句话结束这篇文章:"自动化工具是利器也是坑理解原理避开坑才能发挥利器的威力提升效率和稳定性。"

愿大家都能用好Ansible等自动化工具让运维工作更轻松更高效。