当前位置: 移动技术网 > IT编程>软件设计>架构 > 用Helm3构建多层微服务

用Helm3构建多层微服务

2019年11月30日  | 移动技术网IT编程  | 我要评论

helm是一款非常流行的k8s包管理工具。以前就一直想用它,但看到它产生的文件比k8s要复杂许多,就一直犹豫,不知道它的好处能不能抵消掉它的复杂度。但如果不用,而是用kubectl来进行调式真的很麻烦。正好最近helm3正式版出来了,比原来的helm2简单了不少,就决定还是试用一下。结果证明确实很复杂,它的好处和坏处大致相当。有了它确实能大大简化对k8s的调式,但也需要花费比较多的时间来学习,而且产生的配置文件要复杂许多。但是事实是现在没有什么很方便的帮助调式k8s的工具,在没有更好的方案之前,我还是建议用它,只是前期需要花些功夫学习和掌握它。

helm3和helm2的语法差不太多,只是使用起来更方便,不用安装tiller。一个比较明显的变化是不再需要“requirements.yaml”, 依赖关系是直接在“chart.yaml”中定义。有关helm3和helm2的区别,详情请参见changes since helm 2

网上有不少讲述helm的文章,但大部分都是主要讲解安装和举一个简单的例子。但helm使用起来还是比较复杂的,一定要有一个复杂的例子才能把它的功能讲清楚,里面有不少设计方面的问题需要思考。我刚开始接触的时候就觉得头绪繁多,不知从哪下手。本文就通过一个相对复杂的例子来讲解用helm3来设计配置文件的思路,使上手更容易。

这里不讲helm3的安装,它比较很容易。也不讲解helm的基本语法,你可以自己去看其他文档。即使你不懂helm,应该也能猜出七八成,剩下的就要读文档了(charts)。helm的语法还是比较复杂的,要想搞懂可能要花一两天时间。

本文假设你对helm有一个大概的了解,想要构建一个复杂的微服务,但有不知如何下手;或者你想了解一下构建helm的最佳实践,那就请你继续读下去。

helm文件结构

chart里一个很重要的概念就是模板(template),它就是go语言模板,它是里面加入了编程逻辑的k8s文件。这些模板文件在使用时都要先进行模板解析,把其中的程序逻辑转化成对应的编码,最终生成k8s配置文件。

file

以上就是helm自动生成的chart目录结构,在helm里每个项目叫一个chart,它由下面几个组成部分:

  • "chart.yaml":存有这个chart的基本信息,
  • "values.yaml":定义模板中要用到的常量。
  • “template”目录:里面存有全部的模板文件,其中最重要的是“deployment.yaml”和“service.yaml”,分别是部署和服务文件. "helpers.tpl"用来定义变量,"ingress.yaml"和"serviceaccount.yaml"分别是对外接口和服务账户,这里暂时没用, “notes.txt”是注释文件。
  • “charts”目录: 存有这个chart依赖的所有子chart。

helm的基本元素

helm有四个基本元素,值,常量,变量和共享常量(这个后面会讲)

值(literal)

helm在k8s的基础之上增加了模板功能,使k8s的配置文件更加灵活。里面的主要概念就是模板(template),也就是在k8s的配置文件里增加了常量和变量以及编程逻辑。如果你不用这些新增功能,那么就是普通的yaml文件(k8s配置文件),里面用到的基本元素就是值。

常量

节点定位(node anchor):

如果你想复用重复的值,能把它定义成常量吗?yaml有一个功能叫节点定位(node anchor),类似于定义一个常量,然后引用。但它有一些限制,定义的必须是一个节点,因此不如真正的常量灵活。
例如如下文件中,用“&”定义了一个常量“&k8sdemodatabaseservice”,然后用“*k8sdemodatabaseservice”引用它。

global:
  k8sdemodatabaseservice: &k8sdemodatabaseservice k8sdemo-database-service
  mysqlhost: *k8sdemodatabaseservice

这时,“k8sdemodatabaseservice:”是yaml文件节点的键名,“&k8sdemodatabaseservice”是节点定位的名字,相当于常量名,“k8sdemo-database-service”是yaml节点的键值。在上述代码中,k8sdemodatabaseservice和mysqlhost的值都是“k8sdemo-database-service”。

有关节点定位(node anchor)的详细内容,请参见 yaml

