CI/CD 概述

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

  • 持续集成
  • (Continuous Integration,CI) :代码合并构建部署测试都在一起,不断地执行这个过程,并对结果反馈。
  • 持续部署
  • (Continuous Deployment,CD):部署到测试环境、生产环境
  • 持续交付
  • (Continuous Delivery,CD):将最终产品发布到生产环境。

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

 

使用 jenkins 构建 CI/CD 平台_ci cd

 

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

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

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

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

 

部署 Harbor 镜像仓库

这个是用来存镜像的,我直接在 ​​kubeadm​​​ 的 ​​node​​​ 节点上部署了,一会把 ​​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

 

使用 jenkins 构建 CI/CD 平台_nginx_02

 

访问一下,

 

使用 jenkins 构建 CI/CD 平台_docker_03

 

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

 

使用 jenkins 构建 CI/CD 平台_ci cd_04

 

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

 

部署 Git 仓库

​git​​​ 是目前比较主流的,先把这个包装一下吧,如果你用 ​​SVN​​ 自行安装配置一下吧,不是很麻烦,

[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_home​​​,​​jenkins​​​ 所有的数据都是存在这个目录下面的,​​jenkins​​​ 还需要一个唯一的网络标识,也就是说需要有状态的去部署 ​​jenkins​​​ 了,这里就不多 ​​BB​​​ 了,直接用 ​​StatefulSet​​​ 方式去部署,依旧使用 ​​NFS​​​ 动态供给作为存储,​​NFS​​​ 的部署方法之前写过了,部署 ​​jenkins​​​ 的 ​​YAML​​​ 文件是使用官方的模板,现在直接 ​​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​​。

 

使用 jenkins 构建 CI/CD 平台_ci cd_05

 

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

 

使用 jenkins 构建 CI/CD 平台_ci cd_06

 

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

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

 

使用 jenkins 构建 CI/CD 平台_git_07

 

如果你不想用 ​​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 个没启用,我翻译了一下,结果如下。

 

使用 jenkins 构建 CI/CD 平台_docker_08

 

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

[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

 

使用 jenkins 构建 CI/CD 平台_nginx_09

 

这样就可以了撒,部署完了,所有 ​​Node​​​ 节点都有运行一个 ​​nginx-ingress​​​ 控制器,除了两个 ​​master​​​ 节点,因为我没允许 ​​master​​​ 运行 ​​Pod​​​,​​Ingress​​​ 部署完了,现在可以部署 ​​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

 

使用 jenkins 构建 CI/CD 平台_nginx_10

 

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

 

使用 jenkins 构建 CI/CD 平台_git_11

 

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

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

 

使用 jenkins 构建 CI/CD 平台_docker_12

 

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

 

使用 jenkins 构建 CI/CD 平台_nginx_13

 

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

 

使用 jenkins 构建 CI/CD 平台_ci cd_14

 

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

 

jenkins 在 k8s 动态创建代理

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

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

 

安装配置插件

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

 

使用 jenkins 构建 CI/CD 平台_docker_15

 

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

 

使用 jenkins 构建 CI/CD 平台_nginx_16

 

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

 

使用 jenkins 构建 CI/CD 平台_nginx_17

 

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

 

使用 jenkins 构建 CI/CD 平台_ci cd_18

 

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

 

构建 jenkins-slave 镜像

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

 

使用 jenkins 构建 CI/CD 平台_docker_19

 

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

 

编写 Dockerfile

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

 

FROM centos:latest
RUN yum -y install java-1.8.0-openjdk maven curl git subversion 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.xml​​​ 是 ​​maven​​​ 的配置文件,这个文件怎么写建议咨询一下开发人员,我还是用我们私服的,用默认的也可以,可能下载依赖包会比较慢,默认地址是国外的,​​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 .

 

使用 jenkins 构建 CI/CD 平台_git_20

 

这样就构建完了,然后把这个镜像推送到镜像仓库,我不用刚刚搭建的 ​​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&svn​​​ 仓库代码和编译 ​​java​​​ 代码的镜像,如果你是别的自行琢磨吧,这种问题多去问开发,下面来了解一下 ​​Pipeline​

 

Pipeline 构建流水线发布

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

示例地址,

声明式

 

使用 jenkins 构建 CI/CD 平台_docker_21

 

脚本式

 

使用 jenkins 构建 CI/CD 平台_nginx_22

 

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

 

创建流水线任务

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

 

使用 jenkins 构建 CI/CD 平台_nginx_23

 

改成这样。

 

使用 jenkins 构建 CI/CD 平台_git_24

 

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

 

使用 jenkins 构建 CI/CD 平台_nginx_25

 

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

 

使用 jenkins 构建 CI/CD 平台_git_26

 

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

 

拉取代码

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

 

使用 jenkins 构建 CI/CD 平台_git_27

 

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

 

使用 jenkins 构建 CI/CD 平台_git_28

 

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

 

使用 jenkins 构建 CI/CD 平台_nginx_29

 

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

 

使用 jenkins 构建 CI/CD 平台_docker_30

 

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

 

使用 jenkins 构建 CI/CD 平台_git_31

 

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

 

使用 jenkins 构建 CI/CD 平台_docker_32

 

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

 

编译代码

上面执行的这些任务都是在 ​​jenkins pod​​​ 中去做的,现在还没有 ​​slave​​​ 节点,一般编译 ​​java​​​ 需要 ​​maven​​​,​​maven​​​ 依赖 ​​jdk​​​,不用看了,目前 ​​jenkins​​​ 的 ​​POD​​​ 上虽有 ​​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​​ 状态。

 

使用 jenkins 构建 CI/CD 平台_nginx_33

 

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

 

使用 jenkins 构建 CI/CD 平台_git_34

 

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

 

准备 java 源代码

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

 

使用 jenkins 构建 CI/CD 平台_nginx_35

 

选完点击绿色按钮你会下载一个名为 ​​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

 

使用 jenkins 构建 CI/CD 平台_docker_36

 

这样就可以了撒,其实 ​​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​​,很久之前在别的文章里提过这个,先来解释一下我镜像仓库那里为什么这样写,看一下我镜像仓库完整的地址你就懂了。

 

使用 jenkins 构建 CI/CD 平台_ci cd_37

 

 

使用 jenkins 构建 CI/CD 平台_nginx_38

 

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

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

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

 

使用 jenkins 构建 CI/CD 平台_docker_39

 

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

 

使用 jenkins 构建 CI/CD 平台_docker_40

 

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

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

这个也是用来拉取代码的,之前提过一个使用 ​​git​​​ 方式去拉取的,刚刚用的那种方式是这个,看下图,通过这个也可以拉取代码,而且推荐使用这个,地址 ​​ID​​​ 分支信息上文都用的是变量,其他的都一样,​​SVN​​​ 代码也用这种方式去拉就行了,​​SVN​​ 拉代码我建议用这个,

checkout([$class: 'SubversionSCM', locations: [[cancelProcessOnExternalsFail: true, credentialsId: "${svn_auth}", depthOption: 'infinity', ignoreExternalsOption: true, local: '.', remote: "${svn_address}"]], quietOperation: true, workspaceUpdater: [$class: 'UpdateUpdater']])

 

使用 jenkins 构建 CI/CD 平台_docker_41

 

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

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

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

 

使用 jenkins 构建 CI/CD 平台_docker_42

 

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

编辑这个 ​​job​​​,找到参数化构建过程,配置成这样,使用 ​​SVN​​ 的不用改这里撒,

 

使用 jenkins 构建 CI/CD 平台_git_43

 

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

 

使用 jenkins 构建 CI/CD 平台_nginx_44

 

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

 

使用 jenkins 构建 CI/CD 平台_nginx_45

 

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

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

 

使用 jenkins 构建 CI/CD 平台_docker_46

 

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

 

jenkins 在 k8s 中持续部署

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

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

 

使用 jenkins 构建 CI/CD 平台_ci cd_47

 

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

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

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

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

 

使用 jenkins 构建 CI/CD 平台_docker_48

 

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

 

使用 jenkins 构建 CI/CD 平台_ci cd_49

 

这个文件具体要怎么获取,如果你的集群是 ​​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}"
}
}
}

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

 

