現代軟體開發流程中,持續整合與持續佈署(Continuous Integration/Continuous Deployment,CI/CD)已成為提升開發效率與軟體品質的核心機制。隨著應用程式架構日益複雜,單純的程式碼建置與測試已無法滿足企業需求,我們需要將 CI/CD Pipeline 的範疇延伸至更廣泛的自動化場景。本文將深入探討如何透過 Docker 容器化技術整合第三方工具、建立自動化容器構建與安全掃描流程、運用特性旗標機制控制功能發布,並特別針對 iOS 與 Android 行動應用程式,說明如何利用 Fastlane 與 GitLab CI/CD 建構完整的自動化建置、測試與佈署流程。

CI/CD Pipeline 範疇延伸策略

企業級 CI/CD Pipeline 的價值不僅在於自動化程式碼的建置與測試,更在於整合整個軟體交付生命週期的各個環節。透過系統性地延伸 Pipeline 的應用範疇,開發團隊能夠實現從程式碼提交到生產環境佈署的全自動化流程,同時確保每個階段都符合品質與安全標準。這種延伸不僅提升了開發效率,更重要的是建立了可重複、可追溯的軟體交付機制,讓團隊能夠更專注於業務價值的實現,而非被繁瑣的手動作業所困擾。

效能與負載測試整合實踐

應用程式的效能表現直接影響使用者體驗與系統穩定性,因此將效能測試納入 CI/CD Pipeline 是確保產品品質的重要環節。透過自動化的負載測試,開發團隊能夠在每次程式碼變更後即時掌握系統效能表現,及早發現潛在的效能瓶頸。k6 作為一個現代化的負載測試工具,提供了直觀的 JavaScript API 與豐富的效能指標,非常適合整合至 CI/CD 流程中。在實務應用上,我們需要根據應用程式的特性設定合理的測試場景與效能閾值,確保測試結果能夠真實反映系統在高負載情境下的表現。

// k6 負載測試腳本範例
import http from 'k6/http';
import { check, sleep } from 'k6';

// 測試組態設定
export let options = {
    // 設定虛擬使用者數量
    vus: 10,
    // 測試持續時間設為 5 分鐘
    duration: '5m',
    // 效能閾值定義:99% 的請求必須在 1500ms 內完成
    thresholds: {
        'http_req_duration': ['p(99)<1500']
    },
};

// 主要測試函式
export default function () {
    // 發送 GET 請求並解析 JSON 回應
    // 建議使用環境變數來設定目標 URL
    const myResponse = http.get(process.env.TARGET_URL || 'https://api.example.com').json();
    
    // 驗證回應內容是否符合預期
    // 確認回應資料長度大於 0,代表成功取得資料
    check(myResponse, { 
        'retrieved url': (obj) => obj.length > 0 
    });
    
    // 每次請求間隔 1 秒,模擬真實使用者行為
    sleep(1);
}

這段測試腳本展示了如何建構一個基本的負載測試場景。我們設定了十個並行的虛擬使用者,在五分鐘的測試期間持續對目標 API 發送請求。效能閾值的設定非常關鍵,在這個範例中,我們要求百分之九十九的請求必須在一點五秒內完成,這個標準可以根據實際的服務等級協議(Service Level Agreement,SLA)需求進行調整。測試過程中會自動驗證每個回應的有效性,並透過適當的間隔時間來模擬真實使用者的操作模式。

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi 300
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 14
skinparam minClassWidth 120

actor 開發者 as dev
participant "GitLab CI" as ci
participant "k6 Runner" as k6
participant "目標應用程式" as app
database "測試報告" as report

dev -> ci: 推送程式碼變更
activate ci

ci -> k6: 啟動負載測試
activate k6

k6 -> app: 發送並行請求\n(10 個虛擬使用者)
activate app

loop 測試持續 5 分鐘
    k6 -> app: HTTP GET 請求
    app --> k6: JSON 回應
    k6 -> k6: 檢查回應有效性\n驗證效能閾值
end

app --> k6: 測試完成
deactivate app

k6 -> report: 產生效能報告\n統計分析結果
activate report

report --> k6: 儲存完成
deactivate report

k6 --> ci: 回傳測試結果\n(通過/失敗)
deactivate k6

ci --> dev: 通知測試狀態\n提供報告連結
deactivate ci

@enduml

特性旗標與漸進式功能發布

特性旗標(Feature Flags)機制為軟體發布提供了極大的靈活性,讓開發團隊能夠在不重新佈署應用程式的前提下,動態控制新功能的開放範圍。這種機制在大型企業環境中特別重要,因為它允許團隊採用漸進式的發布策略,先對少部分使用者開放新功能進行測試,確認穩定性後再逐步擴大開放範圍。GitLab 內建的特性旗標功能基於 Unleash 框架,提供了完整的管理介面與 API,讓開發團隊能夠輕鬆實現複雜的功能發布策略。

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi 300
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 14
skinparam minClassWidth 120

start

:產品經理規劃新功能發布策略;

:開啟 GitLab 專案;

:導航至佈署 > 特性旗標;

:建立新特性旗標;

note right
  定義旗標名稱與描述
  設定預設狀態(開啟/關閉)
end note

:組態發布策略;

partition 策略類型 {
    :環境策略;
    note left
      針對特定環境
      (開發/測試/生產)
    end note
    
    :使用者列表策略;
    note left
      指定特定使用者群組
      進行功能測試
    end note
    
    :百分比策略;
    note left
      逐步開放功能
      控制影響範圍
    end note
}

:應用程式整合旗標檢查邏輯;

if (旗標啟用?) then (是)
    :執行新功能程式碼路徑;
else (否)
    :執行原有功能程式碼路徑;
endif

:監控功能表現與使用者回饋;

