kubernetes(38):持续集成(7)-Jenkins+Gitlab+Harbor+Helm+Kubernetes
Jenkins+Gitlab+Harbor+Helm+Kubernetes
https://www.qikqiak.com/k8s-book/docs/66.devops.html
使用 Jenkins + Gitlab + Harbor + Helm + Kubernetes 来实现一个完整的 CI/CD 流水线作业。
修改polling-app-server代码———提交gitlab——Jenkins构建新的镜像提交给harbor——再从harbor拉取镜像——helm进行更新——helm检查
1 流程
- 开发人员提交代码到 Gitlab 代码仓库
- 通过 Gitlab 配置的 Jenkins Webhook 触发 Pipeline 自动构建
- Jenkins 触发构建构建任务,根据 Pipeline 脚本定义分步骤构建
- 先进行代码静态分析,单元测试
- 然后进行 Maven 构建(Java 项目)
- 根据构建结果构建 Docker 镜像
- 推送 Docker 镜像到 Harbor 仓库
- 触发更新服务阶段,使用 Helm 安装/更新 Release
- 查看服务是否更新成功。
2 项目
本次示例项目是一个完整的基于 Spring Boot、Spring Security、JWT、React 和 Ant Design 构建的一个开源的投票应用,项目地址:https://github.com/callicoder/spring-security-react-ant-design-polls-app。 我也fork一个https://github.com/wangxu01/spring-security-react-ant-design-polls-app 会在该项目的基础上添加部分代码,并实践 CI/CD 流程。
3 服务端
首先需要更改的是服务端配置,我们需要将数据库链接的配置更改成环境变量的形式,写死了的话就没办法进行定制了,修改服务端文件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 wangxu <314144952@qq.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"]
定义了两个阶段,第一个阶段利用maven:3.6-alpine
这个基础镜像将我们的项目进行打包,然后将该阶段打包生成的jar
包文件复制到第二阶段进行最后的镜像打包,这样就可以很好的完成我们的 Docker 镜像的构建工作。
第二种方式就是我们传统的方式,在 Jenkins Pipeline 中添加一个maven
构建的阶段,然后在第二个 Docker 构建的阶段就可以直接获取到前面的jar
包了,也可以很方便的完成镜像的构建工作,为了更加清楚的说明 Jenkins Pipeline 的用法,我们这里采用这种方式,所以 Dockerfile 文件还是使用第一个就行。
同样fork的dithub地址是 https://github.com/wangxu01/polling-app-server
现在我们可以将服务端的代码推送到 Gitlab 上去,我们这里的仓库地址为:
http://gitlab.wangxu.com/root/polling-app-server
注意,这里我们只推送的服务端代码,持续集成的也只是服务端
[root@k8s-master test]# pwd /root/ops/test [root@k8s-master test]# [root@k8s-master test]# git clone https://github.com/wangxu01/polling-app-server 正克隆到 'polling-app-server'... remote: Enumerating objects: 222, done. remote: Counting objects: 100% (222/222), done. remote: Compressing objects: 100% (170/170), done. remote: Total 222 (delta 83), reused 170 (delta 39), pack-reused 0 接收对象中: 100% (222/222), 92.49 KiB | 0 bytes/s, done. 处理 delta 中: 100% (83/83), done. [root@k8s-master test]# ls gitlab-ci-k8s-demo gitlab-demo polling-app-server [root@k8s-master test]# cd polling-app-server/ [root@k8s-master polling-app-server]# git remote set-url origin ssh://git@gitlab.wangxu.com:30022/root/polling-app-server.git[root@k8s-master polling-app-server]# git push -u origin master Counting objects: 222, done. Delta compression using up to 4 threads. Compressing objects: 100% (126/126), done. Writing objects: 100% (222/222), 92.49 KiB | 0 bytes/s, done. Total 222 (delta 83), reused 222 (delta 83) remote: Resolving deltas: 100% (83/83), done. remote: remote: The private project root/polling-app-server was successfully created. remote: remote: To configure the remote, run: remote: git remote add origin git@gitlab.wangxu.com:root/polling-app-server.git remote: remote: To view the project, visit: remote: http://gitlab.wangxu.com/root/polling-app-server remote: To ssh://git@gitlab.wangxu.com:30022/root/polling-app-server.git * [new branch] master -> master 分支 master 设置为跟踪来自 origin 的远程分支 master。 [root@k8s-master polling-app-server]#
4 客户端
客户端我们需要修改 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 wangxu <314144952@qq.com> COPY --from=BUILD /usr/src/app/build /usr/share/nginx/html ADD nginx.conf /etc/nginx/conf.d/default.conf
第二种方式和上面一样在 Jenkins Pipeline 中添加一个打包构建的阶段即可,我们这里采用这种方式,所以 Dockerfile 文件还是使用第一个就行。
我们也fork了一个https://github.com/wangxu01/polling-app-client
现在我们可以将客户端的代码推送到 Gitlab 上去,我们这里的仓库地址为:
http://gitlab.wangxu.com/root/polling-app-client
[root@k8s-master test]# git clone https://github.com/wangxu01/polling-app-client 正克隆到 'polling-app-client'... remote: Enumerating objects: 88, done. remote: Counting objects: 100% (88/88), done. remote: Compressing objects: 100% (85/85), done. remote: Total 88 (delta 3), reused 85 (delta 0), pack-reused 0 Unpacking objects: 100% (88/88), done. [root@k8s-master test]# cd polling-app-client/ [root@k8s-master polling-app-client]# git remote set-url origin ssh://git@gitlab.wangxu.com:30022/root/polling-app-client.git [root@k8s-master polling-app-client]# git push -u origin master Counting objects: 88, done. Delta compression using up to 4 threads. Compressing objects: 100% (85/85), done. Writing objects: 100% (88/88), 254.11 KiB | 0 bytes/s, done. Total 88 (delta 20), reused 0 (delta 0) remote: remote: The private project root/polling-app-client was successfully created. remote: remote: To configure the remote, run: remote: git remote add origin git@gitlab.wangxu.com:root/polling-app-client.git remote: remote: To view the project, visit: remote: http://gitlab.wangxu.com/root/polling-app-client remote: To ssh://git@gitlab.wangxu.com:30022/root/polling-app-client.git * [new branch] master -> master 分支 master 设置为跟踪来自 origin 的远程分支 master。 [root@k8s-master polling-app-client]#
5 Jenkins
现在项目准备好了,接下来我们可以开始 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://10.6.76.25:30002/job/polling-app-server/build?token=server321
然后在下面的流水线区域我们可以选择Pipeline script然后在下面测试流水线脚本,我们这里选择Pipeline script from SCM,意思就是从代码仓库中通过Jenkinsfile文件获取Pipeline script脚本定义,然后选择 SCM 来源为Git,在出现的列表中配置上仓库地址http://gitlab.wangxu.com/course/polling-app-server.git,由于我们是在一个 Slave Pod 中去进行构建,所以如果使用 SSH 的方式去访问 Gitlab 代码仓库的话就需要频繁的去更新 SSH-KEY,所以我们这里采用直接使用用户名和密码的形式来方式:
我们的gitlab没有解析,Jenkins不能解析,用这个 http://gitlab.kube-ops.svc.cluster.local/root/polling-app-server.git
在Credentials区域点击添加按钮添加我们访问 Gitlab 的用户名和密码:
root/ admin321
然后需要我们配置用于构建的分支,如果所有的分支我们都想要进行构建的话,只需要将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" } } } }
需要注意的是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 代码仓库上
项目已经有了,我们Jenkinsfile替换提交触发一下
[root@k8s-master polling-app-server]# vim Jenkinsfile [root@k8s-master polling-app-server]# git add . [root@k8s-master polling-app-server]# git commit -m "修改 Jenkinsfile 文件" [master 13ce99f] 修改 Jenkinsfile 文件 1 file changed, 1 insertion(+), 1 deletion(-) [root@k8s-master polling-app-server]# git push origin master Counting objects: 5, done. Delta compression using up to 4 threads. Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 307 bytes | 0 bytes/s, done. Total 3 (delta 2), reused 0 (delta 0) To ssh://git@gitlab.wangxu.com:30022/root/polling-app-server.git a883f85..13ce99f master -> master [root@k8s-master polling-app-server]#
然后切换到 Jenkins 页面上,正常情况就可以看到我们的流水线任务polling-app-server已经被触发构建了,然后回到我们的 Kubernetes 集群中可以看到多了一个 slave 开头的 Pod,里面有5个容器,就是我们上面 podTemplate 中定义的4个容器,加上一个默认的 jenkins slave 容器,同样的,构建任务完成后,这个 Pod 也会被自动销毁掉:
[root@k8s-master polling-app-server]# kubectl get pods -n kube-ops NAME READY STATUS RESTARTS AGE …… slave-8f3bb83c-042c-4c7e-8caf-ab2c62aa6544-12b3n-8fqct 5/5 Running 0 12s
由于这个namespace权限的问题 我们把 sh "kubectl get pods "和 helm list先注释,先走通流程
接下来的工作就是来实现上面具体的 Pipeline 脚本了。
6 Pipeline
6.1 单元测试
第一个阶段:单元测试,我们可以在这个阶段是运行一些单元测试或者静态代码分析的脚本,我们这里直接忽略。
6.2 代码编译
第二个阶段:代码编译打包,我们可以看到我们是在一个maven
的容器中来执行的,所以我们只需要在该容器中获取到代码,然后在代码目录下面执行 maven 打包命令即可,如下所示:
stage('代码编译打包') { try { container('maven') { echo "2. 代码编译打包阶段" sh "mvn clean package -Dmaven.test.skip=true" } } catch (exc) { println "构建失败 - ${currentBuild.fullDisplayName}" throw(exc) } }
6.3 构建 Docker 镜像
第三个阶段:构建 Docker 镜像,要构建 Docker 镜像,就需要提供镜像的名称和 tag,要推送到 Harbor 仓库,就需要提供登录的用户名和密码,所以我们这里使用到了withCredentials
方法,在里面可以提供一个credentialsId
为dockerhub
的认证信息,如下:
stage('构建 Docker 镜像') { withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: 'dockerhub', usernameVariable: 'DOCKER_HUB_USER', passwordVariable: 'DOCKER_HUB_PASSWORD']]) { container('docker') { echo "3. 构建 Docker 镜像阶段" sh """ docker login ${dockerRegistryUrl} -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASSWORD} docker build -t ${image}:${imageTag} . docker push ${image}:${imageTag} """ } } }
其中 ${image} 和 ${imageTag} 我们可以在上面定义成全局变量:
def imageTag = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim() def dockerRegistryUrl = "harbor.wangxu.com" def imageEndpoint = "root/polling-app-server" def image = "${dockerRegistryUrl}/${imageEndpoint}"
docker 的用户名和密码信息则需要通过凭据来进行添加,进入 jenkins 首页 -> 左侧菜单凭据 -> 添加凭据,选择用户名和密码类型的,其中 ID 一定要和上面的credentialsId的值保持一致:
admin/Harbor12345
6.4 构建 Docker 镜像
第四个阶段:运行 kubectl 工具,其实在我们当前使用的流水线中是用不到 kubectl 工具的,那么为什么我们这里要使用呢?这还不是因为我们暂时还没有去写应用的 Helm Chart 包吗?所以我们先去用原始的 YAML 文件来编写应用部署的资源清单文件,这也是我们写出 Chart 包前提,因为只有知道了应用如何部署才可能知道 Chart 包如何编写,所以我们先编写应用部署资源清单。
k8s.yaml
首先当然就是 Deployment 控制器了,如下所示:(k8s.yaml)
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: polling-server namespace: course labels: app: polling-server spec: strategy: rollingUpdate: maxSurge: 1 maxUnavailable: 1 type: RollingUpdate template: metadata: labels: app: polling-server spec: restartPolicy: Always imagePullSecrets: - name: myreg containers: - image: <IMAGE>:<IMAGE_TAG> name: polling-server imagePullPolicy: IfNotPresent ports: - containerPort: 8080 name: api env: - name: DB_HOST value: mysql - name: DB_PORT value: "3306" - name: DB_NAME value: polling_app - name: DB_USER value: polling - name: DB_PASSWORD value: polling321 --- kind: Service apiVersion: v1 metadata: name: polling-server namespace: course spec: selector: app: polling-server type: ClusterIP ports: - name: api-port port: 8080 targetPort: api --- apiVersion: extensions/v1beta1 kind: Deployment metadata: name: mysql namespace: course spec: template: metadata: labels: app: mysql spec: restartPolicy: Always containers: - name: mysql image: mysql:5.7 imagePullPolicy: IfNotPresent ports: - containerPort: 3306 name: dbport env: - name: MYSQL_ROOT_PASSWORD value: rootPassW0rd - name: MYSQL_DATABASE value: polling_app - name: MYSQL_USER value: polling - name: MYSQL_PASSWORD value: polling321 volumeMounts: - name: db mountPath: /var/lib/mysql volumes: - name: db hostPath: path: /var/lib/mysql --- kind: Service apiVersion: v1 metadata: name: mysql namespace: course spec: selector: app: mysql type: ClusterIP ports: - name: dbport port: 3306 targetPort: dbport
可以看到我们上面的 YAML 文件中添加使用的镜像是用标签代替的:<IMAGE>:<IMAGE_TAG>,这是因为我们的镜像地址是动态的,下依赖我们在上一个阶段打包出来的镜像地址的,所以我们这里用标签代替,然后将标签替换成真正的值即可,另外为了保证应用的稳定性,我们还在应用中添加了健康检查,所以需要在代码中添加一个健康检查的 Controller:(src/main/java/com/example/polls/controller/StatusController.java)
package com.example.polls.controller; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/_status/healthz") public class StatusController { @GetMapping public String healthCheck() { return "UP"; } }
最后就是环境变量了,还记得前面我们更改了资源文件中数据库的配置吗?(src/main/resources/application.properties)因为要尽量通用,我们在部署应用的时候很有可能已经有一个外部的数据库服务了,所以这个时候通过环境变量传入进来即可。另外由于我们这里使用的是私有镜像仓库,所以需要在集群中提前创建一个对应的 Secret 对象:
kubectl create ns course #创建命名空间
kubectl create secret docker-registry myreg --docker-server=harbor.wangxu.com --docker-username=DOCKER_USER --docker-password=DOCKER_PASSWORD --docker-email=DOCKER_EMAIL --namespace course
在代码根目录下面创建一个 manifests 的目录,用来存放上面的资源清单文件,正常来说是不是我们只需要在镜像构建成功后,将上面的 k8s.yaml 文件中的镜像标签替换掉就 OK,所以这一步的动作如下:
stage('运行 Kubectl') { container('kubectl') { echo "查看 K8S 集群 Pod 列表" sh "kubectl get pods" sh """ sed -i "s/<IMAGE>/${image}" manifests/k8s.yaml sed -i "s/<IMAGE_TAG>/${imageTag}" manifests/k8s.yaml kubectl apply -f k8s.yaml """ } }
6.5 运行 Helm 工具
第五阶段:运行 Helm 工具,就是直接使用 Helm 来部署应用了,现在有了上面的基本的资源对象了,要创建 Chart 模板就相对容易了,Chart 模板仓库地址:https://github.com/cnych/polling-helm,我们可以根据values.yaml
文件来进行自定义安装,模板中我们定义了可以指定使用外部数据库服务或者内部独立的数据库服务,具体的我们可以去看模板中的定义。首先我们可以先使用这个模板在集群中来测试下。首先在集群中 Clone 上面的 Chart 模板:
gitclone https://github.com/cnych/polling-helm.git
我也fork
了一个 https://github.com/wangxu01/polling-helm
然后我们使用内部的数据库服务,新建一个 custom.yaml 文件来覆盖 values.yaml 文件中的值:
persistence: enabled: true persistentVolumeClaim: database: storageClass: "database" database: type: internal internal: database: "polling" # 数据库用户 username: "polling" # 数据库用户密码 password: "polling321"
可以看到我们这里使用了一个名为database
的 StorgeClass 对象,所以还得创建先创建这个资源对象:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: database
provisioner: fuseim.pri/ifs
然后我们就可以在 Chart 根目录下面安装应用,执行下面的命令:
[root@k8s-master test]# git clone https://github.com/cnych/polling-helm.git 正克隆到 'polling-helm'... remote: Enumerating objects: 115, done. remote: Counting objects: 100% (115/115), done. remote: Compressing objects: 100% (112/112), done. remote: Total 115 (delta 63), reused 49 (delta 0), pack-reused 0 接收对象中: 100% (115/115), 14.08 KiB | 0 bytes/s, done. 处理 delta 中: 100% (63/63), done. [root@k8s-master test]# [root@k8s-master test]# ls custom.yaml gitlab-ci-k8s-demo gitlab-demo polling-app-client polling-app-server polling-helm s-sc.yaml [root@k8s-master test]# rm -f custom.yaml s-sc.yaml ^C [root@k8s-master test]# cd polling-helm/ [root@k8s-master polling-helm]# ls Chart.yaml README.md templates values.yaml [root@k8s-master polling-helm]# mv ../custom.yaml . [root@k8s-master polling-helm]# mv ../s-sc.yaml mv: 在"../s-sc.yaml" 后缺少了要操作的目标文件 Try 'mv --help' for more information. [root@k8s-master polling-helm]# mv ../s-sc.yaml . [root@k8s-master polling-helm]# helm upgrade --install polling -f custom.yaml . --namespace course^C [root@k8s-master polling-helm]# ls Chart.yaml custom.yaml README.md s-sc.yaml templates values.yaml [root@k8s-master polling-helm]# helm upgrade --install polling -f custom.yaml . --namespace course Release "polling" does not exist. Installing it now. NAME: polling LAST DEPLOYED: Thu Oct 24 15:42:56 2019 NAMESPACE: course STATUS: DEPLOYED RESOURCES: ==> v1/Pod(related) NAME READY STATUS RESTARTS AGE polling-polling-api-7d6575ff64-9jcs2 0/1 ContainerCreating 0 1s polling-polling-database-0 0/1 Pending 0 0s polling-polling-ui-84b74859f-svhkt 0/1 ContainerCreating 0 0s ==> v1/Secret NAME TYPE DATA AGE polling-polling-database Opaque 1 1s ==> v1/Service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE polling-polling-api ClusterIP 10.104.180.131 <none> 8080/TCP 1s polling-polling-database ClusterIP 10.101.246.251 <none> 3306/TCP 1s polling-polling-ui ClusterIP 10.105.223.7 <none> 80/TCP 1s ==> v1/StatefulSet NAME READY AGE polling-polling-database 0/1 1s ==> v1beta1/Ingress NAME HOSTS ADDRESS PORTS AGE polling-polling-ingress ui.polling.domain 80 0s ==> v1beta2/Deployment NAME READY UP-TO-DATE AVAILABLE AGE polling-polling-api 0/1 1 0 1s polling-polling-ui 0/1 1 0 1s NOTES: 1. Get the application URL by running these commands: http://ui.polling.domain [root@k8s-master polling-helm]#
#注意我们这里安装也是使用的helm upgrade
命令,这样有助于安装和更新的时候命令统一。
安装完成后,查看下 Pod 的运行状态:
# kubectl get pods -n courseNAME READY STATUS RESTARTS AGE
polling-polling-api-7d6575ff64-cc2fv 1/1 Running 0 44s
polling-polling-database-0 1/1 Running 0 52m
polling-polling-ui-84b74859f-svhkt 1/1 Running 0 52m
然后我们可以在本地/etc/hosts
里面加上http://ui.polling.domain
的的映射,这样我们就可以通过这个域名来访问我们安装的应用了,可以注册、登录、发表投票内容了:
# kubectl get ingresses -n course NAME HOSTS ADDRESS PORTS AGE polling-polling-ingress ui.polling.domain 80 56m
10.6.76.23 ui.polling.domain 10.6.76.24 ui.polling.domain
不能中文,啊哈哈
这样我们就完成了使用 Helm Chart 安装应用的过程,但是现在我们使用的包还是直接使用的 git 仓库中的,平常我们正常安装的时候都是使用的 Chart 仓库中的包,所以我们需要将该 Chart 包上传到一个仓库中去,比较幸运的是我们的 Harbor 也是支持 Helm Chart 包的。我们可以选择手动通过 Harbor 的 Dashboard 将 Chart 包进行上传,也可以通过使用Helm Push
插件:
helm plugin install https://github.com/chartmuseum/helm-push Downloading and installing helm-push v0.7.1 ... https://github.com/chartmuseum/helm-push/releases/download/v0.7.1/helm-push_0.7.1_linux_amd64.tar.gz Installed plugin: push
需要首先将 Harbor 提供的仓库添加到 helm repo 中,由于是私有仓库,所以在添加的时候我们需要添加用户名和密码:
# helm repo add course https://harbor.wangxu.com/chartrepo/course --username=admin --password=Harbor12345 "course" has been added to your repositories
这里的 repo 的地址是<Harbor URL>/chartrepo/
<Harbor中项目名称>,Harbor 中每个项目是分开的 repo,如果不提供项目名称,则默认使用library
这个项目。
需要注意的是如果你的 Harbor 是采用的自建的 https 证书,这里就需要提供 ca 证书和私钥文件了,否则会出现证书校验失败的错误x509: certificate signed by unknown authority
。我们这里是通过cert-manager
为 Harbor 提供的一个信任的 https 证书,所以没有指定 ca 证书相关的参数。
harbor证书x509: certificate signed by unknown authority
[root@k8s-master test]# helm push polling-helm course Pushing polling-0.1.0.tgz to course... Done.
在右下角看到有添加仓库和安装 Chart 的相关命令。
到这里 Helm 相关的工作就准备好了。那么我们如何在 Jenkins Pipeline 中去使用 Helm 呢?我们可以回顾下,我们平时的一个 CI/CD 的流程:开发代码 -> 提交代码 -> 触发镜像构建 -> 修改镜像tag -> 推送到镜像仓库中去 -> 然后更改 YAML 文件镜像版本 -> 使用 kubectl 工具更新应用。
现在我们是不是直接使用 Helm 了,就不需要去手动更改 YAML 文件了,也不需要使用 kubectl 工具来更新应用了,而是只需要去覆盖下 helm 中的镜像版本,直接 upgrade 是不是就可以达到应用更新的结果了。我们可以去查看下 chart 包的 values.yaml 文件中关于 api 服务的定义:
api: image: repository: cnych/polling-api tag: 0.0.7 pullPolicy: IfNotPresent
将上面关于 api 服务使用的镜像用我们这里 Jenkins 构建后的替换掉就可以了
stage('运行 Helm') { container('helm') { echo "更新 polling 应用" sh """ helm upgrade --install polling polling --set persistence.persistentVolumeClaim.database.storageClass=database --set database.type=internal --set database.internal.database=polling --set database.internal.username=polling --set database.internal.password=polling321 --set api.image.repository=${image} --set api.image.tag=${imageTag} --set imagePullSecrets[0].name=myreg --namespace course """ } }
将需要更改的值都放入一个 YAML 之中来进行修改,我们这里通过--set
来覆盖对应的值,这样整个 API 服务的完整 Jenkinsfile 文件如下所示:
def label = "slave-${UUID.randomUUID().toString()}" def helmLint(String chartDir) { println "校验 chart 模板" sh "helm lint ${chartDir}" } def helmInit() { println "初始化 helm client" sh "helm init --client-only --stable-repo-url https://mirror.azure.cn/kubernetes/charts/" } def helmRepo(Map args) { println "添加 course repo" sh "helm repo add --username ${args.username} --password ${args.password} course http://harbor-harbor-core.kube-ops.svc.cluster.local/chartrepo/course" println "更新 repo" sh "helm repo update" println "获取 Chart 包" sh """ helm fetch course/polling tar -xzvf polling-0.1.0.tgz """ } def helmDeploy(Map args) { helmInit() helmRepo(args) if (args.dry_run) { println "Debug 应用" sh "helm upgrade --dry-run --debug --install ${args.name} ${args.chartDir} --set persistence.persistentVolumeClaim.database.storageClass=database --set api.image.repository=${args.image} --set api.image.tag=${args.tag} --set imagePullSecrets[0].name=myreg --namespace=course" } else { println "部署应用" sh "helm upgrade --install ${args.name} ${args.chartDir} --set persistence.persistentVolumeClaim.database.storageClass=database --set api.image.repository=${args.image} --set api.image.tag=${args.tag} --set imagePullSecrets[0].name=myreg --namespace=course" echo "应用 ${args.name} 部署成功. 可以使用 helm status ${args.name} 查看应用状态" } } 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: '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 def imageTag = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim() def dockerRegistryUrl = "harbor.wangxu.com" def imageEndpoint = "course/polling-api" def image = "${dockerRegistryUrl}/${imageEndpoint}" stage('单元测试') { echo "1.测试阶段" } stage('代码编译打包') { try { container('maven') { echo "2. 代码编译打包阶段" sh "mvn clean package -Dmaven.test.skip=true" } } catch (exc) { println "构建失败 - ${currentBuild.fullDisplayName}" throw(exc) } } stage('构建 Docker 镜像') { withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: 'dockerhub', usernameVariable: 'DOCKER_HUB_USER', passwordVariable: 'DOCKER_HUB_PASSWORD']]) { container('docker') { echo "3. 构建 Docker 镜像阶段" sh """ docker login ${dockerRegistryUrl} -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASSWORD} docker build -t ${image}:${imageTag} . docker push ${image}:${imageTag} """ } } } stage('运行 Helm') { withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: 'dockerhub', usernameVariable: 'DOCKER_HUB_USER', passwordVariable: 'DOCKER_HUB_PASSWORD']]) { container('helm') { // todo,可以做分支判断 echo "4. [INFO] 开始 Helm 部署" helmDeploy( dry_run : false, name : "polling", chartDir : "polling", namespace : "course", tag : "${imageTag}", image : "${image}", username : "${DOCKER_HUB_USER}", password : "${DOCKER_HUB_PASSWORD}" ) echo "[INFO] Helm 部署应用成功..." } } } } }
改了几处
1 harbor的地址不能解析,改成了k8s内部dns解析地址
2 下面这个rbac权限错误,我直接指定的namespace
部署应用 [Pipeline] sh + helm upgrade --install polling polling --set 'persistence.persistentVolumeClaim.database.storageClass=database' --set 'api.image.repository=harbor.wangxu.com/course/polling-api' --set 'api.image.tag=fadff33' --set 'imagePullSecrets[0].name=myreg' '--namespace=course' Error: pods is forbidden: User "system:serviceaccount:kube-ops:default" cannot list resource "pods" in API group "" in the namespace "kube-system" [Pipeline] }
这里只是对后端API进行了更新
[root@k8s-master test]# kubectl -n course get pod,svc NAME READY STATUS RESTARTS AGE pod/polling-polling-api-86c788465f-trjcx 1/1 Running 0 83s pod/polling-polling-database-0 1/1 Running 0 21h pod/polling-polling-ui-744798989-jb6lg 1/1 Running 0 46m NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/polling-polling-api ClusterIP 10.104.180.131 <none> 8080/TCP 21h service/polling-polling-database ClusterIP 10.101.246.251 <none> 3306/TCP 21h service/polling-polling-ui ClusterIP 10.105.223.7 <none> 80/TCP 21h [root@k8s-master test]# [root@k8s-master test]#
我想在gitlab中把副本数调整成3,提交,Jenkins自动发布
构建镜像,发给harbor
再从harbor拉取镜像去部署
但是 很抱歉,这里的helm-chart只是polling-app-server的镜像仓库,副本数不是在这里控制的,所以每次更新的都是server 也就是polling-polling-api ,也就是说我们实现了polling-app-server代码———提交gitlab——Jenkins构建新的镜像提交给harbor——再从harbor拉取镜像——helm进行更新——helm检查,别晕。
但也再次测试了,修改代码的自动构建发布
[root@k8s-master test]# cd polling-helm/ [root@k8s-master polling-helm]# ls Chart.yaml custom.yaml README.md s-sc.yaml templates values.yaml [root@k8s-master polling-helm]# grep rep values.yaml repository: cnych/polling-ui replicas: 1 repository: cnych/polling-api replicas: 1 repository: mysql [root@k8s-master polling-helm]#