50. Helm 部署应用示例

到这里我们就把 Helm 涉及到的一些知识点基本上和大家讲解完了,今天我们就用一个完整的示例来给大家演示下如何从0开始部署一个 Helm 应用。

大家是否还记得前面我们手动部署的 wordpress 示例,如果现在我们来使用 Helm Chart 来部署 wordpress 应用应该怎么操作呢?

创建 chart

首先同样的我们还是手动创建一个 chart 包,执行下面的创建命令:

  1. $ helm create wordpress
  2. Creating wordpress

然后我们就需要来编写 helm 的模板了,但是应该如何开始呢?这个我们就要去回顾下之前在没有使用 helm 的情况下,我们是怎样部署 wordpress 应用的,我们可以前往github 仓库查看之前应用的资源清单,我们知道 wordpress 是依赖于 mysql 数据库的,所以我们在部署的时候是将 wordpress 和 mysql 两个应用独立部署的,然后在 wordpress 的 Pod 中通过环境变量的形式将 mysql 数据库的链接地址、端口以及数据库名称注入到应用中去的,为了解决服务依赖的问题,我们还用了一个 initContainer 来检查 mysql 数据库是否已经启动,保证 wordpress 应用启动之前能够正常连接。所以第一步我们完全就可以将之前的资源清单文件内容直接照搬过来。

将 Deployment 资源中的 yaml 文件复制到 templates/deployment.yaml 文件中,将 Service 资源中的 yaml 文件内容复制到 templates/service.yaml 文件中,其他的暂时不用考虑。

然后同样的我们可以先使用 debug 命令调试模板是否能够正常渲染:

  1. $ helm install --dry-run --debug .

由于我们之前是将应用安装在 blog 这个 namespace 下面的,所以我们需要在提前添加一个 namespace,但是如果我们在 chart 模板中就将这些资源固定在某个 namespace 下面显然是不合适的,我们在使用helm install命令安装的时候可以通过--namespace参数来指定将我们的应用安装在某个 namespace 下面,所以我们在 chart 模板中都不会带上 namespace 的声明,所以这里我们将 deployment.yaml 和 service.yaml 文件中的 namespace 字段都移除掉。然后执行安装命名:

  1. $ kubectl create ns blog
  2. namespace "blog" created
  3. $ helm install . --namespace blog
  4. NAME: viable-ant
  5. LAST DEPLOYED: Sun Oct 7 00:43:40 2018
  6. NAMESPACE: blog
  7. STATUS: DEPLOYED
  8. RESOURCES:
  9. ==> v1/Service
  10. NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
  11. mysql ClusterIP 10.97.198.88 <none> 3306/TCP 0s
  12. wordpress NodePort 10.100.28.2 <none> 80:31306/TCP 0s
  13. ==> v1beta1/Deployment
  14. NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
  15. mysql-deploy 1 1 1 0 0s
  16. wordpress-deploy 1 1 1 0 0s
  17. ==> v1/Pod(related)
  18. NAME READY STATUS RESTARTS AGE
  19. mysql-deploy-67dfd986dd-86bdx 0/1 ContainerCreating 0 0s
  20. wordpress-deploy-5df954b8c-t2h7t 0/1 Init:0/1 0 0s
  21. NOTES:
  22. 1. Get the application URL by running these commands:
  23. export POD_NAME=$(kubectl get pods --namespace blog -l "app=wordpress,release=viable-ant" -o jsonpath="{.items[0].metadata.name}")
  24. echo "Visit http://127.0.0.1:8080 to use your application"
  25. kubectl port-forward $POD_NAME 8080:80

我们在安装的时候指定了 namespace 参数,我们可以看到 RESOURCES 区域现在已经是我们指点的资源对象了,NOTES 区域的显示是默认的信息,可以暂时忽略,现在我们可以查看下 blog 这个 namespace 下面的资源对象:

  1. $ kubectl get pods -n blog
  2. NAME READY STATUS RESTARTS AGE
  3. mysql-deploy-67dfd986dd-86bdx 1/1 Running 0 5m
  4. wordpress-deploy-5df954b8c-t2h7t 1/1 Running 0 5m
  5. $ kubectl get service -n blog
  6. NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
  7. mysql ClusterIP 10.97.198.88 <none> 3306/TCP 5m
  8. wordpress NodePort 10.100.28.2 <none> 80:31306/TCP 5m

