K8s与CICD 部署 - 3. Jenkins - 3.通用pipeline

43 阅读3分钟

这个pipeline是专门部署Java后端的

configMap

apiVersion: v1
kind: ConfigMap
metadata:
  name: maven-settings
  namespace: jenkins
data:
  settings.xml: |
    <?xml version="1.0" encoding="UTF-8"?>
    <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">

      <localRepository>/root/.m2/repository</localRepository>

      <!-- 1. 私有Nexus认证(你的账号密码) -->
      <servers>
        <server>
          <id>maven-public</id>
          <username>admin</username>
          <password>123456</password>
        </server>
      </servers>

      <!-- 2. 核心:解除HTTP拦截 + 公私包分流 -->
      <mirrors>
        <!-- 放行内网HTTP私有仓库,解除Maven拦截(关键!) -->
        <mirror>
            <id>maven-public</id>
            <name>私有Nexus仓库</name>
            <url>http://nexus-nexus-repository-manager.nexus.svc.cluster.local:8081/repository/maven-public/</url>
            <mirrorOf>maven-public</mirrorOf>
        </mirror>
        <!-- 公有依赖走阿里云,速度最快 -->
        <mirror>
            <id>aliyunmaven</id>
            <name>阿里云公共仓库</name>
            <url>https://maven.aliyun.com/repository/public</url>
            <mirrorOf>central,spring,plugins</mirrorOf>
        </mirror>
      </mirrors>

      <!-- 3. 私有仓库配置 -->
      <profiles>
        <profile>
          <id>nexus-profile</id>
          <repositories>
            <repository>
              <id>maven-public</id>
              <url>http://nexus-nexus-repository-manager.nexus.svc.cluster.local:8081/repository/maven-public/</url>
              <releases><enabled>true</enabled></releases>
              <snapshots>
                <enabled>true</enabled>
                <updatePolicy>always</updatePolicy>
              </snapshots>
            </repository>
          </repositories>
          <pluginRepositories>
            <pluginRepository>
              <id>maven-public</id>
              <url>http://nexus-nexus-repository-manager.nexus.svc.cluster.local:8081/repository/maven-public/</url>
              <releases><enabled>true</enabled></releases>
              <snapshots><enabled>true</enabled></snapshots>
            </pluginRepository>
          </pluginRepositories>
        </profile>
      </profiles>

      <activeProfiles>
        <activeProfile>nexus-profile</activeProfile>
      </activeProfiles>

    </settings>
❯ kubectl apply -f maven-settings-cm.yaml -n jenkins
configmap/maven-settings created

❯ pwd
/home/cy/workspace/k8s-helm/jenkins/configMap
❯ ls
docker-config-cm.yaml  maven-settings-cm.yaml
  ~/workspace/k8s-helm/jenkins/configMap ❯  
# docker-config-cm.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: docker-insecure-registry
  namespace: jenkins
data:
  daemon.json: |
    {
      "insecure-registries": ["harbor-core.harbor.svc.cluster.local:80"]
    }

❯ kubectl apply -f docker-config-cm.yaml -n jenkins
configmap/docker-insecure-registry created
  ~/workspace/k8s-helm/jenkins/configMap ❯  

docker推送基础镜像到harbor

# 修改宿主机的配置,让docker可以处理http请求cat /etc/docker/daemon.json
{
  "data-root": "/home/cy/docker",
  "insecure-registries": ["harbor.cyan.com","harbor-core.harbor.svc.cluster.local"]
}

在jenkins构建中直接使用宿主机的docker.socket

# 拉镜像
docker pull eclipse-temurin:21-jdk-alpine

# 1. 给镜像打标签(关键:格式 = harbor域名/项目名/镜像名:版本)
docker tag eclipse-temurin:21-jdk-alpine harbor.cyan.com/library/eclipse-temurin:21-jdk-alpine-3.23

# 2. 登录 Harbor(无需加 http!)
docker login harbor.cyan.com -u admin -p 123456

# 3. 推送镜像到 Harbor
docker push harbor.cyan.com/library/eclipse-temurin:21-jdk-alpine-3.23

kankio

Kaniko 是 Google 开源的、专为 Kubernetes 设计的无守护进程容器镜像构建工具,核心价值是在容器内安全构建镜像、无需 Docker Daemon、无需特权权限

❯ cat kankio-secret.sh
kubectl delete secret harbor-regcred -n jenkins --ignore-not-found=true

kubectl create secret docker-registry harbor-regcred \
  --namespace=jenkins \
  --docker-server=harbor.cyan.com \
  --docker-username=admin \
  --docker-password=123456
  ~/workspace/k8s-helm/jenkins/secret ❯  

clusterrolebinding

