30、Kubernetes持续交付实践一

30、Kubernetes持续交付实践一

TOC

Kubernetes持续交付实践一

    在上两个章节,分别讲述了基础环境的安装以及环境的测试环节,我们也可以理解一个持续集成的环境,那么在此正式进入我们的最终目标,实现整个大框架,将我们的具有生产能力的应用部署到私有Kubernetes集群中来,那么具体搭建Kuberentes环境就不在此赘述,因为机器的内存较小的原因,原本打算开三个节点的Kubernetes,发现实际情况不允许,Gitlab和Harbor服务器台耗费内存,那么带来的另一个原因就是,我们的应用程序多活可能也比较吃力,这里根据实际情况来确定某应用的副本吧;
    接下来就正式进入实践环节,对于SpringCloud项目还有一个特点,任何应用向eureka注册,默认是通过主机名来注册的,这不符合我们的需要,因为在Pod内部是无法访问通过主机名来实现Pod访问的,所以此时我们需要修改我们是SpringCloud项目,将其注册方式修改为IP注册,直接利用IP向eureka注册,具体配置方式,在下面会讲到;
eureka
    在此主要是说我们的eureka这个项目如何实现持续集成的,首先我们需要知道一个问题,对于SpringCloud项目来讲,对于eureka是个特殊的应用,它是一个无状态应用,但是因为其独有的特性,也就是说其他的任何微服务都需要与其进行注册,所以在此,我们需要使用两种控制器,一种的StateFulSet、Deployment,对于我们的eureka则使用StateFulSet,来让其在Pod重建的时候保证其PodName不发生变化;
Kubernetes预配置
    在正式部署之前,我们需要做一些预配置,比如Namespace、ServiceAccount资源也最好事先去创建;
# 创建一个springcloud的Namespace,作为该项目的命名空间
[root@node1 ~]# cat springcloud_ns.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: springcloud
[root@node1 ~]# kubectl apply -f springcloud_ns.yaml


# 创建一个ServiceAccount,并使其能够管理springcloud这个命名空间
[root@node1 ~]# cat springcloud_sa.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: spring
  namespace: springcloud
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: springcloud-clusterrolebinding
  namespace: springcloud
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
  - kind: ServiceAccount
    name: spring
    namespace: springcloud
[root@node1 ~]# kubectl apply -f springcloud_sa.yaml
# 获取该ServiceAccount的token
[root@node1 ~]# kubectl describe secrets -n springcloud $(kubectl describe sa -n springcloud spring|grep '^Tokens'|awk '{print $2}')|awk '/^token/{print $2}'
...
eureka配置更新
    因为eureka之间也是会相互注册的,所以对于eureka也需要修改其注册地址为现在所有可用的eureka的地址,在后面我们将eureka的副本设为两个,所以这里注册地址也需要写两个,然后还需要修改其配置为IP注册,这样才能让各应用之间互相连通;
# 将eureka的代码clone下来
[root@node4 ~]# git clone http://192.168.1.64/SpringCloud/eureka.git
[root@node4 ~]# cd eureka/
[root@node4 eureka]# cat src/main/resources/application.yml 
spring:
  application:
    name: eureka
server:
  port: 6210
eureka:
  instance:
    prefer-ip-address: true  # 修改为IP注册
    hostname: eureka
  client:
    registerWithEureka: true #true表示向注册中心注册自己。
    fetchRegistry: false
    serviceUrl:  #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
      defaultZone: http://eureka-0.eureka-svc.springcloud.svc.cluster.local:6210/eureka/,http://eureka-1.eureka-svc.springcloud.svc.cluster.local:6210/eureka/  # 配置两个eureka的注册地址
# 提交到主干
[root@node4 eureka]# git add *
[root@node4 eureka]# git commit -m 'edit config'
[root@node4 eureka]# git push origin master
  • 此处修改了配置,别忘记重新打包上传到我们的Harbor
修改Docker配置
    我们的Kubernetes主机之上的docker daemon也需要修改一些配置,主要是因为我们的Harbor仓库的非安全的,所以需要在每台Kubernetes的工作节点配置如下配置;
  • 注意:每台Kuberentes集群的工作节点都需要配置
[root@node1 ~]# cat > /etc/docker/daemon.json << EOF
{
    "registry-mirrors": ["https://owcyje1z.mirror.aliyuncs.com"],
    "insecure-registries":["http://192.168.1.64:8181"]
}
EOF
[root@node1 ~]# systemctl daemon-reload
[root@node1 ~]# systemctl restart docker
Harbor认证
    因为我们的Harbor仓库并不是公开访问的,那么此时,我们的Kubernetes在pull镜像的时候有会碰到用户认证的问题,所以我们需要实现配置一个认证的Secret;
