第15章
配置细节
戴夫·坎宁安(Dave Cunningham)和米莎·布鲁克曼(Misha Brukman)与克里斯托弗·卡尔特(Christophe Kalt)和贝茜·拜尔(Betsy Beyer)
管理生产系统是SRE为组织提供价值的多种方式之一。在生产环境中配置和运行应用程序的任务需要深入了解这些系统如何组合在一起以及它们如何工作。当出现问题时,值班工程师需要准确知道配置在哪里以及如何更改它们。如果团队或组织没有投资于解决与配置相关的工作,那么这种责任可能成为负担。
本书详细介绍了琐事的话题(见第6章)。如果您的SRE团队负担了许多与配置相关的工作,那么我们希望实施本章中介绍的一些想法将有助于您节省一些时间来进行配置更改。
配置引起的琐事
在项目生命周期的开始,配置通常是相对轻量和简单的。您可能有一些纯数据格式的文件,例如INI,JSON,YAML,或XML。管理这些文件几乎不需要费力。随着应用程序,服务器和变体的数量随时间增加,配置可能变得非常复杂和冗长。例如,您最初可能通过编辑一个配置文件来”更改设置”,但是现在您必须在多个位置更新配置文件。读取这样的配置也很困难,因为重要的差异隐藏在无关紧要的重复细节中。我们可以将与配置相关的工作描述为重复琐事:这是管理在系统中复制的配置的日常任务。这种工作不仅仅限于大型组织和大型系统,在具有许多独立配置组件的微服务架构中尤其常见。
工程师通常通过构建自动化或配置框架来应对复制工作。它们旨在消除配置系统中的重复项,并使配置更易于理解和维护。这种方法重用了软件工程中的技术,通常使用”配置语言”。Google SRE创建了许多配置语言,旨在减少我们最大,最复杂的生产系统的工作量。
不幸的是,这种策略并不一定消除配置方面的麻烦。从大量的个人配置中解放出来,该项目(及其配置语料库)随着新的活力而增长。不可避免地,您遇到了”复杂的琐事”:应对复杂的自动化的新出现的,有时是不良的行为的挑战性和令人沮丧的任务。这种琐事通常会在大型组织(超过10位工程师)中实现,并且会随着增长而增加。您越早解决复杂性问题就越好;配置的大小和复杂性只会随着时间的推移而增长。
减少配置引起的工作量
如果您的项目充斥着与配置相关的工作,那么您有几种改善情况的基本策略。
在极少数情况下,并且如果您的应用程序是定制构建的,则您可能会选择完全删除配置。在处理某些方面的配置时,该应用程序自然可以比配置语言更好:因为它可以访问有关机器的信息,或者可以动态地更改一些值,因为它可以根据负载进行扩展,因此可以为应用程序分配默认值。
如果删除配置不是一种选择,并且重复琐事成为问题,请考虑自动化以减少配置语料库中的重复。您可能集成了新的配置语言,或者可能需要改进或替换现有的配置设置。1下一部分,”配置系统的关键属性和陷阱”,第317页,提供了有关选择或设计该配置文件的一些指导。系统。
如果您要设置新的配置框架,则需要将配置语言与需要配置的应用程序集成在一起。”集成现有应用程序:第322页的”Kubernetes”将Kubernetes用作要集成的现有应用程序的示例,第326页的”集成自定义应用程序(内部软件)”提供了一些更一般的建议。这些部分介绍了一些使用Jsonnet的示例(出于示例目的,我们选择Jsonnet作为代表配置语言)。
一旦有了一个配置系统来帮助您进行复制工作-无论您是否已经致力于现有的解决方案,还是选择实现一种新的配置语言-都是”有效地操作配置系统”中的最佳做法第329页的”何时评估配置”和第333页的”防止滥用配置”应该有助于优化设置,无论使用哪种语言。采用这些流程和工具可以帮助最大程度地减少复杂性。
配置系统的关键属性和陷阱
第14章概述了任何配置系统的一些关键属性。除了轻量级,易学性,简单性和表达能力等通用理想要求之外,高效的配置系统还必须:
-
通过用于管理配置文件(工具,调试器,格式化程序,IDE集成等)的工具支持配置运行状况,工程师的信心和生产率。
-
提供密封评估配置,以实现回滚和一般重播性。
-
单独的配置和数据,可以轻松分析配置和一系列配置接口。
人们尚未普遍了解这些属性至关重要,因此达成我们目前的理解确实是一个旅程。在此过程中,Google发明了几种缺少这些关键属性的配置系统。我们也不是孤独的。尽管流行的配置系统种类繁多,但很难找到一个不会犯以下至少一个陷阱的系统。
陷阱1: 无法将配置识别为编程语言问题
如果您不是故意设计一种语言,那么最终获得的”语言”就不太可能是一种好语言。
尽管配置语言描述数据而不是行为,但它们仍然具有编程语言的其他特征。如果我们的配置策略以仅使用数据格式为目标开始,则编程语言功能往往会在后门悄悄蔓延。该格式不是保留”仅数据”语言,而是成为一种深奥而复杂的”编程”语言。
例如,某些系统将count属性添加到要配置的虚拟机(VM)的架构中。此属性不是VM本身的属性,而是表示您想要多个。虽然有用,但这是
配置系统的关键属性和陷阱
编程语言,不是数据格式,因为它需要外部评估器或解释器。传统的编程语言方法将使用工件外部的逻辑(例如for循环或列表理解)来根据需要生成更多的VM。
另一个示例是一种配置语言,它使用字符串插值规则而不支持通用表达式。尽管这些字符串实际上可以包含复杂的代码,包括数据结构操作,校验和,base64编码等,但它们似乎”仅仅是数据”。
流行的YAML + Jinja解决方案也有缺点。XML,JSON,YAML和文本格式的协议缓冲区等简单的纯数据格式是纯数据用例的绝佳选择。同样,文本模板引擎(例如Jinja2或Go模板)也非常适合HTML模板。但是,当将它们与配置语言结合使用时,对于人工和工具而言,它们都变得难以维护和分析。在所有这些情况下,这种陷阱使我们陷入了不适合工具使用的复杂而深奥的”语言”。
陷阱2: 设计偶发或临时语言功能
当大规模使用操作系统时,SRE通常会感到配置可用性问题。一种新语言将没有良好的工具支持(IDE支持,良好的linters),并且如果该语言具有未记录的或深奥的语义,则开发自定义工具将很痛苦。
随着时间的推移将临时编程语言功能添加到简单的配置格式可能会创建功能完整的解决方案,但是临时语言比其正式设计的等效语言更复杂并且通常具有较低的表达能力。他们还冒着发展陷阱和特质的风险,因为他们的作者无法提前考虑要素之间的相互作用。
与其希望您的配置系统不会变得复杂到需要简单的编程构造,不如在初始设计阶段考虑这些要求。
陷阱3: 构建过多的特定领域优化
用于特定于域的新解决方案的用户群越小,您需要等待更长的时间来积累足够的用户来证明构建工具的合理性。工程师不愿意花时间正确理解该语言,因为它在该领域之外几乎没有适用性。Stack Overflow之类的学习资源不太可能获得。
陷阱4: 将”配置评估”与”副作用”交织在一起
副作用包括在配置运行期间更改外部系统或咨询带外数据源(DNS,VM ID,最新内部版本)。
允许这些副作用的系统违反了密闭性,并且还防止了配置与数据的分离在极端情况下,如果不花时间通过保留云资源来调试配置是不可能的。为了允许配置和数据分离,请首先评估配置,然后将结果数据提供给用户进行分析,然后再考虑副作用。
陷阱5: 使用现有的通用脚本语言(例如Python,Ruby或Lua)
这似乎是避免前四个陷阱的简单方法,但是使用通用脚本语言的实现是重量级的,并且/或者需要使用侵入式沙箱来确保密封性。由于通用语言可以访问本地系统,因此出于安全考虑,也可能需要沙盒。
此外,我们不能假设维护配置的人员会熟悉所有这些语言。
避免这些陷阱的愿望导致了可配置的可重用领域特定语言(DSL)的开发,例如HOCON,Flabbergast、Dhall和Jsonnet。我们建议使用现有DSL进行配置。即使DSL似乎无法满足您的需求,您有时还是需要其他功能,并且您可以始终使用内部样式指南来限制该语言的功能。
Jsonnet快速入门
Jsonnet是一种封闭的开源DSL,可用作库或命令行工具以为任何应用程序提供配置。它在Google内部和外部均得到广泛使用。2
该语言旨在使程序员熟悉:它使用类似Python的语法,面向对象和功能构造。它是JSON的扩展,意味着JSON文件只是一个输出自身的Jsonnet程序。与JSON相比,Jsonnet在引号和逗号中更宽容,并且支持注释。更重要的是,它增加了计算结构。
尽管您无需特别熟悉Jsonnet语法即可遵循本章的其余部分,但是花一些时间阅读在线教程可以帮助您进行定向。
Google或我们的读者群中没有主流的配置语言,但是我们需要选择某种语言这样允许我们提供例子。本章使用Jsonnet展示了我们在第14章中提供的建议的实际示例。
如果您尚未使用特定的配置语言,并且想使用Jsonnet,则可以直接应用本章中的示例。在所有情况下,我们已尽力使您尽可能轻松地从代码示例中提取基础课程。
另外,一些示例探讨了您可能希望在编程书中找到的概念(例如图灵完整性)。我们非常小心地将深度降至要求的深度,以解释实际上已经在生产中咬住我们的微妙之处。在大多数复杂的系统中(当然还有配置方面),故障都在边缘。
集成配置语言
本节使用Jsonnet讨论如何将配置语言与需要配置的应用程序集成在一起,但是相同的技术也可以转移到其他配置语言。
以特定格式生成配置
配置语言可能以正确的格式本地输出。例如,Jsonnet输出与许多应用程序兼容的JSON。对于使用JSON,JavaScript,YAML或HashiCorp的配置语言之类的扩展JSON的消费者,JSON也足够了。如果是这种情况,则无需执行任何进一步的集成工作。
对于本机不支持的其他配置格式:
-
您需要找到一种在配置语言中表示配置数据的方法。通常,这并不难,因为配置值(如映射,列表,字符串和其他原始值)是通用的,并且在所有语言中均可用。
-
一旦这些数据以配置语言表示,您就可以使用该语言的构造来减少重复(从而减少琐事)。
-
您需要为必要的输出格式编写(或重用)序列化函数。例如,Jsonnet标准库具有用于从其内部类似JSON的表示中输出INI和XML的功能。如果配置数据拒绝使用配置语言(例如Bash脚本)表示,则可以使用基本的字符串模板技术作为最后的手段。您可以在http://bit.ly/2La0zDe上找到实际示例。
驱动多个应用程序
一旦可以使用配置语言驱动任意现有应用程序,您就可以从同一配置中定位多个应用程序。如果您的应用程序使用不同的配置格式,则需要执行一些转换工作。一旦能够以必要的格式生成配置,就可以轻松地统一,同步并消除整个配置语料库中的重复。考虑到JSON和基于JSON的格式的普遍性,您甚至不必生成其他格式-例如,如果您使用使用GCP Deployment Manager,AWS Cloud Formation或Terraform用于基础架构,以及用于容器的Kubernetes。此时,您可以:
-
仅定义一次端口的Jsonnet计算中输出Nginx Web服务器配置和Terraform防火墙配置。
-
从同一文件配置监控仪表板,保留策略和警报通知管道。
-
通过将初始化命令从一个列表移动到另一个列表,来管理VM启动脚本和磁盘映像构建脚本之间的性能折衷。
将完全不同的配置集中到一处之后,您就有很多机会可以优化和抽象配置。配置甚至可以嵌套-例如,Cassandra配置可以嵌入其基础基础架构的Deployment Manager配置中或Kubernetes ConfigMap中。好的配置语言可以处理任何尴尬的字符串引用,并且通常使此操作自然而简单。
为了使为各种应用程序编写许多不同的文件变得容易,Jsonnet提供了一种模式,该模式期望配置执行产生一个JSON对象,该JSON对象将文件名映射到文件内容(根据需要设置格式)。您可以使用其他配置语言来模拟此功能,方法是在字符串之间发出映射,并使用后处理步骤或包装脚本编写文件。
集成配置语言
集成现有应用程序:Kubernetes
Kubernetes进行一个有趣的案例研究有两个原因:
-
需要配置在Kubernetes上运行的作业,其配置可能会变得复杂。
-
Kubernetes没有附带捆绑的配置语言(谢天谢地,甚至还没有一种特定的配置语言)。
具有最小复杂对象的Kubernetes用户只需使用YAML。具有较大基础架构的用户可以使用Jsonnet之类的语言扩展其Kubernetes工作流,以提供该规模所需的抽象工具。
Kubernetes提供什么
Kubernetes是一个开源系统,用于在机器集群上协调容器化工作负载。它的API使您可以管理容器本身以及许多重要的细节,例如容器之间的通信,集群内外的通信,负载平衡,存储,渐进式部署和自动扩展。每个配置项都由一个JSON对象表示,该对象可以通过API端点进行管理。命令行工具kubectl允许您从磁盘读取这些对象并将其发送到API。
在磁盘上,JSON对象实际上被编码为YAML流。3 YAML易于阅读,并可以通过通用库轻松转换为JSON。开箱即用的用户体验包括编写代表Kubernetes对象的YAML文件,并运行kubectl将它们部署到集群。
要了解配置Kubernetes的最佳实践,请参阅关于该主题的Kubernetes文档。
示例Kubernetes配置
YAML是Kubernetes配置的用户界面,提供了一些简单的功能(例如注释),并且具有简洁的语法,大多数人都喜欢原始JSON。但是,YAML在抽象方面不够完善:它仅提供锚点,4在实践中很少使用,Kubernetes不支持。
假设您要使用不同的名称空间,标签和其他较小的变体来复制Kubernetes对象四次。遵循不变基础架构的最佳实践,您将存储所有四个变体的配置,从而复制配置的其他相同方面。以下代码段提供了一种变体(为简洁起见,我们省略了其他三个文件):
# example1.yaml
apiVersion: v1
kind: Service
metadata:
labels:
app: guestbook
tier: frontend
name: frontend
namespace: prod
spec:
externalTrafficPolicy: Cluster ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: guestbook
tier: frontend
sessionAffinity: None
type: NodePort
这些变体很难阅读和维护,因为重要的差异被掩盖了。
集成配置语言
如第315页的”配置导致的负担”中所述,管理大量的YAML文件可能会花费大量时间。配置语言可以帮助简化此任务。最直接的方法是,在每次执行Jsonnet时都发出一个Kubernetes对象,然后将生成的JSON直接传递到kubectl中,后者像处理YAML一样处理JSON。或者,您可以发出YAML流(此类对象的序列5)或单个kubectl列表对象,或者让Jsonnet从同一配置中发出多个文件。有关更多讨论,请参见Jsonnet网站。
开发人员应注意,通常,YAML允许您编写在JSON中无法表达的配置(因此,Jsonnet无法生成)。YAML配置可以包含异常的IEEE浮点值(如NaN)或具有非字符串字段的对象(如数组,其他对象或null)。实际上,这些功能很少使用,而且Kubernetes不允许使用这些功能,因为在将配置发送到API时必须对其进行JSON编码。
以下代码片段显示了我们的示例Kubernetes配置在Jsonnet中的显示效果:
// templates.libsonnet
{
MyTemplate:: {
local service = self,
tier:: error 'Needs tier',
apiVersion: 'v1',
kind: 'Service',
local selector_labels = { app: 'guestbook', tier: service.tier },
metadata: {
labels: selector_labels,
name: 'guestbook-' + service.tier, namespace: 'default',
},
spec: {
externalTrafficPolicy: 'Cluster',
ports: [{
port: 80,
protocol: 'TCP',
targetPort: 80,
}],
selector: selector_labels, sessionAffinity: 'None', type: 'NodePort',
}, },
}
// example1.jsonnet
local templates = import 'templates.libsonnet';
templates.MyTemplate { tier: 'frontend',
}
// example2.jsonnet
local templates = import 'templates.libsonnet';
templates.MyTemplate { tier: 'backend', metadata+: {
namespace: 'prod',
},
}
// example3.jsonnet
local templates = import 'templates.libsonnet';
templates.MyTemplate { tier: 'frontend', metadata+: {
namespace: 'prod',
labels+: { foo: 'bar' },
},
}
// example4.jsonnet
local templates = import 'templates.libsonnet';
templates.MyTemplate { tier: 'backend',
}
请注意以下几点:
-
我们通过实例化抽象模板四次来表示所有四个变体,但是您也可以使用功能抽象。
-
虽然我们为每个实例使用单独的Jsonnet文件,但您也可以将它们合并到一个文件中。
-
在抽象模板中,名称空间默认为默认,并且层必须被覆盖。
-
乍一看,Jsonnet稍微冗长一些,但是随着模板实例化数量的增加而减少了工作量。
在MyTemplate中,local关键字定义了一个变量服务,该服务被初始化为self(对最近的封闭对象的引用)。这样,您就可以从嵌套对象(重新定义self)中引用该对象。
tier字段具有两个冒号(而不是常规JSON单个冒号),并且在生成的JSON中隐藏(不输出)。否则,Kubernetes将拒绝tier作为无法识别的字段。隐藏的字段仍然可以被覆盖和引用-在这种情况下,作为service.tier。
该模板本身不能使用,因为引用service.tier会触发错误构造,从而使用给定的文本引发运行时错误。为避免该错误,模板的每个实例都使用其他一些表达式覆盖tier字段。换句话说,此模式表示类似于纯虚拟/抽象方法的内容。
使用函数进行抽象意味着只能对config进行参数化。相反,模板允许您覆盖父项中的任何字段。如中所述
集成现有应用程序:Kubernetes
第14章,虽然简单性是设计的基础,但逃避简单性的能力很重要。模板替代提供了一个有用的转义填充,用于更改通常被认为太低级的特定细节。例如:
templates.MyTemplate { tier: 'frontend', spec+: {
sessionAffinity: 'ClientIP',
},
}
这是将现有模板转换为Jsonnet的典型工作流程:
-
将YAML变体之一转换为JSON。
-
通过Jsonnet格式化程序运行结果JSON。
-
手动添加Jsonnet构造以抽象和实例化代码(如示例中所示)。
该示例说明了如何在保留某些不同字段的同时删除重复项。随着差异变得更加微妙(例如,字符串略有不同)或难以表达(例如,配置具有结构上的差异,如数组中的其他元素,或应用于数组中所有元素的相同差异),使用配置语言将变得更具吸引力。
通常,跨不同配置抽象公共性可促进关注点分离,并具有与编程语言中的模块化相同的好处。您可以在许多不同的用例中利用抽象功能:
-
一个团队可能需要创建几乎相同(但不是完全相同)的多个版本的配置-例如,在跨各种环境(产品/阶段/开发/测试)管理部署时,在不同的架构,或在不同地区调整容量。
-
组织可能拥有一个基础架构团队,该团队维护着由应用程序团队使用的可重用组件-API服务框架,缓存服务器或MapReduces。对于每个组件,基础架构团队都可以维护一个模板,该模板定义大规模运行该组件所需的Kubernetes对象。每个应用程序团队都可以实例化该模板以添加其应用程序的详细信息。
集成自定义应用程序(内部软件)
如果您的基础架构使用了任何自定义应用程序(即,内部开发的软件,而不是现成的解决方案),则可以设计这些应用程序与可重用的配置语言共存。当您编写配置文件或与生成的配置数据进行交互时(例如,出于调试目的或与其他工具集成时),本节中的建议应改善总体用户配置体验。他们还应该简化应用程序的设计,并将配置与数据分开。
处理自定义应用程序的广泛策略应该是:
-
让配置语言处理其被设计用来处理的部分:问题的语言方面。
-
让您的应用程序处理所有其他功能。
以下最佳实践包括使用Jsonnet的示例,但相同的建议也适用于其他语言:
-
使用一个纯数据文件,然后让配置语言使用导入将配置拆分为文件。这意味着配置语言实现仅需发出(并且应用程序仅需使用)单个文件。而且,由于应用程序可以以不同的方式组合文件,因此该策略明确明确地描述了如何组合文件以形成应用程序配置。
-
使用对象表示命名实体的集合,其中字段包含对象名称,值包含实体的其余部分。避免使用每个元素都有名称字段的对象数组。
错误的JSON:
[ { "name": "cat", ... }, { "name": "dog", ... } ]
良好的JSON:
{ "cat": { ... }, "dog": { ... } }
此策略使集合(和单个动物)更易于扩展,您可以按名称引用实体(例如,Animals.cat),而不是引用脆弱的索引(例如,Animals[0])。
-
避免在顶层按类型对实体进行分组。结构化JSON,以便将与逻辑相关的配置分组在同一子树中。这允许抽象(在配置语言级别)遵循功能边界。
错误的JSON:
{ "pots": { "pot1": { ... }, "pot2": { ... } }, "lids": { "lid1": { ... }, "lid2": { ... } } }
良好的JSON:
{ "pot_assembly1": { "pot": { ... }, "lid": { ... } }, "pot_assembly2": { "pot": { ... }, "lid": { ... } } }
在配置语言级别,此策略启用如下的抽象:
local kitchen = import 'kitchen.libsonnet'; { pot_assembly1: kitchen.CrockPot, pot_assembly2: kitchen.SaucePan { pot+: { color: 'red' } }, }
-
通常使数据表示设计简单:
—避免在数据表示形式中嵌入语言功能(如”陷阱1:未能将配置识别为编程语言问题”(第317页)。这些类型的抽象将功能不足,只会造成混乱,因为它们迫使用户决定是在数据表示形式还是在配置语言中使用抽象功能。
—不用担心过于冗长的数据表示。减少冗长的解决方案会带来复杂性,并且可以使用配置语言来解决问题。
—避免在您的应用程序中解释自定义字符串插值语法,例如条件或字符串中的占位符引用。有时解释是不可避免的-例如,当您需要描述在生成配置的纯数据版本(警报,处理程序等)之后执行的操作时。但是否则,请让配置语言尽可能多地执行语言级别的工作。
如前所述,如果您可以完全删除配置,那么这样做始终是您的最佳选择。尽管配置语言可以通过使用具有默认值的模板来隐藏基础模型的复杂性,但是生成的配置数据并未完全隐藏-可以由工具进行处理,由人工检查或加载到配置数据库中。出于同样的原因,不要依赖配置语言来解决基础模型中不一致的命名,复数或错误-而是将它们修复在模型本身中。如果您无法解决模型中的不一致问题,那么最好在语言级别上使用它们,以避免更多的不一致之处。
根据我们的经验,随着时间的推移,配置更改往往会主导系统的中断根本原因(请参阅附录C中的主要中断原因列表)。验证配置更改是保持可靠性的关键步骤。我们建议配置执行后立即验证生成的配置数据。仅语法验证(即检查JSON是否可解析)不会发现很多错误。通用模式验证后,检查特定于应用程序域的属性-例如,是否存在必填字段,是否存在引用的文件名以及所提供的值在允许的范围内。
您可以使用JSONschema验证Jsonnet的JSON。对于使用protocol buffers的应用程序,您可以轻松地从Jsonnet生成这些缓冲区的规范JSON形式,并且协议缓冲区实现将在反序列化期间进行验证。
不管您决定如何验证,都不要忽略无法识别的字段名称,因为它们可能表示在配置语言级别上有错字。Jsonnet可以屏蔽不应使用::语法输出的字段。在precommit hook中执行相同的验证也是一个好主意。
有效地操作配置系统
在以任何语言实现”代码配置”时,我们建议您遵循有助于整体软件工程的规则和流程。
版本化
配置语言通常会触发工程师编写模板和实用程序功能的库。通常,一个团队维护这些库,但是许多其他团队可能会使用它们。当需要对库进行重大更改时,有两种选择:
-
提交所有客户端代码的全局更新,以重构代码以使其仍然有效(这在组织上可能不可行)。
-
对库进行版本控制,以便不同的使用者可以使用不同的版本并独立迁移。选择使用不推荐使用的版本的用户将无法获得新版本的好处,并且会招致技术性债务-某天,他们将不得不重构代码以使用新的库。
包括Jsonnet在内的大多数语言都没有提供对版本控制的任何特定支持。相反,您可以轻松使用目录。有关Jsonnet中的实际示例,请参见ksonnet-lib存储库,其中版本是导入路径的第一个组件:
local k = import 'ksonnet.beta.2/k.libsonnet';
源代码控制
第14章主张保留配置更改的历史记录(包括进行更改的人),并确保回滚容易且可靠。将配置检入到源代码管理中将带来所有这些功能,以及通过代码查看配置更改的功能。
工具
考虑如何实施样式和配置,并调查是否存在将这些工具集成到工作流程中的编辑器插件。您的目标是在所有作者之间保持一致的风格,提高可读性并检测错误。一些编辑器支持post-write hook,这些hook可以为您运行格式化程序和其他外部工具。您也可以使用precommit hook来运行相同的工具,以确保签入的配置是高质量的。
测试
我们建议对上游模板库实施单元测试。确保以各种方式实例化这些库时,它们会生成预期的具体配置。同样,功能库应包括单元测试,以便可以放心地对其进行维护。
在Jsonnet中,您可以将测试编写为Jsonnet文件,这些文件包括:
-
导入要测试的库。
-
练习这个库。
-
使用assert语句或标准库assertEqual函数来验证其输出。后者会在其错误消息中显示任何不匹配的值。
以下示例测试joinName函数和MyTemplate:
// utils_test.jsonnet
local utils = import 'utils.libsonnet';
std.assertEqual(utils.joinName(['foo', 'bar']), 'foo-bar') && std.assertEqual(utils.MyTemplate { tier: 'frontend' }, { ... })
对于较大的测试套件,您可以利用由Jsonnet社区成员开发的更全面的单元测试框架。您可以使用此框架以结构化的方式定义和运行测试套件-例如,报告所有失败测试的集合,而不是在第一个失败的断言时中止执行。
何时评估配置
我们的关键特性包括密封性;也就是说,无论配置语言在何处或何时执行,都必须生成相同的配置数据。如第14章所述,如果系统依赖于可以在其封闭环境之外更改的资源,则可能难以回滚或无法回滚。通常,封闭性意味着Jsonnet代码始终可以与其表示的扩展JSON互换。因此,您可以在更新Jsonnet到需要JSON的任何时间(甚至每次都需要JSON时)从Jsonnet生成JSON。
我们建议将配置存储在版本控制中。然后,您最早的来验证配置的机会是在签入之前。另一方面,应用程序可以在需要JSON数据时评估配置。作为中间选项,您可以在构建时进行评估。这些选项中的每一个都有各种折衷,您应该根据用例的具体情况进行优化。
很早期:签入JSON
您可以从Jsonnet代码生成JSON,然后再将其签入版本控制。典型的工作流程如下:
-
修改Jsonnet文件。
-
运行Jsonnet命令行工具(可能包装在脚本中)以重新生成JSON文件。
-
使用precommit hook来确保Jsonnet代码和JSON输出始终保持一致。
-
将所有内容打包到请求请求中以进行代码审查。
优点
-
审阅者可以全面检查具体更改-例如,重构完全不影响生成的JSON。
-
您可以在生成和抽象级别检查多个作者在不同版本上的行注释。这对于审核更改很有用。
-
您不需要在运行时运行Jsonnet,这可以帮助限制复杂性,二进制文件大小和/或风险暴露。
缺点
-
生成的JSON不一定是可读的-例如,如果它嵌入了长字符串。
-
JSON可能由于其他原因而不适用于版本控制-例如,如果它太大或包含秘密。
-
如果用于分隔Jsonnet文件的许多并发编辑收敛到单个JSON文件,则可能会发生合并冲突。
中期:在构建时评估
您可以通过在构建时运行Jsonnet命令行实用程序并将生成的JSON嵌入到发布工件中(例如,作为tarball)来避免将JSON检入到源代码控制中。应用程序代码仅在初始化时从磁盘读取JSON文件。如果您使用的是Bazel,则可以使用[Jsonnet Bazel规则]轻松实现这一目标。(http://bit.ly/2xz0QxH)在Google上,由于下面列出的优点,我们通常偏爱这种方法。
优点
-
您可以控制运行时复杂性,二进制文件大小和风险敞口,而不必在每个拉取请求中重建JSON文件。
-
在原始Jsonnet代码和生成的JSON之间不存在失去同步的风险。
缺点
-
构建更加复杂。
-
在代码检查期间很难评估具体更改。
后期:在运行时评估
链接Jsonnet库可让应用程序本身随时解释配置,从而生成一个生成的JSON配置的内存表示形式。
优点
-
更简单,因为您不需要事先评估。
-
您可以在执行期间评估用户提供的Jsonnet代码。
缺点
-
任何链接的库都会增加占用空间和风险。
-
可能在运行时发现配置错误,为时已晚。
-
如果Jsonnet代码不受信任,则必须格外小心。(我们在第333页的”防范滥用配置”中讨论了原因。)
遵循我们的运行示例,如果要生成Kubernetes对象,什么时候应该运行Jsonnet?
答案取决于您的实现。如果您要构建类似ksonnet(从本地文件系统运行Jsonnet代码的客户端命令行工具)之类的东西,最简单的解决方案是将Jsonnet库链接到该工具并评估过程中的Jsonnet。这样做是安全的,因为代码在作者自己的计算机上运行。
Box.com的基础结构使用Git hook将配置更改推送到生产环境。为了避免在服务器上执行Jsonnet,Git hook会对存储在存储库中的生成的JSON起作用。对于像Helm或Spinnaker这样的部署管理守护程序,您唯一的选择是在运行时评估服务器上的Jsonnet(下一节将描述警告)。
防止滥用配置
与长期运行的服务不同,配置执行应以生成的配置迅速终止。不幸的是,由于错误或故意攻击,配置可能会花费任意数量的CPU时间或内存。为了说明原因,请考虑以下非终止Jsonnet程序:
local f(x) = f(x + 1); f(0)
使用无限制内存的程序类似:
local f(x) = f(x + [1]); f([])
您可以使用对象而不是函数或其他配置语言来编写等效的示例。
您可能会通过限制语言来避免过度消耗资源,从而使其不再图灵完备。但是,强制所有配置终止并不一定可以防止过度消耗资源。编写消耗足够时间或内存以至于几乎无法终止的程序很容易。例如:
local f(x) = if x == 0 then [] else [f(x - 1), f(x - 1)]; f(100)
实际上,即使使用简单的配置格式(例如XML和YAML),此类程序也存在。
防止滥用配置
实际上,这些场景的风险取决于情况。在问题较少的方面,假设命令行工具使用Jsonnet构建Kubernetes对象,然后部署这些对象。在这种情况下,Jsonnet代码是受信任的:非终止的事故很少,您可以使用Ctrl-C缓解它们。偶然的内存耗尽是极不可能的。另一方面,对于像Helm或Spinnaker这样的服务,该服务从最终用户接受任意配置代码并在请求处理程序中对其进行评估,您必须非常小心,以避免可能会占用请求处理程序或耗尽内存的DOS攻击。
如果在请求处理程序中评估不受信任的Jsonnet代码,则可以通过对Jsonnet执行沙盒操作来避免此类攻击。一种简单的策略是使用单独的进程和ulimit(或它的非UNIX等效项)。通常,您需要派生到命令行可执行文件,而不是链接Jsonnet库。结果,未在给定资源内完成的程序将安全地失败并通知最终用户。为了进一步防御C++内存漏洞,可以使用Jsonnet的本地Go实现。
结论
无论您使用Jsonnet,采用其他配置语言还是自行开发,我们希望您可以应用这些最佳实践来自信地配置生产系统所需的复杂性和操作负载。
至少,配置语言的关键属性是良好的工具,密封配置以及配置和数据的分隔。
您的系统可能不够复杂,不需要配置语言。过渡到特定领域的语言(如Jsonnet)是一种在复杂性增加时要考虑的策略。这样做将使您能够提供一致且结构合理的界面,并腾出您的SRE团队用于其他重要项目的时间。