最终测试

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

 

使用 jenkins 构建 CI/CD 平台_docker_50

 

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

 

使用 jenkins 构建 CI/CD 平台_nginx_51

 

然后手写 ​​hosts​​ 访问一下,

 

使用 jenkins 构建 CI/CD 平台_docker_52

 

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

 

使用 Jenkinsfile

其实这个 ​​pipeline​​​ 脚本的内容也可以放在项目的根目录,也就是和 ​​deploy-webstarter.yaml​​​ 同级,不需要写在 ​​jenkins​​​ 里,这样的话比较方便管理,再加项目的时候你只需要在 ​​jenkins​​​ 上创建 ​​job​​​ 直接引用仓库地址就可以了,现在就要达到这个目的,部署一个东西撒,一个名为 ​​DimpleBlog​​​ 的 ​​java​​​ 博客,感兴趣的去 ​​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:
updateStrategy:
type: RollingUpdate
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
volumeClaimTemplates:
- metadata:
name: redis-data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
[root@master-1 ~/demo]# kubectl apply -f redis.yaml
service/redis created
statefulset.apps/redis 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​​,配置如下,

 

使用 jenkins 构建 CI/CD 平台_nginx_53

 

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

 

使用 jenkins 构建 CI/CD 平台_git_54

 

 

