跳到主要内容
工程的博客

带有Jsonnet模板语言的声明性基础设施

分享这篇文章

本文是我们内部工程博客系列的一部分,涉及Databricks平台、基础设施管理、集成、工具、监控和供应。bob体育客户端下载

在数据工程,我们是狂热的粉丝Kubernetes.我们的大部分平台基础设施bob体育客户端下载都在Kubernetes中运行,无论是在AWS云还是更规范的环境

然而,我们发现Kubernetes本身并不足以管理复杂的服务基础设施,它可能包含在Kubernetes中创建的两个资源(例如,豆荚服务)和外部资源,例如我的角色.管理的复杂性来自于(1)对基础设施当前状态的可见性的需要(2)关于如何对基础设施进行更改的推理。

为了帮助降低管理的复杂性,基础设施最好是声明(即,由一组配置文件或模板描述)。第一个优点是可以通过读取配置文件轻松地检查基础结构的目标状态。其次,由于基础结构完全由文件描述,因此可以提出、审查并将更改作为标准软件开发工作流的一部分加以应用。

缺失的是什么?

KubernetesYAML配置文件已经在Kubernetes中实现了对对象的声明性更新。用户只需要编辑对象的YAML文件,然后运行$ kubectl应用-f object.yaml同步到Kubernetes。这些YAML文件可以被检入源代码控制,如果需要,用户可以查询Kubernetes来检查活动版本和本地文件之间的差异。

然而,当试图将这种方法应用于更大的生产环境时,人们会遇到痛点:

  1. 公共结构不能在YAML文件之间共享。你通常不是一次而是多次设置一个服务,可能是在{dev, staging, prod}环境中,跨越不同的地理区域,如{us-west, us-east, asia-pacific}和云提供商{AWS, Azure, GCE}。
  2. YAML文件必须经常引用有关外部定义的实体(如关系数据库、网络配置或SSL证书)的元数据。

  3. 复杂的多层服务部署可能需要组合许多不同的资源或YAML文件。更新由许多这样的文件组成的部署会变得很麻烦。

在这篇博文中,我们将描述如何使用谷歌Jsonnet配置语言为了解决这些问题,通过一个使用Jsonnet进行服务部署模板化的示例,并提出一个Jsonnet风格指南对于基础架构模板用例。我们发现Jsonnet很容易上手,并且可以很好地扩展到复杂的用例。

Jsonnet在Databricks的使用

2015年底,我们开始尝试Jsonnet作为工作的一部分数据库社区版,我们的免费Apache Spark教育服务。从那时起,Jsonnet在Databricks工程中迅速流行起来,超过40000行Jsonnet在1000多个不同的文件中签入了我们的主要开发存储库。一旦实体化,这些模板将扩展为数十万行原始YAML。

Databricks的团队目前使用Jsonnet来管理Kubernetes资源的配置(包括运行在Kubernetes上的服务的内部配置),AWS CloudFormation起程拓殖砖的工作,也用于任务,如TLS证书管理和定义普罗米修斯警报.在过去的两年里,Jsonnet已经发展成为工程领域事实上的标准配置语言。

Jsonnet基础知识

Jsonnet是否有一种配置语言可以帮助您定义JSON数据。其基本思想是,一些JSON字段可以保留为变量或表达式,在编译时进行计算。例如,JSON对象{“计数”:4}可以表示为{计数:2 + 2}在Jsonnet。你也可以用"::"声明隐藏字段,可以在编译过程中引用。{x:: 2, y:: 2, count: $。X + $.y}也计算为{“计数”:4}

Jsonnet对象可能是从它派生出子类通过将(“+”)对象连接在一起来覆盖字段值,例如,假设我们定义如下。

