Jenkins 配合Pipeline使用Docker


配合Pipeline使用Docker

許多組織使用Docker跨機器統一構建和測試環境,並為部署應用程序提供高效機制。從Pipeline 2.5及更高版本開始,Pipeline內置了從Jenkinsfile中與Docker交互的支持。下文將介紹從Jenkinsfile中使用Docker的基礎知識

定制執行環境

Pipeline的設計可以輕松地使用Docker鏡像作為單個Stage或整個 Pipeline 的執行環境。這意味着用戶可以定義管道所需的工具,而無需手動配置代理。

pipeline {
    agent {
        docker { image 'node:16.13.1-alpine' }
    }
    stages {
        stage('Test') {
            steps {
                sh 'node --version'
            }
        }
    }
}

當Pipeline執行時,Jenkins將自動啟動指定的容器並在其中執行預定義的步驟:

...略
+ docker inspect -f . node:16.13.1-alpine

Error: No such object: node:16.13.1-alpine
[Pipeline] isUnix
[Pipeline] sh
+ docker pull node:16.13.1-alpine
16.13.1-alpine: Pulling from library/node
59bf1c3509f3: Pulling fs layer
...略
503741069fa0: Pull complete
88b2b4880461: Pull complete
Digest: sha256:0e071f3c5c84cffa6b1035023e1956cf28d48f4b36e229cef328772da81ec0c5
Status: Downloaded newer image for node:16.13.1-alpine
[Pipeline] withDockerContainer
Jenkins does not seem to be running inside a container
$ docker run -t -d -u 0:0 -w /var/lib/jenkins/workspace/CI-Builder_testBranch -v /var/lib/jenkins/workspace/CI-Builder_testBranch:/var/lib/jenkins/workspace/CI-Builder_testBranch2:rw,z -v /var/lib/jenkins/workspace/CI-Builder_testBranch2@tmp:/var/lib/jenkins/workspace/CI-Builder_testBranch2@tmp:rw,z -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** node:16.13.1-alpine cat
$ docker top 90c6a3a70e38ad03c8318cedcf78df34e5b8533d634ec2356e8babc32742a74c -eo pid,comm
[Pipeline] {
[Pipeline] stage
[Pipeline] { (Test)
[Pipeline] sh
+ node --version
v16.13.1
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
$ docker stop --time=1 90c6a3a70e38ad03c8318cedcf78df34e5b8533d634ec2356e8babc32742a74c
$ docker rm -f 90c6a3a70e38ad03c8318cedcf78df34e5b8533d634ec2356e8babc32742a74c
[Pipeline] // withDockerContainer
[Pipeline] }
[Pipeline] // withEnv
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS

從輸出可知,Jenkins自動創建了指定鏡像的容器,並且在容器中執行指定Step,最后,停止並強制刪除創建的容器

工作空間同步

如果保持工作區與其他Stage同步很重要,請使用reuseNode true。否則,除了臨時工作空間,Docker化的Stage還可以在任何其他任意代理或同一代理上運行

默認的,對於容器化的Stage, Jenkin會執行以下動作:

  • 任選一個代理
  • 創建臨時工作空間
  • 克隆pipeline代碼到該工作空間
  • 加載該工作空間到容器

如果你有多個Jenkins代理,你的容器化Stage可以在其中任何一個代理上啟動

當設置reuseNode設置為true時:不會創建新的工作區,當前代理的當前工作區將被裝入容器,且將在同一節點上啟動該容器,所以整體數據將被同步

pipeline {
    agent any
    stages {
        stage('Build') {
            agent {
                docker {
                    image 'gradle:6.7-jdk11'
                    // Run the container on the node specified at the top-level of the Pipeline, in the same workspace, rather than on a new node entirely:
                    reuseNode true
                }
            }
            steps {
                sh 'gradle --version'
            }
        }
    }
}

為容器緩存數據

許多構建工具會下載外部依賴項並為了后續重用,會在本地緩存它們,以備將來重用。由於容器最初是用“干凈”的文件系統創建的,這可能會導致Pipeline運行速度變慢,因為它們可能無法利用后續Pipeline運行之間的磁盤緩存。

Pipeline支持添加傳遞給Docker的自定義參數,允許用戶指定要加載的自定義Docker 卷,該卷可用於在Pipeline運行之間緩存agent上的數據。下面的示例將在Pipeline運行之間為maven容器緩存~/.m2,從而避免了為后續Pipeline運行重新下載依賴項的需要

pipeline {
    agent {
        docker {
            image 'maven:3.8.1-adoptopenjdk-11'
            args '-v $HOME/.m2:/root/.m2'
        }
    }
    stages {
        stage('Build') {
            steps {
                sh 'mvn -B'
            }
        }
    }
}

使用多個容器

代碼庫依賴多種不同的技術已經變得越來越普遍。例如,源碼庫可能既有基於Java的后端API實現,也有基於JavaScript的前端實現。Docker和Pipeline的結合允許Jenkinsfile通過在不同stage使用不同的 agent {}指令來使用多種技術。

pipeline {
    agent none
    stages {
        stage('Back-end') {
            agent {
                docker { image 'maven:3.8.1-adoptopenjdk-11' }
            }
            steps {
                sh 'mvn --version'
            }
        }
        stage('Front-end') {
            agent {
                docker { image 'node:16.13.1-alpine' }
            }
            steps {
                sh 'node --version'
            }
        }
    }
}

使用Dockerfile

對於需要更定制的執行環境的項目,Pipeline還支持從源碼庫中的Dockerfile構建和運行容器。與之前使用“現成”容器的方法不同,使用代理 agent { dockerfile true }語法將從Dockerfile中構建新鏡像,而不是從Docker Hub中拉取鏡像。

在上面的示例的基礎上增加一個自定義的Dockerfile

FROM node:16.13.1-alpine

RUN apk add -U subversion

通過將上述文件提交到源存儲庫的根目錄,可以將Jenkins文件更改為基於此Dockerfile構建一個容器,然后使用該容器運行定義的步驟

pipeline {
    agent { dockerfile true }
    stages {
        stage('Test') {
            steps {
                sh 'node --version'
                sh 'svn --version'
            }
        }
    }
}

agent { dockerfile true } 語法支持許多其它選項,在 Pipeline 語法中 有更多關於這些選項的更詳細介紹。

腳本化Pipeline運行“sidecar”容器的高級用法

在Pipeline中使用Docker是運行構建或一組測試可能依賴的服務的有效方法。與sidecar模式類似,Docker Pipeline可以“在后台”運行一個容器,同時在另一個容器中執行工作。利用這種sidecar方法,PIpeline可以為每次PIpeline運行准備一個“干凈”的容器

備注:將本將屬於應用程序的功能拆分成單獨的進程,這個進程可以被理解為Sidecar

假設有一個集成測試套件,它依賴於本地MySQL數據庫運行。使用Docker Pipeline插件為支持腳本化Pipeline實現的withRun方法,Jenkinsfile可以將MySQL作為一個sidecar運行:

node {
    checkout scm
    /*
     * In order to communicate with the MySQL server, this Pipeline explicitly
     * maps the port (3306) to a known port on the host machine.
     */
    docker.image('mysql:5').withRun('-e "MYSQL_ROOT_PASSWORD=my-secret-pw" -p 3306:3306') { c ->
        /* Wait until mysql service is up */
        sh 'while ! mysqladmin ping -h0.0.0.0 --silent; do sleep 1; done'
        /* Run some tests which require MySQL */
        sh 'make check'
    }
}

這個例子可以更進一步,同時使用兩個容器。一個sidecar運行MySQL,另一個通過使用Docker容器鏈接提供 執行環境

node {
    checkout scm
    docker.image('mysql:5').withRun('-e "MYSQL_ROOT_PASSWORD=my-secret-pw"') { c ->
        docker.image('mysql:5').inside("--link ${c.id}:db") {
            /* Wait until mysql service is up */
            sh 'while ! mysqladmin ping -hdb --silent; do sleep 1; done'
        }
        docker.image('centos:7').inside("--link ${c.id}:db") {
            /*
             * Run some tests which require MySQL, and assume that it is
             * available on the host name db
             */
            sh 'make check'
        }
    }
}

上面的示例使用withRun暴露的對象,該對象通過id屬性提供運行容器的ID。使用容器的ID,Pipeline 可以通過向inside()方法傳遞自定義Docker參數來創建鏈接。

id屬性還可用於在管道退出之前檢查正在運行的Docker容器中的日志:

sh "docker logs ${c.id}"

注意:withRun塊內的shell步驟不是在容器內運行的,但它們可以使用本地TCP端口連接到容器

構建容器

為了創建Docker鏡像,Docker Pipeline插件還提供了一個build()方法,用於在PIpeline運行期間根據源碼庫中的Dockerfile創建新鏡像。

使用docker.build("my-image-name")語法的一個主要好處是腳本化Pipeline可以在后續Docker Pipeline調用中使用返回值,例如:

node {
    checkout scm

    def customImage = docker.build("my-image:${env.BUILD_ID}")

    customImage.inside {
        sh 'make test'
    }
}

返回值還可用於通過push()方法將Docker鏡像發布到Docker Hub自定義注冊中心, 例如:

node {
    checkout scm
    def customImage = docker.build("my-image:${env.BUILD_ID}")
    customImage.push()
}

鏡像“tags”的一個常見用法是為最近有效的Docker鏡像版本指定一個latest標簽。push()方法接收一個可選的tag參數,允許Pipeline推送攜帶不同標簽的自定義鏡像,例如:

node {
    checkout scm
    def customImage = docker.build("my-image:${env.BUILD_ID}")
    customImage.push()

    customImage.push('latest')
}

默認情況下,build()方法根據當前目錄中的 Dockerfile進行構建 。可以通過提供包含Dockerfile的目錄路徑作為 build()方法的第二個參數來覆蓋這一點,例如:

node {
    checkout scm
    def testImage = docker.build("test-image", "./dockerfiles/test")  // 從./dockerfiles/test/Dockerfile構建test-image

    testImage.inside {
        sh 'make test'
    }
}

可以通過將其他參數添加到 build()方法的第二個參數並將其傳遞給docker構建。但是需要注意的是,以這種方式傳遞參數時,字符串中的最后一個值必須是Dockerfile的路徑,並且該路徑必須以用作構建上下文的文件夾結尾。

下例通過傳遞-f參參數覆蓋默認的Dockerfile

node {
    checkout scm
    def dockerfile = 'Dockerfile.test'
    def customImage = docker.build("my-image:${env.BUILD_ID}", "-f ${dockerfile} ./dockerfiles") // 根據./dockerfiles/Dockerfile.test構建 my-image:${env.BUILD_ID}
}

使用遠程Docker服務

默認情況下,Docker Pipeline插件會與本地Docker守護進程通信,通常通過/var/run/Docker訪問。

如果要選擇非默認Docker服務器,例如使用 Docker Swarm,應使用withServer()方法。

通過將URI和在Jenkins中預先配置的Docker服務器證書身份驗證的憑據ID(可選)傳遞給方法:

node {
    checkout scm

    docker.withServer('tcp://swarm.example.com:2376', 'swarm-certs') {
        docker.image('mysql:5').withRun('-p 3306:3306') {
            /* do things */
        }
    }
}

注意:

inside()build()無法直接與Docker Swarm服務器一起正常工作

為了讓inside()工作,Docker服務器和Jenkins代理必須使用相同的文件系統,這樣才能裝載工作空間。

目前,Jenkins插件和Docker CLI都不會自動檢測遠程運行的服務器的文件系統;典型的症狀是嵌套的sh命令出錯,例如

cannot create /…@tmp/durable-…/pid: Directory nonexistent

當Jenkins檢測到代理本身正在Docker容器中運行時,它會自動將--volumes from參數傳遞給inside容器,確保它可以與代理共享一個工作空間。

此外,Docker Swarm的一些版本不支持自定義注冊中心。

使用自定義注冊中心

默認情況下,Docker Pipeline假定了Docker Hub的默認Docker注冊中心。為了使用自定義Docker注冊中心,腳本化Pipeline的用戶可以使用withRegistry()方法包裝步驟,傳遞自定義注冊中心 URL,例如:

node {
    checkout scm

    docker.withRegistry('https://registry.example.com') {

        docker.image('my-custom-image').inside {
            sh 'make test'
        }
    }
}

對於需要身份驗證的Docker注冊中心,從Jenkins主頁添加“用戶名/密碼”憑據項,並將憑據ID用作withRegistry()的第二個參數

node {
    checkout scm

    docker.withRegistry('https://registry.example.com', 'credentials-id') {

        def customImage = docker.build("my-image:${env.BUILD_ID}")

        /* Push the container to the custom Registry */
        customImage.push()
    }
}

在容器內運行構建步驟

Jenkins項目通常要求在構建過程中提供特定的工具集或庫。如果Jenkins中的許多項目都有相同的要求,並且代理很少,那么相應地預先配置這些代理並不困難。其他情況下,也可以將此類文件保存在項目源代碼控制中。最后,對於一些工具,尤其是那些具有獨立於平台的自包含下載的工具,比如Maven,可以使用Jenkins工具安裝程序系統和Pipeline tool步驟來按需檢索工具。然而,在許多情況下,這些技術不適用。

對於可以在Linux上運行的構建,Docker為這個問題提供了一個理想的解決方案。每個項目只需要選擇一個包含它所需的所有工具和庫的鏡像(這可能是像maven這樣的公開鏡像,也可能是由這個或另一個Jenkins項目創建的)有兩種方法可以在鏡像中運行Jenkins構建步驟。一種需要在鏡像中包含它所需的所有工具、運行環境,然后在鏡像中運行整個構建,另一種借助插件inside()方法,實現在任意鏡像中運行構建,和前者的區別在於后者可以不用提前在鏡像中包含所需要工具、運行環境,在運行時提供即可。,例如:

1

docker.image('maven:3.3.3-jdk-8').inside {
  git '…your-sources…'
  sh 'mvn -B clean install'
}

以上是一個完整的Pipeline腳本,inside將:

  1. 自動獲取代理和工作區(不需要額外的node塊)
  2. 將請求的鏡像拉取到Docker服務器(如果尚未緩存的話)
  3. 啟動一個運行該鏡像的容器
  4. 使用相同的文件路徑,將Jenkins工作區作為“volume”裝入容器中。
  5. 運行構建步驟。像sh這樣的外部進程將被包裝在docker exec中,以便在容器中運行。其他步驟(如測試報告)未經修改即可運行:它們仍然可以訪問由構建步驟創建的工作區文件。
  6. 運行完上述代碼塊結束時,停止容器並釋放其消耗的所有存儲。
  7. Record the fact that the build used the specified image。這將解鎖其他Jenkins插件中的功能:您可以使用鏡像跟蹤所有項目,或者將此項目配置為在更新的鏡像推送到Docker注冊表時自動觸發。如果您使用Docker Traceability plugin插件,還可以查看Docker服務器上鏡像的歷史記錄。

注意:如果你正在運行一個像Maven這樣有一個大的下載緩存的工具,在其鏡像中運行每次構建將意味着從網絡下載大量數據,這通常是不可取的。避免這種情況的最簡單方法是將緩存重定向到代理工作區,這樣,如果在同一個代理上運行另一個構建,它將運行得更快。就Maven而言:

docker.image('maven:3.3.3-jdk-8').inside {
  git '…your-sources…'
  writeFile file: 'settings.xml', text: "<settings><localRepository>${pwd()}/.m2repo</localRepository></settings>"
  sh 'mvn -B -s settings.xml clean install'
}

(如果希望在代理上的其他位置使用緩存位置,則需要傳遞一個額外的--volume選項給inside,以便容器可以看到該路徑)

其它解決方案是傳遞一個參數給inside以加載共享卷,比如 -v m2repo:/m2repo,並使用該路徑作為 localRepository。要注意的是,Maven中默認的本地存儲庫管理對於並發構建來說並不是線程安全的,nstall:install 安裝可能會跨構建甚至跨Job污染本地存儲庫。最安全的解決方案是使用倉庫鏡像作為緩存。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM