CI/CD详解 & Jenkins 介绍与实战

本文大纲概览:

  • CI/CD 概念与工作流程 详解
  • Jenkins安装、启动、初始化配置
  • 使用Jenkins部署github上的 xzll-im 微服务项目
    • freestyle方式
    • push代码后自动部署
    • pipeline方式部署
    • pipeline + push 方式

本文实操过程中,相关工具版本信息如下:

  • centos版本: CentOS Linux release 7.9.2009 (Core)
  • jdk: jdk-11.0.22
  • maven: 3.8.8
  • git: 1.8.3.1
  • jenkins 2.252
  • docker: 26.1.4
  • docker-compose: v2.27.2
  • 其它项目中使用到的中间件版本请见docekr-compose.yml 中的镜像版本,戳这里

什么是 持续集成(CI)& 持续交付(CD)?

大白话: 对于CI/CD的理解,可能每个人都不一样(一千个人有一千个哈姆雷特!😂),我理解他其实就是一种更加自动化的工作模式,使得开发、测试、运维之间的沟通成本降低,从之前的手动 编译、打包、构建、测试到现在的自动化、流程化去执行这些步骤,从而更加快速,及时发现问题,最终实现:加快软件迭代周期,提高交付速度与质量 的目的!

CI(持续集成) 介绍

CI全称: (Continuous Intergration)

持续集成的重点在于 构建编译测试 (这里更多的指通过脚本和自动化项目来进行的自动化测试),开发人员每天要提交很多次代码到分支,在分支合并到主干前,需要通过编译和测试识别出问题。持续集成的流程就是通过自动化的构建(主要是构建编译、自动化测试)来验证,从而尽早地发现集成错误。

CI(持续集成)的工作流程

  1. CI初始化:包括但不限于(CI服务的安装&启动,脚本、任务定义与配置,webhook设置,凭证设置,系统配置,插件等等)
  2. 开发人员从代码库中获取最新代码(pull)。
  3. 开发人员在本地进行开发和测试。
  4. 开发人员将代码提交(commit & push)到代码仓库(github、 gitlib)
  5. 通过回调方式通知CI服务器,ci服务器自动拉取最新代码并触发构建任务。
  6. 构建(build)-> 自动化测试(test)-> 反馈结果(result)
  7. 将自动化测试的结果反馈给开发人员(钉钉邮件短信等方式),如果测试失败,开发人员会及时修复代码并重新提交
  8. 构建和测试成功后,代码可以部署到进一步的测试环境或生产环境中

集成工具

集成工具有很多种(我所接触的主要还是jenkinsgitlib),包括但不限于以下:

  • Jenkins (常见):一个开源的自动化服务器,支持复杂的构建、测试和部署流水线。
  • GitLab CI/CD (常见):GitLab 内置的 CI/CD 工具,支持从代码提交到生产部署的全流程自动化。
  • CircleCI:一个基于云的持续集成和持续交付平台,支持多种编程语言和构建环境。
  • Travis CI:一个基于云的持续集成服务,特别适用于 GitHub 项目。
  • Spinnaker:一个开源的持续交付平台,支持多云环境的自动化部署

持续集成的好处

  • 提高代码质量:通过频繁的自动化测试和代码质量检查,可以早期发现并修复错误。
  • 加快交付速度:自动化构建和测试减少了手动操作的时间,可以提高开发速度从而加快交付。
  • 提高团队协作效率:开发人员可以更快地得到反馈,后期与测试人员协作更加顺畅。
  • 总之概括起来就是一句话: 省去人工操作,push后自动构建、测试(这里指自动化测试)、反馈,从而尽早的在开发环境发现代码问题并修复

CI 流程图

下边是持续集成(CI) 的流程图:

知道了CI , 那么CD又是指啥呢?接着看

CD(持续交付&持续部署) 介绍

CD 有两个意思

  1. 一是 持续交付(Continuous Delivery)
  2. 另一个是持续部署(Continuous Deployment)

说一句:其实做到持续交付就已经很不错了,持续部署听听就行了,别当真!为什么这么说? 往下看。

何为持续交付?

持续交付指的是将产品尽可能快的发布上线的过程。持续交付是在持续集成基础上的扩展 ,也就是说在成功通过dev环境的自动化部署与测试之后,为了尽快上线我们还需要自动化发布fat或者uat环境(如果需要的话),整个流程实现后,根据实际需要,可以人工控制是否发布prod环境。一般在公司都是开发环境(dev)、测试环境(fat)、预发布环境(uat)和正式生产环境(prod),如果代码在预发布环境测试通过,那么就可通过 手动方式 部署生产环境,从而尽可能的实现产品快速迭代上线。

持续交付的几个关键因素:

  • 自动化构建和测试: 与持续集成(CI)类似,持续交付(CD)依赖于自动化的构建和测试,以确保每次代码更改都能被快速验证。
  • 自动化部署: 持续交付要求自动化部署流程,这意味着每次代码更改都可以自动部署到不同的环境中(如开发、测试、预生产)。
  • 环境一致性: 确保所有环境(从开发到生产)的一致性,以减少部署问题。
  • 持续反馈: 提供持续反馈,确保开发团队及时了解相应服务在不同环境中的状态和问题。
  • 手动发布控制 : 尽管大部分部署过程都是自动化的,但 持续交付 通常 保留手动发布的控制权。发布到生产环境的最终决定由相关人员评估,可以的话再通过手动的方式来发布生产环境

持续交付流程图

何为持续部署?

