跟著 GitLab Auto DevOps 學習 CI/CD Pipeline(四)

系列文第四篇,本系列文將會逐一檢視並解析 GitLab Auto DevOps 背後的 Templates,讓我們一起跟著 GitLab Auto DevOps 學習 GitLab CI/CD Pipeline。

本文為系列文章《跟著 GitLab Auto DevOps 學習 CI/CD Pipeline》的第四篇,藉由逐一檢視並解析 GitLab Auto DevOps 背後的 Templates,讓我們一起跟著 GitLab Auto DevOps 學習 GitLab CI/CD Pipeline。

上一篇文章,我們學習到 workflow: 可以幫助我們控制在哪些條件之下才需要產生 CI/CD Pipeline。這次我們則是要進入第一個 Template - Jobs/Build.gitlab-ci.yml,看看 Auto DevOps 是如何實現同一個 CI Build 卻能適用於不同的程式語言。

此系列文清單:

Jobs/Build.gitlab-ci.yml

首先讓我們直搗黃龍,直接查看 Jobs/Build.gitlab-ci.yml內容

# 下面的版本來自 2022/02/04 的 https://gitlab.com/gitlab-org/gitlab/-/blob/1b56d49344fc468bf3ba537d367fe21ac79b6afa/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
variables:
  AUTO_BUILD_IMAGE_VERSION: 'v1.5.0'

build:
  stage: build
  image: 'registry.gitlab.com/gitlab-org/cluster-integration/auto-build-image:${AUTO_BUILD_IMAGE_VERSION}'
  variables:
    DOCKER_TLS_CERTDIR: ''
  services:
    - name: 'docker:20.10.6-dind'
      command: ['--tls=false', '--host=tcp://0.0.0.0:2375']
  script:
    - |
      if [[ -z "$CI_COMMIT_TAG" ]]; then
        export CI_APPLICATION_REPOSITORY=${CI_APPLICATION_REPOSITORY:-$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG}
        export CI_APPLICATION_TAG=${CI_APPLICATION_TAG:-$CI_COMMIT_SHA}
      else
        export CI_APPLICATION_REPOSITORY=${CI_APPLICATION_REPOSITORY:-$CI_REGISTRY_IMAGE}
        export CI_APPLICATION_TAG=${CI_APPLICATION_TAG:-$CI_COMMIT_TAG}
      fi      
    - /build/build.sh
  artifacts:
    reports:
      dotenv: gl-auto-build-variables.env
  rules:
    - if: '$BUILD_DISABLED'
      when: never
    - if: '$AUTO_DEVOPS_PLATFORM_TARGET == "EC2"'
      when: never
    - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH'

build_artifact:
  stage: build
  script:
    - printf "To build your project, please create a build_artifact job into your .gitlab-ci.yml file.\nMore information at https://docs.gitlab.com/ee/ci/cloud_deployment\n"
    - exit 1
  rules:
    - if: '$BUILD_DISABLED'
      when: never
    - if: '$AUTO_DEVOPS_PLATFORM_TARGET == "EC2"'

接著開始一段一段的解釋。

首先官方定義了一個 Variable - AUTO_BUILD_IMAGE_VERSION 宣告要使用哪一個版本的 Container image。官方為了 Build 有特別準備一個特殊的 image,並持續更新維護。關於 Image 的詳細內容,我們留待後面再說明。

variables:
  AUTO_BUILD_IMAGE_VERSION: 'v1.5.0'

接著在 CI Job - build: 中,立即指定要使用這個特別的 Image。

build:
  # CI Job - build 當然是放在 stage: build
  stage: build
  # 指定要用特別的 container image 來執行這項 CI Job
  image: 'registry.gitlab.com/gitlab-org/cluster-integration/auto-build-image:${AUTO_BUILD_IMAGE_VERSION}'

【補充】如上,可以看見這個 Docker image 存放在官方自己的 Registry,這是一個 Public registry,因此你也可以自行透過 docker pull 指令取得這個 Image。

(實際的 CI Job 中,確實會看到有相關的 docker pull 動作。)

接著有兩個重要的設定。

  variables:
    DOCKER_TLS_CERTDIR: ''
  services:
    - name: 'docker:20.10.6-dind'
      command: ['--tls=false', '--host=tcp://0.0.0.0:2375']

在 Job 中定義的 variables:DOCKER_TLS_CERTDIR 是為了配合 Docker 官方對於 docker:dind 這個 docker image 的更新,透過設置 DOCKER_TLS_CERTDIR: '' 藉此來 Disable TLS,讓 Runner 在 docker in docker 的狀態下,能順利操控 Docker daemon。

【補充】variables:DOCKER_TLS_CERTDIR 會去覆蓋同名的 Environment variable,所以才能達成 Disable TLS。關於 DOCKER_TLS_CERTDIR 的更多資訊可參考 GitLab 官方文章「Update: Changes to GitLab CI/CD and Docker in Docker with Docker 19.03」。

services:

第二個就是 services:,這是一個特別 GitLab CI Keyword,用途是為 CI Job 提供執行該 Job 所需的額外「資源」。以本文的 Job build: 為例,由於這項 CI Job 會需要執行一些 docker command 像是 docker builddocker push,因此我們要讓 GitLab Runner 在執行這項 CI Job 時,先預備一個 Service 即是 docker:dind,如此 Runner 在 CI Job 的過程中,才能正常執行這些 docker command。

【補充】services: 只能運用在以 Container 執行 CI Job 的情境,因為 services: 它所做的事情其實就是幫使用者運行另一個 Container,並讓它和執行 CI Job 的 Container 之間建立連結(link)。services: 常見的運用情境包含:

  • 透過 docker:dind 令 CI Job 能執行 docker command;
  • services: 運行臨時的 DB,以便 CI 能自動化測試應用程式與資料庫有關的功能;
  • 或其他類似的情境,以 services: 運行任何相依的「資源」,以便 CI Job 能順利執行相應的動作。

因此要能善用 services: 的前提是,你是否已熟悉 Container 為主的 Workflow 與 Pipeline,了解如何將 Container 彼此連結,如何用 Container 建構出你所需的工作或應用程式運行環境。(更多 services: 的內容,請參閱官方文件,或等我有空另外撰文介紹。)

rules: + when: nerver

接著先跳過最重要的 script:,以及需要較多說明的 artifacts:,讓我們先看比較容易理解的 rules:

如我們在上一篇文章提過的,rules: 是 GitLab CI 用來做條件判斷的功能,透過它我們可以控制在何種條件之下才需要執行 CI Job,並且 rules: 能與 when: 搭配使用。

因此那整段 rules: 轉換成白話文後,即是下面的意思。

  rules:
    # 假如變數 $BUILD_DISABLED 為 true,就不執行此 CI Job,
    # 藉此讓使用者可以彈性的控制,是否需要讓 Auto DevOps 自動用 Template 的方式幫你 build artifacts。
    - if: '$BUILD_DISABLED'
      when: never
    # 假如變數 $AUTO_DEVOPS_PLATFORM_TARGET 等於 EC2
    # 就不要執行此 CI Job。
    - if: '$AUTO_DEVOPS_PLATFORM_TARGET == "EC2"'
      when: never
    # 假如有為此 Project 建立 Tag,或有 Commit 被送到任何 branch,
    # 就要執行此 CI Job。
    - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH'

第一個 if: '$BUILD_DISABLED' 是為了 Auto DevOps 的彈性而設置的條件,讓使用者可以自主的取消 build。

【補充】variable:BUILD_DISABLED 到底何時會用到呢?基本上就是如果你打算對 Auto DevOps 做一些客製化的動作時,就可能用到它,例如你不想用 Auto DevOps 內建的 Job build:,想改換成自己撰寫的 Job 時。在本系列文我們一開始檢閱的檔案 Auto-DevOps.gitlab-ci.yml 它也是一個可以被引用的 Template(template: Auto-DevOps.gitlab-ci.yml),因此你確實可以做到只取代 Job build:,讓 Pipeline 後續的測試與部署依然按著 Auto DevOps 的規劃繼續執行下去。

第二個 if: '$AUTO_DEVOPS_PLATFORM_TARGET == "EC2"' 則讓我們搭配第二個 CI Job - build_artifact: 一起看。

