GitLab持續(xù)集成
互聯(lián)網(wǎng)軟件的開發(fā)和發(fā)布,已經(jīng)形成了一套標(biāo)準(zhǔn)流程,最重要的組成部分就是持續(xù)集成(Continuous integration,簡(jiǎn)稱CI)。
持續(xù)集成
持續(xù)集成指的是,頻繁地(一天多次)將代碼集成到主干。它的好處主要有兩個(gè):
快速發(fā)現(xiàn)錯(cuò)誤。每完成一點(diǎn)更新,就集成到主干,可以快速發(fā)現(xiàn)錯(cuò)誤,定位錯(cuò)誤也比較容易。
防止分支大幅偏離主干。如果不是經(jīng)常集成,主干又在不斷更新,會(huì)導(dǎo)致以后集成的難度變大,甚至難以集成。
Martin Fowler 說(shuō)過(guò),"持續(xù)集成并不能消除 Bug,而是讓它們非常容易發(fā)現(xiàn)和改正。"

持續(xù)集成
持續(xù)集成強(qiáng)調(diào)開發(fā)人員提交了新代碼之后,立刻進(jìn)行構(gòu)建、(單元)測(cè)試。根據(jù)測(cè)試結(jié)果,我們可以確定新代碼和原有代碼能否正確地集成在一起。
與持續(xù)集成相關(guān)的,還有兩個(gè)概念,分別是持續(xù)交付和持續(xù)部署。
持續(xù)交付
持續(xù)交付(Continuous delivery)指的是,頻繁地將軟件的新版本,交付給質(zhì)量團(tuán)隊(duì)或者用戶,以供評(píng)審。如果評(píng)審?fù)ㄟ^(guò),代碼就進(jìn)入生產(chǎn)階段。
持續(xù)交付可以看作持續(xù)集成的下一步。它強(qiáng)調(diào)的是,不管怎么更新,軟件是隨時(shí)隨地可以交付的。

持續(xù)交付
持續(xù)交付在持續(xù)集成的基礎(chǔ)上,將集成后的代碼部署到更貼近真實(shí)運(yùn)行環(huán)境的「類生產(chǎn)環(huán)境」(production-like environments)中。比如,我們完成單元測(cè)試后,可以把代碼部署到連接數(shù)據(jù)庫(kù)的 Staging 環(huán)境中更多的測(cè)試。如果代碼沒(méi)有問(wèn)題,可以繼續(xù)手動(dòng)部署到生產(chǎn)環(huán)境中。
持續(xù)部署
持續(xù)部署(continuous deployment)是持續(xù)交付的下一步,指的是代碼通過(guò)評(píng)審以后,自動(dòng)部署到生產(chǎn)環(huán)境。
持續(xù)部署的目標(biāo)是,代碼在任何時(shí)刻都是可部署的,可以進(jìn)入生產(chǎn)階段。
持續(xù)部署的前提是能自動(dòng)化完成測(cè)試、構(gòu)建、部署等步驟。

