只要付出一点点努力,你就可以帮助下一个人,不只是绘制出安全路径,还可以留下危险的警告。
在博客圈里,人们对基础架构即代码、持续集成/持续交付(CI/CD)管道、代码审查和测试制度赞不绝口,但人们很容易忘记,这种精心设计的象牙塔只是一种理想,而不是现实。虽然不完美的系统困扰着我们,但我们必须交付一些东西。
在系统自动化的过程中,很少有比那些通过粘合 API 创建的象牙塔更脆弱的塔。这是一个脆弱的世界。要让它“工作起来”,交付它,然后继续前进,压力巨大。
要解决的问题
想象一个简单的功能请求:编写一些 Ansible 代码,在外部系统中创建几条记录,以记录一个 VLAN 的一些详细信息。我最近很想做一些实验室的管理工作来完成这个任务。这个外部系统是一个常见的互联网协议地址管理(IPAM)工具,但对于一个更抽象的配置管理数据库(CMDB)或一个与网络无关的记录来说,困难是一样的。在这个例子中,我创建一个记录的直接愿望就是让系统保存记录而已。
如果我们的目标是一个超紧凑的、直接的、笨拙的宏,那么它可能用 100 行代码就能写出来。如果我记得 API,我也许能在一个小时内把它敲出来,该代码的作用不会超过预期,除了确切的成品之外,什么也没留下。对它的目的而言是完美的,但是对未来的扩展毫无用处。
如今,我希望几乎每个人都能从一个角色和几个任务文件开始这项任务,准备扩展到十几个创建、读取、更新和删除(CRUD)操作。因为我不了解这个 API,我可能会花上几个小时到几天的时间,仅仅是摆弄它,弄清楚它的内部模式和工艺,弥和它的功能和我用代码编写出来的意图之间的差距。
在研究 API 的时候,我发现创建一个 VLAN 记录需要一个父对象引用 vlan_view_ref
。这看起来像一个路径片段,里面有随机字符。也许它是一个哈希,也许它真的是随机的,我不确定。我猜想,许多在泥泞中挣扎的人,在迫在眉睫的截止日期前,可能会把这个任意的字符串复制粘贴到 Ansible 中,然后继续混下去。忽略这个角色的实现细节,显而易见这个剧本级的任务应该是这样:
- name: "Create VLAN"
include_role:
name: otherthing
tasks_from: vlan_create.yml
vars:
vlan_name: "lab-infra"
vlan_tag: 100
vlan_view_ref: "vlan_view/747f602d-0381"
不幸的是,除了通过 API,vlan_view_ref
标识符是不可用的,所以即使把它移到清单文件或额外的变量中也没有什么帮助。剧本的用户需要对系统有一些更深入的理解,才能找出正确的引用 ID。
在实验室建设的情况下,我会经常重新部署这个记录系统。因此,这个父对象引用 ID 每天都会发生变化,我不希望每次都要手动找出它。所以,我肯定要按名称搜索该引用。没问题:
- name: Get Lab vlan view reference
include_role:
name: otherthing
tasks_from: search_for.yml
vars:
_resource: vlan_view
_query: "name={{ vlan_parent_view_name }}"
最终,它进行了一个 REST 调用。这将“返回” 一个 JSON,按照惯例,为了便于在角色外访问,我把它填充进了 _otherthing_search_result
中,。search_for.yml
的实现是抽象的,它总是返回一个包含零或多个结果的字典。
正如我读过的几乎所有真实世界的 Ansible 代码所证明的那样,大多数 Ansible 开发者将会继续前进,好像一切都很好,并且可以直接访问预期的单个结果:
- name: Remember our default vlan view ref
set_fact:
_thatthig_vlan_view_ref: "{{ _otherthing_search_result[0]._ref }}"
- name: "Create VLAN"
include_role:
name: otherthing
tasks_from: vlan_create.yml
vars:
vlan_name: "lab-infra"
vlan_tag: 100
vlan_view_ref: "{{ vlan_parent_view_name }}"
但有时 _otherthing_search_result[0]
是未定义的,所以 _thatthig_vlan_view_ref
也将是未定义的。很有可能是因为代码运行在不同的真实环境中,而有人忘记了在清单中或在命令行中更新 {{ vlan_parent_view_name }}
。或者,无论公平与否,也许有人进入了工具的图形用户界面(GUI),删除了记录或更改了它的名称什么的。
我知道你在想什么。
“好吧,不要这样做。这是一个没有哑巴的场所。不要那么笨。”。
也许我对这种情况还算满意,反驳道:“Ansible 会很正确的告诉你错误是:list 对象没有元素 0
,甚至会带个行号。你还想怎样?”作为开发者,我当然知道这句话的意思 —— 我刚写的代码。我刚从三天的和 API 斗智斗勇中走出来,我的脑子很清醒。
明天是另一个故事
但是到了明天,我可能会忘记什么是父对象引用,我肯定会忘记第 30 行上的内容。如果一个月后出了问题,就算你能找到我,我也得花一个下午的时间重新解读 API 指南,才能搞清楚到底出了什么问题。
而如果我出门了呢?如果我把代码交给了一个运维团队,也许是一个实习生通过 Tower 来运行,把 vlan_view_name
手动输入到表单之类的东西呢?那第 30 行出的问题是对他们没有帮助的。
你说,加注释吧! 嗯,是的。我可以在代码中写一些梗概,以帮助下周或下个月的开发人员。这对运行代码的人没有帮助,他的“工作”刚刚失败,当然对于企业也无济于事。
记住,我们此刻无所不能。在写代码或者跳过写代码的时候,我们是站在实力和知识的立场上进行的。我们花了几个小时,甚至几天的时间,研究了文档、现实、其他 bug、其他问题,我们留下了代码、注释,甚至可能还有文档。我们写的代码是分享成功的,而成功正是我们用户想要的。但是在这种学习中也有很多失败的地方,我们也可以留下这些。
在代码中留言
“第 30 行有错误”对任何人都没有帮助。至少,我可以用更好的错误信息来处理明显的错误情况:
- name: Fail if zero vlan views returned
fail:
msg: "Got 0 results from searching for VLAN view {{ vlan_parent_view_name }}. Please verify exists in otherthing, and is accessible by the service account."
when: _otherthing_search_result | length == 0
在这四行代码中(没有额外的思考),我把具体的、有用的建议留给了下一个人 —— 那个无助的运维团队成员,或者更有可能是一个月后的我 —— 这是关于现实世界中的问题,其实根本不是关于代码的。这条消息可以让任何人发现一个简单的复制/粘贴错误,或者记录系统发生了变化。不需要 Ansible 知识,不需要凌晨 3 点给开发人员发短信“看看第 30 行”。
但是等等!还有更多!
在了解 otherthing
的过程中,我了解到它在一个关键的方面,嗯,还挺笨的。它的许多记录类型(如果不是全部的话)没有唯一性约束,可能存在几个相同的记录。VLAN 视图被定义为有一个名称、一个开始 ID 和一个结束 ID;其他记录类型也同样简单,显然这应该是一个唯一的元组 —— 基于现实和数据库规范化的抽象概念。但 otherthing
允许重复的元组,尽管在概念上讲永远不可能。
在我的实验室里,我很乐意尝试并记住不要这样做。在企业生产环境中,我可能会写一个策略。不管是哪种方式,经验告诉我,系统会被破坏,会在倒霉的时候被破坏,而且可能需要很长时间才能让这些问题发酵成,嗯,一个问题。
对于 “第 30 行有错误”,一个本来有丰富经验的 Ansible 开发者可能会认识到这是“记录没有找到”,而不用知道其他的事情就足以解决这个问题。但如果 _otherthing_search_result[0]
只有有时是正确的 vlan_view_ref
,那就糟糕多了,它让整个世界被破坏,而悄无声息。而这个错误可能完全表现在其他地方,也许六个月后的安全审计会将其标记为记录保存不一致,如果有多种工具和人工访问方式,可能需要几天或几周的时间才能发现这个特定代码出错的事实。
在几天对 API 的摸索中,我学到了这一点。我不是在找问题,如果有记录,我没有看到。所以我来到了这篇文章的重点。我没有因为它是一个实验室,修复它,然后继续前进而忽略了这种不可能的情况,而是花了两分钟留下了_代码_ —— 不是注释,不是心理笔记,不是文档 —— 而是会一直运行的代码,涵盖了这种不可能的情况:
- name: Fail if > 1 views returned
fail:
msg: "Got {{ _otherthing_search_result | length }} results from searching for VLAN view {{ vlan_parent_view_name }}. Otherthing allows this, but is not handled by this code."
when: _otherthing_search_result | length > 1
我手动创建了失败条件,所以我可以手动测试这个条件。我希望它永远不会在实际使用中运行,但我觉得它会。
如果(当)这个错误发生在生产环境中,那么有人可以决定该怎么做。我希望他们能修复坏数据。如果它经常发生,我希望他们能追踪到另一个损坏的系统。如果他们要求删除这段代码,而这段代码做了未定义和错误的事情,那是他们的特权,也是我不想工作的地方。代码是不完美的,但它是完整的。这是匠人的工作。
现实世界中的自动化是一个迭代的过程,它与不完美的系统进行斗争,并平等地使用。它永远不会处理所有的特殊情况。它甚至可能无法处理所有的正常情况。通过 Lint、代码审查和验收测试的工作代码是处理安全和所需路径的代码。只要付出一点点努力,你就可以帮助下一个人,不仅仅是绘制安全路径,还可以对你发现的危险留下警告。