helm 构建 chart

Posted 看,未来

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了helm 构建 chart相关的知识,希望对你有一定的参考价值。

文章目录

我的文和网上现有的文可能只差百分之一,但是这百分之一,就够了。

应用示例

如果我们想要在 Kubernetes 集群中部署两个副本的 Ghost,可以直接应用下面的资源清单文件即可:

# ghost/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ghost
spec:
  selector:
    matchLabels:
      app: ghost-app
  replicas: 2
  template:
    metadata:
      labels:
        app: ghost-app
    spec:
      containers:
        - name: ghost-app
          image: ghost
          ports:
            - containerPort: 2368
---
# ghost/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: ghost
spec:
  type: NodePort
  selector:
    app: ghost-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 2368

直接通过 kubectl 应用上面的资源对象即可:

$ kubectl apply -f  ghost/
service/ghost created
deployment.apps/ghost created

$ kubectl get pod -l app=ghost-app
NAME                    READY   STATUS    RESTARTS   AGE
ghost-ddb558557-7szrc   1/1     Running   0          2m13s
ghost-ddb558557-brn9p   1/1     Running   0          2m13s


$ kubectl get svc ghost
NAME    TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
ghost   NodePort   10.97.232.158   <none>        80:30152/TCP   2m44s

通过 http://<nodeip>:31950 访问到 Ghost :

清理 deployment

$ delete -f ghost/
deployment.apps "ghost" deleted
service "ghost" deleted

看上去要部署 Ghost 是非常简单的,但是如果我们需要针对不同的环境进行不同的设置呢?比如我们想将它部署到不同环境(staging、prod)中去,是不是我们需要一遍又一遍地复制我们的 Kubernetes 资源清单文件,这还只是一个场景,还有很多场景可能需要我们去部署应用,这种方式维护起来是非常困难的,这个时候就可以理由 Helm 来解放我们了。


基础模板

首先,新建一个新目录,进去。

现在我们开始创建一个新的 Helm Chart 包。直接使用 helm create 命令即可:

$ helm create my-ghost

Creating my-ghost
➜ tree my-ghost
my-ghost
├── Chart.yaml
├── charts
├── templates
│   ├── NOTES.txt
│   ├── _helpers.tpl
│   ├── deployment.yaml
│   ├── hpa.yaml
│   ├── ingress.yaml
│   ├── service.yaml
│   ├── serviceaccount.yaml
│   └── tests
│       └── test-connection.yaml
└── values.yaml

3 directories, 10 files

该命令会创建一个默认 Helm Chart 包的脚手架,helm charts 细节说明请参阅该片文章,可以删掉下面的这些使用不到的文件:

rm -f my-ghost/templates/tests/test-connection.yaml
rm -f my-ghost/templates/serviceaccount.yaml
rm -f my-ghost/templates/ingress.yaml
rm -f my-ghost/templates/hpa.yaml
rm -f my-ghost/templates/NOTES.txt

然后修改 templates/deployment.yaml 模板文件:

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ghost
spec:
  selector:
    matchLabels:
      app: ghost-app
  replicas:  .Values.replicaCount 
  template:
    metadata:
      labels:
        app: ghost-app
    spec:
      containers:
        - name: ghost-app
          image:  .Values.image 
          ports:
            - containerPort: 2368
          env:
            - name: NODE_ENV
              value:  .Values.node_env | default "production" 
            - if .Values.url 
            - name: url
              value: http:// .Values.url 
            - end 

这和我们前面的资源清单文件非常类似,只是将 replicas 的值使用 .Values.replicaCount 模板来进行替换了,表示会用 replicaCount 这个 Values 值进行渲染,然后还可以通过设置环境变量来配置 Ghost,同样修改 templates/service.yaml 模板文件的内容:

# templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: ghost
spec:
  selector:
    app: ghost-app
  type:  .Values.service.type 
  ports:
    - protocol: TCP
      targetPort: 2368
      port:  .Values.service.port 
      - if (and (or (eq .Values.service.type "NodePort") (eq .Values.service.type "LoadBalancer")) (not (empty .Values.service.nodePort))) 
      nodePort:  .Values.service.nodePort 
      - else if eq .Values.service.type "ClusterIP" 
      nodePort: null
      - end 