if (功能穩定?) then (是)
    :擴大開放範圍;
    :最終完全啟用功能;
else (否)
    :回收功能或調整策略;
endif

stop

@enduml

在 GitLab 介面中建立特性旗標的流程相當直觀。首先進入專案的佈署選單,選擇特性旗標功能,然後點選建立新旗標。在建立過程中,我們需要為旗標指定一個有意義的名稱與清楚的描述,這有助於團隊成員理解該旗標的用途。接下來是策略組態階段,這是特性旗標機制最靈活的部分。我們可以根據不同的環境設定不同的策略,例如在開發環境中預設啟用新功能方便測試,而在生產環境中則採用更謹慎的漸進式開放策略。

使用者列表策略允許我們指定特定的使用者群組作為新功能的早期測試者,這在企業內部測試場景中非常實用。百分比策略則提供了更精細的控制能力,我們可以設定只對百分之五的使用者開放新功能,觀察系統表現與使用者反應後,再逐步提升開放比例至百分之二十、百分之五十,最終達到百分之百的全面開放。

應用程式整合特性旗標實作

將特性旗標整合至應用程式需要使用對應的客戶端函式庫,以下展示如何在 Ruby 應用程式中實作旗標檢查機制。這個實作方式同樣適用於其他程式語言,只需使用對應的 Unleash 客戶端函式庫即可。

# 引用 Unleash 客戶端函式庫
require 'unleash'
require 'unleash/context'

# 初始化 Unleash 客戶端
# 連線至 GitLab 的特性旗標 API 端點
unleash = Unleash::Client.new({
    # GitLab 特性旗標 API URL
    # 格式:http://gitlab.com/api/v4/feature_flags/unleash/{PROJECT_ID}
    url: 'http://gitlab.com/api/v4/feature_flags/unleash/42',
    # 應用程式名稱,通常對應環境名稱
    app_name: 'production',
    # 實例識別碼,用於追蹤與統計
    instance_id: '29QmjsW6KngPR5JNPMWx'
})

# 建立使用者上下文
# 用於支援基於使用者的策略判斷
unleash_context = Unleash::Context.new

# 設定使用者識別碼
# 這個識別碼會用於使用者列表策略的比對
unleash_context.user_id = "123"

# 可選:設定更多上下文資訊
# unleash_context.session_id = "session_abc123"
# unleash_context.remote_address = "192.168.1.100"
# unleash_context.properties = { "userTier" => "premium" }

# 檢查特定功能是否啟用
if unleash.is_enabled?("payment_gateway_v2", unleash_context)
    # 旗標啟用時執行新功能邏輯
    puts "使用新版金流閘道處理付款"
    process_payment_with_v2_gateway()
else
    # 旗標未啟用時執行原有功能邏輯
    puts "使用原有金流閘道處理付款"
    process_payment_with_legacy_gateway()
end

# 進階用法:取得旗標的變體(Variant)
# 支援 A/B 測試等更複雜的場景
variant = unleash.get_variant("checkout_page_layout", unleash_context)
case variant.name
when "variant_a"
    render_checkout_layout_a()
when "variant_b"
    render_checkout_layout_b()
else
    render_default_checkout_layout()
end

這段程式碼展示了特性旗標的完整整合流程。首先建立 Unleash 客戶端實例,連線至 GitLab 提供的特性旗標 API。在初始化時需要提供專案識別碼、應用程式名稱以及實例識別碼,這些參數可以從 GitLab 的特性旗標設定頁面取得。接著建立使用者上下文物件,這個上下文會包含當前使用者的相關資訊,用於支援更精細的策略判斷。

在實際應用中,我們透過 is_enabled? 方法檢查特定功能是否啟用,並根據結果執行不同的程式碼路徑。這種機制的優勢在於我們可以在不修改程式碼的情況下,透過 GitLab 介面即時調整功能的開放狀態。更進階的應用場景中,我們還可以使用變體(Variant)功能來實現 A/B 測試,為不同的使用者群組提供不同版本的功能實作。

第三方工具容器化整合策略

在 CI/CD Pipeline 中整合第三方工具時,容器化是最佳的整合方式。透過將工具封裝成 Docker 容器,我們能夠確保工具在不同環境中的一致性執行,同時簡化了 Pipeline 的組態管理。這種方式特別適合整合安全掃描工具、程式碼品質分析工具,或是自訂的企業內部工具。

# 使用輕量級的 Alpine Linux 作為基礎映像
# Alpine 3.13.0 提供了良好的安全性與最小的映像大小
FROM alpine:3.13.0

