devops-5:從0開始構建一條完成的CI CD流水線

2023-04-20 18:01:35

從0開始構建一條完成的CI CD流水線

前文中已經講述了靜態、動態增加agent節點,以動態的k8s cloud為例,下面就以Maven構建Java程式為例,開始構建出一條完整的CI CD流水線。
實現功能目標:
 1.分別可以根據分支和tag從原始碼倉庫clone程式碼
 2.拿到原始碼後開始編譯
 3.構建image,並push到映象倉庫
 4.部署到對應k8s叢集
 5.部署成功後,釘釘告警
以上是此pipeline實現的功能,後續計劃:
 1.通過webhooks實現原始碼倉庫push程式碼後,自動出發pipeline執行
 2.增加SonarQube程式碼質量檢測步驟
 3.配合argoCD實現自動CD
 
後續文章會陸續更新,敬請期待。
映象準備
以k8s cloud當做agent的話,肯定需要一個基礎映象,映象中需要有git、java和maven這些必要的工具環境,當然,可以使用jenkins提供的tools功能來設定工具匯入到環境中,例如:
    tools {
        maven 'apache-maven-3.8.6' 
    }
這種方式是比較方便的,但是每到一個新宿主機都要去主動下載一遍,還需要在jenkins中設定下載地址和方式,遷移時也比較麻煩,所以這裡就採用一勞永逸的方法,把這些環境都提前打包到agent的映象中,方便以後使用。
這裡選用的基礎映象是jenkins官方的agent映象:
docker pull jenkins/agent:latest
這個官方映象內已經包含git、java環境,以及後邊需要和jenkins master建立連線的agent.jar包,所以我們只需再將maven包打包進去即可。
 
maven工具包的準備
官方下載maven包:
wget https://dlcdn.apache.org/maven/maven-3/3.8.6/binaries/apache-maven-3.8.6-bin.tar.gz
 
maven的設定(可選)
很多java程式會用maven來進行構建,maven中又存在很多依賴元件(常用的是jar包、war包、pom等,也可把Zip包等通過POM檔案定義為依賴元件),這個時候就會有一個倉庫的概念,這個倉庫分為三種型別,即:
  1. central:中央倉庫,是由Maven社群提供的資源倉庫,它包含了大量的常用程式庫元件(jar包)。預設Maven的中央倉庫地址為:http://repo1.maven.org/maven2/
  2. local:本地倉庫,是存放maven環境原生的一個資料夾,此資料夾在第一次執行Maven命令時就建立了。Maven在執行構建任務時,根據依賴關係從中心倉庫、或遠端倉庫下載依賴元件到本地倉庫,然後本地倉庫的內容供專案參照。
  3. remote:遠端倉庫,例如專案需要指定外部其他公司、或開源組織的jar包,這些依賴元件通用性等原因,未納入Maven中央倉庫,這個時候就要手動指定一個私有的遠端倉庫來拉取依賴。
 
修改Maven中央倉庫地址
一般Maven的中央倉庫由於網路問題會存取不到,這個時候可以修改地址為國內的Maven倉庫地址或者公司私有的倉庫地址,例如阿里的:http://maven.aliyun.com/nexus/content/groups/public
修改步驟是要修改apache-maven-3.8.6/conf/settings.xml檔案中以下欄位:
0
修改為:
    <mirror>
      <id>nexus-aliyun</id>
      <mirrorOf>central</mirrorOf>
      <name>Nexus aliyun</name>
      <url>http://maven.aliyun.com/nexus/content/groups/public</url>
    </mirror>
若修改後未生效,可以檢查程式碼pom.xml中是否指定了倉庫地址,類似語句:
<repositories>
    <repository>
        <id>springsource-repos</id>
        <name>SpringSource Repository</name>
        <url>http://repo.spring.io/release/</url>
    </repository>
</repositories>

 

修改Maven本地倉庫路徑
Maven本地倉庫路徑預設為 ${user.home}/.m2/repository
0
可以直接在此進行修改,也可以在構建時用引數指定:
mvn clean install -Dmaven.repo.local=/home/maven/local_repo/
也可以在構建時指定組態檔地址:
mvn clean install -s /home/maven/settings.xml

 

