使用GitLab CI和Docker自動部署SpringBoot應用
譯者:王延飛
來源:https://dzone.com/articles/automate-spring-boot-app-deployment-with-gitlab-ci
Docker和Spring Boot是非常流行的組合,我們將利用GitLab CI的優(yōu)勢,并在應用程序服務器上自動構建,推送和運行Docker鏡像。
GitLab CI
Gitlab CI/CD服務是GitLab的一部分。開發(fā)人員將代碼推送到GitLab存儲庫時,GitLab CI就會在用戶指定的環(huán)境中自動構建,測試和存儲最新的代碼更改。
選擇GitLab CI的一些主要原因:
易于學習,使用和可擴展
維護容易
整合容易
CI完全屬于GitLab存儲庫的一部分
良好的Docker集成
鏡像托管(Container registry)-基本上是你自己的私有Docker Hub
從成本上來說,GitLab CI是一個很好的解決方案。每個月你有2000分鐘的免費構建時間,對于某些項目來說,這是綽綽有余的
為什么GitLab CI超越Jenkins
這無疑是一個廣泛討論的話題,但是在本文中,我們將不深入探討該話題。GitLab CI和Jenkins都有優(yōu)點和缺點,它們都是功能非常強大的工具。
那為什么選擇GitLab?
如前所述,Gitlab CI是GitLab存儲庫的一部分,這就意味著當我們有了GitLab后,就不需要再安裝Gitlab CI,也不需要額外維護。并且只需要編寫一個.gitlab-ci.yml文件(下文會詳細說明),你便完成了所有CI工作。
對于小型項目使用Jenkins,你就必須自己配置所有內(nèi)容。通常,你還需要一臺專用的Jenkins服務器,這也需要額外的成本和維護。
使用GitLab CI 前提條件
如果需要與這些前提條件有關的任何幫助,我已提供相應指南的鏈接。
你已經(jīng)在GitLab上推送了Spring Boot項目
你已在應用程序服務器上安裝了Docker(指南)
你具有Docker鏡像的鏡像托管(本文中將使用Docker Hub)
你已經(jīng)在服務器上生成了SSH RSA密鑰(指南)
你要創(chuàng)建什么
你將創(chuàng)建Dockerfile 和.gitlab-ci.yml, 它們將自動用于:
構建應用程序Jar文件
構建Docker鏡像
將鏡像推送到Docker存儲庫
在應用程序服務器上運行鏡像
基本項目信息
本文的Spring Boot應用程序是通過Spring Initializr生成的。這是一個基于Java 8或Java11構建的Maven項目。后面,我們將介紹Java 8和Java 11對Docker鏡像有什么影響。
Docker文件
讓我們從Dockerfile開始。
FROM maven:3.6.3-jdk-11-slim AS MAVEN_BUILD
#FROM maven:3.5.2-jdk-8-alpine AS MAVEN_BUILD FOR JAVA 8
ARG SPRING_ACTIVE_PROFILE
MAINTAINER Jasmin
COPY pom.xml /build/
COPY src /build/src/
WORKDIR /build/
RUN mvn clean install -Dspring.profiles.active=$SPRING_ACTIVE_PROFILE && mvn package -B -e -Dspring.profiles.active=$SPRING_ACTIVE_PROFILE
FROM openjdk:11-slim
#FROM openjdk:8-alpine FOR JAVA 8
WORKDIR /app
COPY --from=MAVEN_BUILD /build/target/appdemo-*.jar /app/appdemo.jar
ENTRYPOINT ["java", "-jar", "appdemo.jar"]Java版本
讓我們從Docker的角度看一下Java 8和11之間的區(qū)別。長話短說:這是Docker鏡像的大小和部署時間。
基于Java 8構建的Docker鏡像將明顯小于基于Java 11的鏡像。這也意味著Java 8項目的構建和部署時間將更快。
Java 8-構建時間:約4分鐘,鏡像大小為 約180 MB
Java 11-構建時間:約14分鐘,鏡像大小約為480 MB
注意:在實際應用中,這些數(shù)字可能會有所不同。
Docker鏡像
正如在前面示例中已經(jīng)看到的那樣,由于Java版本的緣故,我們在應用程序鏡像大小和構建時間方面存在巨大差異。其背后的實際原因是在Dockerfile中使用了不同的Docker鏡像。
如果我們再看一下Dockerfile,那么Java 11鏡像很大的真正原因是因為它包含了沒有經(jīng)過驗證/測試的open-jdk:11鏡像的Alpine版本。
如果你不熟悉OpenJDK鏡像版本,建議你閱讀OpenJDK Docker官方文檔。在這里,你可以找到有關每個OpenJDK版本的鏡像的說明。
備注:動態(tài)的變量
在ENTRYPOINT 中,與環(huán)境相關的屬性,我們只能寫死,如下:
ENTRYPOINT [ “ java”,“ -Dspring.profiles.active = development”,“ -jar”,“ appdemo.jar” ]為了使它動態(tài),你希望將其簡單地轉換為:
ENTRYPOINT [ “ java”,“ -Dspring.profiles.active = $ SPRINT_ACTIVE_PROFILE”,“ -jar”,“ appdemo.jar” ]以前,這是不可能的,但是幸運的是,這將在.gitlab-ci.yml中通過 ARG SPRING_ACTIVE_PROFILE修復。
gitlab-ci.yml
在編寫此文件之前,要準備的東西很少。基本上,我們想要實現(xiàn)的是,只要推送代碼,就會在相應的環(huán)境上自動部署。
創(chuàng)建.env文件和分支
我們首先需要創(chuàng)建包含與環(huán)境相關的分支和.env文件。每個分支實際上代表我們的應用程序將運行的環(huán)境。
我們將在三個不同的環(huán)境中部署我們的應用程序:開發(fā),測試和生產(chǎn)( development, QA, and production )。這意味著我們需要創(chuàng)建三個分支。我們的dev,QA和prod應用程序將在不同的服務器上運行,并且將具有不同的Docker容器標簽,端口和SSH密鑰。這就要求我們的gitlab-ci.yml文件將要是動態(tài)的。我們可以為每個環(huán)境創(chuàng)建單獨的.env文件來解決該問題。
.develop.env
.qa.env
.master.env
重要說明:命名這些文件時,有一個簡單的規(guī)則:使用GitLab分支來命名,因此文件名應如下所示:。$ BRANCH_NAME.env
例如,這是.develop.env文件。
export?SPRING_ACTIVE_PROFILE='development'
export?DOCKER_REPO='username/demo_app:dev'
export?APP_NAME='demo_app_dev'
export?PORT='8080'
export?SERVER_IP='000.11.222.33'
export?SERVER_SSH_KEY="$DEV_SSH_PRIVATE_KEY"與.env文件有關的重要說明:
SPRING_ACTIVE_PROFILE:不言自明,我們要使用哪些Spring應用程序屬性。?DOCKER_REPO:這是Docker鏡像的存儲庫;在這里,我們唯一需要注意的是Docker image TAG,對于每種環(huán)境,我們將使用不同的標簽,這意味著我們將使用dev,qa 和prod 標簽。
我們的Docker中心看起來像這樣。

