文章目录
基于sharedLibrary进行CI/CD流程的优化
由于公司内部项目众多,大量的项目使用同一套流程做CICD
- 那么势必会存在大量的重复代码
- 一旦某个公共的地方需要做调整,每个项目都需要修改
因此本章主要通过使用实现Jenkins的sharedLibrary的开发,以提取项目在CICD实践过程中的公共逻辑,提供一系列的流程的接口供公司内各项目调用。
开发完成后,对项目进行Jenkinsfile的改造,最后仅需通过简单的Jenkinsfile的配置,即可优雅的完成CICD流程的整个过程,此方式已在大型企业内部落地应用。
Library工作模式
由于流水线被组织中越来越多的项目所采用,常见的模式很可能会出现。 在多个项目之间共享流水线有助于减少冗余并保持代码 “DRY”。
流水线支持引用 “共享库” ,可以在外部源代码控制仓库中定义并加载到现有的流水线中。
@Library('my-shared-library') _
在实际运行过程中,会把library中定义的功能添加到构建目录中:
/var/jenkins_home/jobs/test-maven-build/branches/feature-CDN-2904.cm507o/builds/2/libs/my-shared-library/vars/devops.
使用library后,Jenkinsfile大致的样子如下:
@Library('my-shared-library') _
...
stages {
stage('build image') {
steps {
container('tools') {
devops.buildImage("Dockerfile","172.21.51.67:5000/demo:latest")
}
}
}
}
post {
success {
script {
container('tools') {
devops.notificationSuccess("dingTalk")
}
}
}
}
...
开发环境搭建
补录章节:及SpringBoot、SpringCloud都会使用
- java
- intelliJ idea
下载安装包
链接:https://pan.baidu.com/s/1B-bg2_IsB8dU7_62IEtnTg
提取码:wx6j
安装java
安装路径:D:\software\jdk
环境变量:
- JAVA_HOME D:\software\jdk
- CLASSPATH .;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;
- PATH %JAVA_HOME%\bin
安装
解压路径:D:\software-3.0.2
环境变量:
- _PATH D:\software-3.0.2
- PATH D:\software-3.0.2\bin
安装idea
安装路径:D:\software\IntelliJ IDEA 2019.2.3
新建项目测试
Library代码结构介绍
共享库的目录结构如下:
(root)
+- src # source files
| +- org
| +- foo
| +- Bar. # for org.foo.Bar class
+- vars
| +- foo. # for global 'foo' variable
| +- foo.txt # help for 'foo' variable
src
目录应该看起来像标准的 Java 源目录结构。当执行流水线时,该目录被添加到类路径下。
vars
目录定义可从流水线访问的全局变量的脚本。 每个 *.
文件的基名应该是一个 (~ Java) 标识符, 通常是 camelCased
。
基本语法介绍
新建项目
-
变量
使用数据类型的本地语法,或者使用def关键字
// Defining a variable in lowercase int x = 5; // Defining a variable in uppercase int X = 6; // Defining a variable with the underscore in it's name def _Name = "Joe"; println(x); println(X); println(_Name);
-
方法
-
调用本地方法
def sum(int a, int b){ return a + b } println(sum(1,2))
-
调用类中的方法
# Hello. package demo def sayHi(String content) { return ("hi, " + content) } # Demo. import demo.Hello def demo() { return new Hello().sayHi("devops") } println(demo()) # 级联调用 # Hello. package demo def init(String content) { this.content = content return this } def sayHi() { println("hi, " + this.content) return this } def sayBye() { println("bye " + this.content) } # Demo. import demo.Hello def demo() { new Hello().init("devops").sayHi().sayBye() } demo()
-
-
异常捕获
def exceptionDemo(){ try { def val = 10 / 0 println(val) }catch(Exception e) { println(e.toString()) throw e } } exceptionDemo()
-
计时器与循环
import .time.TimeCategory use( TimeCategory ) { def endTime = TimeCategory.plus(new Date(), TimeCategory.getSeconds(15)) def counter = 0 while(true) { println(counter++) sleep(1000) if (new Date() >= endTime) { println("done") break } } }
-
解析yaml文件
import org.yaml.snakeyaml.Yaml def readYaml(){ def content = new File('myblog.yaml').text Yaml parser = new Yaml() def data = parser.load(content) def kind = data["kind"] def name = data["metadata"]["name"] println(kind) println(name) } readYaml()
library与Jenkins集成
先来看一下如何使用shared library实现最简单的helloworld输出功能,来理清楚使用shared library的流程。
Hello.
package com.luffy.devops
/**
* @author Yongxin
* @version v0.1
*/
/**
* say hello
* @param content
*/
def hello(String content) {
this.content = content
return this
}
def sayHi() {
echo "Hi, ${this.content},how are you?"
return this
}
def answer() {
echo "${this.content}: fine, thank you, and you?"
return this
}
def sayBye() {
echo "i am fine too , ${this.content}, Bye!"
return this
}
在gitlab创建项目,把library代码推送到镜像仓库。
配置Jenkins
[系统管理] -> [系统设置] -> [ Global Pipeline Libraries ]
- Library Name:luffy-devops
- Default Version:master
- Source Code Management:Git
Jenkinsfile中引用
jenkins/pipelines/p11.yaml
@Library('luffy-devops') _
pipeline {
agent { label 'jnlp-slave'}
stages {
stage('hello-devops') {
steps {
script {
devops.hello("树哥").sayHi().answer().sayBye()
}
}
}
}
post {
success {
echo 'Congratulations!'
}
failure {
echo 'Oh no!'
}
always {
echo 'I will always say Hello again!'
}
}
}
创建vars/devops.
import com.luffy.devops.Hello
def hello(String content) {
return new Hello().hello(content)
}
library集成镜像构建及推送
需要实现的逻辑点:
- docker build,docker push,docker login
- 账户密码,jenkins凭据,(library中获取凭据内容),
- docker login 172.21.51.67:5000
- try catch
镜像构建逻辑实现
devops.
/**
*
* @param repo, 172.21.51.67:5000/demo/myblog/xxx/
* @param tag, v1.0
* @param dockerfile
* @param credentialsId
* @param context
*/
def docker(String repo, String tag, String credentialsId, String dockerfile="Dockerfile", String context=".") {
return new Docker().docker(repo, tag, credentialsId, dockerfile, context)
}
Docker.
逻辑中需要注意的点:
- 构建和推送镜像,需要登录仓库(需要认证)
- 构建成功或者失败,需要将结果推给gitlab端
- 为了将构建过程推送到钉钉消息中,需要将构建信息统一收集
package com.luffy.devops
/**
*
* @param repo
* @param tag
* @param credentialsId
* @param dockerfile
* @param context
* @return
*/
def docker(String repo, String tag, String credentialsId, String dockerfile="Dockerfile", String context="."){
this.repo = repo
this.tag = tag
this.dockerfile = dockerfile
this.credentialsId = credentialsId
this.context = context
this.fullAddress = "${this.repo}:${this.tag}"
this.isLoggedIn = false
return this
}
/**
* build image
* @return
*/
def build() {
this.login()
retry(3) {
try {
sh "docker build ${this.context} -t ${this.fullAddress} -f ${this.dockerfile} "
}catch (Exception exc) {
throw exc
}
return this
}
}
/**
* push image
* @return
*/
def push() {
this.login()
retry(3) {
try {
sh "docker push ${this.fullAddress}"
}catch (Exception exc) {
throw exc
}
}
return this
}
/**
* docker registry login
* @return
*/
def login() {
if(this.isLoggedIn || credentialsId == ""){
return this
}
// docker login
withCredentials([usernamePassword(credentialsId: this.credentialsId, usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) {
def regs = this.getRegistry()
retry(3) {
try {
sh "docker login ${regs} -u $USERNAME -p $PASSWORD"
} catch (Exception exc) {
echo "docker login err, " + exc.toString()
}
}
}
this.isLoggedIn = true;
return this;
}
/**
* get registry server
* @return
*/
def getRegistry(){
def sp = this.repo.split("/")
if (sp.size() > 1) {
return sp[0]
}
return this.repo
}
Jenkinsfile
需要先在Jenkins端创建仓库登录凭据credential-registry
@Library('luffy-devops') _
pipeline {
agent { label 'jnlp-slave'}
options {
timeout(time: 20, unit: 'MINUTES')
gitLabConnection('gitlab')
}
environment {
IMAGE_REPO = "172.21.51.67:5000/demo/myblog"
IMAGE_CREDENTIAL = "credential-registry"
}
stages {
stage('checkout') {
steps {
container('tools') {
checkout scm
}
}
}
stage('docker-image') {
steps {
container('tools') {
script{
devops.docker(
"${IMAGE_REPO}",
"${GIT_COMMIT}",
IMAGE_CREDENTIAL
).build().push()
}
}
}
}
}
post {
success {
echo 'Congratulations!'
}
failure {
echo 'Oh no!'
}
}
}
丰富构建通知逻辑
目前的构建镜像逻辑中缺少如下内容:
- try逻辑中,若发生异常,是否该把异常抛出
- 若直接抛出异常可能会导致多次重复的异常信息
- 若不抛出,则如果未构建成功镜像,流水线感知不到错误
- 通知gitlab端构建任务及状态
- 构建通知格式
需要针对上述问题,做出优化
-
优化try逻辑
def build() { this.login() def isSuccess = false def errMsg retry(3) { try { sh "docker build ${this.context} -t ${this.fullAddress} -f ${this.dockerfile}" isSuccess = true }catch (Exception err) { //ignore errMsg = err.toString() } // check if build success if(isSuccess){ //todo }else { // throw exception,aborted pipeline error errMsg } return this } }
-
通知gitlab端构建任务及状态
def build() { this.login() def isSuccess = false def errMsg = "" retry(3) { try { sh "docker build ${this.context} -t ${this.fullAddress} -f ${this.dockerfile} " isSuccess = true }catch (Exception err) { //ignore errMsg = err.toString() } // check if build success def stage = env.STAGE_NAME + '-build' if(isSuccess){ updateGitlabCommitStatus(name: '${stage}', state: 'success') }else { updateGitlabCommitStatus(name: '${stage}', state: 'failed') // throw exception,aborted pipeline error errMsg } return this } }
-
钉钉消息通知格式
由于每个stage都需要构建通知任务,因此抽成公共的逻辑,为各stage调用
BuildMessage.
package com.luffy.devops def updateBuildMessage(String source, String add) { if(!source){ source = "" } env.BUILD_TASKS = source + add + "\n \n " return env.BUILD_TASKS }
Docker.
中调用def getObject(String repo, String tag, String credentialsId, String dockerfile="Dockerfile", String context="."){ ... this.msg = new BuildMessage() return this } ... def build() { ... // check if build success def stage = env.STAGE_NAME + '-build' if(isSuccess){ updateGitlabCommitStatus(name: '${stage}', state: 'success') this.msg.updateBuildMessage(env.BUILD_TASKS, "${stage} OK... √") }else { updateGitlabCommitStatus(name: '${stage}', state: 'failed') this.msg.updateBuildMessage(env.BUILD_TASKS, "${stage} Failed... x") // throw exception,aborted pipeline error errMsg } return this } }
使用Jenkinsfile
来验证上述修改是否正确:
@Library('luffy-devops') _
pipeline {
agent { lab