包管理工具 HELM(7)
包管理工具 HELM(7)
1. Helm安装使用
Helm
这个东西其实早有耳闻,但是一直没有用在生产环境,而且现在对这货的评价也是褒贬不一。正好最近需要再次部署一套测试环境,对于单体服务,部署一套测试环境我相信还是非常快的,但是对于微服务架构的应用,要部署一套新的环境,就有点折磨人了,微服务越多、你就会越绝望的。虽然我们线上和测试环境已经都迁移到了kubernetes
环境,但是每个微服务也得维护一套yaml
文件,而且每个环境下的配置文件也不太一样,部署一套新的环境成本是真的很高。如果我们能使用类似于yum的工具来安装我们的应用的话是不是就很爽歪歪了啊?Helm
就相当于kubernetes
环境下的yum包管理工具。
1.1 用途
做为 Kubernetes 的一个包管理工具,Helm
具有如下功能:
- 创建新的 chart
- chart 打包成 tgz 格式
- 上传 chart 到 chart 仓库或从仓库中下载 chart
- 在
Kubernetes
集群中安装或卸载 chart- 管理用
Helm
安装的 chart 的发布周期
1.2 重要概念
Helm 有三个重要概念:
- chart:包含了创建
Kubernetes
的一个应用实例的必要信息- config:包含了应用发布配置信息
- release:是一个 chart 及其配置的一个运行实例
1.3 Helm组件
Helm 有以下两个组成部分: Helm Structrue
Helm Client
是用户命令行工具,其主要负责如下:
- 本地 chart 开发
- 仓库管理
- 与 Tiller sever 交互
- 发送预安装的 chart
- 查询 release 信息
- 要求升级或卸载已存在的 release
Tiller Server
是一个部署在Kubernetes
集群内部的 server,其与 Helm client、Kubernetes API server 进行交互。Tiller server 主要负责如下:
- 监听来自 Helm client 的请求
- 通过 chart 及其配置构建一次发布
- 安装 chart 到
Kubernetes
集群,并跟踪随后的发布- 通过与
Kubernetes
交互升级或卸载 chart- 简单的说,client 管理 charts,而 server 管理发布 release
1.4 安装
我们可以在Helm Realese页面下载二进制文件,这里下载的v2.10.0版本,解压后将可执行文件helm
拷贝到/usr/local/bin
目录下即可,这样Helm
客户端就在这台机器上安装完成了。
现在我们可以使用Helm
命令查看版本了,会提示无法连接到服务端Tiller
:
[root@node01 ~]# wget https://get.helm.sh/helm-v2.10.0-linux-amd64.tar.gz
[root@node01 ~]# tar xf helm-v2.10.0-linux-amd64.tar.gz
[root@node01 ~]# ls linux-amd64/
helm LICENSE README.md
[root@node01 ~]# cp -a ./linux-amd64/helm /usr/local/bin/
[root@node01 ~]# helm version
Client: &version.Version{SemVer:"v2.10.0", GitCommit:"9ad53aac42165a5fadc6c87be0dea6b115f93090", GitTreeState:"clean"}
Error: could not find tiller
要安装 Helm 的服务端程序,我们需要使用到kubectl
工具,所以先确保kubectl
工具能够正常的访问 kubernetes 集群的apiserver
哦。
然后我们在命令行中执行初始化操作:
[root@node01 ~]# helm init --upgrade --tiller-image cnych/tiller:v2.10.0 --stable-repo-url https://cnych.github.io/kube-charts-mirror/
Creating /root/.helm
Creating /root/.helm/repository
Creating /root/.helm/repository/cache
Creating /root/.helm/repository/local
Creating /root/.helm/plugins
Creating /root/.helm/starters
Creating /root/.helm/cache/archive
Creating /root/.helm/repository/repositories.yaml
Adding stable repo with URL: https://cnych.github.io/kube-charts-mirror/
Adding local repo with URL: http://127.0.0.1:8879/charts
$HELM_HOME has been configured at /root/.helm.
Tiller (the Helm server-side component) has been installed into your Kubernetes Cluster.
Please note: by default, Tiller is deployed with an insecure 'allow unauthenticated users' policy.
To prevent this, run `helm init` with the --tiller-tls-verify flag.
For more information on securing your installation see: https://docs.helm.sh/using_helm/#securing-your-helm-installation
Happy Helming!
这个命令会把默认的 google 的仓库地址替换成我同步的一个镜像地址。
如果在安装过程中遇到了一些其他问题,比如初始化的时候出现了如下错误:
E0125 14:03:19.093131 56246 portforward.go:331] an error occurred forwarding 55943 -> 44134: error forwarding port 44134 to pod d01941068c9dfea1c9e46127578994d1cf8bc34c971ff109dc6faa4c05043a6e, uid : unable to do port forwarding: socat not found.
2018/01/25 14:03:19 (0xc420476210) (0xc4203ae1e0) Stream removed, broadcasting: 3
2018/01/25 14:03:19 (0xc4203ae1e0) (3) Writing data frame
2018/01/25 14:03:19 (0xc420476210) (0xc4200c3900) Create stream
2018/01/25 14:03:19 (0xc420476210) (0xc4200c3900) Stream added, broadcasting: 5
Error: cannot connect to Tiller
解决方案:在节点上安装socat
可以解决
$ sudo yum install -y socat
Helm 服务端正常安装完成后,Tiller
默认被部署在kubernetes
集群的kube-system
命名空间下:
[root@node01 ~]# kubectl get pod -n kube-system -l app=helm
NAME READY STATUS RESTARTS AGE
tiller-deploy-86b844d8c6-d2kw8 1/1 Running 0 27s
此时,我们查看 Helm 版本就都正常了:
[root@node01 ~]# helm version
Client: &version.Version{SemVer:"v2.10.0", GitCommit:"9ad53aac42165a5fadc6c87be0dea6b115f93090", GitTreeState:"clean"}
Server: &version.Version{SemVer:"v2.10.0", GitCommit:"9ad53aac42165a5fadc6c87be0dea6b115f93090", GitTreeState:"clean"}
另外一个值得注意的问题是RBAC
,我们的 kubernetes 集群是1.10.0版本的,默认开启了RBAC
访问控制,所以我们需要为Tiller
创建一个ServiceAccount
,让他拥有执行的权限,详细内容可以查看 Helm 文档中的Role-based Access Control。 创建rbac.yaml
文件:
[root@node01 ~]# vim rbac-config.yaml
[root@node01 ~]# cat rbac-config.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: tiller
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: tiller
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: tiller
namespace: kube-system
然后使用kubectl
创建:
[root@node01 ~]# kubectl create -f rbac-config.yaml
serviceaccount "tiller" created
clusterrolebinding.rbac.authorization.k8s.io "tiller" created
创建了tiller
的 ServceAccount 后还没完,因为我们的 Tiller 之前已经就部署成功了,而且是没有指定 ServiceAccount 的,所以我们需要给 Tiller 打上一个 ServiceAccount 的补丁:
[root@node01 ~]# kubectl patch deploy --namespace kube-system tiller-deploy -p '{"spec":{"template":{"spec":{"serviceAccount":"tiller"}}}}'
deployment.extensions "tiller-deploy" patched
上面这一步非常重要,不然后面在使用 Helm 的过程中可能出现
Error: no available release name found
的错误信息。
至此, Helm
客户端和服务端都配置完成了,接下来我们看看如何使用吧。
1.5 使用
我们现在了尝试创建一个 Chart:
[root@node01 ~]# helm create hello-helm
Creating hello-helm
[root@node01 ~]# tree hello-helm
hello-helm
├── charts
├── Chart.yaml
├── templates
│ ├── deployment.yaml
│ ├── _helpers.tpl
│ ├── ingress.yaml
│ ├── NOTES.txt
│ └── service.yaml
└── values.yaml
2 directories, 7 files
我们通过查看templates
目录下面的deployment.yaml
文件可以看出默认创建的 Chart 是一个 nginx 服务,具体的每个文件是干什么用的,我们可以前往 Helm 官方文档进行查看,后面会和大家详细讲解的。比如这里我们来安装 1.7.9 这个版本的 nginx,则我们更改 value.yaml 文件下面的 image tag 即可,将默认的 stable 更改为 1.7.9
,为了测试方便,我们把 Service 的类型也改成 NodePort
[root@node01 ~]# vim hello-helm/values.yaml
[root@node01 ~]# cat hello-helm/values.yaml
...
image:
repository: nginx
tag: 1.7.9
pullPolicy: IfNotPresent
nameOverride: ""
fullnameOverride: ""
service:
type: NodePort
port: 80
...
现在我们来尝试安装下这个 Chart :
[root@node01 ~]# helm install ./hello-helm
NAME: foppish-billygoat
LAST DEPLOYED: Mon Sep 20 15:11:34 2021
NAMESPACE: default
STATUS: DEPLOYED
RESOURCES:
==> v1beta2/Deployment
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
foppish-billygoat-hello-helm 1 0 0 0 0s
==> v1/Pod(related)
NAME READY STATUS RESTARTS AGE
foppish-billygoat-hello-helm-79f56fc76f-9cdm8 0/1 Pending 0 0s
==> v1/Service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
foppish-billygoat-hello-helm NodePort 10.109.79.65 <none> 80:31941/TCP 0s
NOTES:
1. Get the application URL by running these commands:
export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services foppish-billygoat-hello-helm)
export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
[root@node01 ~]# kubectl get pods -l app=hello-helm
NAME READY STATUS RESTARTS AGE
foppish-billygoat-hello-helm-79f56fc76f-9cdm8 1/1 Running 0 2m
[root@node01 ~]# kubectl get svc -l app=hello-helm
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
foppish-billygoat-hello-helm NodePort 10.109.79.65 <none> 80:31941/TCP 3m
等到 Pod 创建完成后,我们可以根据创建的 Service 的 NodePort 来访问该服务了,然后在浏览器中打开http://k8s.haimaxy.com:31941就可以正常的访问我们刚刚部署的 nginx 应用了。
查看release
:
[root@node01 ~]# helm list
NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
foppish-billygoat 1 Mon Sep 20 15:11:34 2021 DEPLOYED hello-helm-0.1.0 1.0 default
打包chart
:
[root@node01 ~]# helm package hello-helm
Successfully packaged chart and saved it to: /root/hello-helm-0.1.0.tgz
[root@node01 ~]# ll -d /root/hello-helm-0.1.0.tgz
-rw-r--r-- 1 root root 2598 9月 20 15:27 /root/hello-helm-0.1.0.tgz
然后我们就可以将打包的tgz
文件分发到任意的服务器上,通过helm fetch
就可以获取到该 Chart 了。
删除release
:
[root@node01 ~]# helm delete foppish-billygoat
release "foppish-billygoat" deleted
然后我们看到kubernetes
集群上的该 nginx 服务也已经被删除了。
[root@node01 ~]# kubectl get pods -l app=hello-helm
No resources found.
还有更多关于Helm的使用命令,我们可以前往官方文档查看。
2. Helm 的基本使用
上节课我们成功安装了Helm
的客户端以及服务端Tiller Server
,我们也自己尝试创建了我们的第一个 Helm Chart 包,这节课就来和大家一起学习下 Helm 中的一些常用的操作方法。
2.1 仓库
Helm 的 Repo 仓库和 Docker Registry 比较类似,Chart 库可以用来存储和共享打包 Chart 的位置,我们在安装了 Helm 后,默认的仓库地址是 google 的一个地址,这对于我们不能上网的同学就比较苦恼了,没办法访问到官方提供的 Chart 仓库,可以用helm repo list
来查看当前的仓库配置:
[root@node01 ~]# helm repo list
NAME URL
stable https://cnych.github.io/kube-charts-mirror/
local http://127.0.0.1:8879/charts
我们可以看到除了一个默认的 stable 的仓库配置外,还有一个 local 的本地仓库,这是我们本地测试的一个仓库地址。其实要创建一个 Chart 仓库也是非常简单的,Chart 仓库其实就是一个带有index.yaml
索引文件和任意个打包的 Chart 的 HTTP 服务器而已,比如我们想要分享一个 Chart 包的时候,将我们本地的 Chart 包上传到该服务器上面,别人就可以使用了,所以其实我们自己托管一个 Chart 仓库也是非常简单的,比如阿里云的 OSS、Github Pages,甚至自己创建的一个简单服务器都可以。
为了解决上网的问题,我这里建了一个 Github Pages 仓库,每天会自动和官方的仓库进行同步,地址是:https://github.com/cnych/kube-charts-mirror,这样我们就可以将我们的 Helm 默认仓库地址更改成我们自己的仓库地址了:
[root@node01 ~]# helm repo remove stable
"stable" has been removed from your repositories
[root@node01 ~]# helm repo add stable https://cnych.github.io/kube-charts-mirror/
"stable" has been added to your repositories
[root@node01 ~]# helm repo list
NAME URL
local http://127.0.0.1:8879/charts
stable https://cnych.github.io/kube-charts-mirror/
[root@node01 ~]# helm repo update
Hang tight while we grab the latest from your chart repositories...
...Skip local chart repository
...Successfully got an update from the "stable" chart repository
Update Complete. ⎈ Happy Helming!⎈
仓库添加完成后,可以使用 update 命令进行仓库更新。当然如果要我们自己来创建一个 web 服务器来服务 Helm Chart 的话,只需要实现下面几个功能点就可以提供服务了:
- 将索引和
Chart
置于服务器目录中- 确保索引文件
index.yaml
可以在没有认证要求的情况下访问- 确保 yaml 文件的正确内容类型(text/yaml 或 text/x-yaml)
如果你的 web 服务提供了上面几个功能,那么也就可以当做 Helm Chart 仓库来使用了。
2.2 查找 chart
Helm 将 Charts 包安装到 Kubernetes 集群中,一个安装实例就是一个新的 Release,要找到新的 Chart,我们可以通过搜索命令完成。
记住,如果不能上网,将默认的 stable 的仓库地址更换成上面我们创建的地址
直接运行helm search
命令可以查看有哪些 Charts 是可用的:
[root@node01 ~]# helm search
NAME CHART VERSION APP VERSION DESCRIPTION
...
stable/minio 1.6.3 RELEASE.2018-08-25T01-56-38Z Minio is a high performance distributed object storage se...
stable/mission-control 0.4.2 3.1.2 A Helm chart for JFrog Mission Control
stable/mongodb 4.2.2 4.0.2 NoSQL document-oriented database that stores JSON-like do...
stable/mongodb-replicaset 3.5.6 3.6 NoSQL document-oriented database that stores JSON-like do...
...
stable/zetcd 0.1.9 0.0.3 CoreOS zetcd Helm chart for Kubernetes
...
如果没有使用过滤条件,helm search 显示所有可用的 charts。可以通过使用过滤条件进行搜索来缩小搜索的结果范围:
[root@node01 ~]# helm search mysql
NAME CHART VERSION APP VERSION DESCRIPTION
stable/mysql 0.10.2 5.7.14 Fast, reliable, scalable, and easy to use open-source rel...
stable/mysqldump 1.0.0 5.7.21 A Helm chart to help backup MySQL databases using mysqldump
stable/prometheus-mysql-exporter 0.2.1 v0.11.0 A Helm chart for prometheus mysql exporter with cloudsqlp...
stable/percona 0.3.3 5.7.17 free, fully compatible, enhanced, open source drop-in rep...
stable/percona-xtradb-cluster 0.5.0 5.7.19 free, fully compatible, enhanced, open source drop-in rep...
stable/phpmyadmin 1.3.0 4.8.3 phpMyAdmin is an mysql administration frontend
stable/gcloud-sqlproxy 0.6.0 1.11 Google Cloud SQL Proxy
stable/mariadb 5.2.3 10.1.37 Fast, reliable, scalable, and easy to use open-source rel...
可以看到明显少了很多 charts 了,同样的,我们可以使用 inspect 命令来查看一个 chart 的详细信息:
[root@node01 ~]# helm inspect stable/mysql|head -27
appVersion: 5.7.14
description: Fast, reliable, scalable, and easy to use open-source relational database
system.
engine: gotpl
home: https://www.mysql.com/
icon: https://www.mysql.com/common/logos/logo-mysql-170x115.png
keywords:
- mysql
- database
- sql
maintainers:
- email: o.with@sportradar.com
name: olemarkus
- email: viglesias@google.com
name: viglesiasce
name: mysql
sources:
- https://github.com/kubernetes/charts
- https://github.com/docker-library/mysql
version: 0.10.2
---
## mysql image version
## ref: https://hub.docker.com/r/library/mysql/tags/
##
image: "mysql"
imageTag: "5.7.14"
...
使用 inspect 命令可以查看到该 chart 里面所有描述信息,包括运行方式、配置信息等等。
通过 helm search 命令可以找到我们想要的 chart 包,找到后就可以通过 helm install 命令来进行安装了。
2.3 安装 chart
要安装新的软件包,直接使用 helm install 命令即可。最简单的情况下,它只需要一个 chart 的名称参数:
[root@node01 ~]# helm install stable/mysql
NAME: geared-moth
LAST DEPLOYED: Mon Sep 20 16:01:21 2021
NAMESPACE: default
STATUS: DEPLOYED
RESOURCES:
==> v1/Secret
NAME TYPE DATA AGE
geared-moth-mysql Opaque 2 1s
==> v1/ConfigMap
NAME DATA AGE
geared-moth-mysql-test 1 1s
==> v1/PersistentVolumeClaim
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
geared-moth-mysql Pending 1s
==> v1/Service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
geared-moth-mysql ClusterIP 10.105.138.133 <none> 3306/TCP 1s
==> v1beta1/Deployment
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
geared-moth-mysql 1 1 1 0 1s
==> v1/Pod(related)
NAME READY STATUS RESTARTS AGE
geared-moth-mysql-58778f7d84-jrx57 0/1 Pending 0 0s
NOTES:
MySQL can be accessed via port 3306 on the following DNS name from within your cluster:
geared-moth-mysql.default.svc.cluster.local
To get your root password run:
MYSQL_ROOT_PASSWORD=$(kubectl get secret --namespace default geared-moth-mysql -o jsonpath="{.data.mysql-root-password}" | base64 --decode; echo)
To connect to your database:
1. Run an Ubuntu pod that you can use as a client:
kubectl run -i --tty ubuntu --image=ubuntu:16.04 --restart=Never -- bash -il
2. Install the mysql client:
$ apt-get update && apt-get install mysql-client -y
3. Connect using the mysql cli, then provide your password:
$ mysql -h geared-moth-mysql -p
To connect to your database directly from outside the K8s cluster:
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
# Execute the following command to route the connection:
kubectl port-forward svc/geared-moth-mysql 3306
mysql -h ${MYSQL_HOST} -P${MYSQL_PORT} -u root -p${MYSQL_ROOT_PASSWORD}
现在 mysql chart 已经安装上了,安装 chart 会创建一个新 release 对象。上面的 release 被命名为 hmewing-squid。如果你想使用你自己的 release 名称,只需使用--name参数指定即可,比如:
$ helm install stable/mysql --name mydb
在安装过程中,helm 客户端将打印有关创建哪些资源的有用信息,release 的状态以及其他有用的配置信息,比如这里的有访问 mysql 服务的方法、获取 root 用户的密码以及连接 mysql 的方法等信息。
值得注意的是 Helm 并不会一直等到所有资源都运行才退出。因为很多 charts 需要的镜像资源非常大,所以可能需要很长时间才能安装到集群中去。
要跟踪 release 状态或重新读取配置信息,可以使用 helm status 查看:
[root@node01 ~]# helm status geared-moth|head
LAST DEPLOYED: Mon Sep 20 16:01:21 2021
NAMESPACE: default
STATUS: DEPLOYED
RESOURCES:
==> v1/PersistentVolumeClaim
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
geared-moth-mysql Pending 4m
==> v1/Service
...
可以看到当前 release 的状态是DEPLOYED,下面还有一些安装的时候出现的信息。
[root@node01 ~]# helm ls
NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
geared-moth 1 Mon Sep 20 16:01:21 2021 DEPLOYED mysql-0.10.2 5.7.14
2.4 自定义 chart
上面的安装方式是使用 chart 的默认配置选项。但是在很多时候,我们都需要自定义 chart 以满足自身的需求,要自定义 chart,我们就需要知道我们使用的 chart 支持的可配置选项才行。
要查看 chart 上可配置的选项,使用helm inspect values
命令即可,比如我们这里查看上面的 mysql 的配置选项:
[root@node01 ~]# helm inspect values stable/mysql
## mysql image version
## ref: https://hub.docker.com/r/library/mysql/tags/
##
image: "mysql"
imageTag: "5.7.14"
## Specify password for root user
##
## Default: random 10 character string
# mysqlRootPassword: testing
## Create a database user
##
# mysqlUser:
## Default: random 10 character string
# mysqlPassword:
## Allow unauthenticated access, uncomment to enable
##
# mysqlAllowEmptyPassword: true
## Create a database
##
# mysqlDatabase:
## Specify an imagePullPolicy (Required)
## It's recommended to change this to 'Always' if the image tag is 'latest'
## ref: http://kubernetes.io/docs/user-guide/images/#updating-images
##
imagePullPolicy: IfNotPresent
extraVolumes: |
# - name: extras
# emptyDir: {}
extraVolumeMounts: |
# - name: extras
# mountPath: /usr/share/extras
# readOnly: true
extraInitContainers: |
# - name: do-something
# image: busybox
# command: ['do', 'something']
# Optionally specify an array of imagePullSecrets.
# Secrets must be manually created in the namespace.
# ref: https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod
# imagePullSecrets:
# - name: myRegistryKeySecretName
## Node selector
## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector
nodeSelector: {}
livenessProbe:
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 3
readinessProbe:
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 1
successThreshold: 1
failureThreshold: 3
## Persist data to a persistent volume
persistence:
enabled: true
## database data Persistent Volume Storage Class
## If defined, storageClassName: <storageClass>
## If set to "-", storageClassName: "", which disables dynamic provisioning
## If undefined (the default) or set to null, no storageClassName spec is
## set, choosing the default provisioner. (gp2 on AWS, standard on
## GKE, AWS & OpenStack)
##
# storageClass: "-"
accessMode: ReadWriteOnce
size: 8Gi
annotations: {}
## Configure resource requests and limits
## ref: http://kubernetes.io/docs/user-guide/compute-resources/
##
resources:
requests:
memory: 256Mi
cpu: 100m
# Custom mysql configuration files used to override default mysql settings
configurationFiles: {}
# mysql.cnf: |-
# [mysqld]
# skip-name-resolve
# ssl-ca=/ssl/ca.pem
# ssl-cert=/ssl/server-cert.pem
# ssl-key=/ssl/server-key.pem
# Custom mysql init SQL files used to initialize the database
initializationFiles: {}
# first-db.sql: |-
# CREATE DATABASE IF NOT EXISTS first DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
# second-db.sql: |-
# CREATE DATABASE IF NOT EXISTS second DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
metrics:
enabled: false
image: prom/mysqld-exporter
imageTag: v0.10.0
imagePullPolicy: IfNotPresent
resources: {}
annotations: {}
# prometheus.io/scrape: "true"
# prometheus.io/port: "9104"
livenessProbe:
initialDelaySeconds: 15
timeoutSeconds: 5
readinessProbe:
initialDelaySeconds: 5
timeoutSeconds: 1
## Configure the service
## ref: http://kubernetes.io/docs/user-guide/services/
service:
## Specify a service type
## ref: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services---service-types
type: ClusterIP
port: 3306
# nodePort: 32000
ssl:
enabled: false
secret: mysql-ssl-certs
certificates:
# - name: mysql-ssl-certs
# ca: |-
# -----BEGIN CERTIFICATE-----
# ...
# -----END CERTIFICATE-----
# cert: |-
# -----BEGIN CERTIFICATE-----
# ...
# -----END CERTIFICATE-----
# key: |-
# -----BEGIN RSA PRIVATE KEY-----
# ...
# -----END RSA PRIVATE KEY-----
## Populates the 'TZ' system timezone environment variable
## ref: https://dev.mysql.com/doc/refman/5.7/en/time-zone-support.html
##
## Default: nil (mysql will use image's default timezone, normally UTC)
## Example: 'Australia/Sydney'
# timezone:
# To be added to the database server pod(s)
podAnnotations: {}
然后,我们可以直接在 YAML 格式的文件中来覆盖上面的任何配置,在安装的时候直接使用该配置文件即可:(config.yaml)
[root@node01 ~]# vim config.yaml
[root@node01 ~]# cat config.yaml
mysqlUser: haimaxyUser
mysqlDatabase: haimaxyDB
service:
type: NodePort
我们这里通过 config.yaml 文件定义了 mysqlUser 和 mysqlDatabase,并且把 service 的类型更改为了 NodePort,然后现在我们来安装的时候直接指定该 yaml 文件:
[root@node01 ~]# helm install -f config.yaml stable/mysql --name mydb
NAME: mydb
LAST DEPLOYED: Mon Sep 20 16:19:48 2021
NAMESPACE: default
STATUS: DEPLOYED
RESOURCES:
==> v1/Pod(related)
NAME READY STATUS RESTARTS AGE
mydb-mysql-dfc999888-rpp4m 0/1 Pending 0 0s
==> v1/Secret
NAME TYPE DATA AGE
mydb-mysql Opaque 2 0s
==> v1/ConfigMap
NAME DATA AGE
mydb-mysql-test 1 0s
==> v1/PersistentVolumeClaim
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
mydb-mysql Pending 0s
==> v1/Service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
mydb-mysql NodePort 10.97.123.216 <none> 3306:31001/TCP 0s
==> v1beta1/Deployment
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
mydb-mysql 1 1 1 0 0s
NOTES:
MySQL can be accessed via port 3306 on the following DNS name from within your cluster:
mydb-mysql.default.svc.cluster.local
To get your root password run:
MYSQL_ROOT_PASSWORD=$(kubectl get secret --namespace default mydb-mysql -o jsonpath="{.data.mysql-root-password}" | base64 --decode; echo)
To connect to your database:
1. Run an Ubuntu pod that you can use as a client:
kubectl run -i --tty ubuntu --image=ubuntu:16.04 --restart=Never -- bash -il
2. Install the mysql client:
$ apt-get update && apt-get install mysql-client -y
3. Connect using the mysql cli, then provide your password:
$ mysql -h mydb-mysql -p
To connect to your database directly from outside the K8s cluster:
MYSQL_HOST=$(kubectl get nodes --namespace default -o jsonpath='{.items[0].status.addresses[0].address}')
MYSQL_PORT=$(kubectl get svc --namespace default mydb-mysql -o jsonpath='{.spec.ports[0].nodePort}')
mysql -h ${MYSQL_HOST} -P${MYSQL_PORT} -u root -p${MYSQL_ROOT_PASSWORD}
我们可以看到当前 release 的名字已经变成 mydb 了。然后可以查看下 mydb 关联的 Service 是否变成 NodePort 类型的了:
[root@node01 ~]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
geared-moth-mysql ClusterIP 10.105.138.133 <none> 3306/TCP 19m
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 97d
mydb-mysql NodePort 10.97.123.216 <none> 3306:31001/TCP 1m
看到服务 mydb-mysql 变成了 NodePort 类型的,二之前默认创建的 mewing-squid-mysql 是 ClusterIP 类型的,证明上面我们通过 YAML 文件来覆盖 values 是成功的。
接下来我们查看下 Pod 的状况:
[root@node01 ~]# kubectl get pods
NAME READY STATUS RESTARTS AGE
geared-moth-mysql-58778f7d84-jrx57 0/1 Pending 0 20m
mydb-mysql-dfc999888-rpp4m 0/1 Pending 0 2m
比较奇怪的是之前默认创建的和现在的 mydb 的 release 创建的 Pod 都是 Pending 状态,直接使用 describe 命令查看下:
[root@node01 ~]# kubectl describe pod mydb-mysql-dfc999888-rpp4m
Name: mydb-mysql-dfc999888-rpp4m
Namespace: default
Node: <none>
Labels: app=mydb-mysql
pod-template-hash=897555444
Annotations: <none>
Status: Pending
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 16s (x15 over 3m) default-scheduler pod has unbound PersistentVolumeClaims
我们可以发现两个 Pod 处于 Pending 状态的原因都是 PVC 没有被绑定上,所以这里我们可以通过 storageclass 或者手动创建一个合适的 PV 对象来解决这个问题。
另外为了说明 helm 更新的用法,我们这里来直接禁用掉数据持久化,可以在上面的 config.yaml 文件中设置:
persistence:
enabled: false
另外一种方法就是在安装过程中使用--set
来覆盖对应的 value 值,比如禁用数据持久化,我们这里可以这样来覆盖:
$ helm install stable/mysql --set persistence.enabled=false --name mydb
2.5 升级
我们这里将数据持久化禁用掉来对上面的 mydb 进行升级:
[root@node01 ~]# cat config.yaml
mysqlUser: haimaxyUser
mysqlDatabase: haimaxyDB
service:
type: NodePort
persistence:
enabled: false
[root@node01 ~]# helm upgrade -f config.yaml mydb stable/mysql
Release "mydb" has been upgraded. Happy Helming!
LAST DEPLOYED: Mon Sep 20 17:30:24 2021
NAMESPACE: default
STATUS: DEPLOYED
RESOURCES:
...
可以看到已经变成 DEPLOYED 状态了,现在我们再去看看 Pod 的状态呢:
[root@node01 ~]# kubectl get pods
NAME READY STATUS RESTARTS AGE
geared-moth-mysql-58778f7d84-jrx57 0/1 Pending 0 1h
mydb-mysql-6ffc84bbf6-jrkgx 1/1 Running 0 1m
我们看到 mydb 关联的 Pod 已经变成了 PodInitializing 的状态,已经不是 Pending 状态了,同样的,使用 describe 命令查看:
[root@node01 ~]# kubectl describe pod mydb-mysql-6ffc84bbf6-jrkgx
Name: mydb-mysql-6ffc84bbf6-jrkgx
Namespace: default
Node: node01/192.168.200.11
Start Time: Mon, 20 Sep 2021 17:30:24 +0800
Labels: app=mydb-mysql
pod-template-hash=2997406692
Annotations: <none>
Status: Running
IP: 10.244.1.7
Controlled By: ReplicaSet/mydb-mysql-6ffc84bbf6
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 2m default-scheduler Successfully assigned mydb-mysql-6ffc84bbf6-jrkgx to node01
Normal SuccessfulMountVolume 2m kubelet, node01 MountVolume.SetUp succeeded for volume "data"
Normal SuccessfulMountVolume 2m kubelet, node01 MountVolume.SetUp succeeded for volume "default-token-ztfk2"
Normal Pulled 2m kubelet, node01 Container image "busybox:1.25.0" already present on machine
Normal Created 2m kubelet, node01 Created container
Normal Started 2m kubelet, node01 Started container
Normal Pulled 2m kubelet, node01 Container image "mysql:5.7.14" already present on machine
Normal Created 2m kubelet, node01 Created container
Normal Started 2m kubelet, node01 Started container
我们可以看到现在没有任何关于 PVC 的错误信息了,这是因为我们刚刚更新的版本中就是禁用掉了的数据持久化的,证明 helm upgrade 和 --values 是生效了的。现在我们使用 helm ls 命令查看先当前的 release:
[root@node01 ~]# helm ls
NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
geared-moth 1 Mon Sep 20 16:01:21 2021 DEPLOYED mysql-0.10.2 5.7.14 default
mydb 2 Mon Sep 20 17:30:24 2021 DEPLOYED mysql-0.10.2 5.7.14 default
可以看到 mydb 这个 release 的REVISION已经变成2了,这是因为 release 的版本是递增的,每次安装、升级或者回滚,版本号都会加1,第一个版本号始终为1,同样我们可以使用 helm history 命令查看 release 的历史版本:
[root@node01 ~]# helm history mydb
REVISION UPDATED STATUS CHART DESCRIPTION
1 Mon Sep 20 16:19:48 2021 SUPERSEDED mysql-0.10.2 Install complete
2 Mon Sep 20 17:30:24 2021 DEPLOYED mysql-0.10.2 Upgrade complete
当然如果我们要回滚到某一个版本的话,使用 helm rollback 命令即可,比如我们将 mydb 回滚到上一个版本:
[root@node01 ~]# #helm rollback mydb 1
2.6 删除
上节课我们就学习了要删除一个 release 直接使用 helm delete 命令就 OK:
[root@node01 ~]# helm delete geared-moth
release "geared-moth" deleted
这将从集群中删除该 release,但是这并不代表就完全删除了,我们还可以通过--deleted
参数来显示被删除掉 release:
[root@node01 ~]# helm list --deleted
NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
geared-moth 1 Mon Sep 20 16:01:21 2021 DELETED mysql-0.10.2 5.7.14 default
[root@node01 ~]# helm list --all
NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
geared-moth 1 Mon Sep 20 16:01:21 2021 DELETED mysql-0.10.2 5.7.14 default
mydb 2 Mon Sep 20 17:30:24 2021 DEPLOYED mysql-0.10.2 5.7.14 default
helm list --all则会显示所有的 release,包括已经被删除的
由于 Helm 保留已删除 release 的记录,因此不能重新使用 release 名称。(如果 确实 需要重新使用此 release 名称,则可以使用此 --replace 参数,但它只会重用现有 release 并替换其资源。)这点是不是和 docker container 的管理比较类似
请注意,因为 release 以这种方式保存,所以可以回滚已删除的资源并重新激活它。
如果要彻底删除 release,则需要加上--purge
参数:
[root@node01 ~]# helm delete geared-moth --purge
release "geared-moth" deleted
[root@node01 ~]# helm list --deleted
[root@node01 ~]#
[root@node01 ~]# helm list --all
NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
mydb 2 Mon Sep 20 17:30:24 2021 DEPLOYED mysql-0.10.2 5.7.14 default
3. Helm 模板之内置函数和Values
上节课和大家一起学习了Helm
的一些常用操作方法,这节课来和大家一起定义一个chart
包,了解 Helm 中模板的一些使用方法。
3.1 定义 chart
Helm 的 github 上面有一个比较完整的文档,建议大家好好阅读下该文档,这里我们来一起创建一个chart
包。
一个 chart 包就是一个文件夹的集合,文件夹名称就是 chart 包的名称,比如创建一个 mychart 的 chart 包:
[root@node01 ~]# helm create mychart
Creating mychart
[root@node01 ~]# tree mychart
mychart
├── charts
├── Chart.yaml
├── templates
│ ├── deployment.yaml
│ ├── _helpers.tpl
│ ├── ingress.yaml
│ ├── NOTES.txt
│ └── service.yaml
└── values.yaml
2 directories, 7 files
chart 包的目录上节课我们就已经学习过了,这里我们再来仔细看看 templates 目录下面的文件:
- NOTES.txt:chart 的 “帮助文本”。这会在用户运行 helm install 时显示给用户。
- deployment.yaml:创建 Kubernetes deployment 的基本 manifest
- service.yaml:为 deployment 创建 service 的基本 manifest
- ingress.yaml: 创建 ingress 对象的资源清单文件
- _helpers.tpl:放置模板助手的地方,可以在整个 chart 中重复使用
这里我们明白每一个文件是干嘛的就行,然后我们把 templates 目录下面所有文件全部删除掉,这里我们自己来创建模板文件:
[root@node01 ~]# rm -rf mychart/templates/*.*
3.2 创建模板
这里我们来创建一个非常简单的模板 ConfigMap,在 templates 目录下面新建一个configmap.yaml
文件:
[root@node01 ~]# vim mychart/templates/configmap.yaml
[root@node01 ~]# cat mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-configmap
data:
myvalue: "Hello World"
实际上现在我们就有一个可安装的 chart 包了,通过helm install
命令来进行安装:
[root@node01 ~]# helm install ./mychart/
NAME: doltish-scorpion
LAST DEPLOYED: Mon Sep 20 23:18:29 2021
NAMESPACE: default
STATUS: DEPLOYED
RESOURCES:
==> v1/ConfigMap
NAME DATA AGE
mychart-configmap 1 0s
在上面的输出中,我们可以看到我们的 ConfigMap 资源对象已经创建了。然后使用如下命令我们可以看到实际的模板被渲染过后的资源文件:
[root@node01 ~]# helm get manifest doltish-scorpion
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mychart-configmap
data:
myvalue: "Hello World"
现在我们看到上面的 ConfigMap 文件是不是正是我们前面在模板文件中设计的,现在我们删除当前的release
:
[root@node01 ~]# helm delete doltish-scorpion
release "doltish-scorpion" deleted
3.3 添加一个简单的模板
我们可以看到上面我们定义的 ConfigMap 的名字是固定的,但往往这并不是一种很好的做法,我们可以通过插入 release 的名称来生成资源的名称,比如这里 ConfigMap 的名称我们希望是:ringed-lynx-configmap,这就需要用到 Chart 的模板定义方法了。
Helm Chart 模板使用的是Go语言模板编写而成,并添加了Sprig库中的50多个附件模板函数以及一些其他特殊的函。
需要注意的是kubernetes资源对象的 labels 和 name 定义被限制 63个字符,所以需要注意名称的定义。
现在我们来重新定义下上面的 configmap.yaml 文件:
[root@node01 ~]# vim mychart/templates/configmap.yaml
[root@node01 ~]# cat mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
我们将名称替换成了{{ .Release.Name }}-configmap
,其中包含在{{
和}}
之中的就是模板指令,{{ .Release.Name }}
将 release 的名称注入到模板中来,这样最终生成的 ConfigMap 名称就是以 release 的名称开头的了。这里的 Release 模板对象属于 Helm 内置的一种对象,还有其他很多内置的对象,稍后我们将接触到。
现在我们来重新安装我们的 Chart 包,注意观察 ConfigMap 资源对象的名称:
[root@node01 ~]# helm install ./mychart
NAME: veering-toad
LAST DEPLOYED: Mon Sep 20 23:25:48 2021
NAMESPACE: default
STATUS: DEPLOYED
RESOURCES:
==> v1/ConfigMap
NAME DATA AGE
veering-toad-configmap 1 0s
可以看到现在生成的名称变成了quoting-zebra-configmap,证明已经生效了,当然我们也可以使用命令helm get manifest quoting-zebra
查看最终生成的清单文件的样子。
3.4 调试
我们用模板来生成资源文件的清单,但是如果我们想要调试就非常不方便了,不可能我们每次都去部署一个release
实例来校验模板是否正确,所幸的时 Helm 为我们提供了--dry-run --debug
这个可选参数,在执行helm install
的时候带上这两个参数就可以把对应的 values 值和生成的最终的资源清单文件打印出来,而不会真正的去部署一个release
实例,比如我们来调试上面创建的 chart 包:
[root@node01 ~]# helm install --dry-run --debug ./mychart
[debug] Created tunnel using local port: '46499'
[debug] SERVER: "127.0.0.1:46499"
[debug] Original chart version: ""
[debug] CHART PATH: /root/mychart
NAME: dunking-manta
REVISION: 1
RELEASED: Mon Sep 20 23:48:30 2021
CHART: mychart-0.1.0
USER-SUPPLIED VALUES:
{}
...
service:
port: 80
type: ClusterIP
tolerations: []
HOOKS:
MANIFEST:
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: dunking-manta-configmap
data:
myvalue: "Hello World"
现在我们使用--dry-run
就可以很容易地测试代码了,不需要每次都去安装一个 release 实例了,但是要注意的是这不能确保 Kubernetes 本身就一定会接受生成的模板,在调试完成后,还是需要去安装一个实际的 release 实例来进行验证的。
3.5 内置对象
刚刚我们使用{{.Release.Name}}
将 release 的名称插入到模板中。这里的 Release 就是 Helm 的内置对象,下面是一些常用的内置对象,在需要的时候直接使用就可以:
- Release:这个对象描述了 release 本身。它里面有几个对象:
- Release.Name:release 名称
- Release.Time:release 的时间
- Release.Namespace:release 的 namespace(如果清单未覆盖)
- Release.Service:release 服务的名称(始终是 Tiller)。
- Release.Revision:此 release 的修订版本号,从1开始累加。
- Release.IsUpgrade:如果当前操作是升级或回滚,则将其设置为 true。
- Release.IsInstall:如果当前操作是安装,则设置为 true。
- Values:从
values.yaml
文件和用户提供的文件传入模板的值。默认情况下,Values 是空的。 - Chart:
Chart.yaml
文件的内容。所有的 Chart 对象都将从该文件中获取。chart 指南中Charts Guide列出了可用字段,可以前往查看。 - Files:这提供对 chart 中所有非特殊文件的访问。虽然无法使用它来访问模板,但可以使用它来访问 chart 中的其他文件。请参阅 "访问文件" 部分。
- Files.Get 是一个按名称获取文件的函数(.Files.Get config.ini)
- Files.GetBytes 是将文件内容作为字节数组而不是字符串获取的函数。这对于像图片这样的东西很有用。
- Capabilities:这提供了关于 Kubernetes 集群支持的功能的信息。
- Capabilities.APIVersions 是一组版本信息。
- Capabilities.APIVersions.Has $version 指示是否在群集上启用版本(batch/v1)。
- Capabilities.KubeVersion 提供了查找 Kubernetes 版本的方法。它具有以下值:Major,Minor,GitVersion,GitCommit,GitTreeState,BuildDate,GoVersion,Compiler,和 Platform。
- Capabilities.TillerVersion 提供了查找 Tiller 版本的方法。它具有以下值:SemVer,GitCommit,和 GitTreeState。
- Template:包含有关正在执行的当前模板的信息
- Name:到当前模板的文件路径(例如 mychart/templates/mytemplate.yaml)
- BasePath:当前 chart 模板目录的路径(例如 mychart/templates)。
上面这些值可用于任何顶级模板,要注意内置值始终以大写字母开头。这也符合Go的命名约定。当你创建自己的名字时,你可以自由地使用适合你的团队的惯例。
3.6 values 文件
上面的内置对象中有一个对象就是 Values,该对象提供对传入 chart 的值的访问,Values 对象的值有4个来源:
- chart 包中的 values.yaml 文件
- 父 chart 包的 values.yaml 文件
- 通过 helm install 或者 helm upgrade 的
-f
或者--values
参数传入的自定义的 yaml 文件(上节课我们已经学习过)- 通过
--set
参数传入的值
chart 的 values.yaml 提供的值可以被用户提供的 values 文件覆盖,而该文件同样可以被--set
提供的参数所覆盖。
这里我们来重新编辑 mychart/values.yaml 文件,将默认的值全部清空,添加一个新的数据:(values.yaml)
[root@node01 ~]# vim mychart/values.yaml
[root@node01 ~]# cat mychart/values.yaml
course: k8s
然后我们在上面的 templates/configmap.yaml 模板文件中就可以使用这个值了:(configmap.yaml)
[root@node01 ~]# vim mychart/templates/configmap.yaml
[root@node01 ~]# cat mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
course: {{ .Values.course }}
可以看到最后一行我们是通过{{ .Values.course }}
来获取 course 的值的。现在我们用 debug 模式来查看下我们的模板会被如何渲染:
[root@node01 ~]# helm install --dry-run --debug ./mychart
[debug] Created tunnel using local port: '37197'
[debug] SERVER: "127.0.0.1:37197"
[debug] Original chart version: ""
[debug] CHART PATH: /root/mychart
NAME: garish-waterbuffalo
REVISION: 1
RELEASED: Mon Sep 20 23:52:57 2021
CHART: mychart-0.1.0
USER-SUPPLIED VALUES:
{}
COMPUTED VALUES:
course: k8s
HOOKS:
MANIFEST:
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: garish-waterbuffalo-configmap
data:
myvalue: "Hello World"
course: k8s
我们可以看到 ConfigMap 中 course 的值被渲染成了 k8s,这是因为在默认的 values.yaml 文件中该参数值为 k8s,同样的我们可以通过--set
参数来轻松的覆盖 course 的值:
[root@node01 ~]# helm install --dry-run --debug --set course=python ./mychart
...
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: flippant-hound-configmap
data:
myvalue: "Hello World"
course: python
由于--set
比默认 values.yaml 文件具有更高的优先级,所以我们的模板生成为 course: python。
values 文件也可以包含更多结构化内容,例如,我们在 values.yaml 文件中可以创建 course 部分,然后在其中添加几个键:
[root@node01 ~]# vim mychart/values.yaml
[root@node01 ~]# cat mychart/values.yaml
course:
k8s: devops
python: django
现在我们稍微修改模板:
[root@node01 ~]# vim mychart/templates/configmap.yaml
[root@node01 ~]# cat mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
k8s: {{ .Values.course.k8s }}
python: {{ .Values.course.python }}
同样可以使用 debug 模式查看渲染结果:
[root@node01 ~]# helm install --dry-run --debug ./mychart
...
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: truculent-gorilla-configmap
data:
myvalue: "Hello World"
k8s: devops
python: django
可以看到模板中的参数已经被 values.yaml 文件中的值给替换掉了。虽然以这种方式构建数据是可以的,但我们还是建议保持 value 树浅一些,平一些,这样维护起来要简单一点。
到这里,我们已经看到了几个内置对象的使用方法,并用它们将信息注入到了模板之中。
4. Helm 模板之模板函数与管道
上节课我们学习了如何将信息渲染到模板之中,但是这些信息都是直接传入模板引擎中进行渲染的,有的时候我们想要转换一下这些数据才进行渲染,这就需要使用到 Go 模板语言中的一些其他用法。
4.1 模板函数
比如我们需要从.Values
中读取的值变成字符串的时候就可以通过调用quote模板函数来实现:(templates/configmap.yaml)
[root@node01 ~]# vim mychart/templates/configmap.yaml
[root@node01 ~]# cat mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
k8s: {{ quote .Values.course.k8s }}
python: {{ .Values.course.python }}
模板函数遵循调用的语法为:functionName arg1 arg2...
。在上面的模板文件中,quote .Values.course.k8s
调用quote
函数并将后面的值作为一个参数传递给它。最终被渲染为:
[root@node01 ~]# helm install --dry-run --debug ./mychart
[debug] Created tunnel using local port: '41661'
...
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: vested-iguana-configmap
data:
myvalue: "Hello World"
k8s: "devops"
python: django
我们可以看到.Values.course.k8s
被渲染成了字符串devops
。上节课我们也提到过 Helm 是一种 Go 模板语言,拥有超过60多种可用的内置函数,一部分是由Go 模板语言本身定义的,其他大部分都是Sprig模板库提供的一部分,我们可以前往这两个文档中查看这些函数的用法。
比如我们这里使用的quote
函数就是Sprig 模板库
提供的一种字符串函数,用途就是用双引号将字符串括起来,如果需要双引号"
,则需要添加\
来进行转义,而squote
函数的用途则是用双引号将字符串括起来,而不会对内容进行转义。
所以在我们遇到一些需求的时候,首先要想到的是去查看下上面的两个模板文档中是否提供了对应的模板函数,这些模板函数可以很好的解决我们的需求。
4.2 管道
模板语言除了提供了丰富的内置函数之外,其另一个强大的功能就是管道的概念。和UNIX
中一样,管道我们通常称为Pipeline
,是一个链在一起的一系列模板命令的工具,以紧凑地表达一系列转换。简单来说,管道是可以按顺序完成一系列事情的一种方法。比如我们用管道来重写上面的 ConfigMap 模板:(templates/configmap.yaml)
[root@node01 ~]# vim mychart/templates/configmap.yaml
[root@node01 ~]# cat mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
k8s: {{ .Values.course.k8s | quote }}
python: {{ .Values.course.python }}
这里我们直接调用quote
函数,而是调换了一个顺序,使用一个管道符|将前面的参数发送给后面的模板函数:{{ .Values.course.k8s | quote }}
,使用管道我们可以将几个功能顺序的连接在一起,比如我们希望上面的 ConfigMap 模板中的 k8s 的 value 值被渲染后是大写的字符串,则我们就可以使用管道来修改:(templates/configmap.yaml)
[root@node01 ~]# vim mychart/templates/configmap.yaml
[root@node01 ~]# cat mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
k8s: {{ .Values.course.k8s | upper | quote }}
python: {{ .Values.course.python }}
这里我们在管道中增加了一个upper
函数,该函数同样是Sprig 模板库提供的,表示将字符串每一个字母都变成大写。然后我们用debug
模式来查看下上面的模板最终会被渲染成什么样子:
[root@node01 ~]# helm install --dry-run --debug ./mychart
[debug] Created tunnel using local port: '44004'
...
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: jaundiced-giraffe-configmap
data:
myvalue: "Hello World"
k8s: "DEVOPS"
python: django
我们可以看到之前我们的devops
已经被渲染成了"DEVOPS"
了,要注意的是使用管道操作的时候,前面的操作结果会作为参数传递给后面的模板函数,比如我们这里希望将上面模板中的 python 的值渲染为重复出现3次的字符串,则我们就可以使用到Sprig 模板库提供的repeat
函数,不过该函数需要传入一个参数repeat COUNT STRING
表示重复的次数:(templates/configmap.yaml)
[root@node01 ~]# vim mychart/templates/configmap.yaml
[root@node01 ~]# cat mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
k8s: {{ .Values.course.k8s | upper | quote }}
python: {{ .Values.course.python | quote | repeat 3 }}
该repeat
函数会将给定的字符串重复3次返回,所以我们将得到这个输出:
[root@node01 ~]# helm install --dry-run --debug ./mychart
[debug] Created tunnel using local port: '35730'
[debug] SERVER: "127.0.0.1:35730"
[debug] Original chart version: ""
[debug] CHART PATH: /root/mychart
Error: YAML parse error on mychart/templates/configmap.yaml: error converting YAML to JSON: yaml: line 7: did not find expected key
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: dining-goose-configmap
data:
myvalue: "Hello World"
k8s: "DEVOPS"
python: "django""django""django"
我们可以看到上面的输出中 python 对应的值变成了3个相同的字符串,这显然是不符合我们预期的,我们的预期是形成一个字符串,而现在是3个字符串了,而且上面还有错误信息,根据管道处理的顺序,我们将quote
函数放到repeat
函数后面去是不是就可以解决这个问题了:(templates/configmap.yaml)
[root@node01 ~]# vim mychart/templates/configmap.yaml
[root@node01 ~]# cat mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
k8s: {{ .Values.course.k8s | upper | quote }}
python: {{ .Values.course.python | repeat 3 | quote }}
现在是不是就是先重复3次.Values.course.python
的值,然后调用quote
函数:
[root@node01 ~]# helm install --dry-run --debug ./mychart
[debug] Created tunnel using local port: '34845'
...
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: precise-lionfish-configmap
data:
myvalue: "Hello World"
k8s: "DEVOPS"
python: "djangodjangodjango"
现在是不是就正常了,也得到了我们的预期结果,所以我们在使用管道操作的时候一定要注意是按照从前到后一步一步顺序处理的。
4.3 default 函数
另外一个我们会经常使用的一个函数是default 函数
:default DEFAULT_VALUE GIVEN_VALUE
。该函数允许我们在模板内部指定默认值,以防止该值被忽略掉了。比如我们来修改上面的 ConfigMap 的模板:(templates/configmap.yaml)
[root@node01 ~]# vim mychart/templates/configmap.yaml
[root@node01 ~]# cat mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: {{ .Values.hello | default "Hello World" | quote }}
k8s: {{ .Values.course.k8s | upper | quote }}
python: {{ .Values.course.python | repeat 5 | quote }}
由于我们的values.yaml
文件中只定义了 course 结构的信息,并没有定义 hello 的值,所以如果没有设置默认值的话是得不到{{ .Values.hello }}
的值的,这里我们为该值定义了一个默认值:Hello World
,所以现在如果在values.yaml
文件中没有定义这个值,则我们也可以得到默认值:
[root@node01 ~]# helm install --dry-run --debug ./mychart
[debug] Created tunnel using local port: '36302'
...
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: exiled-ferret-configmap
data:
myvalue: "Hello World"
k8s: "DEVOPS"
python: "djangodjangodjangodjangodjango"
我们可以看到myvalue
值被渲染成了Hello World,证明我们的默认值生效了。
5. Helm 模板之控制流程
模板函数和管道
是通过转换信息并将其插入到YAML文
件中的强大方法。但有时候需要添加一些比插入字符串更复杂一些的模板逻辑。这就需要使用到模板语言中提供的控制结构了。
控制流程为我们提供了控制模板生成流程的一种能力,Helm 的模板语言提供了以下几种流程控制:
if/else
条件块with
指定范围range
循环块
除此之外,它还提供了一些声明和使用命名模板段的操作:
define
在模板中声明一个新的命名模板template
导入一个命名模板block
声明了一种特殊的可填写的模板区域
关于命名模板
的相关知识点,我们会在后面的课程中和大家接触到,这里我们暂时和大家介绍if/else
、with
、range
这3中控制流程的用法。
5.1 if/else 条件
if/else
块是用于在模板中有条件地包含文本块的方法,条件块的基本结构如下:
{{ if PIPELINE }}
# Do something
{{ else if OTHER PIPELINE }}
# Do something else
{{ else }}
# Default case
{{ end }}
当然要使用条件块就得判断条件是否为真,如果值为下面的几种情况,则管道的结果为 false:
- 一个布尔类型的
假
- 一个数字
零
- 一个
空
的字符串- 一个
nil
(空或null
)- 一个空的集合(
map
、slice
、tuple
、dict
、array
)
除了上面的这些情况外,其他所有条件都为真
。
同样还是以上面的 ConfigMap 模板文件为例,添加一个简单的条件判断,如果 python 被设置为 django,则添加一个web: true
:(tempaltes/configmap.yaml)
[root@node01 ~]# vim mychart/templates/configmap.yaml
[root@node01 ~]# cat mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: {{ .Values.hello | default "Hello World" | quote }}
k8s: {{ .Values.course.k8s | upper | quote }}
python: {{ .Values.course.python | repeat 3 | quote }}
{{ if eq .Values.course.python "django" }}web: true{{ end }}
在上面的模板文件中我们增加了一个条件语句判断{{ if eq .Values.course.python "django" }}web: true{{ end }}
,其中运算符eq
是判断是否相等的操作,除此之外,还有ne
、lt
、gt
、and
、or
等运算符都是 Helm 模板已经实现了的,直接使用即可。这里我们{{ .Values.course.python }}
的值在values.yaml
文件中默认被设置为了django,所以正常来说下面的条件语句判断为真
,所以模板文件最终被渲染后会有web: true
这样的的一个条目:
[root@node01 ~]# helm install --dry-run --debug ./mychart
[debug] Created tunnel using local port: '36977'
...
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: dandy-crab-configmap
data:
myvalue: "Hello World"
k8s: "DEVOPS"
python: "djangodjangodjango"
web: true
可以看到上面模板被渲染后出现了web: true
的条目,如果我们在安装的时候覆盖下 python 的值呢,比如我们改成 ai:
[root@node01 ~]# helm install --dry-run --debug --set course.python=ai ./mychart
[debug] Created tunnel using local port: '45502'
...
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: banking-fish-configmap
data:
myvalue: "Hello World"
k8s: "DEVOPS"
python: "aiaiai"
根据我们模板文件中的定义,如果{{ .Values.course.python }}
的值为django
的话就会新增web: true
这样的一个条目,但是现在我们是不是通过参数--set
将值设置为了 ai,所以这里条件判断为假,正常来说就不应该出现这个条目了,上面我们通过 debug 模式查看最终被渲染的值也没有出现这个条目,证明条件判断是正确的。
5.2 空格控制
上面我们的条件判断语句是在一整行中的,如果平时经常写代码的同学可能非常不习惯了,我们一般会将其格式化为更容易阅读的形式,比如:
[root@node01 ~]# vim mychart/templates/configmap.yaml
[root@node01 ~]# cat mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: {{ .Values.hello | default "Hello World" | quote }}
k8s: {{ .Values.course.k8s | upper | quote }}
python: {{ .Values.course.python | repeat 3 | quote }}
{{ if eq .Values.course.python "django" }}
web: true
{{ end }}
这样的话看上去比之前要清晰很多了,但是我们通过模板引擎来渲染一下,会得到如下结果:
[root@node01 ~]# helm install --dry-run --debug ./mychart
[debug] Created tunnel using local port: '43335'
...
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: pioneering-zebra-configmap
data:
myvalue: "Hello World"
k8s: "DEVOPS"
python: "djangodjangodjango"
web: true
我们可以看到渲染出来会有多余的空行,这是因为当模板引擎运行时,它将一些值渲染过后,之前的指令被删除,但它之前所占的位置完全按原样保留剩余的空白了,所以就出现了多余的空行。YAML
文件中的空格是非常严格的,所以对于空格的管理非常重要,一不小心就会导致你的YAML
文件格式错误。
我们可以通过使用在模板标识{{
后面添加破折号和空格{{-
来表示将空白左移,而在}}
前面添加一个空格和破折号-}}
表示应该删除右边的空格,另外需要注意的是换行符也是空格!
使用这个语法,我们来修改我们上面的模板文件去掉多余的空格:(templates/configmap.yaml)
[root@node01 ~]# vim mychart/templates/configmap.yaml
[root@node01 ~]# cat mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: {{ .Values.hello | default "Hello World" | quote }}
k8s: {{ .Values.course.k8s | upper | quote }}
python: {{ .Values.course.python | repeat 3 | quote }}
{{- if eq .Values.course.python "django" }}
web: true
{{- end }}
现在我们来查看上面模板渲染过后的样子:
[root@node01 ~]# helm install --dry-run --debug ./mychart
[debug] Created tunnel using local port: '38602'
...
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: quieting-deer-configmap
data:
myvalue: "Hello World"
k8s: "DEVOPS"
python: "djangodjangodjango"
web: true
现在是不是没有多余的空格了,另外我们需要谨慎使用-}},比如上面模板文件中:
python: {{ .Values.course.python | repeat 3 | quote }}
{{- if eq .Values.course.python "django" -}}
web: true
{{- end }}
如果我们在if条件后面增加-}},这会渲染成:
python: "djangodjangodjango"web: true
因为-}}
它删除了双方的换行符,显然这是不正确的。
有关模板中空格控制的详细信息,请参阅官方 Go 模板文档Official Go template documentation
5.3 使用 with 修改范围
接下来我们来看下with
关键词的使用,它用来控制变量作用域。还记得之前我们的{{ .Release.xxx }}
或者{{ .Values.xxx }}
吗?其中的.就是表示对当前范围的引用,.Values
就是告诉模板在当前范围中查找Values
对象的值。而with
语句就可以来控制变量的作用域范围,其语法和一个简单的if
语句比较类似:
{{ with PIPELINE }}
# restricted scope
{{ end }}
with
语句可以允许将当前范围.设置为特定的对象,比如我们前面一直使用的.Values.course
,我们可以使用with
来将.范围指向.Values.course
:(templates/configmap.yaml)
[root@node01 ~]# vim mychart/templates/configmap.yaml
[root@node01 ~]# cat mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: {{ .Values.hello | default "Hello World" | quote }}
{{- with .Values.course }}
k8s: {{ .k8s | upper | quote }}
python: {{ .python | repeat 3 | quote }}
{{- if eq .python "django" }}
web: true
{{- end }}
{{- end }}
可以看到上面我们增加了一个{{- with .Values.course }}xxx{{- end }}
的一个块,这样的话我们就可以在当前的块里面直接引用.python
和.k8s
了,而不需要进行限定了,这是因为该with
声明将.指向了.Values.course
,在{{- end }}
后.就会复原其之前的作用范围了,我们可以使用模板引擎来渲染上面的模板查看是否符合预期结果。
[root@node01 ~]# helm install --dry-run --debug ./mychart
[debug] Created tunnel using local port: '39377'
...
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: intentional-goose-configmap
data:
myvalue: "Hello World"
k8s: "DEVOPS"
python: "djangodjangodjango"
web: true
不过需要注意的是在with
声明的范围内,此时将无法从父范围访问到其他对象了,比如下面的模板渲染的时候将会报错,因为显然.Release
根本就不在当前的.
范围内,当然如果我们最后两行交换下位置就正常了,因为{{- end }}
之后范围就被重置了:
{{- with .Values.course }}
k8s: {{ .k8s | upper | quote }}
python: {{ .python | repeat 3 | quote }}
release: {{ .Release.Name }}
{{- end }}
5.4 range 循环
如果大家对编程语言熟悉的话,几乎所有的编程语言都支持类似于for
、foreach
或者类似功能的循环机制,在 Helm 模板语言中,是使用range
关键字来进行循环操作。
我们在values.yaml
文件中添加上一个课程列表:
[root@node01 ~]# vim mychart/values.yaml
[root@node01 ~]# cat mychart/values.yaml
course:
k8s: devops
python: django
courselist:
- k8s
- python
- search
- golang
现在我们有一个课程列表,修改 ConfigMap 模板文件来循环打印出该列表:
[root@node01 ~]# vim mychart/templates/configmap.yaml
[root@node01 ~]# cat mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: {{ .Values.hello | default "Hello World" | quote }}
{{- with .Values.course }}
k8s: {{ .k8s | upper | quote }}
python: {{ .python | repeat 3 | quote }}
{{- if eq .python "django" }}
web: true
{{- end }}
{{- end }}
courselist:
{{- range .Values.courselist }}
- {{ . | title | quote }}
{{- end }}
可以看到最下面我们使用了一个range
函数,该函数将会遍历{{ .Values.courselist }}
列表,循环内部我们使用的是一个.
,这是因为当前的作用域就在当前循环内,这个.从列表的第一个元素一直遍历到最后一个元素,然后在遍历过程中使用了title
和quote
这两个函数,前面这个函数是将字符串首字母变成大写,后面就是加上双引号变成字符串,所以按照上面这个模板被渲染过后的结果为:
[root@node01 ~]# helm install --dry-run --debug ./mychart
[debug] Created tunnel using local port: '39867'
...
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: banking-cow-configmap
data:
myvalue: "Hello World"
k8s: "DEVOPS"
python: "djangodjangodjango"
web: true
courselist:
- "K8s"
- "Python"
- "Search"
- "Golang"
- "Golang"
我们可以看到courselist
按照我们的要求循环出来了。除了 list 或者 tuple,range 还可以用于遍历具有键和值的集合(如map 或 dict),这个就需要用到变量的概念了。
5.5 变量
前面我们已经学习了函数、管理以及控制流程的使用方法,我们知道编程语言中还有一个很重要的概念叫:变量,在 Helm 模板中,使用变量的场合不是特别多,但是在合适的时候使用变量可以很好的解决我们的问题。如下面的模板:
{{- with .Values.course }}
k8s: {{ .k8s | upper | quote }}
python: {{ .python | repeat 3 | quote }}
release: {{ .Release.Name }}
{{- end }}
我们在with
语句块内添加了一个.Release.Name
对象,但这个模板是错误的,编译的时候会失败,这是因为.Release.Name
不在该with
语句块限制的作用范围之内,我们可以将该对象赋值给一个变量可以来解决这个问题:
[root@node01 ~]# vim mychart/templates/configmap.yaml
[root@node01 ~]# cat mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
{{- $releaseName := .Release.Name -}}
{{- with .Values.course }}
k8s: {{ .k8s | upper | quote }}
python: {{ .python | repeat 3 | quote }}
release: {{ $releaseName }}
{{- end }}
我们可以看到我们在with
语句上面增加了一句{{- $releaseName := .Release.Name -}}
,其中$releaseName
就是后面的对象的一个引用变量,它的形式就是$name
,赋值操作使用:=
,这样with
语句块内部的$releaseName
变量仍然指向的是.Release.Name
,同样,我们 DEBUG 下查看结果:
[root@node01 ~]# helm install --dry-run --debug ./mychart
[debug] Created tunnel using local port: '45820'
...
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: musty-cheetah-configmap
data:
k8s: "DEVOPS"
python: "djangodjangodjango"
release: musty-cheetah
可以看到已经正常了,另外变量在range
循环中也非常有用,我们可以在循环中用变量来同时捕获索引的值:
[root@node01 ~]# vim mychart/templates/configmap.yaml
[root@node01 ~]# cat mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
courselist:
{{- range $index, $course := .Values.courselist }}
- {{ $index }}: {{ $course | title | quote }}
{{- end }}
例如上面的这个列表,我们在range
循环中使用$index
和$course
两个变量来接收后面列表循环的索引和对应的值,最终可以得到如下结果:
[root@node01 ~]# helm install --dry-run --debug ./mychart
[debug] Created tunnel using local port: '46112'
...
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: hasty-pug-configmap
data:
k8s: "DEVOPS"
python: "djangodjangodjango"
release: hasty-pug
courselist:
- 0: "K8s"
- 1: "Python"
- 2: "Search"
- 3: "Golang"
我们可以看到 courselist 下面将索引和对应的值都打印出来了,实际上具有键和值的数据结构我们都可以使用range
来循环获得二者的值,比如我们可以对.Values.course
这个字典来进行循环:
[root@node01 ~]# vim mychart/templates/configmap.yaml
[root@node01 ~]# cat mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
{{- range $key, $value := .Values.course }}
{{ $key }}: {{ $value | quote }}
{{- end }}
[root@node01 ~]# helm install --dry-run --debug ./mychart
[debug] Created tunnel using local port: '34481'
...
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: gilded-seagull-configmap
data:
k8s: "devops"
python: "django"
直接使用range
循环,用变量$key
、$value
来接收字段.Values.course
的键和值。这就是变量在 Helm 模板中的使用方法。
6. Helm模板之命名模板
前面我们学习了一些 Helm 模板中的一些常用使用方法,但是我们都是操作的一个模板文件,在实际的应用中,很多都是相对比较复杂的,往往会超过一个模板,如果有多个应用模板,我们应该如何进行处理呢?这就需要用到新的概念:命名模板。
命名模板我们也可以称为子模板,是限定在一个文件内部的模板,然后给一个名称。在使用命名模板的时候有一个需要特别注意的是:模板名称是全局的,如果我们声明了两个相同名称的模板,最后加载的一个模板会覆盖掉另外的模板,由于子 chart 中的模板也是和顶层的模板一起编译的,所以在命名的时候一定要注意,不要重名了。
为了避免重名,有个通用的约定就是为每个定义的模板添加上 chart 名称:{{define "mychart.labels"}}
,define
关键字就是用来声明命名模板的,加上 chart 名称就可以避免不同 chart 间的模板出现冲突的情况。
6.1 声明和使用命名模板
使用define
关键字就可以允许我们在模板文件内部创建一个命名模板,它的语法格式如下:
{{ define "ChartName.TplName" }}
# 模板内容区域
{{ end }}
比如,现在我们可以定义一个模板来封装一个 label 标签:
{{- define "mychart.labels" }}
labels:
from: helm
date: {{ now | htmlDate }}
{{- end }}
然后我们可以将该模板嵌入到现有的 ConfigMap 中,然后使用template
关键字在需要的地方包含进来即可:
[root@node01 ~]# vim mychart/templates/configmap.yaml
[root@node01 ~]# cat mychart/templates/configmap.yaml
{{- define "mychart.labels" }}
labels:
from: helm
date: {{ now | htmlDate }}
{{- end }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
{{- template "mychart.labels" }}
data:
{{- range $key, $value := .Values.course }}
{{ $key }}: {{ $value | quote }}
{{- end }}
我们这个模板文件被渲染过后的结果如下所示:
[root@node01 ~]# helm install --dry-run --debug ./mychart
[debug] Created tunnel using local port: '40254'
...
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: singed-cardinal-configmap
labels:
from: helm
date: 2021-09-21
data:
k8s: "devops"
python: "django"
我们可以看到define
区域定义的命名模板被嵌入到了template
所在的区域,但是如果我们将命名模板全都写入到一个模板文件中的话无疑也会增大模板的复杂性。
还记得我们在创建 chart 包的时候,templates 目录下面默认会生成一个_helpers.tpl
文件吗?我们前面也提到过 templates 目录下面除了NOTES.txt
文件和以下划线_
开头命令的文件之外,都会被当做 kubernetes 的资源清单文件,而这个下划线开头的文件不会被当做资源清单外,还可以被其他 chart 模板中调用,这个就是 Helm 中的partials
文件,所以其实我们完全就可以将命名模板定义在这些partials
文件中,默认就是_helpers.tpl
文件了。
现在我们将上面定义的命名模板移动到 templates/_helpers.tpl 文件中去:
{{/* 生成基本的 labels 标签 */}}
{{- define "mychart.labels" }}
labels:
from: helm
date: {{ now | htmlDate }}
{{- end }}
一般情况下面,我们都会在命名模板头部加一个简单的文档块,用/**/
包裹起来,用来描述我们这个命名模板的用途的。
现在我们讲命名模板从模板文件 templates/configmap.yaml 中移除,当然还是需要保留 template 来嵌入命名模板内容,名称还是之前的 mychart.lables,这是因为模板名称是全局的,所以我们可以能够直接获取到。我们再用 DEBUG 模式来调试下是否符合预期?
6.2 模板范围
上面我们定义的命名模板中,没有使用任何对象,只是使用了一个简单的函数,如果我们在里面来使用 chart 对象相关信息呢:
[root@node01 ~]# vim mychart/templates/_helpers.tpl
[root@node01 ~]# cat mychart/templates/_helpers.tpl
{{/* 生成基本的 labels 标签 */}}
{{- define "mychart.labels" }}
labels:
from: helm
date: {{ now | htmlDate }}
chart: {{ .Chart.Name }}
version: {{ .Chart.Version }}
{{- end }}
当命名模板被渲染时,它会接收由 template 调用时传入的作用域,我们还要在 template 后面加上作用域范围即可:
[root@node01 ~]# vim mychart/templates/configmap.yaml
[root@node01 ~]# cat mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
{{- template "mychart.labels" . }}
data:
{{- range $key, $value := .Values.course }}
{{ $key }}: {{ $value | quote }}
{{- end }}
我们在 template 末尾传递了.
,表示当前的最顶层的作用范围,如果我们想要在命名模板中使用.Values
范围内的数据,当然也是可以的,现在我们再来渲染下我们的模板:
[root@node01 ~]# helm install --dry-run --debug ./mychart
[debug] Created tunnel using local port: '34355'
...
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: crazy-lionfish-configmap
labels:
from: helm
date: 2021-09-21
chart: mychart
version: 0.1.0
data:
k8s: "devops"
python: "django"
我们可以看到 chart 的名称和版本号都已经被正常渲染出来了。
6.3 include 函数
假如现在我们将上面的定义的 labels 单独提取出来放置到 _helpers.tpl 文件中:
[root@node01 ~]# vim mychart/templates/_helpers.tpl
[root@node01 ~]# cat mychart/templates/_helpers.tpl
{{/* 生成基本的 labels 标签 */}}
{{- define "mychart.labels" }}
from: helm
date: {{ now | htmlDate }}
chart: {{ .Chart.Name }}
version: {{ .Chart.Version }}
{{- end }}
现在我们将该命名模板插入到 configmap 模板文件的 labels 部分和 data 部分:
[root@node01 ~]# vim mychart/templates/configmap.yaml
[root@node01 ~]# cat mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
labels:
{{- template "mychart.labels" . }}
data:
{{- range $key, $value := .Values.course }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- template "mychart.labels" . }}
然后同样的查看下渲染的结果:
[root@node01 ~]# helm install --dry-run --debug ./mychart
[debug] Created tunnel using local port: '44078'
...
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: incendiary-lionfish-configmap
labels:
from: helm
date: 2021-09-21
chart: mychart
version: 0.1.0
data:
k8s: "devops"
python: "django"
from: helm
date: 2021-09-21
chart: mychart
version: 0.1.0
我们可以看到渲染结果是有问题的,不是一个正常的 YAML 文件格式,这是因为template
只是表示一个嵌入动作而已,不是一个函数,所以原本命名模板中是怎样的格式就是怎样的格式被嵌入进来了,比如我们可以在命名模板中给内容区域都空了两个空格,再来查看下渲染的结构:
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mortal-cricket-configmap
labels:
from: helm
date: 2021-09-21
chart: mychart
version: 0.1.0
data:
k8s: "devops"
python: "django"
from: helm
date: 2021-09-21
chart: mychart
version: 0.1.0
我们可以看到 data 区域里面的内容是渲染正确的,但是上面 labels 区域是不正常的,因为命名模板里面的内容是属于 labels 标签的,是不符合我们的预期的,但是我们又不可能再去把命名模板里面的内容再增加两个空格,因为这样的话 data 里面的格式又不符合预期了。
为了解决这个问题,Helm 提供了另外一个方案来代替template
,那就是使用include
函数,在需要控制空格的地方使用indent
管道函数来自己控制,比如上面的例子我们替换成include
函数:
[root@node01 ~]# vim mychart/templates/configmap.yaml
[root@node01 ~]# cat mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
labels:
{{- include "mychart.labels" . | indent 4 }}
data:
{{- range $key, $value := .Values.course }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- include "mychart.labels" . | indent 2 }}
在 labels 区域我们需要4个空格,所以在管道函数indent
中,传入参数4就可以,而在 data 区域我们只需要2个空格,所以我们传入参数2即可以,现在我们来渲染下我们这个模板看看是否符合预期呢:
[root@node01 ~]# helm install --dry-run --debug ./mychart
[debug] Created tunnel using local port: '39001'
...
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: ugly-mouse-configmap
labels:
from: helm
date: 2021-09-21
chart: mychart
version: 0.1.0
data:
k8s: "devops"
python: "django"
from: helm
date: 2021-09-21
chart: mychart
version: 0.1.0
可以看到是符合我们的预期,所以在 Helm 模板中我们使用 include 函数要比 template 更好,可以更好地处理 YAML 文件输出格式。
7. Helm模板之其他注意事项
上节课我们学习了命名模板的使用,命名模板是 Helm 模板中非常重要的一个功能,在我们实际开发 Helm Chart 包的时候非常有用,到这里我们基本上就把 Helm 模板中经常使用到的一些知识点和大家介绍完了。但是仍然还是有一些在开发中值得我们注意的一些知识点,比如 NOTES.txt 文件的使用、子 Chart 的使用、全局值的使用,这节课我们就来和大家一起了解下这些知识点。
7.1 NOTES.txt 文件
我们前面在使用 helm install 命令的时候,Helm 都会为我们打印出一大堆介绍信息,这样当别的用户在使用我们的 chart 包的时候就可以根据这些注释信息快速了解我们的 chart 包的使用方法,这些信息就是编写在 NOTES.txt 文件之中的,这个文件是纯文本的,但是它和其他模板一样,具有所有可用的普通模板函数和对象。
现在我们在前面的示例中 templates 目录下面创建一个 NOTES.txt 文件:
[root@node01 ~]# vim mychart/templates/NOTES.txt
[root@node01 ~]# cat mychart/templates/NOTES.txt
Thank you for installing {{ .Chart.Name }}.
Your release is named {{ .Release.Name }}.
To learn more about the release, try:
$ helm status {{ .Release.Name }}
$ helm get {{ .Release.Name }}
我们可以看到我们在 NOTES.txt 文件中也使用 Chart 和 Release 对象,现在我们在 mychart 包根目录下面执行安装命令查看是否能够得到上面的注释信息:
[root@node01 ~]# helm install ./mychart
NAME: eloping-fly
LAST DEPLOYED: Tue Sep 21 23:07:31 2021
NAMESPACE: default
STATUS: DEPLOYED
RESOURCES:
==> v1/ConfigMap
NAME DATA AGE
eloping-fly-configmap 6 0s
NOTES:
Thank you for installing mychart.
Your release is named eloping-fly.
To learn more about the release, try:
$ helm status eloping-fly
$ helm get eloping-fly
现在已经安装成功了,而且下面的注释部分也被渲染出来了,我们可以看到 NOTES.txt 里面使用到的模板对象都被正确渲染了。
为我们创建的 chart 包提供一个清晰的 NOTES.txt 文件是非常有必要的,可以为用户提供有关如何使用新安装 chart 的详细信息,这是一种非常友好的方式方法。
7.2 子 chart 包
我们到目前为止都只用了一个 chart,但是 chart 也可以有 子 chart 的依赖关系,它们也有自己的值和模板,在学习字 chart 之前,我们需要了解几点关于子 chart 的说明:
- 子 chart 是独立的,所以子 chart 不能明确依赖于其父 chart
- 子 chart 无法访问其父 chart 的值
- 父 chart 可以覆盖子 chart 的值
- Helm 中有全局值的概念,可以被所有的 chart 访问
7.3 创建子 chart
现在我们就来创建一个子 chart,还记得我们在创建 mychart 包的时候,在根目录下面有一个空文件夹 charts 目录吗?这就是我们的子 chart 所在的目录,在该目录下面添加一个新的 chart:
[root@node01 ~]# cd mychart/charts/
[root@node01 charts]# helm create mysubchart
Creating mysubchart
[root@node01 charts]# rm -rf mysubchart/templates/*.*
[root@node01 charts]# tree ..
..
├── charts
│ └── mysubchart
│ ├── charts
│ ├── Chart.yaml
│ ├── templates
│ └── values.yaml
├── Chart.yaml
├── templates
│ ├── configmap.yaml
│ ├── config.yaml
│ ├── _helpers.tpl
│ └── NOTES.txt
└── values.yaml
5 directories, 8 files
同样的,我们将子 chart 模板中的文件全部删除了,接下来,我们为子 chart 创建一个简单的模板和 values 文件了。
[root@node01 charts]# cat > mysubchart/values.yaml <<EOF
> in: mysub
> EOF
[root@node01 charts]# cat mysubchart/values.yaml
in: mysub
[root@node01 charts]# cat > mysubchart/templates/configmap.yaml <<EOF
> apiVersion: v1
> kind: ConfigMap
> metadata:
> name: {{ .Release.Name }}-configmap2
> data:
> in: {{ .Values.in }}
> EOF
[root@node01 charts]# cat mysubchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap2
data:
in: {{ .Values.in }}
我们上面已经提到过每个子 chart 都是独立的 chart,所以我们可以单独给 mysubchart 进行测试:
[root@node01 charts]# helm install --dry-run --debug ./mysubchart
[debug] Created tunnel using local port: '42049'
...
---
# Source: mysubchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: cold-peahen-configmap2
data:
in: mysub
我们可以看到正常渲染出了结果。
7.4 值覆盖
现在 mysubchart 这个子 chart 就属于 mychart 这个父 chart 了,由于 mychart 是父级,所以我们可以在 mychart 的 values.yaml 文件中直接配置子 chart 中的值,比如我们可以在 mychart/values.yaml 文件中添加上子 chart 的值:
[root@node01 ~]# vim mychart/values.yaml
[root@node01 ~]# cat mychart/values.yaml
course:
k8s: devops
python: django
courselist:
- k8s
- python
- search
- golang
mysubchart:
in: parent
注意最后两行,mysubchart 部分内的任何指令都会传递到 mysubchart 这个子 chart 中去的,现在我们在 mychart 根目录中执行调试命令,可以查看到子 chart 也被一起渲染了:
[root@node01 ~]# helm install --dry-run --debug ./mychart
[debug] Created tunnel using local port: '44314'
...
---
# Source: mychart/charts/mysubchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: hazy-albatross-configmap2
data:
in: parent
---
# Source: mychart/templates/configmap.yaml
...
我们可以看到子 chart 中的值已经被顶层的值给覆盖了。但是在某些场景下面我们还是希望某些值在所有模板中都可以使用,这就需要用到全局 chart 值了。
7.5 全局值
全局值可以从任何 chart 或者子 chart中进行访问使用,values 对象中有一个保留的属性是Values.global
,就可以被用来设置全局值,比如我们在父 chart 的 values.yaml 文件中添加一个全局值:
[root@node01 ~]# vim mychart/values.yaml
[root@node01 ~]# cat mychart/values.yaml
course:
k8s: devops
python: django
courselist:
- k8s
- python
- search
- golang
mysubchart:
in: parent
global:
allin: helm
我们在 values.yaml 文件中添加了一个 global 的属性,这样的话无论在父 chart 中还是在子 chart 中我们都可以通过{{ .Values.global.allin }}
来访问这个全局值了。比如我们在 mychart/templates/configmap.yaml 和 mychart/charts/mysubchart/templates/configmap.yaml 文件的 data 区域下面都添加上如下内容:
...
data:
allin: {{ .Values.global.allin }}
...
现在我们在 mychart 根目录下面执行 debug 调试模式:
[root@node01 ~]# helm install --dry-run --debug ./mychart
[debug] Created tunnel using local port: '32908'
...
---
# Source: mychart/charts/mysubchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: joyous-lambkin-configmap
data:
allin: helm
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: joyous-lambkin-configmap
data:
allin: helm
我们可以看到两个模板中都输出了allin: helm
这样的值,全局变量对于传递这样的信息非常有用,不过也要注意我们不能滥用全局值。
另外值得注意的是我们在学习命名模板的时候就提到过父 chart 和子 chart 可以共享模板。任何 chart 中的任何定义块都可用于其他 chart,所以我们在给命名模板定义名称的时候添加了 chart 名称这样的前缀,避免冲突。
8. Helm Hooks
和 Kubernetes 里面的容器一样,Helm 也提供了 Hook 的机制,允许 chart 开发人员在 release 的生命周期中的某些节点来进行干预,比如我们可以利用 Hooks 来做下面的这些事情:
- 在加载任何其他 chart 之前,在安装过程中加载 ConfigMap 或 Secret
- 在安装新 chart 之前执行作业以备份数据库,然后在升级后执行第二个作业以恢复数据
- 在删除 release 之前运行作业,以便在删除 release 之前优雅地停止服务
值得注意的是 Hooks 和普通模板一样工作,但是它们具有特殊的注释,可以使 Helm 以不同的方式使用它们。
Hook 在资源清单中的 metadata 部分用 annotations 的方式进行声明:
apiVersion: ...
kind: ....
metadata:
annotations:
"helm.sh/hook": "pre-install"
# ...
接下来我们就来和大家介绍下 Helm Hooks 的一些基本使用方法。
8.1 Hooks
在 Helm 中定义了如下一些可供我们使用的 Hooks:
- 预安装
pre-install
:在模板渲染后,kubernetes 创建任何资源之前执行- 安装后
post-install
:在所有 kubernetes 资源安装到集群后执行- 预删除
pre-delete
:在从 kubernetes 删除任何资源之前执行删除请求- 删除后
post-delete
:删除所有 release 的资源后执行- 升级前
pre-upgrade
:在模板渲染后,但在任何资源升级之前执行- 升级后
post-upgrade
:在所有资源升级后执行- 预回滚
pre-rollback
:在模板渲染后,在任何资源回滚之前执行- 回滚后
post-rollback
:在修改所有资源后执行回滚请求crd-install
:在运行其他检查之前添加 CRD 资源,只能用于 chart 中其他的资源清单定义的 CRD 资源。
8.2 生命周期
Hooks 允许开发人员在 release 的生命周期中的一些关键节点执行一些钩子函数,我们正常安装一个 chart 包的时候的生命周期如下所示:
- 用户运行
helm install foo
- chart 被加载到服务端 Tiller Server 中
- 经过一些验证,Tiller Server 渲染 foo 模板
- Tiller 将产生的资源加载到 kubernetes 中去
- Tiller 将 release 名称和其他数据返回给 Helm 客户端
- Helm 客户端退出
如果开发人员在 install 的生命周期中定义了两个 hook:pre-install
和post-install
,那么我们安装一个 chart 包的生命周期就会多一些步骤了:
- 用户运行
helm install foo
- chart 被加载到服务端 Tiller Server 中
- 经过一些验证,Tiller Server 渲染 foo 模板
- Tiller 将 hook 资源加载到 kubernetes 中,准备执行
pre-install
hook- Tiller 会根据权重对 hook 进行排序(默认分配权重0,权重相同的 hook 按升序排序)
- Tiller 然后加载最低权重的 hook
- Tiller 等待,直到 hook 准备就绪
- Tiller 将产生的资源加载到 kubernetes 中
- Tiller 执行
post-install
hook- Tiller 等待,直到 hook 准备就绪
- Tiller 将 release 名称和其他数据返回给客户端
- Helm 客户端退出
等待 hook 准备就绪,这是一个阻塞的操作,如果 hook 中声明的是一个 Job 资源,那么 Tiller 将等待 Job 成功完成,如果失败,则发布失败,在这个期间,Helm 客户端是处于暂停状态的。
对于所有其他类型,只要 kubernetes 将资源标记为加载(添加或更新),资源就被视为就绪状态,当一个 hook 声明了很多资源是,这些资源是被串行执行的。
另外需要注意的是 hook 创建的资源不会作为 release 的一部分进行跟踪和管理,一旦 Tiller Server 验证了 hook 已经达到了就绪状态,它就不会去管它了。
所以,如果我们在 hook 中创建了资源,那么不能依赖helm delete
去删除资源,因为 hook 创建的资源已经不受控制了,要销毁这些资源,需要在pre-delete
或者post-delete
这两个 hook 函数中去执行相关操作,或者将helm.sh/hook-delete-policy
这个 annotation 添加到 hook 模板文件中。
8.3 写一个 hook
上面我们也说了 hook 和普通模板一样,也可以使用普通的模板函数和常用的一些对象,比如Values
、Chart
、Release
等等,唯一和普通模板不太一样的地方就是在资源清单文件中的 metadata 部分会有一些特殊的注释 annotation。
例如,现在我们来创建一个 hook,在前面的示例 templates 目录中添加一个 post-install-job.yaml 的文件,表示安装后执行的一个 hook:
[root@node01 ~]# vim mychart/templates/post-install-job.yaml
[root@node01 ~]# cat mychart/templates/post-install-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Release.Name }}-post-install-job
lables:
release: {{ .Release.Name }}
chart: {{ .Chart.Name }}
version: {{ .Chart.Version }}
annotations:
# 注意,如果没有下面的这个注释的话,当前的这个Job就会被当成release的一部分
"helm.sh/hook": post-install
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": hook-succeeded
spec:
template:
metadata:
name: {{ .Release.Name }}
labels:
release: {{ .Release.Name }}
chart: {{ .Chart.Name }}
version: {{ .Chart.Version }}
spec:
restartPolicy: Never
containers:
- name: post-install-job
image: alpine
command: ["/bin/sleep", "{{ default "10" .Values.sleepTime }}"]
上面的 Job 资源中我们添加一个 annotations,要注意的是,如果我们没有添加下面这行注释的话,这个资源就会被当成是 release 的一部分资源:
annotations:
"helm.sh/hook": post-install
当然一个资源中我们也可以同时部署多个 hook,比如我们还可以添加一个post-upgrade
的钩子:
annotations:
"helm.sh/hook": post-install,post-upgrade
另外值得注意的是我们为 hook 定义了一个权重,这有助于建立一个确定性的执行顺序,权重可以是正数也可以是负数,但是必须是字符串才行。
annotations:
"helm.sh/hook-weight": "-5"
最后还添加了一个删除 hook 资源的策略:
annotations:
"helm.sh/hook-delete-policy": hook-succeeded
删除资源的策略可供选择的注释值:
hook-succeeded
:表示 Tiller 在 hook 成功执行后删除 hook 资源hook-failed
:表示如果 hook 在执行期间失败了,Tiller 应该删除 hook 资源before-hook-creation
:表示在删除新的 hook 之前应该删除以前的 hook
当 helm 的 release 更新时,有可能 hook 资源已经存在于群集中。默认情况下,helm 会尝试创建资源,并抛出错误"... already exists"。
我们可以选择 "helm.sh/hook-delete-policy": "before-hook-creation",取代 "helm.sh/hook-delete-policy": "hook-succeeded,hook-failed" 因为:
例如为了手动调试,将错误的 hook 作业资源保存在 kubernetes 中是很方便的。 出于某种原因,可能有必要将成功的 hook 资源保留在 kubernetes 中。同时,在 helm release 升级之前进行手动资源删除是不可取的。 "helm.sh/hook-delete-policy": "before-hook-creation" 在 hook 中的注释,如果在新的 hook 启动前有一个 hook 的话,会使 Tiller 将以前的release 中的 hook 删除,而这个 hook 同时它可能正在被其他一个策略使用。