Featured image of post 用Jenkins Pipeline实现服务器集群部署

用Jenkins Pipeline实现服务器集群部署

用Jenkins Pipeline实现服务器集群不停机自动化部署

临走之前公司希望最后能搞定生产服务器的自动发布,因此有了此文

Jenkins Pipeline编译与部署

Jenkins Pipeline配置

Jenkins的Pipeline是依靠于插件实现的,所以目前我是安装了以下这些插件

  • Pipeline
  • Pipeline Graph Analysis Plugin
  • Pipeline Maven Integration Plugin
  • Pipeline: Stage View Plugin
  • SSH Pipeline Steps

其余的很多基础插件会作为依赖自动被安装。

Pipeline编写

详细细节会在注释中给出

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
def remotes = [
    [name: "project-1", host: "IP", allowAnyHosts: true],
    // 可以继续添加更多的 remote
]

pipeline {
    agent any

    environment {
        // 用于配置后面的文件名
        PROJECT = 'PROJECT_NAME'
    }

    stages {
        stage('Preparation') {
            steps {
                // Git拉取代码
                git branch: 'dev', credentialsId: '403F_VCS', url: 'https://git.url'
            }
        }

        stage('Maven Package') {
            steps {
                script {
                    // 这里的maven-3.9.8是在系统配置中配置完成的
                    withMaven(globalMavenSettingsConfig: 'maven_mirror_and_properties', maven: 'maven-3.9.8', mavenSettingsConfig: '', traceability: true) {
                        // JAVA_HOME特别采用jdk_1.8是因为当前项目代码中用了javax,必须要用8
                        withEnv(["JAVA_HOME=/var/jenkins_home/tools/jdk_1.8"]) {
                            sh '"mvn" clean package -U'
                        }
                    }
                }
            }
        }

        // 这一阶段会获取Git Commit Hash并进行组合后形成新的文件名
        stage('Post Process Artifacts') {
            steps {
                script {
                    echo "${PROJECT}"
                    // 获取Commit Hash以供后面使用
                    def commitHash = sh(script: 'git log -n 1 --pretty=format:\'%H\'', returnStdout: true).trim()
                    def project = "${PROJECT}"
                    def fileName = "${new Date().format('yyyyMMddHHmmss')}-${commitHash}.war"
                    echo "Build project ${project}, git commit ${commitHash}"

                    // 删除掉target文件夹中生成的其他文件,只留存一个最终的war包
                    sh "find target -mindepth 1 -type f ! -name '*.war' -exec rm -f {} +"
                    sh "find target -mindepth 1 -type d ! -name '.' ! -name '..' -exec rm -rf {} +"
                    def warFilesCount = sh(script: 'ls target/*.war 2>/dev/null | wc -l', returnStdout: true).trim().toInteger()
                    
                    // 确保最终留下来的war包只有一个
                    if (warFilesCount == 1) {
                        def warFile = sh(script: 'ls target/*.war', returnStdout: true).trim()

                        if (fileName.isEmpty()) {
                            error "file_name env variable not found!"
                        }

                        // 重命名最终的war包
                        sh "mv '${warFile}' 'target/${project}_${fileName}'"

                        echo "WAR file renamed"
                    } else {
                        error "WAR file in target not single!"
                    }
                }
            }
        }

        // 留存一份最终的war包供后续归档与回滚等使用
        stage('Results') {
            steps {
                archiveArtifacts artifacts: 'target/*.war'
            }
        }
        
        // 部署阶段
        stage('Deploy') {
            steps {
                script {
                    // 所有需要部署的服务器采用同一个SSH RSA Key,这样可以保证一个用户凭证可以连接所有的服务器
                    withCredentials([sshUserPrivateKey(credentialsId: 'aliyun-prod', keyFileVariable: 'identity', usernameVariable: 'username')]) {
                        remotes.each { remote ->
                            remote.user = userName
                            remote.identityFile = identity
                            stage("Stop Tomcat server") {
                                sshCommand remote:remote, command: 'bash /usr/local/deploy/shutdown.sh'
                            }
                            stage("Upload new WAR package") {
                                def warFile = sh(script: 'ls target/*.war 2>/dev/null', returnStdout: true).trim()
                                sshPut remote: remote, from: warFile, into: '/usr/local/deploy/server.war'
                            }
                            stage("Start up Tomcat") {
                                sshCommand remote:remote, command: 'bash /usr/local/deploy/start.sh'
                            }
                            stage("Check Tomcat startup status") {
                                sshCommand remote:remote, command: 'bash /usr/local/deploy/check.sh'
                            }
                        }
                    }
                }
            }
        }
    }
}

