前文我们已经将Kubernetes集群搭建好,并且将应用容器化。然而作为开发者,在项目部署中依然觉得直接去管理各种Kubernetes对象是件非常费时且枯燥的事情(要直接去管理各种Kubernetes资源及相关联的标签与选择算符、命名空间等)。
很多时候,当某处繁琐且重复时,便是优化开始的地方。 —— 皮皮叨叨
Helm是什么
Helm是对Kubernetes资源部署进一步的抽象,通俗地讲就是个Kubernetes包管理器,相当于Linux中的yum或apt。Helm通过直接部署将Kubernetes资源打包后的Charts,能够简化微服务应用的部署,并且能方便地更新与回滚应用。Helm Chart通过模板的方式部署应用程序。
Helm Chart具有如下目录结构:
YOUR-CHART-NAME/
|
|- .helmignore
|
|- Chart.yaml
|
|- values.yaml
|
|- charts/
|
|- templates/
其中:
.helmignore
:声明所有在Chart打包时需忽略的文件,类似.gitignore功能。Chart.yaml
:存放该Chart的基本信息,如Chart名,版本号等values.yaml
:定义所有需要注入模板中的值,该文件中的值可被命令行参数覆盖。templates
:此文件夹存放该Chart所需要的Kubernetes资源清单(包括但不限于service、ingress、deployment等),其中的yaml文件使用golang 模板编写,value可以通过Values.yaml中定义的值注入。
小试牛刀(使用Helm安装WordPress)
-
安装Helm:
sudo snap install helm --classic # 或 microk8s.enable helm
-
新增环境变量(KUBECONFIG)
安装完后直接运行helm ls:
会报“Kubernetes集群不可达”的错误。这是因为:helm v3版本不再需要Tiller,而是直接访问ApiServer来与k8s交互,通过环境变量KUBECONFIG来读取存有ApiServre的地址与token的配置文件地址,默认地址为~/.kube/config。而microk8s的相关配置文件路径为/var/snap/microk8s/current/credentials/client.config。解决方法:
# 在.bashrc文件末尾追加新的KUBECONFIG echo export KUBECONFIG=/var/snap/microk8s/current/credentials/client.config >> ~/.bashrc # 执行.bashrc source .bashrc
重新运行helm ls:
可以看到helm已经能于Kubernetes集群正常交互。
-
安装WordPress
# 添加远程helm chart仓库 helm repo add bitnami https://charts.bitnami.com/bitnami # 安装WordPress helm install wordpress bitnami/wordpress \ --namespace=wordpress \ --set wordpressUsername=your-name,wordpressPassword=your-pwd,mariadb.mariadbRootPassword=db-root-pwd,ingress.enabled=true,ingress.hostname=your.domain.name
参数含义及更多参数请参考:bitnami/wordpress。
通过运行
helm ls -n wordpress
可以看到wordpress已经部署(-n 代表namespace)。能看到部署的WordPress已经成功运行,就这么简单,一两条命令的功夫就能拥有个人专属博客站点,Amazing!!!
通过kubectl get命令,可以看到Helm在底下帮我们创建好了WordPress站点所需的各种资源,Cool~
那么Helm究竟使用了什么黑魔法呢?下面我们将通过为容器化后的应用创建Helm Chart来揭开其神秘面纱~
为应用程序定制Helm Chart
上面WordPress安装实质上是通过Helm安装远程仓库经过打包后的Chart实现的,在实际业务场景中我们也可以为应用程序定制Helm Chart。
-
创建Helm Chart
helm create
可以看到我们已经成功创建了Helm Chart:
-
定制Helm Chart
上面我们已经生成了一个通用的Chart,对于一些更为复杂的应用我们也可以在此基础上进行定制。基本操作流程就是根据需求修改或新增templates目录下各Kubernetes资源声明模板文件,在其中通过golang template语法注入values.yaml文件中定义的值,具有非常大的灵活性。
举个栗子🌰,对于一般的API应用我们常常需要部署Job或者CronJob以执行特定任务,而且需要将kubernetes Secrets或kubernetes ConfigMaps注入到应用程序容器的环境变量中。
除了工作负载Deployment的模板以外,我们还需要创建Job或者CronJob相关的模板(当然你也可以将三者放到一个模板中,再通过values.yaml中传入条件去生成对应的Kubernetes声明文件)。
我们发现上述多个工作负载模板中会有很多重复的内容(如容器的环境变量定义),对于这些可复用的模块我们可以将其在_helpers.tpl文件中新增声明:
{{/* Add following code to templates/_helpers.tpl */}} {{/* Create container env to use */}} {{- define ".helm.container.env" -}} env: - name: MONGODB_HOST valueFrom: configMapKeyRef: name: your-api key: mongodb_host - name: MONGODB_DATABASE valueFrom: configMapKeyRef: name: your-api key: mongodb_database - name: MONGODB_USERNAME valueFrom: secretKeyRef: name: your-api key: mongodb_username - name: MONGODB_PASSWORD valueFrom: secretKeyRef: name: your-api key: mongodb_password - name: NODE_ENV value: {{ default "production" .Values.nodeEnv }} {{- end -}}
templates/deployment.yaml
模板文件:{{- if eq .Values.workload.kind "Deployment" }} apiVersion: apps/v1 kind: Deployment metadata: name: {{ include ".helm.fullname" . }} labels: {{- include ".helm.labels" . | nindent 4 }} spec: replicas: {{ .Values.replicaCount }} strategy: type: {{ .Values.updateStrategy.type }} rollingUpdate: maxSurge: {{ .Values.updateStrategy.rollingUpdate.maxSurge }} maxUnavailable: {{ .Values.updateStrategy.rollingUpdate.maxUnavailable }} selector: matchLabels: {{- include ".helm.selectorLabels" . | nindent 6 }} template: metadata: labels: {{- include ".helm.selectorLabels" . | nindent 8 }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include ".helm.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: - name: {{ .Chart.Name }} securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" imagePullPolicy: {{ .Values.image.pullPolicy }} {{- include ".helm.container.env" . | nindent 10 }} {{- if .Values.workload.isGeneralDeployment }} ports: - name: http containerPort: 3000 protocol: TCP livenessProbe: httpGet: path: /api-health-check-endpoint port: http readinessProbe: httpGet: path: /api-health-check-endpoint port: http {{- else }} command: {{ .Values.workload.command }} args: {{ .Values.workload.args }} {{- end }} resources: {{- toYaml .Values.resources | nindent 12 }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} {{- end }}
templates/cronJob.yaml
文件{{- if eq .Values.workload.kind "CronJob" }} apiVersion: batch/v1beta1 kind: CronJob metadata: name: {{ include ".helm.fullname" . }} labels: {{- include ".helm.labels" . | nindent 4 }} spec: schedule: {{ .Values.workload.schedule }} jobTemplate: spec: template: metadata: labels: {{- include ".helm.selectorLabels" . | nindent 12 }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 12 }} {{- end }} serviceAccountName: {{ include ".helm.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 12 }} containers: - name: {{ .Chart.Name }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" imagePullPolicy: {{ .Values.image.pullPolicy }} {{- include ".helm.container.env" . | nindent 14 }} command: {{ .Values.workload.command }} args: {{ range .Values.workload.args }} - {{ . }} {{ end }} restartPolicy: OnFailure {{- end }}
templates/job.yaml
文件{{- if eq .Values.workload.kind "Job" }} apiVersion: batch/v1 kind: Job metadata: name: {{ include ".helm.fullname" . }} labels: {{- include ".helm.labels" . | nindent 4 }} spec: template: metadata: labels: {{- include ".helm.selectorLabels" . | nindent 8 }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include ".helm.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: - name: {{ .Chart.Name }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" imagePullPolicy: {{ .Values.image.pullPolicy }} {{- include ".helm.container.env" . | nindent 10 }} command: {{ .Values.workload.command }} args: {{ range .Values.workload.args }} - {{ . }} {{ end }} restartPolicy: Never {{- end }}
templates/service.yaml
文件:{{- if and .Values.workload.isGeneralDeployment (eq .Values.workload.kind "Deployment") -}} apiVersion: v1 kind: Service metadata: name: {{ include ".helm.fullname" . }} labels: {{- include ".helm.labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} ports: - port: {{ .Values.service.port }} targetPort: http protocol: TCP name: http selector: {{- include ".helm.selectorLabels" . | nindent 4 }} {{- end -}}
在使用支持外部负载均衡器的云提供商的服务时,设置
type
的值为 LoadBalancer, 将为 Service 提供负载均衡器。由于我们使用自建microk8s集群,LoadBalancer并没有效果,所以需要设置ingress以将Service暴露至公网。templates/ingress.yaml
文件:{{- if .Values.ingress.enabled -}} {{- $fullName := include ".helm.fullname" . -}} {{- $svcPort := .Values.service.port -}} {{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} apiVersion: networking.k8s.io/v1beta1 {{- else -}} apiVersion: extensions/v1beta1 {{- end }} kind: Ingress metadata: name: {{ $fullName }} labels: {{- include ".helm.labels" . | nindent 4 }} {{- with .Values.ingress.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: {{- if .Values.ingress.tls }} tls: {{- range .Values.ingress.tls }} - hosts: {{- range .hosts }} - {{ . | quote }} {{- end }} secretName: {{ .secretName }} {{- end }} {{- end }} rules: {{- range .Values.ingress.hosts }} - host: {{ .host | quote }} http: paths: {{- range .paths }} - path: {{ . }} backend: serviceName: {{ $fullName }} servicePort: {{ $svcPort }} {{- end }} {{- end }} {{- end }}
通过
values.yaml
定义默认值,实际部署时可传参替换。# Default values for .helm. # This is a YAML-formatted file. # Declare variables to be passed into your templates. replicaCount: 1 updateStrategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 1 nodeEnv: production image: repository: your.image.repository/path tag: latest pullPolicy: IfNotPresent imagePullSecrets: [{ name: your-image-registry }] nameOverride: "your-app-name" fullnameOverride: "your-app-name" workload: # Support Deployment,Job,CronJob kind: Deployment isGeneralDeployment: true command: ["yarn"] args: ["start"] serviceAccount: # Specifies whether a service account should be created create: false annotations: {} podSecurityContext: {} securityContext: {} service: type: ClusterIP port: 80 ingress: enabled: true annotations: kubernetes.io/ingress.class: nginx kubernetes.io/tls-acme: "true" hosts: - host: your.domain.name paths: - '/' tls: [] resources: {} nodeSelector: {} tolerations: [] affinity: {}
-
启用Ingress控制器
为了让 Ingress 资源工作,集群必须有一个正在运行的 Ingress 控制器。
Ingress控制器
有很多种,我们将使用Kubernetes官方维护的nginx ingress控制器,在microk8s中启用ingress控制器非常简单:microk8s.enable ingress # 将启用nginx ingress控制器
-
部署定制的Helm Chart
helm upgrade -f values.yaml --install --namespace=default --set nodeEnv=staging,workload.kind=Job,workload.args={start-your-job,additional-args},replicaCount=2 your-app-name .
其中:
- -f : 默认value文件。
- --namespace : 部署到某命名空间(对应命名空间需要在Kubernetes集群中存在)
- --install : 若命名为
your-app-name
的helm应用不存在则安装,否则更新。 - --set : 替换默认value的值。
- your-app-name : helm应用名称(方便管理)
- . : Helm Chart 所在根目录
PS: 后续若有需要可以将定制的Helm Chart进行打包,并推送到指定远程仓库。
小结
Helm大大减少了向Kubernetes集群部署应用程序的复杂性,让我们能够快速的部署应用程序,同时也可通过远程的Chart仓库直接安装所需应用。编写定制的Chart也比较直接,需要对Kubernetes资源对象以及golang template有所了解。通过对Helm的应用,能更好地提高开发者的工作效率,以将更多精力聚焦在业务代码的开发上。