build_artifact:
  stage: build
  script:
    - printf "To build your project, please create a build_artifact job into your .gitlab-ci.yml file.\nMore information at https://docs.gitlab.com/ee/ci/cloud_deployment\n"
    - exit 1
  rules:
    - if: '$BUILD_DISABLED'
      when: never
    - if: '$AUTO_DEVOPS_PLATFORM_TARGET == "EC2"'

如上,在第二個 CI Job 的 rules: 也有一組條件是 if: '$AUTO_DEVOPS_PLATFORM_TARGET == "EC2"',但這次並沒有 when: never。也就是當使用者設置了 variable:AUTO_DEVOPS_PLATFORM_TARGET,在 Stage: build 就只會有 build_artifact: 這項 CI Job。

【補充】variable:AUTO_DEVOPS_PLATFORM_TARGET 到底是用來做啥的呢?老實說在本文中一點用處的沒有(喂),它只是用來取消 Auto DevOps 預設的 Job build:。這個 Variable 算是 Auto DevOps Template 的另一條路線(示範),官方示範了可以如何運用 GitLab CI 來自動將你的 Application 直接部署至 AWS 的 EC2。有興趣了解更多細節的朋友,可以先收看官方的示範影片,未來有時間我也會再另外撰文詳細介紹。(謎之音:好啊好啊,全部都說有空有時間啊~)

artifacts:reports:dotenv

接下來,輪到 artifacts:

  artifacts:
    reports:
      dotenv: gl-auto-build-variables.env

artifacts: 大家應該都知道,就是 GitLab 的 Job Artifacts 功能,透過設置 artifacts: 可以將指定的檔案儲存至 Job Artifacts 供使用者事後下載,以及讓 Pipeline 能繼續運用同一份 Artifacts 執行後續的 CI Job。

artifacts:reports 則是 Job Artifacts 的另一種用法,是專門用來儲存執行 CI Job 所產生的各種 Report,例如有些自動化測試執行完畢後會產生一些 Report,而 artifacts:reports 即是幫助使用者更容易整合這一類 Report 的功能。

【補充】artifacts:reports 目前也不是所有類型及所有工具產生的 Report 都能接受,在目前 GitLab 版本 14.7,可接受 19 種不同的 Report,但其中有 11 種需要升級至付費版的等級。

至於 artifacts:reports:dotenv 則是一個並非 Report 的 Report,它並不是用來儲存 Report,而是用來儲存 Environment variable。下面用一個其他範例來說明。


# 首先我有一個 CI Job - build
build:
  stage: build
  script:
    ## 假的,假裝我真的有 build 了啥
    - echo "fake build-job!!!"
    ## 我用 echo 的方式產生出一個 deploy.env,裡面會定義我後續會用到的 Variable
    ## 總共會有三個 variable - WEBSITE_URL, ENVIRONMENT, HOST
    - echo "WEBSITE_URL=https://chengweichen.com" >> deploy.env
    - echo "ENVIRONMENT=production" >> deploy.env
    - echo "HOST=my_server" >> deploy.env
  ## 接著用 artifacts:reports:dotenv 來儲存它
  artifacts:
    reports:
      dotenv: deploy.env   

# 在下一個 CI Job - deploy 中,我們就可以直接使用這些 Environment variable
deploy:
  stage: deploy  
  script:
    ## 如果使用 env 指令來印出所有的 Environment variable,會發現前面定義的 Variable 都有被傳遞過來。
    - env
    ## 既然有傳遞過來,當然可以直接在 command 中使用它們。
    - ansible-playbook deploy.yml -e DEPLOY_HOST=$HOST -e DEPLOY_ENVIRONMENT=$ENVIRONMENT -e WEBSITE_URL=$WEBSITE_URL
  environment:
    name: production
    ## 傳遞過來的 Environment variable 也可以運用在 environment:
    url: $WEBSITE_URL

artifacts:reports:dotenv 的功能就如上範例,官方文件也有其他的示範與說明,由於 Auto DevOps 的 Job build: 會根據狀況產生出後續 CI Job 會使用到的 Environment variable,因此才會在 Job build: 中使用 artifacts:reports:dotenv

(在實際的 Job build: 可以看見確實有儲存了 artifacts:reports:dotenv。)

【補充】基本上我覺得 GitLab 目前將它放在 artifacts:reports: 之下,在語義上有點怪,也許未來會被改掉。

黑魔法 /build/build.sh

