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容器直接build def 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) } // 获取质量阈ID def 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){ // 获取质量阈id gateId = 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.devops def 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 -X echo "${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_69 return new Date().format('yyyyMMddHHmmss') + "_${env.BUILD_ID}" } // 获取时间 def getTime() { // 定义一个版本号作为当次构建的版本,输出结果 20191210175842 return new Date().format('yyyyMMddHHmmss') }
在Gitlab上创建一个YAML管理仓库
我这里创建了一个叫devops-cd的共享仓库,如下:
然后以应用名创建一个目录,并在目录下创建以下几个文件。
它们的内容分别如下。
service.yaml
kind: Service apiVersion: v1 metadata: name: the-service namespace: default spec: selector: deployment: hello type: NodePort ports: - protocol: TCP port: 8080 targetPort: 8080
ingress.yaml
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: the-ingress namespace: default spec: rules: - host: test.coolops.cn http: paths: - backend: serviceName: the-service servicePort: 8080 path: /
deploymeny.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: the-deployment namespace: default spec: replicas: 3 selector: matchLabels: deployment: hello template: metadata: labels: deployment: hello spec: containers: - args: - -jar - /opt/myapp.jar - --server.port=8080 command: - java env: - name: HOST_IP valueFrom: fieldRef: apiVersion: v1 fieldPath: status.hostIP image: registry.cn-hangzhou.aliyuncs.com/rookieops/myapp:latest imagePullPolicy: IfNotPresent lifecycle: preStop: exec: command: - /bin/sh - -c - /bin/sleep 30 livenessProbe: failureThreshold: 3 httpGet: path: /hello port: 8080 scheme: HTTP initialDelaySeconds: 60 periodSeconds: 15 successThreshold: 1 timeoutSeconds: 1 name: myapp ports: - containerPort: 8080 name: http protocol: TCP readinessProbe: failureThreshold: 3 httpGet: path: /hello port: 8080 scheme: HTTP periodSeconds: 15 successThreshold: 1 timeoutSeconds: 1 resources: limits: cpu: "1" memory: 2Gi requests: cpu: 100m memory: 1Gi terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirstWithHostNet imagePullSecrets: - name: gitlab-registry
kustomization.yaml
# Example configuration for the webserver # at https://github.com/monopole/hello commonLabels: app: hello resources: - deployment.yaml - service.yaml - ingress.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: - name: registry.cn-hangzhou.aliyuncs.com/rookieops/myapp newTag: "20201127150733_70" namespace: dev