轻量化CICD落地:基于Jenkins与Supervisor的中小企业服务发布实践

文章目录

  • 一、核心思路
    • [1.1 项目的意义](#1.1 项目的意义)
    • [1.2 流程梳理](#1.2 流程梳理)
      • [1.2.1 配置阶段](#1.2.1 配置阶段)
      • [1.2.2 Pipeline 打包与分发](#1.2.2 Pipeline 打包与分发)
      • [1.2.3 Supervisor 服务管控](#1.2.3 Supervisor 服务管控)
    • [1.3 本质](#1.3 本质)
  • 二、Jenkins搭建
    • [2.1 使用Docker搭建Jenkins](#2.1 使用Docker搭建Jenkins)
    • [2.2 安装Jenkins必要插件](#2.2 安装Jenkins必要插件)
  • 三、配置Jenkins
    • [3.1 配置全局凭证](#3.1 配置全局凭证)
    • [3.2 配置ssh密钥](#3.2 配置ssh密钥)
  • 四、Pipeline脚本编写
    • [4.1 脚本文件](#4.1 脚本文件)
    • [4.2 脚本文件解读](#4.2 脚本文件解读)
  • 五、Supervisor脚本
    • [5.1 Supervisor执行脚本](#5.1 Supervisor执行脚本)
    • [5.2 supervisor模版文件](#5.2 supervisor模版文件)
    • [5.3 脚本与模版解读](#5.3 脚本与模版解读)
  • 六、全部流程实践
    • [6.1 选择服务](#6.1 选择服务)
    • [6.2 拉取代码](#6.2 拉取代码)
    • [6.3 构建服务jar包](#6.3 构建服务jar包)
    • [6.4 sftp下发服务jar包](#6.4 sftp下发服务jar包)
    • [6.5 配置启动文件并启动服务](#6.5 配置启动文件并启动服务)
  • 七、小总结和感受

在当前数字化转型浪潮中,中小型企业往往面临着技术资源有限与业务快速迭代的双重挑战。对于非K8s部署场景,传统服务发布依赖人工完成代码拉取、编译打包、跨节点传输等一系列离散操作,不仅效率低下,更因人为失误潜藏着服务中断的风险。So,基于 Jenkins Pipeline与Supervisor构建的多节点服务发布方案,就是哥们儿最近研究出来的一种实现架构。嘻嘻:

对于小团队来说,一套 "够用、好用、用得起" 的 CICD 解决方案尤为关键。本方案正是瞄准这一需求,摒弃了大型企业常用的复杂 K8s 生态,转而通过 Jenkins 与 Supervisor 的轻量集成,实现服务发布全流程的半自动化管控。其设计理念贯穿高稳定(通过自动化脚本减少人为干预)、易维护(标准化配置模板与清晰的流程拆解)、低成本(基于现有服务器架构,无需额外硬件投入)三大原则,既解决了传统人工发布的混乱与低效,又避免了过度技术堆砌带来的维护负担,为中小企业提供了一条贴合自身规模与资源的数字化交付路径。

一、核心思路

1.1 项目的意义

针对中小型企业非K8s部署场景,本方案采用Jenkins Pipeline流水线与Supervisor自动化脚本相结合的方式,通过Jenkins的pipeline流水线结合supervisor自动化脚本,实现服务从代码->上线 运行的全流程实现,替代传统服务发布依赖人工完成代码拉取、编译打包、服务器上传、启动配置编写、服务启停等流程操作,以半自动化的形式进行完成服务发布。

其核心价值在于彻底替代传统依赖人工的发布模式,将代码拉取、编译打包、跨节点传输、启动配置生成、服务生命周期管控等离散操作整合为标准化半自动化流程,既降低了人为操作失误风险,又提升了服务发布效率,同时为后续分布式部署架构的演进奠定技术基础。

1.2 流程梳理

一阶段为准备工作,二三阶段为CICD实现自动发布的简要概括。

1.2.1 配置阶段

Jenkins 基础部署:搭建 Jenkins 服务(如容器化部署),安装必备插件(Git、Maven Integration、SSH、Pipeline 等)。

工具配置:配置 Maven 环境(指定路径、私服设置),关联到 Jenkins 全局工具。

通信配置:生成 Jenkins SSH 密钥对,配置目标主机免密登录,在 Jenkins 中登记主机 SSH 信息。

凭证管理:添加代码仓库(如 Git)访问凭证(用户名 + 令牌),供流水线拉取代码使用。

1.2.2 Pipeline 打包与分发

拉取代码:根据参数选择的服务和分支,从代码仓库拉取最新代码。

编译打包:通过 Maven 按配置编译代码(跳过测试),筛选需部署的 Jar 包(排除非服务类 Jar)。

上传分发:根据选择的目标主机,通过 SSH 将筛选后的 Jar 包批量上传到对应主机的指定目录。

1.2.3 Supervisor 服务管控

状态判断:根据上传的 Jar 包,判断服务是新增(无配置文件)还是更新(已有配置文件)。

新增服务处理:基于模板生成 Supervisor 配置文件,校验语法后加载配置并启动服务。

更新服务处理:通过 Supervisor 直接重启对应服务(使用新 Jar 包),完成更新上线。

1.3 本质

本质上是建立了一条从代码提交到服务上线的"软件生产流水线"

  • 对开发人员而言:它简化了发布过程,只需选择功能点和目标环境,即可一键完成部署,将重心更多地放在代码开发上。
  • 对运维人员而言 :它将以人工操作为主、充满不确定性的发布日 ,变成了一个标准化、可预测的自动化流程,极大地降低了因操作失误导致的服务中断风险。
  • 对业务而言 :它显著提升了软件交付的速度和可靠性 ,使得新功能、修复能更快、更稳地抵达用户,提升了业务的敏捷性。简而言之,这个流程的意义在于 "把混乱的、依赖人的发布,变成了有序的、由工具驱动的流水线"

二、Jenkins搭建

2.1 使用Docker搭建Jenkins

拉取镜像并完成Jenkins的搭建工作:

bash 复制代码
# 国内镜像源
docker pull swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/jenkins/jenkins:lts-jdk17
docker tag  swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/jenkins/jenkins:lts-jdk17  docker.io/jenkins/jenkins:lts-jdk17

mkdir /data/jenkins_home
chown -R 1000:1000 /data/jenkins_home 

# 启动Jenkins
docker run -itd --restart=always \
  -p 15105:8080 \
  -p 50000:50000 \
  -v /data/jenkins_home:/var/jenkins_home \
  --name jenkins \
  docker.io/jenkins/jenkins:lts-jdk17

(过于简单的流程直接跳过了,比如查/var的那个文件啥的哈,搭建过程中都有提示的)

2.2 安装Jenkins必要插件

添加相关插件:

插件名称 作用 必装与否
Git Plugin 拉取 Git 仓库代码 必装
Maven Integration Plugin 集成 Maven,自动打包 必装
Publish Over SSH SSH 传文件、远程执行命令 必装
Pipeline 流水线支持 推荐

三、配置Jenkins

3.1 配置全局凭证

访问 Jenkins → 点击左侧「凭证」→ 「系统」→ 「全局凭证」→ 「添加凭证」;

凭证类型选择:「用户名和密码」;

填写信息:

  • 用户名:你的云效 Git 账号(通常是手机号 / 邮箱);
  • 密码:你的云效 Git 密码(或云效个人访问令牌);
  • ID:自定义(如 yunxiao,后续 Pipeline 中会用到);
  • 描述:可选(如 "云效 Git 拉取凭证");

点击「确定」,凭证添加完成。

3.2 配置ssh密钥

为了实现sftp进行jar包的分发,需要配置Jenkins的ssh密钥,并下发到需要运行服务的主机的ssh文件里。

bash 复制代码
root@master:~# docker exec -it jenkins /bin/bash 
jenkins@f4c93a31e108:/$ ssh-keygen -t rsa 
# 一路回车
Generating public/private rsa key pair.
Enter file in which to save the key (/var/jenkins_home/.ssh/id_rsa): 
Created directory '/var/jenkins_home/.ssh'.
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /var/jenkins_home/.ssh/id_rsa
Your public key has been saved in /var/jenkins_home/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:TB0NN3lWQYeeXi8LrJqv1zvVQLtlL8nA1FGMNdok/po jenkins@f4c93a31e108
The key's randomart image is:
+---[RSA 3072]----+
|          ooo+o&*|
|         . o=.% +|
|        . .o B + |
|       o    o * +|
|        S  . + Xo|
|            o @ +|
|           o E + |
|         .o o .  |
|        +=. .o   |
+----[SHA256]-----+
jenkins@f4c93a31e108:/$ exit 
exit
root@master:~# cat /data/jenkins_home/.ssh/id_rsa.pub 
ssh-rsa xxxxxxxxxxxxxxxxxxxxxxx

root@master:~# echo "ssh-rsa  xxxxxxxxxxxxxxxxxxxxxxxxx" >> /root/.ssh/authorized_keys

配置ssh,在Jenkins上配置需要运行服务的主机:(可以使用test测试,查看是否生效)

配置maven

bash 复制代码
# 将maven包解压放在jenkins_home目录下
jenkins@f4c93a31e108:/$ cat > /var/jenkins_home/.bashrc << EOF
export MAVEN_HOME=/var/jenkins_home/apache-maven
export PATH=\$PATH:\$MAVEN_HOME/bin
EOF
jenkins@f4c93a31e108:/$ cat /var/jenkins_home/.bashrc 
export MAVEN_HOME=/var/jenkins_home/apache-maven
export PATH=$PATH:$MAVEN_HOME/bin
jenkins@f4c93a31e108:/$ source /var/jenkins_home/.bashrc 
jenkins@f4c93a31e108:/$ mvn -v 
Apache Maven 3.9.11 (3e54c93a704957b63ee3494413a2b544fd3d825b)
Maven home: /var/jenkins_home/apache-maven
Java version: 17.0.14, vendor: Eclipse Adoptium, runtime: /opt/java/openjdk
Default locale: en, platform encoding: UTF-8
OS name: "linux", version: "6.8.0-88-generic", arch: "amd64", family: "unix"
jenkins@f4c93a31e108:/$ 
jenkins@f4c93a31e108:/$ exit 
exit

基本环境配置完成。

四、Pipeline脚本编写

4.1 脚本文件

yaml 复制代码
pipeline {
    agent any
    tools {
        maven 'maven'
    }
    parameters {
        choice(
            name: 'SERVICE_NAME',
            choices: [
                '全量打包',
                'service1',
                'service2',
                'service3',
                'service4',
                'service5'
            ],
            description: '选择要打包的服务(只需填服务名,不用加 .jar 后缀)'
        )
        choice(
            name: 'TARGET_HOST',
            choices: [
                '全部主机',
                'uat',
                'master'
            ],
            description: '选择要部署的目标主机(选「全部主机」则批量部署所有配置的主机)'
        )
    }
    environment {
    	// git仓库地址
        GIT_URL = 'https://service.git'
        // 全局凭证名(git仓库密钥存放的名)
        GIT_CRED = 'yunxiao'
        // 代码分支
        BRANCH = 'master'
        // 服务jar包上传目录
        REMOTE_DIR = '/data/projects/'
        // 日志目录
        LOG_DIR = '/data/logs'
        EXCLUDE_PATTERN = '*/common/*|*-api.jar'
        DEPLOYED_HOSTS_FILE = "${WORKSPACE}/deployed_hosts.txt"
        DEPLOYED_IPS_FILE = "${WORKSPACE}/deployed_ips.txt"
        
        // supervisor 执行脚本路径
        SERVER_SCRIPT_PATH = '/etc/supervisor/supervisor_deploy.sh'
        // supervisor 模版文件路径
        SERVER_TEMPLATE_PATH = '/etc/supervisor/supervisor_template.conf'
        // supervisor 启动文件目录
        SUPERVISOR_CONF_DIR = '/etc/supervisor/conf.d'
    }
    stages {
        stage('拉取Git代码') {
            steps {
                git url: env.GIT_URL, credentialsId: env.GIT_CRED, branch: env.BRANCH
                sh "echo '' > ${DEPLOYED_HOSTS_FILE} && echo '' > ${DEPLOYED_IPS_FILE}"
            }
        }

        stage('maven打包及智能筛选') {
            steps {
                script {
                    def targetService = params.SERVICE_NAME
                    echo "==== 执行${targetService == '全量打包' ? '全量' : '单个'}服务打包:${targetService} ===="
                    sh "mvn package -Dmaven.test.skip=true -Djdk.version=17"

                    // 核心修复:用单引号包裹Shell命令,变量通过字符串拼接插入,避免解析冲突
                    def filterCmd
                    if (targetService == '全量打包') {
                        filterCmd = 'find ' + WORKSPACE + ' -path \'*/target/*.jar\' ! -path \'*common/*\' ! -name \'*-api.jar\' | sort -u > ./deploy_jars.txt'
                    } else {
                        // 正则表达式部分用单引号隔离,变量单独拼接
                        filterCmd = 'find ' + WORKSPACE + ' -path \'*/target/*.jar\' ! -path \'*common/*\' ! -name \'*-api.jar\' | grep -E \'' + targetService + '(-biz)?\\.jar$\' | sort -u > ./deploy_jars.txt'
                    }

                    sh filterCmd

                    sh '''
                        if [ ! -s ./deploy_jars.txt ]; then
                            echo "ERROR:未找到对应Jar包!"
                            find ${WORKSPACE} -path '*/target/*.jar' ! -path '*common/*' ! -name '*-api.jar'
                            exit 1
                        fi
                        echo "筛选成功!共找到 $(wc -l < ./deploy_jars.txt) 个Jar包"
                        cat ./deploy_jars.txt
                    '''
                }
            }
        }

        stage('多主机SFTP批量上传') {
            steps {
                script {
                    def HOST_CONFIG = [
                        'master': [SSH_USER: 'root', REMOTE_HOST: '192.168.119.7', SSH_KEY: '/var/jenkins_home/.ssh/id_rsa'],
                        'uat': [SSH_USER: 'root', REMOTE_HOST: '192.168.119.3', SSH_KEY: '/var/jenkins_home/.ssh/id_rsa']
                    ]
                    def targetHosts = params.TARGET_HOST == '全部主机' ? HOST_CONFIG.keySet().collect() : [params.TARGET_HOST]

                    for (def host : targetHosts) {
                        def hostInfo = HOST_CONFIG[host]
                        def sshUser = hostInfo.SSH_USER
                        def remoteHost = hostInfo.REMOTE_HOST
                        def sshKey = hostInfo.SSH_KEY
                        def remoteDir = env.REMOTE_DIR


                        echo "正在上传到主机:${host}(IP:${remoteHost})"
                        sh """
                            [ -f "${sshKey}" ] || { echo "ERROR:私钥文件不存在!"; exit 1; }
                            chmod 600 "${sshKey}"
                            while read -r jarPath; do
                                sftp -i "${sshKey}" -o StrictHostKeyChecking=no "${sshUser}@${remoteHost}" << EOF
                                put "\${jarPath}" "${remoteDir}/"
                                bye
EOF
                            done < ./deploy_jars.txt
                        """

                        sh "echo -n '${host}, ' >> ${DEPLOYED_HOSTS_FILE}"
                        sh "echo -n '${remoteHost}, ' >> ${DEPLOYED_IPS_FILE}"
                        echo "主机${host}上传完成!"
                    }
                }
            }
        }

        stage('Supervisor自动发布服务') {
            steps {
                script {
                    def HOST_CONFIG = [
                    	// 密钥的存放路径,为Jenkins路径
                        'master': [SSH_USER: 'root', REMOTE_HOST: '192.168.119.7', SSH_KEY: '/var/jenkins_home/.ssh/id_rsa'],
                        'uat': [SSH_USER: 'root', REMOTE_HOST: '192.168.119.3', SSH_KEY: '/var/jenkins_home/.ssh/id_rsa']
                    ]
                    def targetHosts = params.TARGET_HOST == '全部主机' ? HOST_CONFIG.keySet().collect() : [params.TARGET_HOST]
                    def deployTimestamp = sh(script: "date +%s", returnStdout: true).trim()

                    for (def host : targetHosts) {
                        def hostInfo = HOST_CONFIG[host]
                        def sshUser = hostInfo.SSH_USER
                        def remoteHost = hostInfo.REMOTE_HOST
                        def sshKey = hostInfo.SSH_KEY
                        def remoteDir = env.REMOTE_DIR
                        def logDir = env.LOG_DIR
                        def serverScript = env.SERVER_SCRIPT_PATH
                        def serverTemplate = env.SERVER_TEMPLATE_PATH
                        def supervisorConfDir = env.SUPERVISOR_CONF_DIR

                        echo "正在${host}执行服务发布"
                        // 上传Jar包列表
                        sh """
                            sftp -i "${sshKey}" -o StrictHostKeyChecking=no "${sshUser}@${remoteHost}" << EOF
                            put "${WORKSPACE}/deploy_jars.txt" "${remoteDir}/"
                            bye
EOF
                        """
                        sh "ssh -i '${sshKey}' -o StrictHostKeyChecking=no '${sshUser}@${remoteHost}' 'mkdir -p ${logDir} && chmod 777 ${logDir} && ${serverScript} --deploy-timestamp ${deployTimestamp} --jar-list-path ${remoteDir}/deploy_jars.txt --remote-dir ${remoteDir} --log-dir ${logDir} --supervisor-conf-dir ${supervisorConfDir} --supervisor-template ${serverTemplate}'"
                    }
                }
            }
        }
    }

    post {
        success {
            echo "完整CICD流程执行成功"
            echo "操作服务:${params.SERVICE_NAME}"
            echo "部署主机:${params.TARGET_HOST}"
        }
        failure {
            echo "CICD流程执行失败,请查看日志排查!"
        }
    }
}

4.2 脚本文件解读

第一部分:选项部分

这个部分可以自定义service的名字,也就是说可以自定义我们要上传哪个服务,也可以选择要部署的主机,脚本保存之后会有Build With Partments模块,在里面就可以选择。

bash 复制代码
parameters {
        choice(
            name: 'SERVICE_NAME',
            choices: [
                '全量打包',
                'service1',
                'service2',
                'service3',
                'service4',
                'service5'
            ],
            description: '选择要打包的服务(只需填服务名,不用加 .jar 后缀),后续新增服务在此添加即可'
        )
        choice(
            name: 'TARGET_HOST',
            choices: [
                '全部主机',
                'uat',
                'master'
            ],
            description: '选择要部署的目标主机(选「全部主机」则批量部署所有配置的主机)'
        )
    }

SERVICE_NAME(服务选择)

  • 功能定位:此选项定义了CI/CD流程的构建范围,决定了哪些微服务会被打包和部署
  • 选项说明
  • 全量打包:构建和部署所有可用服务,适用于大规模发布或环境初始化。
  • 单个服务:针对特定服务进行精准构建,实现快速迭代和热修复
  • 使用场景:开发阶段可选择单个服务进行快速测试验证,生产发布可选择全量打包确保服务版本一致性

TARGET_HOST(目标环境)

  • 功能定位:控制代码部署的目标环境,实现分级发布策略
  • 选项说明
  • 全部主机:批量部署到所有配置环境,提高发布效率
  • 特定环境:定向部署到指定环境,支持灰度发布策略

第二部分:核心配置部分,根据自己环境进行修改

bash 复制代码
    environment {
    	// git仓库地址
        GIT_URL = 'https://service.git'
        // 全局凭证名(git仓库密钥存放的名)
        GIT_CRED = 'yunxiao'
        // 代码分支
        BRANCH = 'master'
        // 服务jar包上传目录
        REMOTE_DIR = '/data/projects/'
        // 日志目录
        LOG_DIR = '/data/logs'
        // 需要排除的jar包
        EXCLUDE_PATTERN = '*/common/*|*-api.jar'
        // 部署主机,自动生成
        DEPLOYED_HOSTS_FILE = "${WORKSPACE}/deployed_hosts.txt"
        // 部署主机,自动生成
        DEPLOYED_IPS_FILE = "${WORKSPACE}/deployed_ips.txt"
        
        // supervisor 执行脚本路径
        SERVER_SCRIPT_PATH = '/etc/supervisor/supervisor_deploy.sh'
        // supervisor 模版文件路径
        SERVER_TEMPLATE_PATH = '/etc/supervisor/supervisor_template.conf'
        // supervisor 启动文件目录
        SUPERVISOR_CONF_DIR = '/etc/supervisor/conf.d'
    }

根据自己的环境进行配置,supervisor脚本文件和模版文件放在哪里无所谓,重点是。SUPERVISOR_CONF_DIR = '/etc/supervisor/conf.d'不允许更改,除非你的supervisor的配置文件不是默认配置文件。

完整阶段视图:

五、Supervisor脚本

5.1 Supervisor执行脚本

bash 复制代码
#!/bin/bash
set -euo pipefail

# 解析传入参数(新增log-dir,适配日志目录)
while [[ $# -gt 0 ]]; do
    case $1 in
        --deploy-timestamp) DEPLOY_TIMESTAMP="$2"; shift 2 ;;
        --jar-list-path) JAR_LIST_PATH="$2"; shift 2 ;;
        --remote-dir) REMOTE_DIR="$2"; shift 2 ;;
        --log-dir) LOG_DIR="$2"; shift 2 ;;
        --supervisor-conf-dir) SUPERVISOR_CONF_DIR="$2"; shift 2 ;;
        --supervisor-template) SUPERVISOR_TEMPLATE="$2"; shift 2 ;;
        *) echo "未知参数:$1"; exit 1 ;;
    esac
done

# 校验依赖和参数
check_dependency() {
    local cmd=$1
    if ! command -v ${cmd} &> /dev/null; then
        echo "ERROR:未找到命令 ${cmd},请安装后重试!"
        exit 1
    fi
}
check_dependency "supervisorctl"
check_dependency "java"
[[ -z ${DEPLOY_TIMESTAMP:-} ]] && { echo "ERROR:缺少部署时间戳"; exit 1; }
[[ -f ${JAR_LIST_PATH} ]] || { echo "ERROR:Jar包列表文件不存在 ${JAR_LIST_PATH}"; exit 1; }
[[ -d ${SUPERVISOR_CONF_DIR} ]] || { echo "ERROR:Supervisor配置目录不存在"; exit 1; }
[[ -f ${SUPERVISOR_TEMPLATE} ]] || { echo "ERROR:Supervisor模板文件不存在"; exit 1; }

# 提取服务名 即Jar包名→服务名
extract_service_name() {
    local jar_path=$1
    local jar_filename=$(basename "${jar_path}")
    echo "${jar_filename%.jar}"
}

# 判断是否是本次部署的新包(时间戳±300秒误差)
is_latest_jar() {
    local jar_path=$1
    local jar_mtime=$(stat -c %Y "${jar_path}")
    if [[ ${jar_mtime} -ge $((DEPLOY_TIMESTAMP - 300)) && ${jar_mtime} -le $((DEPLOY_TIMESTAMP + 300)) ]]; then
        return 0
    else
        return 1
    fi
}

# 循环处理每个Jar包
echo "开始处理本次部署的Jar包(共 $(wc -l < ${JAR_LIST_PATH}) 个)"
while read -r jar_path; do
    [[ -z ${jar_path} ]] && continue

    # 提取服务名
    service_name=$(extract_service_name "${jar_path}")
    jar_filename=$(basename "${jar_path}") # 仅Jar包文件名(如easewise-register.jar)
    echo -e "\n========================================"
    echo "当前处理:服务名=${service_name} | Jar包=${jar_filename}"

    # 校验Jar包存在且是本次新包
    [[ -f "${REMOTE_DIR}/${jar_filename}" ]] || { echo "WARNING:Jar包不存在 ${REMOTE_DIR}/${jar_filename},跳过"; continue; }
    if ! is_latest_jar "${REMOTE_DIR}/${jar_filename}"; then
        local jar_time=$(date -d @$(stat -c %Y "${REMOTE_DIR}/${jar_filename}") +'%Y-%m-%d %H:%M:%S')
        echo "WARNING:Jar包不是本次部署的新包(修改时间:${jar_time}),跳过";
        continue;
    fi

    # 核心逻辑:新增/更新服务
    supervisor_conf="${SUPERVISOR_CONF_DIR}/${service_name}.conf"
    if [[ -f ${supervisor_conf} ]]; then
        # 场景1:更新服务(配置已存在)
        echo "识别为【更新服务】,配置文件:${supervisor_conf}"
        echo "正在重启服务..."
        supervisorctl stop "${service_name}" || true # 允许服务未启动
        supervisorctl start "${service_name}"
        if [[ $? -eq 0 ]]; then
            echo "服务 ${service_name} 重启成功!"
        else
            echo "服务 ${service_name} 重启失败!"
            echo "查看日志:tail -f ${LOG_DIR}/${service_name}.log"
            exit 1
        fi
    else
        # 场景2:新增服务(配置不存在)
        echo "识别为【新增服务】,生成配置文件:${supervisor_conf}"
        # 从模板生成配置
        sed \
            -e "s/{{SERVICE_NAME}}/${service_name}/g" \
            -e "s/{{REMOTE_DIR}}/${REMOTE_DIR//\//\\/}/g" \
            -e "s/{{JAR_FILENAME}}/${jar_filename}/g" \
            -e "s/{{LOG_DIR}}/${LOG_DIR//\//\\/}/g" \
            "${SUPERVISOR_TEMPLATE}" > "${supervisor_conf}"

        # 校验配置语法
        echo "正在校验配置文件..."
        if ! supervisorctl reread > /dev/null 2>&1; then
            echo "配置文件语法错误!"
            rm -f "${supervisor_conf}"
            exit 1
        fi

        # 加载配置并启动服务
        echo "正在启动服务..."
        supervisorctl update "${service_name}"
        supervisorctl start "${service_name}"
        if [[ $? -eq 0 ]]; then
            echo "服务 ${service_name} 新增并启动成功!"
        else
            echo "服务 ${service_name} 启动失败!"
            echo "查看日志:tail -f ${LOG_DIR}/${service_name}.log"
            rm -f "${supervisor_conf}"
            exit 1
        fi
    fi
done < "${JAR_LIST_PATH}"

echo -e "\n========================================"
echo "所有服务处理完成!"

5.2 supervisor模版文件

yaml 复制代码
[program:{{SERVICE_NAME}}]
directory={{REMOTE_DIR}}
command=java -Dfile.encoding=utf-8 -jar {{JAR_FILENAME}}
autostart=true
autorestart=true
startsecs=5
startretries=3
user=root
redirect_stderr=false
stdout_logfile={{LOG_DIR}}/{{SERVICE_NAME}}.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=3
stderr_logfile={{LOG_DIR}}/{{SERVICE_NAME}}.err
stderr_logfile_maxbytes=20MB
stderr_logfile_backups=3

5.3 脚本与模版解读

这个脚本的核心设计思想是实现服务部署的"智能自动化",它将传统依赖人工判断和操作的服务上线流程,抽象为一个能自主决策的标准化过程。其关键在于通过判断Supervisor配置文件是否存在,来自动识别当前服务是"新增"还是"更新",从而触发不同的标准化处理流程,最终目标是将繁琐的人工发布转变为一条可靠、高效的自动化流水线。

在实现上,脚本作为"大脑"负责逻辑判断和命令执行:它解析参数、检查环境、遍历Jar包列表,并根据服务状态决定是调用模板生成新配置并启动,还是直接重启现有服务。而模板文件则作为"蓝图",提供了标准化的服务运行配置;脚本通过填充模板中的变量(如服务名、路径)来动态生成每个服务专属的配置文件,从而确保所有服务都能以统一、规范的方式被管理和监控。

六、全部流程实践

6.1 选择服务

首先,我们要去选择要构建的服务和要部署服务的主机:

6.2 拉取代码

git开始从代码仓库拉取对应分支的代码:

6.3 构建服务jar包

拉取完成后,maven会进行自动构建服务jar包:

6.4 sftp下发服务jar包

代码构建完成后,sftp会将服务jar包上传至对应服务器的服务启动目录下

6.5 配置启动文件并启动服务

服务的jar包上传完成后,supervisor_deploy.sh脚本会被执行,相关参数Jenkins会完成补全,脚本会检查启动配置文件目录下是否存在我们本次构建的服务名对应的文件,如果有,直接复用,重启服务完成更新,如果没有,则会根据我们的模版文件和相关服务名等参数完成启动文件的创建,并且启动服务,全部流程结束。服务完成了更新/上线/启动。

七、小总结和感受

其实不想写这个的,但是还是写了。

总结就是,道阻且长。感觉运维这条路就是会的越多会的越少的一门,门门了解,门门不精,只能通过不断的实践和去实现创新想法,才能去一点点的完善,有的时候就是做的之前想的太多了,我啊,就是做之前想的想的太多,导致感觉不管做什么都有点唯唯诺诺哈哈哈,但是想的太多不去做,还是白费的嘛你说是不,所以我现在在改变,做之前不想太多,先做,再想,遇到啥解决啥,行吧。

其实这个,也是一次尝试,本来想玩Jenkins+K8s的形式,但是深刻考虑了一下整个项目实施起来的难度,服务从裸金属服务器部署的形式转变为容器化实施,向DevOps转型,这虽然说是迟早的事情,但是我还是觉得步子不能迈得太大,步子迈大了,咔,扯到蛋就有意思了,所以还是一步一个脚印往前走,况且等我们公司服务的规模变得越来越大的时候,有几十台服务器的时候,我们再玩K8s也不迟,我也会一直去学,所以这个也算是在中小型公司CICD的一个选择实施的一种思路吧。好啦,就这样,想交流的可以后台私信聊哦。

拜拜。

相关推荐
weixin_307779131 小时前
Jenkins ASM API 插件:详解与应用指南
java·运维·开发语言·后端·jenkins
温启志c#1 小时前
【无标题极简版的 TCP 服务端和客户端实现,保留核心功能,去掉复杂封装,适合快速测试:】
运维·服务器·网络
hid558845361 小时前
LS-DYNA在爆炸与冲击领域的应用研究:从隧道支护到地下采场爆破模拟
jenkins
北京耐用通信1 小时前
三步打通数据壁垒:耐达讯自动化Ethernet/IP转CC-Link方案全解析。建议点赞收藏
运维·tcp/ip·自动化
羊村积极分子懒羊羊1 小时前
nginx的https的搭建
运维·nginx·https
像风一样自由20202 小时前
Docker 与 Docker Compose:从零开始的容器化之旅
运维·docker·容器
北珣.2 小时前
docker镜像操作
运维·docker·容器·镜像
Evan芙2 小时前
用fping编写脚本扫描10.0.0.0/24网段在线主机
linux·运维·网络·excel
SongYuLong的博客2 小时前
ARM Linux 交叉编译工具链(toolchain)
linux·运维·arm开发