一、背景

Kubernetes是由谷歌开源的容器集群管理系统,是目前最为流行、成为事实标准的旨在自动部署、扩展和运行应用容器的开源平台。我们知道,容器(Container)的本义是“集装箱”,而Kubernetes的名字则来源于希腊语中的“舵手”。正如名字所预示的,我们希望Kubernetes这个舵手能够帮助我们把装满集装箱(应用容器)的货轮(容器集群)顺利、安稳地驶向目的地。

好舵手的成功驾驶,离不开得心应手的工具——船舵,而应用的Helm charts(Helm的本义就是操作船舵的方向盘),无疑为Kubernetes这个舵手提供了如臂使指的好工具。Helm charts是可以从Helm仓库获取的一个文件集合,用以描述应用对应的一组相互关联的K8s资源。以最有效的方式制作应用的Helm charts,能够助力Kubernetes在将应用容器部署到生产环境时跨越浅滩、勇往直前。当然,正如我们开发那些用以部署产品的公开发布的K8s charts时所发现的那样,某些不当的Helm charts也会导致我们的容器货轮四处漂泊、徘徊不进。

在开发过程中,每次提交拉取请求(PR,Pull Request)时Helm社区提供的反馈,都指引我们采用某些Helm charts的最佳实践,从而使得运维和更新应用容器都获得了理想的效果。当编写为社区或客户使用的公开发布的K8s charts时,以下这些事项是需要认真考虑的:

· 需要定义哪些依赖关系?

· 应用是否需要维护持久化的状态?

· 如何通过私密和权限来处理安全性?

· 如何控制运行中的容器?

· 如何确保应用试运行的,而且能够接收请求?

· 如何对外公开应用提供的服务?

· 如何测试编写的K8s charts?

本文列出了我们开发过程中应用的一些最佳实践,使得开发出的Helm charts能够具有更好的结构化和针对性。希望这些实践能够帮助K8s驾驶您的容器货轮顺利地到达彼岸。

二、起步

在启航之前,希望你能首先熟悉开发Helm charts的基本概念和过程。请参考Helm的官方文档,https://helm.sh/docs/developing_charts/

在本文中,我们将遵循这些最佳实践来创建一个Helm chart,用以部署一个基于Express.js框架的,两层的,针对Mongo数据库进行增、删、改、查(CRUD)的应用。该应用的示例代码位于https://github.com/jainishshah17/express-mongo-crud

三、创建Helm chart并填写chart信息

3.1 创建模板

首先,利用helm客户端的create命令创建我们的helm chart模板:

$ helm create express-crud

该命令将会为名为“express-crud”的Helm chart创建相应的目录结构。

3.2 填写chart信息

修改刚刚生成的模板目录中的Chart.yaml文件,添加描述chart信息的元数据,包括恰当的:

· apiVersion(必选):chart使用的Helm API的版本,目前固定为“v1”;

· 名称name(必选):Helm chart的名称;

· 版本version(必选):Helm chart的版本,是符合语义化版本2.0的版本字符串;

· 描述description(可选):对Helm chart应用范围的描述;

· 应用版本号appVerions(可选):Helm chart包含的应用版本,可用作对应的docker镜像的标签tag(可以不遵循语义化版本的命名规则);

· 源码位置source(可选):项目源代码的位置;

· 维护者maintainer(可选):记录Helm chart维护者的基本信息;

· 图标icon(可选):为Helm chart选个个性化的图标吧。

Chart.yaml里还有其他一些可选项,请参考Helm的文档https://helm.sh/docs/developing_charts/

本例中Chart.yaml的内容如下:

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: http://mycompany.com/

3.3 定义依赖关系

如果这个应用有外部依赖,也就是需要引用其他的Helm charts,那就需要在Helm chart的模板目录中增加requirements.yaml文件,来记录这些依赖关系。由于本例中的应用需要用到mongodb数据库,所以需要创建requirements.yaml文件,把mongodb加入到其中的依赖列表当中。

本例中requirements.yaml的内容如下:

dependencies:

- name: mongodb

version: 3.0.4

repository: https://kubernetes-charts.storage.googleapis.com/

condition: mongodb.enabled

一旦创建了requirements.yaml文件,需要运行Helm客户端的依赖更新命令。该命令验证目前存储的依赖Helm charts是否满足依赖列表中的定义,并按需下载并更新依赖chart包。下载的依赖chart包都存储在模板目录的charts/子目录下。

$ helm dep update

四、创建部署文件

部署文件位于模板目录的templates/子目录,用以指定K8s如何部署应用容器。而在开发部署文件时,需要作出一些关键性的决策:

4.1 部署对象还是状态集对象

创建的部署文件根据K8s管理应用的方式分为部署对象和状态集对象两种。

部署对象是指在名为deployment.yaml的文件中声明的无状态应用,其kind参数被指定为deployment。

状态集对象是指有状态的应用,通常在分布式系统中使用。这些对象在名为stateless.yaml的文件里声明,其kind参数被指定为stateful。

部署对象

状态集对象

部署对象是指无状态应用,通常较为轻量级。

状态集对象用于必须持久化保留状态的场景。通过在持久卷(PV,persistent volumes)上使用volumeClaimTemplates来确保组件重启时能够保持状态。

如果应用是无状态的,或者状态是在启动时由后台系统创建时,使用部署对象。

如果应用是有状态的,或者希望在K8s上部署有状态的存储时,使用状态集对象。

由于本例中的应用不需要保持状态,所以使用了部署对象。文件deployment.yaml已经在执行helm create命令时自动创建了。

