Docker和Spring Boot是非常流行的組合,我們將利用GitLab CI的優(yōu)勢(shì),并在應(yīng)用程序服務(wù)器上自動(dòng)構(gòu)建,推送和運(yùn)行Docker鏡像。
# GitLab CI
Gitlab CI/CD服務(wù)是GitLab的一部分。開發(fā)人員將代碼推送到GitLab存儲(chǔ)庫(kù)時(shí),GitLab CI就會(huì)在用戶指定的環(huán)境中自動(dòng)構(gòu)建,測(cè)試和存儲(chǔ)最新的代碼更改。
選擇GitLab CI的一些主要原因:
易于學(xué)習(xí),使用和可擴(kuò)展
維護(hù)容易
整合容易
CI完全屬于GitLab存儲(chǔ)庫(kù)的一部分
良好的Docker集成
鏡像托管(Container registry)-基本上是你自己的私有Docker Hub
從成本上來說,GitLab CI是一個(gè)很好的解決方案。每個(gè)月你有2000分鐘的免費(fèi)構(gòu)建時(shí)間,對(duì)于某些項(xiàng)目來說,這是綽綽有余的
為什么GitLab CI超越Jenkins
這無疑是一個(gè)廣泛討論的話題,但是在本文中,我們將不深入探討該話題。GitLab CI和Jenkins都有優(yōu)點(diǎn)和缺點(diǎn),它們都是功能非常強(qiáng)大的工具。
那為什么選擇GitLab?
如前所述,Gitlab CI是GitLab存儲(chǔ)庫(kù)的一部分,這就意味著當(dāng)我們有了GitLab后,就不需要再安裝Gitlab CI,也不需要額外維護(hù)。并且只需要編寫一個(gè).gitlab-ci.yml文件(下文會(huì)詳細(xì)說明),你便完成了所有CI工作。
對(duì)于小型項(xiàng)目使用Jenkins,你就必須自己配置所有內(nèi)容。通常,你還需要一臺(tái)專用的Jenkins服務(wù)器,這也需要額外的成本和維護(hù)。
使用GitLab CI 前提條件
如果需要與這些前提條件有關(guān)的任何幫助,我已提供相應(yīng)指南的鏈接。
你已經(jīng)在GitLab上推送了Spring Boot項(xiàng)目
你已在應(yīng)用程序服務(wù)器上安裝了Docker(指南)
你具有Docker鏡像的鏡像托管(本文中將使用Docker Hub)
你已經(jīng)在服務(wù)器上生成了SSH RSA密鑰(指南)
你要?jiǎng)?chuàng)建什么
你將創(chuàng)建Dockerfile 和.gitlab-ci.yml, 它們將自動(dòng)用于:
構(gòu)建應(yīng)用程序Jar文件
構(gòu)建Docker鏡像
將鏡像推送到Docker存儲(chǔ)庫(kù)
在應(yīng)用程序服務(wù)器上運(yùn)行鏡像
基本項(xiàng)目信息
本文的Spring Boot應(yīng)用程序是通過Spring Initializr生成的。這是一個(gè)基于Java 8或Java11構(gòu)建的Maven項(xiàng)目。后面,我們將介紹Java 8和Java 11對(duì)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 8ARG SPRING_ACTIVE_PROFILEMAINTAINER JasminCOPY 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_PROFILEFROM openjdk:11-slim#FROM openjdk:8-alpine FOR JAVA 8WORKDIR /appCOPY --from=MAVEN_BUILD /build/target/appdemo-*.jar /app/appdemo.jarENTRYPOINT ["java", "-jar", "appdemo.jar"]
# Java版本
讓我們從Docker的角度看一下Java 8和11之間的區(qū)別。長(zhǎng)話短說:這是Docker鏡像的大小和部署時(shí)間。
基于Java 8構(gòu)建的Docker鏡像將明顯小于基于Java 11的鏡像。這也意味著Java 8項(xiàng)目的構(gòu)建和部署時(shí)間將更快。
Java 8-構(gòu)建時(shí)間:約4分鐘,鏡像大小為 約180 MB
Java 11-構(gòu)建時(shí)間:約14分鐘,鏡像大小約為480 MB
注意:在實(shí)際應(yīng)用中,這些數(shù)字可能會(huì)有所不同。
# Docker鏡像
正如在前面示例中已經(jīng)看到的那樣,由于Java版本的緣故,我們?cè)趹?yīng)用程序鏡像大小和構(gòu)建時(shí)間方面存在巨大差異。其背后的實(shí)際原因是在Dockerfile中使用了不同的Docker鏡像。
如果我們?cè)倏匆幌翫ockerfile,那么Java 11鏡像很大的真正原因是因?yàn)樗藳]有經(jīng)過驗(yàn)證/測(cè)試的open-jdk:11鏡像的Alpine版本。
如果你不熟悉OpenJDK鏡像版本,建議你閱讀OpenJDK Docker官方文檔。在這里,你可以找到有關(guān)每個(gè)OpenJDK版本的鏡像的說明。
備注:動(dòng)態(tài)的變量
在ENTRYPOINT 中,與環(huán)境相關(guān)的屬性,我們只能寫死,如下:
ENTRYPOINT [ “ java”,“ -Dspring.profiles.active = development”,“ -jar”,“ appdemo.jar” ]
為了使它動(dòng)態(tài),你希望將其簡(jiǎn)單地轉(zhuǎn)換為:
ENTRYPOINT [ “ java”,“ -Dspring.profiles.active = $ SPRINT_ACTIVE_PROFILE”,“ -jar”,“ appdemo.jar” ]
以前,這是不可能的,但是幸運(yùn)的是,這將在.gitlab-ci.yml中通過 ARG SPRING_ACTIVE_PROFILE修復(fù)。
gitlab-ci.yml
在編寫此文件之前,要準(zhǔn)備的東西很少?;旧?,我們想要實(shí)現(xiàn)的是,只要推送代碼,就會(huì)在相應(yīng)的環(huán)境上自動(dòng)部署。
創(chuàng)建.env文件和分支
我們首先需要?jiǎng)?chuàng)建包含與環(huán)境相關(guān)的分支和.env文件。每個(gè)分支實(shí)際上代表我們的應(yīng)用程序?qū)⑦\(yùn)行的環(huán)境。
我們將在三個(gè)不同的環(huán)境中部署我們的應(yīng)用程序:開發(fā),測(cè)試和生產(chǎn)( development, QA, and production )。這意味著我們需要?jiǎng)?chuàng)建三個(gè)分支。我們的dev,QA和prod應(yīng)用程序?qū)⒃诓煌姆?wù)器上運(yùn)行,并且將具有不同的Docker容器標(biāo)簽,端口和SSH密鑰。這就要求我們的gitlab-ci.yml文件將要是動(dòng)態(tài)的。我們可以為每個(gè)環(huán)境創(chuàng)建單獨(dú)的.env文件來解決該問題。
.develop.env
.qa.env
.master.env
重要說明:命名這些文件時(shí),有一個(gè)簡(jiǎn)單的規(guī)則:使用GitLab分支來命名,因此文件名應(yīng)如下所示:。$ 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文件有關(guān)的重要說明:
- SPRING_ACTIVE_PROFILE:不言自明,我們要使用哪些Spring應(yīng)用程序?qū)傩?。DOCKER_REPO:這是Docker鏡像的存儲(chǔ)庫(kù);在這里,我們唯一需要注意的是Docker image TAG,對(duì)于每種環(huán)境,我們將使用不同的標(biāo)簽,這意味著我們將使用dev,qa 和prod 標(biāo)簽。
我們的Docker中心看起來像這樣。
如你所見,存在一個(gè)帶有三個(gè)不同標(biāo)簽的存儲(chǔ)庫(kù),每當(dāng)將代碼推送到GitLab分支上時(shí),每個(gè)標(biāo)簽(應(yīng)用程序版本)都會(huì)被更新。
APP_NAME: 此屬性非常重要,它是對(duì)容器的命名。如果你未設(shè)置此屬性,則Docker將為你的容器隨機(jī)命名。這可能是一個(gè)問題,因?yàn)槟銓o法以干凈的方式停止運(yùn)行容器。
端口:這是我們希望運(yùn)行Docker容器的端口。
SERVER_IP:應(yīng)用程序使用的服務(wù)器IP。通常,每個(gè)環(huán)境都將位于不同的服務(wù)器上。
SERVER_SSH_KEY:這是我們已經(jīng)在每臺(tái)服務(wù)器上生成的SSH密鑰。$DEV_SSH_PRIVATE_KEY 實(shí)際上是來自GitLab存儲(chǔ)庫(kù)的變量。
創(chuàng)建GitLab變量
最后需要做的是創(chuàng)建GitLab變量。
打開你的GitLab存儲(chǔ)庫(kù),然后轉(zhuǎn)到:Settings -> CI/CD。在 Variables部分中, 添加新變量:
DOCKER_USER:用于訪問Docker Hub或其他鏡像托管的用戶名
DOCKER_PASSWORD: 用于訪問鏡像托管的密碼
$ENV_SSH_PRIVATE_KEY: 先前在服務(wù)器上生成的SSH私鑰。
SSH KEY的重要說明:
- 你需要復(fù)制完整的密鑰值,包括:—– BEGIN RSA PRIVATE KEY —–和—– END RSA PRIVATE KEY —–
最后,你的GitLab變量應(yīng)如下所示。
創(chuàng)建gitlab-ci.yml文件
最后,讓我們創(chuàng)建將所有內(nèi)容放在一起的文件。
services: - docker:19.03.7-dindstages: - build jar - build and push docker image - deploybuild: 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/*.jardocker 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_REPOdeploy: 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 *ntStrictHostKeyChecking nonn" > ~/.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
這是一項(xiàng)服務(wù),使我們可以在Docker中使用Docker。在Docker中運(yùn)行Docker通常不是一個(gè)好主意,但是對(duì)于此用例來說,這是完全可以的,因?yàn)槲覀儗?gòu)建鏡像并將其推送到存儲(chǔ)庫(kù)中。
stages: - build jar - build and push docker image - deploy
對(duì)于每個(gè)gitlab-ci.yml文件,必須首先定義執(zhí)行步驟。腳本將按照步驟定義的順序執(zhí)行。
在每個(gè)步驟,我們都必須添加以下部分:
before_script: - source .${CI_COMMIT_REF_NAME}.env
這只是預(yù)先加載之前創(chuàng)建的 env. files文件。根據(jù)正在運(yùn)行的分支來自動(dòng)注入變量。(這就是為什么我們必須使用分支名稱來命名.env文件的原因)
這些是我們部署過程中的執(zhí)行步驟。
如你所見,,有三個(gè)帶有綠色復(fù)選標(biāo)記的圓圈,這表示所有步驟均已成功執(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í)行第一步驟代碼的一部分,構(gòu)建了一個(gè)jar文件,該文件可以下載。這實(shí)際上是一個(gè)可選步驟,僅用于演示構(gòu)建jar并從GitLab下載它是多么容易。
第二步驟是在Docker存儲(chǔ)庫(kù)中構(gòu)建并推送Docker鏡像。
docker build: image: docker:stable stage: build and push docker image before
這一步驟,我們不得不使用docker:19.03.7-dind服務(wù)。如你所見,我們使用的是最新的穩(wěn)定版本的Docker,我們只是在為適當(dāng)?shù)沫h(huán)境構(gòu)建鏡像,然后對(duì)Dockerhub進(jìn)行身份驗(yàn)證并推送鏡像。
我們腳本的最后一部分是:
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 *ntStrictHostKeyChecking nonn" > ~/.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到我們的應(yīng)用程序服務(wù)器并運(yùn)行一些Docker命令。其中的部分代碼 before_script大部分來自官方文檔,但是,當(dāng)然,我們可以對(duì)其進(jìn)行一些調(diào)整以滿足我們的需求。為不對(duì)私鑰進(jìn)行驗(yàn)證,添加了以下代碼行:
- echo -e "Host *ntStrictHostKeyChecking nonn" > ~/.ssh/config
你也可以參考指南驗(yàn)證私鑰。如你在最后階段的腳本部分中所見,我們正在執(zhí)行一些Docker命令。
停止正在運(yùn)行的Docker容器:docker stop $APP_NAME。(這就是我們要在.env文件中定義APP_NAME的原因 )
刪除所有未運(yùn)行的Docker鏡像:docker system prune -a -f,這實(shí)際上不是強(qiáng)制性的,但我想刪除服務(wù)器上所有未使用的鏡像。
拉取最新版本的Docker鏡像(該鏡像是在上一個(gè)階段中構(gòu)建并推送的)。
最后,使用以下命令運(yùn)行Docker鏡像:
docker container run -d --name $APP_NAME -p $PORT:8080 -e SPRING_PROFILES_ACTIVE=$SPRING_AC