基于Jenkins和Docker,对于Java应用CI/CD实践的记录。

关于CI/CD

持续集成(CI)

持续集成开发人员能够频繁地将其代码集成到公共代码仓库的主分支中。 开发人员能够在任何时候多次向仓库提交作品,而不是独立地开发每个功能模块并在开发周期结束时一一提交。主要目的是尽早发现集成错误,使团队更加紧密结合,更好地协作。

目的

  • 减少集成的开销,如代码冲突
  • 将集成简化成一个简单、易于重复的日常开发任务,降低总体的构建成本,及早发现缺陷

持续交付(CD)

持续交付(CD)是CI的扩展,指软件交付流程的进一步自动化,可以随时自动化地部署到生产环境。使用CD后,开发团队可以在日常开发的任何时间进行产品级的发布,而不需要详细的发布方案或者特殊的后期测试。

持续交付与流水线

  • CD 集中依赖于部署流水线,通过流水线自动化测试和部署过程
  • 流水线包含了 构建-测试-部署
  • 在流水线的每个阶段,如果无法构建通过或者关键测试失败会向团队发出警报,流水线的最后一个部分会将构建部署到和生产环境等效的环境中

持续部署(CD)

持续部署扩展了持续交付,以便软件构建在通过所有测试时自动部署。在这样的流程中, 不需要人为决定何时及如何投入生产环境。CI/CD 系统的最后一步将在构建后的组件/包退出流水线时自动部署。 此类自动部署可以配置为快速向客户分发组件、功能模块或修复补丁,并准确说明当前提供的内容。

目的

  • 将新功能快速传递给用户,得到用户对于新版本的快速反馈,并且可以迅速处理任何明显的缺陷。

jenkins安装/配置(基于Docker)

安装

# docker获取jenkins镜像
docker pull jenkins/jenkins

# 启动Jenkins
docker run -u root -itd --name jenkins \
-p 10080:8080 \
-v $(which docker):/usr/bin/docker -v /var/run/docker.sock:/var/run/docker.sock \
-e TZ="Asia/Shanghai" -v /etc/localtime:/etc/localtime \
-e JAVA_OPTS='-Xms1024m -Xmx1024m -XX:NewSize=512m' \
-v /volume1/docker/jenkins:/var/jenkins_home jenkins/jenkins

# -p 10080:8080 映射容器的8080端口到外部主机的10080端口
# -v $(which docker):/usr/bin/docker -v /var/run/docker.sock:/var/run/docker.sock 使jenkins内部可以使用docker命令
# -e TZ="Asia/Shanghai" -v /etc/localtime:/etc/localtime 配置Jenkins容器的时区
# -v /volume1/docker/jenkins:/var/jenkins_home jenkins/jenkins 将Jenkins的配置映射到外部主机卷/volume1/docker/jenkins(删除容器后可保留)

插件推荐

插件名 作用
Blue Ocean Jenkins的一种新视图,能够通过图形化的界面创建和编辑Jenkinsfile,实现pipeline as code
Pipeline Maven Integration Plugin 在pipeline中集成maven,即可使用withMaven{}命令
Config File Provider Plugin 可创建并管理Maven的settings文件及其他配置文件
JUnit Attachments Plugin 可以对单元测试生成的测试结果在Jenkins中进行展示
HTTP Request Plugin 发送HTTP请求

配置

  • 配置Git凭据

图片

  • 配置maven setting文件

需要安装插件Config File Provider Plugin

图片

Jenkins Pipeline

什么是Pipline

pipeline 是jenkins2.X 最核心的特性, 帮助jenkins 实现从CI 到 CD与 DevOps的转变。pipeline 是一套运行于jenkins上的工作流框架,将原本独立运行于单个或者多个节点的任务连接起来,实现单个任务难以完成的复杂流程编排与可视化。

Pipeline优势

  1. 代码:pipeline 以代码的形式实现,通过被捡入源代码控制, 使团队能够编译,审查和迭代其cd流程
  2. 可连续性:jenkins 重启 或者中断后都不会影响pipeline job
  3. 停顿:pipeline 可以选择停止并等待人工输入或者批准,然后在继续pipeline运行
  4. 多功能:pipeline 支持现实世界的复杂CD要求, 包括fork、join子进程,循环和并行执行工作的能力
  5. 可扩展:pipeline 插件支持其DSL的自动扩展以及其插件集成的多个选项。

Pipeline组成

  • 基本结构
pipeline {
    agent { docker 'maven:3.3.3' }
    stages {
        stage('build') {
            steps {
                sh 'mvn --version'
            }
        }
    }
	post {
        always {
            echo 'This will always run'
        }
        success {
            echo 'This will run only if successful'
        }
        failure {
            echo 'This will run only if failed'
        }
        ...
    }
}
  • agent

agent指定整个Pipeline或特定stage将在Jenkins环境中执行的位置,具体取决于该agent 部分的位置。该部分必须在pipeline块内的顶层定义,stage块内的agent是可选的。

  • post

