Helm Charts 最佳实践引导您前行

Kubernetes 是容器应用程序中流行的编排工具,它的名字来自希腊语,意思是“领航员”,也就是掌舵的人。 但在任何旅程中,领航员都不可能脱离地图就取得成功。

应用程序的 Helm Chart 就是这个地图,它是一个可以从 Helm Charts 制品库部署的文件集合,描述了一组相关的 K8s 资源。 当 Kubernetes 将容器部署到您的生产环境中时,以最有效的方式制作 Helm Charts 将有助于它通过决策灵活地运行。

但是,正如我在开发公开可用的 K8s Charts 来部署产品时所发现的那样,还是有其他会让我们迷失方向的方法的。 对于每一个拉取请求PR,来自 Helm 社区的反馈帮助引导我找到了 Helm Charts 最佳实践,为运行和更新容器提供了强有力的结果。


了解更多: 10 部 Helm 教程助您开启 Kubernetes 之旅


在编写供社区或客户在生产中使用的 K8s Charts 时,需要考虑一些事项, 其中包括以下几点:

  • 您需要定义哪些依赖项?
  • 您的应用程序是否需要维持持久运行状态?
  • 您将如何通过密钥和权限来处理安全问题?
  • 您将如何控制运行的 kubelet 容器?
  • 您将如何确保您的应用程序正在运行并能够接收调用?
  • 您将如何向外界公布应用程序的服务?
  • 您将如何测试 Chart ?

本指南将提供一些最佳实践,以构建和特例化您的 Helm Charts,帮助 K8s 顺利交付容器化应用程序。


Helm 适合所有人: 获取免费 Helm 指南


开始使用

在开始之前,确保您熟悉开发 Helm Charts 的基本程序。

在本指南中,我们将按照所建议的最佳实践创建一个 Helm Chart,以使用 Express.js 部署一款两层的创建、读取、更新和删除 (CRUD) Mongo 数据库的应用程序。

您可以在 GitHub 的 express-crud 中找到示例应用程序的源代码。

创建和填充 Helm Chart

我们使用 Helm 客户端的 create 命令来创建模板 Helm Chart:

$ helm create express-crud

这会为 express-crud Helm Chart 创建目录结构。

要开始,请更新刚创建的 Chart.yaml 文件中的 Chart 元数据。 确保为 appVersion(要用作 Docker 镜像标记的应用程序版本)、descriptionversionSemVer 2 版本字符串)、sourcesmaintainers icon 添加正确的信息。

apiVersion: v1
appVersion: "1.0.0"
description: A Helm chart for express-crud application
name: express-crud
version: 0.1.0
sources:
- https://github.com/jainishshah17/express-mongo-crud
maintainers:
- name: myaccount
 email: myacount@mycompany.com
icon: https://github.com/mycompany17/mycompany.com/blob/master/app/public/images/logo.jpg
home: https://mycompany.com/

定义依赖项

如果您的应用程序具有依赖项,则您必须在 Helm Chart 的目录结构中创建一个 requirements.yaml 文件来指定它们。 我们的应用程序需要 mongodb 数据库,因此我们必须在我们所创建的 requirements.yaml 文件的依赖项列表中指定它。

本示例的 requirements.yaml 包含

dependencies:
- name: mongodb
 version: 3.0.4
 repository: https://kubernetes-charts.storage.googleapis.com/
 condition: mongodb.enabled

创建 requirements.yaml 文件后,您必须在 Helm 客户端运行依赖项更新命令:

$ helm dep update

创建部署文件

您的 Helm Chart 的部署文件位于 \templates 子目录中,用于指定 K8s 将如何部署容器应用程序。

在开发部署文件时,您需要做出一些关键决策。

Deployment 对象与 StatefulSet 对象

您创建的部署文件将取决于应用程序是需要 K8s 将其作为 Deployment 对象还是作为 StatefulSet 对象进行管理。

Deployment 对象是在 deployment.yaml 文件名中声明的无状态应用程序,可将 kind 参数指定为 deployment

Stateful 对象代表有状态应用程序,与分布式系统一起使用。 它们在 stateless.yaml 文件名中进行声明,可将 kind 参数指定为 stateful

Deployment StatefulSet
Deployment 意味着无状态使用,且具有轻量级的特点。 可在必须处于持久性状态时使用 StatefulSet。 因此,它在持久卷上使用 volumeClaimTemplates,以确保它们可以在组件重启时保持该状态。
如果您的应用程序处于无状态,或者状态可以在启动期间从后端系统构建,则使用 Deployment。 如果您的应用程序处于有状态,或者您想要基于 Kubernetes 部署有状态存储,则使用 StatefulSet。

 

由于此应用程序不需要持久性状态,因此,我使用的是 Deployment 对象。
deployment.yaml 文件已通过 helm create 命令创建。