# 首先,我们在Kubernetes主节点上登录一遍,生成registry认证文件
[root@node1 ~]# docker login 192.168.1.64:8181 
[root@node1 ~]# cat /root/.docker/config.json 
{
        "auths": {
                "192.168.1.64:8181": {
                        "auth": "YWRtaW46Y2FpY2hhbmdlbg=="
                }
        },
        "HttpHeaders": {
                "User-Agent": "Docker-Client/19.03.8 (linux)"
        }
}
# 得到该文件的base64密文
[root@node1 ~]# base64 -w 0 /root/.docker/config.json 
ewoJImF1dGhzIjogewoJCSIxOTIuMTY4LjEuNjQ6ODE4MSI6IHsKCQkJImF1dGgiOiAiWVdSdGFXNDZZMkZwWTJoaGJtZGxiZz09IgoJCX0KCX0sCgkiSHR0cEhlYWRlcnMiOiB7CgkJIlVzZXItQWdlbnQiOiAiRG9ja2VyLUNsaWVudC8xOS4wMy44IChsaW51eCkiCgl9Cn0=
# 基于认证文件来创建Secret
[root@node1 ~]# cat registry_auth.yaml
apiVersion: v1
kind: Secret
metadata:
  name: registry
  namespace: springcloud
data:
  .dockerconfigjson: ewoJImF1dGhzIjogewoJCSIxOTIuMTY4LjEuNjQ6ODE4MSI6IHsKCQkJImF1dGgiOiAiWVdSdGFXNDZZMkZwWTJoaGJtZGxiZz09IgoJCX0KCX0sCgkiSHR0cEhlYWRlcnMiOiB7CgkJIlVzZXItQWdlbnQiOiAiRG9ja2VyLUNsaWVudC8xOS4wMy44IChsaW51eCkiCgl9Cn0=  # 上面的base64的密文
type: kubernetes.io/dockerconfigjson
配置清单
    我们需要预先将eureka的配置清单给做出来,并且将其上传到我们的eureka仓库,具体如下;
# 将eureka的代码clone下来
[root@node4 ~]# git clone http://192.168.1.64/SpringCloud/eureka.git
[root@node4 ~]# cd eureka/
[root@node4 eureka]# cat > eureka.yaml << EOF
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: eureka
  labels:
    app: eureka
  annotations:
    author: cce
  namespace: springcloud
spec:
  serviceName: eureka-svc
  replicas: 2
  template:
    metadata:
      name: eureka-pod
      namespace: springcloud
      labels:
        app: eureka
    spec:
      imagePullSecrets:
        - name: registry
      containers:
        - name: eureka-container
          image: 192.168.1.64:8181/springcloud/eureka:4
          imagePullPolicy: IfNotPresent
          livenessProbe:  # 配置健康状态检查
            initialDelaySeconds: 3
            successThreshold: 1
            timeoutSeconds: 10
            failureThreshold: 3
            httpGet:
              port: 6210
              scheme: HTTP
      restartPolicy: Always
  selector:
    matchLabels:
      app: eureka
---
apiVersion: v1
kind: Service
metadata:
  name: eureka-svc
  namespace: springcloud
spec:
  selector:
    app: eureka
  ports:
    - name: http
      protocol: TCP
      port: 6210
      targetPort: 6210
  clusterIP: None   # 无头Service
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: springcloud-ingress
  namespace: springcloud
spec:
  rules:
    - host: eureka.springcloud.com # 记得将其加入hosts文件 192.168.1.61 eureka.springcloud.com
      http:
        paths:
          - backend:
              serviceName: eureka-svc
              servicePort: 6210
            path: /
EOF
[root@node4 eureka]# git add *
[root@node4 eureka]# git commit -m 'add kubernetes config'
[root@node4 eureka]# git push origin master
# 第一次部署的时候先手动部署,但是别忘记了上面做了很多修改,此时需要重新构建下Jenkins,重新生成新的镜像
[root@node1 ~]# kubectl apply -f eureka.yaml
[root@node1 ~]# kubectl get all -n springcloud 
NAME           READY   STATUS    RESTARTS   AGE
pod/eureka-0   1/1     Running   0          3m10s
pod/eureka-1   1/1     Running   0          2m56s


NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
service/eureka-svc   ClusterIP   None         <none>        6210/TCP   3m10s


NAME                      READY   AGE
statefulset.apps/eureka   2/2     3m10s
# 测试连通性
[root@node1 ~]# kubectl exec -it -n springcloud eureka-0 sh
/ # wget -S -O /dev/null -q http://eureka-0.eureka-svc.springcloud.svc.cluster.local:6210  # 第一个eureka正常
  HTTP/1.1 200 
  Content-Type: text/html;charset=UTF-8
  Content-Language: en-US
  Transfer-Encoding: chunked
  Date: Wed, 22 Apr 2020 04:28:12 GMT
  Connection: close