如你所見,存在一個帶有三個不同標簽的存儲庫,每當將代碼推送到GitLab分支上時,每個標簽(應用程序版本)都會被更新。
APP_NAME:?此屬性非常重要,它是對容器的命名。如果你未設置此屬性,則Docker將為你的容器隨機命名。這可能是一個問題,因為你將無法以干凈的方式停止運行容器。
端口:這是我們希望運行Docker容器的端口。
SERVER_IP:應用程序使用的服務器IP。通常,每個環(huán)境都將位于不同的服務器上。
SERVER_SSH_KEY:這是我們已經(jīng)在每臺服務器上生成的SSH密鑰。$DEV_SSH_PRIVATE_KEY 實際上是來自GitLab存儲庫的變量。
創(chuàng)建GitLab變量
最后需要做的是創(chuàng)建GitLab變量。
打開你的GitLab存儲庫,然后轉到:Settings -> CI/CD。在 Variables部分中, 添加新變量:
DOCKER_USER:用于訪問Docker Hub或其他鏡像托管的用戶名
DOCKER_PASSWORD:?用于訪問鏡像托管的密碼
$ENV_SSH_PRIVATE_KEY:?先前在服務器上生成的SSH私鑰。
SSH KEY的重要說明:
你需要復制完整的密鑰值,包括:—– BEGIN RSA PRIVATE KEY —–和—– END RSA PRIVATE KEY —–
最后,你的GitLab變量應如下所示。

