vlambda博客
学习文章列表

K8S部署方式选择——没有最好的,只有最合适的

Together for a Shared future

一起向未来

K8S部署方式选择——没有最好的,只有最合适的
K8S部署方式选择——没有最好的,只有最合适的
K8S部署方式选择——没有最好的,只有最合适的


前言

前面几篇文章讲解了K8S相关的基础知识,但是忘记说了应该怎么去部署K8S的相关组件和服务。今天利用这篇文章补充下。

先不考虑K8S的方式,先考虑下在CentOS下我们是如何安装软件的,常规的方式其实就是两种:

  • 下载tar包使用命令:tar -zxvf xxxx.tar来进行解压安装,并配置相关的环境变量,修改相关的系统参数来完成软件的安装。
  • 使用centos自带的yum(rpm)进行安装,当你在使用tar命令进行一顿安装配置的时候,一个yum install xxxx可能会让你感觉整个世界都美好了。

上面两种方式对应的就是手动安装和自动安装。无独有偶,K8S中也有同样的安装方式:

  • 编写各种yaml文件,使用kubectl -f xxxx.yaml命令来进行K8S应用的部署和管理。
  • 将各种依赖的yaml文件进行编排,封装成K8S的chart,使用helm命令进行服务的部署和管理,命令也是类似于centos的helm install xxxx等。

那么问题来了,对于初学者或者刚上手的人,到底该怎么选择呢?下面我们分别来讲一下这两种方式的特点。

正文

kubectl命令

前面的例子绝大数都是使用kubectl命令来创建和管理K8S组件的,其实就是通过yaml文件来进行编排的。这样做的好处很明显,那就是逻辑清晰,便于扩展和修改。

这对于逻辑简单的应用来说是个很好的选择,毕竟只需要写一个deployment或者一个deployment+一个service即可。

这里kubectl命令就不过多说明了,最后说一下kubectl命令最常用的两个命令create和apply的区别吧:

  • kubectl create -f xxx.yaml:先删除yaml上所有的组件,然后重新根据yaml文件生成新的组件。所以要求yaml文件中的配置必须是完整的。
  • kubectl apply -f xxx.yaml:根据配置文件里面列出来的内容,找到增量的更新内容,然后对更新内容升级现有的组件。所以yaml文件的内容可以只写需要升级的属性。

所以,当配置文件是完整的时候,create和apply除了执行效率不同外,执行效果效果几乎相同。

helm

虽然kubectl命令控制起来比较方便,也便于理解,但是当你要构建一个复杂的组件,比如nosql数据库时,直接使用yaml文件直接写会十分的繁琐且复杂,上手的门槛也很高。

下面的图就是对elasticsearch在k8s上yaml编排的文件目录,这还没考虑到很多非必要的配置就已经如此多的yaml文件了,更不用说如果出问题该如何调试了。


K8S部署方式选择——没有最好的,只有最合适的


看到这些yaml文件估计绝大多数初学者就已经望而却步了,而直接使用kubectl命令带来的弊端也同样暴露了出来:

  • 如何将这些yaml作为一个整体管理
  • 这些yaml文件如何高效复用
  • 不支持应用级别的版本管理

为了解决上述的问题,K8S推出了Helm来对chart进行管理,以达到yum在centos中所起到的作用。下面我们就来看看Helm是怎么玩的?

首先先说明下,Helm跟随着K8S的版本升级,经历了Helm2和Helm3两个版本。两者最大的差别就是Helm2有一个server端Tiller,而Helm3移除了Tiller。差别产生的原因是因为开发Helm2时,由于K8S没有基于角色的访问控制(RBAC),Helm不得不自己控制权限以及在哪里能够安装应用。直到K8S 1.6中开启了RBAC ,这件事就变得简单了。Helm也不必再做重复的事情,因此Helm3彻底移除了Tiller。

所以如果是新版本的K8S,建议直接使用Helm3。另外现在网上Helm的资料很多都是Helm2版本的,导致在Helm3上无法使用。作者曾纠结于为什么网上很普通的helm init命令总是提示命令找不到,后来才知道是版本的锅。所以在查资料的时候一定要确定好版本。一般来说,过滤资料中是否有Tiller会是一个很好的方法。

下面我们直接来直接创建一个demo来了解下helm的工作机制:

首先使用helm创建一个新的chart:


K8S部署方式选择——没有最好的,只有最合适的

创建完毕后,就会在当前目录下创建了一个chart名称的文件夹,进入文件夹,看看目录结构:


K8S部署方式选择——没有最好的,只有最合适的

使用tree命令看下目录的树结构:



下面针对树结构对各个文件夹以及文件的作用进行描述:


# tree everisk
everisk
├── Chart.yaml        # Chart元数据信息,包含名称、版本等
├── charts          # 依赖Chart集合: 一些应用的Helm chart有多个额外的chart或者subchart,需要与主要的应用程序一起部署;当这种情况发生时,value文件将用每个chart的value进行更新,这样应用程序将会同时配置和部署
├── templates        # K8S资源模板集合[运维人员写的配置文件模板]
   ├── NOTES.txt       # 创建后空文件,可以手动编辑以及编写,在安装Chart时自动显示的用户帮助文档,通常会包含该Chart的使用和配置方法
   ├── _helpers.tpl      # 定义一些可以在Chart里引用的Yaml内容片段
   ├── deployment.yaml    # 用来创建Deployment的资源描述示例
   ├── hpa.yaml       # 用来创建hpa的资源描述示例
   ├── ingress.yaml      # 用来创建ingress的资源描述示例
   ├── service.yaml      # 用来创建service的资源描述示例
   ├── serviceaccount.yaml   # 用来创建serviceaccount的资源描述示例
   └── tests         # 测试目录
       └── test-connection.yaml  # 连接到应用程序的测试
└── values.yaml       # 参数配置模板[开发人员写的可选配置参数],与templates文件夹中的各种资源模板进行联动,共同生成chart包

3 directories, 10 files


Helm中有两个重要的入口,一个就是Chart.yaml,另外一个就是values.yaml,这两个配置文件里面的配置项会在模板中被引用,所有的编排以及配置入口也主要是这两个文件。

模板文件的设置格式可以从values.yaml文件中收集部署信息,故当自定义helm chart时,需要配置values.yaml文件


# Default values for everisk.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

replicaCount: 1

image:
  repository: 172.16.36.145/bangcle/webservice
  pullPolicy: Always
  # Overrides the image tag whose default is the chart appVersion.
  tag: ""

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

serviceAccount:
  # Specifies whether a service account should be created
  create: false
  # Annotations to add to the service account
  annotations: {}
  # The name of the service account to use.
  # If not set and create is true, a name is generated using the fullname template
  name: ""

podAnnotations: {}

podSecurityContext: {}
  # fsGroup: 2000

securityContext: {}
  # capabilities:
  #   drop:
  #   - ALL
  # readOnlyRootFilesystem: true
  # runAsNonRoot: true
  # runAsUser: 1000

service:
  type: ClusterIP
  port: 9990

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

autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 100
  targetCPUUtilizationPercentage: 80
  # targetMemoryUtilizationPercentage: 80

nodeSelector: {}

tolerations: []

affinity: {}


再看来看看deployment模板相关的内容:


apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "everisk.fullname" . }}
  labels:
    {{- include "everisk.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
{{- end }}
  selector:
    matchLabels:
      {{- include "everisk.selectorLabels" . | nindent 6 }}
  template:
    metadata:
    {{- with .Values.podAnnotations }}
      annotations:
        {{- toYaml . | nindent 8 }}
    {{- end }}
      labels:
        {{- include "everisk.selectorLabels" . | nindent 8 }}
    spec:
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      serviceAccountName: {{ include "everisk.serviceAccountName" . }}
      securityContext:
        {{- toYaml .Values.podSecurityContext | nindent 8 }}
      containers:
        - name: {{ .Chart.Name }}
          securityContext:
            {{- toYaml .Values.securityContext | nindent 12 }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: 80
              protocol: TCP
          livenessProbe:
            httpGet:
              path: /
              port: http
          readinessProbe:
            httpGet:
              path: /
              port: http
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.affinity }}
      affinity:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
      {{- end }}


文件都是helm相关的函数,绝大部分都是从values.yaml中获取属性,最后生成完整的deployment.yaml文件。仔细看image属性中有Chart的依赖,所以需要对Chart.yaml的相关内容进行编辑:


apiVersion: v2
name: everisk
description: A Helm chart for Kubernetes

# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application

# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0

# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
appVersion: ver4.8.9.4_EVERSK_rel_220104.1


appVersion会成为deployment中iamge属性的版本信息,这里修改成对应包的版本信息。

万事俱备后,就到了看看我们之前一顿操作的成果,使用下面的命令可以达到预览的效果:helm install --dry-run web everisk/

真的是dry-run啊,翻译过来可以用一句我们那边的方言:干拉来描述,看看干拉的效果:


NAME: web
LAST DEPLOYED: Thu Feb 17 10:57:20 2022
NAMESPACE: default
STATUS: pending-install
REVISION: 1
HOOKS:
---
# Source: everisk/templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "web-everisk-test-connection"
  labels:
    helm.sh/chart: everisk-0.1.0
    app.kubernetes.io/name: everisk
    app.kubernetes.io/instance: web
    app.kubernetes.io/version: "ver4.8.9.4_EVERSK_rel_220104.1"
    app.kubernetes.io/managed-by: Helm
  annotations:
    "helm.sh/hook": test-success
spec:
  containers:
    - name: wget
      image: busybox
      command: ['wget']
      args: ['web-everisk:9990']
  restartPolicy: Never
MANIFEST:
---
# Source: everisk/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: web-everisk
  labels:
    helm.sh/chart: everisk-0.1.0
    app.kubernetes.io/name: everisk
    app.kubernetes.io/instance: web
    app.kubernetes.io/version: "ver4.8.9.4_EVERSK_rel_220104.1"
    app.kubernetes.io/managed-by: Helm
spec:
  type: ClusterIP
  ports:
    - port: 9990
      targetPort: http
      protocol: TCP
      name: http
  selector:
    app.kubernetes.io/name: everisk
    app.kubernetes.io/instance: web
---
# Source: everisk/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-everisk
  labels:
    helm.sh/chart: everisk-0.1.0
    app.kubernetes.io/name: everisk
    app.kubernetes.io/instance: web
    app.kubernetes.io/version: "ver4.8.9.4_EVERSK_rel_220104.1"
    app.kubernetes.io/managed-by: Helm
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: everisk
      app.kubernetes.io/instance: web
  template:
    metadata:
      labels:
        app.kubernetes.io/name: everisk
        app.kubernetes.io/instance: web
    spec:
      serviceAccountName: default
      securityContext:
        {}
      containers:
        - name: everisk
          securityContext:
            {}
          image: "172.16.36.145/bangcle/webservice:ver4.8.9.4_EVERSK_rel_220104.1"
          imagePullPolicy: Always
          ports:
            - name: http
              containerPort: 80
              protocol: TCP
          livenessProbe:
            httpGet:
              path: /
              port: http
          readinessProbe:
            httpGet:
              path: /
              port: http
          resources:
            {}

NOTES:
1. Get the application URL by running these commands:
  export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=everisk,app.kubernetes.io/instance=web" -o jsonpath="{.items[0].metadata.name}")
  echo "Visit http://127.0.0.1:8080 to use your application"
  kubectl --namespace default port-forward $POD_NAME 8080:80


可以看看效果是否符合预期,如果不符合,再根据问题在两个配置文件中进行相应的适配即可。

最后总结下Helm的作用,主要体现在基础运维建设及业务应用两方面:

  • 基础运维建设:更方便地部署与升级基础设施,如gitlab,prometheus,grafana,ES等,可以直接通过helm install直接安装使用,降低了这些复杂组件的部署和管理门槛,使得部署和管理过程对用户透明。
  • 业务应用:更方便地部署,管理与升级公司内部应用,为公司内部的项目配置Chart,使用Helm结合CI/CD,在k8s中部署应用如一行命令般简单

总结

上面描述了K8S的两种部署方式,各有各的特点以及使用场景,大家可以根据需求进行选择。但是对于新手来说下面的规则值得借鉴:

  • 对于复杂的成熟的基础组件,建议使用Helm直接安装,降低使用门槛
  • 对于简单的业务模块,建议直接使用kubectl命令进行管理,方便直观且上手简单
  • 对于复杂的业务模块,尤其是业务逻辑复杂以及依赖关系复杂的场景,则可以根据实际情况来定,一个好的方案是前期使用kubectl命令进行管理,后期慢慢过渡到helm chart的模式,毕竟Helm的管理更专业且方便,能更好的和其他功能配合和协作

最后再说一句,Helm的内容还是比较多的,网上的培训资料也很多,动辄就二三十节课,学习成本还是很高的,建议大家不必过于纠结于此,暂时先放一下,会安装基础的公共组件即可。先集中力量解决主要问题和矛盾,等有时间和精力再来仔细研究。

作者也是简单的了解了下原理就没有继续深入了,所以详细的例子暂时不能提供,后续找机会再补上吧,这个应该不会耽误大家学习K8S。

最后,如果想一起入门学习K8S的小伙伴,欢迎点赞转发加关注,下次学习不迷路!



点个在看你最好看