Administrator
发布于 2025-06-16 / 14 阅读
0
0

一条会说话的电商后端流水线:Jenkins + Docker + Kubernetes + Discord

从 代码仓库 拉取 release 分支代码,构建出带有“提交哈希 + 时间戳”的不可变镜像标签,将镜像推送到 digitalocean 私有镜像仓库,然后直接更新线上 k8s deployment 资源,完成滚动升级。

在部署成功或失败后,通过 Discord Webhook 主动把状态、提交信息和操作者推送到团队频道,同时在 post { always } 中清理本地镜像,避免 Jenkins 节点磁盘被长时间累积的镜像拖垮。

在凭证管理上,通过 jenkins界面选择 Username with password,将 DigitalOcean Registry 的用户名和 Access Token 以全局凭证形式保存,并指定一个语义清晰的 id,之后在 pipeline 中只暴露这个 id,而不直接暴露账号和密码,这种“凭证中心化 + id 引用”的方式也适用于 git、harbor 等多种私有仓库,具有通用性。

pipeline {
    agent any
    
    environment {
        REPOSITORY_ADDR = "registry.digitalocean.com/registry-yc"
        PROJECT_NAME = "b2c-backend"
        DEPLOYMENT_NAME = "b2c-backend"
        CONTAINER_NAME = "backend"
        NAMESPACE_NAME = "b2c"
        WEBHOOK_URL = "https://discord.com/api/webhooks/14047768xxxxxxxx/xxxxxxxxxxx"
        TITLE = "DIGITALOCEAN-ADMIN"
        PROXY_URL = "http://192.168.2.30:10808"
        API_SERVER="https://77d0ef66-6a5e-4cdf-9da7-31635991c4e4.k8s.ondigitalocean.com"
        KUBECTL_CREDENTIAL="DIGITALOCEAN-CLUSTER"
        GIT_BRANCHE = "release"
        GIT_CREDENTIALID = "gitea_access_token"
        GIT_REPOSITORY_URL = "http://gitea.repository.yc/mall-b2c/mall-b2c-backend.git"
    }
    
    stages {

        stage('Pull') {
            steps {
                git branch: "${GIT_BRANCHE }", credentialsId: "${GIT_CREDENTIALID }", url: "${GIT_REPOSITORY_URL }"
            }
        }
    
        stage('Prepare') {
            steps {
                script {
                    // 获取当前最新 commit 哈希(短版本,更易读)
                    env.COMMIT_HASH = sh(
                        script: 'git rev-parse --short HEAD',
                        returnStdout: true
                    ).trim()
                    
                    // 生成时间戳
                    env.TIMESTAMP = new Date().format('yyyyMMdd-HHmmss')
                    
                    // 组合成最终的IMAGE_TAG
                    env.IMAGE_TAG = "${env.COMMIT_HASH}-${env.TIMESTAMP}"
                    
                    echo "当前最新提交哈希:${env.COMMIT_HASH}"
                    echo "构建时间戳:${env.TIMESTAMP}"
                    echo "最终镜像标签:${env.IMAGE_TAG}"
                    echo "Building docker image: ${PROJECT_NAME}:${env.IMAGE_TAG}"
                }
            }
        }
        
        stage('Build Docker Image') {
            steps {
                script {
                    sh """
                        docker build --no-cache -t ${PROJECT_NAME}:${env.IMAGE_TAG} -f Dockerfile-cicd . --network host
                    """
                }
            }
        }
        
        stage('Push to Registry') {
            steps {
                script {
                    withCredentials([usernamePassword(credentialsId: 'DIGITALOCEAN-REGISTRY', 
                                                    passwordVariable: 'REGISTRY_PASSWORD', 
                                                    usernameVariable: 'REGISTRY_USERNAME')]) {
                        sh """
                            echo "Logging in to Docker registry"
                            echo \$REGISTRY_PASSWORD | docker login ${REPOSITORY_ADDR} -u \$REGISTRY_USERNAME --password-stdin
                            
                            echo "Pushing docker image to registry"
                            docker tag ${PROJECT_NAME}:${env.IMAGE_TAG} ${REPOSITORY_ADDR}/${PROJECT_NAME}:${env.IMAGE_TAG}
                            docker push ${REPOSITORY_ADDR}/${PROJECT_NAME}:${env.IMAGE_TAG}
                            
                            echo "Logging out from Docker registry"
                            docker logout ${REPOSITORY_ADDR}
                        """
                    }
                }
            }
        }

        
        stage('Update Kubernetes Deployment') {
            steps {
                script {
                    // 使用withKubeConfig注入凭证
                    withKubeConfig([
                        credentialsId: "${KUBECTL_CREDENTIAL}",
                        serverUrl: "${API_SERVER}"
                    ]) {
                    sh """
                        echo "Updating deployment image"
                        kubectl -n ${NAMESPACE_NAME} set image deployment/${DEPLOYMENT_NAME} ${CONTAINER_NAME}=${REPOSITORY_ADDR}/${PROJECT_NAME}:${env.IMAGE_TAG}
                    """
                    }
                }
            }
        }
        
        stage('Add Change Cause') {
            steps {
                script {
                    echo "add CHANGE CAUSE"
                    def commitMessage = sh(
                        script: 'git show -s --pretty=%s',
                        returnStdout: true
                    ).trim()
                
                    // 使用withKubeConfig注入凭证
                    withKubeConfig([
                        credentialsId: "${KUBECTL_CREDENTIAL}",
                        serverUrl: "${API_SERVER}"
                    ]) {
                    sh """
                        echo "Updating deployment image"
                        kubectl annotate deployment/${DEPLOYMENT_NAME} -n ${NAMESPACE_NAME} kubernetes.io/change-cause="${commitMessage}"
                        echo "Deployment image updated successfully."
                    """
                    }
                }
            }
        }
    }
    
    post {
        success {
            script {
                sendDiscordNotification('success')
            }
        }
        failure {
            script {
                sendDiscordNotification('failure')
            }
        }
        always {
            // 清理本地镜像(可选)
            script {
                sh """
                    docker rmi ${PROJECT_NAME}:${env.IMAGE_TAG} || true
                    docker rmi ${REPOSITORY_ADDR}/${PROJECT_NAME}:${env.IMAGE_TAG} || true
                """
            }
        }
    }
}