常量:

由于节点定位的局限性,helm引入了真正的常量,也就是在"values.yaml"里定义的内容,它可以定义是任何东西,不只限于节点。

在"values.yaml"里定义常量:

replicacount: 1

在部署模板里引用:

replicas: {{ .values.replicacount }}

那么什么时候用常量,什么时候用值(literal)呢?如果一个值在模板中出现多次,就要定义常量,避免重复。例如“accessmodes”,既要在存储卷里出现,又要在存储卷申请里出现。另外如果值有可能变化(不论是随部署环境变化,还是随时间变化),那么就定义成常量,这样在修改时就只用改"values.yaml",而不必修改模板文件。例如“replicas”的值(也就是集群的个数)是可能变化的,就要定义成常量。在模板里可以引用常量的,但在"values.yaml"里不行,因为它只是普通yaml文件,没有模板解析功能,因此不支持常量,这里就只能用节点定位(来代替常量)。

有关helm常量的详细内容,请参见 use placeholders in yamluse yaml with variables

变量

节点定位的功能是有限的,例如你想利用已有的节点定位,对它进行转换,定义一个新的节点定位,这在"values.yaml"里就不行了。
例如你已有节点定位“name”,你想在这个基础上定义一个新的节点定位“servicename”,这个"values.yaml"就不支持了,你必须要用模板。

如下所示,这在"values.yaml"里是不支持的。

name: &name k8sdemo-backend
servicename:*name-service

这就引出了变量的概念,但它只能在模板里才行。 换句话说,模板既支持常量,也支持变量。但如果把变量的定义逻辑放在helm每个模板里,就显得很乱。因此一般的做法是把这些逻辑放在一个单独的模板文件里,这个就是前面讲到的"_helpers.tpl"文件。当你需要对常量进行转换,生成新的常量,你就在定义变量,这部分代码就放在"_helpers.tpl"里。

下面就是"_helpers.tpl"中定义"k8sdemo.name"的代码。

{{- define "k8sdemo.name" -}}
{{- default .chart.name .values.nameoverride | trunc 63 | trimsuffix "-" -}}
{{- end -}}

在以上这些元素中,常量(也就是在"values.yaml"中定义的)是最灵活的,能用它时尽量用它。而且因为它是定义在普通yaml文件中("values.yaml"),应用程序可以直接访问它,这样可以实现应用程序和k8s之间的数据共享。但如果你需要对常量进行编程转换,那就没办法了,只能定义变量,把它放在"_helpers.tpl"中。

configmap和secret

在k8s中configmap和secret是用来存储共享配置参数和保密参数的,但在helm中,由于有了上面讲的helm基本元素,它们完全可以代替configmap的功能,因此configmap就不需要了,但secret还是需要的,因为要存储加密信息。下面会讲解。

有关configmap的设计局限性,请参见

chart设计

现在我们就用一个具体的例子来展示helm的chart设计。这个例子是一个微服务应用程序,它共有三层: 前端,后端和数据库,只有这样才能让helm的一些设计问题付出水面,如果只有一层的话,就太简单了,没有参考价值。

在k8s中,每一层就是一个单独的服务,它里面有各种配置文件。helm的优势是把这些不同的服务组成一个chart来共同管理和调式,方便了许多。

file

上面就是最终的chart目录结构图。“chart”是总目录,里面有三个子目录“k8sdemo”,“k8sdemo-backend”,“k8sdemo-database”, 每一个对应一个服务,每个服务都是一个独立的chart,能单独调式部署,chart之间也可以有依赖关系。其中“k8sdemo”是父chart,同时也是前端服务,它的“charts”目录里有它依赖的另外两个服务。“k8sdemo-backend”是后端服务,“k8sdemo-database”是数据库服务。

处理chart的依赖关系有两种方式:

  1. 嵌入式:就是直接把依赖的chart放在“charts”子目录里,这样子chart是父chart的一部分。它是一种紧耦合的关系,好处是比较简单,但不够灵活。
  2. 依赖导入式:就是各个chart是并列关系,各自单独调试部署,互相独立,需要合并时再把子chart导入父chart里,它是一种松耦合的关系,好处是比较灵活,但设计更复杂。 在这种结构下,各个chart可以单独工作也可以联合工作,不过你需要更好的设计。