post定义将在Pipeline运行或stage结束时运行的操作。

  • stage stages

包含一个或多个stage指令的序列,该stages部分是Pipeline 描述的大部分“工作”所在的位置。

  • steps

steps部分定义了在给定stage指令中执行的一系列一个或多个步骤。

Jenkins Pipeline实现CI/CD

基本流程

架构图:

图片

基本流程:

图片

步骤:

  1. Jenkins拉取Git代码
  2. 静态代码分析
  3. mavem构建项目
  4. 单元测试
  5. build镜像
  6. push镜像到镜像仓库
  7. 远程部署(完成下载镜像,执行镜像的命令)

Pipeline实现

Jenkins拉取Git代码

可以使用checkout插件进行Git代码拉取(利用pipeline-syntax生成pipeline流水线脚本)

 steps {
			//checkout到master分支进行代码拉取
            checkout([$class: 'GitSCM', 
            branches: [[name: 'master']], 
            doGenerateSubmoduleConfigurations: false, 
            gitTool: 'Default', 
            extensions: [],
            submoduleCfg: [], 
            userRemoteConfigs: [[credentialsId: '8f240564-XXX-40b7-9faf-c08167aa27a3', url: 'git@gitee.com:XXX/XXX.git']]])
        }

适配git-flow分支模型,实现发布前进行merge操作和push代码操作。勾选参数化构建过程,选择Git Parameter,然后在pipeline实现相关参数。

图片


   parameters {
    	gitParameter branchFilter: 'origin/(.*)', defaultValue: 'master', name: 'BRANCH', type: 'PT_BRANCH', sortMode:'DESCENDING'
   }
   
   stages {
       stage('Pull Source code') {
        steps {
			//checkout到master 和 merge 指定分支操作
            checkout([$class: 'GitSCM', 
            branches: [[name: 'master']], 
            doGenerateSubmoduleConfigurations: false, 
            gitTool: 'Default', 
            extensions: [
                [$class: 'PreBuildMerge', options: [mergeRemote: 'origin', mergeStrategy: 'RECURSIVE', mergeTarget: "${params.BRANCH}"]]
            ],
            submoduleCfg: [], 
            userRemoteConfigs: [[credentialsId: '8f240564-XXX-40b7-9faf-c08167aa27a3', url: 'git@gitee.com:XXX/XXX.git']]])
        
			//代码推送到master操作(可以将此步骤下放到远程部署后或者post阶段,确保一个完整的成功的发布后才进行分支合并提交)
			sshagent(['8f240564-XXX-40b7-9faf-c08167aa27a3']) {
                sh """
                  git config --global user.email "******"
                    git config --global user.name "******"
				    git commit -m "header changed ci commit automatically" || true
                    git push origin HEAD:refs/heads/master
				"""
			}
		}
      }
		...
	}

Mavem构建项目

stage('Build') {
         steps {
          withMaven(maven: 'Maven3.6.3', mavenSettingsConfig: 'f6f97751-3c04-4394-876f-cee0879cfc02') {
              //这里使用了maven-resources-plugin插件对资源进行统一的COPY到path目录
                 sh 'mvn clean package -Dmaven.test.skip=true -Dbuild.path=/var/jenkins_home/build/bfreeman -f pom.xml'
            }
         }
      }

单元测试

简单的单元测试可以基于maven-shade-plugin插件,配合Junit跑单元测试。也可以 配合jacoco插件和 maven-jacoco-plugin 进行测试覆盖率的检测。

stage('Unit testing') {
         steps {
             withMaven(maven: 'Maven3.6.3', mavenSettingsConfig: 'f6f97751-3c04-4394-876f-cee0879cfc02') {
                 //这里使用了maven-shade-plugin插件进行单元测试
                 sh 'mvn clean test'
            }
         }
      }

Build Docker镜像

  • 编写Dockerfile
