使用 jenkins pipeline 在代码合并后自动打包并部署到 docker swarm

发布于 2023-02-27  1.02k 次阅读


此前 java 项目工作流程: 提交代码创建合并请求 --  等待代码合并 -- 到 jenkins 控制台 build (全部配置均于控制台完成,构建,打包镜像,推送镜像) 等待完成 -- 到后端机器拉取镜像并停止镜像更新 (生产环境使用多实例依次切负载均衡更新)

整套流程下来还是比较费时,不同项目和环境的配置修改也不方便,且没有版本管理,为了提高效率,改为由 git 管理的 pipeline 流水线打包,打包完成后远程后端机器在 docker swarm 管理下自动热更新镜像

流程变为: 提交代码创建合并请求 -- 代码合并发送 webhook 到 jenkins -- jenkins 更新读取 pipeline 配置进行打包并远程后端机器部署新镜像 -- 发送更新成功或失败通知到企业微信

即开发人员提交代码后就全部自动化了

#1. 备份当前 jenkins 所有项目的配置


#!/usr/bin/python3
"""备份 jenkins 配置, 需要调用 jenkins-cli.jar"""
import subprocess

jenkins_host = ""
jenkins_user = ""
jenkins_password = ""
jenkins_cli_path = r'C:\Users\Administrator\Desktop\sh\jenkins-cli.jar'
jenkins_cmd = f'java -jar {jenkins_cli_path} -s {jenkins_host} -auth {jenkins_user}:{jenkins_password} -webSocket'


def execute_command(cmd):
    output = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
    output_string = str(output, encoding='utf-8')
    return output_string


# 获取所有任务
jobs = execute_command(f'{jenkins_cmd} list-jobs').split('\n')[:-1]

# 备份所有任务配置到 xml 文件
for job in jobs:
    job_config = execute_command(f'{jenkins_cmd} get-job {job}')
    with open(f'{job}-backup.xml', 'w', encoding='utf-8') as f:
        f.write(job_config)
        print(job)

# 如需恢复,调用 update-job api 导入即可

#2. 新建 pipeline, docker-swarm, dockerfile 配置文件仓库


# 说明:
# 在 jenkins 机器克隆本配置仓库, jenkins 构建时 pipeline 脚本会 pull 此仓库根据 Dockerfile 开始编译打包上传镜像,调用 swarm 下脚本远程后端机器更新镜像,发送通知
# 后续构建改动都直接在此仓库提交即可,方便管理

# 仓库目录树
/root/jenkins-pipeline-config
├── dev
│   ├── dockerfile
│   ├── pipeline
│   └── swarm
├── old-xml-config
├── prod
│   ├── dockerfile
│   └── pipeline
└── test
    ├── dockerfile
    ├── pipeline
    └── swarm

# dockerfile 示例:
# cat /root/jenkins-pipeline-config/dev/dockerfile/docker-swarm.Dockerfile
#Nginx Dockerfile

FROM centos:centos7.7.1908

LABEL maintainer="stomach@ley.best"

RUN yum install -y  gcc gcc-c++  pcre pcre-devel zlib zlib-devel openssl openssl-devel \
    && useradd -r -s /sbin/nologin nginx \
    && yum clean all

ADD nginx-1.16.1.tar.gz /usr/local/src/

RUN cd /usr/local/src/nginx-1.16.1 \
    && ./configure --prefix=/apps/nginx \
    && make \
    && make install \
    && rm -rf /usr/local/src/nginx*

ADD nginx.conf /apps/nginx/conf/nginx.conf

COPY index.html /apps/nginx/html/

RUN ln -s /apps/nginx/sbin/nginx /usr/sbin/nginx

EXPOSE 80 443

CMD ["nginx","-g","daemon off;"]


# pipeline 示例:
# cat /root/jenkins-pipeline-config/dev/pipeline/docker-swarm.pipeline
def project_name="docker-swarm"
def tag = "dev"
def build_project="middleware-docker-swarm"

def registry_domain = 'registry.cn-shanghai.aliyuncs.com'
def registry_addr = "'${registry_domain}'/xxxxxx"
def username = ''
def password = ''

def workspace = "/var/jenkins_home/workspace/docker-swarm"
def jk_conf_dir = "/opt/jenkins-pipeline-config"
def swarm_script = "'$jk_conf_dir'/'$tag'/swarm/'$project_name'.sh"
def dockerfile = "'$jk_conf_dir'/'$tag'/dockerfile/'$project_name'.Dockerfile"

def wework_notify(msg) {
    sh """
    curl -s 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxx' \
    -H 'Content-Type: application/json' \
    -d '
    {
    "msgtype": "markdown",
    "markdown": {      
        "content": "${msg}"
    }
    }'
    """
}


