DevOps主要原理方面,不是本次分享的重点,我就不赘述了
虽然我们前面说DevOps开发模式还在演进,但是我们今天讲解的CI/CD却是DevOps中,比较确定的部分,也是其必要组成部分:
下面来着重讲解下CI/CD的核心概念:
CI/CD的概念:一种通过在应用开发阶段引入自动化来频繁向客户交付应用的方法,核心概念是持续集成、持续交付和持续部署;
从上图可以看到,CI/CD的过程随项目实际情况会有些差异;并且,CI和CD也没有一个完全确定的先后关系,需要按照项目需求特点去规范,定义。
这个流程图跟上一张ppt上面的示意图差不多,却能更具体的体现出来了jenkins的内部功能流程,具体包括:
总结起来,jenkins就是通过定义了一种编程语法和插件规则,以及丰富的对外API接口,用以实现复杂流程:
pipeline{
agent any
tools {
maven 'maven-3.5.2'
jdk 'JDK8'
}
stages {
stage('=============================================代码检出已经打包docker镜像=============================================') {
steps{
script{
uuidUrl="[email protected]:saas/uuid-service.git";
deployUrl="[email protected]:caoyong1/knowledge_share.git";
credentialsId="caoyong1-gitlab-lenovo-com";
}
echo "清理当前目录中的代码"
sh("rm -rf *")
dir("deploy"){
echo "=============================================开始deploy部分检出代码============================================="
checkout([$class: 'GitSCM', branches: [[name: 'refs/heads/master']], userRemoteConfigs: [[credentialsId: "$credentialsId", url: "$deployUrl"]], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: []])
}
dir("uid"){
echo "=============================================开始uuid部分检出代码============================================="
checkout([$class: 'GitSCM', branches: [[name: 'refs/heads/master']], userRemoteConfigs: [[credentialsId: "$credentialsId", url: "$uuidUrl"]], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: []])
## 此处是读取pom文件中的version,用来指定镜像的版本号$vName
script{
pom = readMavenPom file: 'pom.xml'
vName=pom.version
}
echo "=============================================打包镜像uuid:$vName============================================="
sh("mvn clean compile package -P saas")
sh("docker build --no-cache -f ../deploy/demo/docker/uuid/uuid.dockerfile -t harbor.aipo.lenovo.com/apps/uuid:$vName .")
sh("docker push harbor.aipo.lenovo.com/apps/uuid:$vName")
}
}
}
}
}
顺便看下dockerfile:
FROM harbor.aipo.lenovo.com/base_env/alpine_java:8
WORKDIR /root/services
COPY target/*.jar ./app.jar
EXPOSE 30001
ENTRYPOINT ["java","-jar","app.jar","--server.port=30001"]
简单说明下dockerfile步骤…
如上就是整个持续集成的过程,因为我们CI/CD的要求之一是自动化,即人工参与的越少越好,那么如何触发整个job就是一个很重要的问题:我们可以选择1,在gitlab中设置webhook,2:在外部使用postman进行触发;3:也可以在jenkins本地触发
打开:https://gitlab.lenovo.com/saas/uuid-service/-/settings/integrations
如果使用gitlab进行触发,可以指定push事件、tag push等事件规则进行触发,目前因为jenkins和gitlab不在一个网段,所以,不能webhook成功;
下面我们演示下通过postman模拟在外部进行触发;
为了节省事件,大家直接看我之前触发的结果:http://10.110.149.185/view/demo/job/uuid.image/21/console
通过日志进行简单过程讲解…
该操作过程结果是,我们打包好的docker镜像能够在https://harbor.aipo.lenovo.com中看到(caoyong1
/Caoyong1@lenovo)
打开harbor进行镜像查看apps/uuid…
接下来我们继续看gitlab相关功能:
gitlab其实大家都很熟悉,大致讲下内容
首先我们给docker下一个非常简单的定义:
ppt上面这张图描述的就是镜像和容器文件系统的层叠结构,如何理解呢?下面演示下,大家就很容易就懂了:
我们来看下一个dockerfile,所谓dockerfile,就是创建镜像的配置文件
FROM centos:7.6.1810
LABEL maintainer=caoyong1
WORKDIR /root
COPY caoyong.txt .
#指定时区以及安装各种插件
RUN yum -y install iputils && yum -y install net-tools.x86_64 && yum install -y redhat-lsb && yum -y install bridge-utils && yum -y install traceroute && yum -y install vim*
#指定字符集
COPY caoyong2.txt .
大致说下过程,
1.依赖于centos7.6基础镜像
2.拷贝宿主机文件caoyong.txt到镜像中
3.安装若干软件到镜像中
4.拷贝caoyong2.txt到镜像中
FROM centos:7.6.1810
LABEL maintainer=caoyong1
WORKDIR /root
COPY caoyong.txt .
#指定时区以及安装各种插件
RUN yum -y install iputils && yum -y install net-tools.x86_64 && yum install -y redhat-lsb && yum -y install bridge-utils && yum -y install traceroute && yum -y install vim*
COPY caoyong2.txt .
同样,为了节省时间,我在机器上面提前运行出来了结果;
先看下镜像的文件层级结构:
docker images
docker inspect 5d647d726379
先看Upper层,在看Lower层
其中
find . -type f |more
所以,可以看到docker的文件目录是一层一层叠加起来的
https://blog.csdn.net/11b202/article/details/21389067
容器启动之后,当读取文件的时候,按照从上往下的顺序进行读取,在某一层找到,就将副本读取到Upper层,如果修改,也是修改的Upper层中的副本,所以,镜像层的文件是只读的,容器层,即Upper层,的文件是可写的,在Upper层可以写入文件。
所以,我们平时写dockerfile的时候,尽量一次性拷贝所有的文件,一次性安装所有的文件,能够避免不必要的分层
其他的资源隔离,比如说网络隔离,pid隔离,ipc(就是进程间通讯)隔离、UTS(主机和域名)隔离等有兴趣的可以研究下,这里就不多说了。
通过实例看下进程如何隔离的
在宿主机上面,查看到进程 ps -ef|grep ef6f2aab9bec
执行docker exec -it ef6f2aab9bec /bin/bash,切换到容器的namespace下,ps -ef可以看到PID为1的进程id,
其实这两个进程是同一个进程,不过是在容器的namespace下,进程的被克隆,改变了一下名字和id而已;容器中的其他的进程,其实都是PID为1的这个进程的子进程而已,所以,如果PID这个进程退出了,其他的进程就自动退出了;在k8s的pod中,也是通过管理容器的PID为1的这个进程,来控制容器的启动退出的;
另一个值得注意的方面是,docker exec命令可以进入指定的容器内部执行命令。由它启动的进程属于容器的namespace和相应的cgroup。但是这些进程的父进程是Docker Daemon而非容器的PID1进程。
上面介绍了docker的本质,就是进程集合和文件系统的隔离,需要的所有功能,都要依赖宿主机的接口来提供,做到这些,我们就可以只需要配置一次部署环境,将应用以克隆的方式部署到任意的主机上了。
当然,我们光隔离是不行的,因为隔离带来了好处的同时,也带来了很多相关的问题,比如说网络不通,比如说存储,各种挂载管理起来也比较混乱,还有日志收集,服务检测等等,于是我们就需要一种,能够打通隔离和存储等资源治理混乱局面的工具,于是,kubernetes就是这样闪亮登场了。
kubernetes是谷歌开源的容器集群管理系统,非常牛掰,目前行业标准,完了。
(参考:https://www.cnblogs.com/Tao9/p/12026232.html)
因为时间关系,这里就不讲了,图中也能大致看下整个pod创建和维护的过程,有时间大家可以自己研究下
pipeline{
agent any
tools {
maven 'maven-3.5.2'
jdk 'JDK8'
}
stages {
stage('=========================================将服务在k8s集群上运行============================================') {
steps{
script{
uuidUrl="[email protected]:saas/uuid-service.git";
deployUrl="[email protected]:caoyong1/knowledge_share.git";
credentialsId="caoyong1-gitlab-lenovo-com";
ns = "ns-uuid-$envName"
}
dir("namespaces/$ns"){
echo "==================================创建namespace:$ns=================================="
sh("cp ../../demo/k8s/uid/namespace.yaml .")
sh("cp ../../demo/k8s/uid/cm-${envName}.yaml .")
sh("sed -i -e 's/\${NAMESPACE}/${ns}/g' namespace.yaml")
sh("kubectl apply -f namespace.yaml --validate=false")
echo "===================================创建configMap:env-config=================================="
sh("kubectl apply -f cm-${envName}.yaml --validate=false")
}
dir("pods/uuid"){
echo "===================================创建应用pod:uuid:${version}===================================="
sh("cp ../../demo/k8s/uid/uuid.yaml .")
sh("sed -i -e 's/\${NAMESPACE}/${ns}/g' \
-e 's/\${VERSION}/$version/g' \
-e 's/\${NODEPORT}/$nodePort/g' \
uuid.yaml")
sh("kubectl apply -f uuid.yaml --validate=false")
}
}
}
}
}
apiVersion: v1
kind: Namespace
metadata:
name: ${NAMESPACE}
apiVersion: v1
kind: ConfigMap
metadata:
name: env-config
namespace: ns-uuid-prod
data:
demo.value: prod #demoValue的值是prod
apiVersion: v1
kind: ConfigMap
metadata:
name: env-config
namespace: ns-uuid-test
data:
demo.value: test #demoValue的值是test
我们待会演示的时候,会在访问接口中看到demoValue中的值
3. 运行pod,启动uuidService容器
讲解下uuid的deployment编排文件:
apiVersion: apps/v1
kind: Deployment # Deployment只负责管理不同的版本的ReplicaSet,由ReplicaSet管理Pod副本数
# 每个ReplicaSet对应Deployment template的一个版本
# 一个ReplicaSet下的Pod都是相同的版本。
metadata:
name: uuid-deploy
namespace: ${NAMESPACE}
spec: #说明书开始,其实是ReplicaSet的说明书,隐藏了ReplicaSet这也是配置很不好理解的原因
replicas: 3 # pod副本数
selector:
matchLabels:
app: uuid-label #通过spec.selector字段来指定这个ReplicaSet管理哪些Pod。在上面的例子中,新建的ReplicaSet会管理所有拥有app:nginxLabel的Pod。
strategy:
type: Recreate
template: # template中所描述的是pod
metadata:
labels:
app: uuid-label # pod的标签
spec:
containers: # 容器描述
- name: uuid-server #容器的名称
image: harbor.aipo.lenovo.com/apps/uuid:${VERSION} # 容器的全名
imagePullPolicy: Always # 容器拉取策略
ports:
- name: http
containerPort: 30001 #容器暴露的端口
envFrom:
- configMapRef:
name: env-config #使用的configMap环境变量
---
apiVersion: v1
kind: Service #定义service
metadata:
name: uuidservice
namespace: ${NAMESPACE}
spec:
type: NodePort #service类型,NodePort是可以对外暴露服务
selector:
app: uuid-label #此处的选择器选择的是pod
ports:
- name: http
port: 80 #service暴露的端口
targetPort: 30001 #对应容器的端口
nodePort: ${NODEPORT} # service对外暴露的端口
#通过template.metadata.labels字段为即将新建的Pod附加Label。在上面的例子中,新建了一个名称为nginx的Pod,它拥有一个键值对为app:nginx的Label。
#通过spec.selector字段来指定这个RC管理哪些Pod。在上面的例子中,新建的RC会管理所有拥有app:nginxLabel的Pod。这样的spec.selector在Kubernetes中被称作Label Selector。
调用prod过程:使用postman:
curl --location --request POST 'http://10.110.149.185/generic-webhook-trigger/invoke?token=uuid.runner' \
--header 'Content-Type: application/json' \
--header 'X-Gitlab-Event: Tag Push Hook' \
--header 'X-Gitlab-Token: 93e7593af81a3862890f99dc53dee758' \
--header 'Content-Type: text/plain' \
--data-raw '{
"version": "v1.0.0.0",
"envName": "prod",
"nodePort":30051
}'
调用test过程:使用postman,调用触发jenkins:
4. 打开浏览器测试
test环境:http://10.110.149.172:30050/status
prod环境:http://10.110.149.172:30051/status
我们在上面的接口中,可以看到在不同的命名空间下,即在测试环境和生成环境中,demoValue的值是不同的,两个值分别来自于两个命名空间下的configMap;
接下来,我们来对版本回退和升级做介绍:
前文中我们提到,在uuid项目中,是根据pom中的version来定义镜像的版本的;我提前打包完成了两个镜像,在harbor上面我们也可以看到,分别是v1.0.0.0和v2.0.0.0
应用版本升级:
可以直接重新调用jenkins,在参数中指定镜像版本,重新对k8s上面的pod进行编排,效率有点低,有点浪费时间和资源
还有更好的方式,就是直接对镜像进行升级:
#镜像升级命令
#先查看pods,有三个副本
kubectl get pods -n ns-uuid-test
kubectl set image deployment/uuid-deploy uuid-server=harbor.aipo.lenovo.com/apps/uuid:v2.0.0.0 -n ns-uuid-test
#查看pods,有三个副本
kubectl get pods -n ns-uuid-test
#查看pod启动过程中的事件
kubectl describe pod/uuid-deploy-577cfb7467-cggr4 -n ns-uuid-test
#查看镜像版本
kubectl get pod/uuid-deploy-5fcdc755f6-6hb8w -o yaml -n ns-uuid-test
在通过接口访问,可以看到镜像的版本为v2.0.0.0;
回退版本过程:
#看下deployment的reversion
kubectl describe deployment/uuid-deploy -n ns-uuid-test
#查询版本列表
kubectl rollout history deployment/uuid-deploy -n ns-uuid-test
#保存的版本数量是可以指定的
#回滚到Deployment到某个版本,需要先查询版本列表
kubectl rollout undo deployment/uuid-deploy --to-revision=1 -n ns-uuid-test
#回退到上一个版本
kubectl rollout undo deployment/uuid-deploy -n ns-uuid-test
在通过接口访问,可以看到镜像的版本为v1.0.0.0;
配置文件其实也是可以使用
虽然说,在版本升级、回退过程中,可能还有很多事情要做,比如数据库中数据结构的变化,配置文件的变化等等,起码在应用层级上,k8s模式,为我们提供了无需手动的版本升级回退功能,带来了很大的方便。
如上就是我今天分享的k8s环境下的CI/CD集成 方面的全部内容,非常感谢李飞,李冰洁,王磊对本次分享的大力支持,在他们提供的大量资料,我只是对材料进行了部分整理,并且提供了演示环境,谢谢大家!