GitLab-CI/CD入門實操

萊布尼茨發表於2021-01-21

以Spring boot專案為例。傳統方式是本地生成jar包,FTP上傳伺服器,重啟服務;如果是內網測試服,也可以在伺服器上安裝git,在伺服器上編譯打包。但這都需要人為干預,於是CI/CD就出現了。

  • CI:Continuous Integration(持續整合)。自動構建和測試每次提交的程式碼,以確保所引入的更改符合所有測試、準則和程式碼合規性標準。
  • CD:Continuous Delivery(持續交付)和Continuous Deployment(持續部署)。基於CI,前者側重於交付給客戶或質量團隊(比如決定是否對新版本進行壓測),而後手動部署/自動部署,如果是自動部署的話就是持續部署了。

CI/CD的工具有很多,最流行的當屬jenkins。不過以筆者為數不多的經驗來看,作為後起之秀的gitlab更簡單一點,也更靈活,不會像jenkins那樣笨重。當然,兩者的概念都是挺多的,沒有師父,光靠自己入門都不容易。

GitLab-CI/CD流程示例

從左往右看,首先研發人員完成需求提交程式碼到 GitLab。GitLab 觸發一次 Build,構建好服務,然後開始跑單元測試、整合測試。等待測試結果通過後,再由負責該專案的同事進行 CodeReview,灰度釋出,正式部署到線上。

概念

本文基於GitLab 13.7版本

Pipline

Pipelines comprise:

  • Jobs, which define what to do. For example, jobs that compile or test code.
  • Stages, which define when to run the jobs. For example, stages that run tests after stages that compile the code.

Jobs are executed by runners. Multiple jobs in the same stage are executed in parallel, if there are enough concurrent runners.

Stages

  • Manage:專案週期或團隊週期的各項資料統計分析。主要是各環節耗時統計,比如對於典型的Issue(提出問題)->Plan(列入計劃)->Code(編碼)->Test(測試)->Package(打包)流程,每個環節的耗時決定了整體問題處理的響應速度。
  • Plan:藉助諸多工具進行有效的專案管理。
  • Create:程式碼管理。
  • Verify:程式碼質量分析、程式碼合併(持續整合)、單元測試等。
  • Package:將程式碼打包,並作為依賴庫對外提供。
  • Secure(ULTIMATE版提供):檢查應用程式是否存在可能導致未經授權訪問、資料洩漏或拒絕服務的安全漏洞。GitLab可以對應用程式的程式碼執行靜態和動態測試,查詢已知的缺陷並在合併請求中報告它們。然後可以在合併之前修復缺陷。安全團隊可以使用儀表板獲取專案和組的高階檢視,並在需要時啟動修正過程。
  • Release:持續交付。
  • Configure:配置[檔案]參與DevOps各環節。
  • Monitor:GitLab收集並顯示已部署應用程式的效能指標,以便您可以立即知道程式碼更改如何影響生產環境。
  • Defend:若干用於服務安全防禦的中介軟體。

上述包含了GitLab-DevOps整個流程的所有環節,CI/CD只是其中的一部分。

GitLab Runner

可以安裝在任意機子上,通過它可以[在一臺機子上]註冊多個runner例項到gitlab伺服器。每個runner用於執行一個或多個具體任務(如build、test)。

runner有以下三類,可用範圍從大到小

  • Shared runners are available to all groups and projects in a GitLab instance.
  • Group runners are available to all projects and subgroups in a group.
  • Specific runners are associated with specific projects. Typically, specific runners are used for one project at a time.

我們可以直接安裝GitLab Runner到宿主機,也可以使用docker方式安裝。注意這兩種方式會影響到後續.gitlab-ci.yml中對pipline的定義。比如要編譯maven專案,如果executor設為shell,那麼若宿主機中安裝有mvn命令,前者可以在scripts中直接使用mvn,而後者並不能,只能通過定義Dockerfile,在其中定義搭建mvn環境到編譯程式碼的整個流程。

採用docker方式安裝的話,可以參考Docker搭建自己的Gitlab CI Runner

docker pull gitlab/gitlab-runner:latest
docker run -d --name inkscreen-api-runner --restart always \
  -v /srv/gitlab-runner/config:/etc/gitlab-runner \
  -v /var/run/docker.sock:/var/run/docker.sock \
  gitlab/gitlab-runner:latest

註冊runner例項

docker exec -it inkscreen-api-runner gitlab-runner register

會讓我們填一系列配置項,如下:

