Docker+Jenkins+Pipeline实现持续集成(二)java项目构建

3字数 1464阅读 1781

插件推荐

插件名 作用
Blue Ocean Jenkins2.7以后可安装,是Jenkins的一种新视图,能够通过图形化的界面创建和编辑Jenkinsfile,实现pipeline as code
Pipeline Maven Integration Plugin 在pipeline中集成maven,即可使用withMaven{}命令
Config File Provider Plugin 可创建并管理Maven的settings文件及其他配置文件
JUnit Attachments Plugin 可以对单元测试生成的测试结果在Jenkins中进行展示
Task Scanner Plugin 跟踪项目中TODO和FIXTURE
Rancher Plugin 可以在Rancher1.*上创建及更新服务
HTTP Request Plugin 可以发送HTTP请求

创建pipeline项目

在Jenkins首页点击“新建”进入项目的创建页面,输入任务名后,选择“流水线”(或"Pipeline"),点击“确定”,即可创建一个新的pipeline项目,如下图所示:


创建pipeline项目

编写pipeline脚本

在项目配置页面,找到"流水线"一项,选择"Pipeline script"即可编辑脚本
下面首先介绍脚本式pipeline的基本结构,以及如何使用Jenkins自带的语法生成器:

pipeline的基本结构

pipeline语法分为了脚本式pipeline和声明式pipeline,以下均为脚本式pipeline。脚本式Pipeline本质是一个groovy脚本,可以直接使用Groovy提供的大多数功能,执行的顺序是从顶部开始的顺序执行。整个Pipeline使用node块结构,每一个阶段的执行用stage表示,如下是Jenkins提供的GitHub+Maven的模板(在脚本右上方处try sample Pipeline选择GitHub+Maven即会自动生成):

      node {
       def mvnHome
       stage('Preparation') { // for display purposes
          // Get some code from a GitHub repository
          git 'https://github.com/jglick/simple-maven-project-with-tests.git'
          // Get the Maven tool.
          // ** NOTE: This 'M3' Maven tool must be configured
          // **       in the global configuration.           
          mvnHome = tool 'M3'
       }
       stage('Build') {
          // Run the maven build
          if (isUnix()) {
             sh "'${mvnHome}/bin/mvn' -Dmaven.test.failure.ignore clean package"
          } else {
             bat(/"${mvnHome}\bin\mvn" -Dmaven.test.failure.ignore clean package/)
          }
       }
       stage('Results') {
          junit '**/target/surefire-reports/TEST-*.xml'
          archive 'target/*.jar'
       }
    }

语法生成

Jenkins提供了语法生成器,可以帮助我们编写pipeline流程。点击脚本下的Pipeline Syntax可进入pipiline-syntax页面,如下图所示:

语法生成器

在pipiline-syntax页面中选择需要执行的构建步骤,填写参数,然后点击“Generate Pipeline Script”即可生成需要的语法,如下图所示:
语法生成器使用

点击pipeline-syntax页面右方的Global Variables Reference,页面会展示pipeline脚本中可直接使用的全局变量,有docker、env、currentBuild等。当需要在脚本中使用全局变量,则使用"."连接全局变量和属性/方法,例如,使用docker变量构建镜像的方法为docker.build(your_imagename),获取当前构建结果的属性为currentBuild.result

Java项目的pipeline编写

这里我们以java项目为例编写脚本式pipeline,这个pipeline进行了打包构建、生成docker镜像、并将镜像推送到docker仓库中,最后实现在Rancher上的自动服务部署)。

node {

    stage('Preparation') {
    }

    stage('Build') {
    }

    stage('DockerBuild') {
    }

    stage('Rancher') {
    }

}

gitlab代码拉取

Jenkins提供了有两种获取源代码的语法gitcheckout,推荐使用checkout的方式,因为其更强大,可以配置工作目录、选择是否使用钩子等等,如下为两种语法的使用模板:

checkout scm: [$class: 'GitSCM', branches: [[name: "*/${repoBranch}"]], doGenerateSubmoduleConfigurations: false, userRemoteConfigs: [[credentialsId: "${gitCredentialsId}", url: "${repoUrl}"]]]

git branch: "${repoBranch}", credentialsId: "${gitCredentialsId}", url: "${repoUrl}"

checkout的语法生成配置如下,需填写仓库url、branch等:

checkout语法生成

可以看到上图中选择了拉取gitlab代码的认证权限为”demo-ssh“,该认证需要在Credentials中进行配置;如果没有,也可点击Add进行添加,如下图为配置ssh密钥(对应公钥已添加在git仓库中):
添加认证

此时仓库认证成功,点击下方的GeneratePipelineScript生成语法,如下:
checkout语法生成

将该语法粘贴到Pipeline脚本中,如下:

stage('Preparation') {
    checkout([$class: 'GitSCM', branches: [[name: '*/jenkins-test']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c26f36ef-031d-4dbd-9d71-95be6a59e0f6', url: 'git@****.git']]])
}

maven构建

接下来需要添加代码构建阶段“Build”,通过mvn命令实现代码构建,在这之前我们需要在Jenkins添加一些配置。
(1)下载插件Pipeline Maven Integration PluginJenkins Config File Provicer Plugin,辅助插件JUnit Attachments PluginTask Scanner Plugin
(2)配置maven工具,进入系统管理——全局工具配置,找到Maven项,点击新增Maven,即可配置安装一个新的Maven工具,如下图所示:

maven工具安装

(3)对于一些java项目,需要定制化的使用settings.xml文件(例如添加maven镜像和仓库)。进入系统管理——Managed files,点击Add a new Config后,选择Maven settings.xml,点击Submit,如下图:
添加settings.xml文件

然后在配置页面编写settings.xml文件。
然后pipeline-syntax页面,找到withMaven语法,进行maven的配置,包括选择maven工具、settings文件等等,如下图:
withMaven

最后将语法添加在Pipeline脚本中,并在withMaven里使用批处理命令来执行mvn的构建命令,如下:

stage('Build') {
    withMaven(
        maven: 'maven3.5.2',
        mavenSettingsConfig: '9e88adc5-8b36-4f00-b6f6-fdb15e9286ae') {
        sh 'mvn -U clean package -Dmaven.test.skip=true'
    }
}

(4)辅助插件可帮助我们查看JUnit执行情况、追踪项目的TODO和FIXME等等

docker镜像生成和发布

在上篇中,我们介绍了如何在Jenkins容器里使用Docker命令,此时,我们可以直接利用全局变量docker进行docker的各种操作(具体见Global Variables)。
我们首先将Build中生成的jar包复制到Dockerfile所在目录下,然后使用方法docker.build()生成docker镜像,且该方法会返回一个Image的对象,然后使用Image.push()方法即可将该镜像推送到远程仓库,如下:

stage('DockerBuild') {
    sh """
    rm -f src/docker/*.jar
    cp target/*.jar src/docker/*.jar
    """
    
    dir ("src/docker/") {
        def image = docker.build("*****/demo:1.0.0")
        image.push()
    }
}

注:由于当前我们使用root用户运行Jenkins容器,因此无法直接利用docker.withRegistry()或withDockerRegistry连接dockerhub仓库(该语句会生成仓库的config文件,该文件存放在/var/jenkins_home文件夹下,但docker命令会直接从/root下读取配置文件,导致配置文件无效)。因此,我们可以将config文件提前打包到Jenkins容器中,或者直接在命令行中进行登录。

Rancher1.X上的服务部署

我们的服务都使用了Rancher进行部署运行。当docker镜像已经推送到dockerhub仓库后,需要做的就是向Rancher发送请求,使其用最新构建的镜像进行服务更新。
(1)下载安装Rancher Plugin插件
(2)需要在Rancher中添加Environment Api Key,具体步骤为进入需部署服务的Rancher环境,点击API下的Keys,点击打开ADVANCEDOPTIONS,点击AddEnvironmentAPIKeys,如下图:

Rancher添加Api Key

(3)然后在弹出的对话框中填写Name、Description,点击Create,记录创建的Access Key和Secret Key,如下图:
记录Key

(4)在Jenkins的Credentials中添加一个类型为Username with password的认证,username和password分别对应于上一步生成的Access Key和Secret Key,如下图
添加Rancher认证

(5)然后在语法生成器中,找到rancher进行如下图的配置:
Rancher语法生成

(6)最后的Rancher服务部署阶段的pipeline脚本流程如下

stage('Rancher') {
    rancher confirm: false, credentialId: 'b56bd9b2-3277-4072-baae-08d73aa26549', endpoint: 'https://*******.com/v2-beta', environmentId: '1a226', environments: '', image: '*/demo:1.0.0', ports: '', service: 'jenkins/demo', timeout: 50
}

Rancher2.X上的服务部署

在编写文档时,尚未有插件支持Rancher2.X的服务自动部署,因此采取了直接向Rancher的Api发送请求的方式实现容器的更新。
(1)下载插件HTTP Request Plugin
(2)类似Rancher1.X,首先需要在Rancher上新增一个Api Key。进入需要部署服务的Rancher环境,选择右上角用户头像下的API & Keys进入配置页面,点击Add Key,填写描述和过期时间,进入API Key Created页面后,记录Endpoint、Access Key和Secret Key,如下图:

Rancher2.X添加API Key

(3)类似Rancher1.X,然后在Jenkins的Credentials中添加一个类型为Username with password的认证,username和password分别对应于上一步生成的Access Key和Secret Key,如下图
配置Rancher2认证

(4)不同与Rancher1.X,Rancher2.X尚未有插件支持,因此需要用http请求的方式调用Rancher的API。在实践过程中,我主要用到了4个Rancher API,分别是:

GET https://<rancher_server>/v3/project/<project_id>/workloads/deployment:<rancher_namespace>:<rancher_service> # 获取一个服务的详细信息
GET https://<rancher_server>/v3/project/<project_id>/pods/?workloadId=deployment:<rancher_namespace>:<rancher_service> # 获取服务的所有容器信息
DELETE https://<rancher_server>/v3/project/<project_id>/pods/<rancher_namespace>:<container_name> # 根据容器名删除容器
PUT https://<rancher_server>/v3/project/<project_id>/workloads/deployment:<rancher_namespace>:<rancher_service> # 更新服务

具体的脚本如下:

// 查询服务信息
def response = httpRequest acceptType: 'APPLICATION_JSON', authentication: "${RANCHER_API_KEY}", contentType: 'APPLICATION_JSON', httpMode: 'GET', responseHandle: 'LEAVE_OPEN', timeout: 10, url: "${rancherUrl}/workloads/deployment:${rancherNamespace}:${rancherService}"
def serviceInfo = new JsonSlurperClassic().parseText(response.content)
response.close()

def dockerImage = imageName+":"+imageTag
if (dockerImage.equals(serviceInfo.containers[0].image)) {
    // 如果镜像名未改变,直接删除原容器
    // 查询容器名称
    response = httpRequest acceptType: 'APPLICATION_JSON', authentication: "${RANCHER_API_KEY}", contentType: 'APPLICATION_JSON', httpMode: 'GET', responseHandle: 'LEAVE_OPEN', timeout: 10, url: "${rancherUrl}/pods/?workloadId=deployment:${rancherNamespace}:${rancherService}"
    def podsInfo = new JsonSlurperClassic().parseText(response.content)
    def containerName = podsInfo.data[0].name
    response.close()
    // 删除容器
    httpRequest acceptType: 'APPLICATION_JSON', authentication: "${RANCHER_API_KEY}", contentType: 'APPLICATION_JSON', httpMode: 'DELETE', responseHandle: 'NONE', timeout: 10, url: "${rancherUrl}/pods/${rancherNamespace}:${containerName}"
    
} else {
    // 如果镜像名改变,使用新镜像名更新容器
    serviceInfo.containers[0].image = dockerImage
    // 更新
    def updateJson = new JsonOutput().toJson(serviceInfo)
    httpRequest acceptType: 'APPLICATION_JSON', authentication: "${RANCHER_API_KEY}", contentType: 'APPLICATION_JSON', httpMode: 'PUT', requestBody: "${updateJson}", responseHandle: 'NONE', timeout: 10, url: "${rancherUrl}/workloads/deployment:${rancherNamespace}:${rancherService}"
}

注:如果Rancher1.X的部署不能通过插件满足,也可以采取调用API的方式实现,例如一个服务有多个容器的情况。

执行构建

脚本编写完成后,点击保存进入项目页面。点击项目的“立即构建”,会手动触发构建过程,项目的构建历史会在右侧显示,同时每个阶段的执行状态及日志会以表格的形式在Stage View中展示,如下图:

Pipeline构建视图

推荐阅读更多精彩内容