最後讓我們進入重頭戲,到底 script: 中 Auto DevOps 用了什麼黑魔法。

  script:
    - |
      if [[ -z "$CI_COMMIT_TAG" ]]; then
        export CI_APPLICATION_REPOSITORY=${CI_APPLICATION_REPOSITORY:-$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG}
        export CI_APPLICATION_TAG=${CI_APPLICATION_TAG:-$CI_COMMIT_SHA}
      else
        export CI_APPLICATION_REPOSITORY=${CI_APPLICATION_REPOSITORY:-$CI_REGISTRY_IMAGE}
        export CI_APPLICATION_TAG=${CI_APPLICATION_TAG:-$CI_COMMIT_TAG}
      fi      
    - /build/build.sh

首先,這裡官方再次為我們示範如何在 script: 中直接使用 shell 的 if / else。如果你只是要做簡單的情境判斷,就如同上面的範例,它只是為了判斷是不是有打 tag,再因此產生不同內容的 Variable,這時直接使用 shell 的 if / else 是比較省事的。但若是要做的判斷及動作較多,建議還是另外撰寫成單一的 shell script,不然 script: 會被你塞爆變得太長反而不易閱讀。

script: 的最後執行了黑魔法 /build/build.sh,這是已經打包在本文一開始提到的 Image - registry.gitlab.com/gitlab-org/cluster-integration/auto-build-image:${AUTO_BUILD_IMAGE_VERSION} 之內的 shell script,而我們必須前往另一個官方 Project 才能找到它的內容。

官方 Image - auto-build-image 一樣有公開 Project,在其中即可找到 /build/build.sh,但由於內容有一點多,我就不轉貼在文章中,有興趣的朋友可以自行追 Code,下面我就直接以條列的方式簡單說明它會做哪些事情。

  1. Login GitLab Container Registry。
  2. Login GitLab Dependency proxy。
  3. 使用知名的 Image heroku/buildpacks:18,運行 Container 執行真正的 build 動作。(buildpacks 又是另一個黑魔法。)
  4. 續上,build.sh 會將必要的參數與設定傳遞給 buildpacks,於是 buildpacks 會自動判斷你的 Application 是何種程式語言,要跑哪一些 build 動作,並將成果打包成 Image。
  5. 最後該 Image 會被 push 至步驟 1 登入的 GitLab Container Registry 供後續 CI Job 使用。

因此 /build/build.sh 其實就是一個內容有點多的 shell script,官方在裡面幫我們寫好許多判斷式並傳遞 Variabe 給 buildpacks,讓我們可以無腦的使用 Auto DevOps,任由 CI Job 自行完成所有應該在 Job build: 做的事情。

(Login Container Registry,然後開始執行 buildpacks。)

(有偵測出這是 ruby on rails。)

(build 成 image,然後 push 至 Container Registry。)

結語

這次的內容有點多,按慣例回顧我們學到哪些重點:

  • GitLab 官方有自行建立一個 Docker image - auto-build-image,它會幫我們代為運用知名的 buildpacks 完成 CI Job build:
  • 如果你不想要 Auto DevOps 的 Job build: 是可以自行替換的。(但本文沒有示範,以後再說嘍~)
  • GitLab CI Keyword - services:。如果你想要搭建以 Container 為主的 CI/CD Pipeline,這會是你絕對要認識的重要功能,它可以幫你以 Container 運行各種 Services,滿足 CI Job 的相依環境需求,例如自動化測試需要一個臨時的 DB,或操控 Docker daemon。
  • script: 中,我們可以直接使用 shell 的 if / else 做出邏輯判斷。
  • rules: 可以搭配 when: never,讓 CI Job 在特定條件下不會被執行。
  • 當 CI Job 會產生出需要傳遞給後續 Job 使用之 Variable 時,可以將它儲存至 .env 檔案,再利用 artifacts:reports:dotenv 儲存為 Environment variable,後續的 Job 即可直接使用。

以上就是本篇文章的所有內容,下一篇文章我們會繼續介紹下一個 Template - Jobs/Test.gitlab-ci.yml

參考資料

轉貼本文時禁止修改,禁止商業使用,並且必須註明來自「艦長,你有事嗎?」原創作者 Cheng Wei Chen,及附上原文連結。

工商服務

更多文章