把应用程序迁移到k8s须要修改什么?

程序部署环境的容器化已是大势所趋,微服务为容器化提供了广阔的应用舞台,k8s已经把Docker归入为它的底层支撑容器引擎,一统江湖,成为了容器技术事实上的标准。通常的应用程序是不能直接拿来部署到容器上的,须要通过一些修改才能移植到k8s上。那么这些改动包括哪些内容呢?前端

它主要有两个部分:
第一部分是服务调用。不管是微服务之间的调用,仍是微服务调用数据库或前端调用后端,调用的方式都是同样的。都须要知道IP地址,端口和协议,例如“http://127.0.0.1:80”, 其中“http”是协议,“127.0.0.1”是IP地址,“80”是端口。它的关键是让k8s的配置文件和应用程序都共享相同的调用地址。
第二部分是数据的持久存储。在程序运行时,常常要访问持久存储(硬盘)上的数据,例如日志,配置文件或临时共享数据。程序在容器中运行,一旦出现问题,容器会被摧毁,k8s会自动从新生成一个与原来如出一辙的容器,并在上面从新部署应用程序。在集群环境下,用户感受不到容器故障,由于系统已经自动修复了。但当容器被摧毁时,容器上的数据也一块儿被摧毁了,所以要保证程序运行的连续性,就要让持久存储不受容器故障的影响。node

程序实例:

咱们经过一个Go(别的语言也大同小异)微服务程序作例子来展现要作的修改。它自己的功能很是简单,只是用SQL语句访问数据库中的数据,并写入日志。你能够简单地把它分红两层,后端数据访问层和数据库层。在k8s中它被分红两个服务。一个是后端服务程序,另外一个是数据库(用MySQL)服务。后端程序要调用数据库服务,而后会把一些数据写入日志,并且这个日志不能由于容器故障而丢失。数据库对数据的保存要求更高,即便k8s集群或虚拟机出了问题或断电也要保证数据的存在。mysql

file

上面是程序的目录结构。咱们重点讲一下与k8s相关的。“config”目录包含与程序配置有关的代码,“logs”目录是用来存储日志文件的,没有代码。“script”目录是重点,里面包含了全部与部署程序相关的文件。其中“database”子目录里面是数据库脚本,“kubernetes”子目录存有k8s的全部配置文件,一回儿还会详细讲解。git

服务调用:

服务调用涉及到两个不一样的部分。一部分是k8s的配置文件,它负责服务的注册和发现。全部部署在k8s上的应用都经过k8s的服务来进行互相调用。另外一部分是应用程序,它须要经过k8s的服务来访问其余程序。在没有k8s时,后端要想访问数据库,代码是这样的:github

db, err := sql.Open("mysql", "dbuser:dbuser@tcp(localhost:3306)/service_config?charset=utf8")

其中,“dbuser:dbuser”是数据库用户名和口令,“localhost:3306”是数据库主机名和端口地址,“service-config”是数据库名,共有五个数据须要读取。迁移到k8s以后,咱们要把这些参数从程序中提取出来,转化成从k8s中读取相关数据。web

k8s配置:

先来看一下k8s的配置文件。sql

file

上面就是k8s的配置文件目录结构,最外层(kubernetes目录下)有两个“yaml”文件“k8sdemo-config.yaml”和"k8sdemo-secret.yaml",它们是被不一样服务共享的,所以放在最外层。另外还有一个"k8sdemo.sh"文件是k8s命令文件,用来建立k8s对象。“kubernetes”目录下有两个子目录“backend”和“database”分别存放后端程序和数据库的配置文件。它们内部的结构是相似的,都有三个“yaml”文件:
backend-deployment.yaml:部署配置文件,
backend-service.yaml:服务配置文件
backend-volume.yaml:持久卷配置文件.docker

关于k8s的核心概念,请参阅“经过实例快速掌握k8s(Kubernetes)核心概念”. “backend”目录还多了一个“docker”子目录用来存储backend应用的Docker镜像,database的镜像文件直接从Docker的库中取得,所以不须要另外生成镜像文件。shell

k8s参数配置:

要想集成应用程序和k8s须要两个层面的参数共享,一个是应用程序和k8s之间的参数共享,另外一个是不一样k8s服务之间的参数共享。数据库

k8s共享参数定义:

共享参数能够经过两种方式实现,一个是环境变量,另外一个是持久卷。这两种方式大同小异,咱们这里用环境变量的方式。这其中最关键的是“k8sdemo-config.yaml”和"k8sdemo-secret.yaml"这两个文件,它们分别存储了普通参数和保密参数。这些参数是属于整个应用程序的,被各个服务共享。

