/

一次 CI/CD 調教經驗

前言

最近受朋友委託,協助改善了一個現有的 CI/CD Pipeline 流程。對方的專案主要是一群熟悉軟體的設計的開發者所開發,但團隊中並沒有人對於維護 Pipeline 有相關經驗,於是我接下任務,一方面也想看一下在調教前與調教之後可以得到多大的效益提升。對於一個沒有開發該專案的人來說,最快了解專案整體的整合與部署流程的方式就是直接去看現有的 Pipeline。於是我打算從檢視 Pipeline 開始下手,看有哪些地方可以改良。

Runner 的選擇

對方當時為了快速將 Pipeline 搭建上線,所以在選擇 Runner 的時候選擇了一個最快速(但可能不安全)的 Runner,也就是 Exec Runner(對方採用的 CI 工具為 Drone CI )。然而因為移植性的關係,我們都會希望自己的 Pipeline 環境可以跑在容器之中,方便後續的遷移以及不耦合於特定作業系統。於是我將對方伺服器上的 Runner 重新架設了 Docker Runner,並且將原有的 .drone.yml 設定檔由 Exec Runner 形式轉換成 Docker Runner,結果馬上遇到了第一個問題

原本的 CI 整合測試的時間由 2 分鐘直接變成了 8 分鐘。

經過後續的排查才發現原來對方的 Exec Runner 在執行 Maven 打包時,因為是直接在作業系統上開啟 Process 來處理,所以每一次執行都是一樣的環境,造成後續進行打包的時候都是共用 /root/.m2/repository 裡面的套件,造成每一次 CI 其實都在快取之前用過的套件,並不是完全乾淨的環境。若使用 Docker Runner,也想要類似的快取行為,就只能透過共享特定 host 上的目錄來達成,在 DroneCI 中此類的設定也是非常好撰寫。

Docker in Docker

接下來這個問題幾乎是所有 CI/CD 平台 的 Docker Runner 都會遇到的問題。如何在 CI/CD Stage 中使用 Docker 指令來建構 Docker Image?這個問題的解法大致上可分為兩種(DinD 與 DooD)。然而最快的方法是直接將 /var/run/docker.sock 掛載於 Stage 的 Container 中,來達到可以在容器之中透過 UNIX Sock 與 host 上的 Docker Engine 溝通(當然還要有 docker cli)。實際上在執行 Pipeline 時,發現在對方專案中的 testcontainer 在進行整合測試時,嘗試去連線自己(Maven 中的 testcontainer)所建立出來 testcontainers/ryuk 容器,並且因為無法正常連線至該容器而導致錯誤。後來查看了系統環境才發現對方 Runner 的機器上使用 ufw 去管理防火牆

ufw 會將 iptables 中的 INPUT chain (filter table) Policy 改成 DROP

造成 Drone CI Stage 中的 Container 無法正常存取其他 Container,主要因為 Drone CI 因為安全原因所以將這些 Stage 開出來的 Container 都放在獨立的 Linux Bridge 之下,而不是預設的 docker0,這時候跨介面的通訊會因為 iptables 規則而被擋下(當時 ufw 為開啟狀態)。

latest tag 造成的隱含行為

將 Pipeline 整理得差不多時,發現在 CD 階段時的基礎設施都是用 docker-compose 執行,可是這些 Image 並沒有指定 Image tag(預設採用 latest)。於是使用了 docker images 指令發現這些 Image 都是在數個月之前被拉到本地端的,無法得知當時版本,也就沒辦法指定特定版本來維持專案的移植性。使用 Image 的 id 在 docker hub 上面搜尋可能是一個辦法,但是 docker hub 並沒有提供讓用戶直接用 Image id 來搜尋的方式。在網路上找了一些工具也沒有解決想知道這些 Image 到底是哪一個版本的問題。在最後使用 docker inspect 時無意間看到

ContainerConfig 欄位,其中的 Env 欄位中有應用程式的版本資訊

雖然沒有辦法完全知道 Image 的 tag,但是知道 Image 中應用程式的版本也可以方便使用這些版本來進行測試,測試沒問題後就將這些版本都固定,避免日後轉移時因為 latest 標籤而無法確定版本。

後記

這篇為這幾天最佳化 Pipeline 後的紀錄小總結,在將這些初步的最佳化完成之後。之後則會進行一些設計方向的最佳化,包含

  • 如何規劃快取才可以加速流程但不希望因此而造成測試不乾淨
  • 如何設計一個可以在 monorepo 的專案中,不用因為改了 A,就將全部的微服務(A, B, C)重新打包
  • 使否需要一個儲存敏感資訊的區域,讓多個 Runner 在部署時可以去拉取資訊(而不透過 host 共享檔案)