持续部署是持续交付的进一步扩展。在持续部署中,所有的代码更改在通过自动化测试和验证后,都会自动部署到生产环境中。与持续交付不同,持续部署不通过手动发布来发布生产环境,而是自动发布生产环境。一般来说,非生产环境的持续部署基本都能实现。但生产环境的持续部署并不是每个企业都能做到 ,主要原因是受限于各种系统功能依赖、自动化测试不完善等因素,自动化方式部署到生产,将可能造成严重生产事故。 所以实际我觉得没几个企业这样做。能做到持续交付就已经很不错了。生产环境的部署 一定要认为控制。持续部署 听听就行了,不要玩火呀! 哈哈!😂😂😂😂😂😂😂😂😂😂😂😂

持续部署的关键要素:

  • 自动化构建、测试和部署: 持续部署依赖于完全自动化的构建、测试和部署流程,以确保每次代码更改都能被快速、可靠地部署到生产环境中。
  • 强大的精准的测试覆盖: 持续部署需要非常高的测试覆盖率,包括单元测试、集成测试、端到端测试等,以确保所有代码更改都能被全面验证。
  • 快速回滚机制: 当新代码引入问题时,必须有快速回滚的机制,以确保生产环境的稳定性。
  • 持续监控: 持续监控生产环境中的应用性能和健康状态,确保及时发现和解决问题

注意: 无论是持续集成、持续交付还是持续部署,如果要想实现,都离不开CI服务器!

ok到这里你对CI/CD有自己的理解了吗?下边我们来学习一个非常重要且常见功能丰富的CI/CD 工具 : Jenkins ! 想玩CI/CD以及再往大一点的DevOps? 那么首先玩转Jnekins是必不可少的。换句话说,其实Jenkins是CI/CD、DevOps 这些概念的 其中一个重要 "实现"


介绍完CI/CD 下边我们开始学习Jenkins

Jenkins 安装、启动、初始化