这里采用的是依赖导入式方式,主要原因是我原来认为嵌入式需要一起调试,复杂度太高,如果你觉得这不是问题,这也是个不错的办法。用依赖导入式方式,可以单独调试各个chart,简单了很多。后来发现其实采用嵌入式也可以单独调试子chart,只是父chart不能单独调试而已。

调试顺序:

当你采用依赖导入式方式时,调试顺序关系不大,因为各个chart是各自独立的,可以单独调试。举个例子,虽然“k8sdemo-backend”需要“k8sdemo-database”才能正常运行,但当没有数据库服务时,你的程序也可以运行,只不过输出的是错误信息,但这并不影响你调试chart。

我先调试“k8sdemo”,它虽然依赖另外两个chart,但没有它们也能单独工作。然后再调试“k8sdemo-backend”和“k8sdemo-database”,最后再把它们导入到“k8sdemo”中去再进行联调。

调式“k8sdemo”

它的调试是最容易的,由于它里面没有真正的前端代码,只要把nginx调试成功了就可以了。只要在生成的文件基础上做些修改就行了。

键入如下命令创建chart,其中“k8sdemo”是chart的名字,这个名字很重要,服务的名字和label都是由它产生的。

helm create k8sdemo

这之后,系统会自动创建前面讲到的chart目录结构。让后就是对已经生成的文件进行修改。

修改"values.yaml":
以下是"values.yaml"主要修改的地方

image:
  repository: nginx:1.17.6
  pullpolicy: never

imagepullsecrets: []
nameoverride: "k8sdemo"
fullnameoverride: "k8sdemo"

service:
  type: nodeport
  port: 80
  nodeport: 31080

另外,由于"ingress.yaml"和"serviceaccount.yaml"暂时没用,就把它们都设成了“false”

ingress:
  enabled: false

serviceaccount:
  # specifies whether a service account should be created
  create: false

修改"service.yaml":

apiversion: v1
kind: service
metadata:
  name: {{ include "k8sdemo.fullname" . }}
  labels:
    {{- include "k8sdemo.labels" . | nindent 4 }}
spec:
  type: {{.values.service.type}}
  ports:
    - port: {{.values.service.port}}
      nodeport: {{.values.service.nodeport}}
      targetport: http
      protocol: tcp
      name: http
  selector:
    {{- include "k8sdemo.selectorlabels" . | nindent 4 }}

修改"deployment.yaml":

。。。
containers:
  - name: {{ .chart.name }}
    securitycontext:
      {{- toyaml .values.securitycontext | nindent 12 }}
    image: {{ .values.image.repository }}
    imagepullpolicy: {{ .values.image.pullpolicy }}
。。。

以上都是简单的修改,不涉及到设计问题。由于篇幅的关系,这里没有列出全部源码,如果有兴趣请在本文末尾找到源码地址。

共享常量

在进行下面的调试之前,先要讲一个重要概念。 前面介绍helm的基本元素时讲的都是在一个chart里共享值,如果要在不同chart之间共享值(例如k8s服务名,数据库用户名和端口),那么这些还不够,你需要共享常量. 通常情况下子chart和父chart之间的常量是不能共享的,如果需要共享,需要有一种特殊的方法来定义常量,这就是共享常量。它必须是定义在父chart中。

共享常量

例如,你在“k8sdemo”的“values.yaml”加入下面代码,注意节点的名字必须是子chart名(例如“k8sdemo-backend”)

k8sdemo-backend:
  replicacount: 2
k8sdemo-database:
  replicacount: 2

在“k8sdemo”的模板里就可以通过“{{ .values.k8sdemo-backend.replicacount }}” 来访问。当helm发现节点名是子chart名时,它会自动拷贝这个常量到子chart的“values.yaml”中,因此,在“k8sdemo-backend”中,你也可以通过“{{ .values.replicacount }}” 来访问这个常量。注意这里并没有包含子chart名(“k8sdemo-backend”),而是只有常量名,因为子chart名只是一个标识,而不是常量名的一部分。

全局常量

共享常量只能把常量共享给一个字chart,如果你需要多个子chart之间共享,就需要创建全局常量,它用“global”来标识,下面是示例。

在“k8sdemo-backend”的"values.yaml"中定义:

global:
  k8sdemodatabaseservice: &k8sdemodatabaseservice k8sdemo-database-service
  mysqlusername: dbuser
  mysqluserpassword: dbuser
  mysqlport: 3306
  mysqlhost: *k8sdemodatabaseservice
  mysqldatabase: service_config

在“k8sdemo-backend”的“deployment.yaml”中引用。

env:
  - name: mysql_user_name
    value: {{ .values.global.mysqlusername }}
  - name: mysql_user_password
    value: {{ .values.global.mysqluserpassword }}
  - name: mysql_host
    value: {{ .values.global.mysqlhost }}
  - name: mysql_port
    value: "{{ .values.global.mysqlport }}"
  - name: mysql_database
    value: {{ .values.global.mysqldatabase }}

在“k8sdemo-database”的"values.yaml"中定义:

global:
  k8sdemodatabaseservice: k8sdemo-database-service
  mysqlusername: dbuser
  mysqluserpassword: dbuser
  mysqlrootpassword: root
  mysqldatabase: service_config

在“k8sdemo-database”的“deployment.yaml”中引用。

env:
  - name: mysql_root_password
    value: {{ .values.global.mysqlrootpassword }}
  - name: mysql_user_name
    value: {{ .values.global.mysqlusername }}
  - name: mysql_user_password
    value: {{ .values.global.mysqluserpassword }}
  - name: mysql_database
    value: {{ .values.global.mysqldatabase }}

当把“k8sdemo-backend”和“k8sdemo-database”导入"k8sdemo"后进行联调时, 就要把上面提到的全局常量写入"k8sdemo"的"values.yaml"文件中,这样就能让各个子chart共享这些常量。如下所示:

global:
  k8sdemobackendservice: k8sdemo-backend-service
  k8sdemodatabaseservice: &k8sdemodatabaseservice k8sdemo-database-service
  mysqlusername: dbuser
  mysqluserpassword: dbuser
  mysqlrootpassword: root
  mysqlport: 3306
  mysqlhost: *k8sdemodatabaseservice
  mysqldatabase: service_config

如果父chart和子chart有重复的全局常量,这时父chart("k8sdemo")的全局常量值就会覆盖子chart的全局常量。

它的使用原则就是如果只是子chart独有的常量就在子chart的"values.yaml"中定义,如果是共享的常量就在父chart中定义。但如果采用的是依赖导入方式,由于子chart也要单独调试,这时你在子chart里也要定义这些全局常量。这样在进行chart总调试时,就会使用父chart的中的值。

详情请参见 subcharts and global values

调试“k8sdemo-backend”

“k8sdemo-backend”的chart需要取(与“k8ssdemo”)不同的名字,
创建:

helm create k8sdemo-backend

file

上面就是“k8sdemo-backend”的目录图。由于它需要建持久卷,因此这里增加了两个文件“persistentvolume.yaml”和“persistentvolumeclaim.yaml” ( 不是自动生成的)。

值得一提的是k8s对象的命名。一般情况下,如果不需要对其进行引用,用chart的全名就行了。例如部署的名称,如下所示。

name: {{ include "k8sdemo.fullname" . }}

如果是服务名(service name),它需要在应用程序和k8s之间共享,也需要在父chart和子chart之间共享,这时最好单独定义一个全局共享常量。

在“values.yaml”中定义:

global:
  k8sdemobackendservice: k8sdemo-backend-service

在“service.yaml”中引用:

name: {{.values.global.k8sdemobackendservice}}

调试“k8sdemo-database”

它的调试方式与“k8sdemo-backend”大同小异,就不详细讲解了。

联合调试:

上面各个chart都单独调试成功之后,就要把它们合在一起进行联合调试。
在“k8sdemo”(父chart)中加入依赖关系(chart.yaml)。

dependencies:
  - name: k8sdemo-backend
    repository: file://../k8sdemo-backend
    version: 0.1.0
  - name: k8sdemo-database
    repository: file://../k8sdemo-database
    version: 0.1.0

这里为了简单起见,没有用到chart库(chart repository),使用了本地目录。这里的“file://”是针对chart的根的相对路径,“file://..”就是“k8sdemo”的上级目录。

详情请参见how to refer to a helm chart in the same repository

修改全局常量("values.yaml"):