/ # wget -S -O /dev/null -q http://eureka-1.eureka-svc.springcloud.svc.cluster.local:6210 第一个eureka访问第二个eureka正常
  HTTP/1.1 200 
  Content-Type: text/html;charset=UTF-8
  Content-Language: en-US
  Transfer-Encoding: chunked
  Date: Wed, 22 Apr 2020 04:28:16 GMT
  Connection: close
测试访问
    好了,现在我们的eureka已经手动部署完成,接下来就来测试下eureka是否能够正常访问,因为我们使用的是内部域名所以需要先添加hosts解析
# 添加hosts解析

测试访问eureka是否部署成功

部署脚本
    此处并非进行最后的应用部署,只是测试行的利用Python来调通Kubernetes集群,并且直接使用Python来远程管理集群,为最后一步应用部署做基础;
# 找到上面创建的ServiceAccount的token
[root@node1 ~]# kubectl describe secrets -n springcloud $(kubectl describe sa -n springcloud spring|grep '^Tokens'|awk '{print $2}')|awk '/^token/{print $2}'
...
# 该token就可以作为我们管理这个springcloud命名空间的资源了,先测试下是否能够连通

# 在原有的持续集成的脚本之上附加一项持续部署的到Kubernetes的代码,实现持续部署
#!/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 = 'eureka'
webfile = 'eureka.jar'
sts_name = 'eureka'
sts_namespace = 'springcloud'




class Deploy(object):
    def __init__(self, rep, pro, name, file, sname, snamespace):
        '''
        :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.StsName = sname
        self.StsNamespace = snamespace
        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_stateful_set(name=self.StsName, namespace=self.StsNamespace)
        sts_instance.spec.template.spec.containers[0].image=self.BuildTag
        api_instance.patch_namespaced_stateful_set(name=self.StsName, namespace=self.StsNamespace,body=sts_instance)
        print('\033[1;32mStep5 Deploy Kubernetes %.3f\n部署成功\033[0m' % float(time.time() - StartTime))
        return None




deploy = Deploy(registry, project, sitename, webfile, sts_name, sts_namespace)
deploy.createEnv()

正式部署
    接下来就是正式的正式上线环节了,但是有一点别忘记了,镜像的tag是通过Jenkins的BUILDID来生成的;

# 可以看到我们的两个eureka副本的镜像tag也变成了Jenkins的BUILDID
[root@node1 ~]# kubectl get pods -n springcloud -o json|jq .items[].spec.containers[].image
"192.168.1.64:8181/springcloud/eureka:5"
"192.168.1.64:8181/springcloud/eureka:5"
收官测试
    接下来做最后一次测试,eureka的收官之测;

# 查看创建的镜像

# 可以看到,我们的上线流程的切换着来
[root@node1 ~]# kubectl get pods -n springcloud -o json|jq .items[].spec.containers[].image
"192.168.1.64:8181/springcloud/eureka:5"
"192.168.1.64:8181/springcloud/eureka:5"
[root@node1 ~]# kubectl get pods -n springcloud -o json|jq .items[].spec.containers[].image
"192.168.1.64:8181/springcloud/eureka:5"
"192.168.1.64:8181/springcloud/eureka:6"
[root@node1 ~]# kubectl get pods -n springcloud -o json|jq .items[].spec.containers[].image
"192.168.1.64:8181/springcloud/eureka:5"
"192.168.1.64:8181/springcloud/eureka:6"
[root@node1 ~]# kubectl get pods -n springcloud -o json|jq .items[].spec.containers[].image
"192.168.1.64:8181/springcloud/eureka:5"
"192.168.1.64:8181/springcloud/eureka:6"
[root@node1 ~]# kubectl get pods -n springcloud -o json|jq .items[].spec.containers[].image
"192.168.1.64:8181/springcloud/eureka:5"
"192.168.1.64:8181/springcloud/eureka:6"
[root@node1 ~]# kubectl get pods -n springcloud -o json|jq .items[].spec.containers[].image
"192.168.1.64:8181/springcloud/eureka:5"
"192.168.1.64:8181/springcloud/eureka:6"
[root@node1 ~]# kubectl get pods -n springcloud -o json|jq .items[].spec.containers[].image
"192.168.1.64:8181/springcloud/eureka:5"
"192.168.1.64:8181/springcloud/eureka:6"
[root@node1 ~]# kubectl get pods -n springcloud -o json|jq .items[].spec.containers[].image
"192.168.1.64:8181/springcloud/eureka:6"
"192.168.1.64:8181/springcloud/eureka:6"

  • 至此,整个eureka的部署已经完成,那么在下一个环节将结束整个SpingCloud项目的持续交付的操作;

发表评论