FROM openjdk:8u212-jdk
COPY /dev/* /build/conf/dev/
COPY /production/* /build/conf/production/
COPY api-1.0.0-fat.jar /build/api.jar
ENTRYPOINT ["java", "-jar","/build/api.jar","--vertx-id='bfreeman-api'","--java-opts='-Xms512m -Xmx512m -XX:NewSize=256m'","-conf /build/conf/production/conf.json"]

大致为COPY jar和配置文件到指定目录,ENTRYPOINT指定JVM参数和vertx参数

  • 执行docker build相关Shell脚本
#! /bin/bash

cd /var/jenkins_home/build/bfreeman
docker build -t bfreeman/api:1.0.0 .

Push Docker镜像到镜像仓库

这里使用的是阿里云容器镜像服务,参考文档

相关Shell脚本如下

#! /bin/bash

# 1. 获取构建的image id
IMAGES_ID=`docker images|grep -i bfreeman/api|awk '{print $3}'`
# 2. 设置repository相关信息
REPOSITORY="bfreeman/web"
TAG="1.0.0"
# 3. 登录Docker Registry ,使用docker tag命令重命名镜像并推送至登录Docker Registry
docker login -u=xx -p=xx registry.cn-hangzhou.aliyuncs.com
docker tag ${IMAGES_ID} registry.cn-hangzhou.aliyuncs.com/${REPOSITORY}:${TAG}
docker push registry.cn-hangzhou.aliyuncs.com/${REPOSITORY}:${TAG}

远程部署(镜像Pull,重新部署)

  • jenkins pipeline sshagent实现远程执行shell脚本

生成ssh密钥对,并将私钥配置到jenkins的凭证中,将公钥配置到对应部署服务器的.ssh/authorized_keys文件中,pipeline脚本如下:

sshagent(['feb51b70-****-45a5-a69b-319e41f7f146']) {
                    sh """
                   ...
                    """
}
  • 镜像Pull

相关Shell脚本如下

#! /bin/bash

# 1. 设置repository相关信息
REPOSITORY="bfreeman/web"
TAG="1.0.0"

# 2. 登录Docker Registry ,从Docker Registry中拉取镜像
docker login -u=xx -p=xx registry.cn-hangzhou.aliyuncs.com
docker pull registry.cn-hangzhou.aliyuncs.com/${REPOSITORY}:${TAG}

  • 重新部署

相关Shell脚本如下

#! /bin/bash

# 1. 设置repository相关信息
REPOSITORY="bfreeman/web"
TAG="1.0.0"

docker stop bfreeman-api
docker container rm bfreeman-api
docker run -u root -d --name bfreeman-api \
-p 10010:10010  \
-p 10030:10030  \
-v /volume1/docker/java:/data registry.cn-hangzhou.aliyuncs.com/${REPOSITORY}:${TAG}

此处可以基于k8s进行改造,实现jenkins部署到k8s集群。

相关pipeline

pipeline {
  agent any
  
  environment {
       imageid = ""
  }
   
  parameters {
    gitParameter branchFilter: 'origin/(.*)', defaultValue: 'master', name: 'BRANCH', type: 'PT_BRANCH', sortMode:'DESCENDING'
  }
   
   stages {
      //拉取代码
      stage('Pull Source Code') {
        steps {
			//checkout到master 和 merge 指定分支操作
			checkout([$class: 'GitSCM', 
			branches: [[name: 'master']], 
			doGenerateSubmoduleConfigurations: false, 
			 extensions: [
                [$class: 'PreBuildMerge', options: [mergeRemote: 'origin', mergeStrategy: 'RECURSIVE', mergeTarget: "${params.BRANCH}"]]
            ],
			submoduleCfg: [], 
			gitTool: 'Default',
			userRemoteConfigs: [[credentialsId: '0baad767-754c-****-8639-be2559a124ac', url: 'git@gitee.com:****/****.git']]]
			)
			}
      }
      
      //代码静态分析
      stage('Static Analysis') {
         steps {
            echo 'Program Static Analysis' 
         }
      }
      
      //构建代码
      stage('Build') {
         steps {
          withMaven(maven: 'Maven3.6.3', mavenSettingsConfig: 'f6f97751-****-4394-876f-cee0879cfc02') {
                 sh 'mvn clean package -Dmaven.test.skip=true -Dbuild.path=/var/jenkins_home/build/bfreeman -f pom.xml'
            }
         }
      }
      
      //单元测试
      stage('Unit testing') {
         steps {
             withMaven(maven: 'Maven3.6.3', mavenSettingsConfig: 'f6f97751-****-4394-876f-cee0879cfc02') {
                 sh 'mvn clean test'
            }
         }
      }
      
      //构建Docker镜像
      stage('Build Docker Image') {
         steps {
            sh """
				    cd /var/jenkins_home/script/auto/deploy
				    sh docker_build.sh
				"""
         }
      }
      
      //推送Docker镜像到远程仓库
      stage('Push Remote Repositry') {
         steps {
            sh """
				    cd /var/jenkins_home/script/auto/deploy
				    sh docker_push.sh
				"""
         }
      }
      
      //部署代码
      stage('Deploy') {
         steps {
            sshagent(['feb51b70-****-45a5-a69b-319e41f7f146']) {
                    sh """
                    ssh -p 16022 root@**** "sh /home/bfreeman/script/auto/deploy/docker_pull.sh"
                     ssh -p 16022 root@**** "sh /home/bfreeman/script/auto/deploy/docker_run.sh"
                    """
                }
         }
      }
      
      //推送代码
      stage('Push Code') {
         steps {
            //代码推送到master操作
			sshagent(['0baad767-****-4122-8639-be2559a124ac']) {
                sh """
                    git config --global user.email "******"
                    git config --global user.name "******"
				    git commit -m "header changed ci commit automatically" || true
                    git push origin HEAD:refs/heads/master
				"""
			}
         }
         
      }
      
   }
   
}

参考

https://jenkins.io/zh/doc/

https://www.jianshu.com/nb/30544461