我们将使用 AppVersion 作为应用程序的 Docker 镜像标签。 这样,我们只需通过更改 Chart.yaml 中的值,即可使用新版应用程序来升级 Helm Chart

image: "{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}"

Secret 与 ConfigMap

您将需要确定哪些凭证或配置数据适合存储为 Secret,哪些可以存储在 ConfigMap 中。

Secret 用于敏感信息,如 K8s 将以加密格式存储的密码。

ConfigMap 是一个文件,包含可能由应用程序共享的配置信息。 ConfigMap 中的信息没有加密,因此不应该包含任何敏感信息。

Secret ConfigMap
将这些信息保存在 Secret 中比逐字地放在 Pod 定义或 Docker 镜像中更安全、更灵活 ConfigMap 允许您将配置制品从镜像内容中分离出来,以保持容器化应用程序的可移植性
用于机密数据 用于非机密数据
使用示例: API 密钥、密码、令牌和 ssh 密钥 使用示例: Log rotator、无机密数据的配置

 

在本例中,我们将允许 Helm 使用镜像提取密钥从私有 Docker 镜像中心中提取 Docker 镜像。

该过程依赖于 Kubernetes 集群拥有密钥,可用于指定制品库管理工具登录凭证。 该密钥可由 kubectl 命令行创建,如:

$ kubectl create secret docker-registry regsecret --docker-server=$DOCKER_REGISTRY_RUL --docker-username=$USERNAME --docker-password=$PASSWORD --docker-email=$EMAIL

然后,在 Helm Chart 的 values.yaml 文件中,您可以将密钥名称传递给值:

imagePullSecrets: regsecret

接下来,您可以利用该密钥,允许 Helm 在 deployment.yaml 中通过这些行访问 Docker 镜像中心:

{{- if .Values.imagePullSecrets }}
 imagePullSecrets:
 - name: {{ .Values.imagePullSecrets }}
{{- end }}

对于应用程序可用的密钥,您应该将该信息直接添加到 values.yaml 中。

例如,要将我们的应用程序配置为使用预先创建的用户和数据库访问 mongodb,请将该信息添加到 values.yaml

mongodb:
 enabled: true
 mongodbRootPassword:
 mongodbUsername: admin
 mongodbPassword: 
 mongodbDatabase: test

请注意,在此,我们没有在 Helm Chart 中硬编码默认凭证。 而是,当没有通过 –set flag 或 values.yaml 提供密码时,我们使用逻辑随机生成密码

我们将使用密钥,通过 deployment.yaml 中的这些行向应用程序传递 mongodb 凭证。

env:
- name: DATABASE_PASSWORD
  valueFrom:
    secretKeyRef:
      name: {{ .Release.Name }}-mongodb
      key: mongodb-password

您可以通过专门的 Init Containers 或通过 Container Lifecycle Hooks 来控制 kubelet 容器的运行。

 

InitContainers Container Lifecycle Hooks
InitContainers 是在应用程序容器之前运行的特殊容器,可以包含应用程序镜像中不存在的实用工具或设置脚本。 容器可以使用 Container Lifecycle Hook 框架来运行由其管理生命周期中的事件所触发的代码。
一个 Pod 可以有一个或多个 Init Container,它们在应用程序容器启动之前运行。

 

一个 Pod 只能有一个 PostStartPreStop 钩子
PostStart 钩子在容器创建后立即执行。 但是,不能保证钩子会在容器 ENTRYPOINT 之前执行。 系统不会将任何参数传递给处理程序。
例如,将使用 ConfigMap/Secrets 挂载的文件移到其他位置。
PreStop 钩子在容器终止之前立即调用。 它会受阻,这意味着它具有同步性,因此,它必须在可发送删除容器的调用之前完成。

例如,正常关闭应用程序

在继续之前,您可以使用 initContainers 添加等待,以检查依赖的微服务是否正常。 您可以使用 PostStart 钩子来更新同一 Pod 中的文件,如通过服务 IP 来更新配置文件

 

在我们的示例中,将这些 initContainers 规格添加到 deployments.yaml 中,以保持应用程序的启动,直到数据库启动并运行。

initContainers:
- name: wait-for-db
 image: "{{ .Values.initContainerImage }}"
 command:
 - 'sh'
 - '-c'
 - >
   until nc -z -w 2 {{ .Release.Name }}-mongodb 27017 && echo mongodb ok;
     do sleep 2;
   done

添加就绪状态和活动探针

通常情况下,最好添加就绪状态和活动探针来检查应用程序的持续运行状况。 如果不这样做,则应用程序运行可能会失败,表面看起来像是在运行,实则不响应调用或查询。

deployment.yaml 文件中的以下行将添加这些探针以执行定期检查:

livenessProbe:
 httpGet:
   path: '/health'
   port: http
 initialDelaySeconds: 60
 periodSeconds: 10
 failureThreshold: 10