開始構建映象
準備好的物料包及Dockerfile:
[root@node01 agent-jenkins]# ls
apache-maven-3.8.6.tar.gz  Dockerfile  jenkins-agent kubectl.tar.gz
這裡要說下jenkins-agent這個指令碼檔案,這個指令碼檔案也是官方提供的,原始碼檔案在這裡:https://github.com/jenkinsci/docker-inbound-agent,這是專門用來agent連線jenkins master的,採用的jnlp的方式。
 
檢視Dockerfile內容
[root@node01 agent-jenkins]# cat Dockerfile 
FROM jenkins/agent:latest

USER root

ADD apache-maven-3.8.6.tar.gz /opt/
ADD kubectl.tar.gz /usr/local/bin/
ENV PATH $PATH:/opt/apache-maven-3.8.6/bin/
COPY jenkins-agent /usr/local/bin/
CMD ["/bin/sh","-c","/usr/local/bin/jenkins-agent"]
用於CD環節的工具,這裡新增了kubectl命令,可根據需要新增。
 
構建映象
# docker build -t registry.example.com:5000/jenkins/agent:v1 .
# docker push registry.example.com:5000/jenkins/agent:v1
registry.example.com:5000 是我的私有倉庫
 
設定k8s cloud的pod Template
前邊映象準備完畢,下邊要準備一個pod yaml模板,來執行每次臨時加入和執行job的agent,預設情況下,k8s cloud會有一個名稱為jnlp的容器專門來和jenkins master連線,然後我們可以再啟動一個容器專門來跑Pipeline的job,但這裡有一點要注意,如果pod中有多個容器,我們需要在Pipeline中指定某個在哪個容器中執行,這個具體怎麼指定後邊再說,我們這裡採用覆蓋截jnlp容器的方式來實現全部的工作都由一個container來完成,最終pod Template如下:
apiVersion: "v1"
kind: "Pod"
metadata:
  name: jenkins-agent
  namespace: "default"
spec:
  containers:
  - env:
    - name: "MAVEN_HOME"
      value: "/opt/apache-maven-3.8.6/"
    image: "registry.example.com:5000/jenkins/agent:v1"
    imagePullPolicy: "IfNotPresent"
    name: "jnlp"
    resources:
      limits:
        memory: "2G"
        cpu: "1500m"
      requests:
        memory: "1G"
        cpu: "100m"
    volumeMounts:
    - mountPath: "/root/.m2"
      name: "m2"
      readOnly: false
    - mountPath: "/home/jenkins/agent"
      name: "workspace-volume"
      readOnly: false
    - mountPath: "/usr/bin/docker"
      name: "docker-client"
      readOnly: true
    - mountPath: "/var/run/docker.sock"
      name: "docker-engine"
      readOnly: true
  volumes:
  - hostPath:
      path: "/root/.m2"
      type: "DirectoryOrCreate"
    name: "m2"
  - hostPath:
      path: "/home/jenkins"
    name: "workspace-volume"
  - hostPath:
      path: "/usr/bin/docker"
      type: File
    name: "docker-client"
  - hostPath:
      path: "/var/run/docker.sock"
      type: Socket
    name: "docker-engine"
這裡有四個volume:
  • m2:這個是用作maven的本地倉庫路徑,使用hostpath掛載到了本地目錄,當然也可以儲存到某些共用儲存中,目的就是讓依賴包只下載一次。
  • workspace-volume:這個是將jenkins的工作目錄也使用hostpath掛載。
  • docker-client:docker命令的掛載,用於build、push等命令
  • docker-engine:docker engine的掛載,用於build、push等
 
gitlab專案克隆
simple-java-maven-app專案地址:https://github.com/jenkins-docs/simple-java-maven-app,將此專案克隆到本地gitlab即可。
網存取github慢的話,可以git我的碼雲:https://gitee.com/vfancloud/simple-java-maven-app.git
 
Pipeline編寫
建立憑證
1.程式碼倉庫我們使用前邊搭建的gitlab,需要提前將gitlab的使用者憑證在Jenkins建立好,方便後邊Jenkins下載程式碼使用:
系統管理—>憑證管理—>建立Username with password型別憑證(id需要記住,Pipeline中會使用)
0
 