持續(xù)部署
持續(xù)集成的操作流程
根據(jù)持續(xù)集成的設(shè)計(jì),代碼從提交到生產(chǎn),整個(gè)過(guò)程有以下幾步。
提交
流程的第一步,是開發(fā)者向代碼倉(cāng)庫(kù)提交代碼。所有后面的步驟都始于本地代碼的一次提交(commit)。
測(cè)試(第一輪)
代碼倉(cāng)庫(kù)對(duì) commit 操作配置了鉤子(hook),只要提交代碼或者合并進(jìn)主干,就會(huì)跑自動(dòng)化測(cè)試。
測(cè)試的種類:
單元測(cè)試:針對(duì)函數(shù)或模塊的測(cè)試
集成測(cè)試:針對(duì)整體產(chǎn)品的某個(gè)功能的測(cè)試,又稱功能測(cè)試
端對(duì)端測(cè)試:從用戶界面直達(dá)數(shù)據(jù)庫(kù)的全鏈路測(cè)試
第一輪至少要跑單元測(cè)試。
構(gòu)建
通過(guò)第一輪測(cè)試,代碼就可以合并進(jìn)主干,就算可以交付了。
交付后,就先進(jìn)行構(gòu)建(build),再進(jìn)入第二輪測(cè)試。所謂構(gòu)建,指的是將源碼轉(zhuǎn)換為可以運(yùn)行的實(shí)際代碼,比如安裝依賴,配置各種資源(樣式表、JS腳本、圖片)等等。
常用的構(gòu)建工具如下:
Jenkins
Travis
Codeship
Strider
Jenkins 和 Strider 是開源軟件,Travis 和 Codeship 對(duì)于開源項(xiàng)目可以免費(fèi)使用。它們都會(huì)將構(gòu)建和測(cè)試,在一次運(yùn)行中執(zhí)行完成。
測(cè)試(第二輪)
構(gòu)建完成,就要進(jìn)行第二輪測(cè)試。如果第一輪已經(jīng)涵蓋了所有測(cè)試內(nèi)容,第二輪可以省略,當(dāng)然,這時(shí)構(gòu)建步驟也要移到第一輪測(cè)試前面。
第二輪是全面測(cè)試,單元測(cè)試和集成測(cè)試都會(huì)跑,有條件的話,也要做端對(duì)端測(cè)試。所有測(cè)試以自動(dòng)化為主,少數(shù)無(wú)法自動(dòng)化的測(cè)試用例,就要人工跑。
需要強(qiáng)調(diào)的是,新版本的每一個(gè)更新點(diǎn)都必須測(cè)試到。如果測(cè)試的覆蓋率不高,進(jìn)入后面的部署階段后,很可能會(huì)出現(xiàn)嚴(yán)重的問(wèn)題。
部署
通過(guò)了第二輪測(cè)試,當(dāng)前代碼就是一個(gè)可以直接部署的版本(artifact)。將這個(gè)版本的所有文件打包( tar filename.tar * )存檔,發(fā)到生產(chǎn)服務(wù)器。
生產(chǎn)服務(wù)器將打包文件,解包成本地的一個(gè)目錄,再將運(yùn)行路徑的符號(hào)鏈接(symlink)指向這個(gè)目錄,然后重新啟動(dòng)應(yīng)用。這方面的部署工具有Ansible,Chef,Puppet等。
回滾
一旦當(dāng)前版本發(fā)生問(wèn)題,就要回滾到上一個(gè)版本的構(gòu)建結(jié)果。最簡(jiǎn)單的做法就是修改一下符號(hào)鏈接,指向上一個(gè)版本的目錄。
使用 GitLab 持續(xù)集成
從 GitLab 8.0 開始,GitLab CI 就已經(jīng)集成在 GitLab 中,我們只要在項(xiàng)目中添加一個(gè).gitlab-ci.yml文件,然后添加一個(gè) Runner,即可進(jìn)行持續(xù)集成。而且隨著 GitLab 的升級(jí),GitLab CI 變得越來(lái)越強(qiáng)大。
概念
Pipeline
一次 Pipeline 其實(shí)相當(dāng)于一次構(gòu)建任務(wù),里面可以包含多個(gè)流程,如安裝依賴、運(yùn)行測(cè)試、編譯、部署測(cè)試服務(wù)器、部署生產(chǎn)服務(wù)器等流程。
任何提交或者 Merge Request 的合并都可以觸發(fā) Pipeline,如下圖所示:
+------------------+ +----------------+
| | trigger | |
| Commit / MR +---------->+ Pipeline |
| | | |
+------------------+ +----------------+
復(fù)制代碼
Stages
Stages 表示構(gòu)建階段,說(shuō)白了就是上面提到的流程。我們可以在一次 Pipeline 中定義多個(gè) Stages,這些 Stages 會(huì)有以下特點(diǎn):
所有 Stages 會(huì)按照順序運(yùn)行,即當(dāng)一個(gè) Stage 完成后,下一個(gè) Stage 才會(huì)開始
只有當(dāng)所有 Stages 完成后,該構(gòu)建任務(wù) (Pipeline) 才會(huì)成功
如果任何一個(gè) Stage 失敗,那么后面的 Stages 不會(huì)執(zhí)行,該構(gòu)建任務(wù) (Pipeline) 失敗
因此,Stages 和 Pipeline 的關(guān)系就是:
+--------------------------------------------------------+
| |
| Pipeline |
| |
| +-----------+ +------------+ +------------+ |
| | Stage 1 |---->| Stage 2 |----->| Stage 3 | |
| +-----------+ +------------+ +------------+ |
| |
+--------------------------------------------------------+
復(fù)制代碼
Jobs
Jobs 表示構(gòu)建工作,表示某個(gè) Stage 里面執(zhí)行的工作。我們可以在 Stages 里面定義多個(gè) Jobs,這些 Jobs 會(huì)有以下特點(diǎn):
相同 Stage 中的 Jobs 會(huì)并行執(zhí)行
相同 Stage 中的 Jobs 都執(zhí)行成功時(shí),該 Stage 才會(huì)成功
如果任何一個(gè) Job 失敗,那么該 Stage 失敗,即該構(gòu)建任務(wù) (Pipeline) 失敗
所以,Jobs 和 Stage 的關(guān)系圖就是:
+------------------------------------------+
| |
| Stage 1 |
| |
| +---------+ +---------+ +---------+ |
| | Job 1 | | Job 2 | | Job 3 | |
| +---------+ +---------+ +---------+ |
| |
+------------------------------------------+
復(fù)制代碼
基于 Docker 安裝 GitLab Runner
GitLab Runner 簡(jiǎn)介
理解了上面的基本概念之后,有沒(méi)有覺(jué)得少了些什么東西 —— 由誰(shuí)來(lái)執(zhí)行這些構(gòu)建任務(wù)呢?
答案就是 GitLab Runner 了!
想問(wèn)為什么不是 GitLab CI 來(lái)運(yùn)行那些構(gòu)建任務(wù)?
一般來(lái)說(shuō),構(gòu)建任務(wù)都會(huì)占用很多的系統(tǒng)資源 (譬如編譯代碼),而 GitLab CI 又是 GitLab 的一部分,如果由 GitLab CI 來(lái)運(yùn)行構(gòu)建任務(wù)的話,在執(zhí)行構(gòu)建任務(wù)的時(shí)候,GitLab 的性能會(huì)大幅下降。
GitLab CI 最大的作用是管理各個(gè)項(xiàng)目的構(gòu)建狀態(tài),因此,運(yùn)行構(gòu)建任務(wù)這種浪費(fèi)資源的事情就交給 GitLab Runner 來(lái)做拉!
因?yàn)?GitLab Runner 可以安裝到不同的機(jī)器上,所以在構(gòu)建任務(wù)運(yùn)行期間并不會(huì)影響到 GitLab 的性能
基于 Docker 安裝 GitLab Runner
準(zhǔn)備目錄結(jié)構(gòu)