本地基数= {x::错误"你必须定义x的值"y::2//该字段有默认值:美元。x+$.y};

然后是Jsonnet表达式(base + {x:: 10})将编译为{“计数”:12}。事实上,Jsonnet编译器要求您重写x,因为默认值会引发错误。因此,您可以将base看作是在Jsonnet中定义一个抽象基类。

Jsonnet编译是完全确定的,不能执行外部I/O,因此非常适合定义配置。我们发现Jsonnet在限制性和灵活性之间取得了正确的平衡——以前我们使用Scala代码生成配置,这在灵活性方面犯了太多错误,导致了许多令人头痛的问题。

在Databricks,我们扩展了我们的kubectl工具(称为kubecfg),以便它可以直接将Jsonnet文件作为参数。在将数据发送到Kubernetes之前,它在内部将Jsonnet编译为纯JSON / YAML。然后,Kubernetes根据上传的配置根据需要创建或更新对象。

用Jsonnet编写Kubernetes对象

为了更好地理解Jsonnet如何与Kubernetes一起使用,让我们考虑为企业客户部署一个理想的[1]单租户“Databricks平台”的任务。bob体育客户端下载这里我们想要创建两个不同但相关的服务部署:Webapp(用于交互式工作空间)和Cluster manager(用于管理Spark集群)。此外,服务需要访问AWS RDS数据库存储持久数据。

[1]:请注意,实际上我们并没有为每个客户创建单独的Jsonnet文件——那样会产生大量的Jsonnet文件,这本身就是一个问题。

Kubernetes部署定义模板

对于这些例子,我们将使用service-deployment.jsonnet.TEMPLATE,是我们定义Kubernetes的一个内部基本模板的简化版本服务部署作为一对在一起。实例化的服务和部署一起构成了Kubernetes中可以接收网络流量的独立“生产服务”。注意,除了几个可选参数外,模板还有两个必选参数,包括传递给服务二进制本身的可选配置:

service-deployment.jsonnet.TEMPLATE

// Kubernetes (service, deployment) pair模板。{//该模板的参数::错误"serviceName必须指定"dockerImage::错误必须指定dockerImage//该模板的可选参数。serviceConf:: {}}

例1:每个服务部署一个文件

service-deployment.jsonnet。模板,我们拥有的最简单的选择是为Webapp和集群管理器创建单独的Jsonnet文件。在每个文件中,我们必须导入基本模板,对其进行子类化,并填写所需的参数。

我们指定服务名、包含服务二进制的Docker映像以及一些服务特定的配置,包括RDS地址。在本例中,RDS地址是硬编码的,但稍后我们将展示如何从运行CloudFormation脚本的输出文件中导入元数据。

下面的文件描述了管理器服务(这里的服务采用其标准含义,我们将引用Kubernetes服务明确地像这样进行)。方法在Jsonnet模板中构造特定于服务的配置serviceConf:字段,模板将其作为环境变量传递给pod。我们发现这样统一服务和Kubernetes配置很有用:

简单/ foocorp-manager.jsonnet

local serviceDeployment =进口“. . / service-deployment.jsonnet.TEMPLATE”// foocorp.的集群管理器部署。serviceDeployment + {::“foocorp-manager”dockerImage::“经理:2.42 rc1”serviceConf:: {customerName“foocorp”数据库“用户- db.databricks.us -西方- 2. - rds.amazonaws.com”},}

webapp服务要创建集群,必须指定集群管理器Kubernetes DNS地址,可以通过Kubernetes服务名称提前确定:

简单/ foocorp-webapp.jsonnet

local serviceDeployment =进口“. . / service-deployment.jsonnet.TEMPLATE”// foocorp.的webapp部署。serviceDeployment + {::“foocorp-webapp”dockerImage::“webapp: 2.42 rc1”serviceConf:: {customerName“foocorp”数据库“用户- db.databricks.us -西方- 2. - rds.amazonaws.com”managerAddress“foocorp-manager.prod.svc.cluster.local”},}

如果你有示例代码克隆后,你可以通过在文件上运行jsonnet编译器来查看这些模板的实体化输出,例如:

$ jsonnet examples/databricks/simple/foocorp-webapp.jsonnet

我们得到了什么?我们至少已经设法删除了一些关于定义服务的标准Kubernetes样板文件,将每个部署定义从100多行减少到大约10行。然而,我们可以做得更好——如果有许多不同的客户或每个客户需要更多的服务,仍然存在重复的参数,这将使该模式难以维持。

例2:将部署组合在一个文件中

由于Webapp和集群管理器服务都作为一个单元部署在一起碎片对于每个客户,每个客户只应该有一个描述其独特需求的文件是有意义的。定义一个单一模板确实是可能的,shard.jsonnet.TEMPLATE,它将Webapp和Cluster管理器部署组合在一起,而不需要重复参数。

这里的技巧是模板定义了一个Kubernetes“List”对象,它可以在一个JSON对象中包含多个Kubernetes资源。我们使用Jsonnet合并服务部署模板生成的子列表标准库函数std.flattenArrays

shard-v1 / shard.jsonnet.TEMPLATE

local serviceDeployment =进口“. . / service-deployment.jsonnet.TEMPLATE”
              {//必填参数customerName::错误必须定义customerName释放::错误“释放必须被定义”//可选参数commonConf:: {customerName: $ .customerName数据库“用户- db.databricks.us -西方- 2. - rds.amazonaws.com”},
              本地webapp = serviceDeployment +…local manager = serviceDeployment +…种类“列表”项目: std. flatarrays ([webapp, manager]),}

现在我们可以方便地定义FooCorp的整个部署,没有任何重复的数据,只使用一个文件:

shard-v1 / foocorp-shard.jsonnet

当地的shardTemplate=导入“shard.jsonnet.TEMPLATE”;shardTemplate+{customerName::“foocorp”,释放::“2.42 rc1”,}

例3:多个环境的模板子类化

要了解Jsonnet的灵活性,请考虑如何对上面示例中定义的模板进行子类化,以创建在开发环境中运行的碎片。在开发中,部署应该使用开发数据库而不是生产数据库,我们还希望使用最新的Docker映像运行。为此,我们可以进一步将分片.jsonnet. template子类化到不同的环境。这使得从这些专门的模板创建特定的生产或开发碎片成为可能:

为了使shard.jsonnet.TEMPLATE公开的接口更显式,我们将导出一个函数而不是导出一个对象newShard(name, release, env),可以调用它来构造shard对象。env对象封装了环境之间的差异(例如数据库URL)。

shard-v2 / shard.jsonnet.TEMPLATE

当地的serviceDeployment=导入“. . / service-deployment.jsonnet.TEMPLATE”;当地的newShard (customerName释放env)={当地的commonConf={customerName: customerName,数据库:env.database,},当地的webapp=serviceDeployment+{名:customerName+“应用”,dockerImage::“webapp:“+释放serviceConf: commonConf+{managerAddress: customerName+“-manager.prod.svc.cluster.local”,},},当地的经理=serviceDeployment+{名:customerName+“经理”,dockerImage::“经理:”+释放serviceConf:: commonConf,},
              apiVersion:“v1”,:“名单”,项目:std.flattenArrays ([webapp。项目,manager.items]),};//导出函数作为一个构造函数碎片{newShard:: newShard,}

dev碎片模板现在只需要为dev填写所需的env,并在默认情况下指定前沿版本。它通过导入一个包含所需数据库元数据的纯JSON文件来获得env:

shard-v2 / dev-shard.jsonnet.TEMPLATE

当地的shardTemplate=导入“shard.jsonnet.TEMPLATE”;当地的devEnv=导入“dev-env.json”;当地的newDevShard (shardName释放=“尖端”)=shardTemplate.newShard (shardName释放, env=devEnv));
              {newDevShard:: newDevShard,}