下面就是“k8sdemo-config.yaml”,它里面(在“data:”下面)定义了三个数据库参数,分别是数据库主机(MYSQL_HOST),数据库端口(MYSQL_PORT),数据库名(MYSQL_DATABASE)。

apiVersion: v1
kind: ConfigMap
metadata:
  name: k8sdemo-config  # ConfigMap的名字, 在引用数据时须要
  labels:
    app: k8sdemo
data:
  MYSQL_HOST: k8sdemo-database-service   # 数据库主机
  MYSQL_PORT: "3306" # 数据库端口
  MYSQL_DATABASE: service_config # 数据库名

下面就是“k8sdemo-secret.yaml”,它里面(在“data:”下面)也定义了三个数据库参数,根用户口令(MYSQL_ROOT_PASSWORD),普通用户名(MYSQL_USER_NAME),普通用户口令(MYSQL_USER_PQSSWORD)

apiVersion: v1
kind: Secret
metadata:
  name: k8sdemo-secret
  labels:
    app: k8sdemo
data:
  MYSQL_ROOT_PASSWORD: cm9vdA== # 根用户口令("root")
  MYSQL_USER_NAME: ZGJ1c2Vy # 普通用户名("dbuser")
  MYSQL_USER_PASSWORD: ZGJ1c2Vy # 普通用户口令("dbuser") 

有关k8s的参数配置详细信息,请参阅“经过搭建MySQL掌握k8s(Kubernetes)重要概念(下):参数配置”.

引用k8s共享参数:

下面就是“backend-deployment.yaml”,它定义了“backend“服务的部署(Deployment)配置。它的“containers:”部分定义了容器,“env:”部分定义了环境变量,也就是咱们所熟悉的操做系统的环境变量,通常是由系统来定义。不一样的系统例如Linux和Windows都有本身的方法来定义环境变量。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: k8sdemo-backend-deployment
  labels:
    app: k8sdemo-backend
spec:
  selector:
    matchLabels:
      app: k8sdemo-backend
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: k8sdemo-backend
    spec:
      containers: # 定义容器
        - image: k8sdemo-backend-full:latest
          name: k8sdemo-backend-container
          imagePullPolicy: Never
          env: # 定义环境变量
            - name: MYSQL_USER_NAME
              valueFrom:
                secretKeyRef:
                  name: k8sdemo-secret
                  key: MYSQL_USER_NAME
            - name: MYSQL_USER_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: k8sdemo-secret
                  key: MYSQL_USER_PASSWORD
            - name: MYSQL_HOST
              valueFrom:
               configMapKeyRef:
                 name: k8sdemo-config
                 key: MYSQL_HOST
            - name: MYSQL_PORT
              valueFrom:
                configMapKeyRef:
                  name: k8sdemo-config
                  key: MYSQL_PORT
            - name: MYSQL_DATABASE
              valueFrom:
                configMapKeyRef:
                  name: k8sdemo-config
                  key: MYSQL_DATABASE
          ports:
            - containerPort: 80
              name: portname
          volumeMounts:
            - name: k8sdemo-backend-persistentstorage
              mountPath: /app/logs
      volumes:
        - name: k8sdemo-backend-persistentstorage
          persistentVolumeClaim:
            claimName: k8sdemo-backend-pvclaim

k8s的环境变量主要是用来向容器传递参数的。环境变量引用了“k8sdemo-config.yaml”和"k8sdemo-secret.yaml"文件里的参数,这样就在k8s内部用过共享参数定义和参数引用实现了k8s层的参数共享。

下面是部署配置文件里的环境变量的片断。“ - name: MYSQL_USER_PASSWORD”是环境变量名,“secretKeyRef”说明它的值来自于secret,“name: k8sdemo-secret”是secret的名字,“key: MYSQL_USER_PASSWORD”是secret里的键名,它的最终含义就是环境变量“MYSQL_USER_PASSWORD”的值是由secret里的量“MYSQL_USER_PASSWORD”来定义。

env:
    - name: MYSQL_USER_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: k8sdemo-secret
                  key: MYSQL_USER_PASSWORD

下面是另外一个定义环境变量的片断,与上面的相似,只不过它的键值来自于configMap,而不是secret。

env:
     - name: MYSQL_DATABASE
              valueFrom:
                configMapKeyRef:
                  name: k8sdemo-config
                  key: MYSQL_DATABASE

关于k8s的部署配置细节,请参阅“经过搭建MySQL掌握k8s(Kubernetes)重要概念(上):网络与持久卷”. "

程序和k8s的参数共享:

k8s在建立容器时,会建立环境变量。应用程序在容器里运行时能够从环境变量里读取共享参数已达到应用程序和k8s共享参数的目的。下面就是Go程序访问数据库的代码片断。

type dbConfig struct {
   dbHost     string
   dbPort     string
   dbDatabase string
   dbUser string
   dbPassword string
}

func buildMysql() (dataservice.UserDataInterface, error) {
   tool.Log.Debug("connect to database ")
   dc :=  buildDbConfig ()
   dataSourceName := dc.dbUser   ":"  dc.dbPassword   "@tcp("  dc.dbHost  ":"  dc.dbPort  ")/"   dc.dbDatabase   "?charset=utf8";
   tool.Log.Debug("dataSourceName:", dataSourceName)
   //db, err := sql.Open("mysql", "dbuser:dbuser@tcp(localhost:3306)/service_config?charset=utf8")
   db, err := sql.Open("mysql", dataSourceName)
   checkErr(err)
   dataService := userdata.UserDataMysql{DB: db}
   return &dataService, err
}

func buildDbConfig () dbConfig{
   dc :=dbConfig{}
   dc.dbHost = os.Getenv("MYSQL_HOST")
   dc.dbPort = os.Getenv("MYSQL_PORT")
   dc.dbDatabase = os.Getenv("MYSQL_DATABASE")
   dc.dbUser = os.Getenv("MYSQL_USER_NAME")
   dc.dbPassword = os.Getenv("MYSQL_USER_PASSWORD")
   return dc
}

上面程序中,“buildDbConfig()”函数从环境变量中读取k8s给容器设置好的参数,并上传给“buildMysql()”函数,用来链接数据库。上面是用Go程序读取环境变量,但其它语言例如Java也有相似的功能。

持久存储:

“backend”服务日志:

持久存储相对比较简单,它不须要作额外的应用程序修改 ,但须要程序和k8s相互配合来完成。

Go代码:

下面是日志设置的Go代码片断,它把日志的输出设为k8sdemo的logs目录和Stdout。

