Devops系列六(CI篇之jenkinsfile)jenkins将gitlab helm yaml和argocd 串联,自动部署到K8S

一、为什么是jenkinsfile

上文我们说了pipeline,已为本文铺路不少,接下里就是将之串联起来。
先想说下,为什么是jenkinsfile, 因为jenkins job还支持pipeline方式。
Devops系列六(CI篇之jenkinsfile)jenkins将gitlab helm yaml和argocd 串联,自动部署到K8S_第1张图片
这种方式,不建议实际使用,仅限于测试或调试groovy代码。

下面贴出来,我们的使用方式。好处是:采用分布式的思想,改动git上的jenkinsfile,就可以让所有的job更新。
Devops系列六(CI篇之jenkinsfile)jenkins将gitlab helm yaml和argocd 串联,自动部署到K8S_第2张图片

二、jenkinsfile的参数

这也有两种方式,我反而又不建议你写在jenkinsfile里。

  • Job (建议方式,因为每个工程的参数不一样,需要持久化保存)
  • jenkinsfile

Devops系列六(CI篇之jenkinsfile)jenkins将gitlab helm yaml和argocd 串联,自动部署到K8S_第3张图片
对于java语言来说, 我梳理了以下几个必须参数:

  • jarName
  • port
  • projectName(消息通知)
  • codeUrl
  • branch

为了一些额外需求,我们还定义了几个扩展参数:

  • packageType(打包方式,mvn或者gradle。默认为mvn)
  • robotKey(消息通知的机器人robot,默认为空)
  • childModule(子模块, 用于一个多module的项目打包,指定某模块以发布。默认为空)

三、groovy编码的几点建议

1、变量一定要使用trim()方法

举例,shell命令读取程序版本号,返回值如果没有使用trim(),会额外多一个换行符。-- 后期在使用该变量拼接的时候,很容易就把明明一行执行的,硬生生地被拆成了两行执行。

// 读取java应用的版本号
def getAppVersion(packagePath) {
    def appVersion = sh returnStdout: true,
            script: """
                    grep 'git.build.version' ${packagePath}/classes/git.properties | cut -d'=' -f2 | sed 's/\\r//'
                    """
    // trim()
    return appVersion.trim()
}

// 镜像版本号
def dockerImageVersion = appVersion + "-001"

docker build -f ${dockerfileName} -t ${repoProject}/${appName}:${dockerImageVersion} .
// 如果你没有使用trim()的话, 上面的这行代码,就会变成:
docker build -f ${dockerfileName} -t ${repoProject}/${appName}:${dockerImageVersion}
// 我还在纳闷,怎么最后的.被谁吃掉了呢。。。
// 当你期望的是
docker build -f /opt/Dockerfile -t xxx/devops-service:1.0.7-001 .

// 实际却是:
docker build -f /opt/Dockerfile -t xxx/devops-service:1.0.7

2、引入pipeline library

这里的名称,对应前文配置中的name

为了避免jenkinsfile的代码量过于复杂,我们往往会抽取出公共的方法。

@Library('jenkinslib') _

def tools = new com.xhtech.devops.tools()

tools.PrintMes("pull code!!!", "green")

3、读取参数

这里的参数分为两种,一是自定义,一是系统自带。

  • 字符串类型的变量,后面都紧跟着trim()方法。
// JOB_NAME是系统自带
String jobName = "${env.JOB_NAME}".trim()

// jarName是我们在job的参数化构建中配置
String jarName = "${env.jarName}".trim()

4、运行shell命令

// 正确的写法,必须由script括起来
script {
    sh "mvn clean package -Dmaven.test.skip=true -U"                
}

5、切换目录

dir("${env.WORKSPACE}") 

6、指定容器

默认容器是jnlp, 如果你的Pod配置了多个容器,docker image的相关操作就必须在docker容器里执行。

  • container(‘docker’),这里的docker就是对应PodTemplate中的容器名称。
container('docker') {
    dir("${env.WORKSPACE}") {
        docker.buildAndPushImage(jarName, port, dockerImageVersion, packagePath, dockerfileName)
    }
}

7、归档archiveArtifacts

把版本号写入到文件,并且归档

dir("${env.WORKSPACE}") {
    sh "echo ${appVersion} > version_${appVersion}.txt"

    archiveArtifacts artifacts: 'version_*.txt', followSymlinks: false
}

Devops系列六(CI篇之jenkinsfile)jenkins将gitlab helm yaml和argocd 串联,自动部署到K8S_第4张图片

8、设置操作的超时时间

timeout(time: 1, unit: "MINUTES") {
  // 具体操作,预计在1分钟内完成
}

9、指定工作空间

我们把工作空间持久化到某个目录,而不是放在Pod里。这样的好处是:起到了缓存的作用,不会随着Pod的销毁而需重建。

// 我们会对/opt/.m2进行持久化操作
String sharefile = "/opt/.m2"
String jobName = "${env.JOB_NAME}".trim()

String PROJECT_WORKSPACE = "$sharefile/java-workspaces/$jobName"

agent {
    kubernetes {
        inheritFrom 'jnlp-maven'
        customWorkspace "${PROJECT_WORKSPACE}"
    }
}

10、清理空间

安装jenkins plugin: Workspace Cleanup

// 文档地址: https://plugins.jenkins.io/ws-cleanup/

stage('Clean Workspace') {
    steps {
        cleanWs()
    }
}

四、完整的java-k8s.jenkinsfile


#!groovy

@Library('jenkinslib') _

String sharefile = "/opt/.m2"
String dockerfileName = sharefile + "/" + "Dockerfile"

// java代码仓库gitlab的ssh密钥
String gitlabCredentialsId = "ffcbbd41-1e6a-49ec-8277-87a1299606dd"

def tools = new com.xxtech.devops.tools()
def http = new com.xxtech.devops.http()
def docker = new com.xxtech.devops.docker()
def helm = new com.xxtech.devops.helm()

// OA:项目管理--研发项目--产品列表中的英文名称
String projectName = "${env.projectName}".trim()

// jenkins job
String jobName = "${env.JOB_NAME}".trim()

// jar包的名称
String jarName = "${env.jarName}".trim()
// java应用的端口
int port = "${env.port}"
// java代码的git仓库地址
String codeUrl = "${env.codeUrl}".trim()
// java代码的git分支
String branch = "${env.branch}".trim()

// 消息通知的机器人robot
String robotKey = "${env.robotKey}".trim()

// 打包方式,mvn或者gradle
String packageType = "${env.packageType}".trim()

// 构建者
String buildUser = ""

// 版本号, 由jenkins去读取
String appVersion = ""

//docker image version
String dockerImageVersion = ""

// 子模块, 用于一个多module的项目打包,指定某模块以发布
String childModule = "${env.childModule}".trim()
String packagePath = "target"
if (childModule && childModule != "null" && childModule.trim() != "") {
    tools.PrintMes("build package for childModule: " + childModule, "green")
    packagePath = childModule + "/target"
}

// 判断是否为gradle打包
boolean gradlePackage = packageType.equalsIgnoreCase("gradle")
if (gradlePackage) {
    packagePath = "build/libs"
}
String gradleUserHome = "$sharefile/java-gradle-homes"
String gradleProjectCacheDir = "$gradleUserHome/$jobName"

// workspace
String PROJECT_WORKSPACE = "$sharefile/java-workspaces/$jobName"

// 校验不能为空
if (!projectName) {
    tools.PrintMes("projectName is null", "red")
    return
}

if (!jarName) {
    tools.PrintMes("jarName is null", "red")
    return
}

if (!codeUrl) {
    tools.PrintMes("codeUrl is null", "red")
    return
}

if (!branch) {
    tools.PrintMes("branch is null", "red")
    return
}

//根据jobName读取环境区分
String[] jobInfoArray = jobName.split("_")
// 环境
String buildEnv = jobInfoArray[0]