隔一会儿时间等镜像下载完成、Pod 启动起来后就可以通过 NodeIP:31306 去访问下我们部署的服务是否能够正常访问。

wordpress ui

定制数据

尽管现在我们通过上面的方法可以直接使用 helm 来安装我们的应用了,但是完全不能定制化,比如我们希望资源的名称和 helm release 的名称产生关联,比如我希望可以通过参数化的形式去控制应用的资源大小,比如我还希望能够自己去控制数据库的数据持久化,因为在生产环境下面很大的可能是使用 PV/PVC 或者 StorageClass,而不是使用 hostPath。所以其实现在还远远不够。

名称

首先,我们将资源的名称定义成一个命名模板,因为除了 Deployment 里面需要使用,Service 资源里面也可以使用,这样两个地方直接引用同一个命名模板就可以了,我们打开 templates/_helpers.tpl 文件,其实可以看到默认的 partial 文件中就已经有这样的名称的命名模板的定义了:

  1. {{/* vim: set filetype=mustache: */}}
  2. {{/*
  3. Expand the name of the chart.
  4. */}}
  5. {{- define "wordpress.name" -}}
  6. {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
  7. {{- end -}}
  8. {{/*
  9. Create a default fully qualified app name.
  10. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
  11. If release name contains chart name it will be used as a full name.
  12. */}}
  13. {{- define "wordpress.fullname" -}}
  14. {{- if .Values.fullnameOverride -}}
  15. {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
  16. {{- else -}}
  17. {{- $name := default .Chart.Name .Values.nameOverride -}}
  18. {{- if contains $name .Release.Name -}}
  19. {{- .Release.Name | trunc 63 | trimSuffix "-" -}}
  20. {{- else -}}
  21. {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
  22. {{- end -}}
  23. {{- end -}}
  24. {{- end -}}
  25. {{/*
  26. Create chart name and version as used by the chart label.
  27. */}}
  28. {{- define "wordpress.chart" -}}
  29. {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
  30. {{- end -}}

其中wordpress.fullname这个命名模板就是我们需要的,但是我们需要仔细分析下这个命名模板的逻辑:

  • 首先是如果定义了值 fullnameOverride,则直接截取前63个字符(这是因为 kubernetes 的命名机制决定的),然后使用-进行连接
  • 如果没有定义值 fullnameOverride,那么就直接取值 nameOverride(默认是 chart 的名称):
    • 如果值 nameOverride 包含了当前 release 的名称,则同样直接截取前63个字符,用-进行连接
    • 如果值 nameOverride 没包含当前 release 的名称,则将 release 的名称和值 nameOverride 用-进行拼接,然后截取前63个字符作为应用的名称

所以现在我们将 templates 目录下面的 deployment.yaml 和 service.yaml 文件中关于 wordpress 的 name 替换成命名模板:

  1. apiVersion: apps/v1beta1
  2. kind: Deployment
  3. metadata:
  4. name: {{ template "wordpress.fullname" . }}
  5. ......
  6. apiVersion: v1
  7. kind: Service
  8. metadata:
  9. name: {{ template "wordpress.fullname" . }}
  10. ......

然后将 values.yaml 文件中的内容全部清空,添加下面两个值定义:

  1. nameOverride: ""
  2. fullnameOverride: ""

将 templates 目录下面的 ingress.yaml 和 NOTES.txt 文件删除,后面我们再手动添加,然后使用 debug 命令来查看模板是否能够按照我们的要求就行渲染:

  1. $ helm install --dry-run --debug .
  2. helm install --dry-run --debug .
  3. [debug] Created tunnel using local port: '38888'
  4. ......
  5. ---
  6. # Source: wordpress/templates/service.yaml
  7. apiVersion: v1
  8. kind: Service
  9. metadata:
  10. name: mysql
  11. ......
  12. ---
  13. # Source: wordpress/templates/service.yaml
  14. apiVersion: v1
  15. kind: Service
  16. metadata:
  17. name: old-fox-wordpress
  18. ......
  19. ---
  20. # Source: wordpress/templates/deployment.yaml
  21. apiVersion: apps/v1beta1
  22. kind: Deployment
  23. metadata:
  24. name: mysql-deploy
  25. ......
  26. ---
  27. # Source: wordpress/templates/deployment.yaml
  28. apiVersion: apps/v1beta1
  29. kind: Deployment
  30. metadata:
  31. name: old-fox-wordpress
  32. labels:
  33. app: wordpress
  34. ......

我们可以看到 Deployment 和 Service 的名称都是 old-fox-wordpress,这是因为我们没有定义 fullnameOverride 和 nameOverride 这两个值,所以最后渲染的时候就是使用的 release 的名称拼接上 chart 的名称,如果我们用 —set 参数来指定下值 fullnameOverride 呢:

  1. $ helm install --dry-run --debug --set fullnameOverride=mywordpress .
  2. [debug] Created tunnel using local port: '41110'
  3. ......
  4. ---
  5. # Source: wordpress/templates/service.yaml
  6. apiVersion: v1
  7. kind: Service
  8. metadata:
  9. name: mywordpress
  10. ......
  11. ---
  12. # Source: wordpress/templates/deployment.yaml
  13. apiVersion: apps/v1beta1
  14. kind: Deployment
  15. metadata:
  16. name: mywordpress
  17. labels:
  18. app: wordpress
  19. ......

可以看到资源名称被我们指定的值覆盖了,一般情况下面我们还会为我们的资源添加上合适的 labels 标签,比如我们这里可以给 wordpress 的 Deployment 和 Service 都添加上下面的 labels 标签:

  1. apiVersion: apps/v1beta1
  2. kind: Deployment
  3. metadata:
  4. name: {{ template "wordpress.fullname" . }}
  5. labels:
  6. app: {{ .Chart.Name }}
  7. chart: {{ template "wordpress.chart" . }}
  8. release: {{ .Release.Name }}
  9. ......
  10. apiVersion: v1
  11. kind: Service
  12. metadata:
  13. name: {{ template "wordpress.fullname" . }}
  14. labels:
  15. app: {{ .Chart.Name }}
  16. chart: {{ template "wordpress.chart" . }}
  17. release: {{ .Release.Name }}

然后使用 debug 模式查看下模板渲染结果:

  1. $ helm install --dry-run --debug .
  2. [debug] Created tunnel using local port: '40645'
  3. ......
  4. ---
  5. # Source: wordpress/templates/service.yaml
  6. apiVersion: v1
  7. kind: Service
  8. metadata:
  9. name: youngling-clam-wordpress
  10. labels:
  11. app: wordpress
  12. chart: wordpress-0.1.0
  13. release: youngling-clam
  14. ......
  15. ---
  16. # Source: wordpress/templates/deployment.yaml
  17. apiVersion: apps/v1beta1
  18. kind: Deployment
  19. metadata:
  20. name: youngling-clam-wordpress
  21. labels:
  22. app: wordpress
  23. chart: wordpress-0.1.0
  24. release: youngling-clam
  25. ......

有的同学可能已经发现了,我们这个地方的应用是依赖于 mysql 的,那么为什么我们只是把 wordpress 相关的数据来做了定制呢?当然我们也可以在我们的这个 chart 中来定制 mysql,但是这却不是最好的方法,最好的方法是让我们去依赖一个独立的 mysql chart,这样可以将 wordpress 和 mysql 之间的耦合关系降低,后面我们再和大家来看看怎样解耦。

为了不影响对 wordpress 的操作,我们可以临时将 templates 目录下面的 mysql 的资源对象单独提取出来,比如我们这里统一放到一个叫 mysql.yaml 的资源文件中,现在我们的结构就是这样的了:

  1. $ tree .
  2. .
  3. ├── charts
  4. ├── Chart.yaml
  5. ├── templates
  6. ├── deployment.yaml
  7. ├── _helpers.tpl
  8. ├── mysql.yaml
  9. └── service.yaml
  10. └── values.yaml
  11. 2 directories, 6 files

镜像

现在我们使用的镜像还是固定的wordpress:latest,为了方便其他人使用,我们在编写 chart 包的时候会提供一个定制参数值,可以自由指定使用的镜像,包括 tag 版本。我们可以先去添加 values.yaml 文件中的内容:

  1. nameOverride: ""
  2. fullnameOverride: ""
  3. ## 官方 WordPress 镜像
  4. ## 引用:https://hub.docker.com/r/library/wordpress/tags/
  5. image:
  6. registry: docker.io
  7. repository: wordpress
  8. tag: 4.9.8
  9. ## 指定一个 imagePullPolicy
  10. ## 如果镜像 tag 是'latest',则默认是'Always',否则设置'IfNotPresent'
  11. ## 引用: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images
  12. ##
  13. pullPolicy: IfNotPresent
  14. ## 指定一组 imagePullSecretes[可选]
  15. ## Secrets 必须在 namespace 下面手动创建
  16. ## 引用: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
  17. ##
  18. # pullSecrets:
  19. # - myRegistrKeySecretName

我们在 values.yaml 文件中添加了一个 image 的对象,里面包含仓库地址、镜像名称、镜像版本,这是因为一个标准的镜像就包含这3个部分,每一个部分都是可能被定制的,然后指定一个镜像拉取策略的参数 imagePullPolicy,还不算完,为什么呢?如果我们要使用的镜像是一个私有仓库的镜像怎么办?所以我们这里还预留了一个参数:pullSecrets,用来指定私有仓库地址的 Secrets,现在我们再去修改 templates/deployment.yaml 文件就简单很多了:

  1. apiVersion: apps/v1beta1
  2. kind: Deployment
  3. metadata:
  4. name: {{ template "wordpress.fullname" . }}
  5. labels:
  6. app: {{ .Chart.Name }}
  7. chart: {{ template "wordpress.chart" . }}
  8. release: {{ .Release.Name }}
  9. spec:
  10. strategy:
  11. type: RollingUpdate
  12. rollingUpdate:
  13. maxSurge: 1
  14. maxUnavailable: 1
  15. template:
  16. metadata:
  17. labels:
  18. app: {{ .Chart.Name }}
  19. chart: {{ template "wordpress.chart" . }}
  20. release: {{ .Release.Name }}
  21. spec:
  22. initContainers:
  23. - name: init-db
  24. image: busybox
  25. imagePullPolicy: IfNotPresent
  26. command: ['sh', '-c', 'until nslookup mysql;do echo waiting for mysql service; sleep 2; done;']
  27. {{- if .Values.image.pullSecrets }}
  28. imagePullSecrets:
  29. {{- range .Values.image.pullSecrets }}
  30. - name: {{ . }}
  31. {{- end }}
  32. {{- end }}
  33. containers:
  34. - name: {{ template "wordpress.fullname" . }}
  35. image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag }}"
  36. imagePullPolicy: {{ .Values.image.pullPolicy | quote }}
  37. ......

我们首先判断是否存在值pullSecrets,如果存在,则将 Secrets 循环渲染出来,然后是容器的名称还是使用命名模板 wordpress.fullname 的定义,然后就是 image 的地址以及 imagePullPolicy,这样我们就完成了对镜像的定制,默认的值直接写入到 values.yaml 文件中,现在我们使用 debug 命令查看下模板渲染的结果:

  1. helm install --dry-run --debug .
  2. [debug] Created tunnel using local port: '46735'
  3. ......
  4. ---
  5. # Source: wordpress/templates/deployment.yaml
  6. apiVersion: apps/v1beta1
  7. kind: Deployment
  8. metadata:
  9. name: eponymous-narwhal-wordpress
  10. labels:
  11. app: wordpress
  12. chart: wordpress-0.1.0
  13. release: eponymous-narwhal
  14. spec:
  15. strategy:
  16. type: RollingUpdate
  17. rollingUpdate:
  18. maxSurge: 1
  19. maxUnavailable: 1
  20. template:
  21. metadata:
  22. labels:
  23. app: wordpress
  24. chart: wordpress-0.1.0
  25. release: eponymous-narwhal
  26. spec:
  27. initContainers:
  28. - name: init-db
  29. image: busybox
  30. imagePullPolicy: IfNotPresent
  31. command: ['sh', '-c', 'until nslookup mysql;do echo waiting for mysql service; sleep 2; done;']
  32. containers:
  33. - name: eponymous-narwhal-wordpress
  34. image: "docker.io/wordpress:4.9.8"
  35. imagePullPolicy: "IfNotPresent"
  36. ......

假如现在我们的镜像地址是 youdianzhishi.com/wordpress:4.9,那么我们在安装的就可以覆盖 image 对象中的相关参数了:

  1. $ helm install --dry-run --debug --set image.registry=youdianzhishi.com --set image.tag=4.9 .
  2. [debug] Created tunnel using local port: '36449'
  3. ......
  4. # Source: wordpress/templates/deployment.yaml
  5. apiVersion: apps/v1beta1
  6. kind: Deployment
  7. metadata:
  8. name: peddling-prawn-wordpress
  9. labels:
  10. app: wordpress
  11. chart: wordpress-0.1.0
  12. release: peddling-prawn
  13. spec:
  14. strategy:
  15. type: RollingUpdate
  16. rollingUpdate:
  17. maxSurge: 1
  18. maxUnavailable: 1
  19. template:
  20. metadata:
  21. labels:
  22. app: wordpress
  23. chart: wordpress-0.1.0
  24. release: peddling-prawn
  25. spec:
  26. initContainers:
  27. - name: init-db
  28. image: busybox
  29. imagePullPolicy: IfNotPresent
  30. command: ['sh', '-c', 'until nslookup mysql;do echo waiting for mysql service; sleep 2; done;']
  31. containers:
  32. - name: peddling-prawn-wordpress
  33. image: "youdianzhishi.com/wordpress:4.9"
  34. imagePullPolicy: "IfNotPresent"
  35. ......

我们可以看到镜像地址是不是就被替换了,当然如果你需要覆盖的值比较多,最好还是通过指定一个 yaml 文件来覆盖默认的这些 values 值。

健康检查、资源限制

按照我们前面的资源文件声明,接下来我们就应该定制健康检查部分和资源限制部分,同样还是先添加模板值:(values.yaml)

  1. ......
  2. ## liveness 和 readliness probes 配置
  3. ## 引用: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes)
  4. livenessProbe:
  5. initialDelaySeconds: 120
  6. periodSeconds: 10
  7. timeoutSeconds: 5
  8. failureThreshold: 6
  9. successThreshold: 1
  10. readinessProbe:
  11. initialDelaySeconds: 30
  12. periodSeconds: 10
  13. timeoutSeconds: 5
  14. failureThreshold: 6
  15. successThreshold: 1

我们在 values.yaml 文件中添加了 livenessProbe 和 readinessProbe 这两个对象,里面都是健康检测的相关属性,然后我们需要将这些值都嵌入到模板中去,按照以前的方法我们是不是一个属性一个属性的添加,但是这样太麻烦了,我们可以用一个函数toYaml将这两个对象中的属性一次性全部输出到模板中去:

  1. containers:
  2. - name: {{ template "wordpress.fullname" . }}
  3. image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag }}"
  4. imagePullPolicy: {{ .Values.image.pullPolicy | quote }}
  5. ports:
  6. - name: wdport
  7. containerPort: 80
  8. livenessProbe:
  9. tcpSocket:
  10. port: 80
  11. {{ toYaml .Values.livenessProbe | indent 10 }}
  12. readinessProbe:
  13. tcpSocket:
  14. port: 80
  15. {{ toYaml .Values.readinessProbe | indent 10 }}