global:
  k8sdemobackendservice: k8sdemo-backend-service
  k8sdemodatabaseservice: &k8sdemodatabaseservice k8sdemo-database-service
  mysqlusername: dbuser
  mysqluserpassword: dbuser
  mysqlrootpassword: root
  mysqlport: 3306
  mysqlhost: *k8sdemodatabaseservice
  mysqldatabase: service_config

只有需要在chart之间共享的常量才需要在父chart里的"values.yaml"定义,其余的在各自子chart里的"values.yaml"定义就可以了。

键入如下命令“helm dependency update k8sdemo”,更新依赖关系

~ # vagrant@ubuntu-xenial:~/jfeng45/k8sdemo/script/kubernetes/chart$ helm dependency update k8sdemo
hang tight while we grab the latest from your chart repositories...
...successfully got an update from the "stable" chart repository
update complete. ⎈happy helming!⎈
saving 2 charts
deleting outdated charts

完成之后,生成的图如下所示,这时在“charts”目录下就导入了新的依赖关系“k8sdemo-backend”和“k8sdemo-database”的chart。

file

有一点需要注意的是,单独调试和联合调试时,生成的k8s配置文件大部分都是一样的,但有一个地方不同

下面是联合调试时“k8sdemo-database”的部署文件,最后一行“app.kubernetes.io/instance: ”的值是“k8sdemo”。

# source: k8sdemo/charts/k8sdemo-database/templates/deployment.yaml
apiversion: apps/v1
kind: deployment
metadata:
  name: k8sdemo-database
  labels:
    helm.sh/chart: k8sdemo-database-0.1.0
    app.kubernetes.io/name: k8sdemo-database
    app.kubernetes.io/instance: k8sdemo
。。。

下面是单独调试时“k8sdemo-database”的部署文件,最后一行“app.kubernetes.io/instance: ”的值是“”k8sdemo-database”。

# source: k8sdemo/charts/k8sdemo-database/templates/deployment.yaml
apiversion: apps/v1
kind: deployment
metadata:
  name: k8sdemo-database
  labels:
    helm.sh/chart: k8sdemo-database-0.1.0
    app.kubernetes.io/name: k8sdemo-database
    app.kubernetes.io/instance: k8sdemo-database
。。。

因为“instance”的名字是“{{ .release.name }}”,而单独调试和联合调试时给的“release”名字不同。而其他的值都是由配置文件决定的,因此不会有意外。

安装k8sdemo:

vagrant@ubuntu-xenial:~/jfeng45/k8sdemo/script/kubernetes/chart$ helm upgrade k8sdemo ./k8sdemo
release "k8sdemo" has been upgraded. happy helming!
name: k8sdemo
last deployed: fri nov 29 01:28:55 2019
namespace: default
status: deployed
revision: 2
notes:
1. get the application url by running these commands:
  export node_port=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodeport}" services k8sdemo)
  export node_ip=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")
  echo http://$node_ip:$node_port

获取pod名称:

vagrant@ubuntu-xenial:~/jfeng45/k8sdemo/script/kubernetes/chart$ kubectl get pod
name                                          ready   status    restarts   age
k8sdemo-74cb7b997c-pgcj4                      1/1     running   0          33s
k8sdemo-backend-5cd9d79856-dqlmz              1/1     running   0          33s
k8sdemo-database-85855485c6-jtksb             1/1     running   0          33s
k8sdemo-jenkins-deployment-675dd574cb-r57sb   1/1     running   3          23d

运行程序进行测设:

vagrant@ubuntu-xenial:~/jfeng45/k8sdemo/script/kubernetes/chart$ kubectl exec -ti k8sdemo-backend-5cd9d79856-dqlmz -- /bin/sh
~ # ./main.exe
time="2019-11-27t07:03:03z" level=debug msg="connect to database "
time="2019-11-27t07:03:03z" level=debug msg="datasourcename:dbuser:dbuser@tcp(k8sdemo-database-service:3306)/service_config?charset=utf8"
time="2019-11-27t07:03:03z" level=debug msg="findall()"
time="2019-11-27t07:03:03z" level=debug msg="created=2019-10-21"
time="2019-11-27t07:03:03z" level=debug msg="find user:{1 tony it 2019-10-21}"
time="2019-11-27t07:03:03z" level=debug msg="find user list:[{1 tony it 2019-10-21}]"
time="2019-11-27t07:03:03z" level=debug msg="user lst:[{1 tony it 2019-10-21}]"
~ #