prod和dev碎片模板的完整示例代码可以在这里找到:https://github.com/databricks/jsonnet-style-guide/tree/master/examples/databricks/shard-v2

注意,使用函数进行子类化是可选的——我们可以使用派生函数dev-shard.jsonnet.TEMPLATEprod-shard.jsonnet.TEMPLATE只使用字段覆盖。然而,根据我们的经验,使用临时字段覆盖虽然功能强大,但往往会导致更脆弱的模板。我们发现,建立最佳实践来限制在大型模板中使用此类构造是很有用的。

Jsonnet风格指南

在上面的例子中,我们看到了如何使用Jsonnet模板来删除Kubernetes对象之间的配置数据重复,将多个部署组合在一起,并引用外部实体。这极大地简化了基础设施管理。然而,在大型项目中,模板本身可能成为复杂性的来源。我们发现,通过一些最佳实践和围绕Jsonnet的工具投资,这是可以管理的:

以下是我们发现的一些Jsonnet最佳实践:

  1. 对于大型模板(>10个参数),避免在子类化时直接覆盖内部字段。相反,应该使用Jsonnet函数为模板定义显式构造函数,这有助于提高可读性并鼓励模块化。
  2. 将Jsonnet配置检入源代码控制。
    • 添加预提交测试以确保所有检入模板都能编译。这可以避免在更新公共模板时无意中造成破坏。还可以使用。创建“单元测试”Jsonnet断言表达式在模板变量上断言不变量。
    • 还可以考虑签入Jsonnet物化JSON / YAML,以便在代码审查期间更容易看到更改。由于通用Jsonnet模板可能由多个文件导入,因此更改一个文件可能会影响多个文件的输出。幸运的是,如果物化的YAML也包含在Jsonnet更改中,则可以在编译时检测到非预期的更改。
    • 经常重构——没有破坏的风险。由于Jsonnet模板编译为具体的JSON / YAML文件,因此可以检查之前和之后的输出以确保重构是正确的(即零净物化差异)。
  3. 将尽可能多的配置推到Jsonnet。基于某些环境标志(例如dev vs prod)将逻辑引入到服务中可能很有诱惑力。我们发现这是一种反模式——这样的配置应该在模板编译时而不是在运行时确定。这个原则密封的配置帮助防止在将服务部署到新环境时出现意外。

更多详细的建议,请查看我们新发布的Jsonnet风格指南.我们也欢迎来自社区的任何评论或贡献。

结论

在这篇博文中,我们分享了Databricks使用Jsonnet简化基础设施管理的经验。作为一种统一的模板语言,Jsonnet支持在广泛的基础设施和服务之间进行组合,它使我们离完全声明式的基础设施又近了一步。我们发现Jsonnet易于使用且足够灵活,可以作为我们的主要配置语言,建议您也尝试一下!

如果您觉得这个主题很有趣,请关注关于我们围绕Jsonnet和Kubernetes构建的工具的后续博客文章。我们也招聘云平台、基础设施和bob体育客户端下载砖Serverless

免费试用Databricks
看到所有工程的博客的帖子