func RegisterLogrusLog() error {
	//standard configuration
	log := logrus.New()
	log.SetFormatter(&logrus.TextFormatter{})
	log.SetReportCaller(true)
	file, err := os.OpenFile("../logs/demo.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
	if err != nil {
		fmt.Println("Could Not Open Log File : ", err)
		return errors.Wrap(err, "")
	}
	mw := io.MultiWriter(os.Stdout,file)
	log.SetOutput(mw)
	...
	return nil
}

挂载持久卷:

下一步要作的就是挂载本地目录到容器的“logs”目录,这样日志在写入“logs”目录的时候就写入了本地目录。下面是生成k8s持久卷的配置文件“backend-volume.yaml”,它内部分红两部分(用“—”隔开)。上半部分是持久卷,下半部分是持久卷申请。它由本地硬盘的“/home/vagrant/app/k8sdemo/logs”目录生成k8s的持久卷。

apiVersion: v1
kind: PersistentVolume
metadata:
  name: k8sdemo-backend-pv
  labels:
    app: k8sdemo-backend
spec:
  capacity:
    storage: 1Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  storageClassName: standard
  local:
    path: /home/vagrant/app/k8sdemo/logs
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - minikube
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: k8sdemo-backend-pvclaim
  labels:
    app: k8sdemo-backend
spec:
  accessModes:
    - ReadWriteOnce
  # storageClassName: local-storage
  resources:
    requests:
      storage: 1Gi #1 GB

下面是“backend-deployment.yaml”部署文件片断,它把k8s的持久卷挂载到容器的“app/logs”上。

volumeMounts:
            - name: k8sdemo-backend-persistentstorage
              mountPath: /app/logs
      volumes:
        - name: k8sdemo-backend-persistentstorage
          persistentVolumeClaim:
            claimName: k8sdemo-backend-pvclaim

完成以后,就能够在本地目录上查看日志文件,这样即便容器或k8s集群出现问题,日志也不会丢失。

为何目录是“app/logs”呢?由于在生成“beckend”的镜像时,设定的容器的运行程序根目录是“app”。关于如何建立Go镜像文件,请参阅“建立优化的Go镜像文件以及踩过的坑”.

数据库持久卷:

Mysql数据库的持久卷设置与日志相似,详情请参阅“经过搭建MySQL掌握k8s(Kubernetes)重要概念(上):网络与持久卷”.

存在的问题:

细心的读者可能已经发现了,在定义的环境变量中,有两个与其余的有些不一样,这两个就是“MYSQL_HOST”和"MYSQL_PORT"。全部的环境变量都是在参数文件(k8sdemo-config.yaml)中定义,别的环境变量是在k8s配置文件(例如backend-deployment.yaml)中引用,但这两个虽然在k8s的部署配置文件提到了,但只是用来定义环境变量,最终只是被应用程序引用了,但服务的配置文件并无真正引用它。

apiVersion: v1
kind: Service
metadata:
  name: k8sdemo-database-service # 这里并无引用环境变量
  labels:
    app: k8sdemo-database
spec:
  type: NodePort
  selector:
    app: k8sdemo-database
  ports:
    - protocol : TCP
      nodePort: 30306
      port: 3306 # 这里并无引用环境变量
      targetPort: 3306

上面是数据库服务的配置文件“database-service.yaml”, 这里并无引用“MYSQL_HOST”和"MYSQL_PORT",而是直接写上“k8sdemo-database-service”和“3306”。为何会是这样呢?由于k8s的环境变量是有局限性的,它只能定义在“containers:”里面,也就是说只有容器才能定义环境变量,这从理论上也说得过去。由于若是没有容器,那么环境变量定义给谁呢?但这就致使了服务名不能引用配置参数,结果就是服务名要在两处被定义,一个是参数文件,另外一个是服务配置文件。若是你要修改它,就要在两处同时修改,加大了出错的概率。有什么办法能够解决呢?

Helm

这在k8s内部是无法解决的,但在k8s外是能够解决的。有一个很流行的k8s的包管理工具,叫“helm”, 可以用来定义服务变量。

下面就是使用了Helm以后的Pod的配置文件。

alpine-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: {{ template "alpine.fullname" . }}
  labels:
    # The "app.kubernetes.io/managed-by" label is used to track which tool deployed a given chart.
    # It is useful for admins who want to see what releases a particular tool
    # is responsible for.
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    # The "app.kubernetes.io/instance" convention makes it easy to tie a release to all of the
    # Kubernetes resources that were created as part of that release.
    app.kubernetes.io/instance: {{ .Release.Name | quote }}
    app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
    # This makes it easy to audit chart usage.
    helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
    app.kubernetes.io/name: {{ template "alpine.name" . }}
spec:
  # This shows how to use a simple value. This will look for a passed-in value called restartPolicy.
  restartPolicy: {{ .Values.restartPolicy }}
  containers:
  - name: waiter
    image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
    imagePullPolicy: {{ .Values.image.pullPolicy }}
    command: ["/bin/sleep", "9000"]

下面是变量的定义文件values.yaml

image:
  repository: alpine
  tag: latest
  pullPolicy: IfNotPresent

restartPolicy: Never

程序来源

Helm使用了Go的模板(template)。模板是用数据驱动的文本生成器。它在文本模板里用特殊符号(这里是“{{ }}”)定义变量或数据,而后在执行模板时再将变量转换成变量值,生成最终文本,通常在前端用的比较多。在Helm模板里,“{{ }}”里面的就是变量引用,变量是定义在“values.yaml”文件里的。

上面的例子有两个文件,一个是“alpine-pod.yaml”,另外一个是“values.yaml”。变量定义在“values.yaml”里,再在“alpine-pod.yaml”文件里引用,这样就解决了k8s的环境变量的局限性。

Helm是功能很是强大的k8s包管理工具,并且能够简化容器部署,是一款很是流行的工具。但它的问题是Helm增长了配置文件的复杂度,下降了可读性。如今的版本是Helm2,但Helm3不久就要出炉了。Helm3有一个功能是支持Lua模板,能直接用对象编程(详情请见A First Look at the Helm 3 Plan),新的模板比如今的看起来要强很多,若是你想使用新的还须要再等一等。

结论:

通常的应用程序是不能直接部署到k8s上的,须要通过一些改动才行。它主要有两个部分。第一个是服务调用。第二个是数据的持久存储。服务调用的关键是让k8s和应用程序共享参数。k8s里已经有这种机制,但它还有一点缺陷,只能用来定义容器的环境变量,须要引入其余工具,例如Helm才能解决这个问题。持久存储不须要修改程序,但须要k8s的配置和应用程序配合才能成功。

源码:

完整源码的github连接

备注:

本文中的Go程序只是示例程序,只有k8s配置文件部分是认真写的,能够直接拷贝或引用。其余部分都是临时拼凑来的,主要是为了做为例子,所以没有花时间完善它们,总的来讲它们写得比较粗糙,千万不要直接拷贝。

索引:

  1. 经过实例快速掌握k8s(Kubernetes)核心概念
  2. 经过搭建MySQL掌握k8s(Kubernetes)重要概念(上):网络与持久卷
  3. 经过搭建MySQL掌握k8s(Kubernetes)重要概念(下):参数配置
  4. helm/helm
  5. Alpine: A simple Helm chart
  6. A First Look at the Helm 3 Plan

本文由博客一文多发平台 OpenWrite 发布!