創(chuàng)建gitlab-ci.yml文件
最后,讓我們創(chuàng)建將所有內(nèi)容放在一起的文件。
services:
??- docker:19.03.7-dind
stages:
??- build jar
??- build and push docker image
??- deploy
build:
??image: maven:3.6.3-jdk-11-slim
??stage: build jar
??before_script:
????- source?.${CI_COMMIT_REF_NAME}.env
??script:
????- mvn clean install -Dspring.profiles.active=$SPRING_ACTIVE_PROFILE?&& mvn package -B -e -Dspring.profiles.active=$SPRING_ACTIVE_PROFILE
??artifacts:
????paths:
??????- target/*.jar
docker build:
??image: docker:stable
??stage: build and push docker image
??before_script:
????- source?.${CI_COMMIT_REF_NAME}.env
??script:
????- docker build --build-arg SPRING_ACTIVE_PROFILE=$SPRING_ACTIVE_PROFILE?-t $DOCKER_REPO?.
????- docker login -u $DOCKER_USER?-p $DOCKER_PASSWORD?docker.io
????- docker push $DOCKER_REPO
deploy:
??image: ubuntu:latest
??stage: deploy
??before_script:
????- 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
????- eval?$(ssh-agent -s)
????- echo?"$SSH_PRIVATE_KEY"?| tr -d '\r'?| ssh-add -
????- mkdir -p ~/.ssh
????- chmod 700 ~/.ssh
????- echo?-e "Host *\n\tStrictHostKeyChecking no\n\n"?> ~/.ssh/config
????- source?.${CI_COMMIT_REF_NAME}.env
??script:
????- ssh root@$SERVER?"docker login -u $DOCKER_USER?-p $DOCKER_PASSWORD?docker.io; docker stop $APP_NAME; docker system prune -a -f; docker pull $DOCKER_REPO; docker container run -d --name $APP_NAME?-p $PORT:8080 -e SPRING_PROFILES_ACTIVE=$SPRING_ACTIVE_PROFILE?$DOCKER_REPO; docker logout"讓我們解釋一下這里發(fā)生了什么:
services:
- docker:19.03.7-dind這是一項服務,使我們可以在Docker中使用Docker。在Docker中運行Docker通常不是一個好主意,但是對于此用例來說,這是完全可以的,因為我們將構建鏡像并將其推送到存儲庫中。
stages:
??- build jar
??- build and?push docker image
??- deploy對于每個gitlab-ci.yml文件,必須首先定義執(zhí)行步驟。腳本將按照步驟定義的順序執(zhí)行。
在每個步驟,我們都必須添加以下部分:
before_script:
?- source?.${CI_COMMIT_REF_NAME}.env這只是預先加載之前創(chuàng)建的 env. files文件。根據(jù)正在運行的分支來自動注入變量。(這就是為什么我們必須使用分支名稱來命名.env文件的原因)
這些是我們部署過程中的執(zhí)行步驟。

如你所見,,有三個帶有綠色復選標記的圓圈,這表示所有步驟均已成功執(zhí)行。
build:
image: maven:3.6.3-jdk-11-slim
stage: build jar
before_script:
- source?.${CI_COMMIT_REF_NAME}.env
script:
- mvn clean install -Dspring.profiles.active=$SPRING_ACTIVE_PROFILE && mvn package -B -e?-Dspring.profiles.active=$SPRING_ACTIVE_PROFILE
artifacts:
paths:
- target/*.jar這是執(zhí)行第一步驟代碼的一部分,構建了一個jar文件,該文件可以下載。這實際上是一個可選步驟,僅用于演示構建jar并從GitLab下載它是多么容易。
第二步驟是在Docker存儲庫中構建并推送Docker鏡像。
docker build:
??image: docker:stable
??stage: build and push docker image
??before_script:
????- source?.${CI_COMMIT_REF_NAME}.env
??script:
????- docker build --build-arg SPRING_ACTIVE_PROFILE=$SPRING_ACTIVE_PROFILE?-t $DOCKER_REPO?.
????- docker login -u $DOCKER_USER?-p $DOCKER_PASSWORD?docker.io
????- docker push $DOCKER_REPO這一步驟,我們不得不使用docker:19.03.7-dind服務。如你所見,我們使用的是最新的穩(wěn)定版本的Docker,我們只是在為適當?shù)沫h(huán)境構建鏡像,然后對Dockerhub進行身份驗證并推送鏡像。
我們腳本的最后一部分是:
deploy:
??image: ubuntu:latest
??stage: deploy
??before_script:
????- 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
????- eval?$(ssh-agent -s)
????- echo?"$SSH_PRIVATE_KEY"?| tr -d '\r'?| ssh-add -
????- mkdir -p ~/.ssh
????- chmod 700 ~/.ssh
????- echo?-e "Host *\n\tStrictHostKeyChecking no\n\n"?> ~/.ssh/config
????- source?.${CI_COMMIT_REF_NAME}.env
??script:
????- ssh root@$SERVER?"docker stop $APP_NAME; docker system prune -a -f; docker pull $DOCKER_REPO; docker container run -d --name $APP_NAME?-p $PORT:8080 -e SPRING_PROFILES_ACTIVE=$SPRING_ACTIVE_PROFILE?$DOCKER_REPO"
在此步驟中,我們使用Ubuntu
Docker鏡像,因此我們可以SSH到我們的應用程序服務器并運行一些Docker命令。其中的部分代碼
before_script大部分來自官方文檔,但是,當然,我們可以對其進行一些調整以滿足我們的需求。為不對私鑰進行驗證,添加了以下代碼行:
- echo?-e?"Host *\n\tStrictHostKeyChecking no\n\n">~/.ssh/config你也可以參考指南驗證私鑰。如你在最后階段的腳本部分中所見,我們正在執(zhí)行一些Docker命令。
停止正在運行的Docker容器:docker stop $APP_NAME。(這就是我們要在.env文件中定義APP_NAME的原因 )
刪除所有未運行的Docker鏡像:?docker system prune -a -f,這實際上不是強制性的,但我想刪除服務器上所有未使用的鏡像。
拉取最新版本的Docker鏡像(該鏡像是在上一個階段中構建并推送的)。
最后,使用以下命令運行Docker鏡像:
docker container run -d --name $APP_NAME?-p $PORT:8080-e?SPRING_PROFILES_ACTIVE=$SPRING_ACTIVE_PROFILE?$DOCKER_REPO- END -
?推薦閱讀? 記一次 Linux服務器被入侵后的排查思路 Nginx為什么快到根本停不下來? 用了3年Kubernetes,我們得到的5個教訓 Linux 運維必備的 40 個命令總結,收好了~ 大白話理解Session和Cookie是什么? 系統(tǒng)架構性能優(yōu)化思路 運維工程師必備技能:網(wǎng)絡排錯思路大講解~
點亮,服務器三年不宕機