目錄結(jié)構(gòu)
docker-compose.yml
version: '2'
services:
gitlab:
image: twang2218/gitlab-ce-zh:10.5
restart: always
hostname: '10.3.50.160'
container_name: gitlab
environment:
TZ: 'Asia/Shanghai'
GITLAB_OMNIBUS_CONFIG: |
external_url 'http://10.3.50.160:8080'
gitlab_rails['gitlab_shell_ssh_port'] = 2222
unicorn['port'] = 8888
nginx['listen_port'] = 8080
ports:
- '8080:8080'
- '8443:443'
- '2222:22'
volumes:
- /etc/localtime:/etc/localtime
- ./conf:/etc/gitlab
- ./data/logs:/var/log/gitlab
- ./data/data:/var/opt/gitlab
gitlab-runner:
image: gitlab/gitlab-runner
restart: always
hostname: gitlab-runner
container_name: gitlab-runner
extra_hosts:
- git.imlcs.top:10.3.50.160
depends_on:
- gitlab
volumes:
- /etc/localtime:/etc/localtime
- ./runner:/etc/gitlab-runner
- /var/run/docker.sock:/var/run/docker.sock
復(fù)制代碼
GitLab CI 地址與令牌參數(shù)
項(xiàng)目–>設(shè)置–>CI/CD–>Runner 設(shè)置