在本例中使用appVersion作为应用Docker镜像的标签tag。这使得我们为新版本应用升级Helm chart时,只需修改Chart.yaml文件中的值即可。

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

4.2 私密(Secret)还是配置映射(ConfigMap)

需要确定哪些认证信息或配置数据适合于存储为私密信息,或者做为配置映射数据。

私密信息是如密码等敏感信息,需要K8s以加密格式进行存储。

配置映射(ConfigMap)是记录了被应用所共享的配置数据的一个文件。配置映射中的数据是不加密的,所以不应该包含任何敏感信息。

私密

配置映射

将这些信息处理为私密,比把它们放在K8s的Pod定义,或Docker镜像当中更安全、更灵活

配置映射可以将配置数据从镜像内容中分离出来, 以保持容器化应用的可移植性

用于认证信息

用于非认证信息

示例用法:API密钥、密码、Token和ssh密钥

示例用法: 日志分割设置、无认证数据的配置

在本例中,我们允许Helm利用镜像拉取的私密信息从私有Docker镜像中心拉取Docker镜像。这个过程依赖于K8s集群中包含可用的私密信息,用于指定私有仓库的登录认证信息。这个私密可以通过如下的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就可以利用这个私密的配置项去访问Docker镜像中心,如本例中deployment.yaml中定义的这些行:

{{- 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参数或values.yaml提供时,我们使用逻辑来随机生成密码。

在本例中,我们最终在deployment.yaml文件中的下列行,把mongodb的认证信息通过私密传递给了应用:

env:

- name: DATABASE_PASSWORD

valueFrom:

secretKeyRef:

name: {{ .Release.Name }}-mongodb

key: mongodb-password

4.3 初始化容器还是容器生命周期钩子(Hook)

可以通过特定的初始化容器(InitContainers)或容器生命周期钩子(Container Lifecycle Hooks)来控制kubelet容器的运行。

初始化容器

容器生命周期钩子

初始化容器是在应用容器之前运行的特殊容器,其中包含不在应用镜像之内的工具或设置脚本。

容器可以使用容器生命周期钩子框架来运行由其生命周期中的事件触发的代码。

一个Pod 可以包含一个或多个初始化容器。这些容器在应用程序容器启动之前运行。

一个Pod只能包含一个PostStart hook或PreStop hook

PostStart hook在创建容器后立即执行 。但是,不能保证hook会在容器的入口点(ENTRYPOINT)之前执行。

没有参数传递给hook的处理程序。

例如,将使用配置映射或机密装入的文件移动到不同的位置。

PreStop hook在容器被终止之前立即调用。它是阻塞的,这意味着它是同步的,因此必须先完成该hook的处理才能发送删除容器的调用。

例如,优雅地关闭应用。

可以使用初始化容器添加等待,来检查依赖的微服务是否正常工作,然后再继续运行。

可以使用PostStart hook来更新同一个Pod里的文件,如更新包含服务IP的配置文件。

在本例中,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

4.4 添加就绪探针(Readimess Probe)和存活探针(Liveness Probe)

添加就绪探针和存活探针来检查应用的持续运行状况是很好的实践。如果不这样做,那么应用可能看起来还在运行,但实际已经出错了,不再响应调用或查询。

在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

4.5 添加RBAC支持

当应用需要时,下列过程可以在Helm 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.yaml文件中添加下列内容:

集群角色(ClusterRole)可以和角色一样用来授权相同的权限,但是因为它们是集群范围的,只能用于授权访问到:

· 集群范围的资源(如node)

· 非资源的端点(如“/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: 创建服务账户, 在serviceacount.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:使用helper模板设置服务账户名称

在_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 -}}

4.6 添加服务

现在到了通过服务(Service)来对外公开我们应用的时候了。

服务使得应用能够通过一个IP地址来接收流量。通过指定类型,可以通过不同的方式来公开服务:

集群IP(ClusterIP)

服务只能通过集群中的一个对外IP来访问

节点端口(NodePort)

服务可以通过NodeIP和NodePort从集群外访问

负载均衡器(LoadBalancer)

服务可以通过外部负载均衡器从集群外访问。

可以应用K8s的Ingress来访问服务。

本例中通过在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 }}

注意,在上述代码中,服务的类型引用了values.yaml文件中的配置:

service:

type: LoadBalancer

internalPort: 3000

externalPort: 80

五、values.yaml总览

在values.yaml文件中定义配置是保持Helm charts可维护性的最佳实践。

本例中的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命令,基于Helm chart将应用部署到K8s环境:

$ 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 [http://$SERVICE\_IP:80](http://$SERVICE_IP:80)

运行上述的Helm安装命令将为负载均衡器创建一个外部IP,可以通过这个IP运行我们的应用。

以下就是我们的应用运行起来的样子:

image

七、总结

正如本文示例所展示的,Helm是一个用途极其广泛的系统,使得在架构和开发Helm chart时有很大的灵活性。符合Helm社区惯例的方式将有助于简化提交您的Helm charts供公开使用的过程,并使得这些chart在您更新应用时更易于维护。

本文示例的完整Helm chart保存在GitHub的express-crud项目:https://github.com/jainishshah17/express-mongo-crud。您可以通过查看这些可正常运行的文件,来帮助您更彻底地了解它们的工作原理。


**欢迎观看JFrog杰蛙每周二在线课堂,点击报名:

https://www.bagevent.com/even...**


JFrogChina
4 声望1 粉丝

JFrog的使命是成为一家为世界上所有软件更新提升效能的公司,其驱动力是实现“流式软件”的愿景,即允许二进制制品从开发端无缝、安全地流向边缘应用节点。公司的端到端DevOps平台——JFrog Platform——提供了所需的工...