使用 Docker 全自動構建 Java 應用
- 2019 年 12 月 23 日
- 筆記
這次的流水線中,我們使用 Docker 容器來構建我們的 Java 應用。
我們會在 Docker 容器里運行 Jenkins,再使用 Jenkins 啟動一個 Maven 容器,用來編譯我們的程式碼,接著在另一個 Maven 容器中運行測試用例並生成製品(例如 jar 包),然後再在 Jenkins 容器中製作 Docker 鏡像,最後將鏡像推送到 Docker Hub。
我們會用到兩個 Github 倉庫。
- Jenkins-complete:這是主倉庫,包含了啟動 Jenkins 容器所需的配置文件。
- Simple-java-maven-app:使用 Maven 創建的 簡單的 Java 應用。
在搭建之前,我們先來了解一下這兩個倉庫。
了解 Jenkins-complete
這是我們構建 Jenkins 鏡像的核心倉庫,它包含了所需的配置文件。我們通過 Jenkins 官方提供的 Docker 鏡像啟動 Jenkins 容器,然後完成一些動作,例如安裝插件、創建用戶等。
安裝好之後,我們會創建用來獲取 Java 應用的 Github 憑據,還有推送鏡像到 Dockerhub 的 Docker 憑據。最後,開始創建我們應用的流水線 job。
這個過程很長,我們的目標是讓所有這些事都自動化。主倉庫包含的文件和詳細配置會用來創建鏡像。當創建好的鏡像啟動運行以後,我們就有了:
- 新創建的 admin/admin 用戶
- 已經裝好的一些插件
- Docker 和 Github 憑據
- 新創建的名為 sample-maven-job 的流水線。
如果把源碼列成樹狀,就看到下面的結構:
jagadishmanchala@Jagadish-Local:/Volumes/Work$ tree jenkins-complete/ jenkins-complete/ ├── Dockerfile ├── README.md ├── credentials.xml ├── default-user.groovy ├── executors.groovy ├── install-plugins.sh ├── sample-maven-job_config.xml ├── create-credential.groovy └── trigger-job.sh
我們來看看它們都是幹嘛的:
- default-user.groovy – 這個文件用來創建默認用戶 admin/admin。
- executors.groovy – 這個 Groovy 腳本設置 Jenkins 的執行器數量為 5。一個 Jenkins 執行器相當於一個處理進程,Jenkins job 就是通過它運行在對應的 slave/agent 機器上。
- create-credential.groovy – 用來創建 Jenkins 全局憑據的 Groovy 腳本。這個文件可以創建任意的 Jenkins 全局憑據,包括 Docker hub 憑據。我們要修改文件里 Docker hub 的用戶名密碼,改成我們自己的。這個文件會被複制到鏡像里,然後在 Jenkins 啟動時運行。
- credentials.xml – XML 憑據文件。這個文件包含了 Github 和 Docker 憑據。它看起來是這樣的:
<com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl> <scope>GLOBAL</scope> <id>github</id> <description>github</description> <username>jagadish***</username> <password>{AQAAABAAAAAQoj3DDFSH1******</password> </com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl>
仔細觀察上面的程式碼,我們可以看到一個 id 為 「github」 的用戶名以及加密後的密碼。這個 id 很重要,我們會在後面的流水線中用到。
怎樣拿到加密後的密碼呢?
想要拿到密碼加密後的內容,你需要到這裡去 Jenkins server -> Manage Jenkins -> Script console
,然後在輸入框里輸入下面的程式碼
import hudson.util.Secret def secret = Secret.fromString("password") println(secret.getEncryptedValue())
將 「password」 換成你自己的密碼,點擊運行,你就得到了加密後的內容。再把這個內容粘貼到 credentials.xml
文件裡面就可以了。
DockerHub 的密碼加密過程同上。
- sample-maven-job_config.xml – 這個 XML 文件包含了流水線 job 的細節內容。Jenkins 會在
Jenkins console
里創建一個名為「sample-maven-job」的 job,這個文件包含了它的詳細配置。
這個配置很簡單,Jenkins 讀取文件後,會先創建一個名為 「sample-maven-job」 的流水線 job,然後把倉庫指向 Github。一併設置的還有名為 「github」 的憑據 id。看起來像是這個樣子:
配置好倉庫地址以後,用來遠程觸發 job 的 token 也就生成了。為了設置遠程觸發,我們需要打開 「Trigger builds remotely」 選項, 然後把上面的 token 設置到這裡。這些配置可以在流水線配置頁面的 「Build Triggers」 那一節中看到。為了在後面的 shell 腳本中用這個 token 觸發 job, 我們把這個 token 命名為 「MY-TOKEN」。
- trigger-job.sh – 這是一個簡單的 shell 腳本,其中的 curl 命令用來觸發 job。
雖然,我們在容器里創建了 Jenkins 服務和一個 job,我們還需要一個觸發器來觸發整個自動構建。我喜歡下面的方法:
- 啟動 Jenkins Docker 容器時,完成所有需要做的事,例如創建 job、憑據、用戶等。
- 當容器啟動好後觸發 job。
我寫的這個簡單 shell 腳本就是用來在容器啟動好以後觸發 job 的。shell 腳本用 curl 向 Jenkins 發送了一個 post 請求命令。內容像這樣。
- Install-plugins.sh – 這是我們用來安裝所有所需插件的腳本。我們會把這個腳本複製到 Jenkins 鏡像,並把插件名作為它的參數。容器啟動好以後,這個腳本就會根據插件名對應的插件。
- Dockerfile – 這是自動化過程中最重要的文件。我們會用這個 Docker 文件來創建完整的 Jenkins 服務和所有配置。理解這個文件對於編寫你自己的自動化構建是很重要的。
FROM jenkins/jenkins:lts ARG HOST_DOCKER_GROUP_ID # 使用內置的 install-plugins.sh 腳本安裝我們所需的插件 RUN install-plugins.sh pipeline-graph-analysis:1.9 cloudbees-folder:6.7 docker-commons:1.14 jdk-tool:1.2 script-security:1.56 pipeline-rest-api:2.10 command-launcher:1.3 docker-workflow:1.18 docker-plugin:1.1.6 # 設置 admin 用戶的環境變數 ENV JENKINS_USER admin ENV JENKINS_PASS admin # 跳過初始設置嚮導 ENV JAVA_OPTS -Djenkins.install.runSetupWizard=false # 啟動腳本,設置執行器的數量、創建 admin 用戶 COPY executors.groovy /usr/share/jenkins/ref/init.groovy.d/ COPY default-user.groovy /usr/share/jenkins/ref/init.groovy.d/ COPY create-credential.groovy /usr/share/jenkins/ref/init.groovy.d/ # 命名 job ARG job_name_1="sample-maven-job" RUN mkdir -p "$JENKINS_HOME"/jobs/${job_name_1}/latest/ RUN mkdir -p "$JENKINS_HOME"/jobs/${job_name_1}/builds/1/ COPY ${job_name_1}_config.xml /usr/share/jenkins/ref/jobs/${job_name_1}/config.xml COPY credentials.xml /usr/share/jenkins/ref/ COPY trigger-job.sh /usr/share/jenkins/ref/ # 添加自定義配置到容器里 #COPY ${job_name_1}_config.xml "$JENKINS_HOME"/jobs/${job_name_1}/config.xml USER root #RUN chown -R jenkins:jenkins "$JENKINS_HOME"/ RUN chmod -R 777 /usr/share/jenkins/ref/trigger-job.sh # 用給定的用戶組 ID 創建 'Docker' 用戶組 # 將 'jenkins' 用戶加到 'Docker' 用戶組 RUN groupadd docker -g ${HOST_DOCKER_GROUP_ID} && usermod -a -G docker jenkins RUN apt-get update && apt-get install -y tree nano curl sudo RUN curl https://get.docker.com/builds/Linux/x86_64/docker-latest.tgz | tar xvz -C /tmp/ && mv /tmp/docker/docker /usr/bin/docker RUN curl -L "https://github.com/docker/compose/releases/download/1.23.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose RUN chmod 755 /usr/local/bin/docker-compose RUN usermod -a -G sudo jenkins RUN echo "jenkins ALL=(ALL:ALL) NOPASSWD:ALL" >> /etc/sudoers RUN newgrp docker USER jenkins #ENTRYPOINT ["/bin/sh -c /var/jenkins_home/trigger-job.sh"]
- FROM jenkins/jenkins:lts – 我們將使用 Jenkins 官方提供的鏡像。
- ARG HOST_DOCKER_GROUP_ID – 需要記住的重點出現了,雖然我們在 Jenkins 容器里創建了 Docker 容器,但我們沒有在 Jenkins 自身內部創建容器。相反,我們是在它們自己的宿主機上創建了容器。確切的說,是我們讓安裝在 Jenkins 容器里的 Docker tool 部署一個 Maven 容器到宿主機上。為了實現這個部署,我們需要 Jenkins 容器和宿主機設置一樣的用戶組。
為了允許 Jenkins 這樣的未授權用戶訪問,我們要把 Jenkins 用戶加到 Docker 用戶組裡去。要做到這件事,我們只需要保證容器里的 Docker 用戶組與宿主機上的 Docker 有一致的 GID 即可。用戶組 id 可以通過命令 getent group Docker
獲得。
HOST_DOCKER_GROUP_ID
被設為了構建參數,我們要在構建時將宿主機的 Docker 用戶組 id 做為參數傳進來參與構建。
# 使用內置的 install-plugins.sh 腳本安裝我們所需的插件 RUN install-plugins.sh pipeline-graph-analysis:1.9 cloudbees-folder:6.7 docker-commons:1.14
接下來是 install-plugins.sh 腳本,把要安裝的插件作為參數傳給腳本。這個腳本是默認提供的,也可以從宿主機複製一份。
給 Jenkins Admin 用戶設置環境變數
ENV JENKINS_USER admin ENV JENKINS_PASS admin
我們設置了 JENKINS_USER
和 JENKINS_PASS
兩個環境變數,default-user.groovy
腳本會用它們創建帳號 admin 用戶(密碼 admin)。
# 跳過初始設置嚮導 ENV JAVA_OPTS -Djenkins.install.runSetupWizard=false
這個使得 Jenkins 以靜默模式安裝
# 設置啟動器數量和創建 admin 用戶的啟動腳本 COPY executors.groovy /usr/share/jenkins/ref/init.groovy.d/ COPY default-user.groovy /usr/share/jenkins/ref/init.groovy.d/ COPY create-credential.groovy /usr/share/jenkins/ref/init.groovy.d/
像我們討論的那樣,上面的腳本會設置執行器個數為 5,創建默認用戶 admin/admin。
需要注意的是,如果去看 Jenkins 官方的 Docker 鏡像,你會看到有一個 VOLUME 指向了 /vars/jenkins_home
目錄。這個意思是設置 Jenkins 的家目錄,類似於物理機上使用包管理器安裝 Jenkins 時的目錄 /var/lib/jenkins
。
但是,當 volume 掛載好以後,就只有 root 用戶有許可權在那裡編輯或者添加文件。為了讓未授權的 jenkins 用戶複製內容到 volume, 將所有東西複製到 /usr/share/Jenkins/ref/
。這樣當容器啟動後,Jenkins 會自動使用 Jenkins 用戶把這個位置的文 件拷貝一份到 /vars/jenkins_home
中。
同樣,複製到 /usr/share/jenkins/ref/init.groovy.d/
的腳本會在 Jenkins 啟動後被執行。
# 命名 job ARG job_name_1="sample-maven-job" RUN mkdir -p "$JENKINS_HOME"/jobs/${job_name_1}/latest/ RUN mkdir -p "$JENKINS_HOME"/jobs/${job_name_1}/builds/1/ COPY ${job_name_1}_config.xml /usr/share/jenkins/ref/jobs/${job_name_1}/config.xml COPY credentials.xml /usr/share/jenkins/ref/ COPY trigger-job.sh /usr/share/jenkins/ref/
在上面的例子中,我把我的 job 名字設置為 「sample-maven-job」,然後創建目錄,複製一些文件。
RUN mkdir -p "$JENKINS_HOME"/jobs/${job_name_1}/latest/ RUN mkdir -p "$JENKINS_HOME"/jobs/${job_name_1}/builds/1/
這些說明很重要,它們在 Jenkins 家目錄創建了一些用來存放配置文件的文件夾。latest/
和 builds/1
存放的目錄也需要與其 job 相對應。
這些創建好以後,我們把已經複製到 /var/share/jenkins/ref
的文件 「sample-maven-job_config.xml」,再讓 Jenkins 複製 到 /var/jenkins_home/jobs/
,這樣就有了 sample-maven-job。
最後,我們同樣把 credentials.xml
和 trigger-job.sh
文件複製到 /usr/share/jenkins/ref
。當容器啟動以後, 所有這個目錄下的文件都會以 Jenkins 用戶的許可權移動到 /var/jenkins_home
。
USER root #RUN chown -R jenkins:jenkins "$JENKINS_HOME"/ RUN chmod -R 777 /usr/share/jenkins/ref/trigger-job.sh # 用指定的用戶組組 ID 創建 'docker' 用戶組 # 並將 'jenkins' 用戶添加到該組 RUN groupadd docker -g ${HOST_DOCKER_GROUP_ID} && usermod -a -G docker jenkins RUN apt-get update && apt-get install -y tree nano curl sudo RUN curl https://get.docker.com/builds/Linux/x86_64/docker-latest.tgz | tar xvz -C /tmp/ && mv /tmp/docker/docker /usr/bin/docker RUN curl -L "https://github.com/docker/compose/releases/download/1.23.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose RUN chmod 755 /usr/local/bin/docker-compose RUN usermod -a -G sudo jenkins RUN echo "jenkins ALL=(ALL:ALL) NOPASSWD:ALL" >> /etc/sudoers RUN newgrp docker USER jenkins
下面的指令以 root 用戶執行。在 root 用戶的指令下,我們使用宿主機上的 Docker group ID 在容器里創建新的 Docker 用戶組。然後把 Jenkins 用戶加到 Docker 組當中。
通過這些,我們就可以使用 Jenkins 用戶創建容器了。這樣就能突破只有 root 用戶能創建容器的限制。為了讓 Jenkins 用戶能創建容器,我們需要把 Jenkins 用戶添加到 Docker 用戶組當中去。
在下面的指令里,我們安裝了 docker-ce 和 docker-compose 工具。我們設置了 Docker-compose 的許可權。最後,我們把 Jenkins 用戶加到 sudoers 文件里,以給到 root 用戶特定的許可權。
RUN newgrp docker
這個指令非常重要。通常我們修改一個用戶的用戶組,都需要重新登錄以使新的設置生效。為了略過這一步,我們使用 Docker 命令 newgrp 使設置直接生效。最後,我們回到 Jenkins 用戶。
構建鏡像
理解了 Docker 文件後,我們就要用它構建我們的鏡像:
docker build --build-arg HOST_DOCKER_GROUP_ID="`getent group docker | cut -d':' -f3`" -t jenkins1 .
在 Dockerfile 的所在目錄下運行上面的 Docker 構建指令。在上面的命令中,我們傳了 Docker 用戶組 ID 給 build-arg。這個值會傳給 HOST_DOCKER_GROUP_ID
,用來在 Jenkins 容器里創建相同 ID 的用戶組。下載以及安裝 Jenkins 插件會增加構建鏡像的時間。
運行鏡像
鏡像構建好以後,我們以下面的命令運行:
docker run -itd -v /var/run/docker.sock:/var/run/docker.sock -v $(which docker):/usr/bin/docker -p 8880:8080 -p 50000:50000 jenkins1
關於卷掛載有兩件重要的事。第一是我們把 Docker 命令掛載到了容器里,當需要其它容器時,就可以在當前容器創建了。
另一個重要的是掛載 /var/run/Docker.sock
。Docker.sock 是 Docker 守護進程監聽的一個 UNIX socket。這是訪問 Docker API 的主要入口點。它也可以是 TCP 類型的 socket,但是出於安全原因,默認設定是 UNIX 類型的。
Docker 默認通過這個 socket 執行命令。我們把它掛載到 Docker 容器里,是為了能在容器里啟動新的其它容器。這個掛載也可以用於服務自省和日誌目的。但這增加了被攻擊的風險,使用的時候要小心。
上面的命令執行後,我們就得到一個運行著的 Jenkins 容器。可以通過 URL<ip address>:8880
查看 Jenkins 控制台。使用 「admin/admin」 登錄 Jenkins。我們就可以看到還沒有運行過的、使用 SCM,Token 和憑據創建的 sample-maven-job。
運行 Job
要運行這個 job,我們只需要帶著 containerID 以下面的方式執行 trigger-job.sh。
docker exec <Jenkins Container ID> /bin/sh -C /var/jenkins_home/trigger-job.sh
運行後我們就可以看到流水線的構建開始了。
了解 Simple Java Maven App
如上面所說,這個倉庫是我們的 Java 應用。它使用 Maven 打包成品,還包含一個 Dockerfile,一個 Jenkinsfile 以及源程式碼。源程式碼結構與其它 Maven 項目類似。
- Jenkinsfile – 這是 sample-maven-job 啟動前的核心文件。流水線 job 使用 Github 憑據從 Github 下載源程式碼。
Jenkinsfile 文件里最重要的是定義 agent。我們使用 「agent any」 選擇任何可用的 agent 來構建程式碼。我們也可以為某個 stage 定義 agent 環境。
stage("build"){ agent { docker { image 'maven:3-alpine' args '-v /root/.m2:/root/.m2' } steps { sh 'mvn -B -DskipTests clean package' stash includes: 'target/*.jar', name: 'targetfiles' } } }
在上面的 stage 中,我們設置它的 agent 環境為 Docker 鏡像 「maven:3-alpine.」 這樣 Jenkins 就會觸發 maven:3-alpine 容器, 然後執行定義在步驟里的命令 mvn -B -DskipTests clean package
。
同樣的,單元測試也是以這樣的方式運行。docker 啟動一個 Maven 鏡像,然後執行 mvn test
。
environment { registry = "docker.io/<user name>/<image Name>" registryCredential = 'dockerhub' dockerImage = '' }
另一件重要的事是定義環境。我定義了名為 docker.io/jagadesh1982/sample
的倉庫,意味著使用最終製品(jar 包)所創建的鏡像名稱也將遵循這個格式 docker.io/jagadesh1982/sample:<version>
。如果你的鏡像需要推送到 Dockerhub 的話,記住這一點是非常重要的。Dockerhub 希望鏡像名按照 docker.io/<user Name>/<Image Name>
這樣的風格命名,以方便上傳。
當構建結束後,新的鏡像會被上傳到 Dockerhub,本地的鏡像則會被刪除。
- Dockerfile – 這個倉庫包含的 Dockerfile 用來創建 jar 包的鏡像。它會拷貝我的
my-app-1.0-SNAPSHOT.jar
到鏡像中去。它的內容是這樣:
FROM alpine:3.2 RUN apk --update add openjdk7-jre CMD ["/usr/bin/java", "-version"] COPY /target/my-app-1.0-SNAPSHOT.jar / CMD /usr/bin/java -jar /my-app-1.0-SNAPSHOT.jar
譯者:tomatofrommars