使用 jenkins 构建 CI/CD 平台_ci cd_55

 

写 ​​hosts​​ 访问一下,

 

使用 jenkins 构建 CI/CD 平台_git_56

 

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

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

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

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

 

使用 jenkins 构建 CI/CD 平台_ci cd_57

 

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

 

使用 jenkins 构建 CI/CD 平台_ci cd_58

 

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

我这里说的项目不是指的存在于 ​​jenkins​​​ 中的 ​​job​​​,说白了就是一个 ​​pipeline​​​ 脚本可以将一套系统的所有项目都串起来,譬如之前 ​​29​​​ 个项目组成一套系统,那时候创建了 ​​29​​​ 个 ​​job​​​,现在只需一个 ​​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​​ 了,看到这个错就是内存占用超了我的资源限制,所以就被杀了,所以在做资源限制的时候也要慎重,但这东西还不能不做,不做的话可能会给你的宿主机带来麻烦,现在已经更新完了。

 

使用 jenkins 构建 CI/CD 平台_ci cd_59

 

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

 

使用 jenkins 构建 CI/CD 平台_nginx_60

 

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

 

使用 jenkins 构建 CI/CD 平台_nginx_61

 

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

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

 

使用 jenkins 构建 CI/CD 平台_ci cd_62

 

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

需要使用两套 ​​deployment​​​,上面提到了,​​deployment​​​ 会通过 ​​replicasets​​​ 实现新旧版本的更新,灰度发布实现的原理和他是样的,譬如现在我要灰度发布,现在已经有一个 ​​deployment​​​ 了,我再创建一个部署新版应用的 ​​deployment​​​,当然这个 ​​deployment​​​ 的名字不要和之前一样撒,相同的地方是这两个的 ​​deployment​​​ 所关联的 ​​service​​​ 是同一个,这样当有新的 ​​pod​​​ 启动后就会和旧的 ​​pod​​​ 并行提供服务,这个是重点,结合上面提到的东西,我先把新的 ​​deployment​​​ 所关联的 ​​replicasets​​​ 进行扩展,譬如目前老版本有 ​​10​​​ 个 ​​pod​​​,我先启动两个新的,这两个新的启动完成之后我再去缩容旧的 ​​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​​ 个新的,启动三个没问题之后关掉三个旧的,再唠叨一遍,健康检查生产环境必加,不加健康检查再牛逼的更新策略都无法帮你平滑更新。