以下说明是我选择哪种方式安装jenkins的过程记录,这里也记录下来,做个备忘。

  • 说明1:为什么不使用docker安装jenkins? 其实我也想使用docker安装,但是当我从docker hub中找到对应的镜像进行下载时,发现要么是下载不下来镜像,要么是下载下来的镜像 运行时的jenkins版本过低,导致一些插件安装报错,使得我很恼火。
    • 下载不下来镜像: 到docker hub 去找到对应镜像 ,戳此进: jenkins 的 docker hub 执行docker pull 镜像 发现下载失败,都是超时。。。。

    • 版本过低插件安装不了: ,后来终于有两个镜像可以下载了 就是这个镜像:jenkins/jenkins:lts-jdk11jenkins/jenkins:latest镜像,但是却提示jenkins版本过低导致一些插件安装不了,报错如下:因为这个搞了半天,其实最主要原因可能就是欺负我的虚拟机没开vpn,哎 手头紧,暂时先不开了毕竟有点小贵,所以就决定弃启用docker安装jenkins, 我用war或RPM也行呀,真是的,😄

  • 说明2:为什么安装jenkins 2.252版本?
    • 由于在使用jenkins过程中我需要一些最新插件,而 LTS(官方长期支持版本)版本缺少对最新插件的支持,所以这里弃用LTS版本而使用较新的版本来安装部署学习,我使用的是: 2.452 (生产环境还是建议使用LTS版本来安装启动jenkins)
  • 说明3:为什么选择war包安装jenkins?
    • 首先第一原因是因为上边的说明1才迫使我使用war安装jenkins,另外因为我是centos7,所以其实我本应该使用RPM软件包方式安装jenkins,对应的版本是redhat的 LTS Release 的 redhat-stable(已发布的LTS版本)来安装jenkins 下边的引导页面戳这里,但是因为遇到了一些问题主要还是网络问题(还是欺负我虚拟机没vpn 😂😂 再欺负我真要买Strong vpn了啊 哈哈),所以我不用此方式,如果网络好(能fq)可以使用,(安装指南戳这里
    • 最终:我选择使用war包安装jenkins ,因为经过实践我发现此方式比较好用不用担心被q的问题,选择war安装的话点击下边这个LTS Release:war-stable 然后找到我要安装的版本(注意在安装此版本之前,我安装过较低版本 (LTS版本的2.361)但是我发现有些插件不能用,必须高版本才行所以这里选择较高的一个版本 2.452,ok接下来开干

ps: 其实某些场合下,docker部署不一定是最优方案。不一定要执着于所有东西都往docker堆,当然如果是开发测试环境,当我没说。

下载war、启动并初始化 jenkins

有了上边的铺垫,接下来我们首先找到2.452版本的war包:

下载war包

之后使用命令下载war包:

bash 复制代码
wget https://mirrors.jenkins-ci.org/war/2.452/jenkins.war --no-check-certificate

编写启动脚本 来启动jenkins

ok从上边截图可知下载成功,接下来编写jenkins启动脚本: 完整jenkins启动脚本在这:

bash 复制代码
#!/bin/bash

args=$1
# 注意修改jenkinswar包的目录
jenkins_war_path="/usr/local/soft_hzz/jenkins"
# jenkins开放端口
jenkins_http_port="8079"
# java安装路径
java_home="/usr/lib/jvm/java-11-openjdk-11.0.22.0.7-1.el7_9.x86_64"
# 日志文件路径
jenkins_log_path="/tmp/data/logs/jenkins.log"

function isRunning(){
    local jenkinsPID=$(ps -ef | grep jenkins.war | grep -v grep | awk '{print $2}')
    if [ -z ${jenkinsPID} ]; then
        echo "0"
    else
        echo ${jenkinsPID}
    fi
}

# 停止jenkins
function stop(){
    local runFlag=$(isRunning)
    if [ ${runFlag} -eq "0" ]; then
        echo "Jenkins is already stopped."
    else
        kill -9 ${runFlag}
        echo "Stop Jenkins success."
    fi
}

# 启动jenkins
function start(){
    local runFlag=$(isRunning)
    echo "${runFlag}"
    if [ ${runFlag} -eq "0" ]; then
        # nohup ${java_home}/bin/java -jar ${jenkins_war_path}/jenkins.war --httpPort=${jenkins_http_port} > ${jenkins_log_path} 2>&1 &

      # 对jenkins内存大小进行限制 否则总是超内存 被系统kill
        nohup ${java_home}/bin/java -Xms512m -Xmx2g -jar ${jenkins_war_path}/jenkins.war --httpPort=${jenkins_http_port} > ${jenkins_log_path} 2>&1 &
        if [ $? -eq 0 ]; then
            echo "Start Jenkins success."
            exit
        else
            echo "Start Jenkins fail."
        fi
    else
        echo "Jenkins is running now."
    fi
}

# 重启jenkins
function restart(){
    local runFlag=$(isRunning)
    if [ ${runFlag} -eq "0" ]; then
        echo "Jenkins is already stopped."
        exit
    else
        stop
        start
        echo "Restart Jenkins success."
    fi
}

# 根据输入的参数执行不同的动作
# 参数不能为空
if [ -z ${args} ]; then
    echo "Arg can not be null."
    exit
# 参数个数必须为1个
elif [ $# -ne 1 ]; then
    echo "Only one arg is required: start|stop|restart"
# 参数为start时启动jenkins
elif [ ${args} = "start" ]; then
    start
# 参数为stop时停止jenkins
elif [ ${args} = "stop" ]; then
    stop
# 参数为restart时重启jenkins
elif [ ${args} = "restart" ]; then
    restart
else
    echo "One of following args is required: start|stop|restart"
    exit 0
fi

接下来启动:

初始化jenkins

好,现在我们通过 172.30.128.65:8079访问jenkins,首次访问会出现下边这个解锁jenkins的页面,按提示找到密码并填进去点击继续就行了:

设置初始密码

将启动日志中的密码复制: 填入下边的管理员密码中:

安装jenkins推荐的插件

点击继续,之后,jenkins会推荐你安装一些插件,这里建议都安装上以免后续还得再手动安装,如下: 插件有点多需要等待一会:

在这里替换插件源(注意 : 替换后一定要重启jenkins

创建管理账号

之后提示你创建个管理员账号: 然后填写上你的jenkins地址:

完成后查看安装的插件

看到下边这个,代表 初始化工作就完成了: 在这里你可以看到你初始化时候安装的那些插件:

一些插件介绍:

插件名 作用
Pipeline 流水线部署项目
Role-based Authorization Strategy 提供了一种基于角色(Role)的用户权限管理策略,支持创建global角色、Project角色、Slave角色,以及给用户分配这些角色。这款插件是最常用的Jenkins权限策略和管理插件
Git 支持使用Github、GitLab、Gerrit等系统管理代码仓库,创建普通job时会用到
Gitee Gitee Jenkins Plugin 是Gitee基于 GitLab Plugin 开发的 Jenkins 插件。用于配置 Jenkins 触发器,接受Gitee平台发送的 WebHook 触发 Jenkins 进行自动化持续集成或持续部署,并可将构建状态反馈回Gitee平台。
Git Parameter 可用于把git的tag branch当作构建参数传进来,方便使用branch构建
Extended Choice Parameter 参数化构建
Maven Integration 这个插件为Maven 2 / 3项目提供了高级集成功能
SonarQube Scanner 代码扫描
Email Extension 扩展了发送告警邮件的控制力度。可以定义邮件触发器、邮件内容、收件人
Workspace Cleanup 每次build之前删除workspace目录下指定的文件
Monitoring 监控Jenkins节点的CPU、系统负载、平均响应时间和内存使用
Build Monitor View 将Jenkins项目以一块看板的形式呈现
ThinBackup 备份与恢复
jacoco 单元测试覆盖率
Generic Webhook Trigger webohook

使用Jenkins部署项目

这里声明个前提:我使用的代码仓库是github,gitlib、gitee的操作略有不同。

全局工具配置

非pipeline的Java项目如果要构建成功,全局工具的环境需要配置好

  • 配置入口:Manage Jenkins->Global Tool Configuration
  • 配置方式:指定服务器上已经安装好的服务位置(不需要勾选自动安装)
  • 配置前提:服务器已经安装好jdk、maven、git

首先找到我的git、maven、jdk的安装路径,如下:

bash 复制代码
mvn -version
which git 
echo $JAVA_HOME

然后点击这个进行工具添加:

添加git、maven、jdk

添加后点击应用并保存。

方式一、 使用freestyle方式部署项目

新建freestyle类型的任务

首先我们需要建一个任务,点击新建item:

然后定义任务: 简单描述一下此任务干的事情,以及勾选上github项目并填入url,如下:

配置源码管理(这里使用ssh方式与github认证)

之后往下拉来到源码管理 填入仓库地址(ssh的话是git协议),之后点击添加,点击jenkins弹出认证设置 我这里选择ssh方式认证:

之后可以填写你想要的认证方式(我这里使用ssh认证,也就是说把你github上公钥对应的私钥(私钥一般在你本地使用 cat ~/.ssh/id_rsa 命令查看),粘到下边的private key中) 当然如果没有生成过或者删除了,那么使用下边方式重新生成,并将公钥粘到github,步骤如下:

bash 复制代码
# 注意在生成前最好是先删除一下旧的

# 删除公钥和私钥
rm -f ~/.ssh/id_rsa ~/.ssh/id_rsa.pub
# 清除之前的主机秘钥
rm -f ~/.ssh/known_hosts

# 重新生成
ssh-keygen -t rsa -b 4096 -C "h163361631@163.com"

# 查看私钥
cat ~/.ssh/id_rsa

# 查看公钥
`cat ~/.ssh/id_rsa.pub`

# 测试与github的连接是否正常(在github配完公钥之后操作)
`ssh -T git@github.com`

将公钥粘到github:

在将公钥配到github后可以使用命令ssh -T git@github.com 来测试是否连接成功,如果输出: Hi xxx! You've successfully authenticated, but GitHub does not provide shell access. 则说明ssh配置没问题了。

选择 SSH Username with private key 认证方式(有好几种认证方式比如 用户名密码,access token, ssh等 我们这里使用ssh方式),并使用cat ~/.ssh/id_rsa找到私钥并粘到下图的private key中去:

之后我们填入仓库地址,以及刚刚新加的认证方式,以及要部署的分支: 然后在构建环境这里,选择jdk版本:

编写你的shell脚本(用来定义你如何构建&启动等动作)

编写shell脚本,定义启动的一些动作: shell脚本内容:

bash 复制代码
#!/bin/bash

# 使用freestyle方式启动xzll-im项目时的shell脚本,此脚本在 jenkins的  build steps处填写。

log() {
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}

exit_on_error() {
  if [ $? -ne 0 ]; then
    log "$1"
    exit 1
  fi
}

log ">>>>>>>>>>>> 开始部署 >>>>>>>>>>>>>"


# 确保 docker-compose 目录存在 不存在则创建
COMPOSE_DIR="/usr/local/soft_hzz/xzll-im/jar-file/jenkins_way_build_docker_compose/"
if [ ! -d "$COMPOSE_DIR" ]; then
  log ">>>>>>>>>>>> 目录不存在,创建目录 $COMPOSE_DIR >>>>>>>>>>>>>"
  mkdir -p "$COMPOSE_DIR" || { log "Failed to create directory $COMPOSE_DIR"; exit 1; }
fi

# 进入 docker-compose 目录
log ">>>>>>>>>>>> 进入到 docker-compose 目录中 >>>>>>>>>>>>>"
cd "$COMPOSE_DIR" || { log "Failed to enter directory $COMPOSE_DIR"; exit 1; }



# 如果项目目录不存在,则执行 clone,否则执行 pull
if [ ! -d "xzll-im" ]; then
  log ">>>>>>>>>>>> 项目目录不存在,执行 git clone >>>>>>>>>>>>>"
  git clone git@github.com:598572/xzll-im.git || { log "Git clone failed"; exit 1; }
else
  log ">>>>>>>>>>>> 项目目录已存在,进入目录并执行 git pull >>>>>>>>>>>>>"
  cd xzll-im/ || { log "Failed to enter directory"; exit 1; }
  git pull || { log "Git pull failed"; exit 1; }
  cd ..
fi

# 进入项目根目录
log ">>>>>>>>>>>> 进入项目根目录 >>>>>>>>>>>>>"
cd xzll-im/ || { log "Failed to enter project directory"; exit 1; }

git fetch

git checkout jenkins_build_20240709

# 开始打包
log ">>>>>>>>>>>> 开始打包 >>>>>>>>>>>>>"

mvn clean package -P prod || { log "Maven build failed"; exit 1; }

log ">>>>>>>>>>>> 打包完成 >>>>>>>>>>>>>"

# 确保 Docker 构建上下文包含所有 JAR 文件
declare -A JAR_FILES=(
  ["im-gateway/target/im-gateway.jar"]="im-gateway/src/main/resources"
  ["im-connect/im-connect-service/target/im-connect-service.jar"]="im-connect/im-connect-service/src/main/resources"
  ["im-auth/target/im-auth.jar"]="im-auth/src/main/resources"
  ["im-business/im-business-service/target/im-business-service.jar"]="im-business/im-business-service/src/main/resources"
  ["im-console/im-console-service/target/im-console-service.jar"]="im-console/im-console-service/src/main/resources"
)

for JAR_FILE in "${!JAR_FILES[@]}"; do
  TARGET_DIR="${JAR_FILES[$JAR_FILE]}"
  log ">>>>>>>>>>>> 复制 $JAR_FILE 到 $TARGET_DIR >>>>>>>>>>>>>"
  cp "$JAR_FILE" "$TARGET_DIR" || { log "Failed to copy $JAR_FILE to $TARGET_DIR"; exit 1; }
done


cp docker-compose.yml ../

# 返回 docker-compose 目录
log ">>>>>>>>>>>> 返回到 docker-compose 目录 >>>>>>>>>>>>>"
cd .. || { log "Failed to return to docker-compose directory"; exit 1; }




# 停止所有 docker-compose 运行的容器
log ">>>>>>>>>>>> 停止所有 docker-compose 运行的容器 >>>>>>>>>>>>>"
docker-compose down || { log "Docker-compose down failed"; exit 1; }

# 构建镜像并启动容器
log ">>>>>>>>>>>> 构建镜像并启动容器 >>>>>>>>>>>>>"
docker-compose up -d --build || { log "Docker-compose up failed"; exit 1; }

log ">>>>>>>>>>>> 部署结束 >>>>>>>>>>>>>"

# 清理 Docker 相关的垃圾
log ">>>>>>>>>>>> 清理 Docker 相关的垃圾 >>>>>>>>>>>>>"
docker system prune -f --volumes || { log "Docker system prune failed"; exit 1; }

log ">>>>>>>>>>>> 清理完成 >>>>>>>>>>>>>"

部署xzll-im项目

之后我们在这里点击build now开始构建并启动: 部署结果:

看到Finished: SUCCESS说明构建成功了(注意构建成功不等于微服务相关项目启动成功哦) 看一下启动情况(可知都正常启动了): 当然最好看下各个服务的日志,这样才能确保真的启动成功(以im-connect服务为例):

以上就是使用freestyle方式部署github项目的流程。挺简单的,但是大部分场景下不使用此方式,需要手动服务多的话比较费时费力。

方式二、 push代码后自动部署

一般在dev环境(测试环境一般不需要push即部署,因为那样影响测试稳定性)可能需要每次push后都需要让最新代码生效(也就是希望每次push后部署一遍,而如果这个工作靠人去完成的话服务多的话将会很累很烦,所以jenkins提供了个回调接口,也就是在github中配上jenkins的回调地址,当有某些事件时(一般是push事件)那么就回调jenkins 然后jenkins去进行自动部署)下边我们就看下如何 在push后让jenkins自动部署?

整个流程简单梳理为以下步骤:

  1. 首先是配置github和jenkins (详情见下边)
  2. 开发者push代码到github仓库
  3. github通过此项目设置的webhook 告知(回调)jenkins接口,哎,有push事件了啊,你看你该怎么处理。
  4. jenkins收到事件,进行处理(也就是部署项目)

在github设置webhook

webhook的url其实就是 jenkins提供的回调接口的地址

进入项目,找到setting , 找到webhooks 并点击 add webhook 如下:

注意: 由于我的虚拟机是在局域网,github在外网访问不到我的虚拟机ip, 所以需要做一下 内网穿透 ,我这里使用内网穿透 工具为 :灵曜 , 一个好用、配置简单的内网穿透工具, 戳此进官网 www.http01.cn/

内网穿透配置:

首先安装客户端(linux版本):

直粘贴脚本然后新建个目录执行就行,脚本如下: 执行脚本 如下: 完事你会在客户端这里看到你机器的信息: 之后编辑你的隧道:

暂时我先配成8079端口,也就是说通过此隧道只能访问jenkins服务(8079是jenkins端口),后续搞个nginx自己转发一下就更好了。

配置之后你可以看到分配给你的公网域名以及过期时间等等信息:

注意:(灵曜支持域名绑定,也就是说可以将你的腾讯云,阿里云域名绑定到你的隧道,并且可以设置https ,从而实现https访问方式访问你的局域网上的机器, 这里我为了简便就不做绑定了也不做https了 ,而是使用http 方式)

ok现在我们就将此公网域名配到github。

配置webhook 以及触发回调的事件(这里为push代码时)

在github配置webhook地址:(注意接口url 是 github-webhook,这是固定的) 设置好webhook后,点击add webhook,就可以看到已经添加的webhook了,注意,一定要保证图标是小对勾✅ 时说明github能访问通jenkins ,如果是红色叹号说明不行这时需要排查你的jenkins是否启动成功以及内网穿透是否好使,(当然,也可以从下边提示语知道成功与否)

之后我们还需要在github干一件事,就是生成access token ,从而让jenkins访问到github 服务(注意access token和 ssh方式不同,他可以更细粒度的控制用户的操作权限)。

接下来生成access token : 起个名字并设置此access token的相关权限: 之后点击最底部的生成,就看到下边的界面了:

(注意一定要粘贴保存起来最好是你本地防止遗忘,因为下次再进来就不展示这个token了)

ok到这一步,github设置好了,接下来就是设置jenkins了

配置jenkins

在jenkins 配置github server信息:

在jenkins的System config 添加github服务器设置:

注意想要配置这一步,你一定要安装以下插件: 配置github server:

新增access token凭证

具体如下截图: 选择刚创建的凭证并 连接测试

修改 xzll-im-freestyle-project 项目的触发器构建环境

修改xzll-im-freestyle-project项目对应的配置,改动点如下: 到此github和jenkins相关的配置就配完了,点击应用。接下来我们push代码到github仓库,试试能不能触发jenkins自动部署!

push代码测试一下jenkins能不能自动部署

随便改点东西,push上去:

观察日志,收到github回调通知

查看jenkins日志发现 收到了github的回调,如下:

观察并验证启动结果

查看jenkins,可以看到32号任务正在执行:

看一下控制台的日志:

看一下各容器的状态以及随便找一个服务看看日志: 可以看出,各容器启动正常,日志也正常都起来了。

ok到这里,就实现了 通过push 触发jenkins自动构建 的目的了。当然按照CI的概念来说,你还需要在部署后自动运行自动化测试脚本,以及将测试的结果通知给开发人员以便进行问题修复(如果有的话)。

方式三、 pipleine构建

首先你要确保这一堆pipeline插件一键安装了,想使用pipeline功能,相关插件是不可少的。

官方中文文档: www.jenkins.io/zh/doc/book...

pipeline简介与使用方式

pipeline是什么?

注:pipeline使用中文可以理解为流水线、管道。

Pipeline 是Jenkins 2.X 的最核心的特性,帮助Jenkins 实现从CI 到 CD 与 DevOps的转变。 Pipeline 是一组插件,让jenkins 可以实现持续交付管道的落地和实施。持续交付管道是将软件从版本控制阶段到交付给用户/客户的完整过程的自动化表现。

大白话说其实Pipeline就是一个:逻辑上的管道,在该管道中 你可以通过脚本 定义一系列的动作,pipeline会根据你的定义,自动化的完成整个构建、测试、交付等过程,从而实现(CD)持续交付的目的。

如何创建pipeline?

创建并定义pipeline有两种方式(这里我们简单描述下这两种方式 ,下边会进行实战演示),如下:

  1. 直接在 Jenkins Web 界面输入脚本
  2. 编写 Jenkinsfile 文件并将文件存放到项目根目录。 何为Jenkinsfile? 对Jenkins 流水线的定义可以被写在一个文本文件中 (此文件称为Jenkinsfile),该文件可以放在项目的根目录下,从而将 流水线的定义作为应用程序的一部分,像其他代码一样进行版本控制和代码审查。

在哪里配置pipeline?

  1. 将流水线定义放在名为 Jenkinsfile 的文件中,存储在项目的根目录。使用 Jenkins 的 "Pipeline from SCM" 功能从版本控制系统中读取 Jenkinsfile 并执行流水线。
  2. 直接在 Jenkins 的 Web 界面中编写和配置流水线脚本。在创建或配置一个 Pipeline 项目时,可以在 "Pipeline" 部分选择 "Pipeline script" 并直接输入脚本。

pipeline 的两种书写方式

分别是:

  1. Declarative Pipeline(声明式流水线)
  2. Scripted Pipeline(脚本化流水线)
声明式流水线(Declarative Pipeline)

声明式流水线通过结构化、简单化的方式来定义流水线。它使用起来更为简单直观,使得新用户更容易上手。

声明式流水线 示例(语法看不懂没关系,下边会有语法介绍):

groovy 复制代码
pipeline {
    agent any

    stages {
        stage('构建') {
            steps {
                echo '构建阶段...'
            }
        }
        stage('测试') {
            steps {
                echo '测试阶段...'
            }
        }
        stage('交付') {
            steps {
                echo '交付阶段...'
            }
        }
    }
}
脚本化流水线(Scripted Pipeline)

脚本化流水线提供了更为灵活和强大的语法,适用于需要复杂逻辑的场景。它完全基于 Groovy 脚本,可以使用 Groovy 的所有特性。

脚本化流水线 示例

groovy 复制代码
node {
    stage('构建') {
        echo '构建中...'
    }
    stage('测试') {
        echo '测试中...'
    }
    stage('交付') {
        echo '交付中...'
    }
}
两者对比

Jenkinsfile能使用两种语法进行编写 - Declarative Pipeline声明式和Scripted Pipeline脚本化,两者都支持建立连续输送的Pipeline。这两种语法对比如下:

  • 共同点: 两者都是pipeline代码的持久实现,都能够使用pipeline内置的插件或者插件提供的steps,两者都可以利用共享库扩展。

  • 区别: 两者不同之处在于语法和灵活性。Declarative pipeline对用户来说,语法更严格,有固定的组织结构,更容易生成代码段,使其成为用户更理想的选择。

但是Scripted pipeline更加灵活,因为Groovy本身只能对结构和语法进行限制,对于更复杂的pipeline来说,用户可以根据自己的业务进行灵活的实现和扩展

建议选择哪种语法? 通常建议使用Declarative Pipeline的方式进行编写,从jenkins社区的动向来看,很明显这种语法结构也会是未来的趋势。声明式流水线比 Jenkins 脚本化流水线的特性:

  • 相比脚本化的流水线语法,它提供更丰富的语法特性
  • 是为了使编写和读取流水线代码更容易而设计的

注:在下边实战时,有关pipeline的脚本也是基于Declarative Pipeline(声明式流水线)的方式进行编写。

pipeline语法

声明式流水线 的基本语法和表达式遵循 groovy语法,但是有以下例外

  • 声明式pipeline : 必须包含在固定格式的pipeline{} 块内,比如:

    arduino 复制代码
    pipeline {
        /* insert Declarative Pipeline here */
    }
    • 每个声明语句必须独立一行, 行尾无需使用分号
    • 块只能由阶段(stages{})、指令、步骤 (steps{})或赋值语句组成
    • 属性引用语句被视为无参数方法调用, 如input()
  • agent :指定在哪个节点上执行流水线。可以是任意节点(agent any),特定标签的节点(agent { label 'my-label' }),或 Docker 容器(agent { docker { image 'maven:3-alpine' } })。

  • stages : 定义流水线的不同阶段,每个阶段包含多个步骤。

  • steps : 每个阶段中执行的具体操作,如构建、测试和部署等。

  • post :在流水线的各个阶段结束后执行的操作,例如清理、发送通知等

语法很多,上边只列了一些比较重要的。

以上是常用的一些语法,,更详细的语法请参见官方文档: 英文版戳这里中文版戳这里,中文版本翻译的有点那啥,将就看吧,英文好的直接看英文版官方文档。

声明式pipeline 语法demo

由于本文准备使用声明式语法来构建JenkinsFile 所以在实战之前,写个简单的demo来熟悉下,声明式 pipeline内容 如下:

typescript 复制代码
// Jenkinsfile (Declarative Pipeline)
pipeline { 
    agent any 
    stages {
        stage('Build') { 
            steps { 
                sh 'make' 
                echo "hello world"
                script {
                    def browsers = ['chrome', 'firefox']
                    for (int i = 0; i < browsers.size(); ++i) {
                        echo "Testing the ${browsers[i]} browser"
                    }
                }
            }
        }
        stage('Test'){
            steps {
                sh 'make check'
                junit 'reports/**/*.xml' 
            }
        }
        stage('Deploy') {
            steps {
                sh 'make publish'
            }
        }
    }
}

下边是解释:

  • pipeline是声明式流水线的一种特定语法,他定义了包含执行整个流水线的所有内容和指令的 "block" 。
  • agent 是声明式流水线的一种特定语法,指示 Jenkins 为整个流水线分配一个执行器(在 Jenkins 环境中的任何可用代理/节点上)和工作区。一般用作于指定在哪个节点上构建,如果不指定就写any表示任意节点。
  • 定义 "Build" 、 "Test" 、 "Deploy" 三个阶段,每个阶段内部执行不同的步骤。
  • stage 是一个描述 stage of this Pipeline的语法块。stage 块定义了在整个流水线的执行任务的概念性地不同的子集(比如 "Build", "Test" 和 "Deploy" 阶段), 它被许多插件用于可视化 或Jenkins流水线目前的 状态/进展,stage 块是可选的。
  • steps 是声明式流水线的一种特定语法,它描述了在这个 stage 中要运行的步骤。
  • sh 是一个执行给定的shell命令的流水线。
  • echo 写一个简单的字符串到控制台输出。
  • junit 是另一个聚合测试报告的流水线。
  • 注意stage括号的值表示阶段名称,值内容不是固定的,根据需要自定义即可。

在有了基本的认识后,接下来就是对我的xzll-im进行 Jenkins pipeline实战了。


使用pipeline的方式,部署我的xzll-im项目

注意:本实战演示中使用的是声明式流水线(Declarative Pipeline)方式编写的pipeline脚本,且 web控制台方式项目根目录创建JenkinsFile方式 都会演示。

在Jenkins web页面配置声明式流水线脚本

首先创建个pipeline类型的任务:

然后点击配置,如下: 进入配置页面后,拉到最下边,在定义这个栏目中,选择 Pipeline Script

注意,这里有两种类型,一个是 我们现在选择的 Pipeline script,一个是Pipeline script from SCM。

Pipeline script :代表就从下边的脚本输入框中寻找 pipeline脚本并执行

Pipeline script from SCM: 则代表从项目根目录中寻找 JenkinsFile中的内容作为 pipeline脚本,当是此模式时 ,无需在jenkins web中配置脚本了。

因为本小节演示的是jenkins web控制台配置 声明式pipeline脚本,所以在这里就选择 Pipeline script了,如下:

声明式脚本内容是我提前写好的(其实就是将方式一的脚本 改一下(但增加了参数选择,从而在构建时可以选择分支),改成遵循声明式pipeline语法的shell,本质上主流程还是不变的),声明式pipeline脚本如下:(每个步骤都有注释,这里不过多解释了,详情看注释好了

groovy 复制代码
pipeline {
    agent any

    parameters {
        choice(
            name: 'GIT_BRANCH',
            choices: ['main', 'dev01', 'jenkins_build_20240709', 'docker_compose_way_bak','group_chat_dev_20240623'], 
            description: '选择Git分支' // 选择Git分支
        )
    }

    environment {
        COMPOSE_DIR = "/usr/local/soft_hzz/xzll-im/jar-file/jenkins_way_build_docker_compose/" // Docker Compose目录
        GIT_REPO = "git@github.com:598572/xzll-im.git" // Git仓库地址
        GIT_BRANCH = "${params.GIT_BRANCH}" // 选择的Git分支
    }

    stages {
        stage('Prepare Environment') {
            steps {
                script {
                    log("开始部署") // 日志记录

                    if (!fileExists(COMPOSE_DIR)) {
                        log("目录不存在,创建目录 $COMPOSE_DIR")
                        sh "mkdir -p $COMPOSE_DIR" // 创建Docker Compose目录
                        exit_on_error("Failed to create directory $COMPOSE_DIR")
                    }

                    log("进入到 docker-compose 目录中")
                    dir(COMPOSE_DIR) {} // 进入Docker Compose目录
                }
            }
        }

        stage('Clone or Update Repo') {
            steps {
                script {
                    dir(COMPOSE_DIR) {
                        if (!fileExists('xzll-im')) {
                            log("项目目录不存在,执行 git clone")
                            sh "git clone $GIT_REPO" // 克隆Git仓库
                            exit_on_error("Git clone failed")
                        } else {
                            log("项目目录已存在,进入目录并执行 git pull")
                            dir('xzll-im') {
                                sh "git pull" // 更新Git仓库
                                exit_on_error("Git pull failed")
                            }
                        }
                    }
                }
            }
        }

        stage('Checkout Branch') {
            steps {
                script {
                    dir("$COMPOSE_DIR/xzll-im") {
                        log("进入项目根目录")
                        sh "git fetch" // 拉取最新的分支信息
                        sh "git checkout $GIT_BRANCH" // 切换到选择的分支
                        exit_on_error("Failed to checkout branch")
                    }
                }
            }
        }

        stage('Build Project') {
            steps {
                script {
                    dir("$COMPOSE_DIR/xzll-im") {
                        log("开始打包")
                        sh "mvn clean package -P test" // 使用Maven打包项目
                        exit_on_error("Maven build failed")
                        log("打包完成")
                    }
                }
            }
        }

        stage('Copy JAR Files') {
            steps {
                script {
                    def jarFiles = [
                        'im-gateway/target/im-gateway.jar': 'im-gateway/src/main/resources',
                        'im-connect/im-connect-service/target/im-connect-service.jar': 'im-connect/im-connect-service/src/main/resources',
                        'im-auth/target/im-auth.jar': 'im-auth/src/main/resources',
                        'im-business/im-business-service/target/im-business-service.jar': 'im-business/im-business-service/src/main/resources',
                        'im-console/im-console-service/target/im-console-service.jar': 'im-console/im-console-service/src/main/resources'
                    ]
                    
                    dir("$COMPOSE_DIR/xzll-im") {
                        jarFiles.each { jar, targetDir ->
                            log("复制 $jar 到 $targetDir")
                            sh "cp $jar $targetDir" // 复制JAR文件到相应目录
                            exit_on_error("Failed to copy $jar to $targetDir")
                        }
                        sh "cp docker-compose.yml ../" // 复制docker-compose.yml文件
                    }
                }
            }
        }

        stage('Deploy with Docker Compose') {
            steps {
                script {
                    dir(COMPOSE_DIR) {
                        log("返回到 docker-compose 目录")
                        sh "docker-compose down" // 停止并移除Docker容器
                        exit_on_error("Docker-compose down failed")

                        log("构建镜像并启动容器")
                        sh "docker-compose up -d --build" // 构建镜像并启动容器
                        exit_on_error("Docker-compose up failed")
                    }
                }
            }
        }

        stage('Clean Up') {
            steps {
                script {
                    log("清理 Docker 相关的垃圾")
                    sh "docker system prune -f --volumes" // 清理Docker垃圾
                    exit_on_error("Docker system prune failed")
                    log("清理完成")
                }
            }
        }
    }

    post {
        always {
            script {
                log("部署结束") // 部署结束的日志
            }
        }
        success {
            script {
                log("构建成功") // 构建成功的日志
            }
            emailext (
                subject: "Jenkins 构建成功通知: ${currentBuild.fullDisplayName}",
                body: """Jenkins 构建成功通知:
                        项目: ${env.JOB_NAME}
                        构建编号: ${env.BUILD_NUMBER}
                        构建URL: ${env.BUILD_URL}""",
                to: 'h163361631@163.com' // 构建成功发送邮件通知
            )
        }
        failure {
            script {
                log("构建失败") // 构建失败的日志
            }
            emailext (
                subject: "Jenkins 构建失败通知: ${currentBuild.fullDisplayName}",
                body: """Jenkins 构建失败通知:
                        项目: ${env.JOB_NAME}
                        构建编号: ${env.BUILD_NUMBER}
                        构建URL: ${env.BUILD_URL}""",
                to: 'h163361631@163.com' // 构建失败发送邮件通知
            )
        }
    }
}

def log(message) {
    echo "[${new Date().format('yyyy-MM-dd HH:mm:ss')}] ${message}" // 日志记录函数
}

def exit_on_error(message) {    
    if (currentBuild.result == 'FAILURE') {
         error(message) // 构建失败时退出并记录错误信息
    }
}

提示:发邮件的话需要安装个插件:

配完pipeline后,来试一把,首先我们点击build with parameters 然后选择你要部署的分支,我这里是:jenkins_build_20240709,之后点击build: 随后jenkins就开始构建了,找到控制台日志,可以看到已经部署成功(任务编号是13) 构建成功后会发送邮件,如下: 你可以直观的查看整个管道的步骤,用时等等,如下: 看下docker容器和日志发现,服务都启动成功了:

接下来我们看下pipeline 的第二种方式

在项目根目录的JenkinsFile 文件中编写声明式流水线脚本的方式启动构建任务

首先我们创建一个jenkins 任务,命名为: xzll-im-pipeline-build-by-Jenkinsfile,如下:

之后我们点击配置:

之后我们在项目根目录创建Jenkinsfile文件,并将上边的脚本粘到此文件中,并提交到代码仓库,如下:

然后选择你想要部署的分支进行部署

(注意:第一次时可能不展示build with parameters 还是 build now 那么这时你点击下budil now 再给他停掉,刷新下就展示 build with parameters 构建类型了 ),如下:

看下控制台打印的部署日志、构建过程视图、邮件、以及docker容器启动情况,如下:

ok到这里使用pipeline方式部署项目的实战演示就结束了,在这两种方式中(两种方式指的是 jenkins web配置pipeline脚本项目根目录创建并编写Jenkinsfile文件),我们都是手动build触发任务的,如果想做到push代码即部署?好办,参考方式2 配一个 构建触发器就好啦! 下边我们试试。

pipeline + push

由于现在是push就提交,所以没发选择了只能是在push时指定,所以把choice选择参数这个注掉,然后push到仓库,如下:

之后我发现Jenkins 已经收到了github的回调,并且开始执行23号部署任务:

部署结果【成功部署,以及启动docker容器和发通知邮件】,如下:

至此,本文就结束了。

别看Jenkins是个工具,但是他很重要,东西也挺多的,想用好的话也得下点功夫研究。 到此,你get了什么是CI/CD 以及Jenkins的几种构建方式了吗??

相关推荐
Iareges42 分钟前
【25届秋招】饿了么0817算法岗笔试
c++·算法·面试·求职招聘·笔试·面经
山川而川-R2 小时前
Flask 安装和应用
后端·python·flask
api茶飘香4 小时前
商品视频与多媒体展示:API返回值中的新趋势
java·linux·前端·python·django·tomcat·音视频
Pandaconda4 小时前
【C++ 面试 - 面向对象】每日 3 题(十)
开发语言·c++·后端·面试·职场和发展
小果冻啦4 小时前
记录一次经历:使用flask_sqlalchemy集成flask造成循环导入问题
后端·python·sql·flask
小扳4 小时前
SpringBootWeb 篇-深入了解 SpringBoot + Vue 的前后端分离项目部署上线与 Nginx 配置文件结构
java·vue.js·spring boot·后端·spring
One_T.5 小时前
bug----jdk17使用JOL无法输出java对象的存储布局
java·开发语言·jvm·bug
&木头人&5 小时前
Spring Boot 中的 starter 是什么
java·spring boot·后端
friklogff5 小时前
【Rust光年纪】构建跨语言桥梁:深度解析Rust FFI绑定生成器及其应用
开发语言·后端·rust
๑҉ 晴天6 小时前
Java中的异步编程与CompletableFuture
java·开发语言