devopscube,Devops魔方为个人公众号,主要用于一些有关Devops,容器,kubernetes,自动化运维,以及敏捷开发相关的分享。同时也会不定期的分享一些个人心得,比如推荐一些个人使用的办公小软件,对一些事件的评论等。欢迎大家关注交流。
注: 本文需要对kubernetes docker等平台有一定的理解。
Helm作为一个kuberentes平台的包管理器,已经基本上成为kubernetes的唯一应用打包工具。
那么helm是什么,为什么需要helm?
Helm相对于kubernetes的关系,就像是apt/yum之于Linux操作系统的关系,也相当于windows上将应用包装成exe文件然后只要在界面上点击需要的参数即可快速安装应用。如果把kubernetes比作操作系统的话,如何在这个“操作系统”之上去快速的安装应用,就变得非常重要。
Helm规定了如何打包kubernetes需要部署的资源,通过Go 语言的模板技术,实现能够将资源中的配置模板化,并对外暴露出可配置的属性值。
任何“打包”的目的都是为了能够在不同的平台去运行已经打包好的资源,所以其优势就是打包好的helm包可以在任意平台去部署,你需要做的只是根据平台的需求,修改相应的输入参数即可。
另外一个好处是便于应用的管理,因为kuberentes的复杂性一直令人诟病,但是也是因为其灵活的设计也很难找到更为适合的方式。成功部署一个复杂应用可能需要很多组件单独部署和配合。你需要编写大量的资源文件。如果需要在一个新的环境中部署,你仍然需要修改大量资源文件中的参数,也容易出错,不容易迁移。
再次,除了应用部署,应用的存储和分发,helm也提供了完整的机制,例如使用helm repo存储相应的应用包,可以在远程访问helm仓库进行helm的部署操作。
最后,helm在部署的同时,也实现了更新以及历史更新的管理。可以在此基础上实现回滚等高级操作。
集合以上优点,我们经常会看到基于helm可以实现各种“一键部署”,一键部署wordpress,一键部署mysql集群等。helm的精髓就在于打包一次,随处(kuberentes平台之上)运行。
Helm刚刚出了第三个大版本(2019.11.13发行3.0.0版本),本文的所有操作都是基于helm3进行的,会在下一篇文章中简单介绍一下helm2 和helm3的区别以及如何迁移。
Helm中的重要概念:
Helm的架构很简单,一个客户端Helm Client,一个服务端Helm Library,两者都是使用go语言编写。
请注意,helm在helm 3中有重大更新,在helm2版本及以前,helm有一个在集群中运行的服务叫Tiller,需要安装部署在集群中去执行同kuberentes的交互操作。但是在helm3中删除了Tiller这个架构,在客户端的位置几乎重新写了一个类似tiller作用的Library,很多时出于安全性的目的,设计成将所有的同kuberentes api交互的操作都采用kuberentes原生的client进行管理。后面我们将helm2和helm3的区别时也会讲到。
同helm2不同,helm3无须再去安装tiller,所以只要在客户端配置好helm即可。
方法也很简单:
1. 下载指定版本的helm安装包(https://github.com/helm/helm/releases)
2. 解压
# tar -zxvf helm-v3.0.0-linux-amd64.tar.gz
3. 找到helm命令的位置,然后放到希望放到的位置,例如/usr/local/bin,然后在任意位置都可以执行helm命令了
# mv linux-amd64/helm /usr/local/bin/helm
其他的安装方式请参考: https://helm.sh/docs/intro/install/
同时你也需要安装kubectl并配置好相应的kubeconfig文件,保证kubectl命令行能够访问到集群。
配置好之后就可以使用helm的功能了。
我们从编写HelmChart开始,了解Helmchart封装了哪些资源以及如何利用Go模板的技术对资源进行进一步的封装和使用。
首先我们可以利用helm create命令,创建一个简单的示例。helm create的示例chart也并不是一无所有,它包含了大部分的资源抽象,例如kubernetes deployment,service,ingress,serviceaccount等资源。
$ helm create helloworld
Creating hellworld
$ tree hellworld
hellworld
├── charts
├── Chart.yaml
├── templates
│ ├── deployment.yaml
│ ├── _helpers.tpl
│ ├── ingress.yaml
│ ├── NOTES.txt
│ ├── serviceaccount.yaml
│ ├── service.yaml
│ └── tests
│ └── test-connection.yaml
└── values.yaml
3 directories, 9 files
从结构中我们看到有不同级别的文件夹,以及一些yaml文件。
charts: 用于存放其他依赖和关联的chart。例如应用依赖数据库的chart。
Chart.yaml:存储一些元数据,例如chart的信息,描述等等
templates文件夹:是所有资源的位置,我们可以看到很多kubernetes的资源文件都在这里存放。
其中的_helpers.tpl,用于存储模板片段,可以在文件中直接使用template函数调用。
value.yaml:存储该chart的默认值,实际安装时可以对默认值进行覆盖。
NOTES.txt:相当于你运行helm install的时候给用户输出的提示。
程序员最喜欢helloworld,为了力求描述简单,我们清空template文件下的内容,以及value.yaml中的内容
我们添加一个configmap.yaml文件到template目录下
apiVersion: v1
kind: ConfigMap
metadata:
name: hellworld-configmap
data:
myvalue: "Hello World"
这是我们运行helm install命令,看到已经部署到集群中了,
$ helm install helloworld ./helloworld/
NAME: helloworld
LAST DEPLOYED: Tue Mar 31 14:52:30 2020
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
$ kubectl get cm
NAME DATA AGE
helloworld-configmap 1 108s
所以我们看到,helm做的操作就是将template下的kuberentes资源文件部署到集群之中。这是helm最最基本的设计,也是后续所有功能的前提。
我们修改configmap.yaml,不同的是使用模板的语言。
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: {{ .Values.myvalue | quote }}
然后在value.yaml文件中添加一个键值对最为默认值
myvalue: helloworldfromvalue
然后运行helm install,可以看到configmap中的myvalue就是默认值。
$ helm install helloworld ./helloworld/
NAME: helloworld
LAST DEPLOYED: Tue Mar 31 15:08:11 2020
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
$ kubectl get cm -o yaml
apiVersion: v1
items:
- apiVersion: v1
data:
myvalue: helloworldfromvalue
kind: ConfigMap
metadata:
creationTimestamp: "2020-03-31T07:08:11Z"
name: helloworld-configmap
namespace: default
resourceVersion: "3735016"
selfLink: /api/v1/namespaces/default/configmaps/helloworld-configmap
uid: 9bf21509-cd29-4e9d-b6ab-bb9dde006bb4
kind: List
metadata:
resourceVersion: ""
selfLink: ""
我们尝试使用传递值的方式更新value,我们看到configmap中存储的mybalue就改成了我们命令行传进去的值。
$ helm upgrade helloworld ./helloworld/ --set myvalue=helloworldfromcommand
Release "helloworld" has been upgraded. Happy Helming!
NAME: helloworld
LAST DEPLOYED: Tue Mar 31 15:12:10 2020
NAMESPACE: default
STATUS: deployed
REVISION: 2
TEST SUITE: None
$ kubectl get cm -o yaml
apiVersion: v1
items:
- apiVersion: v1
data:
myvalue: helloworldfromcommand
kind: ConfigMap
metadata:
creationTimestamp: "2020-03-31T07:08:11Z"
name: helloworld-configmap
namespace: default
resourceVersion: "3735330"
selfLink: /api/v1/namespaces/default/configmaps/helloworld-configmap
uid: 9bf21509-cd29-4e9d-b6ab-bb9dde006bb4
kind: List
metadata:
resourceVersion: ""
selfLink: ""
分析一下我们使用的configmap.yaml,我们使用了两种模板的值
{{ .Release.Name }} 这种是helm内部自带的值,都是一些内建的变量,所有人都可以访问
{{ .Values.myvalue | quote }} 这种是我们从values.yaml文件中获取或者从命令行中获取的值。
除了Release,helm还提供了其他内建对象,如:
Chart, 在Chart.yaml中定义的变量,如Chart.name,Chart.version等
Files,任何Chart目录下的文件都可以通过Files获取,例如ini文件
Capabilities, 用于提供kuberentes 集群的一些能力,如Capabilities.APIVersions,Capabilities.KubeVersion.Minor,Capabilities.KubeVersion
关于Value,我们在value.xml中是这样定义的,以及如何在文件中调用该值
# 使用.Values.key调用值
key:value
# 使用.Values.key.key1.key2调用值
key:
key1:
key2:value
我们在上一节提到的使用自定义变量的方法: {{ .Values.myvalue | quote }},其实是使用到了管道的技术。
模板方法的使用如下,quote是一个模板方法,可以将输入的参数添加双引号。
{{ quote .Values.myvalue }}
模板方法的调用结构是: functionname arg1 arg2 ...
Helm有60多个模板的方法。在实际操作中我们可以记录一些常用的方法。
更多的方法我们可以查看
https://godoc.org/text/template
同样使用管道,我们可以将value传递到方法之上,例如value全部大写后,添加双引号。而且默认值为”helloworld“。
{{ .Values.myvalue | default 'hellworld' | upper | quote }}
使用管道的好处是可以嵌套多个方法,更加容易阅读。
很多计算符号也是方法:
eq`, `ne`, `lt`, `gt`, `and`, `or
例如
# eq方法,判断两者是否相等,其他操作类似
eq arg1 arg2
经典的流控制包括If-else,range循环。除此之外还有一个with(控制值的相对作用域)
经典的结构为
{{ if PIPELINE }}
# Do something
{{ else if OTHER PIPELINE }}
# Do something else
{{ else }}
# Default case
{{ end }}
PIPELINE输出的是true/false,用于判断。
例如:
{{ if eq .Values.myvalue "hellworld" }}
result: true
{{ end }}
因为 {{ if eq .Values.myvalue "hellworld" }}
和{{ end }}
都各占了一行,这样实际操作会产生空行,那我们需要一种方式去除空行
{{- if eq .Values.myvalue "hellworld" }}
result: true
{{- end }}
{{- with .Values.favorite }}
drink: {{ .drink | default "tea" | quote }}
food: {{ .food | upper | quote }}
{{- end }}
默认情况,我们访问值使用.Values.aaa.bbb。 使用with时可以改变当前作用域。例如上面的例子中,改变了值访问的作用域为.Values.favorite. 那么想要访问.Values.favorite.drink,就可以直接with内部写成.drink
ragne是针对列表来指定的,如果我们在value.yaml中配置了列表
zoo:
- tiger
- lion
- monkey
我们想用循环的方式读取
{- range .Values.zoo }}
- {{ . | title | quote }}
{{- end }}
输出结果:
- "tiger"
- "lion"
- "monkey"
模板中也可去定义变量,例如将.Release.Name
传递给$relname
{{- $relname := .Release.Name -}}
之后就可以在文件的任意地方调用该变量了
在range中使用变量非常方便,例如,j将列表赋值到key和val两个变量上
{{- range $key, $val := .Values.zoo }}
{{ $key }}: {{ $val | quote }}
{{- end }}
$
符号代表根,例如可以使用 . C h a r t . V e r s i o n , .Chart.Version, .Chart.Version,.Release.Name。这样可以打破scope的界限,访问到chart的内建资源。避免因为scope的改变而访问不到(例如在with或range内部)
之前我们看到有个文件叫做_helpers.tpl,我们介绍是说存储模板片段的地方。
模板片段其实也可以在文件中定义,但是为了更好管理,可以在_helpers.tpl中定义,使用时直接调用即可。
例如在_helpers.tpl中定义了如下模板片段
{{- define "mychart.labels" }}
labels:
generator: helm
app: helloworld
{{- end }}
我们在configmap.yaml文件中调用如下:
apiVersion: v1
kind: ConfigMap
metadata:
name: helloworld-configmap
{{- template "mychart.lables" }}
data:
myvalue: helloworld
实际的输出变成:
apiVersion: v1
kind: ConfigMap
metadata:
name: helloworld-configmap
labels:
generator: helm
app: helloworld
data:
myvalue: helloworld
未完待续,请见下期。