可以看到我们这里使用的方法是{{ toYaml .Values.livenessProbe | indent 10 }},至于为什么是保留10个空格呢?这个就需要大家对这些对象的层级关系要比较清楚才行。现在我们用 debug 来查看下模板渲染后的结果呢:

  1. helm install --dry-run --debug .
  2. [debug] Created tunnel using local port: '40467'
  3. ......
  4. containers:
  5. - name: mouthy-crocodile-wordpress
  6. image: "docker.io/wordpress:4.9.8"
  7. imagePullPolicy: "IfNotPresent"
  8. ports:
  9. - name: wdport
  10. containerPort: 80
  11. livenessProbe:
  12. tcpSocket:
  13. port: 80
  14. failureThreshold: 6
  15. initialDelaySeconds: 120
  16. periodSeconds: 10
  17. successThreshold: 1
  18. timeoutSeconds: 5
  19. readinessProbe:
  20. tcpSocket:
  21. port: 80
  22. failureThreshold: 6
  23. initialDelaySeconds: 30
  24. periodSeconds: 10
  25. successThreshold: 1
  26. timeoutSeconds: 5
  27. ......

可以看到符合我们的渲染结果的。然后就是我们的 resource 资源部分,因为并不是所有的应用资源消耗情况都是一样的,还需要结合自己的集群去进行定制

  1. ## Configure resource requests and limits
  2. ## ref: http://kubernetes.io/docs/user-guide/compute-resources/
  3. ##
  4. resources:
  5. requests:
  6. memory: 512Mi
  7. cpu: 300m

todo……