前言
一直以來公司的開發(fā)、測試及生產(chǎn)環(huán)境都基于實(shí)體機(jī),CI/CD通過Jenkins完成。
最近公司的運(yùn)維工程師離職了,新的還未覓得。另外,公司的業(yè)務(wù)正朝著多線方向發(fā)展,未來計(jì)劃采用基于SeviceMesh的微服務(wù)方式部署到K8S平臺(tái)。先將環(huán)境遷移到Docker,對于零運(yùn)維經(jīng)驗(yàn)的人,看上去是一個(gè)不錯(cuò)的開始。
本文假設(shè)GitLab已成功搭建運(yùn)行,若想了解如何搭建GitLab,請參考這篇文章。
1. GitLab CI/CD工作流
先來看一張官網(wǎng)的圖:

說明:
- GitLab CI/CD的PIPELINE是由一系列
stage構(gòu)成的,如圖中CI PIPELINE的BUILD,UNIT TEST和INTEGRATION TESTS; - 每個(gè)
stage又包含一系列任務(wù),如INTEGRATION TESTS包含了3個(gè)任務(wù); - 默認(rèn)上一個(gè)
stage的所有任務(wù)都成功執(zhí)行,才會(huì)執(zhí)行下一個(gè)stage中的任務(wù)(可自定義執(zhí)行規(guī)則); - 系統(tǒng)默認(rèn)設(shè)置了3個(gè)
stage:build,test和deploy(可自定義,示例見下面配置文件); - 主要配置都由項(xiàng)目根目錄下的
.gitlab-ci.yml設(shè)定;
再來看一下GitLab的執(zhí)行過程:

說明:
- 每個(gè)項(xiàng)目根目錄都有一個(gè)
.gitlab-ci.yml配置文件; - 配置文件的主要內(nèi)容包括:
- 定義一系列任務(wù);
- 設(shè)置任務(wù)在哪個(gè)
stage執(zhí)行; - 設(shè)置任務(wù)應(yīng)該由哪個(gè)
GitLab Runner負(fù)責(zé)執(zhí)行; - 設(shè)置
GitLab Runner應(yīng)該使用什么執(zhí)行環(huán)境執(zhí)行該任務(wù),如某個(gè)docker鏡像; - 設(shè)置任務(wù)依賴的
git分支; - 設(shè)置任務(wù)的觸發(fā)條件,如代碼提交或手工觸發(fā);
-
GitLab Runner需要在使用前先在GitLab注冊:- 一般每個(gè)
GitLab Runner都是相互獨(dú)立的服務(wù)器或虛擬機(jī),如本地辦公室的開發(fā)服務(wù)器、云端的測試服務(wù)器、專門用于打包構(gòu)建app的黑蘋果電腦、專門用于某個(gè)項(xiàng)目的服務(wù)器等; -
GitLab Runner根據(jù)任務(wù)配置,為任務(wù)準(zhǔn)備執(zhí)行環(huán)境,如shell,docker,k8s等; -
GitLab Runner注冊時(shí)可以設(shè)置一到多個(gè)tag; -
GitLab通過配置文件中任務(wù)設(shè)置tag,調(diào)度相應(yīng)的GitLab Runner運(yùn)行任務(wù); - 若多個(gè)
GitLab Runner匹配執(zhí)行條件,系統(tǒng)會(huì)隨機(jī)選擇一個(gè); - 若沒有相匹配的
GitLab Runner,或所有匹配的GitLab Runner都在忙,則任務(wù)會(huì)處于等待狀態(tài); -
GitLab Runner可設(shè)置同時(shí)執(zhí)行任務(wù)的數(shù)量;
- 一般每個(gè)
2. 安裝、注冊GitLab Runner
- 本示例使用
Docker運(yùn)行GitLab Runner; - 安裝完后還需要在GitLab里注冊,才能使用;
- 本示例采用
alpine-10.7.2;
示例腳本如下:
docker run --detach \
--name gitlab-runner \
--restart always \
--volume /opt/data/gitlab-runner/config:/etc/gitlab-runner \ # 配置文件
--volume /var/run/docker.sock:/var/run/docker.sock \ # 支持dind(Docker in Docker, 在Docker中構(gòu)建Docker鏡像)
gitlab/gitlab-runner:alpine-v10.7.2
GitLab Runner跑起來之后,運(yùn)行以下腳本完成注冊。詳情參考這里。
docker exec -it gitlab-runner gitlab-runner register \
--name shared-runner \ # 給GitLab Runner起個(gè)名
--url "https://gitlab.com/" \ # GitLab服務(wù)器地址
--registration-token "PROJECT_REGISTRATION_TOKEN" \ # GitLab注冊Token,可在GitLab管理界面獲得
--description "ruby-2.5" \ # GitLab Runner的一些描述
--tag-list nodejs,java,ruby \ # 給GitLab Runner打上標(biāo)簽,配置文件可根據(jù)標(biāo)簽指定某個(gè)Runner來執(zhí)行任務(wù)
--run-untagged true \ # 是否可以運(yùn)行未指定標(biāo)簽的任務(wù)
--locked false \ # 是否鎖定到某個(gè)項(xiàng)目
--executor "docker" \ # 任務(wù)執(zhí)行環(huán)境
--docker-volumes /opt/data/ws:/share:rw \ # 使用docker執(zhí)行環(huán)境時(shí),自動(dòng)掛載的目錄(可選)
--docker-image ruby:2.5 # 使用docker執(zhí)行環(huán)境時(shí),設(shè)置默認(rèn)執(zhí)行鏡像
說明:
- 任務(wù)執(zhí)行環(huán)境:每種環(huán)境支持的功能有所區(qū)別。詳情參考這里。
- 自動(dòng)掛載目錄:根據(jù)需求自行決定是否需要,一些通用的腳本和工具可放在這里。
注冊完成后可以GitLab管理界面看到注冊成功的GitLab Runner,如下圖所示:

同時(shí),在/opt/data/gitlab-runner/config/目錄下,可以找到config.toml配置文件:
concurrent = 1 # 任務(wù)并發(fā)數(shù)
check_interval = 0
[[runners]]
name = "rails builder"
url = "https://gitlab.com/"
token = "PROJECT_REGISTRATION_TOKEN"
executor = "docker"
clone_url = "https://gitlab.com/"
[runners.docker]
tls_verify = false
image = "ruby:2.5"
privileged = false
disable_cache = false
volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/opt/data/ws:/share:rw"]
shm_size = 0
[runners.cache]
3. 定義.gitlab-ci.yml
# 重新定義stages,可選,也可以使用默認(rèn)的;
stages:
- compile
- build
- deploy
# 將一些通用設(shè)置抽出來;
.general: &general
only:
- dev # 設(shè)置任務(wù)依賴的 git 分支
when: manual # 設(shè)置手工觸發(fā)
tags:
- ror # 設(shè)置哪個(gè)GitLab Runner來執(zhí)行任務(wù)
image: gitlab.com/builder:ror-v1 # 設(shè)置任務(wù)的執(zhí)行環(huán)境,這里為docker鏡像
script: # 設(shè)置任務(wù)具體內(nèi)容,依次列出shell腳本
- /share/script/$CI_JOB_NAME.sh
# 編譯任務(wù),任務(wù)名稱可任意設(shè)置
compile:
<<: *general # 引用通用設(shè)置
stage: compile # 設(shè)置任務(wù)在哪個(gè)stage執(zhí)行
artifacts: # 任務(wù)執(zhí)行完畢后,哪些內(nèi)容需要打包,供下載或給下一個(gè)任務(wù)使用
expire_in: 12h # 過期時(shí)間,過期后自動(dòng)刪除打包內(nèi)容
paths:
- public/assets/ # rails項(xiàng)目編譯后的assets
- public/packs/ # rails項(xiàng)目中用到了react,這是編譯后的react內(nèi)容
- .bundle/ # bundle install后的配置文件 < 修訂:新增>
# 構(gòu)建docker鏡像任務(wù)
build:
<<: *general
stage: build
image: docker:latest # 使用dind(Docker in Docker)的方式來構(gòu)建鏡像
# 部署任務(wù)
deploy:
<<: *general
stage: deploy
dependencies: [] # 依賴任務(wù)列表
配置文件提交到GitLab后,在管理界面 -> CI/CD -> Pipelines可以看到如下所示:


3.1 圖例說明
- 每次代碼提交都會(huì)產(chǎn)生一條新的
Pipeline,每條都有一個(gè)編號(hào),如圖中1標(biāo)注; - 點(diǎn)擊
Pipeline編號(hào)可以看到詳情,如圖3.2所示。在圖中可以手工觸發(fā)相應(yīng)的任務(wù); - 圖中第一條已經(jīng)手工觸發(fā)運(yùn)行過了,狀態(tài)是
passed,第二條狀態(tài)是skipped(還未手工觸發(fā)); - 配置文件中設(shè)置了3個(gè)
stage,如圖中2標(biāo)注; - 由于
compile任務(wù)設(shè)置了artifacts,圖中3標(biāo)注有可以點(diǎn)擊下載的選項(xiàng); - 圖中3標(biāo)注的左邊可以手工觸發(fā)任務(wù)執(zhí)行;
3.2 script說明
將shell腳本依次列在script的優(yōu)缺點(diǎn):
- 優(yōu)點(diǎn):可以將腳本變更記錄納入版本控制;
- 缺點(diǎn):不方便調(diào)試,每次修改都需要先提交;
為了方便調(diào)試,示例中將所有腳本都寫在單獨(dú)的shell文件中。
前面提到運(yùn)行GitLab Runner時(shí),我們配置了/opt/data/ws:/share:rw。該配置會(huì)自動(dòng)將主機(jī)的/opt/data/ws目錄自動(dòng)掛載到任務(wù)運(yùn)行環(huán)境(Docker)的/share目錄。因此,可以將所有shell腳本都放在本地/opt/data/ws。
GitLab自帶了一些環(huán)境變量供配置文件使用。示例中的$CI_JOB_NAME就是其中的一個(gè),該變量會(huì)自動(dòng)賦值為任務(wù)名稱。例如,在compile任務(wù)中,該變量為compile,執(zhí)行compile.sh。因此,可以在主機(jī)的/opt/data/ws目錄下創(chuàng)建三個(gè)shell文件compile.sh,build.sh和deploy.sh,分別用于執(zhí)行相應(yīng)的任務(wù)。
3.3 artifacts與dependencies說明
- 每個(gè)任務(wù)都可以通過
artifacts聲明,任務(wù)執(zhí)行完畢后,哪些內(nèi)容需要打包暫存,供下載或給下一個(gè)任務(wù)使用; - 若沒有特別聲明,每個(gè)任務(wù)都會(huì)默認(rèn)繼承前面任務(wù)的所有
artifacts; - 可以通過
dependencies聲明,依賴哪些任務(wù)的的artifacts; - 若不想繼承任何
artifacts,可聲明dependencies為空,如deploy任務(wù)所示;
運(yùn)行compile任務(wù),在任務(wù)結(jié)束時(shí),可以看到如下關(guān)于artifacts的信息:
...
Uploading artifacts...
public/assets/: found 631 matching files
public/packs/: found 15 matching files
Uploading artifacts to coordinator... ok id=7282 responseStatus=201 Created token=ExCbBThh
運(yùn)行build任務(wù),在任務(wù)開始前,可以看到如下關(guān)于artifacts的信息:
Downloading artifacts for compile (7282)...
Downloading artifacts from coordinator... ok id=7282 responseStatus=200 OK token=ExCbBThh
...
4. 構(gòu)建Rails編譯環(huán)境
- 將編譯環(huán)境和運(yùn)行環(huán)境分開,主要是想得到一個(gè)小而干凈的鏡像;
- 使用
ubuntu 18.04作為編譯環(huán)境,默認(rèn)可安裝ruby 2.5; - 安裝編譯工具包需要配置時(shí)區(qū),因此順道安裝設(shè)置了時(shí)區(qū);
- 安裝
nodejs和yarn(開發(fā)用到兩者了);
FROM ubuntu:18.04
MAINTAINER jacky.zhang <chenghaoz@gmail.com>
# 安裝并配置ruby、bundler
RUN apt update && \
apt install -y ruby && \
gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/ && \
gem install bundler --no-rdoc --no-ri && \
bundle config mirror.https://rubygems.org https://gems.ruby-china.com
ENV DEBIAN_FRONTEND=noninteractive # 避免設(shè)置時(shí)區(qū)有交互,打斷安裝過程
# 安裝必備軟件包(根據(jù)業(yè)務(wù)要求裁剪),并設(shè)置時(shí)區(qū)
RUN apt-get install -y build-essential libpq-dev libmysqlclient-dev imagemagick ghostscript apt-transport-https curl git ruby-dev tzdata && \
ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
dpkg-reconfigure -f noninteractive tzdata
# 安裝并配置nodejs、yarn
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - && \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
apt-get update && \
apt-get install -y nodejs yarn && \
sh -c 'echo https://registry.npm.taobao.org > ~/.npmrc'
5. 構(gòu)建Rails運(yùn)行環(huán)境
一直以來都使用mina部署Rails服務(wù),服務(wù)器環(huán)境為:Ubuntu + Nginx + Passenger。該環(huán)境穩(wěn)定運(yùn)行了好多年,因此想繼續(xù)沿用。
幾點(diǎn)說明:
- 沒有使用
ruby:2.5-alpine來做基礎(chǔ)鏡像的原因:- 構(gòu)建
Passenger過程相對復(fù)雜,需要從源碼編譯; - 構(gòu)建完的鏡像也沒小多少(也許有優(yōu)化空間?);
-
ubuntu環(huán)境相比較熟悉;
- 構(gòu)建
- 設(shè)置系統(tǒng)時(shí)區(qū):
上海; - 安裝
msyql和postgresql驅(qū)動(dòng)(業(yè)務(wù)同時(shí)需要連接兩個(gè)數(shù)據(jù)庫); - 安裝
imagemagick支持圖像處理; - 安裝
nodejs支持(應(yīng)該可以去掉,未驗(yàn)證); - 安裝
cron定時(shí)任務(wù)服務(wù)(業(yè)務(wù)需要); -
nginx需要單獨(dú)安裝,否則Pas -
Passenger官方安裝文檔中說明,需要先安裝ruby。經(jīng)驗(yàn)證,最新Passenger自帶ruby 2.5運(yùn)行環(huán)境。若滿足業(yè)務(wù)需求,可以不用單獨(dú)安裝ruby; - 構(gòu)建完鏡像大小約
400M,若清理一下/var/lib/apt/lists/,還可以減掉40M;
Dockerfile如下:
FROM ubuntu:18.04
MAINTAINER jacky.zhang <chenghaoz@gmail.com>
ENV DEBIAN_FRONTEND=noninteractive # 避免設(shè)置時(shí)區(qū)有交互,打斷安裝過程
# 安裝必備軟件包(根據(jù)業(yè)務(wù)要求裁剪),并設(shè)置時(shí)區(qū);
RUN apt-get update && \
apt-get install -y nginx cron imagemagick ghostscript libpq-dev libmysqlclient-dev nodejs tzdata && \
ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
dpkg-reconfigure -f noninteractive tzdata
# 安裝Passenger,自帶ruby 2.5;
RUN apt-get install -y dirmngr gnupg && \
apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 561F9B9CAC40B2F7 && \
apt-get install -y apt-transport-https ca-certificates && \
sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger bionic main > /etc/apt/sources.list.d/passenger.list' && \
apt-get update && \
apt-get install -y libnginx-mod-http-passenger && \
apt-get remove -y dirmngr gnupg && \
apt-get autoremove -y && \
apt-get clean
# 安裝并設(shè)置bundle
RUN gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/ && \
gem install bundler --no-rdoc --no-ri && \
bundle config mirror.https://rubygems.org https://gems.ruby-china.com
EXPOSE 80
# 默認(rèn)nginx和cron服務(wù)不開機(jī)啟動(dòng);
# ubuntu 18設(shè)置開機(jī)啟動(dòng)相對復(fù)雜,簡單起見,就寫在入口腳本里了;
ENTRYPOINT service nginx start && service cron start && tail -f /dev/null
6. 部署腳本
6.1 compile.sh
#!/bin/bash
echo 'compiling starts ...'
echo 'bundle: link and install '
# 為了避免每次都安裝所有g(shù)em,將bundle緩存在公共目錄;
ln -fs /share/env/bundle vendor/bundle
bundle install --deployment --clean
echo 'compile assets'
# 為了避免每次都所有安裝npm包,將npm包緩存在公共目錄;
# 注意:
# 這里不能使用link,否則nodejs編譯會(huì)報(bào)錯(cuò),或出現(xiàn)莫名其妙的bug;
# 具體原因應(yīng)該是某些npm包的路徑規(guī)則引起的;
mv /share/env/node_modules node_modules
RAILS_ENV=production bundle exec rails assets:precompile
mv node_modules /share/env/node_modules
echo 'compiling ends.'
6.2 build.sh
#!/bin/sh
echo 'building docker image starts ...'
echo 'copy bundle'
# 將緩存的bundle拷貝過來
cp -rf /share/env/bundle vendor/bundle
echo 'build start ...'
docker build -t test:latest .
echo 'remove untaged images'
# 如有必要移除未打標(biāo)簽的鏡像
docker rmi -f $(docker images | grep none | awk '{print $3}')
echo 'building ends.'
項(xiàng)目根目錄的Dockerfile如下:
FROM gitlab.com/passenger:latest
MAINTAINER jacky.zhang <chenghaoz@gmail.com>
# passenger 工作目錄
ENV APP_ROOT=/var/www/app
RUN mkdir -p $APP_ROOT
# passenger默認(rèn)使用www-data用戶
COPY --chown=www-data . $APP_ROOT
WORKDIR $APP_ROOT
# 再運(yùn)行一次bundle安裝,會(huì)在項(xiàng)目根目錄生成一些配置文件(可以在編譯時(shí)緩存,以后優(yōu)化)
# 如果用到whenver,就更新一下吧
# RUN RAILS_ENV=production bundle install --deployment && \
# RAILS_ENV=production bundle exec whenever --update-crontab
# 修訂:刪除RAILS_ENV=production bundle install --deployment
RUN RAILS_ENV=production bundle exec whenever --update-crontab
6.3 deploy.sh
部署過程主要通過ssh到遠(yuǎn)程服務(wù)器來完成:
- 先做備份;
- 移除舊的
docker容器; - 用新的鏡像重新部署,使用本地的配置文件,如nginx、項(xiàng)目的環(huán)境變量等;
- 部署完畢,根據(jù)需要運(yùn)行
db:migration,或重啟sidekiq服務(wù)等;
上述任務(wù)可以寫在一個(gè)shell腳本中完成,過程相對簡單這里略過;
7. 結(jié)束
本文記錄了從零經(jīng)驗(yàn)開始學(xué)習(xí)使用GitLab搭建CI/CD的一些經(jīng)驗(yàn),希望能幫到新入門的運(yùn)維人員。
后續(xù),正在進(jìn)行rancher + k8s + istio的ServiceMesh實(shí)踐,有時(shí)間話再來分享。