弦而時習之

Rails 部署實踐 - 使用 GitLab CI 自動化建置

自動化建置在軟體業界中已經被使用非常多年,然而許多工具並不一定容易入門,其中 GitLab CI 就屬於相對容易入門的類型,我們可以使用 YAML 格式去撰寫設定。雖然很好上手,但是在功能跟彈性上就比其他工具相對的缺少一些。

GitLab CI 基礎概念

一般來說,這類工具大多會有 Pipeline(管線)跟 Task(任務)兩個概念存在。我們可以將自動化建置想像為一個虛擬的工廠,因此我們會需要生產線(Pipeline)來產生軟體,而這條生產線中會有不同階段的處理,並且產生生成物(Artifacts)傳遞給下一個階段進行。

在 GitLab CI 的設計中,一個專案就是一條生產線,好處是在設定上的撰寫相對容易,相對的我們就不容易進行一些比較複雜的處理,像是不同平台搭配不同建置處理的情況。

基本結構

要撰寫 GitLab CI 非常容易,只需要在專案中新增 .gitlab-ci.yml 這個檔案,並且加入適當的定義即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 預設使用的鏡像
image: ruby:2.7

# 處理階段
stage:
  - dependency
  - precompile
  - containerize
  - publish

# 任務
rubygems:
  stage: dependency
  script:
    - ...
# ...

基本上除了像是 imagestage 這類關鍵字之外,剩下的都會被當作任務的定義,每個任務都需要指定階段(Stage)以及腳本(Script)來說明在什麼時候執行哪些指令,最簡單的使用還算容易上手。

自動建置 Rails 鏡像

要讓 GitLab CI 幫我們進行自動化的建置,我們需要將 Assets Precompile(素材預先編譯)的處理事先做完,轉換成生成物傳遞給容器化的任務封裝,最後再上傳到 GitLab Registry 來進行保存。

因為篇幅的關係,我們不會特別討論任務的優化、執行環境的設定等等比較細節的設定,文章中的範例使用的是 GitLab Runner 的 Docker 模式,因此需要 Docker in Docker 的設定支援。

Assets Precompile

要能夠進行 Rails 的 Assets Precompile 動作,我們至少需要將 Ruby 和 Node.js 的套件都安裝到環境中,因此我們的任務會需要安裝 Ruby Gem、Node Package 並且執行 Assets Precompile 任務。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
image: ruby:2.7

stage:
  - compile

variables:
  RAILS_MASTER_KEY: # 請在 GitLab CI 設定,避免 Commit 到專案中

assets:precompile:
  stage: compile
  variables:
    RAILS_ENV: production
  before_script:
    # Install Ruby Gems
    - gem install bundler -v 2.3.3
    - bundle config set path 'vendor'
    - bundle install
    # Install Node Packages
    - curl -SLO https://nodejs.org/dist/v16.13.0/node-v16.13.0-linux-x64.tar.xz
    - curl -o- -L https://yarnpkg.com/install.sh
    - export PATH=$HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:$PATH;
    - yarn install
  script:
    - bundle exec rails assets:precompile
  artifacts:
    paths:
      - public/packs
      - public/assets

上面這段設定,我們先使用 before_script 來設定「非主要任務」的部分,也就是準備的動作,將 Ruby Gem 跟 Node Package 安裝到環境中,因為一開始已經使用 image: ruby:2.7 指定使用 Ruby 2.7 做為基礎鏡像,就不需要像 Node.js 一樣額外的進行安裝。

除此之外,我們定義了 artfiacts 設定來表示這個任務完成後,會有 public/packspublic/assets 兩個目錄的生成物,這樣就能將 Webpacker 以及 Rails 本身的靜態檔案預處理機制都涵蓋進來。

製作容器

接下來我們要使用 GitLab CI 的 Docker in Docker 功能,在運行的鏡像中執行一個獨立的 Docker 環境,並透過這個環境來製作 Rails 的容器鏡像。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# ...
stage:
  - compile
  - build
# ...

variables:
  # ...
  IMAGE_NAME: "$CI_REGISTRY_IMAGE"
  IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"

docker:build:
  image: docker:stable
  stage: build
  services:
    - docker:20.10-dind
  before_script:
    # Add Docker Buildx Plugin
    - mkdir -p $HOME/.docker
    - mkdir -p /usr/libexec/docker/cli-plugins
    - wget https://github.com/docker/buildx/releases/download/v2.0.2/buildx-v2.0.2.linux-amd64
      -O /usr/libexec/docker/cli-plugins/docker-buildx
    - chmod +x /usr/libexec/docker/cli-plugins/docker-buildx
    # Login Registry
    - echo "$CI_JOB_TOKEN" | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
  script:
    # Setup Tag
    - export DOCKER_TAG_OPTIONS="--tag ${IMAGE_NAME}:${IMAGE_TAG}"
    - if [ $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH ]; then export DOCKER_TAG_OPTIONS="--tag ${IMAGE_NAME}:latest ${DOCKER_TAG_OPTIONS}"; fi
    # Build Docker Image and Push
    - docker buildx build --cache-from type=registry,ref=$IMAGE_NAME:latest --cache-to type=inline ${DOCKER_TAG_OPTIONS} --push .
  needs:
    - job: assets:precompile
      artifacts: true

雖然看起來很複雜,不過大多是在設置一些用來建置的參數。在這個設定中,我們先改用 Docker 專用的 docker:stable 基礎鏡像,讓我們可以使用 docker 命令。

接下來我們使用 needs 選項,告知需要 assets:precompile 任務完成後並且存在生成物,才能滿足這個任務的執行條件,可以開始執行。

before_script 的階段,我額外加入了 Buildx 這個外掛的安裝步驟,使用這個外掛可以用比較簡單的命令完成建置的任務,目前還不確定未來是否會被加入官方的鏡像中,但是在 Docker 官方的GitHub Actions 中已經使用這個方式建置。

外掛安裝完畢後,我們會需要使用 docker login 進行登入,在 GitLab CI 中會自動產生好帳號密碼,我們只需要使用這組臨時的帳號密碼登入即可正常的上傳、下載 GitLab Registry 中對應專案的鏡像。

在建置部分,我們可以永遠的都製作 latest 的標籤推上去,然而這樣在發生問題時想要 Rollback(回滾)的時候並不容易,取而代之的是使用 Git SHA 來當作標籤,這樣就可以指定任意的版本來進行切換。

為了實現這件事情,我們會在這裡使用 Shell Script 來製作 DOCKER_TAG_OPTIONS 的環境變數,並且讓 CI_COMMIT_BRANCHCI_DEFAULT_BRANCH 相同時,額外加上 latest 標籤,表示這是最新的版本。

最後使用 docker buildx 命令建置,可以看到比原本的 docker build 加上 docker push 的步驟單純很多,可以一次地將所有動作完成。

有了這些處理後,我們就可以在自己的 GitLab 專案自動化的製作 Rails 鏡像用於部署。


如果想在第一時間收到更新,歡迎訂閱弦而時習之在這系列文章更新時收到通知,如果有希望了解的知識,可以利用Rails 部署實踐回饋表單告訴我。

電子報

留言