MENU

Kubernetes进阶:使用jenkins构建CI/CD平台

2019 年 06 月 15 日 • 应用服务器

emmmmmm

在看本文章之前您需要熟悉jenkins&&Docker&&k8s的使用,因为这次是基于k8s平台做持续集成(CI)和持续交付(CD)的,了解目前比较主流的版本仓库,譬如Git&&svn,熟悉项目的发布流程,这样就可以了,下面大概了解一下什么是CI/CD

CI/CD概述

大概了解一下CI/CD是啥子,其实之前做过这东西,但是没解释过。

  • 持续集成

    • (Continuous Integration,CI) :代码合并构建部署测试都在一起,不断地执行这个过程,并对结果反馈。
  • 持续部署

    • (Continuous Deployment,CD):部署到测试环境、生产环境
  • 持续交付

    • (Continuous Delivery,CD):将最终产品发布到生产环境。

都有个持续,也就是说在不断重复做这件事情,说白了这东西最终的目的就是将项目更有效的部署/更新,他的流程大概是这样,以java为例,开发提交代码到版本仓库后,通过jenkins这个持续集成的软件进行代码的拉取、单元测试、代码的编译、和镜像的构建,一会会用到jenkinsmaster-slave架构,slave可以分担master的任务,一会部署的jenkins也是在k8s集群中,就是一个pod,所以自动添加的slave节点就是一个pod,有任务触发时,jenkins会自动创建一个pod来作为他的slave,这个slave会去完成代码的拉取、测试、编译、构建镜像、推送镜像到仓库,推到镜像仓库后就可以部署在你需要的地方了,部署完之后通过ingress或是NodePort发布你的应用,发布之后就可以访问了撒,流程图如下。

jenkins会完成上图的所有步骤,其实之前做的jenkins也可以完成这一套操作了,但不是部署到k8s平台的,这次是针对K8S的,一会会涉及到几个jenkins插件,这几个插件会帮助我们将项目部署到k8s平台,要知道jenkins百分之90的功能都是由插件实现的,下面要配置的插件里比较复杂的可能就是Pipeline了,还好我有狗头,哈哈。

先说一下需要准备的环境吧,首先需要一个k8s集群且部署有coredns,这个是必须的,无论你是用啥子方式部署的,如果现在没有集群请参考这篇文章使用kubeadm快速部署一个集群,我还是用之前二进制安装的集群。

还需要一个准备一个镜像仓库,可以自建或是直接用各种云提供商的,自建的话建议用Harbor,其实我不太喜欢用自建的,非https,还得去改docker的配置文件且需要重启,我最讨厌重启了。

再就是准备一个代码仓库,这里直接用git了,也是目前比较主流的版本仓库,我顺便把harbor也装了,但估计不会用它来存储镜像,下面先把Git&harbor装了吧。

部署Harbor镜像仓库

这个是用来存镜像的,我直接在kubeadmnode节点上部署了,一会把git也仍在这里吧,懒得去创建虚拟机了,Harbor官方地址,目前有两种安装包,分别是在线安装和离线安装,我用的在线安装,因为离线安装包有点大,还需要docker-compose的支持,

[root@kubeadm-node ~]# curl -L "https://github.com/docker/compose/releases/download/1.24.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose && chmod +x /usr/local/bin/docker-compose
[root@kubeadm-node /]# wget https://storage.googleapis.com/harbor-releases/release-1.8.0/harbor-online-installer-v1.8.0.tgz
[root@kubeadm-node /]# tar zxf harbor-online-installer-v1.8.0.tgz
[root@kubeadm-node /]# cd harbor/
[root@kubeadm-node /harbor]# ls
harbor.yml  install.sh  LICENSE  prepare

需要先初始化一下,

[root@kubeadm-node /harbor]# ./prepare

会下载一个镜像,目录里面多东西了,编辑一下harbor.yml改点东西,三处。

hostname: 192.168.1.248 #访问harbor的域名,没域名写IP
data_volume: /harbor  #数据目录,改不改看你撒
harbor_admin_password: Harbor12345 # 默认admin登陆密码,改不改随你

改完之后就可以安装了,直接执行install.sh就可以了,会下载N个镜像,下载完成之后会自行启动。

[root@kubeadm-node /harbor]# ./install.sh 
[root@kubeadm-node /harbor]# docker-compose ps 

访问一下,

部署还是比较简单的,随便推个镜像试试。

这个就比较烦人了,我特么实在是不想重启docker,算了,估计一会就不用了,用云提供商的吧,下面部署一下Git

部署Git仓库

git是目前比较主流的,先把这个包装一下吧,

[root@kubeadm-node ~]# yum -y install git

访问仓库的方式这里使用ssh方式来访问了,随便创建一个用户,然后设置一个密码。

[root@kubeadm-node ~]# useradd rj-bai
[root@kubeadm-node ~]# echo Sowhat? | passwd --stdin rj-bai 

切换到刚刚创建的用户,创建一个目录,初始化一下作为代码仓库。

[rj-bai@kubeadm-node ~]$ mkdir rj-bai.git
[rj-bai@kubeadm-node ~]$ cd rj-bai.git/
[rj-bai@kubeadm-node ~/rj-bai.git]$ git --bare init 
Initialized empty Git repository in /home/rj-bai/rj-bai.git/
[rj-bai@kubeadm-node ~/rj-bai.git]$ ls
branches  config  description  HEAD  hooks  info  objects  refs

这就创建完了,具体怎么拉取这个代码,如下,在master上执行了,直接git clone就行了。

[root@master-1 ~]# cd /tmp/ && ls
metrics-server  systemd-private-5afd87d103b74339a4fac02fdb472124-chronyd.service-FVUqYN
[root@master-1 /tmp]# git clone rj-bai@192.168.1.248:/home/rj-bai/rj-bai.git
Cloning into 'rj-bai'...
Warning: Permanently added '192.168.1.248' (ECDSA) to the list of known hosts.
rj-bai@192.168.1.248's password: 
warning: You appear to have cloned an empty repository.
[root@master-1 /tmp]# ls rj-bai/

提示目录时空的,随便创建一个文件提交一下

[root@master-1 /tmp/rj-bai]# cat <<OEF >index.html 
> testing
> OEF
[root@master-1 /tmp/rj-bai]# cat index.html 
testing
[root@master-1 /tmp/rj-bai]# git add .
[root@master-1 /tmp/rj-bai]# git config --global user.email "your@email"
[root@master-1 /tmp/rj-bai]# git config --global user.name "rj-bai"
[root@master-1 /tmp/rj-bai]# git commit -m 'index'
[master (root-commit) 8f27bc9] index
 1 file changed, 1 insertion(+)
 create mode 100644 index.html
[root@master-1 /tmp/rj-bai]# git push origin master 
rj-bai@192.168.1.248's password: 
Counting objects: 3, done.
Writing objects: 100% (3/3), 215 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To rj-bai@192.168.1.248:/home/rj-bai/rj-bai.git
 * [new branch]      master -> master

提交到了master分支,验证一下,这个目录删了,重新拉去一下,能拉到就说明没啥子问题。

[root@master-1 /tmp]# rm -rf rj-bai/
[root@master-1 /tmp]# git clone rj-bai@192.168.1.248:/home/rj-bai/rj-bai.git
Cloning into 'rj-bai'...
rj-bai@192.168.1.248's password: 
remote: Counting objects: 3, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (3/3), done.
[root@master-1 /tmp]# ls rj-bai/
index.html

嗯,就是这样,莫得问题,还有就是每次都要输入密码,配一个秘钥就好了,我现在有了,我直接copy去了。

[root@master-1 ~]# ssh-copy-id rj-bai@192.168.1.248
[root@master-1 ~]# git clone rj-bai@192.168.1.248:/home/rj-bai/rj-bai.git
[root@master-1 ~]# ls rj-bai/
index.html

这样就可以了,仓库和Git都准好了,暂时先不动他了,下面开始在k8s集群中部署jenkins

在K8S中部署jenkins

jenkins官网,既然是在k8s中部署就直接使用docker方式了,github地址,官方说明有一个目录需要做持久化,也就是/var/jenkins_homejenkins所有的数据都是存在这个目录下面的,jenkins还需要一个唯一的网络标识,也就是说需要有状态的去部署jenkins了,这里就不多BB了,直接用StatefulSet方式去部署,依旧使用NFS动态供给作为存储,NFS的部署方法之前写过了,部署jenkinsYAML文件是使用官方的模板,现在直接git clone下来。

[root@master-1 ~]# mkdir jenkins
[root@master-1 ~]# cd jenkins/
[root@master-1 ~/jenkins]# git clone https://github.com/jenkinsci/kubernetes-plugin.git

克隆完之后进入到这个目录。

[root@master-1 ~/jenkins/kubernetes-plugin/src/main/kubernetes]# pwd
/root/jenkins/kubernetes-plugin/src/main/kubernetes
[root@master-1 ~/jenkins/kubernetes-plugin/src/main/kubernetes]# ls
jenkins.yml  service-account.yml

可以看到有两个文件,service-account.yml文件是创建RBAC授权相关的东西,这个不要动,主要看一下jenkins.yml

这里使用了PV的模板,是需要你提供PV自动供给的支持的,默认的类型是anything,目前我只有一个NFS的,也就是这个。

所以说一会创建的时候它默认就会用这个了,如果你有两种类型的存储且需要指定就按正常流程走指定就完了,申请磁盘空间这里我把它改成5G了,1G实在是有点小,还有资源限制那里,限制最小0.5CPU&0.5G内存,最大11G内存,说实话有点小,最起码内存调大一点,我这里最大限制都调成2了。

又看了一眼Service这里,访问方式用的是Ingress,你可以用NodePort方式去访问,我这里用Ingress方式去做了,好久没搞过nginx-ingress了,先把tls方法去了,hosts随便改一下,我改完的如下。

如果你不想用Ingress方式就删除掉上图光标的位置的注释,type改为NodePort且略过部署下面部署nginx-ingress的部分,我这里部署一下nginx-ingress

部署nginx-ingress

还是直接用官方的模板,先git clone下来,进到这个目录。

[root@master-1 ~/demo/nginx-ingress]# git clone https://github.com/nginxinc/kubernetes-ingress.git
[root@master-1 ~/demo/nginx-ingress/kubernetes-ingress/deployments]# pwd
/root/demo/nginx-ingress/kubernetes-ingress/deployments

按着官方的步骤这样部署,当然有些暂时用不到的步骤我直接省略了,官方文档

1.创建命名空间和服务账户。

[root@master-1 ~/demo/nginx-ingress/kubernetes-ingress/deployments]# kubectl apply -f common/ns-and-sa.yaml 
namespace/nginx-ingress created
serviceaccount/nginx-ingress created

2.使用TLS证书和NGINX中默认服务器的密钥创建密钥

说白了就是给nginx一个默认的TLS证书,这个证书也是他们自签的,不受信任。

[root@master-1 ~/demo/nginx-ingress/kubernetes-ingress/deployments]# kubectl apply -f common/default-server-secret.yaml 
secret/default-server-secret created

3.创建用于自定义nginx的配置文件

[root@master-1 ~/demo/nginx-ingress/kubernetes-ingress/deployments]# kubectl apply -f common/nginx-config.yaml 
configmap/nginx-config created

4.配置RBAC

[root@master-1 ~/demo/nginx-ingress/kubernetes-ingress/deployments]# kubectl apply -f rbac/rbac.yaml 
clusterrole.rbac.authorization.k8s.io/nginx-ingress created
clusterrolebinding.rbac.authorization.k8s.io/nginx-ingress created

5.部署ingress控制器

这里的话用两种方法,分别为deployment&&DaemonSet,我这里直接用DaemonSet方式去部署了,具体为什么,官方说明,翻译内容如下。

如果您创建了DaemonSet,则Ingress控制器容器的端口80和443将映射到运行容器的节点的相同端口。要访问Ingress控制器,请使用这些端口以及运行Ingress控制器的群集中任何节点的IP地址。

省得再去创建Services了,看了一下部署控制器的文件,args里面启用了两个,还有6个没启用,我翻译了一下,结果如下。

好像暂时没啥子能用到的,查看状态的可以考虑开启,我这里暂时就不开启了,直接创建了。

[root@master-1 ~/demo/nginx-ingress/kubernetes-ingress/deployments]# kubectl apply -f  daemon-set/nginx-ingress.yaml 
daemonset.extensions/nginx-ingress created
[root@master-1 ~/demo/nginx-ingress/kubernetes-ingress/deployments]# kubectl -n nginx-ingress get pod 

这样就可以了撒,部署完了,所有Node节点都有运行一个nginx-ingress控制器,除了两个master节点,因为我没允许master运行PodIngress部署完了,现在可以部署jenkins了。

部署jenkins

部署文件之前已经修改过了,所以直接创建就完了。