readinessProbe:
 httpGet:
   path: '/health'
   port: http
 initialDelaySeconds: 60
 periodSeconds: 10
 failureThreshold: 10

添加 RBAC 支持

当应用程序需要时,这些过程将向 Chart 中添加基于角色的访问控制 (RBAC) 支持。

第 1 步: 创建角色,方法为:在 role.yaml 文件中添加以下内容:
角色只能用于授予对单个命名空间内资源的访问权限。

{{- if .Values.rbac.create }}
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
 labels:
   app: {{ template "express-crud.name" . }}
   chart: {{ template "express-crud.chart" . }}
   heritage: {{ .Release.Service }}
   release: {{ .Release.Name }}
 name: {{ template "express-crud.fullname" . }}
rules:
{{ toYaml .Values.rbac.role.rules }}
{{- end }}

第 2 步: 创建 RoleBinding,方法为:在 rolebinding.yaml 文件中添加以下内容
ClusterRole 可用于授予和“角色”相同的权限,但因为它们在集群范围内,因此也可用于向以下对象授予访问权限:

  • 集群范围内的资源(如节点)
  • 非资源端点(如“/healthz”)
  • 跨所有命名空间的命名空间内资源(如 Pod)
{{- if .Values.rbac.create }}
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
 labels:
   app: {{ template "express-crud.name" . }}
   chart: {{ template "express-crud.chart" . }}
   heritage: {{ .Release.Service }}
   release: {{ .Release.Name }}
 name: {{ template "express-crud.fullname" . }}
subjects:
- kind: ServiceAccount
 name: {{ template "express-crud.serviceAccountName" . }}
roleRef:
 kind: Role
 apiGroup: rbac.authorization.k8s.io
 name: {{ template "express-crud.fullname" . }}
{{- end }}

第 3 步: 创建 ServiceAccount,方法为:serviceaccount.yaml 文件中添加以下内容

Service Account 为在 Pod 中运行的进程提供身份。

