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项目的持续交付的操作;