TOC

Kubernetes持续交付实践二

    那么在上个章节已经完成了eureka的持续交付的流程,在此章节将结束整个SpringCloud项目的持续交付流程,剩余的三个微服务都将在此章节完成,因为eureka这个服务比较特殊,需要有一个固定的访问地址,供其他微服务调用,所以我们当时采取了StateFulSet控制器,那么接下来的这三个不论是zuul、feign还是ribbon都是一个面向无状态的服务,所以接下来将以Deployment控制器来管理这几个微服务即可;

zuul

    进行到这里整个项目的部署流程已经基本清晰,前面的各种铺垫已经都做得差不多了,那么在接下来的zuul部署中,控制器采用Deployment,因为机器配置的问题,将以两个副本的方式来运行zuul;
    都说Kubernetes和SpringCloud是天仙配,那么此处对于后面的微服务将加入一个新的功能,那就是ConfigMap,实现配置共享,因为我们的SpringCloud的配置文件是以YAML格式的配置文件来提供的,那么对于YAML格式的配置文件有一个特性,那就是它可以引用环境变量,直接到环境变量中取值,那么对于Kubernetes这个问题就非常好解决了,直接引入ConfigMap实现共享配置,当然我们也可以称之为云配置,无疑解决了运维人员在实现新项目上线的对于不同环境需要对每个项目独立修改配置的大问题,
    那么很多人可能会想,项目较多的情况下可能会造成环境变量的紊乱,那么了解Docker的人应该知道,Docker的隔离性那可不是一句空话,每个Docker容器之间的环境变量也是隔离的,container1和container2他们之间的环境变量是不能互相调用的,具体如下;
zuul配置更新
    因为这个zuul默认是通告的eureka地址是127.0.0.1所以,我们还需要对其进行修改为我们是Pod地址,此处我们不会直接配置地址,而是给定一个环境变量的名称;
[root@node4 ~]# git clone http://192.168.1.64/SpringCloud/zuul.git
[root@node4 zuul]# cat src/main/resources/application.yml
spring:
  application:
    name: zuul
server:
  port: 6220
eureka:
  instance:
    prefer-ip-address: true # 别忘记使用IP进行注册到eureka
    hostname: zuul
  client:
    serviceUrl:
      defaultZone: ${EUREKA_URL} # 修改eureka的地址,这个变量到时候会直接在Pod里面去获取

zuul:
  routes:
    ribbonDemo: 
      path: /ribbon/**
      serviceId: ribbon
    feignDemo:
      path: /feign/**
      serviceId: feign
[root@node4 zuul]# git add *
[root@node4 zuul]# git commit -m 'edit config'
[root@node4 zuul]# git push origin master
持续集成
    因为有了前两章的准备工作,这里就很简单了,我们就可以在此直接进行持续集成的操作了,将zuul以Image的形式集成到我们的Harbor,当然,主要是方法还是通过Python来调用API进行操作,上面eureka的持续集成脚本,为了方便我写得很通用,因此就直接拿这个来改改即可,具体如下;
# 创建一个eureka的Job

# 修改配置

# 提供持续集成脚本

# 测试持续集成

# 验证我们的zuul镜像是否已经持续集成到我们的Harbor仓库了

持续交付
    上面已经将我们的zuul打包成镜像持续集成到了我们的Harbor仓库了,那么现在有了这个成品的zuul镜像了,接下来就是持续交付的环境了,那么在下面我们将使用Deployment无状态控制器来管理我们的zuul项目,具体如下;
提供云配置
    提供云配置也就是创建一个Kubernetes标准资源,ConfigMap,作为我们后面所有项目的配置中心;
[root@node1 ~]# cat public-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: public-config
  namespace: springcloud
data:
  eureka_url: http://eureka-0.eureka-svc.springcloud.svc.cluster.local:6210/eureka/,http://eureka-1.eureka-svc.springcloud.svc.cluster.local:6210/eureka/
[root@node1 ~]# kubectl apply -f public-config.yaml 
# 可以看到下面的名为public-config已经创建成功,那么我们在下面就可以直接引用了
[root@node1 ~]# kubectl describe configmaps -n springcloud public-config 
Name:         public-config
Namespace:    springcloud
Labels:       <none>
Annotations:  kubectl.kubernetes.io/last-applied-configuration:
                {"apiVersion":"v1","data":{"eureka_url":"http://eureka-0.eureka-svc.springcloud.svc.cluster.local:6210/eureka/,http://eureka-1.eureka-svc....

Data
====
eureka_url:
----
http://eureka-0.eureka-svc.springcloud.svc.cluster.local:6210/eureka/,http://eureka-1.eureka-svc.springcloud.svc.cluster.local:6210/eureka/
Events:  <none>
配置清单
    那么这一步将为我们的zuul编写一个配置清单,并且在该配置清单里面我们需要引入env,这个env会在我们ConfigMap里面去取值,为我们我zuul提供相关的配置,那么在新项目上线的时候,第一次部署我们需要手动部署,也算一次测试吧,所以下面直接部署了,别忘记把这个文件也提交到我们的Gitlab仓库,留作备用;
[root@node1 ~]# cat > zuul.yaml << EOF
apiVersion: v1
kind: Service
metadata:
  name: zuul-svc
  namespace: springcloud
  labels:
    app: zuul
spec:
  selector:
    app: zuul
  ports:
    - port: 6220
      targetPort: 6220
  type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: zuul
  labels:
    app: zuul
  annotations:
    author: cce
  namespace: springcloud
spec:
  replicas: 2
  template:
    metadata:
      name: zuul
      labels:
        app: zuul
      namespace: springcloud
    spec:
      imagePullSecrets:
        - name: registry
      containers:
        - name: zuul
          image: 192.168.1.64:8181/springcloud/zuul:2
          imagePullPolicy: IfNotPresent
          livenessProbe:  # 配置健康状态检查
            initialDelaySeconds: 3
            successThreshold: 1
            timeoutSeconds: 10
            failureThreshold: 3
            httpGet:
              port: 6220
              path: /check
              scheme: HTTP
          env:   # 下面创建一个EUREKA_URL的变量变量,值是引用于public-config这个ConfigMap中的eureka_url这个key的值
            - name: EUREKA_URL
              valueFrom:
                configMapKeyRef:
                  name: public-config
                  key: eureka_url
      restartPolicy: Always
  selector:
    matchLabels:
      app: zuul
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: zuul-springcloud-ingress
  namespace: springcloud
spec:
  rules:
    - host: zuul.springcloud.com # 记得将其加入hosts文件 192.168.1.61 zuul.springcloud.com
      http:
        paths:
          - backend:
              serviceName: zuul-svc
              servicePort: 6220
            path: /
EOF
[root@node1 ~]# kubectl apply -f zuul.yaml
[root@node1 ~]# kubectl get all -n springcloud -l app=zuul
NAME                       READY   STATUS    RESTARTS   AGE
pod/zuul-b7f69fbf8-bksws   1/1     Running   0          3m18s
pod/zuul-b7f69fbf8-xwgvf   1/1     Running   0          3m20s

NAME               TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
service/zuul-svc   ClusterIP   10.106.232.192   <none>        6220/TCP   4m35s

NAME                   READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/zuul   2/2     2            2           4m35s

NAME                              DESIRED   CURRENT   READY   AGE
replicaset.apps/zuul-5d9dbf4949   0         0         0       4m35s
replicaset.apps/zuul-b7f69fbf8    2         2         2       3m20s
部署脚本
    对于部署脚本也是通过Python来调用Kubernetes的API来进行操作的,zuul和eureka的变化不是很大,无非就是变动一下控制器;
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Time    : 2020/4/15 23:43
# @Author  : CaiChangEn
# @Email   : mail0426@163.com
# @Software: PyCharm
import docker, os, shutil, time, kubernetes

registry = '192.168.1.64:8181'
project = 'springcloud'
sitename = 'zuul'
webfile = 'zuul.jar'
dpl_name = 'zuul'
dpl_namespace = 'springcloud'


class Deploy(object):
    def __init__(self, rep, pro, name, file, dname, dnamespace):
        '''
        :param rep: docker仓库地址(Harbor为例);
        :param pro: 仓库的项目名称(Harbor为例)
        :param name: 仓库的镜像名称;
        :param file: 对于SpringCloud项目编译成功后生成的jar包的名称;
        '''
        print('\033[1;31mStep1 Start initialization\033[0m')
        StartTime = time.time()
        self.KubernetesObj = kubernetes.client.Configuration()
        self.KubernetesObj.host = 'https://192.168.1.61:6443'
        self.KubernetesObj.verify_ssl = False
        self.KubernetesObj.api_key[
            'authorization'] = 'eyJhbGciOiJSUzI1NiIsImtpZCI6IlVxTXN4dGI2UXRlNHFoWmNuSDluRzFLQllKRHVrN2tUVE16TEU2N3lKMVkifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJzcHJpbmdjbG91ZCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJzcHJpbmctdG9rZW4tbWh4enEiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoic3ByaW5nIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiZmUzZWNlM2UtNGY2Mi00ZThhLWEzZWYtY2M5NmFlNTVmMjFkIiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OnNwcmluZ2Nsb3VkOnNwcmluZyJ9.Qe-UOE9oT34VAlqeNIZuISznxn5nvnGcRvOsp5J7njcJgjXrGHkjGna-h9yBA4epQE6iJ1jgr80GboXNseMHZx8Ui6UuZXwR_GmRCuyziji1nDqRR2UqocQuTaXSKMGeHsa7F4L7gGytTcq9NlkeNAfGX6iTibD01bRjFCJGRxH9VGYKnng_Q89C1C_JmdLW3VLXOvecugd6xsP9CzktQjZB1CBZSuatehPHc3yjFzkKImP7d3W-y-OaYOK-5QSA5L4-IW10tJrlQC4ovwHVqKcVe1uLvWC3dSQkS5TMn3PELA2-r3De2J_h8xAcAgnFCI0gypJynssypmNpSa3nXg'
        self.KubernetesObj.api_key_prefix['authorization'] = 'Bearer'
        self.DplName = dname
        self.DplNamespace = dnamespace
        self.DockerClientObj = docker.DockerClient()
        self.WorkSpace = os.getenv('WORKSPACE')
        self.BuildId = os.getenv('BUILD_ID')
        self.Context = os.path.join(self.WorkSpace, 'context')
        self.SrcDockerFile = os.path.join(self.WorkSpace, 'Dockerfile')
        self.DestDockerFile = os.path.join(self.Context, 'Dockerfile')
        self.WebSiteFile = os.path.join(self.WorkSpace, 'target', file)
        self.BuildTag = os.path.join(rep, pro, name) + ':' + self.BuildId
        self.Repository = os.path.join(rep, pro, name)
        print('\033[1;32mStep1 Use Time %.3f\033[0m' % float(time.time() - StartTime))

    def createEnv(self):
        '''
        该方法主要是创建一些初始环境,提供Dockerfile和需要构建的上下文
        '''
        print('\033[1;31mStep2 create env\033[0m')
        StartTime = time.time()
        os.mkdir(self.Context)
        shutil.move(self.WebSiteFile, self.Context)
        shutil.move(self.SrcDockerFile, self.Context)
        print('\033[1;32mStep2 Use Time %.3f\033[0m' % float(time.time() - StartTime))
        return self.buildImage()

    def deleteImage(self):
        '''
        该方法主要是删除镜像,如果正在构建的镜像已在本地存在,那么先删除
        '''
        try:
            self.DockerClientObj.images.remove(self.BuildTag)
            print('\033[1;34mDeleted Image %s\033[0m' % (self.BuildTag))
        except Exception:
            pass

    def buildImage(self):
        '''
        该方法正式开始构建镜像,并且给镜像指定对应的tag,默认的tag为jenkins的BUILD_ID
        '''
        print('\033[1;31mStep3 Build Image\033[0m')
        StartTime = time.time()
        self.deleteImage()
        ImageObj, BuildLog = self.DockerClientObj.images.build(path=self.Context, quiet=False,
                                                               dockerfile=self.DestDockerFile,
                                                               tag=self.BuildTag, rm=True,
                                                               forcerm=True)
        for Log in BuildLog:
            print(Log)
        print('\033[1;32mStep3 Use Time %.3f\033[0m' % float(time.time() - StartTime))
        return self.pushImage()

    def pushImage(self):
        '''
        该方法主要是将镜像上传到私有仓库,此案例主要是基于http协议的harbor仓库
        '''
        print('\033[1;31mStep4 Push Image\033[0m')
        StartTime = time.time()
        PushLog = self.DockerClientObj.images.push(repository=self.Repository, tag=self.BuildId,
                                                   auth_config={"username": "admin", "password": "caichangen"},
                                                   decode=True)
        print(PushLog, end='')
        print('\033[1;32mStep4 Use Time %.3f\033[0m' % float(time.time() - StartTime))
        self.deleteImage()
        return self.deployKubernetes()

    def deployKubernetes(self):
        print('\033[1;31mStep5 Deploy Kubernetes\033[0m')
        StartTime = time.time()
        api_instance = kubernetes.client.AppsV1Api(kubernetes.client.ApiClient(self.KubernetesObj))
        sts_instance = api_instance.read_namespaced_deployment(name=self.DplName, namespace=self.DplNamespace)
        sts_instance.spec.template.spec.containers[0].image=self.BuildTag
        api_instance.patch_namespaced_deployment(name=self.DplName, namespace=self.DplNamespace,body=sts_instance)
        print('\033[1;32mStep5 Use Time %.3f\n部署成功\033[0m' % float(time.time() - StartTime))
        return None

deploy = Deploy(registry, project, sitename, webfile, dpl_name, dpl_namespace)
deploy.createEnv()
正式部署
    将上面的脚本放在Jenkins的zuul执行shell的配置里面,那么接下来我们就可以正式部署了;

# 可以看到我们的两个zuul副本的镜像tag也变成了Jenkins的BUILDID
[root@node1 ~]# kubectl get deployments.apps -n springcloud  -o wide
NAME   READY   UP-TO-DATE   AVAILABLE   AGE   CONTAINERS   IMAGES                                 SELECTOR
zuul   2/2     2            2           17m   zuul         192.168.1.64:8181/springcloud/zuul:3   app=zuul
[root@node1 ~]# kubectl get pods -n springcloud -l app=zuul -o json|jq .items[].spec.containers[].image  
"192.168.1.64:8181/springcloud/zuul:3"
"192.168.1.64:8181/springcloud/zuul:3"

# 测试访问

feign/ribbon

    这两个项目就再在下面赘述了,在做的时候和zuul一样就行,他们都是使用的Deployment控制器管理,所以配置大同小异,稍稍修改即可,但是有一点不同的是,这两个项目后端的微服务,所以他们不需要Ingress资源,直接交给zuul调用即可;

项目完结测试

    到此,整个SpringCloud项目的Kubernetes的持续部署已经完结,那么接下来我们就可以对项目进行一个测试了,这个项目的测试分两个方面,第一个方面是通过zuul调用测试,对于SpringCloud项目,zuul是一个网关,所以我们可以通过zuul来调用后端吗的ribbon或者feign项目测试是否能够调通,第二个方面是通过eureka,eureka对于SpringCloud项目来讲,它是一个注册信息,每一个微服务注册到eureka都会在eureka里面存储一份属于自己的一些元信息,其中还包含了自己的连接信息,对于此项目,我们需要通过feign来测试,所以为了测试,我也为feign这个项目做了一个Ingress资源,为了测试;
测试准备
    接下来将在两台非Kubernetes内部的Node节点的机器做测试,因为我们的内部域名,所以需要先在两台机器上做测试,这里选我们的node4,和我们的宿主机,它们都不在Kubernetes集群之内了,因为我们通过了Ingress,所以需要先在这两台机器之上做hosts解析;
node4解析
    在/etc/hosts文件写入zuul、eureka、feign这三个项目做测试
[root@node4 ~]# cat /etc/hosts|grep 'node1'
192.168.1.61 node1 node1.cce.com eureka.springcloud.com zuul.springcloud.com feign.springcloud.com
宿主机解析 宿主机是一台windows机器,所以我们需要在C:\Windows\System32\drivers\etc\hosts写入我们的hosts信息;

查看注册信息
    因为每个微服务启动都会自动注册到我们的eureka上面,eureka本身也不例外,所以此处我们就来简单的看看他们的注册信息;

zuul测试
    可以看到https://github.com/caichangen/SpringCloud这个项目的README.md文件,里面给出了一些测试方式,那么通过zuul测试的方式就是直接通过zuul的zcheck接口来进行调用测试,如下;
# 测试是否能通过zuul调通feign
[root@node4 ~]# curl http://zuul.springcloud.com/feign/zcheck
Feign Health OK from zuul
# 测试是否能通过zuul调通ribbon # 测试通过
[root@node4 ~]# curl http://zuul.springcloud.com/ribbon/zcheck
Ribbon Health OK from zuul  # 测试通过
eureka测试
    可以看到https://github.com/caichangen/SpringCloud这个项目的README.md文件,里面给出了一些测试方式,那么通过eureka测试的方式和zuul不同的是,这里需要通过feign,因为我们请求feign请求测试,它会到eureka里面去获取ribbon的连接信息,然后直接调用,如下;
[root@node4 ~]# curl http://feign.springcloud.com/echeck
fegin->eureka->ribbon->eureka->fegin  # 测试通过

项目总结

    进行到这里,其实我们还只是完成了一些基础的部分,还有很多因素没考虑到,简单的说下最直接的几个,如下;
  • 版本回退:比如说最直接的一个问题,那么就是回滚,回滚我这里没有做,原因有一点,虽然玩了有四五年的Jenkins了,但是可能还是有的地方不是很熟悉吧,因为每个maven项目的构建,都会进行一个操作,那就是拉取代码,那么既然我们的Image已经上传到Harbor了,那么我们就不需要去拉代码,但是我不是很清楚对于Jenkins来讲,如何在一次maven项目的构建之前关闭拉取代码的操作;
    当然,也有很多解决方法,比如我们把一个Job分开两个一个持续集成,一个来做持续部署,那么这样,这个问题也就迎刃而解了,但是这样带来的问题的Job的数量会越来越多,这是一个问题。此外貌似pipline也可以解决这个问题,但是pipline那Low比的语法实在提不起兴趣,所以就暂时先这样吧,后面有比较好的方法再矫正;
  • 水平扩容:对于水平扩容和上面的回滚是一样的,如果能讲一个Job分为多个Job那么对于扩容也是迎刃而解;
  • 持续集成:对于持续集成还有一个问题,因为我们每次构建都会将我们的带来以Image的形式持续的集成到我们的Harbor,那么常年岁月之下,我们的Harbor的服务器存储的镜像数量可能会越来越庞大,这是有问题的,我们可以留最近5个构建的Image剩下的全部删掉,实现方式很简单,因为我们的Harbor也是有API的,用Python调一调即可,为了节省操作,在此就不考虑这些问题了;
  • 持续交付:对于持续交付,也存在一个问题,就是代码该如何优雅的上线,金丝雀、灰度、滚动、蓝绿等发布方式,我这里是没有做太多的考虑的,不过Kubernetes默认是以滚动的发布方式来上线的,所以如果部署到线上,这也是一个非常值得考虑的问题;
  • 监控/日志:对于一个线上的业务环境,监控是必不可少的,保障业务的稳定性的一个基石,同样的,日志也是,也需要对我们定位问题,后面做数据分析都是很重要的数据,在这里就不考虑这个问题了,主要还是实现我们的主题CI/CD;

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注