LabHub
学习 学习路径 课程

基础设施即代码

从命令式到声明式

在 LabHub 中继续学习

一句话总结

宣言式方法是在定义想要的最终状态后,工具计算与当前状态的差异进行更改的方式,命令式方法是按顺序描述要执行的步骤的方式。用一句句话来说,如果定义了基础设施的什么(What),如何(How)工具处理。

概念图: 状态文件 · 试图再次制作已经有的东西或删除别人的东西。 · 放在共享存储库中锁定。 · 秘密包含在其中。

为什么需要这个?

手动管理的问题可以总结为四个方面。无法重现相同的环境(无法重现),无法留下谁在什么时候改变了什么(无法跟踪变更),随着数量的增加,无法跟上速度,环境之间会一点一点地不同。特别是第三个是决定性的。服务器10台以内可以手动管理,但100台以上实际上是不可能的。

就这样,一点一点地用手摸索的服务器们,就像雪花一样,各自独立。Snowflake Server这个隐喻就由此而来。问题在于,当那个服务器死掉时。没有人能像样的重新制作它。因为设置不是代码,而是只存在于那个机器的磁盘上。

命令式脚本也不能完全解决这个问题。“安装软件包,复制设置文件,重新启动服务”的顺序在第一次运行时是正确的,但在第二次运行时会发生什么,只有阅读完脚本才能知道。声明型完全解决了这个问题。只写目标状态,工具会计算与当前状态的差异。

怎么行动

声明型工具总是分为三个部分:想要的状态(代码)、当前的状态(读取的实际基础设施)、以及两者之间的差异(plan)。差异用符号表示,标准符号如下。+ create是要重新制作的,- destroy要删除的东西,~ update只是在原地修改属性,-/+ replace删除后重新生成,<= read是只读的查询。在评论中最值得注意的符号是-/+都。我以为只是换一个属性,但资源会全部重生的情况在这里被揭示出来。

在自动化中,plan的结果以终止代码接收。plan -detailed-exitcode是0的话没有变更,1的话有错误,2的话意味着存在要适用的变更。有这三个值的话,可以构建类似于“如果有变更就进入批准阶段,没有变更就直接通过”这样的流程。将plan结果保存为文件,用该文件apply的话,即使plan时间点和apply时间点之间代码发生变化,也只会应用plan时间点的变更。在这里可以保证审查的内容和应用的内容是一样的。

谈论漂移时,3轴模型比较方便。代码是Desired,状态文件是Last Known,实际基础架构是Actual。三个都一样才是正常,如果两者中任何一个不一致,就是漂移。GitOps工具不断重复这种比较进行自我修复。如果有人直接用kubectl更改replicas,控制器会检测并恢复到Git定义的值。

在现场相遇的样子

新人最常犯的错误是急于在控制台上修改后没有反映在代码中。那一刻,代码就变成了谎言,下一个人apply会悄悄地恢复那个修改。相反,运转良好的团队会把plan输出贴在PR上进行评论。关键是,被人评论的对象不是代码,而是代码会产生的修改列表。

状态文件是真正沉重的资产。

在声明型工具中,最常发生错误的地方不是代码,而是状态文件。这是在前面的3轴模型中负责“Last Known”的那个。如果这个文件不存在或损坏,工具就不会识别现在有的资源是自己制作的,如果直接apply的话,就会试图再次制作已经有的东西或删除别人的东西。

所以有几个是基础的。

**放在共享存储库中锁定。**如果状态文件存在各自的笔记本电脑上,两个人同时apply时会覆盖对方的记录。放在远程存储库中锁定apply期间是标准做法,没有锁定的配置只有人少时才偶然安全。

**秘密包含在其中。**像数据库密码一样,在创建资源时提供的值在状态文件中以明文形式保留的情况很常见。因此,状态文件不会提交到代码存储库中,而是保存在加密存储库中,并与代码不同的管理访问权限。

**不用手修复。**只按照工具提供的命令移动和删除。用文本打开编辑的话,格式是正确的,但内部引用不一致,在几次apply后会发生莫名其妙的重复性。

而且划分范围越大,越重要。如果将整个基础设施管理成一个状态,plan需要几分钟,而一个小小的变更就会使整个系统陷入停滞。原则是将寿命和负责不同的事情分开。将几乎没有变化的网络、偶尔变化的群集、每天变化的应用程序都放在一个整体中,就会因为每天变化的东西而几乎暴露在每次变更的危险之中。

下次实习要做的事情

这个平台没有terraform二进制文件。所以用YAML声明想要的状态,把实际的容器列表读取成相同格式的JSON,然后比较两者。+ create/- destroy/~ update直接制作输出计划。然后用apply汇总,在外面删除容器,确认plan是否抓住它,产生漂移。知道工具在屏幕上散布的符号是如何制作的,然后别人的计划输出也会看起来不一样。