pipeline {
    agent {
        kubernetes {
            inheritFrom 'jnlp-maven'
            customWorkspace "${PROJECT_WORKSPACE}"
        }
    }

    options {
        timestamps()  //日志会有时间
        skipDefaultCheckout()  //删除隐式checkout scm语句
        disableConcurrentBuilds() //禁止并行
        timeout(time: 1, unit: 'HOURS')  //流水线超时设置1h
    }

    stages {
        stage('Get UserInfo') {
            steps {
                wrap([$class: 'BuildUser']) {
                    script {
                        def BUILD_USER = "${env.BUILD_USER}"
                        def BUILD_USER_ID = "${env.BUILD_USER_ID}"

                        buildUser = BUILD_USER + "(" + BUILD_USER_ID + ")"
                    }
                }
            }
        }

        stage('Pull Code') {
            steps {
                script {
                    tools.PrintMes("pull code!!!", "green")
                    // 拉取代码:git协议
                    git branch: "$branch", credentialsId: "$gitlabCredentialsId", url: "$codeUrl"
                }
            }
            post {
                failure {
                    //当此Pipeline失败时打印消息
                    script {
                        tools.PrintMes("pull code failure!!!", "red")
                        http.imNotify(projectName, "FAIL", buildEnv, "pull code failure", branch, buildUser, robotKey)
                    }
                }
            }
        }

        stage('Compile') {
            steps {
                script {
                    tools.PrintMes("compile!!!", "green")
                    container('maven') {
                        dir("${env.WORKSPACE}") {
                            if (gradlePackage) {
                                sh "$sharefile/gradle-7.6/bin/gradle build -x test --build-cache -g $gradleUserHome --no-daemon --project-cache-dir $gradleProjectCacheDir"
                            } else {
                                sh "mvn clean package -Dmaven.test.skip=true -U  -s  $sharefile/settings.xml"
                            }
                        }
                    }
                }
            }
            post {
                failure {
                    //当此Pipeline失败时打印消息
                    script {
                        tools.PrintMes("compile failure!!!", "red")
                        http.imNotify(projectName, "FAIL", buildEnv, "compile failure", branch, buildUser, robotKey)
                    }
                }
            }
        }

        stage('Get Version') {
            steps {
                script {
                    tools.PrintMes("Get Version!!!", "green")
                    appVersion = tools.getAppVersion(packagePath)
                    // 输出程序的版本号
                    tools.PrintMes("get version: ${appVersion}", "green")
                }
            }
            post {
                failure {
                    //当此Pipeline失败时打印消息
                    script {
                        tools.PrintMes("Get Version failure!!!", "red")
                        http.imNotify(projectName, "FAIL", buildEnv, "Get Version failure", branch, buildUser, robotKey)
                    }
                }
            }
        }

        stage('Push Docker Image') {
            steps {
                script {
                    tools.PrintMes("Push Docker Image!!!", "green")
                    def currentTime = sh returnStdout: true,
                            script: """
                                    date +%m%d%H%M%S
                                    """
                    // 注意:这里的分隔符不能使用冒号, trim()
                    dockerImageVersion = appVersion + "-" + currentTime.trim()
                    tools.PrintMes("Docker Image Version: ${dockerImageVersion}", "green")

                    container('docker') {
                        dir("${env.WORKSPACE}") {
                            docker.buildAndPushImage(jarName, port, dockerImageVersion, packagePath, dockerfileName)
                        }
                    }
                }
            }
            post {
                failure {
                    //当此Pipeline失败时打印消息
                    script {
                        tools.PrintMes("Push Docker Image failure!!!", "red")
                        http.imNotify(projectName, "FAIL", buildEnv, "Push Docker Image failure", branch, buildUser, robotKey)
                    }
                }
            }
        }

        stage('Clean Workspace') {
            steps {
                script {
                    tools.PrintMes("Clean Workspace!!!", "green")
                    cleanWs()
                }
            }
            post {
                failure {
                    script {
                        //当此Pipeline失败时打印消息
                        tools.PrintMes("Clean Workspace failure!!!", "red")
                        http.imNotify(projectName, "FAIL", buildEnv, "Clean Workspace failure", branch, buildUser, robotKey)
                    }
                }
            }
        }

        stage('Update Helm Yaml') {
            steps {
                script {
                    tools.PrintMes("Update Helm Yaml!!!", "green")

                    helm.updateYaml(jarName, dockerImageVersion)
                }
            }
            post {
                failure {
                    script {
                        //当此Pipeline失败时打印消息
                        tools.PrintMes("Update Helm Yaml failure!!!", "red")
                        http.imNotify(projectName, "FAIL", buildEnv, "Update Helm Yaml failure", branch, buildUser, robotKey)
                    }
                }
            }
        }
    }

    post {
        success {
            //当此Pipeline成功时打印消息
            timeout(time: 1, unit: "MINUTES") {
                script {
                    tools.PrintMes("archive jar, send the message of build successfully to user", "green")
                    container('maven') {
                        dir("${env.WORKSPACE}") {
                            sh "echo ${appVersion} > version_${appVersion}.txt"

                            archiveArtifacts artifacts: 'version_*.txt', followSymlinks: false
                        }

                        http.imNotify(projectName, "SUCCESS", buildEnv, "OK", branch, buildUser, robotKey)
                    }
                }
            }
        }
    }
}

五、消息通知

  • TODO: 我们对于构建成功的消息内容,增加程序版本号。

Devops系列六(CI篇之jenkinsfile)jenkins将gitlab helm yaml和argocd 串联,自动部署到K8S_第5张图片
具体消息通知的实现,就不在本文的范畴。

写在末尾的话

后期,我们有空,就Jenkins集群、插件(包括二次开发插件)等话题,聊一聊我们的实际使用情况。

你可能感兴趣的:(ci/cd,jenkins)