其他问题:

由于篇幅有限,本文不可能把所有的问题都讲清楚,还有两个比较重要的问题,这里简单的提一下。

1.secret:
本文用的都是明码,如果需要加密的话有两种方式,一种是 ,另一种是vault,请阅读相关文档。

2.为不同环境设置不同的常量:
本文只创建了针对一种环境的文件 ,如果你需要针对不同环境(例如dev,qa,prod)配置不同的参数的话,你可以在“k8sdemo”的chart里给不同的环境创建不同的"values.yaml",例如“values-dev.yaml”给dev环境。但在子chart里,就不能这样做,因为系统要求"values.yaml"。这时,你可以在父chart的“values-dev.yaml”里为不同的子chart创建常量,这样这些常量就能覆盖子chart里定义的常量。

在“values-dev.yaml”加入下面代码。

k8sdemo-backend:
    replicacount: 2
k8sdemo-database:
    replicacount: 2

键入如下命令试运行:

vagrant@ubuntu-xenial:~$ cd /home/vagrant/jfeng45/k8sdemo/script/kubernetes/chart
vagrant@ubuntu-xenial:~/jfeng45/k8sdemo/script/kubernetes/chart$ helm install --dry-run --values ./k8sdemo/values-dev.yaml --debug k8sdemo ./k8sdemo

查看结果,子chart中的相应参数已被覆盖。

详情请参阅how to set environment related values.yaml in helm subcharts?

常见错误:

在调试过程中还是遇到了不少问题,但大多数都是与语法有关的问题,因为helm和k8s都用的是yaml文件,而它对文件格式有着严格的要求,如果不满足要求就会报错。幸好它报错时包含了错误代码行号,这样查找起来比较容易。

  1. pod的状态是crashloopbackoff

它的症状是在用“helm install --dry-run --debug”调试时没有问题,但正式运行时出了问题,用下面命令检查,pod的状态是“crashloopbackoff”。

vagrant@ubuntu-xenial:~$ kubectl get pod
name                                           ready   status             restarts   age
k8sdemo-74cb7b997c-gn5v2                       1/1     running            1          47h
k8sdemo-backend-6cdbb96964-tb2xd               0/1     crashloopbackoff   129        9h
k8sdemo-database-deployment-578fc88c88-mm6x8   1/1     running            12         37d
k8sdemo-jenkins-deployment-675dd574cb-r57sb    1/1     running            3          19d

这个问题我以前调试k8s时也碰到过,主要是与docker镜像有关,但这次明明镜像是 好的。试了很多组合,最后终于发现是自动生成的代码出了问题。
在“deployment.yaml”里有下面代码,这是helm自动生成用来测试部署的。

livenessprobe:
  httpget:
    path: /
    port: http
readinessprobe:
  httpget:
    path: /
    port: http

把它去掉之后就没有问题了。而且它只在特定的chart(“k8sedemo-backend”)里会出错,在“k8sdemo”里就没有问题。我现在也不是特别清楚问题在哪,只是把它暂时删除掉了。

  1. 持久卷未能绑定到持久卷申请

它的症状是宿主机的持久卷未能绑定到持久卷申请,导致持久卷申请又另外创建了一个持久卷。你用“kubectl get pv”就能看到新创建的持久卷,但实际上它是不必要的,只要把持久卷申请绑定到已有的pv上就行了。这个错误并不是每次都发生,而是随机的。大部分时间绑定正确,少数时候绑定错误。我开始想是不是因为执行k8s文件的顺序问题,但k8s文件是按照文件类别(kind)来执行的,按理来说顺序应该是正确的。再有一个可能就是时间延迟,因为创建持久卷需要时间,而如果持久卷申请没有检测到这个持久卷,那么它就会另外创建一个。如果真是这样的话,就要在创建时设定一个延迟。但它暂时来讲对我影响不大,因此就偷了一下懒,以后有时间再来调试。

源码库

完整源码的github链接:

索引:

  1. changes since helm 2
  2. charts
  3. yaml
  4. use placeholders in yaml
  5. use yaml with variables
  6. subcharts and global values
  7. how to refer to a helm chart in the same repository
  8. vault
  9. how to set environment related values.yaml in helm subcharts?

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网