Enter the GitLab instance URL (for example, https://gitlab.com/):
http://192.168.1.26:9980/
Enter the registration token:
cJMXGJWx7qx9AmpSc6ee
Enter a description for the runner:
[a0debaaf80a9]: runner for InkScreen-API project
Enter tags for the runner (comma-separated):
InkScreenAPI
Registering runner... succeeded                     runner=cJMXGJWx
Enter an executor: docker-ssh, shell, docker-ssh+machine, kubernetes, custom, parallels, ssh, virtualbox, docker+machine, docker:
docker
Enter the default Docker image (for example, ruby:2.6):
maven:3-jdk-8

完事後,我們在gitlab->xxxProjct中就能找到該runner:

接下來,就可以定義專案構建流程了。專案的構建流程是由專案根目錄的.gitlab-ci.yml檔案控制的。當然了,一個pipeline可以涉及到多個runner。

.gitlab-ci.yml

定義一個pipline,以下為示例

variables:
  DOCKER_TLS_CERTDIR: "/certs"

# stage也可以自定義
stages:
  - build jar
#  - test
  - build and run image

#job's name 可以隨意取
buildJar:
  stage: build jar
  variables:
    # 若要使cache生效,須指定-Dmaven.repo.local
    MAVEN_OPTS: "-Dmaven.repo.local=.m2"
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - .m2
  only:
    - dev
  script:
#    package 已包含 test 步驟,所以流程中不需要另外配置test job
    - mvn clean package
  tags:
    - inkscreen_api
  artifacts:
    paths:
      - target/admin.jar
    expire_in: 3600 seconds

deploy:
  stage: build and run image
  image: docker:stable
  services:
    - docker:dind
  only:
    - dev
  variables:
    IMAGE_NAME: newton/inkscreen-api:$CI_COMMIT_REF_NAME
    PORT: 38082
  script:
    - docker build --build-arg JAR_PATH=target/admin.jar -t $IMAGE_NAME .
    - docker run -p $PORT:$PORT  -d --name inkscreen-$CI_COMMIT_REF_NAME --env spring.redis.host=myredis $IMAGE_NAME
  tags:
    - inkscreen_api

cache

cache常用在dacker-based job之間傳遞檔案。比如專案依賴的公共jar包,jobA辛辛苦苦從網上down了下來,結果執行完了,jobA所在容器也跟著被移除,自然裡面的所有檔案都不存在了。後續其它job用到相同的jar包還要重新下載。同樣的,pipline多次執行,jobA自己每次也要重新下載。

為了解決這個問題,gitlab-ci採用了cache的方式。指定檔案/目錄,每次job結束前將其打包,放到/etc/gitlab-runner/config.toml中對應的[runners.docker][volumes]指定的卷內,其它job(包括自己)執行前,對應的cache都會被載入並解壓到容器內。

artifacts

artifacts是job生成的中間產物,會以壓縮包(.zip)的形式生成。它會自動上傳到gitlab伺服器,the artifacts will be downloaded and extracted in the context of later stages。所以它和cache很像,但是設計它們的初衷是不同的。

Don't use caching for passing artifacts between stages, as it is designed to store runtime dependencies needed to compile the project:

  • cache: For storing project dependencies

    Caches are used to speed up runs of a given job in subsequent pipelines, by storing downloaded dependencies so that they don't have to be fetched from the internet again (like npm packages, Go vendor packages, etc.) While the cache could be configured to pass intermediate build results between stages, this should be done with artifacts instead.

  • artifacts: Use for stage results that will be passed between stages.

    Artifacts are files generated by a job which are stored and uploaded, and can then be fetched and used by jobs in later stages of the same pipeline. In other words, you can't create an artifact in job-A in stage-1, and then use this artifact in job-B in stage-1. This data will not be available in different pipelines, but is available to be downloaded from the UI.

The name artifacts sounds like it's only useful outside of the job, like for downloading a final image, but artifacts are also available in later stages within a pipeline.

另外,同樣key的cache會被覆蓋,而artifacts一旦生成就固定了,當然我們可以設定expire_in,過期刪除之。

可參看各類語言/平臺的.gitlab-ci.yml模板

實戰

我們定義一個最簡單的pipline:第一步編譯生成jar包,第二步將jar包匯入docker映象並執行,在某些環節還需加入程式碼review。因為最後我們會以docker容器執行jar包,所以這裡不建議docker-based runner/executor的形式,因為該形式導致Docker-in-Docker的場景,帶來可能的一些麻煩且難以解決的問題(比如內嵌容器如何關聯外部服務以及對外提供服務)。所以我們直接宿主機安裝GitLab Runner。

如果一定要以docker-based形式,那麼可參看使用GitLab CI和Docker自動部署SpringBoot應用。在該文中,並沒有在runner所在宿主機中執行容器,而是將生成的映象釋出到映象倉庫,再登入目標伺服器拉取映象執行,所以不存在Docker-in-Docker的麻煩事。

安裝&註冊[GitLab ]Runner

# 1.Add the official GitLab repository
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh" | sudo bash
# 2.Install the latest version of GitLab Runner
export GITLAB_RUNNER_DISABLE_SKEL=true; sudo -E yum install gitlab-runner

註冊runner

sudo gitlab-runner register -n \
  --url http://192.168.1.26:9980/ \
  --registration-token cJMXGJWx7qx9AmpSc6ee \
  --executor shell \
  --tag-list "inkscreen_hostrunner" \
  --description "Host Runner for InkScreen"

Add the gitlab-runner user to the docker group:

sudo usermod -aG docker gitlab-runner

.gitlab-ci.yml

stages:
  - build jar
  - build and run image

#job's name 可以隨意取
buildJar:
  stage: build jar
  variables:
    # 預設是clone,改為fetch加快拉取速度(若本地無則會自動clone)
    GIT_STRATEGY: fetch
  only:
    - dev
  script:
    - >
      docker run -d --rm --name justforpackage-$CI_COMMIT_REF_NAME
      -v "$(pwd)":/build/inkscreen
      -v /inkscreen/maven/m2:/root/.m2
      -w /build/inkscreen
      maven:3-jdk-8 mvn clean package

    - sleep 60
  tags:
    - inkscreen_hostrunner
  artifacts:
    paths:
      - louwen-admin/target/louwen-admin.jar
    expire_in: 3600 seconds

testDeploy:
  stage: build and run image
  only:
    - dev
  variables:
    # 不拉取程式碼
    GIT_STRATEGY: none
    IMAGE_NAME: louwen/inkscreen-api:$CI_COMMIT_REF_NAME
    PORT: 38082
  before_script:
    # 移除舊容器和映象。這裡為什麼要寫成一行,下面有講
    - if [ docker ps | grep inkscreen-$CI_COMMIT_REF_NAME ]; then docker stop inkscreen-$CI_COMMIT_REF_NAME; docker rm inkscreen-$CI_COMMIT_REF_NAME; docker rmi $IMAGE_NAME; fi
  script:
    - docker build --build-arg JAR_PATH=louwen-admin/target/louwen-admin.jar -t $IMAGE_NAME .
    - >
      docker run -d --name inkscreen-$CI_COMMIT_REF_NAME
      -p $PORT:$PORT
      --network my_bridge --env spring.redis.host=myredis
      -v /inkscreen/inkscreen-api/logs/:/logs/
      -v /inkscreen/inkscreen-api/louwen-admin/src/main/resources/:/configs/
      $IMAGE_NAME
  tags:
    - inkscreen_hostrunner

注意build jar環節我們sleep了60秒,是因為docker run並不會等待內部指令碼執行完,而是啟動後就直接返回了,此時jar包尚未生成,所以此處阻塞一段時間等待打包結束。正常應該寫一段指令碼迴圈判斷jar包是否已生成,若生成或超時則跳出迴圈,此處作為演示簡單sleep。

在testDeploy任務中,before_script被我寫成了一行,最初版本是:

  before_script:
    # 若未找到記錄,則該條命令會返回1,gitlab就直接報錯返回了ERROR: Job failed: exit status 1
    - docker ps | grep inkscreen-$CI_COMMIT_REF_NAME
    - >
      if [ $? -eq 0 ]
      then
        docker stop inkscreen-$CI_COMMIT_REF_NAME
        docker rm inkscreen-$CI_COMMIT_REF_NAME
        docker rmi $IMAGE_NAME
      fi

後改為

  before_script:
    # 將檢測語句直接作為條件內建,解決了上面的問題
    - >
      if       
      docker ps | grep inkscreen-$CI_COMMIT_REF_NAME
      then
        docker stop inkscreen-$CI_COMMIT_REF_NAME
        docker rm inkscreen-$CI_COMMIT_REF_NAME
        docker rmi $IMAGE_NAME      
      fi

報錯syntax error near unexpected token 'fi',估計是換行/回車格式的原因。上述兩個問題都可以通過單獨建.sh檔案的方式解決,我這裡簡單地將所有語句排成一行。

Dockerfile

生成映象自然少不了Dockerfile

FROM openjdk:8-jdk-oracle
MAINTAINER louwen

# 外部傳入,主程式路徑
ARG JAR_PATH
ENV LANG=C.UTF-8 LC_ALL=C.UTF-8

COPY $JAR_PATH /app.jar
EXPOSE 38082
ENTRYPOINT ["java","-jar","/app.jar"]

題外話,其實我們完全可以將build jar環節也放在Dockerfile中,如下

#
# build jar stage
#
FROM maven:3-jdk-8 AS MAVEN_BUILD

COPY pom.xml /build/
COPY src /build/src/
WORKDIR /build/
RUN mvn clean package

#
FROM openjdk:8-jdk-oracle
MAINTAINER louwen
COPY --from=MAVEN_BUILD /build/target/*.jar /app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

程式碼規範

目前較流行的程式碼檢測工具是SonarQube,不過其社群版本對同一個程式碼倉庫無法區分不同分支,從而實現按程式碼的不同分支顯示對應分支的掃描結果。這裡我們使用Gitlab-CI的Code Quality stage,其使用的是Codeclimate,它是為程式碼質量分析平臺提供的一個命令列介面工具,通過它可以在本機 Docker 容器中對要分析的程式碼執行質量分析,並生成分析報告。我們熟知常用的程式碼質量檢測工具例如 SonarQube、CheckStyle 等等,而 Codeclimate 接入了這些工具,而且支援我們自定義檢測工具。

按照官方說法,使用Code Quality需要基於docker-based runner/executor,所以我們另外使用docker方式安裝GitLab-Runner並註冊一個runner(參考上述概念小節),executor選擇docker。

include:
  - template: Code-Quality.gitlab-ci.yml

# 以下配置參考網上一些資料,據說是官方示例,然而我沒有在官方文件找到
code_quality:
  image: docker:stable
  variables:
    DOCKER_DRIVER: overlay2
    # gitlab 13.6及之後版本支援
    REPORT_FORMAT: html
  allow_failure: true
  services:
    - docker:dind
  script:
  # 映象版本號格式參看 https://gitlab.com/gitlab-org/ci-cd/codequality/-/tree/master#versioning-and-release-cycle
#    - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
    - docker run
      --net=host
      --env SOURCE_CODE="$PWD"
      --volume "$PWD":/code
      --volume /var/run/docker.sock:/var/run/docker.sock
      "registry.gitlab.com/gitlab-org/security-products/codequality:${VERSION:-latest}" /code
  artifacts:
    paths: [ gl-code-quality-report.html ]
  tags:
    - InkScreenAPI

執行的時候可能會卡在拉取映象環節。手動docker pull registry.gitlab.com/gitlab-org/security-products/codequality:latest,發現各種超時。我開個阿里雲香港ECS的搶佔式例項(便宜)然後docker pull | save | load將映象檔案遷移到公司測試服,還是報Unable to find image 'registry.gitlab.com/gitlab-org/security-products/codequality:latest' locally,不知如何將host中的映象對映到docker:stable中。看來還是得kexue上網。

理論上,需要專人在合適的時候對提交的程式碼進行質量把關,一般這工作可以放在Merge Request下進行。Merge Request的工作流程可以參看在團隊中使用GitLab中的Merge Request工作模式

FAQ

  1. dial tcp: lookup docker on 192.168.1.1:53: no such host錯誤。
    This error occurs with docker-based gitlab runners such as the one we’re that are configured using a docker executor. The error message means that the inner docker container doesn’t have a connection to the host docker daemon.
    解決:將/etc/gitlab-runner/config.toml中對應的[runners.docker]節點設定privileged = true,增加捲對映volumes = ["/cache", "/var/run/docker.sock:/var/run/docker.sock"]或在.gitlab-ci.yml的job定義中增加services: - docker:dind

  2. Cannot connect to the Docker daemon at tcp://docker:2375. Is the docker daemon running?錯誤
    解決:增加捲對映volumes = ["/certs/client", "/cache"],然後在.gitlab-ci.yml中增加變數DOCKER_TLS_CERTDIR: "/certs"

  3. 拉取程式碼時提示warning: failed to remove xxxx: Permission denied
    簡單粗暴地編輯/etc/passwd,將gitlab-runner賬號對應的uid:gid改為0:0(和root一樣)。

  4. Code Quality提示docker: Error response from daemon: Head https://registry.gitlab.com/v2/gitlab-org/security-products/codequality/manifests/13-7-stable: Get https://gitlab.com/jwt/auth?scope=repository%3Agitlab-org%2Fsecurity-products%2Fcodequality%3Apull&service=container_registry: dial tcp [2606:4700:90:0:f22e:fbec:5bed:a9b9]:443: connect: cannot assign requested address.
    在scripts->docker run增加引數--net=host

其它

發件郵箱配置

在pipline流程執行過程中,我們希望有任何風吹草動都能及時收到訊息,郵件就是一個比較好的提醒方式。

vi /etc/gitlab/gitlab.rb

### GitLab email server settings
###! Docs: https://docs.gitlab.com/omnibus/settings/smtp.html
###! **Use smtp instead of sendmail/postfix.**

gitlab_rails['smtp_enable'] = true
gitlab_rails['smtp_address'] = "smtp.exmail.qq.com"
gitlab_rails['smtp_port'] = 465
gitlab_rails['smtp_user_name'] = "xxxx@yyyy.com"
gitlab_rails['smtp_password'] = "xxxxxxxx"
gitlab_rails['smtp_domain'] = "exmail.qq.com"
gitlab_rails['smtp_authentication'] = "login"
gitlab_rails['smtp_enable_starttls_auto'] = true
gitlab_rails['smtp_tls'] = true

### Email Settings

gitlab_rails['gitlab_email_enabled'] = true

##! If your SMTP server does not like the default 'From: gitlab@gitlab.example.com'
##! can change the 'From' with this setting.
##! 要與上面的 smtp_user_name 保持一致
gitlab_rails['gitlab_email_from'] = 'xxxx@yyyy.com'
# gitlab_rails['gitlab_email_display_name'] = 'Example'
# gitlab_rails['gitlab_email_reply_to'] = 'noreply@example.com'
# gitlab_rails['gitlab_email_subject_suffix'] = ''
# gitlab_rails['gitlab_email_smime_enabled'] = false
# gitlab_rails['gitlab_email_smime_key_file'] = '/etc/gitlab/ssl/gitlab_smime.key'
# gitlab_rails['gitlab_email_smime_cert_file'] = '/etc/gitlab/ssl/gitlab_smime.crt'
# gitlab_rails['gitlab_email_smime_ca_certs_file'] = '/etc/gitlab/ssl/gitlab_smime_cas.crt'

gitlab-ctl reconfigure使配置生效
測試

gitlab-rails console
irb(main):003:0> Notify.test_email('whatever@qq.com', 'Message Subject', 'Message Body').deliver_now

登入whatever@qq.com檢視受否收到信件。


jenkins + gitlab

如果使用jenkins作為CI/CD工具,程式碼由gitlab託管,那麼它們之間的互動需要兩個token:

  1. api token,用於jenkins呼叫gitlab api使用
  2. ssh金鑰對,jenkins拉取程式碼使用(當然我們也可以使用使用者名稱/密碼方式拉取)

mvn package、install、deploy都幹了什麼

  • mvn clean package依次執行了clean、resources、compile、testResources、testCompile、test、jar(打包)等7個階段。
  • mvn clean install依次執行了clean、resources、compile、testResources、testCompile、test、jar(打包)、install等8個階段。
  • mvn clean deploy依次執行了clean、resources、compile、testResources、testCompile、test、jar(打包)、install、deploy等9個階段。

由上可知:

  • package命令完成了專案編譯、單元測試、打包功能,但沒有把打好的可執行jar包(war包或其它形式的包)佈署到本地maven倉庫和遠端maven私服倉庫
  • install命令完成了專案編譯、單元測試、打包功能,同時把打好的可執行jar包(war包或其它形式的包)佈署到本地maven倉庫,但沒有佈署到遠端maven私服倉庫
  • deploy命令完成了專案編譯、單元測試、打包功能,同時把打好的可執行jar包(war包或其它形式的包)佈署到本地maven倉庫和遠端maven私服倉庫

alpine

Alpine Linux 是一個社群開發的面向安全應用的輕量級Linux發行版。很多映象都會專門基於Alpine構建,大小會小很多。比如:

  • gitlab/gitlab-runner:latest based on Ubuntu.
  • gitlab/gitlab-runner:alpine based on Alpine with much a smaller footprint (~160/350 MB Ubuntu vs ~45/130 MB Alpine compressed/decompressed).

修改GitLab-ce域名

剛部署好的GitLab新建的專案ssh地址一般是個短連結如git@AKDJF3ld:xxx,有時候會不太好使,可以通過配置檔案的修改,指向域名。

vim /opt/gitlab/embedded/service/gitlab-rails/config/gitlab.yml
# host: 192.168.xx.xx
# port: xxxx
gitlab-ctl restart

參考資料

當談到 GitLab CI 的時候,我們該聊些什麼(上篇)
什麼是devops,基於Gitlab從零開始搭建自己的持續整合流水線(Pipeline)
持續整合之.gitlab-ci.yml篇
Building Docker images with GitLab CI/CD
自動化 DevOps 使用 Codeclimate 執行程式碼質量分析
GitLab CI/CD
基於 Gitlab 的 Code Review 最佳實踐

相關文章