查找位置
注冊(cè) Runner
方法一
docker exec -it gitlab-runner gitlab-runner register -n \
--url http://10.3.50.160:8080/ \
--registration-token cpR4sgBCsZ-TJUpJVz9t \
--description "dockersock" \
--docker-privileged=true \
--docker-pull-policy="if-not-present" \
--docker-image "docker:latest" \
--docker-volumes /var/run/docker.sock:/var/run/docker.sock \
--docker-volumes /root/m2:/root/.m2 \
--executor docker
復(fù)制代碼
方法二
docker exec -it gitlab-runner gitlab-runner register
# 輸入 GitLab 地址
Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com/):
http://10.3.50.160:8888/
# 輸入 GitLab Token
Please enter the gitlab-ci token for this runner:
1Lxq_f1NRfCfeNbE5WRh
# 輸入 Runner 的說(shuō)明
Please enter the gitlab-ci description for this runner:
可以為空
# 設(shè)置 Tag,可以用于指定在構(gòu)建規(guī)定的 tag 時(shí)觸發(fā) ci
Please enter the gitlab-ci tags for this runner (comma separated):
deploy
# 這里選擇 true ,可以用于代碼上傳后直接執(zhí)行
Whether to run untagged builds [true/false]:
true
# 這里選擇 false,可以直接回車,默認(rèn)為 false
Whether to lock Runner to current project [true/false]:
false
# 選擇 runner 執(zhí)行器,這里我們選擇的是 shell
Please enter the executor: virtualbox, docker+machine, parallels, shell, ssh, docker-ssh+machine, kubernetes, docker, docker-ssh:
#shell
docker # 使用 docker 作為輸出模式
Please enter the default Docker image (e.g. ruby:2.1):
alpine:latest # 使用的基礎(chǔ)鏡像
復(fù)制代碼
使用 Runner
.gitlab-ci.yml
在項(xiàng)目工程下編寫.gitlab-ci.yml配置文件:
示例一,找一個(gè)springboot的簡(jiǎn)單項(xiàng)目
image: docker-maven:alpine
services:
- redis:3-alpine
#Maven 阿里云鏡像
#before_script:
# - echo -e "<?xml version=\""1.0\"" encoding=\""UTF-8\""?><settings xmlns=\""http://maven.apache.org/SETTINGS/1.0.0\"" xmlns:xsi=\""http://www.w3.org/2001/XMLSchema-instance\"" xsi:schemaLocation=\""http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd\""><mirrors><mirror><id>alimaven</id><name>aliyun maven</name><url>http://maven.aliyun.com/nexus/content/groups/public/</url><mirrorOf>central</mirrorOf></mirror></mirrors></settings>" > ~/.m2/settings.xml
# 定義 stages
stages:
# - test
- build
# 定義 jobs
#test app:
# stage: test
# script:
# - echo "I am test job"
# - mvn test
# 定義 job
build app:
stage: build
script:
- mvn -Dmaven.test.skip=true clean package docker:build
復(fù)制代碼
示例二
stages:
- install_deps
- test
- build
- deploy_test
- deploy_production
cache:
key: ${CI_BUILD_REF_NAME}
paths:
- node_modules/
- dist/
# 安裝依賴
install_deps:
stage: install_deps
only:
- develop
- master
script:
- npm install
# 運(yùn)行測(cè)試用例
test:
stage: test
only:
- develop
- master
script:
- npm run test
# 編譯
build:
stage: build
only:
- develop
- master
script:
- npm run clean
- npm run build:client
- npm run build:server
# 部署測(cè)試服務(wù)器
deploy_test:
stage: deploy_test
only:
- develop
script:
- pm2 delete app || true
- pm2 start app.js --name app
# 部署生產(chǎn)服務(wù)器
deploy_production:
stage: deploy_production
only:
- master
script:
- bash scripts/deploy/deploy.sh
復(fù)制代碼
上面的配置把一次 Pipeline 分成五個(gè)階段:
安裝依賴(install_deps)
運(yùn)行測(cè)試(test)
編譯(build)
部署測(cè)試服務(wù)器(deploy_test)
部署生產(chǎn)服務(wù)器(deploy_production)
注意:設(shè)置 Job.only 后,只有當(dāng) develop 分支和 master 分支有提交的時(shí)候才會(huì)觸發(fā)相關(guān)的 Jobs。
節(jié)點(diǎn)說(shuō)明:
stages:定義構(gòu)建階段,這里只有一個(gè)階段 deploy
deploy:構(gòu)建階段 deploy 的詳細(xì)配置也就是任務(wù)配置
script:需要執(zhí)行的 shell 腳本
only:這里的 master 指在提交到 master 時(shí)執(zhí)行
tags:與注冊(cè) runner 時(shí)的 tag 匹配
測(cè)試集成效果
所有操作完成后 push 代碼到服務(wù)器,查看是否成功:

passed 表示執(zhí)行成功
其它命令
刪除注冊(cè)信息
gitlab-ci-multi-runner unregister --name "名稱"
復(fù)制代碼
查看注冊(cè)列表
gitlab-ci-multi-runner list
復(fù)制代碼
附:項(xiàng)目配置 Dockerfile 案例
FROM openjdk:8-jre
MAINTAINER Lusifer <[email protected]>
ENV APP_VERSION 1.0.0-SNAPSHOT
ENV DOCKERIZE_VERSION v0.6.1
RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
&& tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
&& rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
RUN mkdir /app
COPY myshop-service-user-provider-$APP_VERSION.jar /app/app.jar
ENTRYPOINT ["dockerize", "-timeout", "5m", "-wait", "tcp://192.168.10.131:3306", "java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app/app.jar"]
EXPOSE 8501
復(fù)制代碼
持續(xù)集成實(shí)戰(zhàn)用戶管理服務(wù)
部署通用模塊項(xiàng)目
先將所有被依賴項(xiàng)目(通用模塊項(xiàng)目)部署到 Nexus,為項(xiàng)目創(chuàng)建一個(gè)deploy.bat文件,示例代碼如下:
cd ..
cd myshop-dependencies
call mvn deploy
cd ..
cd myshop-commons
call mvn deploy
cd ..
cd myshop-commons-domain
call mvn deploy
cd ..
cd myshop-commons-mapper
call mvn deploy
cd ..
cd myshop-commons-dubbo
call mvn deploy
cd ..
cd myshop-static-backend
call mvn deploy
cd ..
cd myshop-service-user-api
call mvn deploy
復(fù)制代碼
持續(xù)集成依賴管理項(xiàng)目
由于我們所有項(xiàng)目的父工程都是依賴于myshop-dependencies,所以我們持續(xù)集成的第一步是將該項(xiàng)目進(jìn)行持續(xù)集成,在項(xiàng)目目錄創(chuàng)建.gitlab-ci.yml文件,代碼如下:
stages:
- deploy
deploy:
stage: deploy
script:
- /usr/local/maven/apache-maven-3.5.3/bin/mvn clean install
復(fù)制代碼
持續(xù)集成服務(wù)提供者
gitlab-ci.yml
# 定義階段
stages:
- build
- push
- run
- clean
build:
stage: build
script:
- /usr/local/maven/apache-maven-3.5.3/bin/mvn clean package
- cp target/myshop-service-user-provider-1.0.0-SNAPSHOT.jar docker
- cd docker
- docker build -t 192.168.10.133:5000/myshop-service-user-provider:v1.0.0 .
push:
stage: push
script:
- docker push 192.168.10.133:5000/myshop-service-user-provider:v1.0.0
run:
stage: run
script:
- cd docker
- docker-compose down
- docker-compose up -d
clean:
stage: clean
script:
- docker image prune -f
復(fù)制代碼
Dockerfile
FROM openjdk:8-jre
MAINTAINER Lusifer <[email protected]>
ENV APP_VERSION 1.0.0-SNAPSHOT
ENV DOCKERIZE_VERSION v0.6.1
RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
&& tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
&& rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
RUN mkdir /app
COPY myshop-service-user-provider-$APP_VERSION.jar /app/app.jar
ENTRYPOINT ["dockerize", "-timeout", "5m", "-wait", "tcp://192.168.10.131:3306", "java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app/app.jar"]
EXPOSE 8501 22222 20881
復(fù)制代碼
docker-compose.yml
version: '3.1'
services:
myshop-service-user-provider:
image: 192.168.10.133:5000/myshop-service-user-provider:v1.0.0
container_name: myshop-service-user-provider
ports:
- 8501:8501
- 22222:22222
- 20881:20881
networks:
default:
external:
name: dubbo
復(fù)制代碼
持續(xù)集成服務(wù)消費(fèi)者
gitlab-ci.yml
stages:
- build
- push
- run
- clean
build:
stage: build
script:
- /usr/local/maven/apache-maven-3.5.3/bin/mvn clean package
- cp target/myshop-service-user-consumer-1.0.0-SNAPSHOT.jar docker
- cd docker
- docker build -t 192.168.10.133:5000/myshop-service-user-consumer:v1.0.0 .
push:
stage: push
script:
- docker push 192.168.10.133:5000/myshop-service-user-consumer:v1.0.0
run:
stage: run
script:
- cd docker
- docker-compose down
- docker-compose up -d
clean:
stage: clean
script:
- docker image prune -f
復(fù)制代碼
Dockerfile
FROM openjdk:8-jre
MAINTAINER Lusifer <[email protected]>
ENV APP_VERSION 1.0.0-SNAPSHOT
ENV DOCKERIZE_VERSION v0.6.1
RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
&& tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
&& rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
RUN mkdir /app
COPY myshop-service-user-consumer-$APP_VERSION.jar /app/app.jar
ENTRYPOINT ["dockerize", "-timeout", "5m", "-wait", "tcp://192.168.10.131:20881", "java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app/app.jar"]
EXPOSE 8601 8701
復(fù)制代碼
docker-compose.yml
version: '3.1'
services:
myshop-service-user-consumer:
image: 192.168.10.133:5000/myshop-service-user-consumer:v1.0.0
container_name: myshop-service-user-consumer
ports:
- 8601:8601
- 8701:8701
networks:
default:
external:
name: my_net