同样为了能够兼容多个场景,这里我们允许用户来定制 Service 的 type,如果是 NodePort 类型则还可以配置 nodePort 的值,不过需要注意这里的判断,因为有可能即使配置为 NodePort 类型,用户也可能不会主动提供 nodePort,所以这里我们在模板中做了一个条件判断:

- if (and (or (eq .Values.service.type "NodePort") (eq .Values.service.type "LoadBalancer")) (not (empty .Values.service.nodePort))) 

需要 service.type 为 NodePort 或者 LoadBalancer 并且 service.nodePort 不为空的情况下才会渲染 nodePort。

然后最重要的就是要在 values.yaml 文件中提供默认的 Values 值,如下所示是我们提供的默认的 Values 值:

# values.yaml
replicaCount: 1
image: ghost
node_env: production
url: ghost.k8s.local

service:
  type: NodePort
  port: 80

接着,确保我们在前文我让你们创建的目录下,做语法检查,如下为正常,不正常再改。

$ helm lint mychart/
==> Linting .
[INFO] Chart.yaml: icon is recommended

1 chart(s) linted, no failures

然后我们可以使用 helm template 命令来渲染我们的模板输出结果:

$  helm template --debug my-ghost
install.go:178: [debug] Original chart version: ""
install.go:195: [debug] CHART PATH: /Users/ych/devs/workspace/yidianzhishi/course/k8strain3/content/helm/manifests/my-ghost

---
# Source: my-ghost/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: ghost
spec:
  selector:
    app: ghost-app
  type: NodePort
  ports:
    - protocol: TCP
      targetPort: 2368
      port: 80
---
# Source: my-ghost/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ghost
spec:
  selector:
    matchLabels:
      app: ghost-app
  replicas: 1
  template:
    metadata:
      labels:
        app: ghost-app
    spec:
      containers:
        - name: ghost-app
          image: ghost
          ports:
            - containerPort: 2368
          env:
            - name: NODE_ENV
              value: production
            - name: url
              value: http://ghost.k8s.local

面的渲染结果和我们上面的资源清单文件基本上一致了,只是我们现在的灵活性更大了,比如可以控制环境变量、服务的暴露方式等等。


命名模板

虽然现在我们可以使用 Helm Charts 模板来渲染安装 Ghost 了,但是上面我们的模板还有很多改进的地方,比如资源对象的名称我们是固定的,这样我们就没办法在同一个命名空间下面安装多个应用了,所以一般我们也会根据 Chart 名称或者 Release 名称来替换资源对象的名称。

前面默认创建的模板中包含一个 _helpers.tpl 的文件,该文件中包含一些和名称、标签相关的命名模板,我们可以直接使用即可。

然后我们可以将 Deployment 的名称和标签替换掉:

apiVersion: apps/v1
kind: Deployment
metadata:
  name:  template "my-ghost.fullname" . 
  labels:
 include "my-ghost.labels" . | indent 4 
spec:
  selector:
    matchLabels:
 include "my-ghost.selectorLabels" . | indent 6 
  replicas:  .Values.replicaCount 
  template:
    metadata:
      labels:
 include "my-ghost.selectorLabels" . | indent 8 
    spec:
      containers:
        - name: ghost-app
          image:  .Values.image 
          ports:
            - containerPort: 2368
          env:
            - name: NODE_ENV
              value:  .Values.node_env | default "production" 
            - if .Values.url 
            - name: url
              value: http:// .Values.url 
            - end 

为 Deployment 增加 label 标签,同样 labelSelector 中也使用 my-ghost.selectorLabels 这个命名模板进行替换,同样对 Service 也做相应的改造:

apiVersion: v1
kind: Service
metadata:
  name:  template "my-ghost.fullname" . 
  labels:
 include "my-ghost.labels" . | indent 4 
spec:
  selector:
 include "my-ghost.selectorLabels" . | indent 4 
  type:  .Values.service.type 
  ports:
    - protocol: TCP
      targetPort: 2368
      port:  .Values.service.port 
      - if (and (or (eq .Values.service.type "NodePort") (eq .Values.service.type "LoadBalancer")) (not (empty .Values.service.nodePort))) 
      nodePort:  .Values.service.nodePort 
      - else if eq .Values.service.type "ClusterIP" 
      nodePort: null
      - end 

现在我们可以再使用 helm template 渲染验证结果是否正确:

$ helm template --debug my-ghost
# 具体结果就不展示了,太多字数了。

版本兼容

于 Kubernetes 的版本迭代非常快,所以我们在开发 Chart 包的时候有必要考虑到对不同版本的 Kubernetes 进行兼容,最明显的就是 Ingress 的资源版本。Kubernetes 在 1.19 版本为 Ingress 资源引入了一个新的 API:networking.k8s.io/v1,这与之前的 networking.k8s.io/v1beta1 beta 版本使用方式基本一致,但是和前面的 extensions/v1beta1 这个版本在使用上有很大的不同,资源对象的属性上有一定的区别,所以要兼容不同的版本,我们就需要对模板中的 Ingress 对象做兼容处理。

创建ingress对象,确保你已经安装了ingress controller组件

新版本的资源对象格式如下所示:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: minimal-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
  - http:
      paths:
      - path: /testpath
        pathType: Prefix
        backend:
          service:
            name: test
            port:
              number: 80

而旧版本的资源对象格式如下:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: minimal-ingress
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - http:
      paths:
      - path: /testpath
        backend:
          serviceName: test
          servicePort: 80

现在我们再为 Ghost 添加一个 Ingress 的模板,新建 templates/ingress.yaml 模板文件,先添加一个 v1 版本的

Ingress 模板:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ghost
spec:
  ingressClassName: nginx
  rules:
  - host: ghost.k8s.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: ghost
            port:
              number: 80

然后同样将名称和服务名称这些使用模板参数进行替换:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name:  template "my-ghost.fullname" . 
  labels:
 include "my-ghost.labels" . | indent 4 
spec:
  ingressClassName: nginx
  rules:
  - host:  .Values.url 
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name:  template "my-ghost.fullname" . 
            port:
              number:  .Values.service.port 

然后接下来我们来兼容下其他的版本格式,这里需要用到 Capabilities 对象,在 Chart 包的 _helpers.tpl 文件中添加几个用于判断集群版本或 API 的命名模板:

/* Allow KubeVersion to be overridden. */
- define "my-ghost.kubeVersion" -
  - default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -
- end -

/* Get Ingress API Version */
- define "my-ghost.ingress.apiVersion" -
  - if and (.Capabilities.APIVersions.Has "networking.k8s.io/v1") (semverCompare ">= 1.19-0" (include "my-ghost.kubeVersion" .)) -
      - print "networking.k8s.io/v1" -
  - else if .Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" -
    - print "networking.k8s.io/v1beta1" -
  - else -
    - print "extensions/v1beta1" -
  - end -
- end -

/* Check Ingress stability */
- define "my-ghost.ingress.isStable" -
  - eq (include "my-ghost.ingress.apiVersion" .) "networking.k8s.io/v1" -
- end -

/* Check Ingress supports pathType */
/* pathType was added to networking.k8s.io/v1beta1 in Kubernetes 1.18 */
- define "my-ghost.ingress.supportsPathType" -
  - or (eq (include "my-ghost.ingress.isStable" .) "true") (and (eq (include "my-ghost.ingress.apiVersion" .) "networking.k8s.io/v1beta1") (semverCompare ">= 1.18-0" (include "my-ghost.kubeVersion" .))) -
- end -

上面我们通过 .Capabilities.APIVersions.Has 来判断我们应该使用的 APIVersion,如果版本为 networking.k8s.io/v1,则定义为 isStable,此外还根据版本来判断是否需要支持 pathType 属性,然后在 Ingress 对象模板中就可以使用上面定义的命名模板来决定应该使用哪些属性。

由于有的场景下面并不需要使用 Ingress 来暴露服务,所以首先我们通过一个 ingress.enabled 属性来控制是否需要渲染,然后定义了一个 $apiIsStable 变量,来表示当前集群是否是稳定版本的 API,然后需要根据该变量去渲染不同的属性,比如对于 ingressClass,如果是稳定版本的 API 则是通过 spec.ingressClassName 来指定,否则是通过 kubernetes.io/ingress.class 这个 annotations 来指定。然后这里我们在 values.yaml 文件中添加如下所示默认的 Ingress 的配置数据:

ingress:
  enabled: true
  ingressClass: nginx

(讲真的,这一part我没看太懂,语法啥的只能勉强接收一下,等我下午去把语法啥的学一下再说了。)

现在我们再次渲染 Helm Chart 模板来验证资源清单数据:

$ helm template --debug my-ghost
# 自行测验

从上面的资源清单可以看出是符合我们的预期要求的,在我们安装测试前,一定要确认我们的kubernetes集群是否安装ingress controller,我们可以来安装测试下结果:

$ helm upgrade --install my-ghost ./my-ghost -n default
Release "my-ghost" does not exist. Installing it now.
NAME: my-ghost
LAST DEPLOYED: Wed May 18 19:02:14 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None

$ helm ls -n default
NAME    	NAMESPACE	REVISION	UPDATED                                	STATUS  	CHART         	APP VERSION
my-ghost	default  	1       	2022-05-18 19:02:14.606629268 -0700 PDT	deployed	my-ghost-0.1.0	1.16.0     



$ kubectl get pods -n default
NAME                        READY   STATUS    RESTARTS   AGE
my-ghost-6f698dc49d-ccphv   1/1     Running   0          29s


$ kubectl get svc -n default
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP        163m
my-ghost     NodePort    10.102.53.53   <none>        80:32204/TCP   39s


$ kubectl get ingress -n default
NAME       CLASS   HOSTS             ADDRESS   PORTS   AGE
my-ghost   nginx   ghost.k8s.local             80      49s


正常就可以部署成功 Ghost 了,并且可以通过域名 http://ghost.k8s.local 进行访问了:

当然虚拟机部署要配置域名解析:

echo '192.168.211.51	 ghost.k8s.local' >> /etc/hosts

持久化

上面我们使用的 Ghost 镜像默认使用 SQLite 数据库,所以非常有必要将数据进行持久化,当然我们要将这个开关给到用户去选择,修改 templates/deployment.yaml 模板文件,增加 volumes 相关配置:

# other spec...
spec:
  volumes:
    - name: ghost-data
    - if .Values.persistence.enabled 
      persistentVolumeClaim:
        claimName:  .Values.persistence.existingClaim | default (include "my-ghost.fullname" .) 
    - else 
      emptyDir: 
     end 
  containers:
    - name: ghost-app
      image:  .Values.image 
      volumeMounts:
        - name: ghost-data
          mountPath: /var/lib/ghost/content
      # other spec...

这里我们通过 persistence.enabled 来判断是否需要开启持久化数据,如果开启则需要看用户是否直接提供了一个存在的 PVC 对象,如果没有提供,则我们需要自己创建一个合适的 PVC 对象,如果不需要持久化,则直接使用 emptyDir: 即可,添加 templates/pvc.yaml 模板,内容如下所示:

- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) 
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name:  template "my-ghost.fullname" . 
  labels:
    - include "my-ghost.labels" . | nindent 4 
spec:
  - if .Values.persistence.storageClass 
  storageClassName:  .Values.persistence.storageClass | quote 
  - end 
  accessModes:
  -  .Values.persistence.accessMode | quote 
  resources:
    requests:
      storage:  .Values.persistence.size | quote 
- end -

其中访问模式、存储容量、StorageClass、存在的 PVC 都通过 Values 来指定,增加了灵活性。对应的 values.yaml 配置部分我们可以给一个默认的配置:

## 是否使用 PVC 开启数据持久化
persistence:
  enabled: true
  ## 是否使用 storageClass,如果不适用则补配置
  # storageClass: "xxx"
  ##
  ## 如果想使用一个存在的 PVC 对象,则直接传递给下面的 existingClaim 变量
  # existingClaim: your-claim
  accessMode: ReadWriteOnce  # 访问模式
  size: 1Gi  # 存储容量

定制

除了上面的这些主要的需求之外,还有一些额外的定制需求,比如用户想要配置更新策略,因为更新策略并不是一层不变的,这里和之前不太一样,我们需要用到一个新的函数 toYaml:

- if .Values.updateStrategy 
strategy:  toYaml .Values.updateStrategy | nindent 4 
- end 

意思就是我们将 updateStrategy 这个 Values 值转换成 YAML 格式,并

以上是关于helm 构建 chart的主要内容,如果未能解决你的问题,请参考以下文章

Harbor+Helm Chart构建k8s应用程序打包存储发布的基础环境

helm详解

Helm Chart 多环境多集群交付实践,透视资源拓扑和差异

Kubernetes(k8s)之k8s的应用的包管理工具Helm

K8S helm

Helm