{{- if .Values.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
 labels:
   app: {{ template "express-crud.name" . }}
   chart: {{ template "express-crud.chart" . }}
   heritage: {{ .Release.Service }}
   release: {{ .Release.Name }}
 name: {{ template "express-crud.serviceAccountName" . }}
{{- end }}

第 4 步: 使用助手模板设置 ServiceAccount 名称。
方法为:在 _helpers.tpl 文件中添加以下内容

{{/*
Create the name of the service account to use
*/}}
{{- define "express-crud.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{ default (include "express-crud.fullname" .) .Values.serviceAccount.name }}
{{- else -}}
{{ default "default" .Values.serviceAccount.name }}
{{- end -}}
{{- end -}}

添加 Service

现在是时候通过 Service 向外界公布我们的应用程序了。

Service 允许您的应用程序通过 IP 地址接收流量。 通过指定类型,能够以不同的方式公开 Service :

ClusterIP Service 只能由集群内的内部 IP 访问。
NodePort 可从集群外部通过 NodeIP 和 NodePort 访问 Service 。
LoadBalancer 可从集群外部通过外部负载均衡器访问 Service 。 可以进入应用程序。


方法为:将以下内容添加到 service.yaml

apiVersion: v1
kind: Service
metadata:
 name: {{ template "express-crud.fullname" . }}
 labels:
   app: {{ template "express-crud.name" . }}
   chart: {{ template "express-crud.chart" . }}
   release: {{ .Release.Name }}
   heritage: {{ .Release.Service }}
spec:
 type: {{ .Values.service.type }}
 ports:
   - port: {{ .Values.service.externalPort }}
     targetPort: http
     protocol: TCP
     name: http
 selector:
   app: {{ template "express-crud.name" . }}
   release: {{ .Release.Name }}

请注意,在上文中,对于 service type,我们引用了 values.yaml 中的设置:

service:
  type: LoadBalancer
  internalPort: 3000
  externalPort: 80

Values.yaml 摘要

values.yaml 文件中定义众多配置是帮助维护 Helm Chart 的良好实践。

以下是我们的示例 values.yaml 文件,展示了我们为上面讨论的许多特性定义的各种配置:

# Default values for express-mongo-crud.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

## Role Based Access Control
## Ref: https://kubernetes.io/docs/admin/authorization/rbac/
rbac:
 create: true
 role:
   ## Rules to create. It follows the role specification
   rules:
   - apiGroups:
     - ''
     resources:
     - services
     - endpoints
     - pods
     verbs:
     - get
     - watch
     - list

## Service Account
## Ref: https://kubernetes.io/docs/admin/service-accounts-admin/
##
serviceAccount:
 create: true
 ## The name of the ServiceAccount to use.
 ## If not set and create is true, a name is generated using the fullname template
 name:

## Configuration values for the mongodb dependency
## ref: https://github.com/kubernetes/charts/blob/master/stable/mongodb/README.md
##
mongodb:
 enabled: true
 image:
   tag: 3.6.3
   pullPolicy: IfNotPresent
 persistence:
   size: 50Gi
 # resources:
 #  requests:
 #    memory: "12Gi"
 #    cpu: "200m"
 #  limits:
 #    memory: "12Gi"
 #    cpu: "2"
 ## Make sure the --wiredTigerCacheSizeGB is no more than half the memory limit!
 ## This is critical to protect against OOMKill by Kubernetes!
 mongodbExtraFlags:
 - "--wiredTigerCacheSizeGB=1"
 mongodbRootPassword:
 mongodbUsername: admin
 mongodbPassword:
 mongodbDatabase: test
#  livenessProbe:
#    initialDelaySeconds: 60
#    periodSeconds: 10
#  readinessProbe:
#    initialDelaySeconds: 30
#    periodSeconds: 30

ingress:
 enabled: false
 annotations: {}
   # kubernetes.io/ingress.class: nginx
   # kubernetes.io/tls-acme: "true"
 path: /
 hosts:
   - chart-example.local
 tls: []
 #  - secretName: chart-example-tls
 #    hosts:
 #      - chart-example.local

initContainerImage: "alpine:3.6"
imagePullSecrets:
replicaCount: 1

image:
 repository: jainishshah17/express-mongo-crud
 # tag: 1.0.1
 pullPolicy: IfNotPresent

service:
 type: LoadBalancer
 internalPort: 3000
 externalPort: 80

resources: {}
 # We usually recommend not to specify default resources and to leave this as a conscious
 # choice for the user. This also increases chances charts run on environments with little
 # resources, such as Minikube. If you do want to specify resources, uncomment the following
 # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
 # limits:
 #  cpu: 100m
 #  memory: 128Mi
 # requests:
 #  cpu: 100m
 #  memory: 128Mi

nodeSelector: {}

tolerations: []

affinity: {}

测试和安装 Helm Chart

测试 Helm Chart 非常重要,测试方法为:使用 helm lint 命令。

$ helm lint ./

## Output
==> Linting ./
Lint OK

1 chart(s) linted, no failures

使用 helm install 命令,在 Kubernetes 上通过 Helm Chart 部署应用程序。

$ helm install --name test1 ./ 

## Output
NAME:   test1
LAST DEPLOYED: Sat Sep 15 09:36:23 2018
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1beta1/Deployment
NAME           DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
test1-mongodb  1        1        1           0          0s

==> v1beta2/Deployment
test1-express-crud  1  1  1  0  0s

==> v1/Secret
NAME           TYPE    DATA  AGE
test1-mongodb  Opaque  2     0s

==> v1/PersistentVolumeClaim
NAME           STATUS   VOLUME    CAPACITY  ACCESS MODES  STORAGECLASS  AGE
test1-mongodb  Pending  standard  0s

==> v1/ServiceAccount
NAME                SECRETS  AGE
test1-express-crud  1        0s

==> v1/Service
NAME                TYPE          CLUSTER-IP     EXTERNAL-IP  PORT(S)       AGE
test1-mongodb       ClusterIP     10.19.248.205         27017/TCP     0s
test1-express-crud  LoadBalancer  10.19.254.169      80:31994/TCP  0s

==> v1/Role
NAME                AGE
test1-express-crud  0s

==> v1/RoleBinding
NAME                AGE
test1-express-crud  0s

==> v1/Pod(related)
NAME                                READY  STATUS    RESTARTS  AGE
test1-mongodb-67b6697449-tppk5      0/1    Pending   0         0s
test1-express-crud-dfdbd55dc-rdk2c  0/1    Init:0/1  0         0s


NOTES:
1. Get the application URL by running these commands:
     NOTE: It may take a few minutes for the LoadBalancer IP to be available.
           You can watch the status of by running 'kubectl get svc -w test1-express-crud'
  export SERVICE_IP=$(kubectl get svc --namespace default test1-express-crud -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
  echo https://$SERVICE_IP:80

运行上面的 helm install 命令将为负载均衡器生成 External_IP。 您可以使用这个 IP 地址来运行应用程序。

以下是我们的应用程序运行时的样子:

Result

总结

从本示例您可以看到,Helm 是一种通用性极高的系统,让您能够灵活地构建和开发 Chart 。 这样做符合 Helm 社区的惯例,将有助于简化提交 Helm Chart 供公众使用的过程,并使其在您更新应用程序时更容易维护。

可以在 GitHub 上的 express-crud 代码库中找到这个项目示例的完整 Helm Chart,您可以查看这些功能文件,帮您更深入地了解其工作原理。

要了解更多示例,您可以查看我的 Helm Charts 制品库示例,以将产品部署到 Kubernetes。

 

其他资源: