Kubernetes--k8s---进阶--管理工具helm--helm全面介绍

简介

Helm is the best way to find,share,and use software built for Kubernetes.

Helm号称 构建k8s运行的软件 最好的一种途径和方式,实际上Helm是一个软件管理服务,通过Helm Charts的方式帮助我们定义,安装和升级 即使很复杂的 k8s 应用和软件。

有点类似于 Ubuntu 中使用的apt、Centos中使用的yum 或者Python中的 pip 。

helm官网

git地址

helm官方文档

helm中文文档

Helm是经过CNCF认证的软件服务,目前由Helm社区进行维护。

Cloud Native Computing Foundation,云原生计算基金会(以下简称CNCF)是一个开源软件基金会,它致力于云原生(Cloud Native)技术的普及和可持续发展

相关概念

Helm Charts

资源和软件的定义文件,Charts文件里 需要 声明式的 指定 我们需要哪些软件,版本 以及需要发布的内容,它描述了一组相关的 k8s 集群资源。

Helm release

使用 helm install 命令在 Kubernetes 集群中部署的 Chart 称为 Release

Helm 包含两个组件,分别是 helm 客户端 和 Tiller 服务器:

Helm 客户端(helm命令行工具)

helm 是一个命令行工具,用于本地开发及管理chart,chart仓库管理等

Helm的服务端–Tiller

Tiller 是 Helm 的服务端。Tiller 负责接收 Helm 的请求,与 k8s 的 apiserver 交互,根据chart 来生成一个 release 并管理 release

Helm Repoistory

Helm charts 的仓库,Helm 客户端通过 HTTP 协议来访问存储库中 chart 的索引文件和压缩包

Helm体系里也有可以查询公开的Charts的模板的地方,例如:Helm Hub,比如我们需要安装一个Jupyter,那么我们不需要完全重头自己写一个Chart,可以先查看 公开库里有没有已经写好的,别人公开的模板来微调使用。

适用场景

1、需要把使用的软件更透明化,大家通过定义的Charts文件就能了解软件的情况,而不是一个人黑箱操作安装后 其他人并不清楚其中定义和关键

2、期望 比较高的可移植性,如果我们有一份软件应用在k8s集群1中运行,现在需要把这些应用 移植到k8s集群2中,那我们只需要 再运行一次 Charts文件即可,而不需要 复杂的一步步的细节操作安装配置。

3、需要使用的软件应用多而且零散,期望统一管理:比如你安装一个 WordPress 博客,用到了一些 Kubernetes (下面全部简称k8s)的一些资源对象,包括 Deployment 用于部署应用、Service 提供服务发现、Secret 配置 WordPress 的用户名和密码,可能还需要 pv 和 pvc 来提供持久化服务。并且 WordPress 数据是存储在mariadb里面的,所以需要 mariadb 启动就绪后才能启动 WordPress。这些 k8s 资源过于分散,不方便进行管理,直接通过 kubectl 来管理一个应用,你会发现这十分蛋疼。

原理

下面两张图描述了 Helm 的几个关键组件 Helm(客户端)、Tiller(服务器)、Repository(Chart 软件仓库)、Chart(软件包)之间的关系以及它们之间如何通信

在这里插入图片描述

在这里插入图片描述

创建release

helm 客户端从指定的目录或本地tar文件或远程repo仓库解析出chart的结构信息
helm 客户端指定的 chart 结构和 values 信息通过 gRPC 传递给 Tiller
Tiller 服务端根据 chart 和 values 生成一个 release
Tiller 将install release请求直接传递给 kube-apiserver

删除release

helm 客户端从指定的目录或本地tar文件或远程repo仓库解析出chart的结构信息
helm 客户端指定的 chart 结构和 values 信息通过 gRPC 传递给 Tiller
Tiller 服务端根据 chart 和 values 生成一个 release
Tiller 将delete release请求直接传递给 kube-apiserver

更新release

helm 客户端将需要更新的 chart 的 release 名称 chart 结构和 value 信息传给 Tiller
Tiller 将收到的信息生成新的 release,并同时更新这个 release 的 history
Tiller 将新的 release 传递给 kube-apiserver 进行更新

Chart的基本结构

上文已经说过 Chart是 资源和软件的定义文件,Charts文件里 需要 声明式的 指定 我们需要哪些软件,版本 以及需要发布的内容,它描述了一组相关的 k8s 集群资源。

简单的chart结构如下:

$ helm create hello-helm
Creating hello-helm
$ 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

charts

存放子Chart (Subchart) 的定义,Subchart 指的是当前 Chart 依赖的 Chart , 在 requirements.yaml 中定义

Chart.yaml

包含 Chart 信息的 YAML 文件, 包括 Chart 的版本、名称等,在 DCE Helm 插件中还包含 Chart 的 团队授权 信息 和 是否公开 的信息

README.md

可选:Chart 的介绍信息等(该文件对于一个大型 Chart 来说十分重要)
Requirements.yaml 可选:列举当前 Chart 的需要依赖的 Chart

templates

该目录下存放 Chart 所有的 K8s 资源定义模板,通常不同的资源放在不同的文件中

在templates目录中有可以看到deployment.yaml、ingress.yaml、service.yaml文件这些为定义Kubernetes deployment、ingress、service对象的模板文件

_helpers.tpl
模板助手文件,通常这个文件存放可重用的模板片段,该文件中定义的值 可以在 Chart 其它资源定义模板中使用

NOTES.txt
可选:一段简短使用说明的文本文件,用于安装 Release 后提示用户使用

values.yaml

当前 Chart 的默认配置的值

复杂的chart结构则会定义多个依赖的服务如下:

[root@k8s-master mychart]# tree mychart/
mychart                              # Chart的名字,也就是目录的名字
├── charts                           # Chart所依赖的子Chart
│   ├── jenkins-0.14.0.tgz           # 被“mychart”依赖的其中一个subChart
│   ├── mysubchart                   # 被“mychart”依赖的另一个subChart
│   │   ├── charts                   
│   │   ├── Chart.yaml
│   │   ├── templates
│   │   │   └── configmap.yaml
│   │   └── values.yaml
│   └── redis-1.1.17.tgz             
├── Chart.yaml                       # 记录关于该Chart的描述信息:比如名称、版本等等
├── config1.toml                     # 其他文件:可以是任何文件
├── config2.toml                     # 其他文件:可以是任何文件
├── requirements.yaml                # 记录该Chart的依赖,类似pom.xml
├── templates                        # 存放模版文件,模板也就是将k8s的yml文件参数化,最终还是会被helm处理成k8s的正常yml文件,然后用来部署对应的资源
│   ├── configmap.yaml               # 一个ConfigMap资源模版
│   ├── _helpers.tpl                 # "_"开头的文件不会本部署到k8s上,可以用于定于通用信息,在其他地方应用,如loables
│   └── NOTES.txt                    # 在执行helm instll安装此Chart之后会被输出到屏幕的一些自定义信息
└── values.yaml                      # 参数定义文件,这里定义的参数最终会应用到模版中

Charts可以是目录,也可以是tgz格式的压缩包。

Charts目录和requirements.yaml文件到区别就和lib和pom.xml的区别类似,一个用于存放,一个用于描述

安装客户端Helm

二进制包手动安装

包下载地址-v2.16.9

或者在第三方仓库中查找 https://storage.googleapis.com/kubernetes-helm

github的release中查找 https://github.com/kubernetes/helm/releases

$ 下载 Helm 二进制文件
$ wget https://storage.googleapis.com/kubernetes-helm/helm-v2.14.3-linux-amd64.tar.gz
$ 解压缩
$ tar -zxvf helm-v2.14.3-linux-amd64.tar.gz
$ 复制 helm 二进制 到bin目录下
$cp linux-amd64/helm /usr/local/bin/

注意选择合适的平台版本,否则可能遇到/usr/local/bin/helm: cannot execute binary file的问题

比如 mac系统需要使用的链接是 https://get.helm.sh/helm-v2.14.3-darwin-amd64.tar.gz

相关平台的链接可以参考 官网

在这里插入图片描述

使用官方提供的脚本一键安装—推荐

默认是获取master分支的代码进行安装,所以该脚本安装的是最新版本的helm,如果需要指定版本,建议使用二进制手动安装

$ curl https://raw.githubusercontent.com/kubernetes/helm/master/scripts/get > get_helm.sh
$ chmod 700 get_helm.sh
$ ./get_helm.sh

注:因为该地址需要科学上网,可以直接复制文末的附录get_helm.sh的内容 自己新建一个脚本使用命令

vi get_helm.sh
输入附录的内容
保存退出

安装成功输出如下:

zhangxiaofans-MacBook-Pro:helm joe$ vi get_helm.sh
zhangxiaofans-MacBook-Pro:helm joe$ 
zhangxiaofans-MacBook-Pro:helm joe$ 
zhangxiaofans-MacBook-Pro:helm joe$ 
zhangxiaofans-MacBook-Pro:helm joe$ 
zhangxiaofans-MacBook-Pro:helm joe$ chmod 700 get_helm.sh 
zhangxiaofans-MacBook-Pro:helm joe$ ./get_helm.sh 
Helm v2.16.9 is available. Changing from version v2.11.0.
Downloading https://get.helm.sh/helm-v2.16.9-darwin-amd64.tar.gz
Preparing to install helm and tiller into /usr/local/bin
Password:
helm installed into /usr/local/bin/helm
tiller installed into /usr/local/bin/tiller
Run 'helm init' to configure helm.
zhangxiaofans-MacBook-Pro:helm joe$ 

其他方式安装

Homebrew 用户使用 brew install kubernetes-helm.
Chocolatey 用户使用 choco install kubernetes-helm.
Scoop 用户使用 scoop install helm.
GoFish 用户使用 gofish install helm.
Snap 用户使用 sudo snap install helm --classic.

更多安装方式可参考官网安装教程

安装服务端Tiller

Tiller 是以 Deployment 方式部署在 Kubernetes 集群中的,只需使用以下指令便可简单的完成安装。

安装好 helm 客户端后,就可以通过以下命令将 Tiller 安装在 kubernetes 集群中:

helm init

直接使用这个命令进行初始化时默认使用 https://kubernetes-charts.storage.googleapis.com 作为缺省的 stable repository 的地址,但由于国内有一张无形的墙的存在,googleapis.com 是不能访问的。可以使用阿里云的源来配置:

helm init --upgrade -i registry.cn-hangzhou.aliyuncs.com/google_containers/tiller:v2.16.9  --stable-repo-url https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts

版本需要与自己的对应

Helm 服务端正常安装完成后,Tiller默认被部署在kubernetes集群的kube-system命名空间下:
使用命令 查看 tiller 的安装运行情况

kubectl get pod -n kube-system -l app=helm

输出如下

zhangxiaofans-MacBook-Pro:helm joe$ kubectl get pod -n kube-system -l app=helm
NAME                             READY     STATUS    RESTARTS   AGE
tiller-deploy-688bbd554f-86dwq   1/1       Running   0          1d

可能遇到的问题–socat not found-Error: cannot connect to Tiller

具体报错如下:

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.
2020/07/25 14:03:19 (0xc420476278) (0xc4203ae1e0) Stream removed, broadcasting: 3
2020/07/25 14:03:19 (0xc4203ae150) (3) Writing data frame
2020/07/25 14:03:19 (0xc420476250) (0xc4200c3900) Create stream
2020/07/25 14:03:19 (0xc420476250) (0xc4200c3900) Stream added, broadcasting: 5
Error: cannot connect to Tiller

原因 是缺少socat组件

解决方法:
在k8s的每一个节点上都需要安装socat
参考命令

sudo yum install -y socat

权限调整

这一步非常重要,不然后面在使用 Helm 的过程中可能出现Error: no available release name found的错误信息。

由于 kubernetes 从1.6 版本开始加入了 RBAC 授权。当前的 Tiller 没有定义用于授权的 ServiceAccount, 访问 API Server 时会被拒绝,需要给 Tiller 加入授权。

创建 Kubernetes 的服务帐号和绑定角色
使用命令

$ kubectl create serviceaccount --namespace kube-system tiller

和

$ kubectl create clusterrolebinding tiller-cluster-rule --clusterrole=cluster-admin --serviceaccount=kube-system:tiller

输出如下:

$ kubectl create serviceaccount --namespace kube-system tiller                               
serviceaccount "tiller" created

$ kubectl create clusterrolebinding tiller-cluster-rule --clusterrole=cluster-admin --serviceaccount=kube-system:tiller
clusterrolebinding.rbac.authorization.k8s.io "tiller-cluster-rule" created

给 Tiller 的 deployments 添加刚才创建的 ServiceAccount

使用命令:

$ kubectl patch deploy --namespace kube-system tiller-deploy -p '{"spec":{"template":{"spec":{"serviceAccount":"tiller"}}}}'

输出如下:

# 给 Tiller 的 deployments 添加刚才创建的 ServiceAccount
$ kubectl patch deploy --namespace kube-system tiller-deploy -p '{"spec":{"template":{"spec":{"serviceAccount":"tiller"}}}}'
deployment.extensions "tiller-deploy" patched

查看 Tiller deployments 资源是否绑定 ServiceAccount
使用命令

$ kubectl get deploy -n kube-system tiller-deploy -o yaml | grep serviceAccount

输出如下:

$ kubectl get deploy -n kube-system tiller-deploy -o yaml | grep serviceAccount
serviceAccount: tiller
serviceAccountName: tiller

检查是否安装成功

使用命令:

$ helm version 

输出如下:

$ helm version 
Client: &version.Version{SemVer:"v2.16.9", GitCommit:"20adb27c7c5868466912eebdf6664e7390ebe710", GitTreeState:"clean"}
Server: &version.Version{SemVer:"v2.16.9", GitCommit:"20adb27c7c5868466912eebdf6664e7390ebe710", GitTreeState:"clean"}

安装成功后,即可使用 helm install xxx 来安装 helm 应用。

卸载服务端Tiller

如果你需要在 Kubernetes 中卸载已部署的 Tiller,可使用以下命令完成卸载。

kubectl delete deployment tiller-deploy --namespace kube-system 

删除 Tiller 的 deployment

或者使用 helm reset 来删除

$ helm reset 或
$helm reset --force

使用—安装部署单服务nginx为例

使用helm提供的仓库repository中查找到的服务

最简单的方法就是 直接在 helm提供的仓库repository中查找相关的服务,使用install命令进行安装

使用命令

 helm search nginx

输出如下:

zhangxiaofans-MacBook-Pro:helm joe$ helm search nginx
NAME                       	CHART VERSION	APP VERSION	DESCRIPTION                                                 
stable/nginx-ingress       	0.29.2       	0.20.0     	An nginx Ingress controller that uses ConfigMap to store ...
stable/nginx-ldapauth-proxy	0.1.2        	1.13.5     	nginx proxy with ldapauth                                   
stable/nginx-lego          	0.3.1        	           	Chart for nginx-ingress-controller and kube-lego            
stable/gcloud-endpoints    	0.1.2        	1          	DEPRECATED Develop, deploy, protect and monitor your APIs...
zhangxiaofans-MacBook-Pro:helm joe$ 

使用命令 可以查看每个资源的详情和使用方式

helm inspect stable/nginx-ingress

安装命令如下:

$ helm install --name my-nginx-release stable/nginx-ingress

成功运行安装输出如下:

zhangxiaofans-MacBook-Pro:helm joe$ helm install --name my-nginx-release stable/nginx-ingress
NAME:   my-nginx-release
LAST DEPLOYED: Thu Jul 23 16:48:46 2020
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/ClusterRole
NAME                            AGE
my-nginx-release-nginx-ingress  2s

==> v1/ClusterRoleBinding
NAME                            AGE
my-nginx-release-nginx-ingress  2s

==> v1/Deployment
NAME                                            READY  UP-TO-DATE  AVAILABLE  AGE
my-nginx-release-nginx-ingress-controller       0/1    1           0          2s
my-nginx-release-nginx-ingress-default-backend  0/1    1           0          2s

==> v1/Pod(related)
NAME                                                            READY  STATUS             RESTARTS  AGE
my-nginx-release-nginx-ingress-controller-fb795487f-zxjwk       0/1    ContainerCreating  0         2s
my-nginx-release-nginx-ingress-default-backend-dc456984b-xb6gt  0/1    ContainerCreating  0         2s

==> v1/Role
NAME                            AGE
my-nginx-release-nginx-ingress  2s

==> v1/RoleBinding
NAME                            AGE
my-nginx-release-nginx-ingress  2s

==> v1/Service
NAME                                            TYPE          CLUSTER-IP    EXTERNAL-IP  PORT(S)                     AGE
my-nginx-release-nginx-ingress-controller       LoadBalancer  10.30.23.234  <pending>    80:32569/TCP,443:30927/TCP  2s
my-nginx-release-nginx-ingress-default-backend  ClusterIP     10.30.25.32   <none>       80/TCP                      2s

==> v1/ServiceAccount
NAME                                    SECRETS  AGE
my-nginx-release-nginx-ingress          1        2s
my-nginx-release-nginx-ingress-backend  1        2s


NOTES:
The nginx-ingress controller has been installed.
It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status by running 'kubectl --namespace default get services -o wide -w my-nginx-release-nginx-ingress-controller'

An example Ingress that makes use of the controller:

  apiVersion: extensions/v1beta1
  kind: Ingress
  metadata:
    annotations:
      kubernetes.io/ingress.class: nginx
    name: example
    namespace: foo
  spec:
    rules:
      - host: www.example.com
        http:
          paths:
            - backend:
                serviceName: exampleService
                servicePort: 80
              path: /
    # This section is only required if TLS is to be enabled for the Ingress
    tls:
        - hosts:
            - www.example.com
          secretName: example-tls

If TLS is enabled for the Ingress, a Secret containing the certificate and key must also be provided:

  apiVersion: v1
  kind: Secret
  metadata:
    name: example-tls
    namespace: foo
  data:
    tls.crt: <base64 encoded cert>
    tls.key: <base64 encoded key>
  type: kubernetes.io/tls

zhangxiaofans-MacBook-Pro:helm joe$ 

可以使用命令查看nginx服务的pod是否正常安装

kubectl get pods |grep nginx

输出如下:

zhangxiaofans-MacBook-Pro:helm joe$ kubectl get pods |grep nginx
my-nginx-release-nginx-ingress-controller-fb795487f-zxjwk        1/1       running   0          29m
my-nginx-release-nginx-ingress-default-backend-dc456984b-xb6gt   1/1       running   0          29m

如果不是running的状态则需要排查异常,可以参考 k8s服务发布错误排查

可能遇到的问题–ImagePullBackOff

详细报错如下:

  Failed to pull image "us.gcr.io/k8s-artifacts-prod/ingress-nginx/controller:v0.34.1": rpc error: code = Unknown desc = Error response from daemon: Get https://us.gcr.io/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
  Warning  Failed     5m (x118 over 35m)   kubelet, ip-10-30-46-220.cn-northwest-1.compute.internal  Error: ImagePullBackOff
  Normal   BackOff    21s (x139 over 35m)  kubelet, ip-10-30-46-220.cn-northwest-1.compute.internal  Back-off pulling image "us.gcr.io/k8s-artifacts-prod/ingress-nginx/controller:v0.34.1"

这是因为直接使用的资源库中的编排使用的镜像有可能是us的镜像,k8s集群不具备上网能力的话就会获取镜像失败。

解决方式,找到对应的deployment,修改镜像地址。

使用命令如下:

kubectl get deployment |grep nginx

找到对应的pod
使用命令

kubectl edit deployment my-nginx-release-nginx-ingress-controller

输出如下:

zhangxiaofans-MacBook-Pro:helm joe$ kubectl get deployment |grep nginx
my-nginx-release-nginx-ingress-controller        1         1         1            0           27s
my-nginx-release-nginx-ingress-default-backend   1         1         1            0           27s
zhangxiaofans-MacBook-Pro:helm joe$ kubectl edit deployment my-nginx-release-nginx-ingress-controller

替换为国内的镜像,比如阿里云的 registry.aliyuncs.com/google_containers/nginx-ingress-controller:0.26.1
或者
quay.azk8s.cn/kubernetes-ingress-controller/nginx-ingress-controller:0.26.1

成功修改输出如下:

zhangxiaofans-MacBook-Pro:helm joe$ KUBE_EDITOR="vim" kubectl edit deployment my-nginx-release-nginx-ingress-controller   --validate=false
deployment.extensions "my-nginx-release-nginx-ingress-controller" edited

修改成功后 会自动新起pod。

pod状态running成功后可以使用 命令查看 nginx服务的状态以及访问方式

kubectl --namespace default get services -o wide -w my-nginx-release-nginx-ingress-controller

输出如下:

zhangxiaofans-MacBook-Pro:helm joe$ kubectl --namespace default get services -o wide -w my-nginx-release-nginx-ingress-controller
NAME                                        TYPE           CLUSTER-IP     EXTERNAL-IP                                                                       PORT(S)                      AGE       SELECTOR
my-nginx-release-nginx-ingress-controller   LoadBalancer   10.30.23.234   123.cn-northwest-1.elb.amazonaws.com.cn   80:32569/TCP,443:30927/TCP   4m        app.kubernetes.io/component=controller,app=nginx-ingress,release=my-nginx-release
zhangxiaofans-MacBook-Pro:helm joe$ 

使用EXTERNAL-IP对应的值 123.cn-northwest-1.elb.amazonaws.com.cn 即可访问 安装好的nginx服务。

可能遇到的问题—invalid object doesn’t have additional properties

原因是使用kubectl edit deployment 编辑后的格式不符合要求,如果改动不大的话可以忽略schema的检查,增加参数

–validate=false

使用命令

kubectl edit deployment my-nginx-release-nginx-ingress-controller   --validate=false

可能遇到的问题—error: there was a problem with the editor “vi”

原因是kubectl 默认使用系统的vi进行编辑,但是有些格式处理不了,所以需要使用vim进行处理,使用参数

KUBE_EDITOR="vim"

使用命令

KUBE_EDITOR="vim" kubectl edit deployment my-nginx-release-nginx-ingress-controller   --validate=false

自定义的安装方式

上面演示的是 使用第三方别人做好的资源来进行 安装,但有时候我们需要一些定制化的配置,或者是把我们先有的项目修改成helm的方式进行安装,那么就需要了解 怎么 进行 自定义安装。

我们先把上一步安装的nginx删除掉

使用命令:

helm delete my-nginx-release

这里my-nginx-release 与创建时候的 --name对应

成功删除输出如下:

zhangxiaofans-MacBook-Pro:helm joe$ helm delete my-nginx-release
release "my-nginx-release" deleted

使用命令创建一个空的chart示例模板

helm create nginx-helm

这里的nginx-helm是我们自己想要的命名,随意起,创建出来的chart模板是一样的。

运行成功后 输出如下:

zhangxiaofans-MacBook-Pro:helm joe$ helm create nginx-helm
Creating nginx-helm

创建成功后会在当前目录生成一个同名目录nginx-helm

使用tree命令查看目录结构如下:

zhangxiaofans-MacBook-Pro:helm joe$ tree nginx-helm
nginx-helm
├── Chart.yaml
├── charts
├── templates
│   ├── NOTES.txt
│   ├── _helpers.tpl
│   ├── deployment.yaml
│   ├── ingress.yaml
│   ├── service.yaml
│   └── tests
│       └── test-connection.yaml
└── values.yaml

3 directories, 8 files

如果我们查看deployment.yaml以及ingress.yaml和service.yaml等文件 会发现很多带有两个花括号的变量名。
例如:

      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}

说明 这个chart模板比较灵活,实际起作用和需要调整的值都在values.yaml这个文件中。

查看values.yaml的内容

cat nginx-helm/values.yaml

输出如下:

zhangxiaofans-MacBook-Pro:helm joe$ cat nginx-helm/values.yaml 
# Default values for nginx-helm.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

replicaCount: 1

image:
  repository: nginx
  tag: stable
  pullPolicy: IfNotPresent

imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

service:
  type: ClusterIP
  port: 80

ingress:
  enabled: false
  annotations: {}
    # kubernetes.io/ingress.class: nginx
    # kubernetes.io/tls-acme: "true"
  hosts:
    - host: chart-example.local
      paths: []

  tls: []
  #  - secretName: chart-example-tls
  #    hosts:
  #      - chart-example.local

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: {}
zhangxiaofans-MacBook-Pro:helm joe$ 

可以发现 使用create命令创建的模板,默认的示例服务 就是nginx。

我们只需要调整一些 参数即可。

因为ClusterIP方式,外网无法访问到这个service,我们可以考虑使用NodePort方式或者LoadBalancer 以及ingress。

我们这里使用LoadBalancer 方式将

service:
  type: ClusterIP
  port: 80

修改为

service:
  type: LoadBalancer
  port: 80

如果要修改镜像的版本 可以把 stable修改为指定版本
如下:

image:
  repository: nginx
  tag: stable
  pullPolicy: IfNotPresent

修改为:

image:
  repository: nginx
  tag: 1.7.9
  pullPolicy: IfNotPresent

修改完后使用命令

helm install ./nginx-helm

安装输出如下:

zhangxiaofans-MacBook-Pro:helm joe$ helm  install ./nginx-helm/
NAME:   plundering-wasp
LAST DEPLOYED: Fri Jul 24 17:02:13 2020
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/Deployment
NAME                        READY  UP-TO-DATE  AVAILABLE  AGE
plundering-wasp-nginx-helm  0/1    1           0          2s

==> v1/Pod(related)
NAME                                         READY  STATUS             RESTARTS  AGE
plundering-wasp-nginx-helm-699b5f8dc7-qrgvr  0/1    ContainerCreating  0         2s

==> v1/Service
NAME                        TYPE          CLUSTER-IP   EXTERNAL-IP  PORT(S)       AGE
plundering-wasp-nginx-helm  LoadBalancer  10.30.8.157  <pending>    80:30873/TCP  2s


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 --namespace default svc -w plundering-wasp-nginx-helm'
  export SERVICE_IP=$(kubectl get svc --namespace default plundering-wasp-nginx-helm -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
  echo http://$SERVICE_IP:80

zhangxiaofans-MacBook-Pro:helm joe$ 

使用命令
查看外网可以访问的地址

kubectl get --namespace default svc -w plundering-wasp-nginx-helm -o wide

输出如下:

zhangxiaofans-MacBook-Pro:helm joe$ kubectl get --namespace default svc -w plundering-wasp-nginx-helm -o wide
NAME                         TYPE           CLUSTER-IP    EXTERNAL-IP                                                                      PORT(S)        AGE       SELECTOR
plundering-wasp-nginx-helm   LoadBalancer   10.30.8.157   123.cn-northwest-1.elb.amazonaws.com.cn   80:30873/TCP   10m       app.kubernetes.io/instance=plundering-wasp,app.kubernetes.io/name=nginx-helm

使用EXTERNAL-IP对应的值 123.cn-northwest-1.elb.amazonaws.com.cn 即可访问

在这里插入图片描述

使用–安装部署多服务–Wordpress为例

Wordpress 为例,包括 MySQL、PHP 和 Apache。

由于测试环境没有可用的 PersistentVolume(持久卷,简称 PV),暂时将其关闭。

使用安装命令

helm install --name wordpress-test --set "persistence.enabled=false,mariadb.persistence.enabled=false,serviceType=NodePort"  stable/wordpress

安装成功输出如下:

zhangxiaofans-MacBook-Pro:helm joe$ helm install --name wordpress-test --set "persistence.enabled=false,mariadb.persistence.enabled=false,serviceType=NodePort"  stable/wordpress
NAME:   wordpress-test
LAST DEPLOYED: Mon Jul 27 11:53:04 2020
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/ConfigMap
NAME                          DATA  AGE
wordpress-test-mariadb        1     2s
wordpress-test-mariadb-tests  1     2s

==> v1/Deployment
NAME            READY  UP-TO-DATE  AVAILABLE  AGE
wordpress-test  0/1    1           0          2s

==> v1/Pod(related)
NAME                             READY  STATUS             RESTARTS  AGE
wordpress-test-747d457759-rjfnz  0/1    ContainerCreating  0         2s
wordpress-test-mariadb-0         0/1    Pending            0         2s

==> v1/Secret
NAME                    TYPE    DATA  AGE
wordpress-test          Opaque  1     2s
wordpress-test-mariadb  Opaque  2     2s

==> v1/Service
NAME                    TYPE          CLUSTER-IP    EXTERNAL-IP  PORT(S)                     AGE
wordpress-test          LoadBalancer  10.30.22.146  <pending>    80:31769/TCP,443:32180/TCP  2s
wordpress-test-mariadb  ClusterIP     10.30.9.210   <none>       3306/TCP                    2s

==> v1/StatefulSet
NAME                    READY  AGE
wordpress-test-mariadb  0/1    2s


NOTES:
This Helm chart is deprecated

Given the `stable` deprecation timeline (https://github.com/helm/charts#deprecation-timeline), the Bitnami maintained Helm chart is now located at bitnami/charts (https://github.com/bitnami/charts/).

The Bitnami repository is already included in the Hubs and we will continue providing the same cadence of updates, support, etc that we've been keeping here these years. Installation instructions are very similar, just adding the _bitnami_ repo and using it during the installation (`bitnami/<chart>` instead of `stable/<chart>`)

bash
$ helm repo add bitnami https://charts.bitnami.com/bitnami
$ helm install my-release bitnami/<chart>           # Helm 3
$ helm install --name my-release bitnami/<chart>    # Helm 2


To update an exisiting _stable_ deployment with a chart hosted in the bitnami repository you can execute

bash
$ helm repo add bitnami https://charts.bitnami.com/bitnami
$ helm upgrade my-release bitnami/<chart>


Issues and PRs related to the chart itself will be redirected to `bitnami/charts` GitHub repository. In the same way, we'll be happy to answer questions related to this migration process in this issue (https://github.com/helm/charts/issues/20969) created as a common place for discussion.

** Please be patient while the chart is being deployed **

To access your WordPress site from outside the cluster follow the steps below:

1. Get the WordPress URL by running these commands:

  NOTE: It may take a few minutes for the LoadBalancer IP to be available.
        Watch the status with: 'kubectl get svc --namespace default -w wordpress-test'

   export SERVICE_IP=$(kubectl get svc --namespace default wordpress-test --template "{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}")
   echo "WordPress URL: http://$SERVICE_IP/"
   echo "WordPress Admin URL: http://$SERVICE_IP/admin"

2. Open a browser and access WordPress using the obtained URL.

3. Login with the following credentials below to see your blog:

  echo Username: user
  echo Password: $(kubectl get secret --namespace default wordpress-test -o jsonpath="{.data.wordpress-password}" | base64 --decode)

部署完成后,我们可以通过上面的提示信息生成相应的访问地址和用户名、密码等相关信息。

使用命令

 export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services wordpress-test)



export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")


echo http://$NODE_IP:$NODE_PORT/admin



echo Username: user


echo Password: $(kubectl get secret --namespace default wordpress-test -o jsonpath="{.data.wordpress-password}" | base64 --decode)




输出如下:


# 生成 Wordpress 管理后台地址
$ export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services wordpress-test)
$ export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")
$ echo http://$NODE_IP:$NODE_PORT/admin
http://192.168.100.211:8433/admin
 
# 生成 Wordpress 管理帐号和密码
$ echo Username: user
Username: user
$ echo Password: $(kubectl get secret --namespace default wordpress-test -o jsonpath="{.data.wordpress-password}" | base64 --decode)
Password: QfLE7uD5fd

或者使用命令查看 访问链接:

kubectl get --namespace default svc -w wordpress-test  -o wide

输出如下:

zhangxiaofans-MacBook-Pro:helm joe$ kubectl get --namespace default svc -w wordpress-test  -o wide
NAME             TYPE           CLUSTER-IP     EXTERNAL-IP                                                                      PORT(S)                      AGE       SELECTOR
wordpress-test   LoadBalancer   10.30.22.146   123.cn-northwest-1.elb.amazonaws.com.cn   80:31769/TCP,443:32180/TCP   2h        app.kubernetes.io/instance=wordpress-test,app.kubernetes.io/name=wordpress

则使用 123.cn-northwest-1.elb.amazonaws.com.cn 即可访问,访问如下:

在这里插入图片描述

在首页找到login的按钮 ,使用上面打印的账号密码登录后,登录如下:
在这里插入图片描述

常用命令

helm search 在仓库中查找chart

helm fetch stable/redis  下载chart到本地

helm inspect stable/mongodb 可查看该chart的介绍信息

helm install stable/mongodb 可直接下载该chart并安装该chart

helm upgrade releaseName  .   版本升级,可以通过 --version 参数指定需要升级的版本号,如果没有指定版本号,则缺省使用最新版本

helm rollback releaseName 1 回滚到版本1

helm list  查看已经发布的release

helm package hello-helm  打包chart,然后我们就可以将打包的tgz文件分发到任意的服务器上,通过helm fetch就可以获取到该 Chart 了


helm delete hello-helm  删除release

kubectl get pods -l app=hello-helm  查看发布的资源

helm get wordpress-test 查看发布的release

helm get  --revision 1  wordpress-test  查看指定版本的release


helm install local/mychart --name mike-test --namespace mynamespace 部署到指定命名空间


Helm 命令自动补全

为了方便 helm 命令的使用,Helm 提供了自动补全功能,不过需要设置一下 命令自动补全

如果系统使用的是BASH 请执行:

$ source <(helm completion bash)

如果系统使用的是ZSH 请执行:

$ source <(helm completion zsh)

使用第三方Chart存储库

随着 Helm 越来越普及,除了使用预置官方存储库,三方仓库也越来越多了(前提是网络是可达的)。你可以使用如下命令格式添加三方 Chart 存储库。

$ helm repo add 存储库名 存储库URL
$ helm repo update

一些三方存储库资源:

# Prometheus Operator
https://github.com/coreos/prometheus-operator/tree/master/helm
 
# Bitnami Library for Kubernetes
https://github.com/bitnami/charts
 
# Openstack-Helm
https://github.com/att-comdev/openstack-helm
https://github.com/sapcc/openstack-helm
 
# Tick-Charts
https://github.com/jackzampolin/tick-charts

打包分享

使用命令可以打包chart

helm package .

得到一个压缩文件,例如 mychart-0.1.0.tgz文件

压缩包方式分享

使用mychart-0.1.0.tgz文件 进行交互,发给同事后,同事把压缩包解压,然后 使用命令进行安装

helm install mychart

自建repository方式分享

压缩包分享的方式比较繁琐,而且不能复用,每个同事需要都得重新交互一次,而且版本更新也很麻烦,最好得方式就是 公司层面有一个公共得仓库repository,我们只要上传后 同事都能看到。

首先查看目前使用的仓库

使用命令:

helm repo list

输出如下:

zhangxiaofans-MacBook-Pro:helm joe$ helm repo list
NAME  	URL                                             
stable	https://kubernetes-charts.storage.googleapis.com
local 	http://127.0.0.1:8879/charts                    
zhangxiaofans-MacBook-Pro:helm joe$ 

注:新版本中执行 helm init 命令后默认会配置一个名为 local 的本地仓库。

我们可以在本地启动一个 Repository Server,并将其加入到 Helm Repo 列表中。Helm Repository 必须以 Web 服务的方式提供,这里我们就使用 helm serve 命令启动一个 Repository Server,该 Server 缺省使用 $HOME/.helm/repository/local 目录作为 Chart 存储,并在 8879 端口上提供服务。

$ helm serve &
Now serving you on 127.0.0.1:8879

默认情况下该服务只监听 127.0.0.1,如果你要绑定到其它网络接口,可使用以下命令:

比如公司层面 公用一台服务器作为仓库,把需要更新和上传包放到这台服务的 $HOME/.helm/repository/local 目录

$ helm serve --address 192.168.30.211:8879 &

如果你想使用指定目录来做为 Helm Repository 的存储目录,可以加上 --repo-path 参数:

$ helm serve --address 192.168.30.211:8879 --repo-path /data/helm/repository/ --url http://192.168.30.211:8879/charts/

通过 helm repo index 命令将 Chart 的 Metadata 记录更新在 index.yaml 文件中:

# 更新 Helm Repository 的索引文件
$ cd /home/k8s/.helm/repository/local
$ helm repo index --url=http://192.168.30.211:8879 .

完成启动本地 Helm Repository Server 后,就可以将本地 Repository 加入 Helm 的 Repo 列表。

使用命令

$ helm repo add local 192.168.30.211:8879
"local" has been added to your repositories

现在再次查找 mychart 包,就可以搜索到了。

$ helm repo update
$ helm search mychart
NAME          CHART VERSION APP VERSION DESCRIPTION
local/mychart 0.1.0         1.0         A Helm chart for Kubernetes

再有其他同事想要 使用这个chart,只要添加这个repositories即可。

其他问题

1、如何让 Helm 连接到指定 Kubernetes 集群?

Helm 默认使用和 kubectl 命令相同的配置访问 Kubernetes 集群,其配置默认在 ~/.kube/config 中。

2、如何在部署时指定命名空间?

helm install 默认情况下是部署在 default 这个命名空间的。如果想部署到指定的命令空间,可以加上 --namespace 参数,比如:

$ helm install local/mychart --name mike-test --namespace mynamespace

参考链接

https://blog.csdn.net/bbwangj/article/details/81087911
https://www.jianshu.com/p/4bd853a8068b
https://blog.csdn.net/chenleiking/article/details/79539012
详细的chart模板和参数参考 https://blog.csdn.net/chenleiking/article/details/79539012

附录–get_helm.sh

#!/usr/bin/env bash

# Copyright The Helm Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# The install script is based off of the MIT-licensed script from glide,
# the package manager for Go: https://github.com/Masterminds/glide.sh/blob/master/get

PROJECT_NAME="helm"
TILLER_NAME="tiller"

: ${USE_SUDO:="true"}
: ${HELM_INSTALL_DIR:="/usr/local/bin"}

# initArch discovers the architecture for this system.
initArch() {
  ARCH=$(uname -m)
  case $ARCH in
    armv5*) ARCH="armv5";;
    armv6*) ARCH="armv6";;
    armv7*) ARCH="arm";;
    aarch64) ARCH="arm64";;
    x86) ARCH="386";;
    x86_64) ARCH="amd64";;
    i686) ARCH="386";;
    i386) ARCH="386";;
  esac
}

# initOS discovers the operating system for this system.
initOS() {
  OS=$(echo `uname`|tr '[:upper:]' '[:lower:]')

  case "$OS" in
    # Minimalist GNU for Windows
    mingw*) OS='windows';;
  esac
}

# runs the given command as root (detects if we are root already)
runAsRoot() {
  local CMD="$*"

  if [ $EUID -ne 0 -a $USE_SUDO = "true" ]; then
    CMD="sudo $CMD"
  fi

  $CMD
}

# verifySupported checks that the os/arch combination is supported for
# binary builds.
verifySupported() {
  local supported="darwin-386\ndarwin-amd64\nlinux-386\nlinux-amd64\nlinux-arm\nlinux-arm64\nlinux-ppc64le\nwindows-386\nwindows-amd64"
  if ! echo "${supported}" | grep -q "${OS}-${ARCH}"; then
    echo "No prebuilt binary for ${OS}-${ARCH}."
    echo "To build from source, go to https://github.com/helm/helm"
    exit 1
  fi

  if ! type "curl" > /dev/null && ! type "wget" > /dev/null; then
    echo "Either curl or wget is required"
    exit 1
  fi
}

# checkDesiredVersion checks if the desired version is available.
checkDesiredVersion() {
  if [ "x$DESIRED_VERSION" == "x" ]; then
    # Get tag from release URL
    local release_url="https://github.com/helm/helm/releases"
    if type "curl" > /dev/null; then

      TAG=$(curl -Ls $release_url | grep 'href="/helm/helm/releases/tag/v2.[0-9]*.[0-9]*\"' | grep -v no-underline | head -n 1 | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}')
    elif type "wget" > /dev/null; then
      TAG=$(wget $release_url -O - 2>&1 | grep 'href="/helm/helm/releases/tag/v2.[0-9]*.[0-9]*\"' | grep -v no-underline | head -n 1 | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}')
    fi
  else
    TAG=$DESIRED_VERSION
  fi
}

# checkHelmInstalledVersion checks which version of helm is installed and
# if it needs to be changed.
checkHelmInstalledVersion() {
  if [[ -f "${HELM_INSTALL_DIR}/${PROJECT_NAME}" ]]; then
    local version=$("${HELM_INSTALL_DIR}/${PROJECT_NAME}" version -c | grep '^Client' | cut -d'"' -f2)
    if [[ "$version" == "$TAG" ]]; then
      echo "Helm ${version} is already ${DESIRED_VERSION:-latest}"
      return 0
    else
      echo "Helm ${TAG} is available. Changing from version ${version}."
      return 1
    fi
  else
    return 1
  fi
}

# downloadFile downloads the latest binary package and also the checksum
# for that binary.
downloadFile() {
  HELM_DIST="helm-$TAG-$OS-$ARCH.tar.gz"
  DOWNLOAD_URL="https://get.helm.sh/$HELM_DIST"
  CHECKSUM_URL="$DOWNLOAD_URL.sha256"
  HELM_TMP_ROOT="$(mktemp -dt helm-installer-XXXXXX)"
  HELM_TMP_FILE="$HELM_TMP_ROOT/$HELM_DIST"
  HELM_SUM_FILE="$HELM_TMP_ROOT/$HELM_DIST.sha256"
  echo "Downloading $DOWNLOAD_URL"
  if type "curl" > /dev/null; then
    curl -SsL "$CHECKSUM_URL" -o "$HELM_SUM_FILE"
  elif type "wget" > /dev/null; then
    wget -q -O "$HELM_SUM_FILE" "$CHECKSUM_URL"
  fi
  if type "curl" > /dev/null; then
    curl -SsL "$DOWNLOAD_URL" -o "$HELM_TMP_FILE"
  elif type "wget" > /dev/null; then
    wget -q -O "$HELM_TMP_FILE" "$DOWNLOAD_URL"
  fi
}

# installFile verifies the SHA256 for the file, then unpacks and
# installs it.
installFile() {
  HELM_TMP="$HELM_TMP_ROOT/$PROJECT_NAME"
  local sum=$(openssl sha1 -sha256 ${HELM_TMP_FILE} | awk '{print $2}')
  local expected_sum=$(cat ${HELM_SUM_FILE})
  if [ "$sum" != "$expected_sum" ]; then
    echo "SHA sum of ${HELM_TMP_FILE} does not match. Aborting."
    exit 1
  fi

  mkdir -p "$HELM_TMP"
  tar xf "$HELM_TMP_FILE" -C "$HELM_TMP"
  HELM_TMP_BIN="$HELM_TMP/$OS-$ARCH/$PROJECT_NAME"
  TILLER_TMP_BIN="$HELM_TMP/$OS-$ARCH/$TILLER_NAME"
  echo "Preparing to install $PROJECT_NAME and $TILLER_NAME into ${HELM_INSTALL_DIR}"
  runAsRoot cp "$HELM_TMP_BIN" "$HELM_INSTALL_DIR"
  echo "$PROJECT_NAME installed into $HELM_INSTALL_DIR/$PROJECT_NAME"
  if [ -x "$TILLER_TMP_BIN" ]; then
    runAsRoot cp "$TILLER_TMP_BIN" "$HELM_INSTALL_DIR"
    echo "$TILLER_NAME installed into $HELM_INSTALL_DIR/$TILLER_NAME"
  else
    echo "info: $TILLER_NAME binary was not found in this release; skipping $TILLER_NAME installation"
  fi
}

# fail_trap is executed if an error occurs.
fail_trap() {
  result=$?
  if [ "$result" != "0" ]; then
    if [[ -n "$INPUT_ARGUMENTS" ]]; then
      echo "Failed to install $PROJECT_NAME with the arguments provided: $INPUT_ARGUMENTS"
      help
    else
      echo "Failed to install $PROJECT_NAME"
    fi
    echo -e "\tFor support, go to https://github.com/helm/helm."
  fi
  cleanup
  exit $result
}

# testVersion tests the installed client to make sure it is working.
testVersion() {
  set +e
  HELM="$(command -v $PROJECT_NAME)"
  if [ "$?" = "1" ]; then
    echo "$PROJECT_NAME not found. Is $HELM_INSTALL_DIR on your "'$PATH?'
    exit 1
  fi
  set -e
  echo "Run '$PROJECT_NAME init' to configure $PROJECT_NAME."
}

# help provides possible cli installation arguments
help () {
  echo "Accepted cli arguments are:"
  echo -e "\t[--help|-h ] ->> prints this help"
  echo -e "\t[--version|-v <desired_version>]"
  echo -e "\te.g. --version v2.4.0  or -v latest"
  echo -e "\t[--no-sudo]  ->> install without sudo"
}

# cleanup temporary files to avoid https://github.com/helm/helm/issues/2977
cleanup() {
  if [[ -d "${HELM_TMP_ROOT:-}" ]]; then
    rm -rf "$HELM_TMP_ROOT"
  fi
}

# Execution

#Stop execution on any error
trap "fail_trap" EXIT
set -e

# Parsing input arguments (if any)
export INPUT_ARGUMENTS="${@}"
set -u
while [[ $# -gt 0 ]]; do
  case $1 in
    '--version'|-v)
       shift
       if [[ $# -ne 0 ]]; then
           export DESIRED_VERSION="${1}"
       else
           echo -e "Please provide the desired version. e.g. --version v2.4.0 or -v latest"
           exit 0
       fi
       ;;
    '--no-sudo')
       USE_SUDO="false"
       ;;
    '--help'|-h)
       help
       exit 0
       ;;
    *) exit 1
       ;;
  esac
  shift
done
set +u

initArch
initOS
verifySupported
checkDesiredVersion
if ! checkHelmInstalledVersion; then
  downloadFile
  installFile
fi
testVersion
cleanup
  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张小凡vip

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值