ClusterRoleBinding = 把 “集群级权限” 绑定给 “用户 / ServiceAccount / 用户组” 让某个账号能在整个 Kubernetes 集群里干活,而不是只在某个 namespace 里。


❯ cat clusterrolebinding.sh
kubectl create clusterrolebinding jenkins-cluster-admin \
  --clusterrole=cluster-admin \
  --serviceaccount=jenkins:jenkins
  ~/workspace/k8s-helm/jenkins/secret ❯  

pipeline

pipeline {
    agent {
        kubernetes {
            yaml """
apiVersion: v1
kind: Pod
metadata:
  name: dev-template
  namespace: jenkins
spec:
  serviceAccountName: jenkins 
  hostAliases:
  - ip: "10.0.0.2"
    hostnames:
    - "harbor.cyan.com"
  containers:
  - name: maven
    image: maven:3.9.12-eclipse-temurin-21
    command: ['cat']
    tty: true
    volumeMounts:
    - mountPath: /root/.m2/settings.xml
      name: maven-settings
      subPath: settings.xml
    - mountPath: /root/.m2/repository
      name: m2-cache
  - name: kaniko
    image: gcr.io/kaniko-project/executor:debug
    command: ['cat']
    tty: true
    volumeMounts:
    - mountPath: /kaniko/.docker
      name: docker-config
  - name: jnlp
    image: jenkins/inbound-agent:3355.v388858a_47b_33-2-jdk21
    args: ["$(JENKINS_SECRET)", "$(JENKINS_AGENT_NAME)"]
  - name: k8s-tools
    image: gcr.io/cloud-builders/kubectl
    command: ['cat']
    tty: true
    securityContext:
        runAsUser: 1000  
  volumes:
  - name: maven-settings
    configMap:
      name: maven-settings
      items:
      - key: settings.xml
        path: settings.xml
  - name: m2-cache
    hostPath:
      path: /home/cy/workspace/k8s-helm/jenkins/data
      type: DirectoryOrCreate
  - name: docker-config
    projected:
      sources:
      - secret:
          name: harbor-regcred
          items:
          - key: .dockerconfigjson
            path: config.json
"""
        }
    }
    parameters {
        // 选择环境
        choice(name: 'SPRING_PROFILE',choices: ['prod', 'dev'], description: '选择要执行的环境')
        // 这里会自动被脚本填充分支列表
        string(name: 'BRANCH', defaultValue: '1.0.0', description: 'GIT 分支')
    }
    environment {
        APP_NAME = "${JOB_NAME}"
        
        // 全程只用 harbor.cyan.com
        HARBOR_ADDR = "harbor.cyan.com"
        
        HARBOR_USER = "admin"                    
        HARBOR_PWD = "123456"                
        HARBOR_PROJECT = "library"               
        
        // 基础镜像
        JAVA_IMAGE = "${HARBOR_ADDR}/${HARBOR_PROJECT}/eclipse-temurin:21-jdk-alpine-3.23"
        
        // 构建产物
        IMAGE_NAME = "${JOB_NAME}"  
        IMAGE_TAG = "${BUILD_NUMBER}"
        LATEST_TAG = "latest"
        FULL_IMAGE = "${HARBOR_ADDR}/${HARBOR_PROJECT}/${IMAGE_NAME}:${IMAGE_TAG}"
        FULL_IMAGE_LATEST = "${HARBOR_ADDR}/${HARBOR_PROJECT}/${IMAGE_NAME}:${LATEST_TAG}"
        
        GIT_URL = "https://github.com/cyan-daimao/${JOB_NAME}.git"
        JAR_MODULE = "${JOB_NAME}-application"
        K8S_NAMESPACE = "${params.SPRING_PROFILE}"
        WORKSPACE = "/home/jenkins/agent/workspace/${JOB_NAME}"
    }

    stages {
        stage('拉取代码') {
            steps {
                container('maven') {
                    script {
                        sh """
                            git config --global --add safe.directory ${WORKSPACE}
                            rm -rf ${WORKSPACE}/*
                            git clone -b ${params.BRANCH} ${GIT_URL} ${WORKSPACE}
                            cd ${WORKSPACE}
                            echo "CODE_CHANGED=true" > ${WORKSPACE}/build-flag
                            git fetch --all
                        """
        
                        // 核心修复:转成 List,Jenkins 就不报错了
                        def branchList = sh(
                            script: """
                                cd ${WORKSPACE}
                                git branch -r | grep -v HEAD | sed 's/origin\///g' | sort -u
                            """,
                            returnStdout: true
                        ).trim().split('\n').toList() // 这里加 toList()
        
                        // 更新参数下拉框
                        properties([
                            parameters([
                                choice(name: 'SPRING_PROFILE', choices: ['prod', 'dev'], description: '环境'),
                                choice(name: 'BRANCH', choices: branchList, description: 'Git分支')
                            ])
                        ])
                    }
                }
            }
        }
        
        stage('Maven打包') {
            when {
                expression {
                    def flag = readFile("${WORKSPACE}/build-flag").trim()
                    return flag == "CODE_CHANGED=true"
                }
            }
            steps {
                container('maven') {
                    sh """
                        echo "=== 开始Maven打包 ==="
                        cd ${WORKSPACE}
                        mvn clean package -DskipTests

                        echo "=== 验证打包产物 ==="
                        ls -l ./${JAR_MODULE}/target/*.jar
                        JAR_FILE=$(ls ./${JAR_MODULE}/target/*.jar | grep -v "sources" | grep -v "test")
                        if [ -z "${JAR_FILE}" ]; then
                            echo "❌ 未找到有效的JAR包!"
                            exit 1
                        fi
                        echo "找到JAR包:${JAR_FILE}"
                    """
                }
            }
        }

        stage('构建并推送镜像') {
            when {
                expression {
                    def flag = readFile("${WORKSPACE}/build-flag").trim()
                    return flag == "CODE_CHANGED=true"
                }
            }
            steps {
                container('kaniko') {
                    sh """                
                        echo "=== 编写Dockerfile ==="
                        cd ${WORKSPACE}
                        JAR_FILE=$(ls ./${JAR_MODULE}/target/*.jar | grep -v "sources" | grep -v "test")
                        JAR_NAME=$(basename ${JAR_FILE})
                        
                        cat > Dockerfile << EOF
FROM ${JAVA_IMAGE}
WORKDIR /app
COPY ./${JAR_MODULE}/target/${JAR_NAME} /app/app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar","-Dspring.profiles.active=${params.SPRING_PROFILE}", "/app/app.jar"]
EOF

                        echo "=== 查看Dockerfile ==="
                        cat Dockerfile

                        echo "=== Kaniko 构建并推送镜像 ==="
                        /kaniko/executor \
                        --context ${WORKSPACE} \
                        --dockerfile ${WORKSPACE}/Dockerfile \
                        --destination ${FULL_IMAGE} \
                        --destination ${FULL_IMAGE_LATEST} \
                        --insecure \
                        --insecure-pull
                    """
                }
            }
        }

        stage('部署到k3s') {
            steps {
                container('k8s-tools') {  
                    sh """
                        set -e
                        set -x

                        echo "=== 创建dev命名空间(如果不存在) ==="
                        kubectl create namespace ${K8S_NAMESPACE} --dry-run=client -o yaml | kubectl apply -f -

                        echo "=== 创建镜像拉取密钥(用 harbor.cyan.com) ==="
                        kubectl create secret docker-registry harbor-regcred \
                          --namespace=${K8S_NAMESPACE} \
                          --docker-server=${HARBOR_ADDR} \
                          --docker-username=${HARBOR_USER} \
                          --docker-password=${HARBOR_PWD} \
                          --dry-run=client -o yaml | kubectl apply -f -

                        echo "=== 生成部署配置并应用 ==="
                        cd ${WORKSPACE}
                        cat > deploy.yaml <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ${APP_NAME}
  namespace: ${K8S_NAMESPACE}
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ${APP_NAME}
  template:
    metadata:
      labels:
        app: ${APP_NAME}
      annotations:
        build-number: "${BUILD_NUMBER}"
    spec:
      containers:
      - name: ${APP_NAME}
        image: ${FULL_IMAGE}
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
      imagePullSecrets:
      - name: harbor-regcred
---
apiVersion: v1
kind: Service
metadata:
  name: ${APP_NAME}-svc
  namespace: ${K8S_NAMESPACE}
spec:
  selector:
    app: ${APP_NAME}
  ports:
  - port: 8080
    targetPort: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ${APP_NAME}-ingress
  namespace: ${K8S_NAMESPACE}
spec:
  ingressClassName: nginx
  rules:
  - host: ${APP_NAME}-${K8S_NAMESPACE}.cyan.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: ${APP_NAME}-svc
            port:
              number: 8080
EOF

                        kubectl apply -f deploy.yaml -n ${K8S_NAMESPACE}
                        kubectl rollout restart deployment/${APP_NAME} -n ${K8S_NAMESPACE}
                        kubectl rollout status deployment/${APP_NAME} -n ${K8S_NAMESPACE}
                        
                        echo "✅ 部署成功!应用访问地址:http://${APP_NAME}-${K8S_NAMESPACE}.cyan.com"
                    """
                }
            }
        }
    }

    post {
        success {
            echo "🎉 部署成功!"
        }
        failure {
            echo "❌ 构建失败,请检查日志!"
        }
    }
}