---
title: "Rails 部署實踐 - 使用 GitLab CI 自動化建置"
date: 2022-06-03T00:00:00+08:00
publishDate: 2022-06-03T00:00:00Z
lastmod: 2023-09-03T17:41:34+08:00
tags: ["Rails","教學","部署","實作","Rails 部署實踐","GitLab"]
series: "rails-deployment-in-practice"
toc: true
permalink: "https://blog.aotoki.me/posts/2022/06/03/rails-deployment-in-practice-auto-build-with-gitlab-ci/"
language: "zh-tw"
---


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

<!--more-->

## GitLab CI 基礎概念{#gitlab-ci-concept}

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

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

### 基本結構{#basic-structure}

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

```yaml
# 預設使用的鏡像
image: ruby:2.7

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

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

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

## 自動建置 Rails 鏡像{#auto-build-rails-image}

要讓 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 任務。

```yaml
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/packs` 和 `public/assets` 兩個目錄的生成物，這樣就能將 Webpacker 以及 Rails 本身的靜態檔案預處理機制都涵蓋進來。

## 製作容器{#build-container}

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

```yaml
# ...
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_BRANCH` 跟 `CI_DEFAULT_BRANCH` 相同時，額外加上 `latest` 標籤，表示這是最新的版本。

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

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

---

如果想在第一時間收到更新，歡迎[訂閱弦而時習之](https://mailchi.mp/aotoki/rails-deployment-in-practice)在這系列文章更新時收到通知，如果有希望了解的知識，可以利用[Rails 部署實踐回饋表單](https://us4.list-manage.com/survey?u=dd3d68032c0510041f1302539&id=f25e0dc43e&attribution=false)告訴我。

