在我们学习 kubernetes 的过程中,用的最多的是 kubectl 命令行工具,使用 kubectl 工具需要我们编写好各种部署文件,这在生产中是非常不方便的,因此 Helm 这个 kubernetes 包管理工具就应运而生了。
Helm 包管理工具不仅可以为我们安装网上已经成熟的部署库文件,而且可以生产本地部署模板,我们只需要简单改一改,就可以完成一个应用的部署,不需要我们记住那么多的命令和配置。下面我们就看一下 Helm 的使用。
Helm V2/V3 的对比
组件架构对比:
- Helm2:由2个二进制组成:helm(客户端cli)、tiller(服务端)
- Helm3:只有1个二进制组成:helm、移除了tiller
工作原理对比:
- Helm2:helm->tiller->k8s。首先由helm和tiller交互,然后由tiller负责和k8s交互来完成操作。
- Helm3:helm->k8s。由helm命令去调用/root/.kube/config获取k8s权限,然后直接与apiserver交互操作。
Helm 安装的 Chart 的元信息记录到k8s的secret里面,secret名字叫sh.helm.release.v1.${NAME}.v1,另外还有一个serviceaccount,名字叫${NAME}-${CHART名}。helm list命令读取secret才知道安装了哪些包。
Helm 主要模块
- chart:包含应用程序所需要的所有 k8s 资源定义。
- repository:存放chart的仓库,类似于docker的镜像仓库。
- release:chart的实例化,将chart安装到k8s上,就叫做生成一个release。
安装 Helm
因为 helm3 只有一个二进制文件,因此安装非常简单
# 二进制下载地址页面: https://github.com/helm/helm/releases
wget https://get.helm.sh/helm-v3.4.1-linux-amd64.tar.gz
tar zxf helm-v3.4.1-linux-amd64.tar.gz
cp linux-amd64/helm /usr/local/bin/helm
source <(helm completion bash)
添加仓库
命令自动补全,在 /etc/profile 里面增加如下内容:
helm 和 docker 一样有默认的官方仓库,也可以添加第三方仓库和本地仓库
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add stable https://mirror.azure.cn/kubernetes/charts
helm repo add aliyun https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts
helm repo list
更新仓库缓存(helm repo add的时候会获取一次chart列表并缓存,接下来的helm search都是读取本地缓存列表,因此我们需要经常更新缓存)
查看添加了哪些仓库
helm repo update
移除仓库
helm repo remove $仓库名
安装应用
以安装 nginx 为例
# 从官方hub搜索
helm search hub nginx
# 从所有添加的第三方仓库中搜索,支持模糊匹配
helm search repo nginx
# 搜索指定仓库的应用(并没有原生支持,是因为格式都是"仓库名/chart名",所以可以通过"仓库名/"来匹配)
helm search repo bitnami/ | grep nginx
我们安装bitnami仓库的nginx,并指定release名字为myweb
helm install myweb bitnami/nginx
如果要指定命名空间,则加上-n参数
helm install myweb bitnami/nginx -n kube-system
查看安装了哪些库
helm list --all-namespaces
自定义参数
helm支持两种自定义参数的方式
- values.yaml文件
- 命令行参数--set
如果两种同时使用的话,--set的优先级要高于values.yaml,我们可以通过以下命令来查看一个chart支持哪些配置参数
helm show values 仓库名/应用名
helm show values bitnami/nginx
- 使用values.yaml自定义参数来安装
cat > values.yaml <<EOF
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 500m
memory: 256Mi
EOF
helm install -f values.yaml myweb1 bitnami/nginx
- 使用--set自定义参数来安装
# 多个参数用英文逗号,隔开
helm install --set 'resources.limits.cpu=500m,resources.limits.memory=256Mi,resources.requests.cpu=500m,resources.requests.memory=256Mi' myweb2 bitnami/nginx
查看我们在某个chart中自定义了哪些参数
helm get values myweb
升级应用
假设要扩容cpu、memory,则修改values.yaml内容,改为
resources:
limits:
cpu: 1000m
memory: 512Mi
requests:
cpu: 1000m
memory: 512Mi
升级命令
helm upgrade -f values.yaml myweb bitnami/nginx
应用回滚
回滚前先查看历史版本
helm history myweb
获得REVISION号后就可以进行回滚操作
helm rollback myweb 2
卸载应用
卸载并保留历史记录,加上--keep-history(不影响卸载应用,但会保留记录)
helm uninstall myweb1 --keep-history
查看卸载过哪些应用(只有保留记录的才能看到)
helm list --uninstalled --all-namespaces
Chart 目录结构
如果我们不能联网,那么也可以通过helm创建一个本地chart
helm create foo
foo目录结构如下:
├── .helmignore # 编译包的时候忽略哪些文件
├── Chart.yaml # chart信息,包括chart版本、app版本、描述等
├── charts # 放置依赖和子chart
├── values.yaml # 模版的默认参数
└── templates # 存放模版
├── deployment.yaml # k8s deployment模版
├── _helpers.tpl # 定义命名模版(即变量),可在模版里引用,类似编程时候定义一个变量,模版里就可以引用这个变量
├── hpa.yaml # k8s HPA模版
├── ingress.yaml # k8s ingress模版
├── NOTES.txt # 展示在helm install安装完成后看到的NOTES部分,同样支持模版语法
├── serviceaccount.yaml # k8s serviceaccount模版
├── service.yaml # k8s service模版
└── tests # 用于测试release是否运行成功
└── test-connection.yaml # k8s对象,默认是busybox Pod,通过wget测试nginx
templates目录里下划线开头的不会被渲染,tests用于安装完成后,执行检测
测试用例
helm也支持测试用例,例如对于web应用,可以测试http接口是否为200。
那么如何让helm认为是测试使用:helm会读取templates下所有yaml,当metadata带有如下注解时,helm则认为这是测试专用,而不是普通对象。注意,helm不会去认名字为tests的目录,只会认注解,因此如果tests下放置没有注解的k8s对象时会被当作普通对象对待
annotations:
"helm.sh/hook": test
helm install安装后不会自动进行测试,需要手工执行helm test命令,此时会创建k8s对象(如pod)进行测试,若pod执行完毕(状态为Completed,即容器里的程序退出码为0则表示成功)
helm install myfoo foo
helm test myfoo
如果Phase显示Successed表示成功,检测失败则为Failed
Chart 的依赖和父子关系
简介
什么是chart依赖:如果将chart比作rpm包,则chart依赖也相当于rpm包的依赖,当yum install时候会自动安装依赖包。对于helm来说,chart依赖就是在Chart.yaml里设置依赖dependencies哪些仓库的哪些chart,在helm intall时候就会自动把依赖的chart下载到charts目录。也可以手工把依赖包下载下来放到charts目录里。
什么是父子chart:下载、手工放到charts目录里的chart就是子chart,根目录的chart就是父chart。无论是否在Values.yaml里设置了dependencies。
简单来讲:
- 子Chart
- Chart依赖
处理chart依赖的2种方式
- 嵌入式
- 依赖导入式
Chart依赖
我们来测试一下Chart的依赖,首先准备测试环境
mkdir chart
cd chart
helm create web
helm create backend
cd web
编辑Chart.yaml,增加如下内容
dependencies:
- name: backend
repository: file://../backend
version: 0.1.0
- name: mariadb
repository: https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts/
version: 2.1.6
file://是使用本地目录,但是只能用于外部目录,不能用于当前目录下的自定义子目录,比如
file://foo 或 file://charts/foo
# 都会提示错误
处理依赖
helm dependency list
helm dependency update
查看charts目录,应当有2个tgz
另外,除了update,还有build命令
helm dependency build
- update:重新读取Chart.yaml来获取依赖包
- build:要先执行过update才能执行build,否则会提示Chart.lock和Chart.yaml不同步,请先update
父子Chart
怎么确定Chart的父子关系,我们看以下的例子:
helm create foo
cd foo
cd charts
helm create bar
在Chart包foo的子目录charts下创建新的Chart包bar,foo就是父,bar就是子
Helm模板详解
模板基本使用
使用模板的含义就是要创建一个通用的Chart。如果不用模版,就需要为每个应用创建独立的deployment、service等资源对象,那么此时用helm的意义就不大,可以看下面的例子:
helm create mychart
cd mychart
rm -rf templates/*
cd templates
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: mydep
name: mydep
spec:
replicas: 2
selector:
matchLabels:
app: mydep
template:
metadata:
labels:
app: mydep
spec:
containers:
- image: nginx
imagePullPolicy: IfNotPresent
name: nginx
apiVersion: v1
kind: Service
metadata:
labels:
app: mydep
name: mysvc
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: mydep
type: NodePort
测试
kubectl apply -f . # 此时与helm无关
或
helm install myapp ../ # 此时虽然放在模版目录里,但实际上不是模版,因为没用到任何模版标签。
Helm模版,支持很多特性,使用起来非常灵活,本质是基于golang内置的text/template模块。如果想在Chart安装前查看模板渲染后的内容,有2种办法
helm template release名 chart名/目录
helm install --dry-run release名 chart名/目录
- 第一种是很纯粹的看模板渲染效果
- 第二种还有附带install的其他信息
上面自定义参数提到的2种方法,也可以用在helm template
helm template -f values.yaml foo
helm template --set 'aa=bb' foo
helm template foo
上面命令的前提是当前目录下有一个foo目录,如果想看仓库里的,带上仓库名即可
helm template bitnami/nginx
helm template查看的是尚未安装成release的yaml,而不是已经安装成为release的yaml。如果安装后查看yaml的命令是
helm get manifest RELEASE名
模板标签
模板标签:{{}},例如
{{ .Release.Name }}
测试
helm create foo
rm -rf foo/templates/*
cat > foo/templates/a.yaml <<EOF
#{{ .Release.Name }}
EOF
helm template myfoo foo
加上 # 的原因是此时的a.yaml不是合法的k8s资源对象格式
可以看出.Release.Name渲染成了release名,如果想要查看所有的变量/常量
#{{ . }}
模板注释
模板中的注释有2种
- yaml的注释
- 模板的注释
yaml注释
# This is a comment
type: sprocket
模板注释
单行
{{/* a comment */}}
多行
{{/*
This is a comment
*/}}
或
{{- /*
This is a comment
*/}}
内置对象
常用的模版内置对象可查看官方文档
- Release: 该对象描述了版本发布本身。包含了以下对象:
- Values: Values是从文件和用户提供的文件传进模板的。默认为空values.yaml``Values
- Chart:文件内容。 里的任意数据在这里都可以可访问的。比如 会打印出 Chart.yaml``Chart.yaml``{{ .Chart.Name }}-{{ .Chart.Version }}``mychart-0.1.0Chart 指南 中列出了可用字段
- Files: 在chart中提供访问所有的非特殊文件。当你不能使用它访问模板时,你可以访问其他文件。 请查看这个文件访问部分了解更多信息Files.Get 通过文件名获取文件的方法。 .Files.Getconfig.iniFiles.GetBytes 用字节数组代替字符串获取文件内容的方法。 对图片之类的文件很有用Files.Glob 用给定的shell glob模式匹配文件名返回文件列表的方法Files.Lines 逐行读取文件内容的方法。迭代文件中每一行时很有用Files.AsSecrets 使用Base 64编码字符串返回文件体的方法Files.AsConfig 使用YAML格式返回文件体的方法
- Capabilities: 提供关于Kubernetes集群支持功能的信息Capabilities.APIVersions 是一个版本集合Capabilities.APIVersions.Has $version 说明集群中的版本 (e.g., ) 或是资源 (e.g., ) 是否可用batch/v1``apps/v1/DeploymentCapabilities.KubeVersion 和 是Kubernetes的版本号Capabilities.KubeVersion.VersionCapabilities.KubeVersion.Major Kubernetes的主版本Capabilities.KubeVersion.Minor Kubernetes的次版本
- Template: 包含了已经被执行的当前模板信息Template.Name: 当前模板的命名空间文件路径 (e.g. mychart/templates/mytemplate.yaml)Template.BasePath: 当前chart模板目录的路径 (e.g. mychart/templates)
模板函数
使用函数有2种写法:函数名前置和管道,效果是相同的,管道主要用于组合多个函数使用
{{ quote .Release.Name }}
{{ .Release.Name | quote }}
{{ quote .Release.Name | b64enc }}
quote:双引号
helm内置很多模版函数,这里无法一一列举,因此选取几个常用来说明
{{ quote .Release.Name }}
default:默认值
# 当找不到.Values.hello时默认为world
{{ default "world" .Values.hello }}
indent:缩进
{{ indent 4 .Values.hello }}
# 注意,hello必须是string,如果是数字,则报错,此时可以结合quote
{{ quote .Values.hello | indent 4 }}
nindent:先换行,再缩进,并且只能处理字符串,而且不能用#
ReleaseName: {{ indent 2 .Release.Name }}
ReleaseNamespace: {{ nindent 2 .Release.Namespace }}
replicaCount: {{ quote .Values.replicaCount | nindent 2 }}
title:首字母大写
{{ title .Release.Name }}
upper:全部大写
{{ upper .Release.Name }}
b64enc:base64编码
{{ b64enc .Release.Name }}
b64dec:base64解码
{{ b64dec .Release.Name }}
条件判断if
if/else
{{ if PIPELINE }}
# Do something
{{ else if OTHER PIPELINE }}
# Do something else
{{ else }}
# Default case
{{ end }}
if如何判断条件是否为假
- 布尔值false
- 数字0
- 空字符串""
- 不存在的变量/常量,如.Values.xxx(假设xxx不存在)
- 空集合(map, slice, tuple, dict, array),如values.yaml里有imagePullSecrets: []
除此之外其他条件都为真
示例如下:
{{ if false }}
{{ if 0 }}
{{ if "" }}
{{ if .Values.xxx }}
{{ if .Values.imagePullSecrets }}
字符串/数字大小 比较
- eq:字符串相同/数字相同
- ne:字符串不同/数字不同
- lt:字符串(根据编码)前者小于后者/数字前者小于后者
- le:字符串(根据编码)前者小于等于后者/数字前者小于等于后者
- gt:字符串(根据编码)前者大于后者/数字前者大于后者
- ge:字符串(根据编码)前者大于等于后者/数字前者大于等于后者
数字比较,要用float64格式,即带小数点的
示例如下:
{{ if eq 5 5 }}
{{ if eq "5" "5" }}
{{ if gt "a" "b" }}
{{ if gt .Values.replicaCount 2.0 }}
空白符
foo: 123
{{ if eq 2 2 }}
aa: 11
{{ else }}
aa: 22
{{ end }}
bar: 456
遍历with/range
with:用于map结构
假设values.yaml里有这么一段
aa:
xx: 123
yy: 456
用with省去了.Values
{{- with .Values.aa }}
aa:
xx: {{ .xx }}
yy: {{ .yy }}
{{- end }}
可以用toYaml函数进一步简写
{{- with .Values.aa }}
aa:
{{- toYaml . | nindent 2 }}
{{- end }}
range
和with很像,更适合数组而不是map(即hash),因为range会自动将map结构中的value获取出来(忽略key)
data:
{{- range .Values.aa }}
- {{ . }}
{{- end }}
渲染为如下,可以看到少了key
data:
123
456
因此range更适合数组,假设values.yaml有一段
bb:
- 123
- 456
示例如下:
data:
{{- range .Values.bb }}
- {{ . }}
{{- end }}
with里不能用变量
假设values.yaml
aa:
xx: 123
yy: 456
模板
{{- with .Values.aa }}
aa:
xx: {{ .Release.Name }}
yy: {{ .yy }}
{{- end }}
会报错,因为用了with后,.Release.Name变成是从with .Values.aa里找,当然找不到。有2个解决办法:
变量赋值
{{- $releaseName := .Release.Name }}
{{- with .Values.aa }}
aa:
xx: {{ $releaseName }}
yy: {{ .yy }}
{{- end }}
用$符号:$.Release.Name
{{- with .Values.aa }}
aa:
xx: {{ $.Release.Name }}
yy: {{ .yy }}
{{- end }}
range获取键值
data:
{{- range $k, $v := .Values.aa }}
- name: {{ $k }}
value: {{ $v | quote }}
{{- end }}
命名模板
使用define函数定义命名模板,使用template函数引用命名模板
# 定义
{{- define "aa" }}
...
{{- end }}
# 引用
{{ template "aa" }}
可以写在templates的yaml里也可以写在_helper.tpl(templates目录里)作为全局使用
- 直接写在templates的yaml里
{{- define "foo" }}
- name: abc
value: 123
- name: def
value: 456
{{- end }}
kind: ConfigMap
data:
{{- template "foo" }}
- 写在_helper.tpl里
# 以下内容放到templates/_helper.tpl
{{- define "foo" }}
- name: abc
value: 123
- name: def
value: 456
{{- end }}
# 以下内容放到templates/a.yaml
kind: ConfigMap
data:
{{- template "foo" }}
上述2种方法输出结果都是
kind: ConfigMap
data:
- name: abc
value: 123
- name: def
value: 456
template函数不支持管道
{{- define "aaa" -}}
bbb
{{- end }}
xx: {{ template "aaa" | quote }}
会报错,解决办法:改用include函数
{{- define "aaa" -}}
bbb
{{- end }}
xx: {{ include "aaa" . | quote }}
变量/常量/作用域
变量
_helpers.tpl里的{{- define ... -}}
常量
- yaml语法自带的节点定位(Node Anchor)
- 常量:values.yaml里定义的replicaCount: 1
共享常量
- 子chart和父chart之间的常量不能共享,如果需要共享,就需要共享常量,这定义在父chart中
父chart的values.yaml里加入
子chart名:
replicaCount: 2
注意必须是子chart名,即charts目录里存在该名字
接下来,子chart的模板里就可以引用
{{ .Values.replicaCount }}
当helm发现节点名是子chart名时,它会自动拷贝这个常量到子chart的values.yaml中
全局常量
- 共享常量只能把常量共享给一个子chart,如果需要多个子chart之间共享,需要全局常量。在父chart的values.yaml里用global标识
父chart的values.yaml里加入
global:
hello: world
父和子chart的模板里都可以引用
{{ .Values.global.hello }}
搭建私有仓库
安装push插件
yum install git
helm plugin install https://github.com/chartmuseum/helm-push # 比较慢,耐心等
# 如果上面觉得比较慢,可以下载离线宝
# tar zxf helm-push-v0.9.0-allinone.tgz
# helm plugin install helm-push
helm push --help # 新增的push子命令
ChartMuseum
官方提供的私有仓库,功能较少,适合学习测试使用,类似docker的registry
# https://chartmuseum.com/docs/
docker run --rm -tid -p 8080:8080 -v $(pwd)/charts:/charts -e DEBUG=true -e STORAGE=local -e STORAGE_LOCAL_ROOTDIR=/charts chartmuseum/chartmuseum:latest
chmod 777 charts
上传chart方法1:chartmuseum专属方法,不通用
curl --data-binary "@mychart-0.1.0.tgz" http://localhost:8080/api/charts
上传chart方法2:使用push插件(要先添加仓库)
helm repo add chartmuseum http://localhost:8080
helm push foo chartmuseum
查询有哪些chart
curl http://localhost:8080/api/charts # 仅chartmuseum支持
curl http://localhost:8080/index.yaml # 通用,helm repo add读取的是这个文件,helm search repo 是读取缓存到本地的index.yaml
下载chart
helm pull chartmuseum/mychart
Harbor
harbor内置集成了chartmuseum
wget https://github.com/goharbor/harbor/releases/download/v2.1.0/harbor-offline-installer-v2.1.0.tgz
tar zxf harbor-offline-installer-v2.1.0.tgz
cd harbor
cp harbor.yml.tmpl harbor.yml
编辑harbor.yml
1. 将hostname的值改为服务器IP,例如192.168.36.128,如果改成域名,要能解析
2. 注释https部分 # 要在/etc/docker/daemon.json里增加insecure-registries
3. data_volume改为/data/harbor
mkdir -p /data/harbor
./install.sh --with-chartmuseum
在harbor上配置
- 创建用户,假设为user1
- 创建私有项目,假设为helm
- 将user1添加到helm项目里
添加仓库
helm repo add myharbor http://192.168.36.128/chartrepo/helm --username user1
上传chart并查看
输入密码后,成功的话会提示"myharbor" has been added to your repositories
helm push mychart-0.1.0.tgz myharbor
# 此时harbor页面上可以看到新增了mychart
helm repo update
helm search repo myharbor/
下载chart
helm pull myharbor/mychart
helm pull myharbor/mychart --version 0.1.0
生产环境最佳实践
建议1:参数名要统一,尤其在一个团队里。例如表示deployment副本数,不要有的用replicas,有的用replicaCount
建议2:给参数加注释。因为chart写好后很少需要改动,时间长了容易忘。
建议3:针对不同环境(开发/测试/生产),创建不同的values.yaml,如values-dev.yaml、values-production.yaml
建议4:创建values.schema.json来校验values
其中第4点例子如下,假设values.yaml里有如下内容
image:
repository: my-docker-image
pullPolicy: IfNotPresent
- 要求image参数必须存在,要求image必须存在repository和pullPolicy
- repository类型为字符串,值的格式为小写字母a-z、数字0-9、横线-、下划线_
- pullPolicy类型为字符串,值只能有3种:Always、Never、IfNotPresent
在同级目录下创建values.schema.json,内容如下
{
"$schema": "http://json-schema.org/schema#",
"type": "object",
"required": [
"image"
],
"properties": {
"image": {
"type": "object",
"required": [
"repository",
"pullPolicy"
],
"properties": {
"repository": {
"type": "string",
"pattern": "^[a-z0-9-_]+#34;
},
"pullPolicy": {
"type": "string",
"pattern": "^(Always|Never|IfNotPresent)#34;
}
}
}
}
}
执行helm install、helm upgrade、helm lint、helm template测试,若不满足如上3点要求,则应当报错。
建议5:在chart目录下放置README.md,将chart的设计思想、注意事项写清楚,推荐格式如下
# chart名
## 一键安装命令(helm repo add、helm intall)
## 介绍
## 环境要求
## 安装chart
## 卸载chart
## 参数
### global参数
### common参数
### 父chart参数
### 子chart参数
### 注意事项
## 参考文档
## CHANGELOG
建议6:templates里的NOTES.txt是部署完成后看到的内容,针对不同条件设置输出
建议7:helm默认读取/root/.kube/config,但这样权限过大,很不安全,而且helm通常是由开发或CD系统来使用,因此需要提前在K8S的RBAC里创建分配好权限,只允许在指定的namespace里进行部署
建议8:不要把各种微服务集中于一个chart,可以采用父子chart方式,让每个chart更轻量