[root@master-1 ~/jenkins/kubernetes-plugin/src/main/kubernetes]# kubectl apply -f .
[root@master-1 ~/jenkins/kubernetes-plugin/src/main/kubernetes]# kubectl get pod -w 

正常启动了,写个hosts就访问就可以了撒。

不用去看这个文件,pod日志里有输出密码,直接看pod日志就行了。

[root@master-1 ~]# kubectl logs jenkins-0 

之后就需要装插件了,不装任何插件,一会用啥子装啥子,

现在不装插件最主要的原因其实就是有些插件装了需要重启jenkins才可以,如果现在装完了插件完成后续操作后会回到jenkins主页面,但是jenkins主页面就是一片空白,需要重启jenkins才可以,当然pod是没有重启这个概念的,你得想办法把之前的pod弄死,k8s帮你重新拉起一个之后就正常了,所以先不装插件,正常的话完成后续操作就可以看到主页面了。

现在jenkins是装完了,下面需要jenkins和集群融合一下了。

jenkins在k8s动态创建代理

现在需要jenkinsk8s集成,需要用到一个名为kubernetes的插件,这个插件是用来动态创建代理的,也是自动创建slave端,slave就是你添加的工作节点,这里不需要你手动添加了,在有任务的时候jenkins会自动创建pod作为自己的slave节点,master-slave架构解决了master单点负载的问题,使其演变成了一个分布式的架构,其实slave节点就是运行了一个jar包,我之前也搞过这个,这个jar包启动后会去连接master接收任务。

目前jenkinsk8s中的一个pod,所以他动态创建的slave也是集群中的podmasterslave端通讯是通过jnlp协议进行的,在有任务的时候jenkins会请求k8s集群帮他创建slave(也就是pod)来完成任务,任务完成后这个pod就会自动销毁,下次有需要再启,想实现这个功能就需要一个插件了撒。

安装配置插件

暂时先装两个插件吧,名为git&&kubernetes,点开系统设置→插件管理→available搜一下这两个插件,安装就行了,勾上安装完成后重启,其实我只选择了上述的两个插件,其他的都是依赖。

装完之后需要配置一下这个插件,使jenkins支持kubernetes,点开系统管理→系统设置→拉到最下面,可以看到这个,直接点进去,Kubernetes这里的话配这样就行了,这里写的都是dns名称。

测试连接莫得问题,凭据和key都不用到,都已经rbac授权了,如果是部署在集群外key那里就需要写apiserverCA信息了,再然后就是配置jenkins这里了。

这样就可以了,如果你的jenkins在集群外kubernetes地址就不要写内部DNS名称了,key那里需要配置apiserverCA证书就可以了,下面还有一个添加pod模板,也就是这个,

这个东西就是定义如何创建slave pod的一个模板,你可以理解为这里就是一个YAML文件定义了怎么去创建slave-pod,只不过是通过UI的形式去配置,这一块也可以通过Pipeline脚本去定义,所以不需要在这里配置这个了,如果在这里配置你可能每次添加一个项目都要在这里配置一次,不方便管理,在Pipeline里面配置就比较方便了,所以目前只需要配置怎么连接kubernetes就可以了,这个差掉,保存退出就可以了,下面开始构建slave镜像。

构建jenkins-slave镜像

做一个验证,都配置完了,到底能不能用,直接用流水线了,目前还没装这个插件,所以先装一下,插件名就是Pipeline

这次装的比较多,让他装着吧,装完后自动重启,一会会写一个Pipeline脚本,以测试jenkins能不能在集群中动态创建slave-pod,这个是必需步骤,否则无法继续下去,这个slave-pod的镜像需要自己做一个,下面开始制作这个镜像,其实是有默认的镜像,但是不用他默认的,对于我来说功能不全,这个slave镜像会完成代码拉取、单元测试、代码编译、构建镜像、推送镜像的步骤,所以现在做的slave镜像要有这些功能,下面开始编写Dockerfile

编写Dockerfile

上文提到了这个镜像需要完成代码拉取,单元测试(和现在没啥子关系),代码编译,镜像构建以及推送镜像,所以你的镜像需要支持这些功能才可以,所以Dockerfile如下,按着自己的情况更改吧,官方参考地址