pipeline {
    agent any
    options {
        timeout(time: 15, unit: 'MINUTES')
    }
	stages {
		stage('pull jenkins config code') {
			steps {
				script {
					sh """
                    #!/bin/bash +x
					git -C '$jk_conf_dir' pull
					ssh kong "git -C '$jk_conf_dir' pull"
					"""
				}
			}
		}
		stage('Clone code') {
            steps {
                echo 'clone code..'
                git branch:"swarm", url: 'ssh://git@gitlab.xxxxxx.io:2222/xxx/xxxx.git'
            }
        }
		stage('Docker build') {
            steps {
                echo "docker build ..."
                sh """
                cd '$workspace'
                /var/bin/docker build -t '$build_project':'$tag' -f '$dockerfile' .
                """
            }            
        }
        stage('Docker login') {
            steps {
                echo "docker login ..."
                sh """
                /var/bin/docker login --username='$username' '$registry_domain' -p '$password'
                """
            }            
        }
        stage('Docker tag') {
            steps {
                echo "docker tag ..."
                sh """
                /var/bin/docker tag '$build_project':'$tag' '$registry_addr'/'$build_project':'$tag'
                """
            }            
        }
        stage('Docker push') {
            steps {
                echo "docker push ..."
                sh """
                /var/bin/docker push '$registry_addr'/'$build_project':'$tag'
                """
            }            
        }
        stage('Clean image') {
            steps {
                echo "clean push ..."
                sh """
                /var/bin/docker rmi -f '$registry_addr'/'$build_project':'$tag'
                /var/bin/docker rmi -f '$build_project':'$tag'
                """
            }
        }
		stage('docker swarm update image') {
			steps {
				script {
					sh """
					ssh kong "/bin/bash '$swarm_script' update"
					"""
				}
			}
		}
		
	}
	post {
		success {
			script {
                    def changeString = getChangeString()
                    def branch = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim()
                    def msg = "#### ${project_name}已更新部署\n\n分支:${branch}\n\n\n\n${changeString}"
                    wework_notify(msg)
			}
		}
		failure {
			script {
                    def changeString = getChangeString()
                    def branch = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim()
                    def msg = "#### ${project_name}构建失败\n\n分支:${branch}\n\n\n\n${changeString}\n\n\n\n构建详情: [${env.BUILD_URL}](${env.BUILD_URL})"
                    wework_notify(msg)
			}
		}
        aborted {
            script {
                    def changeString = getChangeString()
                    def branch = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim()
                    def msg = "#### ${project_name}构建超时\n\n分支:${branch}\n\n\n\n${changeString}\n\n\n\n构建详情: [${env.BUILD_URL}](${env.BUILD_URL})"
                    wework_notify(msg)
			}
        }
	}
}


@NonCPS
def getChangeString() {
    MAX_MSG_LEN = 100
    def changeString = ""

    def changeLogSets = currentBuild.changeSets
    for (int i = 0; i < changeLogSets.size(); i++) {
        def entries = changeLogSets[i].items
        for (int j = 0; j < entries.length; j++) {
            def entry = entries[j]
            truncated_msg = entry.msg.take(MAX_MSG_LEN)
            changeString += " - ${truncated_msg} (${entry.author})\n"
        }
    }
    if (!changeString) {
        changeString = " - No new changes"
    }
	if (changeString.length() > 4000) {
    	changeString = changeString[0..4000]
	}
	return changeString
}

# swarm 示例:
# cat /root/jenkins-pipeline-config/dev/swarm/docker-swarm.sh
#!/bin/bash
# first-run: bash $0
# update image: bash $0 update

NAME="d_docker-swarm"
IMAGE_ADDR="registry.cn-shanghai.aliyuncs.com/xxxxx/middleware-docker-swarm:dev"
PORT="11111"
INTERNAL_PORT="80"
ENV="dev"
REGISTRY_DOMAIN="registry.cn-shanghai.aliyuncs.com"
R_USERNAME="xxxx"
R_PASSWORD="xxxx"
WORKID=0

SWARM_NETWORK="xxxx_net"
REPLICAS="2"


if [ $(docker service ls |grep replicated |wc -l) -eq 0 ];then
    docker swarm init &>/dev/null
    docker swarm update --task-history-limit=1
    # 多机器需要将 worker 加入集群 docker swarm join-token worker
fi 

if [ $(docker network ls -f name="${SWARM_NETWORK}" |grep -v SCOPE |wc -l) -eq 0 ];then
    docker network create -d overlay "${SWARM_NETWORK}"
fi

if [ $(cat /root/.docker/config.json |grep "${REGISTRY_DOMAIN}" |wc -l) -eq 0 ];then
    docker login -u "${USERNAME}" -p "${PASSWORD}" "${REGISTRY_DOMAIN}"
fi

if [ $(docker service ls -f name="${NAME}" |grep -v MODE |wc -l) -eq 0 ];then
    docker service create --mount "type=bind,src=/data/wwwlogs,dst=/log" --replicas="${REPLICAS}" --with-registry-auth --name="${NAME}" --network="${SWARM_NETWORK}" -p ${PORT}:${INTERNAL_PORT} --limit-memory=4g -e "spring.profiles.active=${ENV}" -e "snow.workId=${WORKID}" --health-cmd="curl -s localhost:${INTERNAL_PORT} -o /dev/null -w "%{http_code}" |grep -E '404|200' ||exit 1" --health-interval=5s --health-retries=24 --health-timeout=2s "${IMAGE_ADDR}"

    docker service update --update-delay=30s --update-failure-action=rollback  --update-order=start-first "${NAME}"
fi

# if [ $# -gt 0 ];then
#     updateImageAddr="$1"
#     docker service update --image="${updateImageAddr}" "${NAME}"
# fi

if [ "${1}" = "update" ];then
    docker service update --image="${IMAGE_ADDR}" --with-registry-auth "${NAME}"
fi

docker images | grep '' | awk '{print $3}' | xargs docker rmi &>/dev/null
echo "done."

 

#3.  配置 jenkins 任务的 Build Triggers 和 Pipeline

i.  勾选 Build when a change is pushed to GitLab. GitLab webhook URL: http://192.168.xx.xx:18091/project/xxxxxxxx
ii.  在 gitlab 对应项目仓库的 Webhooks 填入上述链接,勾选 push events
iii.   Pipeline 配置示例如下,Repository URL 填写 #2 新建的仓库地址:

    使用 jenkins pipeline 在代码合并后自动打包并部署到 docker swarm