GitLab CI/CD實(shí)踐 - Ruby on Rails

前言

一直以來公司的開發(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)的圖:


圖1.1 GitLab CI/CD流程圖

說明:

  • GitLab CI/CD的PIPELINE是由一系列stage構(gòu)成的,如圖中CI PIPELINE的BUILD,UNIT TESTINTEGRATION 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è)stagebuildtestdeploy(可自定義,示例見下面配置文件);
  • 主要配置都由項(xiàng)目根目錄下的.gitlab-ci.yml設(shè)定;

再來看一下GitLab的執(zhí)行過程:


圖1.2 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)境,如shelldocker,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ù)量;

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,如下圖所示:


圖2.1 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 GitLab CI/CD Pipeline
圖3.2 Pipleline詳情

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.shdeploy.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ū);
  • 安裝nodejsyarn(開發(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)境相比較熟悉;
  • 設(shè)置系統(tǒng)時(shí)區(qū):上海;
  • 安裝msyqlpostgresql驅(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í)間話再來分享。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容