CI/CD并不是陌生的东西,大部分企业都有自己的CI/CD,不过今天我要介绍的是使用Jenkins和GitOps实现CI/CD。
整体架构如下:
涉及的软件以及版本信息如下:
| 软件 | 版本 |
|---|---|
| kubernetes | 1.17.9 |
| docker | 19.03.13 |
| jenkins | 2.249.3 |
| argocd | 1.8.0 |
| gitlab | 社区版11.8.1 |
| sonarqube | 社区版8.5.1 |
| traefik | 2.3.3 |
| 代码仓库 | 阿里云仓库 |
涉及的技术:
- Jenkins shareLibrary
- Jenkins pipeline
- Jenkinsfile
- Argocd
- sonarqube api操作
软件安装
软件安装我这里不贴具体的安装代码了,所有的代码我都放在了github上,地址:https://github.com/cool-ops/kubernetes-software-yaml.git
所以这里默认你已经安装好所以软件了。
在Jenkins上安装如下插件
- kubernetes
- AnsiColor
- HTTP Request
- SonarQube Scanner
- Utility Steps
- Email Extension Template
- Gitlab Hook
- Gitlab
在Jenkins上配置Kubernetes集群信息
在系统管理—>系统配置—>cloud
在Jenkins上配置邮箱地址
系统设置—>系统配置—>Email
(1)设置管理员邮箱
配置SMTP服务
在Gitlab上准备一个测试代码
我这里有一个简单的java测试代码,地址如下:https://gitee.com/jokerbai/springboot-helloworld.git
可以将其导入到自己的gitlab仓库。
在Gitlab上创建一个共享库
首先在gitlab上创建一个共享库,我这里取名叫shareLibrary,如下:
然后创建src/org/devops目录,并在该目录下创建一下文件。
它们的内容分别如下:
build.groovy
package org.devops// docker容器直接builddef DockerBuild(buildShell){sh """${buildShell}"""}
sendEmail.groovy
package org.devops//定义邮件内容def SendEmail(status,emailUser){emailext body: """<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4" offset="0"><table width="95%" cellpadding="0" cellspacing="0" style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif"><tr>本邮件由系统自动发出,无需回复!<br/>各位同事,大家好,以下为${JOB_NAME}项目构建信息</br><td><font color="#CC0000">构建结果 - ${status}</font></td></tr><tr><td><br /><b><font color="#0B610B">构建信息</font></b></td></tr><tr><td><ul><li>项目名称:${JOB_NAME}</li><li>构建编号:${BUILD_ID}</li><li>构建状态: ${status} </li><li>项目地址:<a href="${BUILD_URL}">${BUILD_URL}</a></li><li>构建日志:<a href="${BUILD_URL}console">${BUILD_URL}console</a></li></ul></td></tr><tr></table></body></html> """,subject: "Jenkins-${JOB_NAME}项目构建信息 ",to: emailUser}
sonarAPI.groovy
package ore.devops// 封装HTTP请求def HttpReq(requestType,requestUrl,requestBody){// 定义sonar api接口def sonarServer = "http://sonar.devops.svc.cluster.local:9000/api"result = httpRequest authentication: 'sonar-admin-user',httpMode: requestType,contentType: "APPLICATION_JSON",consoleLogResponseBody: true,ignoreSslErrors: true,requestBody: requestBody,url: "${sonarServer}/${requestUrl}"return result}// 获取soanr项目的状态def GetSonarStatus(projectName){def apiUrl = "project_branches/list?project=${projectName}"// 发请求response = HttpReq("GET",apiUrl,"")// 对返回的文本做JSON解析response = readJSON text: """${response.content}"""// 获取状态值result = response["branches"][0]["status"]["qualityGateStatus"]return result}// 获取sonar项目,判断项目是否存在def SearchProject(projectName){def apiUrl = "projects/search?projects=${projectName}"// 发请求response = HttpReq("GET",apiUrl,"")println "搜索的结果:${response}"// 对返回的文本做JSON解析response = readJSON text: """${response.content}"""// 获取total字段,该字段如果是0则表示项目不存在,否则表示项目存在result = response["paging"]["total"]// 对result进行判断if (result.toString() == "0"){return "false"}else{return "true"}}// 创建sonar项目def CreateProject(projectName){def apiUrl = "projects/create?name=${projectName}&project=${projectName}"// 发请求response = HttpReq("POST",apiUrl,"")println(response)}// 配置项目质量规则def ConfigQualityProfiles(projectName,lang,qpname){def apiUrl = "qualityprofiles/add_project?language=${lang}&project=${projectName}&qualityProfile=${qpname}"// 发请求response = HttpReq("POST",apiUrl,"")println(response)}// 获取质量阈IDdef GetQualityGateId(gateName){def apiUrl = "qualitygates/show?name=${gateName}"// 发请求response = HttpReq("GET",apiUrl,"")// 对返回的文本做JSON解析response = readJSON text: """${response.content}"""// 获取total字段,该字段如果是0则表示项目不存在,否则表示项目存在result = response["id"]return result}// 更新质量阈规则def ConfigQualityGate(projectKey,gateName){// 获取质量阈idgateId = GetQualityGateId(gateName)apiUrl = "qualitygates/select?projectKey=${projectKey}&gateId=${gateId}"// 发请求response = HttpReq("POST",apiUrl,"")println(response)}//获取Sonar质量阈状态def GetProjectStatus(projectName){apiUrl = "project_branches/list?project=${projectName}"response = HttpReq("GET",apiUrl,'')response = readJSON text: """${response.content}"""result = response["branches"][0]["status"]["qualityGateStatus"]//println(response)return result}
sonarqube.groovy
package ore.devopsdef SonarScan(projectName,projectDesc,projectPath){// sonarScanner安装地址def sonarHome = "/opt/sonar-scanner"// sonarqube服务端地址def sonarServer = "http://sonar.devops.svc.cluster.local:9000/"// 以时间戳为版本def scanTime = sh returnStdout: true, script: 'date +%Y%m%d%H%m%S'scanTime = scanTime - "\n"sh """${sonarHome}/bin/sonar-scanner -Dsonar.host.url=${sonarServer} \-Dsonar.projectKey=${projectName} \-Dsonar.projectName=${projectName} \-Dsonar.projectVersion=${scanTime} \-Dsonar.login=admin \-Dsonar.password=admin \-Dsonar.ws.timeout=30 \-Dsonar.projectDescription="${projectDesc}" \-Dsonar.links.homepage=http://www.baidu.com \-Dsonar.sources=${projectPath} \-Dsonar.sourceEncoding=UTF-8 \-Dsonar.java.binaries=target/classes \-Dsonar.java.test.binaries=target/test-classes \-Dsonar.java.surefire.report=target/surefire-reports -Xecho "${projectName} scan success!""""}
tools.groovy
package org.devops//格式化输出def PrintMes(value,color){colors = ['red' : "\033[40;31m >>>>>>>>>>>${value}<<<<<<<<<<< \033[0m",'blue' : "\033[47;34m ${value} \033[0m",'green' : "[1;32m>>>>>>>>>>${value}>>>>>>>>>>[m",'green1' : "\033[40;32m >>>>>>>>>>>${value}<<<<<<<<<<< \033[0m" ]ansiColor('xterm') {println(colors[color])}}// 获取镜像版本def createVersion() {// 定义一个版本号作为当次构建的版本,输出结果 20191210175842_69return new Date().format('yyyyMMddHHmmss') + "_${env.BUILD_ID}"}// 获取时间def getTime() {// 定义一个版本号作为当次构建的版本,输出结果 20191210175842return new Date().format('yyyyMMddHHmmss')}
在Gitlab上创建一个YAML管理仓库
我这里创建了一个叫devops-cd的共享仓库,如下:
然后以应用名创建一个目录,并在目录下创建以下几个文件。
它们的内容分别如下。
service.yaml
kind: ServiceapiVersion: v1metadata:name: the-servicenamespace: defaultspec:selector:deployment: hellotype: NodePortports:- protocol: TCPport: 8080targetPort: 8080
ingress.yaml
apiVersion: extensions/v1beta1kind: Ingressmetadata:name: the-ingressnamespace: defaultspec:rules:- host: test.coolops.cnhttp:paths:- backend:serviceName: the-serviceservicePort: 8080path: /
deploymeny.yaml
apiVersion: apps/v1kind: Deploymentmetadata:name: the-deploymentnamespace: defaultspec:replicas: 3selector:matchLabels:deployment: hellotemplate:metadata:labels:deployment: hellospec:containers:- args:- -jar- /opt/myapp.jar- --server.port=8080command:- javaenv:- name: HOST_IPvalueFrom:fieldRef:apiVersion: v1fieldPath: status.hostIPimage: registry.cn-hangzhou.aliyuncs.com/rookieops/myapp:latestimagePullPolicy: IfNotPresentlifecycle:preStop:exec:command:- /bin/sh- -c- /bin/sleep 30livenessProbe:failureThreshold: 3httpGet:path: /helloport: 8080scheme: HTTPinitialDelaySeconds: 60periodSeconds: 15successThreshold: 1timeoutSeconds: 1name: myappports:- containerPort: 8080name: httpprotocol: TCPreadinessProbe:failureThreshold: 3httpGet:path: /helloport: 8080scheme: HTTPperiodSeconds: 15successThreshold: 1timeoutSeconds: 1resources:limits:cpu: "1"memory: 2Girequests:cpu: 100mmemory: 1GiterminationMessagePath: /dev/termination-logterminationMessagePolicy: FilednsPolicy: ClusterFirstWithHostNetimagePullSecrets:- name: gitlab-registry
kustomization.yaml
# Example configuration for the webserver# at https://github.com/monopole/hellocommonLabels:app: helloresources:- deployment.yaml- service.yaml- ingress.yamlapiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationimages:- name: registry.cn-hangzhou.aliyuncs.com/rookieops/myappnewTag: "20201127150733_70"namespace: dev
在Jenkins上配置共享库
(1)需要在Jenkins上添加凭证
(2)在Jenkins的系统配置里面配置共享库(系统管理—>系统配置)
然后点击应用并保存
然后我们可以用一个简单的Jenkinsfile测试一下共享库,看配置是否正确。
在Jenkins上创建一个项目,如下:
然后在最地下的pipeline处贴入以下代码:
def labels = "slave-${UUID.randomUUID().toString()}"// 引用共享库@Library("jenkins_shareLibrary")// 应用共享库中的方法def tools = new org.devops.tools()pipeline {agent {kubernetes {label labelsyaml """apiVersion: v1kind: Podmetadata:labels:some-label: some-label-valuespec:volumes:- name: docker-sockhostPath:path: /var/run/docker.socktype: ''containers:- name: jnlpimage: registry.cn-hangzhou.aliyuncs.com/rookieops/inbound-agent:4.3-4- name: mavenimage: registry.cn-hangzhou.aliyuncs.com/rookieops/maven:3.5.0-alpinecommand:- cattty: true- name: dockerimage: registry.cn-hangzhou.aliyuncs.com/rookieops/docker:19.03.11command:- cattty: truevolumeMounts:- name: docker-sockmountPath: /var/run/docker.sock"""}}stages {stage('Checkout') {steps {script{tools.PrintMes("拉代码","green")}}}stage('Build') {steps {container('maven') {script{tools.PrintMes("编译打包","green")}}}}stage('Make Image') {steps {container('docker') {script{tools.PrintMes("构建镜像","green")}}}}}}
然后点击保存并运行,如果看到输出有颜色,就代表共享库配置成功,如下:
到此共享库配置完成。
编写Jenkinsfile
整个java的Jenkinsfile如下:
def labels = "slave-${UUID.randomUUID().toString()}"// 引用共享库@Library("jenkins_shareLibrary")// 应用共享库中的方法def tools = new org.devops.tools()def sonarapi = new org.devops.sonarAPI()def sendEmail = new org.devops.sendEmail()def build = new org.devops.build()def sonar = new org.devops.sonarqube()// 前端传来的变量def gitBranch = env.branchdef gitUrl = env.git_urldef buildShell = env.build_shelldef image = env.imagedef dockerRegistryUrl = env.dockerRegistryUrldef devops_cd_git = env.devops_cd_gitpipeline {agent {kubernetes {label labelsyaml """apiVersion: v1kind: Podmetadata:labels:some-label: some-label-valuespec:volumes:- name: docker-sockhostPath:path: /var/run/docker.socktype: ''- name: maven-cachepersistentVolumeClaim:claimName: maven-cache-pvccontainers:- name: jnlpimage: registry.cn-hangzhou.aliyuncs.com/rookieops/inbound-agent:4.3-4- name: mavenimage: registry.cn-hangzhou.aliyuncs.com/rookieops/maven:3.5.0-alpinecommand:- cattty: truevolumeMounts:- name: maven-cachemountPath: /root/.m2- name: dockerimage: registry.cn-hangzhou.aliyuncs.com/rookieops/docker:19.03.11command:- cattty: truevolumeMounts:- name: docker-sockmountPath: /var/run/docker.sock- name: sonar-scannerimage: registry.cn-hangzhou.aliyuncs.com/rookieops/sonar-scanner:latestcommand:- cattty: true- name: kustomizeimage: registry.cn-hangzhou.aliyuncs.com/rookieops/kustomize:v3.8.1command:- cattty: true"""}}environment{auth = 'joker'}options {timestamps() // 日志会有时间skipDefaultCheckout() // 删除隐式checkout scm语句disableConcurrentBuilds() //禁止并行timeout(time:1,unit:'HOURS') //设置流水线超时时间}stages {// 拉取代码stage('GetCode') {steps {checkout([$class: 'GitSCM', branches: [[name: "${gitBranch}"]],doGenerateSubmoduleConfigurations: false,extensions: [],submoduleCfg: [],userRemoteConfigs: [[credentialsId: '83d2e934-75c9-48fe-9703-b48e2feff4d8', url: "${gitUrl}"]]])}}// 单元测试和编译打包stage('Build&Test') {steps {container('maven') {script{tools.PrintMes("编译打包","blue")build.DockerBuild("${buildShell}")}}}}// 代码扫描stage('CodeScanner') {steps {container('sonar-scanner') {script {tools.PrintMes("代码扫描","green")tools.PrintMes("搜索项目","green")result = sonarapi.SearchProject("${JOB_NAME}")println(result)if (result == "false"){println("${JOB_NAME}---项目不存在,准备创建项目---> ${JOB_NAME}!")sonarapi.CreateProject("${JOB_NAME}")} else {println("${JOB_NAME}---项目已存在!")}tools.PrintMes("代码扫描","green")sonar.SonarScan("${JOB_NAME}","${JOB_NAME}","src")sleep 10tools.PrintMes("获取扫描结果","green")result = sonarapi.GetProjectStatus("${JOB_NAME}")println(result)if (result.toString() == "ERROR"){toemail.Email("代码质量阈错误!请及时修复!",userEmail)error " 代码质量阈错误!请及时修复!"} else {println(result)}}}}}// 构建镜像stage('BuildImage') {steps {withCredentials([[$class: 'UsernamePasswordMultiBinding',credentialsId: 'dockerhub',usernameVariable: 'DOCKER_HUB_USER',passwordVariable: 'DOCKER_HUB_PASSWORD']]) {container('docker') {script{tools.PrintMes("构建镜像","green")imageTag = tools.createVersion()sh """docker login ${dockerRegistryUrl} -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASSWORD}docker build -t ${image}:${imageTag} .docker push ${image}:${imageTag}docker rmi ${image}:${imageTag}"""}}}}}// 部署stage('Deploy') {steps {withCredentials([[$class: 'UsernamePasswordMultiBinding',credentialsId: 'ci-devops',usernameVariable: 'DEVOPS_USER',passwordVariable: 'DEVOPS_PASSWORD']]){container('kustomize') {script{APP_DIR="${JOB_NAME}".split("_")[0]sh """git remote set-url origin http://${DEVOPS_USER}:${DEVOPS_PASSWORD}@${devops_cd_git}git config --global user.name "Administrator"git config --global user.email "coolops@163.com"git clone http://${DEVOPS_USER}:${DEVOPS_PASSWORD}@${devops_cd_git} /opt/devops-cdcd /opt/devops-cdgit pullcd /opt/devops-cd/${APP_DIR}kustomize edit set image ${image}:${imageTag}git commit -am 'image update'git push origin master"""}}}}}// 接口测试stage('InterfaceTest') {steps{sh 'echo "接口测试"'}}}// 构建后的操作post {success {script{println("success:只有构建成功才会执行")currentBuild.description += "\n构建成功!"// deploy.AnsibleDeploy("${deployHosts}","-m ping")sendEmail.SendEmail("构建成功",toEmailUser)// dingmes.SendDingTalk("构建成功 ✅")}}failure {script{println("failure:只有构建失败才会执行")currentBuild.description += "\n构建失败!"sendEmail.SendEmail("构建失败",toEmailUser)// dingmes.SendDingTalk("构建失败 ❌")}}aborted {script{println("aborted:只有取消构建才会执行")currentBuild.description += "\n构建取消!"sendEmail.SendEmail("取消构建",toEmailUser)// dingmes.SendDingTalk("构建失败 ❌","暂停或中断")}}}}
需要在Jenkins上创建两个凭证,一个id叫dockerhub,一个叫ci-devops,还有一个叫sonar-admin-user。
dockerhub是登录镜像仓库的用户名和密码。
ci-devops是管理YAML仓库的用户名和密码。
sonar-admin-user是管理sonarqube的用户名和密码。
然后将这个Jenkinsfile保存到shareLibrary的根目录下,命名为java.Jenkinsfile。
在Jenkins上配置项目
在Jenkins上新建一个项目,如下:
然后添加以下参数化构建。






然后在流水线处配置Pipeline from SCM

此处需要注意脚本名。
然后点击应用保存,并运行。

也可以在sonarqube上看到代码扫描的结果。
在Argocd上配置CD流程
在argocd上添加代码仓库,如下:

然后创建应用,如下:

点击创建后,如下:
点进去可以看到更多的详细信息。
argocd有一个小bug,它ingress的健康检查必须要loadBalance有值,不然就不通过,但是并不影响使用。
然后可以正常访问应用了。
node项目的Jenkinsfile大同小异,由于我没有测试用例,所以并没有测试。
集成Gitlab,通过Webhook触发Jenkins
在Jenkins中选择项目,在项目中配置gitlab触发,如下:
生成token,如下
在gitlab上配置集成。进入项目—>项目设置—>集成
配置Jenkins上生成的回调URL和TOKEN
到此配置完成,然后点击下方test,可以观察是否触发流水线。
也可以通过修改仓库代码进行测试。
写在最后
本片文章是纯操作步骤,大家在测试的时候可能会对Jenkinsfile做细微的调整,不过整体没什么问题。
