使用Jsonnet模板语言以更好的使用kubernetes
by 伊布
Kubernetes的编排文件支持2种格式:json和yaml,这两种格式是等价的。以RedHat的Openshift CaaS平台为例,用户会先编辑好自己的编排文件,然后丢到Openshift上去跑,这时候编排文件是固定的。但在一些类SaaS的平台,例如Databricks,CaaS这一层对用户来说是透明的,编排文件是由Databricks生成的,但由于具体的环境不同,会带来一些使用上的不便。
-
公共结构不能在YAML文件之间共享。可能需要在不同的环境(dev,staging,prod)、不同的运营商(AWS, Azure)中部署。
-
YAML文件必须经常关联外部定义的实体的元数据,例如关系数据库,网络配置或SSL证书。
-
复杂的多层服务部署可能需要组合许多不同的资源或YAML文件。更新由许多这样的文件组成的部署变得繁重。
对于我司的大数据平台来说,使用Json也有一些不便:由于我司支持多租户,不同租户的编排文件不同,但其差异都是可预计的。当前的做法是在Java中直接生成编排代码,但这样带来的问题就是代码的可读性很差。当然由于修改并不复杂,实际上是可以使用scala的字符串插值来解决这个问题,但Scala又带来了混合编程的问题,不是每个人都能接受这种做法。
Databricks前期的做法也是使用scala来处理编排文件生成的问题,但从2015年开始,逐渐使用Jsonnet来替换之前的做法。在过去两年里,Jsonnet已经发展成为工程中的事实上的标准配置语言。
Jsonnet基础
Jsonnet是一种配置语言,可帮助您定义JSON数据。基本思想是,某些JSON字段可能会作为在编译时计算的变量或表达式留下。例如,JSON对象{"count": 4}
可以表示为{count: 2 + 2}
。您还可以使用可在编译期间引用的::
声明隐藏的字段,例如{x:: 2, y:: 2, count: $.x + $.y}
也可以计算为{"count": 4}
。
Jsonnet对象可以通过将对象(“+”)连接在一起来覆盖字段值来进行子类化,例如,我们定义以下内容。
local base = {
x:: error "you must define the value of 'x'",
y:: 2, // this field has a default value
count: $.x + $.y
};
然后,Jsonnet表达式(base + {x:: 10})
将编译成{"count": 12}
。实际上,Jsonnet编译器要求您重写x,因为默认值会引发错误。因此,您可以将base
视为在Jsonnet中定义抽象基类。
Databricks还扩展了kubectl命令(kubecfg),该命令接收Jsonnet文件,将该文件编译成Json文件,并发送给kubernetes;之后kubernetes使用编译得到的json文件来创建或更新对象。
用Jsonnet组合Kubernetes对象
为了更好地了解Jsonnet如何与Kubernetes一起使用,我们来考虑为企业客户部署一个理想化的1单租户“Databricks平台”的任务。在这里,我们要创建两个不同但相关的服务部署:Webapp(用于交互式工作区)和Cluster manager(用于管理Spark群集)。此外,服务需要访问AWS RDS数据库来存储持久数据。
定义Kubernetes部署的模板
对于这个示例,我们将使用service-deployment.jsonnet.TEMPLATE,这是Databricks内部基本模板之一的简化版本,它们定义了一个Kubernetes 服务并将其部署在一起。实例化的Service和Deployment一起构成了可以接收网络流量的Kubernetes中的独立“生产服务”。
service-deployment.jsonnet.TEMPLATE
这个文件就是下面要部署spark服务的Jsonnet模板文件。
// Template for a Kubernetes (service, deployment) pair.
{
// Required arguments for this template
serviceName:: error "serviceName must be specified",
dockerImage:: error "dockerImage must be specified",
// Optional arguments for this template.
serviceConf:: {},
resources:: {
requests: {
memory: "250Mi",
cpu: "500m",
},
},
numReplicas:: 1,
// Defines a Kubernetes deployment
local service = {
kind: "Service",
metadata: {
name: $.serviceName,
},
spec: {
selector: {
serviceName: $.serviceName,
}
// Some required fields omitted for brevity -- you can refer to
// https://kubernetes.io/docs/concepts/services-networking/service/
// for more information on defining services.
}
},
// Defines a Kubernetes service
local deployment = {
kind: "Deployment",
metadata: {
name: $.serviceName,
},
spec: {
replicas: $.numReplicas,
template: {
metadata: {
labels: {
name: $.serviceName,
},
},
spec: {
containers: [
{
name: "default",
image: $.dockerImage,
resources: $.resources,
env: {
name: "SERVICE_CONF",
value: std.manifestJson($.serviceConf),
},
// Some fields omitted for brevity -- you can refer to
// https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
// for more information on defining deployments.
}
],
},
},
}
},
// Wrap up both the service and deployment in a Kubernetes resource list.
apiVersion: "v1",
kind: "List",
items: [service, deployment],
}
示例1:每个服务部署一个文件
给定service-deployment.jsonnet.TEMPLATE,我们最简单的选择是为Webapp和Cluster管理器创建单独的Jsonnet文件。在每个文件中,我们必须导入基本模板,对其进行子类化,然后填写所需的参数。
我们指定服务名称,包含Docker映像以及包括RDS地址在内的一些服务特定配置。在此示例中,RDS地址是硬编码的,但是稍后将介绍如何从运行CloudFormation脚本的输出文件导入元数据。
以下文件描述了Manager服务(这里服务表示Kubernetes服务)。我们还在Jsonnet模板中通过serviceConf:: field
模板传递到pod作为环境变量来构建服务特定的配置。我们发现以这种方式统一服务和Kubernetes配置是有用的:
simple/foocorp-manager.jsonnet:
local serviceDeployment = import "../service-deployment.jsonnet.TEMPLATE";
// The cluster manager deployment for foocorp.
serviceDeployment + {
serviceName:: "foocorp-manager",
dockerImage:: "manager:2.42-rc1",
serviceConf:: {
customerName: "foocorp",
database: "user-db.databricks.us-west-2.rds.amazonaws.com",
},
}
对于webapp服务来创建集群,它必须指定集群管理器的Kubernetes DNS地址(managerAddress),可以从Kubernetes服务名称提前确定:
simple/foocorp-webapp.jsonnet:
local serviceDeployment = import "../service-deployment.jsonnet.TEMPLATE";
// The webapp deployment for foocorp.
serviceDeployment + {
serviceName:: "foocorp-webapp",
dockerImage:: "webapp:2.42-rc1",
serviceConf:: {
customerName: "foocorp",
database: "user-db.databricks.us-west-2.rds.amazonaws.com",
managerAddress: "foocorp-manager.prod.svc.cluster.local",
},
}
现在我们有了3个文件:模板文件service-deployment.jsonnet.TEMPLATE
,manager编排文件foocorp-manager.jsonnet
,webapp编排文件foocorp-webapp.jsonnet
。具体文件可以git clone 示例代码repo。
用jsonnet编译,可以得到最终的json编排文件。
jsonnet examples/databricks/simple/foocorp-webapp.jsonnet
jsonnet examples/databricks/simple/foocorp-manager.jsonnet
jsonnet命令行的安装,可以从Github jsonnet repo下载后make得到,当然前提是已经安装了g++(源码是c++写的)。
示例2:在单个文件中组合部署
每个用户总是需要webapp和service结合在一起提供服务,所以可以把他们定义在一个文件里,最终达到每个客户一个文件来描述其需求。
定义单独的模板shard.jsonnet.TEMPLATE
(https://github.com/databricks/jsonnet-style-guide/blob/master/examples/databricks/shard-v1/shard.jsonnet.TEMPLATE),它同时组合Webapp和Cluster Manager部署而不重复参数。
这里的诀窍在于模板定义了一个Kubernetes“List”对象,它可以在一个JSON对象中包含多个Kubernetes资源。我们使用Jsonnet 标准库函数 合并由服务部署模板生成的子列表std.flattenArrays:
local serviceDeployment = import "../service-deployment.jsonnet.TEMPLATE";
{
// Required arguments
customerName:: error "customerName must be defined",
release:: error "release must be defined",
// Optional arguments
commonConf:: {
customerName: $.customerName,
database: "user-db.databricks.us-west-2.rds.amazonaws.com",
},
local webapp = serviceDeployment + ...
local manager = serviceDeployment + ...
kind: "List",
items: std.flattenArrays([webapp, manager]),
}
现在我们可以方便地定义FooCorp的整个部署,而不需要任何重复的数据,只能使用一个文件:
shard-v1 / foocorp-shard.jsonnet:
local shardTemplate = import "shard.jsonnet.TEMPLATE";
shardTemplate + {
customerName:: "foocorp",
release:: "2.42-rc1",
}
对于不同的用户来说,真正不同的也就是customerName和使用的镜像的版本了。
示例3:对多个环境进行子类化模板
针对不同的环境(dev, staging, prod),可以将Jsonnet进一步解构、重构以满足复杂环境的要求。这块直接看shard-v2部分的代码,不再贴了。
如何为我所用
像我们这种多租户的场景,使用jsonnet是很合适的,代码可读性更好。但很遗憾的是,jsonnet只提供了c api(make libjsonnet.so
)和python api(sudo pip install jsonnet
),并没有提供java的api,使用起来并不是很方便。
我试着用JNI去封装jsonnet的c api,但在调试的时候发现会段错误。调用c api的处理应该没问题,我单独写c程序验证过;但是通过jni来调用就会有问题,感觉像是嵌套层次太深造成的。源代码在这里,如果你有兴趣可以帮忙走读下代码找找原因,不胜感激。
另一个思路就是用java重写jsonnet。github上有这么一个项目,但它的实现是有问题的。
Ref
Declarative Infrastructure with the Jsonnet Templating Language
Subscribe via RSS