2.我們的服務是部署在k8s叢集中,所以還需要目標k8s的kubeconfig憑證,用來管理操控目標k8s:
系統管理—>憑證管理—>建立Secret file型別憑證
0
一般專案都會有多個環境,所以每個環境的kubeconfig憑證都要提前建立好。
 
3.映象倉庫的賬號密碼也要提前準備好,Username with password型別即可。
0
安裝外掛
一些常用的必須外掛,要提前安裝:
  1. Git
  2. Git Parameter
  3. DingTalk
  4. build user vars plugin
Pipeline
此Pipeline起一個範例效果,有些功能點可以省略或者選擇使用,酌情增刪即可:
pipeline {
    agent {
      kubernetes {
        cloud 'kubernetes-internal'  //指定cloud name
        inheritFrom 'jenkins-agent'  //指定podTemplate,新版本已經不再用label指定
        namespace 'default'
      }
    }
    environment {
        GIT_CERT = credentials('vfan-gitlab')  //gitlab使用者憑證
        HARBOR_HOST = 'registry.example.com:5000'
        SERVER_NAME = 'simple-java-maven-app'
    }
/*    tools {
        maven 'apache-maven-3.8.6' 映象有maven環境了,可以不指定
    } */   
    options {
        buildDiscarder(logRotator(numToKeepStr: '10'))  //保持歷史構建的最大個數
        timeout(20)  //預設單位分鐘,20分鐘
        timestamps()  //Pipeline開始時間以及每個step執行開始時間
    }
    parameters {
        choice(
            name: 'GIT_REPO_URL',
            choices: 'http://10.85.122.128:880/vfan/simple-java-maven-app.git',
            description: 'Git Repo example environment'
        )
        choice(
            name: 'GIT_TYPE',
            choices: ['branch', 'tag'],
            description: 'Git Repo example brance'
        )
        choice(
            name: 'GIT_REPO_BRANCE',
            choices: ['master', 'dev', 'test'],
            description: 'Git Repo example brance'
        )
        gitParameter name: 'GIT_TAG',
            type: 'PT_TAG',
            branch: 'master',
            branchFilter: '.*',
            defaultValue: '',
            selectedValue: 'TOP',
            sortMode: 'DESCENDING_SMART',
            listSize: '1',
                description: 'Select you git tag.'
        choice(
            name: 'ENVIRONMENT', 
            choices: ['INT', 'DEV', 'PROD'], 
            description: 'Select deployment environment'
        )
    }
    stages {
        stage('git clone branch') {
            when {
                 expression { params.GIT_TYPE == "branch" }
            }
            steps {
                git(
                    branch: params.GIT_REPO_BRANCE, 
                    credentialsId: env.GIT_CERT, 
                    url: params.GIT_REPO_URL
                )
            }
            post {
                success {
                    sh '''
                    echo "use branch build"
                    git status
                    '''
                }
            }
        }
        stage('git clone tag') {
            when {
                 expression { params.GIT_TYPE == "tag" }
            }
            steps {
              checkout([$class: 'GitSCM', 
              branches: [[name: "${GIT_TAG}"]], 
              userRemoteConfigs: [[credentialsId: env.GIT_CERT, url: params.GIT_REPO_URL]]])
            }
            post {
                success {
                    sh '''
                    echo "use tag build"
                    git status
                    '''
                }
            }
        }
        stage('Maven Build') {
            steps {
                sh 'mvn -B -DskipTests clean package'
            }
        }
        stage('Test') {
            steps {
                sh 'mvn test'
            }
            post {
                always {
                    junit 'target/surefire-reports/*.xml'
                }
            }
        }
        stage('Deliver') {
            steps {
                sh './jenkins/scripts/deliver.sh'
            }
        }
        stage('Docker build && push') {
            steps {
                withCredentials([usernamePassword(credentialsId: 'harbor-auth', passwordVariable: 'HARBOR_PASSWD', usernameVariable: 'HARBOR_USER')]) {
                    sh '''
                        echo "Other operations..."
                        echo "Start building..."
                        date -d "+8 hour" +%Y%m%d_%H%M%S > /tmp/date
                        BUILD_TIME=`cat /tmp/date`
                        docker build --build-arg APP_NAME=simple-java-maven-app -t ${HARBOR_HOST}/${SERVER_NAME}:${GIT_REPO_BRANCE}_${BUILD_TIME} .
                        echo "Build complete."
                        docker login $HARBOR_HOST -u $HARBOR_USER -p $HARBOR_PASSWD
                        docker push ${HARBOR_HOST}/${SERVER_NAME}:${GIT_REPO_BRANCE}_${BUILD_TIME}
                        docker rmi ${HARBOR_HOST}/${SERVER_NAME}:${GIT_REPO_BRANCE}_${BUILD_TIME}
                    '''
                }
            }
        }
        stage('Deploy to k8s'){
            input{
                message "Should we continue deploy?"
                ok "Yes, we should."

            }
            environment {
                // 提前建立好secret file型別的憑據
                KUBE_CONFIG_INT = credentials('mycluster_int')
                // KUBE_CONFIG_DEV = credentials('mycluster_dev')
                // KUBE_CONFIG_PROD = credentials('mycluster_prod')
            }
            steps{
                sh'''
                    BUILD_TIME=`cat /tmp/date`
                    case $ENVIRONMENT in
                        "INT")
                            kubectl set image deployment ${SERVER_NAME} --kubeconfig=${KUBE_CONFIG_INT} app=${HARBOR_HOST}/${SERVER_NAME}:${GIT_REPO_BRANCE}_${BUILD_TIME}
                            kubectl rollout status deployment ${SERVER_NAME} --kubeconfig=${KUBE_CONFIG_INT}
                        ;;
                        "DEV")
                            kubectl set image deployment ${SERVER_NAME} --kubeconfig=${KUBE_CONFIG_DEV} app=${HARBOR_HOST}/${SERVER_NAME}:${GIT_REPO_BRANCE}_${BUILD_TIME}
                            kubectl rollout status deployment ${SERVER_NAME} --kubeconfig=${KUBE_CONFIG_DEV}
                        ;;
                    esac
                    echo "Deployment complete."
                '''
            }
        }
    }
    post { 
        success{ 
            echo 'Deployment succeeded.'
            dingtalk (
                robot: 'myapp-dingding-robot',
                type: 'MARKDOWN',  // 發什麼型別的訊息,有TEXT、LINK、MARKDOWN、和ACTION_CARD,參考https://jenkinsci.github.io/dingtalk-plugin/guide/pipeline.html
                at: [],
                atAll: false,
                title: 'Jenkins發版成功',
                text: [
                    "## 構建結果:**${currentBuild.result}**",
                    '---',
                    "## 構建資訊",
                    '---',
                    "- 專案名稱:${SERVER_NAME}",
                    "- 構建環境:${ENVIRONMENT}",
                    "- 構建分支:${GIT_REPO_BRANCE}",
                    "- 構建標籤:${GIT_TAG}",
                    "- 專案地址:${GIT_REPO_URL}",
                    "- 構建使用者:${env.BUILD_USER}"
                    ],
            //    messageUrl: '',
            //    picUrl: '',
            //    singleTitle: '',
            //    btns: [],
            //    btnLayout: '', 
            //    hideAvatar: false
            )
        }
        failure{
            echo "Deployment failed."
            dingtalk (
                robot: 'myapp-dingding-robot',
                type: 'MARKDOWN',  // 發什麼型別的訊息,有TEXT、LINK、MARKDOWN、和ACTION_CARD,參考https://jenkinsci.github.io/dingtalk-plugin/guide/pipeline.html
                at: [],
                atAll: false,
                title: 'Jenkins發版失敗',
                text: [
                    "## 構建結果:**${currentBuild.result}**",
                    '---',
                    "## 構建資訊",
                    '---',
                    "- 專案名稱:${SERVER_NAME}",
                    "- 構建環境:${ENVIRONMENT}",
                    "- 構建分支:${GIT_REPO_BRANCE}",
                    "- 構建標籤:${GIT_TAG}",
                    "- 專案地址:${GIT_REPO_URL}",
                    "- 構建使用者:${env.BUILD_USER}"
                    ],
            //    messageUrl: '',
            //    picUrl: '',
            //    singleTitle: '',
            //    btns: [],
            //    btnLayout: '', 
            //    hideAvatar: false
            )
        }
    }
}

 

測試執行Pipeline
0
執行完成,釘釘也已收到通知,後續更新更多內容。