服务器本机部署部分

有些命令,比如宿主机上原始的Tomcat的关停、文件替换,以及最终的重启动与启动结果确保,这些任务会随着宿主机不同而可能有些许不同, 因此更适合直接远程执行Bash脚本而非通过Jenkins执行复杂的脚本。

现有服务关停

1
2
3
4
5
6
7
#!/bin/sh

bash /usr/local/tomcat8.5/bin/shutdown.sh
# 等待30s以让Tomcat graceful shutdown
sleep 30
# 若30s后仍然没有关停则将其kill掉
pids=$(ps aux | grep java | grep tomcat | grep -v grep | grep -v bash | awk '{print $2}') && [ -n "$pids" ] && echo "$pids" | xargs kill -9 || echo "Tomcat already down"

启动新服务端

1
2
3
4
5
#!/bin/sh
# 备份现有服务端以备不时之需
mv /usr/local/tomcat8.5/project /usr/local/tomcat8.5/project_$(date +"%Y%m%d_%H%M%S")
unzip /usr/local/deploy/server.war -d /usr/local/tomcat8.5/project
bash /usr/local/tomcat8.5/bin/startup.sh

监控新服务端启动

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#!/bin/bash

LOG_FILE="/usr/local/logs/wj_project.log"
SEARCH_STRING="FrameworkServlet 'wj_project': initialization completed"
TIMEOUT=300  # 5分钟
CHECK_INTERVAL=10  # 每10秒检查一次
elapsed_time=0

# 等待日志文件被创建
while [ ! -f "$LOG_FILE" ]; do
    echo "日志文件 $LOG_FILE 不存在,等待创建..."
    sleep $CHECK_INTERVAL
    elapsed_time=$((elapsed_time + CHECK_INTERVAL))

    # 如果超过超时时间,退出
    if [ $elapsed_time -ge $TIMEOUT ]; then
        echo "错误:在$TIMEOUT秒内未创建日志文件 $LOG_FILE。"
        exit 1
    fi
done

# 获取日志文件的最后一行
last_position=$(wc -l < "$LOG_FILE")

while [ $elapsed_time -lt $TIMEOUT ]; do
    # 获取当前日志文件的行数
    current_position=$(wc -l < "$LOG_FILE")

    # 如果日志文件有新内容
    if [ "$current_position" -gt "$last_position" ]; then
        # 检查新添加的内容
        tail -n $((current_position - last_position)) "$LOG_FILE" | grep -q "$SEARCH_STRING"

        if [ $? -eq 0 ]; then
            echo "找到字符串:$SEARCH_STRING"
            exit 0
        fi

        # 更新最后检查的位置
        last_position=$current_position
    fi

    echo "未找到字符串: $SEARCH_STRING 等待中"
    sleep $CHECK_INTERVAL
    elapsed_time=$((elapsed_time + CHECK_INTERVAL))
done

echo "错误:在$TIMEOUT秒内未找到字符串$SEARCH_STRING"
exit 1

然后使集群中的机器逐个重新部署,来避免服务中断。

Licensed under CC BY-NC-SA 4.0
最后更新于 Aug 16, 2024 00:00 UTC
使用 Hugo 构建
主题 StackJimmy 设计