弦而時習之

Rails 部署實踐 - 容器化的 Bundler 最佳設定

雖然我們已經轉換為 Alpine 來製作容器鏡像,然而在現代軟體設計中為了加速會有許多快取(Cache)的檔案產生,以利再次執行時能夠更快速的啟動。使用 Bundler 安裝 Ruby Gem 也不例外,這些檔案在容器部署中有非常多是不需要的,因此我們還需要針對 Bundler 進行最佳化。

標準 Bundler 設定

在開始之前,我們可以先來看一下常見的 Bundler 安裝命令是如何的,然後再一步步的進行分析,將命令調整到適合我們的版本。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
FROM ruby:3-alpine

RUN apk add --no-cache build-base

RUN mkdir -p /src
COPY Gemfile Gemfile.lock /src

WORKDIR /src
RUN gem install bundler:2.3.3 \
    && bundle install -j "$(getconf _NPROCESSORS_ONLN)"

當你看到類似這樣的安裝命令,我們可以預期很可能是沒有部署經驗的新手工程師所撰寫的設定,在使用 Capistrano 部署時,會自動幫我們對 Bundler 進行設定,這些設定對於部署有著重要的關鍵。

一個比較常見(正確)的設定,會是下面的版本,也是符合 Capistrano 所設定的版本。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
FROM ruby:3-alpine

RUN apk add --no-cache build-base

RUN mkdir -p /src
COPY Gemfile Gemfile.lock /src

RUN gem install bundler:2.3.3 \
    && bundle config --local deployment 'true' \
    && bundle config --local frozen 'true' \
    && bundle config --local without 'development test' \
    && bundle install -j "$(getconf _NPROCESSORS_ONLN)"

這個版本先使用 Bundler 的 deployment 選項指定成部署模式,此時會先確定像是 Gemfile.lock 是否存在,來確保安裝的套件都是在開發環境或測試環境驗證過可以正常運行的版本。

同時加入了 frozen 選項,來拒絕變更。也就是我們無法透過修改 Gemfile 並且運行 bundler install 或者 bundler update 命令來修改 Gemfile.lock 這樣我們就可以確保不會被透過一些特殊的技巧修改 Gemfile 並且安裝有問題的 Gem 到部署的伺服器上。

最後會使用 without 選項,明確地將 development 以及 test 這兩個群組的 Gem 排除,因為在正式環境中我們不需要用於開發跟測試的套件,這類套件通常都會為了方便開發跟測試加入一些「後門(Backdoor)」而這類特殊功能就很容易被駭客利用,因此必須將其排除。

正常來說,做到這邊就已經非常完美,然而如同我們在使用 Alpine 製作容器鏡像 所說,將快取在鏡像中排除很多時候有助於我們縮小鏡像的大小,因此 Bundler 也許要額外加入這項設定來改善容器鏡像大小。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
FROM ruby:3-alpine

RUN apk add --no-cache build-base

RUN mkdir -p /src
COPY Gemfile Gemfile.lock /src

RUN gem install bundler:2.3.3 \
    && bundle config --local deployment 'true' \
    && bundle config --local frozen 'true' \
    && bundle config --local no-cache 'true' \
    && bundle config --local system 'true' \
    && bundle config --local without 'development test' \
    && bundle install -j "$(getconf _NPROCESSORS_ONLN)"

在這裏我個人偏好將 system 選項打開,把 Ruby Gem 安裝到系統目錄而非專案目錄,可以根據個人的偏好處理,這樣做的好處是在後續的優化中比較好進行管理。

清除無用檔案

網路上大多的教學(英文為主)到上一段落的處理基本上就結束,然而我們依舊能在 Bundler 安裝後的 Ruby Gem 裡面找到不少「無用」的檔案。

最後的 Bundler 安裝版本會是下面所看到的樣子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
FROM ruby:3-alpine

RUN apk add --no-cache build-base

RUN mkdir -p /src
COPY Gemfile Gemfile.lock /src

RUN gem install bundler:2.3.3 \
    && bundle config --local deployment 'true' \
    && bundle config --local frozen 'true' \
    && bundle config --local no-cache 'true' \
    && bundle config --local system 'true' \
    && bundle config --local without 'development test' \
    && bundle install -j "$(getconf _NPROCESSORS_ONLN)" \
    && find /src/vendor/bundle -type f -name '*.c' -delete \
    && find /src/vendor/bundle -type f -name '*.o' -delete \
    && find /usr/local/bundle -type f -name '*.c' -delete \
    && find /usr/local/bundle -type f -name '*.o' -delete \
    && rm -rf /usr/local/bundle/cache/*.gem

這裏段額外的命令,我們做了以下幾件事情

  1. 嘗試尋找專案目錄下的 .c.o 檔案並且刪除
  2. 嘗試尋找系統目錄下的 .c.o 檔案並且刪除
  3. 嘗試尋找系統目錄下的快取並且刪除

雖然我們已經有進行一切設定避免快取產生,然而多做一個檢查確實清理快取(或者基礎鏡像殘留)都能夠確保更乾淨,以及減少被攻擊的風險。

那麼,我們針對 .c.o 的清除又是怎麼一回事呢?這是因為 Ruby 語言是運行在虛擬機器上的,有一些功能需要借助一些 C 語言的套件輔助,或者直接跟作業系統互動,此時就會需要利用 C Extension(C 擴充)的機制來串連,然而當我們編譯為動態函式庫(Dynamic Library)之後,就不需要 C 語言的原始碼以及過程中產生的目的檔(.o

這些檔案看似不起眼,然而在 Rails 專案中相依的 Ruby Gem 裡面,這些檔案在安裝完畢後殘留下來的大小,約有 20 MB ~ 50 MB 左右,以我使用 Boxing 自動製作的鏡像來比較,原本約 100 MB 的鏡像可能會變成 150 MB 左右,這也是為什麼我要去清除的原因。

將這些多餘的檔案清理乾淨後,我們的容器鏡像基本上就達到了可以優化的極限,要再繼續減少大小就會花費非常多的時間並且不太符合成本效益,因此在下一階段我們要想辦法的是減少「編譯用套件」的容量消耗。


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

電子報

留言