上节课和大家介绍了 Gitlab CI
结合 Kubernetes
进行 CI/CD 的完整过程 。这节课结合前面所学的知识点给大家介绍一个完整的示例:使用 Jenkins + Gitlab + Harbor + Helm + Kubernetes 来实现一个完整的 CI/CD 流水线作业。
其实前面的课程中我们就 已经学习了 Jenkins Pipeline 与 Kubernetes 的完美结合 ,我们利用 Kubernetes 来动态运行 Jenkins 的 Slave 节点,可以和好的来解决传统的 Jenkins Slave 浪费大量资源的缺点。之前的示例中我们是将项目放置在 Github 仓库上的,将 Docker 镜像推送到了 Docker Hub,这节课我们来结合我们前面学习的知识点来综合运用下,使用 Jenkins、Gitlab、Harbor、Helm、Kubernetes 来实现一个完整的持续集成和持续部署的流水线作业。
下图是我们当前示例的流程图
本次示例项目是一个完整的基于 Spring Boot、Spring Security、JWT、React 和 Ant Design 构建的一个开源的投票应用,项目地址: https://github.com/callicoder/spring-security-react-ant-design-polls-app 。
我们将会在该项目的基础上添加部分代码,并实践 CI/CD 流程。
首先需要更改的是服务端配置,我们需要将数据库链接的配置更改成环境变量的形式,写死了的话就没办法进行定制了,修改服务端文件 src/main/resources/application.properties
,将下面的数据库配置部分修改成如下形式:
spring.datasource.url= jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:polling_app}?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false spring.datasource.username= ${DB_USER:root} spring.datasource.password= ${DB_PASSWORD:root}
当环境变量中有上面的数据配置的时候,就会优先使用环境变量中的值,没有的时候就会用默认的值进行数据库配置。
由于我们要将项目部署到 Kubernetes 集群中去,所以我们需要将服务端进行容器化,所以我们在项目根目录下面添加一个 Dockerfile
文件进行镜像构建:
FROM openjdk:8-jdk-alpine MAINTAINER cnych <icnych@gmail.com> ENV LANG en_US.UTF-8 ENV LANGUAGE en_US:en ENV LC_ALL en_US.UTF-8 ENV TZ=Asia/Shanghai RUN mkdir /app WORKDIR /app COPY target/polls-0.0.1-SNAPSHOT.jar /app/polls.jar EXPOSE 8080 ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar","/app/polls.jar"]
由于服务端代码是基于 Spring Boot
构建的,所以我们这里使用一个 openjdk
的基础镜像,将打包过后的 jar
包放入镜像之中,然后用过 java -jar
命令直接启动即可,这里就会存在一个问题了,我们是在 Jenkins 的 Pipeline 中去进行镜像构建的,这个时候项目中并没有打包好的 jar
包文件,那么我们应该如何获取打包好的 jar
包文件呢?这里我们可以使用两种方法:
第一种就是如果你用于镜像打包的 Docker 版本大于 17.06
版本的话,那么我墙裂推荐你使用 Docker 的多阶段构建功能来完成镜像的打包过程,我们只需要将上面的 Dockerfile
文件稍微更改下即可,将使用 maven
进行构建的工作放到同一个文件中:
FROM maven:3.6-alpine as BUILD COPY src /usr/app/src COPY pom.xml /usr/app RUN mvn -f /usr/app/pom.xml clean package -Dmaven.test.skip=true FROM openjdk:8-jdk-alpine MAINTAINER cnych <icnych@gmail.com> ENV LANG en_US.UTF-8 ENV LANGUAGE en_US:en ENV LC_ALL en_US.UTF-8 ENV TZ=Asia/Shanghai RUN mkdir /app WORKDIR /app COPY --from=BUILD /usr/app/target/polls-0.0.1-SNAPSHOT.jar /app/polls.jar EXPOSE 8080 ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar","/app/polls.jar"]
前面课程中我们就讲解过 Docker 的多阶段构建,这里我们定义了两个阶段,第一个阶段利用 maven:3.6-alpine
这个基础镜像将我们的项目进行打包,然后将该阶段打包生成的 jar
包文件复制到第二阶段进行最后的镜像打包,这样就可以很好的完成我们的 Docker 镜像的构建工作。
第二种方式就是我们传统的方式,在 Jenkins Pipeline 中添加一个 maven
构建的阶段,然后在第二个 Docker 构建的阶段就可以直接获取到前面的 jar
包了,也可以很方便的完成镜像的构建工作,为了更加清楚的说明 Jenkins Pipeline 的用法,我们这里采用这种方式,所以 Dockerfile 文件还是使用第一个就行。
现在我们可以将服务端的代码推送到 Gitlab 上去,我们这里的仓库地址为: http://git.qikqiak.com/course/polling-app-server.git
注意,这里我们只推送的服务端代码。
客户端我们需要修改 API 的链接地址,修改文件 src/constants/index.js
中 API_BASE_URL
的地址,我们同样通过环境变量来进行区分,如果有环境变量 APISERVER_URL
,则优先使用这个环境变量来作为 API 请求的地址:
let API_URL = 'http://localhost:8080/api'; if (process.env.APISERVER_URL) { API_URL = `${process.env.APISERVER_URL}/api`; } export const API_BASE_URL = API_URL;
因为我们这里的项目使用的就是前后端分离的架构,所以我们同样需要将前端代码进行单独的部署,同样我们要将项目部署到 Kubernetes 环境中,所以也需要做容器化,同样在项目根目录下面添加一个 Dockerfile
文件:
FROM nginx:1.15.10-alpine ADD build /usr/share/nginx/html ADD nginx.conf /etc/nginx/conf.d/default.conf
由于前端页面是单纯的静态页面,所以一般我们使用一个 nginx
镜像来运行,所以我们提供一个 nginx.conf
配置文件:
server { gzip on; listen 80; server_name localhost; root /usr/share/nginx/html; location / { try_files $uri /index.html; expires 1h; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } }
这里我们可以看到我们需要将前面页面打包到一个 build
目录,然后将改目录添加到 nginx 镜像中的 /usr/share/nginx/html
目录,这样当 nginx 镜像启动的时候就是直接使用的改文件夹下面的文件。
所以现在我们需要获取打包后的目录 build
,同样的,和上面服务端项目一样,我们可以使用两种方式来完成这个工作。
第一种方式自然是推荐的 Docker 的多阶段构建,我们在一个 node
镜像的环境中就可以打包我们的前端项目了,所以我们可以更改下 Dockerfile
文件,先进行 node 打包,然后再进行 nginx 启动:
FROM node:alpine as BUILD WORKDIR /usr/src/app RUN mkdir -p /usr/src/app ADD . /usr/src/app RUN npm install && / npm run build FROM nginx:1.15.10-alpine MAINTAINER cnych <icnych@gmail.com> COPY --from=BUILD /usr/src/app/build /usr/share/nginx/html ADD nginx.conf /etc/nginx/conf.d/default.conf
第二种方式和上面一样在 Jenkins Pipeline 中添加一个打包构建的阶段即可,我们这里采用这种方式,所以 Dockerfile 文件还是使用第一个就行。
现在我们可以将客户端的代码推送到 Gitlab 上去,我们这里的仓库地址为: http://git.qikqiak.com/course/polling-app-client.git
现在项目准备好了,接下来我们可以开始 Jenkins 的配置,还记得前面在 Pipeline 结合 Kubernetes 的课程中我们使用了一个 kubernetes
的 Jenkins 插件,但是之前使用的方式有一些不妥的地方,我们 Jenkins Pipeline 构建任务绑定到了一个固定的 Slave Pod 上面,这样就需要我们的 Slave Pod 中必须包含一系列构建所需要的依赖,比如 docker、maven、node、java 等等,这样就难免需要我们自己定义一个很庞大的 Slave 镜像,我们直接直接在 Pipeline 中去自定义 Slave Pod 中所需要用到的容器模板,这样我们需要什么镜像只需要在 Slave Pod Template 中声明即可,完全不需要去定义一个庞大的 Slave 镜像了。
首先去掉 Jenkins 中 kubernetes 插件中的 Pod Template 的定义,Jenkins -> 系统管理 -> 系统设置 -> 云 -> Kubernetes区域,删除下方的 Kubernetes Pod Template
-> 保存。
然后新建一个名为 polling-app-server
类型为 流水线(Pipeline)
的任务:
然后在这里需要勾选 触发远程构建
的触发器,其中令牌我们可以随便写一个字符串,然后记住下面的 URL,将 JENKINS_URL 替换成 Jenkins 的地址,我们这里的地址就是: http://jenkins.qikqiak.com/job/polling-app-server/build?token=server321
然后在下面的 流水线
区域我们可以选择 Pipeline script
然后在下面测试流水线脚本,我们这里选择 Pipeline script from SCM
,意思就是从代码仓库中通过 Jenkinsfile
文件获取 Pipeline script
脚本定义,然后选择 SCM 来源为 Git
,在出现的列表中配置上仓库地址 http://git.qikqiak.com/course/polling-app-server.git
,由于我们是在一个 Slave Pod 中去进行构建,所以如果使用 SSH 的方式去访问 Gitlab 代码仓库的话就需要频繁的去更新 SSH-KEY,所以我们这里采用直接使用用户名和密码的形式来方式:
在 Credentials
区域点击 添加
按钮添加我们访问 Gitlab 的用户名和密码:
然后需要我们配置用于构建的分支,如果所有的分支我们都想要进行构建的话,只需要将 Branch Specifier
区域留空即可,一般情况下不同的环境对应的分支才需要构建,比如 master、develop、test 等,平时开发的 feature 或者 bugfix 的分支没必要频繁构建,我们这里就只配置 master 和 develop 两个分支用户构建:
然后前往 Gitlab 中配置项目 polling-app-server
Webhook,settings -> Integrations,填写上面得到的 trigger 地址:
保存后,可以直接点击 Test
-> Push Event
测试是否可以正常访问 Webhook 地址,这里需要注意的是我们需要配置下 Jenkins 的安全配置,否则这里的触发器没权限访问 Jenkins,系统管理 -> 全局安全配置:取消 防止跨站点请求伪造
,勾选上 匿名用户具有可读权限
:
如果测试出现了 Hook executed successfully: HTTP 201
则证明 Webhook 配置成功了,否则就需要检查下 Jenkins 的安全配置是否正确了。
配置成功后我们只需要往 Gitlab 仓库推送代码就会触发 Pipeline 构建了。接下来我们直接在服务端代码仓库根目录下面添加 Jenkinsfile
文件,用于描述流水线构建流程。
首先定义最简单的流程,要注意这里和前面课程的不同之处,这里我们使用 podTemplate
来定义不同阶段使用的的容器,有哪些阶段呢?
Clone 代码 -> 代码静态分析 -> 单元测试 -> Maven 打包 -> Docker 镜像构建/推送 -> Helm 更新服务。
Clone 代码在默认的 Slave 容器中即可;静态分析和单元测试我们这里直接忽略,有需要这个阶段的同学自己添加上即可;Maven 打包肯定就需要 Maven 的容器了;Docker 镜像构建/推送是不是就需要 Docker 环境了呀;最后的 Helm 更新服务是不是就需要一个有 Helm 的容器环境了,所以我们这里就可以很简单的定义 podTemplate
了,如下定义:(添加一个 kubectl
工具用于测试)
def label = "slave-${UUID.randomUUID().toString()}" podTemplate(label: label, containers: [ containerTemplate(name: 'maven', image: 'maven:3.6-alpine', command: 'cat', ttyEnabled: true), containerTemplate(name: 'docker', image: 'docker', command: 'cat', ttyEnabled: true), containerTemplate(name: 'kubectl', image: 'cnych/kubectl', command: 'cat', ttyEnabled: true), containerTemplate(name: 'helm', image: 'cnych/helm', command: 'cat', ttyEnabled: true) ], volumes: [ hostPathVolume(mountPath: '/root/.m2', hostPath: '/var/run/m2'), hostPathVolume(mountPath: '/home/jenkins/.kube', hostPath: '/root/.kube'), hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock') ]) { node(label) { def myRepo = checkout scm def gitCommit = myRepo.GIT_COMMIT def gitBranch = myRepo.GIT_BRANCH stage('单元测试') { echo "测试阶段" } stage('代码编译打包') { container('maven') { echo "打码编译打包阶段" } } stage('构建 Docker 镜像') { container('docker') { echo "构建 Docker 镜像阶段" } } stage('运行 Kubectl') { container('kubectl') { echo "查看 K8S 集群 Pod 列表" sh "kubectl get pods" } } stage('运行 Helm') { container('helm') { echo "查看 Helm Release 列表" sh "helm list" } } } }
上面这段 groovy
脚本比较简单,我们需要注意的是 volumes
区域的定义,将容器中的 /root/.m2
目录挂载到宿主机上是为了给 Maven
构建添加缓存的,不然每次构建的时候都需要去重新下载依赖,这样就非常慢了;挂载 .kube
目录是为了能够让 kubectl
和 helm
两个工具可以读取到 Kubernetes 集群的连接信息,不然我们是没办法访问到集群的;最后挂载 /var/run/docker.sock
文件是为了能够让我们的 docker
这个容器获取到 Docker Daemon
的信息的,因为 docker
这个镜像里面只有客户端的二进制文件,我们需要使用宿主机的 Docker Daemon
来构建镜像,当然我们也需要在运行 Slave Pod 的节点上拥有访问集群的文件,然后在每个 Stage
阶段使用特定需要的容器来进行任务的描述即可,所以这几个 volumes
都是非常重要的
volumes: [ hostPathVolume(mountPath: '/root/.m2', hostPath: '/var/run/m2'), hostPathVolume(mountPath: '/home/jenkins/.kube', hostPath: '/root/.kube'), hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock') ]
另外一个值得注意的就是 label
标签的定义,我们这里使用 UUID 生成一个随机的字符串,这样可以让 Slave Pod 每次的名称都不一样,而且这样就不会被固定在一个 Pod 上面了,以后有多个构建任务的时候就不会存在等待的情况了,这和我们之前的课程中讲到的固定在一个 label 标签上有所不同。
然后我们将上面的 Jenkinsfile
文件提交到 Gitlab 代码仓库上:
$ git add Jenkinsfile $ git commit -m "添加 Jenkinsfile 文件" $ git push origin master
然后切换到 Jenkins 页面上,正常情况就可以看到我们的流水线任务 polling-app-server
已经被触发构建了,然后回到我们的 Kubernetes 集群中可以看到多了一个 slave 开头的 Pod,里面有5个容器,就是我们上面 podTemplate 中定义的4个容器,加上一个默认的 jenkins slave 容器,同样的,构建任务完成后,这个 Pod 也会被自动销毁掉:
$ kubectl get pods -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-7fbfcc5ddc-xsqmt 1/1 Running 0 1d slave-6e898009-62a2-4798-948f-9c80c3de419b-0jwml-6t6hb 5/5 Running 0 36s ......
正常可以看到 Jenkins 中的任务构建成功了:
未完待续……
最后打个广告,给大家推荐一个本人精心打造的一个精品课程,现在限时优惠中: 从 Docker 到 Kubernetes 进阶
扫描下面的二维码(或微信搜索 k8s技术圈
)关注我们的微信公众帐号,在微信公众帐号中回复 加群 即可加入到我们的 kubernetes 讨论群里面共同学习。