FROM centos:latest
RUN yum install -y java-1.8.0-openjdk maven curl git libtool-ltdl-devel && \ 
    yum clean all && \
    rm -rf /var/cache/yum/* && \
    mkdir -p /usr/share/jenkins
COPY slave.jar /usr/share/jenkins/slave.jar  
COPY jenkins-slave /usr/bin/jenkins-slave
COPY settings.xml /etc/maven/settings.xml
RUN chmod +x /usr/bin/jenkins-slave
ENTRYPOINT ["jenkins-slave"]

settings.xmlmaven的配置文件,这个文件怎么写建议咨询一下开发人员,我还是用我们私服的,用默认的也可以,可能下载依赖包会比较慢,默认地址是国外的,jenkins-slave也是在参考地址直接copy过来的,也不贴了,slave.jar自行下载吧,地址就是$JENKINS_URL/jnlpJars/slave.jar,我这里也不传了,都准备好了直接build就完了。

[root@master-1 /data/docker/jenkins-slave-jdk]# ls
Dockerfile  jenkins-slave  settings.xml  slave.jar
[root@master-1 /data/docker/jenkins-slave-jdk]# docker build -t jenkins-slave-jdk:1.8 .

这样就构建完了,然后把这个镜像推送到镜像仓库,我不用刚刚搭建的Harhor,我直接推到docker hub去了,就是比较慢

[root@master-1 /data/docker/jenkins-slave-jdk]# docker tag jenkins-slave-jdk:1.8 bairuijie/jenkins-slave-jdk:1.8
[root@master-1 /data/docker/jenkins-slave-jdk]# docker push bairuijie/jenkins-slave-jdk:1.8

容器还需要构建和推送镜像,我并没有装在容器里安装docker环境,之后在启动这个pod的时候以数据卷的形式挂载宿主机的docker命令和socket进去就可以了,这只是一个适合拉取git仓库代码和构建java项目的镜像,如果你是别的自行琢磨吧,这种问题多去问开发,下面来了解一下Pipeline

Pipeline 构建流水线发布

Pipeline 就是一套插件,刚刚也安装了,上文提到的流程从拉取到部署都需要由Pipeline来完成,Pipeline是通过特定的语法从简单到复杂的传输管道进行建模,支持两种定义的方式,一种为声明式,遵循Grovvy相同语法,使用pipeline {},还有一种是脚本式,支持Grovvy大部分功能,也是表达灵活的工具,node {},这两种使用哪个都可以,看一下官方的栗子。

示例地址,声明式

脚本式

我的头开始大了,后面主要是使用脚本式,这东西的定义就是一个文本文件,也称为Jenkinsfile,下面创建一个流水线任务来玩玩。

创建流水线任务

自行创建吧,创建完任务之后拉到最下面,选这个,会自动补全一个例子,改改这个例子。

改成这样。

这样就可以了,保存,然后构建一下,构建成功后你会看到这个。

可以看到刚刚定义的三个步骤以图表的形式展现了出来,这是一个最简单的示例,脚本里面都有多个stage,这个stage是脚本最基本的组成部分,它用来告诉jenkins要去干什么,之后就需要在这个脚本中实现从代码拉取到部署到k8s的全部过程,官方的原理图。

说白了还是完成了从构建到发布的流程,所以接下来就开始搞在脚本中完成这些操作。

拉取代码

第一步就是拉取代码,到底怎么拉,这些步骤的语法都可以通过Pipeline脚本语法去帮我们生成,这个是重点,也就是这个位置。

新到一个页面,譬如我现在要拉取git仓库rj-bai.git的代码,我就可以这样做了,

使用master分支,但是提示无法连接到这个仓库,这里也是需要免交互拉取代码的,之前的操作就是将master公钥拷贝到了git服务器,所以现在要用到私钥了,需要添加一个凭据,将用户名私钥添加进去添加即可。

添加后回到主页面之后报错没了,说明可以免交互拉取代码了,点击Generate Pipeline Script之后就会生拉取代码的脚本了。

复制这一串子贴到这个位置就可以拉取代码了,我顺便执行了一条shell命令

保存构建,看了一下log输出,之前添加的Index.html已经拉取下来了,说明拉取代码这块莫得问题。

能拉取代码了,下一步就要开始编译了,下面看一下如何编译代码。

编译代码

上面执行的这些任务都是在jenkins pod中去做的,现在还没有slave节点,一般编译java需要mavenmaven依赖jdk,不用看了,目前jenkinsPOD上虽有java但没有maven,在上文的描述中拉取编译代码也是由slave去完成的,所以现在要启动slave了,既然是动态创建,就需要用到pod模板了。

启动测试slave节点

直接在pipeline脚本中定义吧,改完的pipeline脚本内容如下。

podTemplate(label: 'jenkins-slave', cloud: 'kubernetes', containers: [
    containerTemplate(
        name: 'jnlp', 
        image: "registry.cn-beijing.aliyuncs.com/rj-bai/jenkins-slave-jdk:1.8"
    ),
  ],
  volumes: [
    hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock'),
    hostPathVolume(mountPath: '/usr/bin/docker', hostPath: '/usr/bin/docker')
  ],
) 
{

node ("jenkins-slave"){
   def mvnHome
   stage('拉取') { 
       git credentialsId: 'efb4268b-f758-4b58-9dcd-d966faf8360e', url: 'rj-bai@192.168.1.248:/home/rj-bai/rj-bai.git'
       sh 'ls -l'
   }
   stage('建构') {
   }
   stage('部署') {
   }
}
}

上面的那一段就是定义slave节点的声明,使用jnlp协议,镜像我传到了阿里云的仓库,这个是公开的,一会在构建的时候他回去拉这个镜像作为slave启动,我直接也把docker挂进去了,方便构建推送镜像,这样配置之后构建镜像就不是由master来完成的了,而是由动态创建的slave来完成的,测试一下,保存,然后构建,动态查看pod状态。

这里构建成功了,看一下k8s集群中pod的变化。

这就是动态创建slave了,在需要的时候创建一个,slave完成任务后就会被销毁,这一块没啥子问题,下面准备一下要编译的源代码。

准备java源代码

既然是编译,你就需要有java的源码了,建议在这里生成一个,目前只要有简单的web访问就够了,所以选这个,编译成功后会有一个jar文件,jdk版本1.8,这样就可以了。

选完点击绿色按钮你会下载一个名为demo.zip的压缩包,这就是源码了,然后在git上创建一个仓库,把代码提上去。

[rj-bai@kubeadm-node ~]$ mkdir webstarter.git
[rj-bai@kubeadm-node ~]$ cd webstarter.git/
[rj-bai@kubeadm-node ~/webstarter.git]$ git --bare init 
Initialized empty Git repository in /home/rj-bai/webstarter.git/

仓库这里算是创建完了,然后去master提交代码。

[root@master-1 ~]# git clone rj-bai@192.168.1.248:/home/rj-bai/webstarter.git
[root@master-1 ~]# cd webstarter/
[root@master-1 ~/webstarter]# unzip demo.zip ## 文件自行上传
[root@master-1 ~/webstarter]# mv demo/* .
[root@master-1 ~/webstarter]# rm -rf demo*
[root@master-1 ~/webstarter]# git add .
[root@master-1 ~/webstarter]# git commit -m 'webstarter'
[root@master-1 ~/webstarter]# git push origin master

这样就可以了撒,其实pipeline的配置也可以写到一个名为Jenkinsfile的文件里,这个文件需要放在代码的根目录,这个后面会涉及到,下面开始配置编译代码的部分。

编译代码构建和推送镜像

编译代码这里需要maven去编译,构建镜像的话就是将项目包传到镜像里,一会会用openjdk的镜像,推送镜像就是将刚刚构建好的镜像推送到镜像仓库,既然涉及到了推送镜像就一定会给镜像打标签,具体这个标签怎么打,还是像之前那样,项目名+构建次数,所以脚本暂时写成这样,定义了N多变量。

// 镜像仓库地址
def registry = "registry.cn-beijing.aliyuncs.com"

// 项目&镜像配置信息
def namespace = "rj-bai"
def app_name = "webstarter"
def image_name = "${registry}/${namespace}/${app_name}:${BUILD_NUMBER}"
def git_address = "rj-bai@192.168.1.248:/home/rj-bai/webstarter.git"

// 认证信息ID
def docker_registry_auth = "400878cf-e740-459f-b8bf-ad2c749b833e"
def git_auth = "efb4268b-f758-4b58-9dcd-d966faf8360e"

podTemplate(label: 'jenkins-slave', cloud: 'kubernetes', containers: [
    containerTemplate(
        name: 'jnlp', 
        image: "${registry}/${namespace}/jenkins-slave-jdk:1.8"
    ),
  ],
  volumes: [
    hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock'),
    hostPathVolume(mountPath: '/usr/bin/docker', hostPath: '/usr/bin/docker')
  ],
) 
{
  node("jenkins-slave"){
      stage('拉取代码'){
         checkout([$class: 'GitSCM', branches: [[name: '${Branch}']], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_address}"]]])
      }
      stage('代码编译'){
          sh "mvn clean package -Dmaven.test.skip=true"
      }
      stage('构建镜像'){
          withCredentials([usernamePassword(credentialsId: "${docker_registry_auth}", passwordVariable: 'password', usernameVariable: 'username')]) {
            sh label: '', script: '''              echo \'
                FROM openjdk:8 
                ADD target/*.jar / 
                ADD entrypoint.sh /
                RUN chmod +x /entrypoint.sh
                CMD ["/bin/bash","/entrypoint.sh"]
              \' > Dockerfile
              echo \'
                #!/bin/bash
                app=`ls /*.jar`
                java -jar $app
                \' > entrypoint.sh'''
            sh """
              docker build -t ${image_name} .
              docker login -u ${username} -p \"${password}\" ${registry}
              docker push ${image_name}
              """
            }
      }
  }
}

你就当成shell脚本去看就完了,应该都能看的差不多,def开头的就是定义的变量,Branch是分支变量,现在还没定义,需要通过参数化构建过程去定义,其他的变量如果在这个脚本内找不到那他就是jenkins的内置变量,譬如BUILD_NUMBER,很久之前在别的文章里提过这个,先来解释一下我镜像仓库那里为什么这样写,看一下我镜像仓库完整的地址你就懂了。

如果你是自建的镜像仓库可以不加namespace的配置,我这里就必须得加了,还有一些奇怪的ID,下面分别来解释一下都是用来干嘛的。

docker_registry_auth是用来拉取推送镜像的凭据,但是那里写的是凭据的ID,这个凭证是slave-pod所使用的,因为他需要向私有仓库推送镜像,需要登陆后才能去推送镜像,添加方法如下。

主页面→凭据→系统→全局凭据→添加,类型就是用户名密码,然后填进去点ok就可以了,ID会自动生成一个。

添加之后会自动返回,然后点更新的按钮就可以查看到ID了,要和脚本中的对应上。

再下面的那个git认证之前就创建过了,自己查看ID改一下吧,再看一下拉取代码那里有很奇怪的一段,也就是这个,

         checkout([$class: 'GitSCM', branches: [[name: '${Branch}']], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_address}"]]])

这个也是用来拉取代码的,之前提过一个使用git方式去拉取的,刚刚用的那种方式是这个,看下图,通过这个也可以拉取代码,而且推荐使用这个,地址ID分支信息上文都用的是变量,其他的都一样,不要在意地址和上面的不一样。。。

在看构建镜像那里也有奇怪的一段,也就是这个,

          withCredentials([usernamePassword(credentialsId: "${docker_registry_auth}", passwordVariable: 'password', usernameVariable: 'username')]) {

这一段是用来保存登陆镜像仓库的用户名和密码,以变量的形式,这样做的主要原因就是不会让用户名密码的明文暴露在pipeline脚本里,也是动态生成的,如果所示。

和我脚本中的一致,只不过我脚本中定义的是ID的变量,说白了这样做就是将用户名存到了名为username的变量中,密码保存到了password变量中,直接引用就完了,不会暴露你用户名密码明文,就是这个原理,再下面写了一个Dockerfile和一个启动脚本,现在还有一个变量没有去定义,也就是Branch,用来定义要构建的分支,git的分支会有很多,不止是一个master,所以说不是固定的,所以现在定义一下这个变量。

编辑这个job,找到参数化构建过程,配置成这样,

这就是一个变量,默认值是master,保存回到主界面,点Build with Parameters,会看到这种效果。

中途失败了好多次,各种调试,之前也成功过,但是第25次是才是真正意义上的成功,

现在镜像已经推送到我阿里云的仓库了,直接在服务器上拉一下吧,试着运行一下。

[root@kubeadm-node ~]# docker run -d -p 666:8080 registry.cn-beijing.aliyuncs.com/rj-bai/webstarter:25

莫得问题,可以正常访问,说明之前的步骤都没问题了,现在通过pipeline完成了CI阶段,拉取代码编译构建镜像推送镜像,感觉是比之前的方便很多了,下一步就是需要将刚刚的东西部署到K8s中了,也就是CD阶段。

jenkins在k8s中持续部署

上面镜像已经准备好了,现在该实现自动部署到k8s中了,要想实现这个还需要一个插件,名为kubernetes continuous deploy,用于将资源部署到k8s中,他支持绝大部分的资源类型,像是什么deployment&service,自行安装吧,简单看一下这个插件的介绍。

官方地址,主要看一下这一段,这是配置在pipeline中使用的写法

kubeconfigId这里是需要指定一个kubeconfigID,这个东西就是用于连接k8s的一个配置文件,所以我们要把这个文件内容保存到jenkins中作为凭据,然后去引用凭据的ID

config这里用来指定资源文件,也就是你部署服务的YAML文件。

                 secretNamespace: '<secret-namespace>',
                 secretName: '<secret-name>',

这块是用来指定secret的,有两个是必须的,一个是kubeconfig文件,再就是资源文件,这个插件的pipeline写法也是可以生成的,所以还是用之前的工具来生成一下。

现在开始让你添加kubeconfig的文件了,添加吧,选择这个,

这个文件具体要怎么获取,如果你的集群是kubeadm安装的获取很简单,就是这个文件。

[root@rj-bai ~]# cat .kube/config

把这个文件的内容复制出来保存到jenkins里面就可以了,如果你是二进制部署的集群,就要手动去生成这个文件了,这个文件默认是没有的,具体怎么生成这个文件之前写过,这里就不贴了,我使用的集群也是二进制方式部署的,我直接把之前生成的文件拿过来就直接用了,然后那个config Files就是指定资源文件了,这个文件就是用来部署我们的项目的,emmmm,写一个吧,我写的如下。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3 
  selector:
    matchLabels:
      app: web-starter
  template:
    metadata:
      labels:
        app: web-starter 
    spec:
      imagePullSecrets:
      - name: $SECRET_NAME 
      containers:
      - name: web-starter 
        image: $IMAGE_NAME 
        ports:
        - containerPort: 8080
          name: web
        livenessProbe:
          httpGet:
            path: /favicon.ico
            port: 8080
          initialDelaySeconds: 30
          timeoutSeconds: 5
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /favicon.ico
            port: 8080
          initialDelaySeconds: 30
          timeoutSeconds: 5
          failureThreshold: 3
        resources:
          requests:
            memory: "64Mi"
            cpu: "250m"
          limits:
            memory: "128Mi"
            cpu: "500m"

---
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  type: NodePort
  selector:
    app: web-starter 
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
      name: web

---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: web 
spec:
  rules:
  - host: webstarter.rj-bai.com
    http:
      paths:
      - path: /
        backend:
          serviceName: web 
          servicePort: 80

应该都能看懂,我部署了nginx-ingress-controller,所以就直接以ingress方式发布出去了,如果你没有部署ingress控制器就直接用NodePort吧,看一哈这个文件中有两个变量,一个是用来存放认证登陆信息的secrets名字,再一个就是镜像地址,一会使用sed去替换成相对应的值,暂时就定义这两个变量,其实还有很多可以定义,只要是经常变动的,像是什么健康检查端口绑定域名也可用变量,用变量的目的就是复用,自行琢磨吧,这个文件需要存在你git版本代码仓库中,自行上传提交一下吧,我的名为deploy-webstarter.yaml

所以,最终的配置文件pipeline脚本如下。

// 镜像仓库地址
def registry = "registry.cn-beijing.aliyuncs.com"

// 项目&镜像配置信息
def namespace = "rj-bai"
def app_name = "webstarter"
def image_name = "${registry}/${namespace}/${app_name}:${BUILD_NUMBER}"
def git_address = "rj-bai@192.168.1.248:/home/rj-bai/webstarter.git"

// 认证信息ID
def docker_registry_auth = "400878cf-e740-459f-b8bf-ad2c749b833e"
def git_auth = "efb4268b-f758-4b58-9dcd-d966faf8360e"
def secret_name = "registry-secret"
def k8s_auth = "9823945c-07f6-495c-bc48-a47d8f43536c"

podTemplate(label: 'jenkins-slave', cloud: 'kubernetes', containers: [
    containerTemplate(
        name: 'jnlp', 
        image: "${registry}/${namespace}/jenkins-slave-jdk:1.8"
    ),
  ],
  volumes: [
    hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock'),
    hostPathVolume(mountPath: '/usr/bin/docker', hostPath: '/usr/bin/docker')
  ],
) 
{
  node("jenkins-slave"){
      stage('拉取代码'){
         checkout([$class: 'GitSCM', branches: [[name: '${Branch}']], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_address}"]]])
      }
      stage('代码编译'){
          sh "mvn clean package -Dmaven.test.skip=true"
      }
      stage('构建镜像'){
          withCredentials([usernamePassword(credentialsId: "${docker_registry_auth}", passwordVariable: 'password', usernameVariable: 'username')]) {
            sh label: '', script: '''              echo \'
                FROM openjdk:8 
                ADD target/*.jar / 
                ADD entrypoint.sh /
                RUN chmod +x /entrypoint.sh
                CMD ["/bin/bash","/entrypoint.sh"]
              \' > Dockerfile
              echo \'
                #!/bin/bash
                app=`ls /*.jar`
                java -jar $app
                \' > entrypoint.sh'''
            sh """
              docker build -t ${image_name} .
              docker login -u ${username} -p \"${password}\" ${registry}
              docker push ${image_name}
              """
            }
      }
      stage('部署到K8S'){
          sh """
          sed -i 's#\$IMAGE_NAME#${image_name}#' deploy-webstarter.yaml
          sed -i 's#\$SECRET_NAME#${secret_name}#' deploy-webstarter.yaml
          """
          kubernetesDeploy configs: 'deploy-webstarter.yaml', kubeconfigId: "${k8s_auth}"
      }
  }
}

增加了两个变量,一个是存kubeconfigID,一个是secret的名字,这个是干嘛的不用多说了,如果没有自行创建吧,使用sed替换了镜像地址和secret的名字,最后kubernetesDeploy配置的那里,现在就用到的就这两个,没用到的全部去掉了,开始跑吧。

最终测试

开始构建,然后动态查看pod的状态,先看页面,显示成功。

再看pod的状态,都已经正常启动了,

然后手写hosts访问一下,

莫得问题,就是这种效果,现在也完成了持续部署,生成的demo就是一个简单的web,没有任何的页面,只有默认的404,健康检查那里我检查的还是favicon.ico,反正是起来了,访问也没问题,过。

使用Jenkinsfile

其实这个pipeline脚本的内容也可以放在项目的根目录,也就是和deploy-webstarter.yaml同级,不需要写在jenkins里,这样的话比较方便管理,再加项目的时候你只需要在jenkins上创建job直接引用仓库地址就可以了,现在就要达到这个目的,部署一个东西撒,一个名为DimpleBlogjava博客,感兴趣的去github上搜一下吧,编译好了也是一个jar包,拿到源码之后还是新建仓库,master拉一下把代码将源码传到仓库,先不要提交到git中,需要改点东西。

刚刚提到了这是一个博客程序,要他运行起来需要mysql&&redis,他的sql文件是存在源码的sql文件夹里,我集群中刚好有一个mysql,也是之前创建的,创建方式我就不贴了,之前贴过,我看sql里用的是test表,我手动导入进去了,现在还莫得redis,创建一个redis出来吧,使用StatefulSet创建,和创建mysql的方式一致

[root@master-1 ~/demo]# cat redis.yaml 
apiVersion: v1
kind: Service
metadata:
  name: redis
spec:
  ports:
  - port: 6379
    name: redis 
  clusterIP: None
  selector:
    app: redis

---

apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: redis
spec:
  serviceName: "redis"
  template:
    metadata:
      labels:
        app: redis 
    spec:
      containers:
      - name: redis
        image: redis:latest
        ports: 
        - containerPort: 6379
          name: redis
        volumeMounts:
        - mountPath: "/data"
          name: redis-data
        livenessProbe:
          tcpSocket:
            port: 6379
          initialDelaySeconds: 10
          periodSeconds: 3
          failureThreshold: 3
      volumes: 
      - name: redis-data
        persistentVolumeClaim: 
          claimName: redis-data

--- 

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: redis-data
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: managed-nfs-storage
  resources:
    requests:
      storage: 1Gi
[root@master-1 ~/demo]# kubectl apply -f redis.yaml 
service/redis created
statefulset.apps/redis created
persistentvolumeclaim/redis-data created
[root@master-1 ~/demo]# kubectl get pod redis-0 
NAME      READY   STATUS    RESTARTS   AGE
redis-0   1/1     Running   0          3m10s

已经启动了,然后去改一下这个项目连接数据库的配置文件,这个位置,

[root@master-1 ~/DimpleBlog]# vim src/main/resources/application-druid.yml
      # 主库数据源
      master:
        url: jdbc:mysql://mysql:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: Sowhat?

这是数据库的,还要改一下redis的,

[root@master-1 ~/DimpleBlog]# vim src/main/resources/application.yml
  redis:
    host: redis
    port: 6379
    database: 0

这样就可以了撒,还需要创建资源文件和jenkinsfile,先把资源文件写了吧,按着之前的改改就行了,我改成这样。

[root@master-1 ~/DimpleBlog]# cat deploy-dimpleblog.yaml 
apiVersion: apps/v1 
kind: Deployment
metadata:
  name: web-dimpleblog
spec:
  replicas: 1 
  selector:
    matchLabels:
      app: dimpleblog
  template:
    metadata:
      labels:
        app: dimpleblog 
    spec:
      imagePullSecrets:
      - name: $SECRET_NAME 
      containers:
      - name: dimpleblog 
        image: $IMAGE_NAME 
        ports:
        - containerPort: 80
          name: web-dimpleblog
        livenessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 60
          timeoutSeconds: 10
          failureThreshold: 5
        readinessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 60
          timeoutSeconds: 10
          failureThreshold: 5
        resources:
          requests:
            memory: "512Mi"
            cpu: "0.5"
          limits:
            memory: "1024Mi"
            cpu: "1"

---
apiVersion: v1
kind: Service
metadata:
  name: web-dimpleblog
spec:
  type: NodePort
  selector:
    app: dimpleblog 
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
      name: web-dimpleblog

---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: web-dimpleblog 
spec:
  rules:
  - host: dimpleblog.rj-bai.com
    http:
      paths:
      - path: /
        backend:
          serviceName: web-dimpleblog 
          servicePort: 80

这次健康检查时间调长了,资源文件有了,然后是jenkinsfile,放在和资源文件目录同级的地方,

[root@master-1 ~/DimpleBlog]# cat Jenkinsfile 
// 镜像仓库地址
def registry = "registry.cn-beijing.aliyuncs.com"

// 项目&镜像配置信息
def namespace = "rj-bai"
def app_name = "dimpleblog"
def image_name = "${registry}/${namespace}/${app_name}:${BUILD_NUMBER}"
def git_address = "rj-bai@192.168.1.248:/home/rj-bai/DimpleBlog.git"

// 认证信息ID
def docker_registry_auth = "400878cf-e740-459f-b8bf-ad2c749b833e"
def git_auth = "efb4268b-f758-4b58-9dcd-d966faf8360e"
def secret_name = "registry-secret"
def k8s_auth = "9823945c-07f6-495c-bc48-a47d8f43536c"

podTemplate(label: 'jenkins-slave', cloud: 'kubernetes', containers: [
    containerTemplate(
        name: 'jnlp', 
        image: "${registry}/${namespace}/jenkins-slave-jdk:1.8"
    ),
  ],
  volumes: [
    hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock'),
    hostPathVolume(mountPath: '/usr/bin/docker', hostPath: '/usr/bin/docker')
  ],
) 
{
  node("jenkins-slave"){
      stage('拉取代码'){
         checkout([$class: 'GitSCM', branches: [[name: '${Branch}']], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_address}"]]])
      }
      stage('代码编译'){
          sh "mvn clean package -Dmaven.test.skip=true"
      }
      stage('构建镜像'){
          withCredentials([usernamePassword(credentialsId: "${docker_registry_auth}", passwordVariable: 'password', usernameVariable: 'username')]) {
            sh label: '', script: '''              echo \'
                FROM openjdk:8 
                ADD target/*.jar / 
                ADD entrypoint.sh /
                RUN chmod +x /entrypoint.sh
                CMD ["/bin/bash","/entrypoint.sh"]
              \' > Dockerfile
              echo \'
                #!/bin/bash
                app=`ls /*.jar`
                java -jar $app
                \' > entrypoint.sh'''
            sh """
              docker build -t ${image_name} .
              docker login -u ${username} -p \"${password}\" ${registry}
              docker push ${image_name}
              """
            }
      }
      stage('部署到K8S'){
          sh """
          sed -i 's#\$IMAGE_NAME#${image_name}#' deploy-dimpleblog.yaml
          sed -i 's#\$SECRET_NAME#${secret_name}#' deploy-dimpleblog.yaml
          """
          kubernetesDeploy configs: 'deploy-dimpleblog.yaml', kubeconfigId: "${k8s_auth}"
      }
  }
}

也是按之前的改了改,这样就够了,然后提交代码,

[root@master-1 ~/DimpleBlog]# git add -A
[root@master-1 ~/DimpleBlog]# git commit -m 'master'
[root@master-1 ~/DimpleBlog]# git push origin master

这里准备好了,接下来就去jenkins操作吧,新建流水线任务,还是要添加字符参数,上文写过了,不贴了,主要是看配置流水线那里,选择在代码仓库在去拉取Jenkinsfile,配置如下,

Branches to build里指定的是要在哪个分支去拉取Jenkinsfile,我们这里指定master就可以了,这样配置后脚本就不用写在jenkins里了,保存开始构建吧,这次是一次就成功了,

hosts访问一下,

啊,就是这样,能访问到,说明没啥子问题,说真的这个博客做的还是蛮不错的,而且看上去开源的原因也是作者被逼无奈,哈哈

说实话部署这个博客的流程和我部署我们公司项目的流程是一样的,先部署项目需要的基础服务,基础服务部署完成后就可以部署项目了,部署完项目之后将项目发布出来,当然之前是手写nginx配置文件,现在用的是nginx-ingress,这个东西真的好方便,自动关联services,缺点之前也提过,只能针对域名,不能针对端口。

刚刚扯到了基础服务,目前公司部署项目依赖的基础服务可不止mysql&redis,之前写swarm实战的时候提到过我们这里会用到的基础服务,之后如果没什么意外的话就开始做如何让这些基础服务跑在k8s上了。

其实还有一个最蛋疼的问题没有解决,就是每添加一个项目你就要创建一个任务、写一个资源文件、写一个Jenkinsfile,像我之前用的不是流水线,用的这个,

每次加一个项目我这里就要创建一个任务,然后写两个playbook,一个更新的一个回滚的,看一哈我之前写的,有29个项目写了29个发布的playbook

再之后jenkins融合swarm就好很多了,写了一个可以复用的脚本去创建更新服务,定义了N多变量,也是通过jenkins传进去的,这个就轻松很多了,我不知道我之前的方式能不能把N个项目串到一起,但是通过pipeline可以,下面给你个思路。

我这里说的项目不是指的存在于jenkins中的job,说白了就是一个pipeline脚本可以将一套系统的所有项目都串起来,譬如之前29个项目组成一套系统,那时候创建了29job,现在只需一个pipeline就可以搞定了,但是你需要写一个特别特别特别牛逼的pipeline脚本,配合jenkins的参数化构建去使用,通过pipeline去判断变量去做对应的操作,全部更新发布或是更新某些,这是一个很大的工程,涉及到的东西实在太多了,上面只是对pipeline脚本有一个最初步的认识,更高级的使用方法自行琢磨吧,记住pipeline写法能通过jenkins生成,思路放这里了,自行琢磨吧,下面聊聊k8S滚动更新

K8S滚动更新

k8s更新项目的时候默认使用的策略就是滚动更新(rollingUpdate),他的逻辑就是每一次更新一个或多个服务,更新完成之后会加入到endpoints来接收请求,不断执行这个过程,直到集群中的所有旧版本替换成新版本,譬如我上面的那个webstarter有三个副本,在执行滚动更新的时候会先更新一个或两个,这一波没问题的话就开始更新下一波,直到全部更新到新版本,特点就是无感知平滑过渡,业务不受影响,默认策略,优先使用,下面看看它的原理是啥子。

滚动更新原理

其实滚动更新就是利用了再增加一个ReplicaSet去发布新版本去实现新旧的替换,这个ReplicaSet之前也提到过,在你创建Deployment的时候就会生成一个ReplicaSet,它是用来管理你pod副本数量的,也是一个控制器,说白了就是Deployment通过ReplicaSet去管理你的pod,也做一个版本的记录和滚动更新,这是Deployment引入ReplicaSet的目的。

在触发滚动更新的时候,Deployment会再创建一个ReplicaSet去部署你的新版本,也就是这时候一个Deployment会有两个ReplicaSet,这个新创建的ReplicaSet也会去关联你的service,如果更新新版本莫得问题再去删除你的旧版本,全部更新完之后将新的ReplicaSet应用于当前的Deployment,保留旧的ReplicaSet用于回滚,这样就实现了滚动更新,言语是苍白的,实际操作看一哈。

我直接用jenkins去更新一下最开始的那个java-demo了,因为他的副本数是三个,看着会比较明显,等待更新完成,因为有健康检查了,更新会比较慢撒,更新时候有一个居然被OOMKilled了,看到这个错就是内存占用超了我的资源限制,所以就被杀了,所以在做资源限制的时候也要慎重,但这东西还不能不做,不做的话可能会给你的宿主机带来麻烦,现在已经更新完了。

先看一下Deployment的详情,会有事件显示,主要就是这里,

可以看到在触发滚动更新的时候新创建了一个名为web-5c466f6896replicasets,设置它的副本数为1,这就是用来跑新版本的,这个正常启动之后下一步操作是把名为web-c98f55d49replicasets副本调整成了2,这是旧的replicasets,所以这时候就是有两个旧版本一个新版本在跑,注意这不是同时进行的,是新的启动成功之后才会去停旧的,如果新的启动失败了,滚动更新就会暂停,不会影响旧的replicasets,最终结果就是新的web-5c466f6896副本数设置为3,旧的web-c98f55d49设置成0,下面看一下ReplicaSet就明白了。

可以看到旧的replicasets副本数已经被设置成0了,创建时间是26h以前,新的创建于12m之前,副本数被设置成3,这样就理解了撒,过程就是这样,回滚的话就是反向操作,先设置上一个版本的replicasets的副本数为1,一个旧的正常启动后缩减一个当前的replicasets,我回滚了一下,看图吧。

[root@master-1 ~]# kubectl rollout undo deployment web
deployment.extensions/web rolled back

更新回滚的原理就是这样的,其实你可以利用他这个原理去实现灰度发布,灰度发布是啥子呢?说白了就是先升级部分的服务,譬如你有10pod,我先升级两个,这时候就集群中就又有2个新的8个旧的,会有一小部分用户去访问,如果新版本用户没反馈就开始扩大范围,我再升级3个,现在就一半一半了,如果用户还没啥子反馈就全部升级到新版本,这种灰度发布当前K8s是不支持的,变通一下就可以实现,思路如下。

需要使用两套deployment,上面提到了,deployment会通过replicasets实现新旧版本的更新,灰度发布实现的原理和他是样的,譬如现在我要灰度发布,现在已经有一个deployment了,我再创建一个部署新版应用的deployment,当然这个deployment的名字不要和之前一样撒,相同的地方是这两个的deployment所关联的service是同一个,这样当有新的pod启动后就会和旧的pod并行提供服务,这个是重点,结合上面提到的东西,我先把新的deployment所关联的replicasets进行扩展,譬如目前老版本有10pod,我先启动两个新的,这两个新的启动完成之后我再去缩容旧的pod的数量,譬如我缩容到八个,这时候就有两个新的八个旧的并行提供服务了,反馈没问题之后再进行同样的操作,直到将新的扩容到10,旧的缩容成0,懂我啥意思了吧,这样就实现了灰度发布。

这样做的好处是影响范围可控,灰度发布是目前比较主流的方案,想要实现这个方案有一个很大的难点,就是去控制每一波升级缩容的数量,要扩容多少新的,缩减多少旧的,就白了就是你要有效控制新的扩容旧的缩容,最简单的办法就是手动去scale,哈哈,最好的实现方法还是在程序层面去控制,思路就是这样,下面来看看滚动更新的策略。

滚动更新策略

这个策略就是用来定义滚动更新每次要更新多少个pod,我们之前没有在YAML文件里定义过,所以用的就是默认的策略,现在看一下。

[root@master-1 ~]# kubectl get deployments.apps web -o yaml
strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
    type: RollingUpdate

maxSurge用来设置要新启动多少个pod来作为新副本,目前的配置是当前pod总数量的百分之二十五,maxUnavailable是设置最大不可用的副本,设置的也是当前pod总数量的百分之二十五,也就是说譬如有12 pod,在更新的时候我先启动3个新的,启动三个没问题之后关掉三个旧的,再唠叨一遍,健康检查生产环境必加,不加健康检查再牛逼的更新策略都无法帮你平滑更新,本篇就这样吧,过。

最后编辑于: 2019 年 07 月 08 日
返回文章列表 文章二维码 打赏
本页链接的二维码
打赏二维码