def sendDiscordNotification(String status) {
    script {
        try {
            def epoch = sh(script: 'date +%s', returnStdout: true).trim()
            def commitMessage = sh(script: 'git show -s --pretty=%s', returnStdout: true).trim()
            def commitUser = sh(script: 'git show --format="%an <%ae>" --no-patch', returnStdout: true).trim()
            
            // 转义特殊字符,避免JSON解析错误
            commitMessage = commitMessage.replace('"', '\\"').replace('\n', '\\n')
            commitUser = commitUser.replace('"', '\\"')
            
            def color = status == 'success' ? '3066993' : '15158332'
            def emoji = status == 'success' ? '🎉' : '❌'
            def statusText = status == 'success' ? '✅ Success' : '❌ Failed'
            def actionText = status == 'success' ? '构建完成' : '构建失败'
            
            // 使用heredoc方式创建JSON,避免引号转义问题
            def jsonFile = 'discord_payload.json'
            writeFile file: jsonFile, text: """
{
  "content": "@everyone",
  "username": "CI · Build Bot",
  "avatar_url": "https://i.imgur.com/your-bot-avatar.png",
  "embeds": [
    {
      "title": "${emoji} ${TITLE} ${actionText}",
      "description": "**状态:** ${statusText}\\n**结束时间:** <t:${epoch}:F>\\n**提交描述:** ${commitMessage}\\n**提交用户:** ${commitUser}",
      "color": ${color}
    }
  ]
}
"""
            
            echo "发送Discord通知..."
            echo "----------------------------------------------------------------------------------"
            
            // 使用文件方式传递JSON数据,避免命令行引号问题
            def curlResult = sh(
                script: """
                    curl --connect-timeout 10 --retry 5 --retry-all-errors --retry-delay 2 \\
                         --proxy "${PROXY_URL}" \\
                         -s -w "%{http_code}" -o /tmp/discord_response.txt \\
                         -X POST \\
                         -H "Content-Type: application/json" \\
                         -d @${jsonFile} \\
                         "${WEBHOOK_URL}"
                """,
                returnStdout: true
            ).trim()
            
            // 检查HTTP响应状态
            if (curlResult == "200" || curlResult == "204") {
                echo "✅ Discord通知发送成功 (HTTP ${curlResult})"
            } else {
                error "❌ Discord通知发送失败 (HTTP ${curlResult})"
            }
            
            // 清理临时文件
            sh "rm -f ${jsonFile} /tmp/discord_response.txt"
            
        } catch (Exception e) {
            echo "⚠️ Discord通知发送失败: ${e.getMessage()}"
            echo "继续执行流水线..."
        }
    }
}


评论