# 更新套件索引並建立工具目錄
# apk update 確保我們使用最新的套件資訊
RUN apk update && \
    apk add --no-cache \
        # 安裝基本工具套件
        curl \
        ca-certificates \
        && \
    mkdir -p /opt/myTool && \
    # 清理 apk 快取以減少映像大小
    rm -rf /var/cache/apk/*

# 將當前目錄的所有檔案複製到容器中
# 這包括工具的執行檔、組態檔與相依函式庫
ADD . /opt/myTool

# 設定檔案權限
# 確保工具目錄可被存取,執行檔具有執行權限
RUN chmod 755 -R /opt/myTool && \
    chmod +x /opt/myTool/mybinary

# 切換至非特權使用者執行
# UID 1001 是常用的非 root 使用者識別碼
# 這符合容器安全最佳實踐,避免使用 root 權限
USER 1001

# 設定工作目錄
WORKDIR /opt/myTool

# 設定環境變數
# 讓工具能夠正確載入相依函式庫
ENV PATH="/opt/myTool:${PATH}"
ENV TOOL_HOME="/opt/myTool"

# 定義預設命令
# 提醒使用者這個容器應該在 Pipeline 中使用
CMD ["echo", "此容器專為 CI/CD Pipeline 設計,不應直接執行。請在 Pipeline 中指定正確的執行命令。"]

這個 Dockerfile 展示了如何建立一個生產環境可用的工具容器。我們選擇 Alpine Linux 作為基礎映像,因為它提供了極小的映像大小與良好的安全性。在建置過程中,我們安裝了必要的系統工具,建立了專用的工具目錄,並將所有相關檔案複製進容器。檔案權限的設定非常重要,我們確保工具目錄可被存取,同時將主要執行檔設為可執行。

安全性考量方面,我們特別指定使用非特權使用者(UID 1001)來執行容器,而非預設的 root 使用者。這是容器安全的重要實踐,能夠有效降低潛在的安全風險。環境變數的設定則確保工具能夠正確找到所需的相依函式庫與組態檔案。

自動化容器建置與安全掃描機制

建立完整的容器化 CI/CD 流程需要涵蓋從映像建置、安全掃描到儲存庫推送的完整環節。透過自動化這些流程,我們能夠確保每個容器映像都經過嚴格的安全檢查,並且能夠快速地佈署到各個環境中。GitLab CI/CD 提供了強大的容器建置能力,配合 Docker-in-Docker(DinD)技術,我們可以在 Pipeline 中完成所有容器操作。

GitLab CI/CD 容器建置組態

在 GitLab CI/CD 中建置 Docker 容器需要適當的環境組態與權限設定。以下展示一個完整的容器建置工作組態,包含了安全性考量與最佳實踐。

# 容器建置階段組態
Container_Build:
  # 定義工作所屬的階段
  stage: build
  
  # 指定使用的 Docker 映像版本
  # 使用固定版本號確保建置環境的一致性
  image: docker:20.10.16
  
  # 啟用 Docker-in-Docker 服務
  # 這讓我們能在容器中建置容器
  services:
    - docker:20.10.16-dind
  
  # 環境變數組態
  variables:
    # Docker 主機位址
    # 指向 DinD 服務的 TLS 端口
    DOCKER_HOST: tcp://docker:2376
    
    # Docker TLS 憑證目錄
    # DinD 會自動產生並掛載憑證
    DOCKER_TLS_CERTDIR: "/certs"
    
    # 啟用 TLS 驗證但不驗證憑證
    # 適用於 CI/CD 環境的內部通訊
    DOCKER_TLS_VERIFY: 1
    
    # 映像標籤策略
    # 使用 commit SHA 作為標籤確保可追溯性
    IMAGE_TAG: $CI_COMMIT_SHORT_SHA
  
  # 前置腳本
  # 在主要建置腳本執行前進行環境準備
  before_script:
    # 等待 Docker daemon 完全啟動
    - until docker info; do sleep 1; done
    
    # 登入 GitLab Container Registry
    # 使用 CI/CD 內建變數進行認證
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
  
  # 主要建置腳本
  script:
    # 建置 Docker 映像
    # 使用 commit SHA 與 latest 雙標籤策略
    - docker build 
        --pull  # 確保使用最新的基礎映像
        --cache-from $CI_REGISTRY_IMAGE:latest  # 使用快取加速建置
        --tag $CI_REGISTRY_IMAGE:$IMAGE_TAG  # 使用 commit SHA 標籤
        --tag $CI_REGISTRY_IMAGE:latest  # 更新 latest 標籤
        --file Dockerfile  # 指定 Dockerfile 路徑
        .  # 建置上下文為當前目錄
    
    # 推送映像至 Container Registry
    # 推送兩個標籤以支援不同的使用場景
    - docker push $CI_REGISTRY_IMAGE:$IMAGE_TAG
    - docker push $CI_REGISTRY_IMAGE:latest
    
    # 清理本地映像以節省空間
    - docker rmi $CI_REGISTRY_IMAGE:$IMAGE_TAG
    - docker rmi $CI_REGISTRY_IMAGE:latest
  
  # 工作產出物設定
  # 儲存建置資訊供後續階段使用
  artifacts:
    reports:
      # 產生建置報告
      dotenv: build.env
    paths:
      - build.env
    expire_in: 1 week
  
  # 定義工作執行規則
  rules:
    # 只在 main 分支執行
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: always
    # 在合併請求中執行
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      when: always
    # 手動觸發時執行
    - if: '$CI_PIPELINE_SOURCE == "web"'
      when: manual
  
  # 標籤指定
  # 確保在有 Docker 支援的 Runner 上執行
  tags:
    - docker

這個組態展示了企業級容器建置流程的完整設定。我們使用固定版本的 Docker 映像來確保建置環境的一致性,避免因版本差異導致的建置失敗。Docker-in-Docker 服務的啟用讓我們能夠在 GitLab Runner 容器中執行 Docker 命令,這是實現容器化 CI/CD 的關鍵技術。

環境變數的組態特別重要,DOCKER_HOST 變數指向 DinD 服務的位址,而 TLS 相關設定則確保了通訊的安全性。映像標籤策略採用雙標籤設計,一個使用 commit SHA 確保每個建置版本都可以追溯,另一個使用 latest 標籤方便快速存取最新版本。

前置腳本中的 Docker daemon 就緒檢查非常重要,因為 DinD 服務需要一些時間來完全啟動。我們使用迴圈持續檢查直到 Docker daemon 可用為止。建置腳本中使用了多個最佳實踐,包括 --pull 參數確保使用最新的基礎映像,--cache-from 參數利用快取加速建置過程。

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi 300
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 14
skinparam minClassWidth 140

participant "開發者" as dev
participant "GitLab" as gitlab
participant "GitLab Runner" as runner
participant "Docker Daemon" as docker
participant "Container Registry" as registry
database "映像掃描系統" as scanner

dev -> gitlab: 推送程式碼至 main 分支
activate gitlab

gitlab -> runner: 觸發 CI/CD Pipeline
activate runner

runner -> runner: 啟動 Docker-in-Docker 服務
runner -> docker: 等待 Docker Daemon 就緒
activate docker

docker --> runner: Daemon 已啟動

runner -> registry: 認證至 Container Registry
activate registry
registry --> runner: 認證成功
deactivate registry

runner -> docker: 建置 Docker 映像\n標籤: commit-sha & latest
docker -> docker: 下載基礎映像\n執行 Dockerfile 指令
docker --> runner: 映像建置完成

runner -> registry: 推送映像 (commit-sha)
activate registry
registry --> runner: 推送成功
deactivate registry

runner -> registry: 推送映像 (latest)
activate registry
registry --> runner: 推送成功
deactivate registry

runner -> scanner: 觸發容器安全掃描
activate scanner

scanner -> registry: 拉取待掃描映像
activate registry
registry --> scanner: 映像下載完成
deactivate registry

scanner -> scanner: 執行安全掃描\n檢查已知漏洞
scanner -> scanner: 產生掃描報告

scanner --> runner: 回傳掃描結果
deactivate scanner

runner -> docker: 清理本地映像
docker --> runner: 清理完成
deactivate docker

runner --> gitlab: Pipeline 執行完成\n回報建置與掃描結果
deactivate runner

gitlab --> dev: 發送通知\n提供報告連結
deactivate gitlab

@enduml

容器安全掃描整合

容器安全掃描是確保應用程式安全的重要環節。GitLab 提供了內建的容器掃描功能,能夠自動檢測映像中的已知漏洞與安全風險。整合這項功能非常簡單,只需要在 CI/CD 組態中引入對應的範本即可。

# 引入 GitLab 提供的容器掃描範本
include:
  - template: Jobs/Container-Scanning.gitlab-ci.yml

# 可選:自訂容器掃描組態
container_scanning:
  # 指定要掃描的映像
  variables:
    CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
    
    # 設定掃描嚴重程度閾值
    # 發現高危或嚴重漏洞時將工作標記為失敗
    CS_SEVERITY_THRESHOLD: "high"
    
    # 掃描逾時設定(秒)
    CS_DOCKER_INSECURE: "false"
  
  # 掃描後處理
  after_script:
    # 產生人類可讀的掃描報告
    - |
      if [ -f gl-container-scanning-report.json ]; then
        echo "容器掃描完成,發現以下問題:"
        cat gl-container-scanning-report.json | jq -r '.vulnerabilities[] | "[\(.severity)] \(.name): \(.description)"'
      fi
  
  # 允許掃描失敗但仍繼續 Pipeline
  # 在引入階段可以先設為 true,逐步修復後改為 false
  allow_failure: false
  
  # 只在建置完成後執行掃描
  needs:
    - job: Container_Build
      artifacts: true

容器掃描範本提供了完整的掃描流程,包含映像拉取、漏洞資料庫更新、掃描執行以及報告產生。透過變數組態,我們可以調整掃描行為以符合企業的安全政策。嚴重程度閾值的設定讓我們能夠根據風險等級來決定是否中斷 Pipeline,這在不同的環境中可以採用不同的策略。

掃描報告會以 JSON 格式產生,我們可以在後處理腳本中解析這些報告,產生更易讀的格式或是整合至其他安全管理系統。在實務應用中,建議先將 allow_failure 設為 true,讓團隊有時間修復既有的安全問題,然後再將其設為 false 以強制執行安全標準。

CI/CD Pipeline 中呼叫容器化工具

當我們完成了工具的容器化與映像建置後,就可以在 Pipeline 的其他階段中使用這些工具。以下展示如何在測試階段呼叫容器化的第三方工具。

# 使用容器化工具進行測試
Security_Scan:
  # 定義為測試階段的工作
  stage: test
  
  # 指定使用我們建置的工具容器
  # 可以使用固定標籤或 latest
  image: $CI_REGISTRY_IMAGE/security-tool:latest
  
  # 環境變數組態
  variables:
    # 工具執行參數
    SCAN_TARGET: "src/"
    SCAN_FORMAT: "json"
    SCAN_OUTPUT: "security-report.json"
  
  # 執行安全掃描
  script:
    # 執行容器中的掃描工具
    # 所有工具檔案都在 /opt/myTool 目錄下
    - /opt/myTool/security-scanner 
        --target "$SCAN_TARGET"
        --format "$SCAN_FORMAT"
        --output "$SCAN_OUTPUT"
        --severity high,critical
    
    # 檢查掃描結果
    - |
      if [ -f "$SCAN_OUTPUT" ]; then
        echo "安全掃描完成,分析結果..."
        # 使用 jq 解析 JSON 報告
        CRITICAL_COUNT=$(jq '.summary.critical' "$SCAN_OUTPUT")
        HIGH_COUNT=$(jq '.summary.high' "$SCAN_OUTPUT")
        
        echo "發現 $CRITICAL_COUNT 個嚴重問題"
        echo "發現 $HIGH_COUNT 個高風險問題"
        
        # 如果發現嚴重問題則失敗
        if [ "$CRITICAL_COUNT" -gt 0 ]; then
          echo "錯誤:發現嚴重安全問題,請立即修復"
          exit 1
        fi
      fi
  
  # 保存掃描報告
  artifacts:
    name: "security-report-$CI_COMMIT_SHORT_SHA"
    paths:
      - security-report.json
    reports:
      # 將報告整合至 GitLab 安全儀表板
      sast: security-report.json
    expire_in: 30 days
    when: always
  
  # 執行條件
  rules:
    # 在合併請求中總是執行
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      when: always
    # 在 main 分支執行
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: always
  
  # 依賴關係
  needs:
    - job: Container_Build
      artifacts: false

這個工作組態展示了如何有效地使用容器化工具。我們直接將工具容器指定為工作的執行環境,這樣所有工具相依套件都已經預先安裝好,不需要在每次執行時重新安裝。腳本部分展示了如何呼叫工具並處理其輸出結果,包含了錯誤處理與條件判斷邏輯。

產出物的組態讓我們能夠保存掃描報告供後續分析,同時整合至 GitLab 的安全儀表板,讓團隊能夠集中檢視所有安全問題。執行條件的設定確保在關鍵的程式碼變更點都會執行安全掃描,提供持續的安全保障。

行動應用程式自動化建置流程

行動應用程式的開發與佈署流程通常比網頁應用程式更為複雜,涉及不同的開發工具、建置流程與發布管道。透過 Fastlane 與 GitLab CI/CD 的整合,我們能夠建立完整的自動化流程,涵蓋從程式碼提交到應用程式商店發布的所有環節。這種自動化不僅大幅減少了手動作業的時間,更重要的是確保了每個發布版本都經過相同的建置與測試流程,提升了應用程式的品質與可靠性。

環境準備與基礎設施需求

建立行動應用程式的 CI/CD 環境需要特定的硬體與軟體組態。對於 iOS 應用程式的建置,我們必須使用 macOS 環境,因為 Apple 的開發工具鏈(Xcode)只能在 macOS 上執行。Android 應用程式雖然可以在多種作業系統上建置,但為了統一管理,通常也會在相同的 macOS 環境中處理。

硬體需求方面,建議使用搭載 Apple Silicon 或 Intel 處理器的 Mac mini 或 MacBook,記憶體至少需要十六 GB 以確保建置過程的流暢性。儲存空間建議至少五百 GB,因為 Xcode、模擬器映像以及建置產出物都會佔用大量空間。對於大型團隊,可以考慮使用 Mac Pro 或設置多台 Mac mini 來提供更高的並行建置能力。

軟體環境的設定包含幾個關鍵元件。首先需要安裝最新版本的 Xcode,這是 iOS 開發的核心工具。接著安裝 Fastlane,這個工具將負責自動化所有的建置、測試與發布流程。GitLab Runner 需要在 macOS 上以服務模式執行,並組態為能夠執行 shell 類型的工作。此外,還需要設定 Apple 開發者帳號與 Google Play Console 帳號,並準備好所有必要的簽章憑證與佈建描述檔。

憑證管理是行動應用程式 CI/CD 中最具挑戰性的部分之一。iOS 應用程式需要開發憑證、發布憑證以及對應的佈建描述檔,這些檔案通常會過期且需要定期更新。建議使用 Fastlane Match 來集中管理這些憑證,它會將憑證加密後儲存在 Git 儲存庫中,讓團隊成員與 CI/CD 系統都能安全地存取。

Fastlane 組態與工作流程設計

Fastlane 透過 Fastfile 來定義自動化工作流程,這個檔案使用 Ruby 語法撰寫,提供了豐富的內建動作與外掛支援。以下展示一個完整的 Fastfile 組態,涵蓋了 iOS 與 Android 應用程式的建置、測試與佈署。

# Fastfile - 行動應用程式自動化建置組態
# 此檔案定義了 iOS 與 Android 的建置、測試與佈署流程

# 預設執行平台
default_platform(:ios)

# ===== iOS 平台組態 =====
platform :ios do
  
  # 應用程式基本資訊
  desc "iOS 應用程式組態"
  
  # 測試工作流程
  # 執行單元測試與 UI 測試
  lane :test do
    # 執行測試
    # devices: 指定測試的裝置型號
    # scheme: Xcode 專案中的測試方案
    # clean: 在測試前清理建置產出物
    run_tests(
      devices: ["iPhone 14", "iPhone 14 Pro", "iPad Air"],
      scheme: "MyAppTests",
      clean: true,
      # 產生程式碼覆蓋率報告
      code_coverage: true,
      # 測試結果輸出格式
      output_types: "html,junit",
      output_directory: "./test-results/ios"
    )
    
    # 上傳測試結果至 GitLab
    sh("cp -r ./test-results/ios/* $CI_PROJECT_DIR/test-results/")
  end
  
  # Beta 測試版建置
  # 建置並上傳至 TestFlight
  lane :beta do
    # 確保使用最新的程式碼
    ensure_git_status_clean
    
    # 遞增建置編號
    # 使用 CI 的建置編號確保唯一性
    increment_build_number(
      build_number: ENV['CI_PIPELINE_IID']
    )
    
    # 同步簽章憑證
    # type: 指定憑證類型(開發/發布)
    # readonly: 只讀取不修改憑證
    sync_code_signing(
      type: "appstore",
      readonly: true,
      # 使用 Match 管理憑證
      git_url: ENV['MATCH_GIT_URL'],
      app_identifier: "com.example.myapp"
    )
    
    # 建置應用程式
    # scheme: Xcode 方案名稱
    # export_method: 匯出方法(app-store/ad-hoc/development)
    build_app(
      scheme: "MyApp",
      export_method: "app-store",
      # 輸出路徑
      output_directory: "./build/ios",
      output_name: "MyApp.ipa",
      # 建置組態
      configuration: "Release",
      # 清理建置快取
      clean: true,
      # 匯出選項
      export_options: {
        provisioningProfiles: {
          "com.example.myapp" => "match AppStore com.example.myapp"
        }
      }
    )
    
    # 上傳至 TestFlight
    upload_to_testflight(
      # 跳過等待建置處理
      skip_waiting_for_build_processing: true,
      # TestFlight 測試群組
      distribute_external: true,
      groups: ["Beta Testers"],
      # 更新記錄
      changelog: ENV['CI_COMMIT_MESSAGE']
    )
    
    # 發送通知
    slack(
      message: "iOS Beta 版本已上傳至 TestFlight",
      channel: "#mobile-releases",
      success: true
    )
  end
  
  # 正式版發布
  # 建置並提交至 App Store 審查
  lane :release do
    # 確保在主分支上
    ensure_git_branch(branch: 'main')
    
    # 同步發布憑證
    sync_code_signing(
      type: "appstore",
      readonly: true
    )
    
    # 建置應用程式
    build_app(
      scheme: "MyApp",
      export_method: "app-store",
      configuration: "Release"
    )
    
    # 擷取螢幕截圖
    # 用於 App Store 頁面
    capture_screenshots(
      scheme: "MyAppUITests"
    )
    
    # 上傳至 App Store Connect
    # 自動提交審查
    upload_to_app_store(
      submit_for_review: true,
      # 審查資訊
      submission_information: {
        add_id_info_uses_idfa: false
      },
      # 自動釋出
      automatic_release: true,
      # 分階段釋出
      phased_release: true
    )
    
    # 建立 Git 標籤
    add_git_tag(
      tag: "ios/v#{get_version_number}/#{get_build_number}"
    )
    
    # 推送標籤
    push_to_git_remote
  end
  
end

# ===== Android 平台組態 =====
platform :android do
  
  desc "Android 應用程式組態"
  
  # 測試工作流程
  lane :test do
    # 執行單元測試
    gradle(
      task: "test",
      build_type: "Release",
      # 產生測試報告
      properties: {
        "testCoverageEnabled" => "true"
      }
    )
    
    # 複製測試結果
    sh("cp -r ../app/build/reports/tests/* $CI_PROJECT_DIR/test-results/android/")
  end
  
  # Beta 測試版建置
  # 建置並上傳至 Google Play Internal Testing
  lane :beta do
    # 遞增版本代碼
    increment_version_code(
      version_code: ENV['CI_PIPELINE_IID'].to_i
    )
    
    # 建置 Release APK/AAB
    gradle(
      task: "bundle",
      build_type: "Release",
      # 輸出 AAB 格式(Google Play 要求)
      properties: {
        "android.injected.signing.store.file" => ENV['ANDROID_KEYSTORE_FILE'],
        "android.injected.signing.store.password" => ENV['ANDROID_KEYSTORE_PASSWORD'],
        "android.injected.signing.key.alias" => ENV['ANDROID_KEY_ALIAS'],
        "android.injected.signing.key.password" => ENV['ANDROID_KEY_PASSWORD']
      }
    )
    
    # 上傳至 Google Play Console
    # track: 發布軌道(internal/alpha/beta/production)
    upload_to_play_store(
      track: "internal",
      # AAB 檔案路徑
      aab: "app/build/outputs/bundle/release/app-release.aab",
      # 跳過上傳 APK
      skip_upload_apk: true,
      # 上傳截圖與描述
      skip_upload_metadata: false,
      skip_upload_images: false,
      skip_upload_screenshots: false
    )
    
    # 發送通知
    slack(
      message: "Android Beta 版本已上傳至 Play Console",
      channel: "#mobile-releases"
    )
  end
  
  # 正式版發布
  lane :release do
    # 建置 Release 版本
    gradle(
      task: "bundle",
      build_type: "Release"
    )
    
    # 上傳至 Production 軌道
    upload_to_play_store(
      track: "production",
      aab: "app/build/outputs/bundle/release/app-release.aab",
      # 分階段釋出,從 10% 開始
      rollout: "0.1"
    )
    
    # 建立 Git 標籤
    add_git_tag(
      tag: "android/v#{get_version_name}/#{get_version_code}"
    )
    
    push_to_git_remote
  end
  
end

這個 Fastfile 組態展示了完整的行動應用程式自動化流程。每個 lane(工作流程)都代表一個特定的任務,從測試到建置再到發布。iOS 部分使用了 Fastlane 的內建動作來處理憑證同步、應用程式建置與 TestFlight 上傳。Android 部分則透過 Gradle 來執行建置任務,並使用 Google Play Console API 來上傳應用程式套件。

憑證管理使用了 Match 機制,這是 Fastlane 推薦的憑證管理方式。它會將所有憑證加密後儲存在 Git 儲存庫中,CI/CD 系統只需要提供解密密碼即可使用這些憑證。這種方式大幅簡化了憑證的管理與分享,特別是在團隊環境中。

版本號管理方面,我們使用 CI/CD 系統的建置編號來確保每個建置都有唯一的版本代碼。這避免了版本號衝突的問題,並且讓版本號與 CI/CD Pipeline 的執行記錄直接對應,方便問題追溯。

GitLab CI/CD 行動應用建置組態

在 GitLab CI/CD 中整合 Fastlane 需要適當的 Runner 組態與環境變數設定。以下展示一個完整的 CI/CD 組態,涵蓋測試、建置與發布階段。

# 定義 Pipeline 階段
stages:
  - test
  - build
  - deploy

# 全域變數設定
variables:
  # Fastlane 環境變數
  FASTLANE_SKIP_UPDATE_CHECK: "true"
  FASTLANE_HIDE_TIMESTAMP: "true"
  
  # iOS 組態
  MATCH_GIT_URL: "${CI_SERVER_PROTOCOL}://${CI_SERVER_HOST}/${CI_PROJECT_NAMESPACE}/certificates.git"
  MATCH_PASSWORD: "${MATCH_DECRYPT_PASSWORD}"
  
  # Android 組態
  ANDROID_HOME: "/usr/local/share/android-sdk"

# ===== iOS 測試 =====
ios_test:
  stage: test
  
  # 指定在 macOS Runner 上執行
  tags:
    - macos
    - xcode
  
  # 只在合併請求或主分支變更時執行
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "main"'
  
  before_script:
    # 安裝相依套件
    - bundle install
    
    # 確保 CocoaPods 快取是最新的
    - cd ios && pod install && cd ..
  
  script:
    # 執行 iOS 測試
    - fastlane ios test
  
  # 保存測試報告
  artifacts:
    name: "ios-test-results-$CI_COMMIT_SHORT_SHA"
    paths:
      - test-results/ios/
    reports:
      junit: test-results/ios/report.junit
    expire_in: 7 days
    when: always
  
  # 測試快取
  cache:
    key: ios-test-$CI_COMMIT_REF_SLUG
    paths:
      - ios/Pods/
      - vendor/bundle/

# ===== Android 測試 =====
android_test:
  stage: test
  
  tags:
    - macos
    - android
  
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "main"'
  
  before_script:
    - bundle install
  
  script:
    # 執行 Android 測試
    - fastlane android test
  
  artifacts:
    name: "android-test-results-$CI_COMMIT_SHORT_SHA"
    paths:
      - test-results/android/
    reports:
      junit: test-results/android/report.xml
    expire_in: 7 days
    when: always
  
  cache:
    key: android-test-$CI_COMMIT_REF_SLUG
    paths:
      - .gradle/
      - vendor/bundle/

# ===== iOS Beta 建置 =====
ios_beta:
  stage: build
  
  tags:
    - macos
    - xcode
  
  # 只在主分支執行
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual
  
  # 需要測試通過
  needs:
    - job: ios_test
      artifacts: false
  
  before_script:
    - bundle install
    - cd ios && pod install && cd ..
    
    # 設定 Apple 憑證
    - echo "$APPLE_CERTIFICATES" | base64 -d > /tmp/apple_cert.p12
    
    # 設定 Keychain
    - security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
    - security default-keychain -s build.keychain
    - security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
    - security import /tmp/apple_cert.p12 -k build.keychain -P "$APPLE_CERT_PASSWORD" -T /usr/bin/codesign
    - security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain
  
  script:
    # 執行 Beta 建置
    - fastlane ios beta
  
  after_script:
    # 清理 Keychain
    - security delete-keychain build.keychain || true
    - rm /tmp/apple_cert.p12 || true
  
  artifacts:
    name: "ios-beta-$CI_COMMIT_SHORT_SHA"
    paths:
      - build/ios/MyApp.ipa
    expire_in: 30 days

# ===== Android Beta 建置 =====
android_beta:
  stage: build
  
  tags:
    - macos
    - android
  
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual
  
  needs:
    - job: android_test
      artifacts: false
  
  before_script:
    - bundle install
    
    # 設定 Android Keystore
    - echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > android/app/keystore.jks
    - export ANDROID_KEYSTORE_FILE="$(pwd)/android/app/keystore.jks"
  
  script:
    # 執行 Beta 建置
    - fastlane android beta
  
  after_script:
    # 清理 Keystore
    - rm android/app/keystore.jks || true
  
  artifacts:
    name: "android-beta-$CI_COMMIT_SHORT_SHA"
    paths:
      - app/build/outputs/bundle/release/app-release.aab
    expire_in: 30 days

# ===== iOS 正式發布 =====
ios_release:
  stage: deploy
  
  tags:
    - macos
    - xcode
  
  # 只允許在 main 分支手動觸發
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual
  
  needs:
    - job: ios_beta
      artifacts: false
  
  before_script:
    - bundle install
    - cd ios && pod install && cd ..
    
    # 設定憑證(同 beta)
    - echo "$APPLE_CERTIFICATES" | base64 -d > /tmp/apple_cert.p12
    - security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
    - security default-keychain -s build.keychain
    - security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
    - security import /tmp/apple_cert.p12 -k build.keychain -P "$APPLE_CERT_PASSWORD" -T /usr/bin/codesign
    - security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain
  
  script:
    # 執行正式發布
    - fastlane ios release
  
  after_script:
    - security delete-keychain build.keychain || true
    - rm /tmp/apple_cert.p12 || true

# ===== Android 正式發布 =====
android_release:
  stage: deploy
  
  tags:
    - macos
    - android
  
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual
  
  needs:
    - job: android_beta
      artifacts: false
  
  before_script:
    - bundle install
    - echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > android/app/keystore.jks
    - export ANDROID_KEYSTORE_FILE="$(pwd)/android/app/keystore.jks"
  
  script:
    # 執行正式發布
    - fastlane android release
  
  after_script:
    - rm android/app/keystore.jks || true

這個 GitLab CI/CD 組態展示了完整的行動應用建置流程。我們使用了多階段的 Pipeline 設計,從測試開始,通過後才能進行建置,最後才是發布。每個階段都有明確的職責與產出物,確保整個流程的可追溯性。

環境變數的管理是安全性的關鍵。所有敏感資訊如憑證密碼、Keystore 檔案都應該透過 GitLab 的 CI/CD 變數功能來管理,並且標記為受保護與遮罩狀態。這些變數只會在 CI/CD 執行時注入,不會在日誌中顯示,確保了憑證的安全性。

快取機制的設定能夠大幅加速建置過程。iOS 的 CocoaPods 相依套件與 Android 的 Gradle 快取都會被保存下來,後續的建置就不需要重新下載這些套件。這在網路頻寬有限或套件伺服器不穩定的情況下特別有用。

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi 300
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 14
skinparam minClassWidth 140

|開發團隊|
start

:開發者推送程式碼至 GitLab;

|GitLab CI/CD|
:觸發 Pipeline 執行;

fork
    |iOS 測試流程|
    :執行 iOS 單元測試;
    :執行 iOS UI 測試;
    :產生測試報告;
    
fork again
    |Android 測試流程|
    :執行 Android 單元測試;
    :執行 Android 整合測試;
    :產生測試報告;
    
end fork

:彙整測試結果;

if (所有測試通過?) then (是)
    
    if (是否為 main 分支?) then (是)
        
        |產品經理|
        :手動觸發 Beta 建置;
        
        |GitLab CI/CD|
        fork
            |iOS Beta 建置|
            :同步 iOS 簽章憑證;
            :執行 Xcode 建置;
            :產生 IPA 檔案;
            :上傳至 TestFlight;
            
        fork again
            |Android Beta 建置|
            :設定 Android Keystore;
            :執行 Gradle 建置;
            :產生 AAB 檔案;
            :上傳至 Play Console;
            
        end fork
        
        :發送 Beta 版本通知;
        
        |測試團隊|
        :Beta 測試與驗證;
        
        if (Beta 測試通過?) then (是)
            
            |產品經理|
            :手動觸發正式發布;
            
            |GitLab CI/CD|
            fork
                |iOS 正式發布|
                :建置 App Store 版本;
                :擷取應用程式截圖;
                :上傳至 App Store Connect;
                :提交審查;
                :建立版本標籤;
                
            fork again
                |Android 正式發布|
                :建置 Production 版本;
                :上傳至 Play Console;
                :設定分階段釋出;
                :建立版本標籤;
                
            end fork
            
            :發送發布完成通知;
            
        else (否)
            :記錄問題並通知開發團隊;
            stop
        endif
        
    else (否)
        :結束 Pipeline;
        stop
    endif
    
else (否)
    :通知測試失敗;
    :產生詳細錯誤報告;
    stop
endif

|監控團隊|
:監控應用程式表現;
:收集使用者回饋;

stop

@enduml

實戰經驗與最佳實踐

經過多年的 CI/CD Pipeline 建置與維運經驗,筆者累積了許多實務上的心得與技術選型考量。這些經驗來自於真實的生產環境部署,涵蓋了從小型新創團隊到大型企業級專案的各種場景。以下分享一些關鍵的實踐原則與常見問題的解決方案。

在效能測試整合方面,k6 確實是一個優秀的選擇,但需要注意測試場景的設計必須貼近實際使用情境。不建議只設定單一的效能閾值,而應該根據不同的 API 端點特性設定不同的標準。例如,資料查詢 API 的回應時間要求可能是五百毫秒以內,而複雜的報表產生功能則可以允許三秒的處理時間。另外,負載測試應該在接近生產環境的基礎設施上執行,否則測試結果的參考價值會大打折扣。

特性旗標機制的應用需要團隊建立明確的使用規範。過多的特性旗標會讓程式碼變得難以維護,因此建議在功能完全穩定後就移除對應的旗標程式碼。筆者建議為每個旗標設定生命週期標籤,標記其建立日期與預期移除日期,並定期檢視是否有長期未清理的旗標。此外,特性旗標的命名應該語意清楚,讓團隊成員一看就知道該旗標控制的功能範圍。

容器化工具整合方面,安全性是最需要重視的環節。所有的第三方工具容器都應該經過安全掃描,並且定期更新基礎映像以修補已知漏洞。筆者強烈建議不要直接使用 Docker Hub 上未經驗證的映像,而是自行建置並維護工具容器。這樣不僅能確保安全性,也能針對企業環境進行客製化調整。

行動應用的 CI/CD 建置中,憑證管理永遠是最容易出問題的環節。iOS 憑證的有效期限只有一年,如果沒有建立提醒機制,很容易在憑證過期後才發現建置失敗。筆者建議在憑證到期前兩個月就開始準備更新流程,並且將新舊憑證重疊使用一段時間,確保過渡期間的建置穩定性。Android 的 Keystore 則需要妥善保管,因為一旦遺失就無法更新既有的應用程式。

在 GitLab Runner 的組態上,macOS Runner 的維護需要特別注意。Xcode 的版本更新頻繁,每次更新都可能影響建置流程。建議維護至少兩台 macOS Runner,一台使用穩定版本的 Xcode,另一台用於測試新版本的相容性。這樣可以在不影響主要開發流程的前提下,逐步遷移到新版本的開發工具。

Pipeline 的執行時間控制也是實務上需要關注的重點。完整的建置與測試流程可能需要三十分鐘以上,如果每次提交都觸發完整流程,會嚴重影響開發效率。筆者建議採用分層的測試策略,在功能分支上只執行快速的單元測試與程式碼檢查,只有在合併至主分支時才執行完整的整合測試與建置流程。

錯誤處理與通知機制的設計也很重要。Pipeline 失敗時,團隊成員需要能夠快速定位問題所在。建議在每個關鍵步驟都加入詳細的日誌輸出,並且整合至團隊的通訊工具(如 Slack)來即時通知相關人員。錯誤訊息應該包含足夠的上下文資訊,讓接收到通知的人能夠立即理解問題的嚴重程度與可能的影響範圍。

最後,CI/CD 基礎設施本身也需要監控與維護。定期檢視 Runner 的健康狀態、磁碟空間使用情況、網路連線品質等指標,能夠在問題發生前就及早發現並處理。筆者建議為 CI/CD 系統建立獨立的監控儀表板,追蹤 Pipeline 的成功率、平均執行時間等關鍵指標,持續優化整體的自動化流程。

透過系統性地延伸 CI/CD Pipeline 的應用範疇,整合效能測試、特性旗標、容器化工具以及行動應用自動化建置,開發團隊能夠建立一個完整且可靠的軟體交付機制。這不僅提升了開發效率,更重要的是確保了軟體品質的一致性與可追溯性。隨著應用程式架構日益複雜,投資在 CI/CD 基礎設施的建置與優化上,將會為企業帶來長期且顯著的回報。