分類: 部署運維

  • 10 年舊系統如何安全導入 AI 開發:Strangler Fig 遷移方法論

    重點摘要

    • 10 年的舊系統能跑就是最有價值的資產,不要試圖先修好再遷移
    • 核心方法論:Strangler Fig 模式 — 新軌道在旁邊長起來,舊系統自然退場
    • AI 第一件事不是寫 code,而是讀懂 10 年的系統邏輯,再動手
    • 四個階段:快照現況 → 建平行新軌 → 一次搬一個服務 → 封存舊系統

    你的系統跑了 10 年。它很髒、沒有文件、CI/CD 靠手動、密碼可能在 .env 裡或者在某個工程師的腦袋裡。但它能跑,而且在服務真實的用戶。

    現在你想引入 AI 輔助開發,想現代化整個工作流。問題來了:要從哪裡開始? 要先把舊的修乾淨,還是直接用新方法?

    錯誤的答案是:「先把舊的修好。」正確的答案是:不要動正在跑的東西,在旁邊建一條新軌道。

    為什麼「先修好再用新方法」行不通?

    這個直覺很自然,但在實際工程上幾乎都會失敗,原因有三:

    1. 無法停止開發等你修 — 業務不會暫停,新需求還是會進來,你邊修邊開發,舊問題永遠追不完
    2. 「修好」的定義會不斷移動 — 一開始說只要加 .gitignore,結果發現歷史有密碼,要 filter-repo,然後發現測試覆蓋率是零…沒有終點
    3. 你在修一個不完全理解的系統 — 10 年的系統有太多隱性知識,修的過程中很容易把「能跑的」改成「不能跑的」

    工程界有一個著名的模式專門解決這個問題,叫做 Strangler Fig(絞殺榕)模式

    Strangler Fig 模式:不砍舊樹,讓新藤蔓長過去

    絞殺榕是一種熱帶植物。它的種子落在老樹上,慢慢向下長出根,包住舊樹,最後舊樹自然退場,絞殺榕站立在原位。整個過程中,舊樹從未停止「提供支撐」,直到新系統完全就緒。

    應用到 DevOps 遷移:

    ❌ 錯誤思維:
    舊系統(停機)→ 修好 → 接新流程 → 恢復服務
    
    ✅ 正確思維(Strangler Fig):
    舊系統(持續運行,不動)
        ↓
    新軌道在旁邊建立(不影響舊系統)
        ↓
    一次搬一個服務,驗證後切換流量
        ↓
    所有服務搬完,舊系統自然退場

    關鍵洞察:能跑的系統是你最有價值的資產,不是問題的來源。遷移的目標是「讓它繼續跑,同時讓新系統在旁邊成長」,不是「讓它停下來修好再說」。

    四個階段的完整遷移方法論

    階段一:快照現況(不動任何東西)

    第一步不是改 code,不是設定 CI/CD,而是把「現在是怎麼跑起來的」完整記錄下來。這份快照是整個遷移過程的地基。

    為什麼要快照?因為在 10 年的系統裡,repo 裡的 .env 可能是舊的,文件可能是錯的,只有正在跑的進程才是真相:

    # 從正在跑的容器抽出真實的環境變數
    docker inspect <container_name> \
      --format='{{range .Config.Env}}{{println .}}{{end}}' \
      > /tmp/real-env-snapshot.txt
    
    # 或直接讀進程的環境變數
    cat /proc/$(pgrep java)/environ | tr '\0' '\n' | grep -E "DB_|API_|SECRET_"
    
    # K8s 環境
    kubectl get pods -n production -o name | while read pod; do
      echo "=== $pod ==="
      kubectl exec $pod -n production -- env 2>/dev/null
    done > /tmp/real-k8s-env-snapshot.txt

    同時盤點服務清單和依賴關係:

    # 有哪些服務在跑
    docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
    
    # 服務之間怎麼通訊
    docker network inspect bridge
    
    # 對外開放哪些 port
    ss -tlnp | grep LISTEN

    這個階段的產出是一份真實架構圖和一份真實密碼清單(妥善保管,不進任何 repo)。

    階段二:AI 讀懂你的系統

    這是大多數人忽略的步驟,也是決定 AI 協作能否成功的關鍵。

    AI 第一件事不是寫 code。

    在 AI 動手之前,它需要讀你 10 年的系統。這個過程大概需要幾天,但會產出你可能從未有過的東西:

    AI 讀完後的產出 為什麼重要
    系統架構圖 你們可能自己也沒有,新人上手和遷移規劃的基礎
    模組依賴關係 知道改哪個地方會影響哪些服務
    高風險區域標記 「這段 code 10 個人改過,有 3 個已知 bug 的修復」
    技術債清單(按影響排序) 知道先解決什麼,不是看到髒的就改
    新人上手文件 從 code 反推出來,不需要老工程師口傳

    這個階段同樣不動任何 code。AI 只是閱讀和理解。等到真正開始寫新功能時,AI 已經知道你的系統慣例是什麼、有哪些地雷不能踩。

    階段三:建立平行的新軌道

    新建一個乾淨的 repo,在旁邊建立完整的現代化工作流,舊 repo 繼續照舊運作

    舊 GitLab repo(繼續跑,不動)
         │
         │  正在服務用戶的系統
         │
    新 repo(乾淨起點)
         │
         ├─ 正確的 .gitignore(.env 全部排除)
         ├─ CI/CD pipeline(gitleaks + build + sign)
         ├─ K8s Secrets(從快照搬進來)
         └─ Branch Protection Rules

    把真實密碼從快照搬到 K8s Secrets(不是從舊 repo 搬,是從正在跑的進程抽出來):

    # 從快照建立 K8s Secrets
    kubectl create secret generic app-prod-creds \
      --from-literal=DB_PASSWORD="$(grep DB_PASSWORD /tmp/real-env-snapshot.txt | cut -d= -f2)" \
      --from-literal=SHOPEE_KEY="$(grep SHOPEE_KEY /tmp/real-env-snapshot.txt | cut -d= -f2)" \
      -n production
    
    # 建立完成後,安全刪除快照
    shred -u /tmp/real-env-snapshot.txt

    階段四:一次搬一個服務

    這是遷移的主體。原則是:每次只搬一個服務,驗證通過才搬下一個

    服務搬移的優先順序建議:

    優先順序 選擇原則 理由
    第一批 流量最小的非核心服務 風險最低,可以放心試錯
    第二批 獨立性高、依賴少的服務 不會牽一髮動全身
    最後 核心業務邏輯(訂單、付款) 等前幾批證明新流程可靠後再動

    每個服務的搬移步驟:

    1. AI 在新 repo 重寫該服務的乾淨版本(理解舊 code 後重寫,不是 copy paste)
    2. Jenkins 構建新鏡像,部署到 Staging
    3. 用影子流量驗證新舊行為一致(新舊同時收請求,比對回應)
    4. 確認無誤,切換這個服務的流量到新系統
    5. 觀察 24-48 小時
    6. 舊服務下線

    影子流量驗證(用 nginx 實現,不影響用戶):

    # nginx 配置:新舊同時收請求,比對行為
    location /api/orders {
        mirror /mirror-new;       # 複製請求到新系統
        proxy_pass http://old-api; # 用戶實際收到舊系統的回應
    }
    
    location = /mirror-new {
        internal;
        proxy_pass http://new-api;  # 新系統的回應只寫 log,不回傳用戶
    }

    AI 在整個過程中的實際角色

    理解了遷移方法論後,AI 在這個過程中的職責非常清楚:

    AI 能做(且做得很好):
      ✅ 讀 10 年的舊 code,產出架構文件
      ✅ 識別技術債,按優先順序排列
      ✅ 用「符合你們系統慣例」的方式寫新功能
      ✅ 在 feature branch 開發,開 MR,等你 review
      ✅ 補測試(補現有 code 的測試覆蓋率)
      ✅ Debug(分析 log,找根因)
      ✅ 在新服務和舊系統的接縫處寫 adapter
    
    AI 不能做(也不應該做):
      ❌ 直接 push 到 main 或 production branch
      ❌ 決定什麼時候切換流量
      ❌ 接觸任何密碼或 production secrets
      ❌ 在沒有充分理解舊系統的情況下動手

    一個典型的 AI 協作對話長這樣:

    你:「我要加一個批次出貨的功能」
    
    AI:「讓我先讀一下現有的出貨相關 code...
         我看到 OrderService.java 的 shipOrder()
         和 ShipmentRepository,還有一個舊的
         BatchJob 在 legacy/ 目錄。
    
         注意:legacy/BatchJob 有個已知的
         transaction 問題(commit a3f2c1b 的修復紀錄),
         新功能要避開。
    
         建議的實作方向是...你確認嗎?」
    
    你:「方向對,去做吧」
    
    AI:「好,我開 feature/batch-shipment,
         預計改動:
         - 新增 BatchShipmentService(核心邏輯)
         - 修改 OrderController(加 API endpoint)
         - 不動 legacy/ 目錄
         
         完成後開 MR 給你 review」

    AI 因為讀過 10 年的系統,不會做出「不符合你們慣例」的 code,也不會踩進已知的地雷。

    最常遇到的三個現實問題

    問題一:「不知道哪個 .env 是現在真正在用的」

    10 年的系統通常有多個 .env 版本,有的是舊的,有的是工程師自己改過的。以正在跑的進程為準,不要相信文件

    # 找到 Java 進程真正使用的環境變數
    cat /proc/$(pgrep java)/environ | tr '\0' '\n' | grep -E "DB_|API_|SECRET_"
    
    # 不要用
    cat .env  # 這可能是 6 個月前的版本

    問題二:「團隊還在開發,不能凍結」

    不需要凍結。舊 repo 繼續用,新 repo 並行開發。重要的 bugfix 透過 cherry-pick 同步:

    舊 repo: feature → dev → main(繼續照舊)
                  │
                  └─ cherry-pick 重要修復
                          │
    新 repo:              └─ feature → dev → main(新流程)

    問題三:「歷史 commit 有密碼怎麼辦」

    分兩步處理:

    1. 立即輪換所有洩露的密碼 — 因為 GitHub/GitLab 可能已有快取,這一步不能等
    2. 舊 repo 等到遷移完成後再 archive — 不需要現在重寫歷史,新 repo 一開始就是乾淨的

    注意:很多人以為 git rm --cached .env 就安全了,但舊 commit 裡的內容仍然可以被 git show <old-commit>:.env 讀出。唯一的技術修復是 git filter-repo,但遷移方法論讓你可以跳過這一步——因為所有新開發都在新的乾淨 repo 上進行。

    時間軸規劃

    時間 工作 風險
    第 1 週 快照現況、輪換外部 API Key、AI 開始讀系統 零(不動任何 code)
    第 2-4 週 建新 repo、CI/CD pipeline、K8s Secrets、第一個服務搬過去 低(影子流量驗證)
    第 1-3 個月 逐服務遷移,每個驗證 24-48 小時後才繼續 中(每次只影響一個服務)
    遷移完成後 舊 repo archive、完整安全強化(RBAC、Audit Log、鏡像簽名) 低(新系統已穩定)

    決策樹:你現在該從哪裡開始

    你的系統現在能跑嗎?
      │
      ├─ 能跑 → 用 Strangler Fig 模式(本文的方法)
      │           │
      │           ├─ 步驟 1:快照現況(本週就做)
      │           ├─ 步驟 2:AI 讀系統(同步進行)
      │           ├─ 步驟 3:建新軌道(第 2-4 週)
      │           └─ 步驟 4:逐服務遷移(之後)
      │
      └─ 不能跑 → 先讓它跑起來,再回到這裡

    這套方法論的本質

    Strangler Fig 模式應用在 AI 輔助開發遷移上,核心洞察只有一個:

    「10 年的技術債是過去的決策的結果,你無法在不破壞現有價值的情況下一次消除它。但你可以選擇:從今天開始,所有新的工作都用正確的方式做。」

    舊系統是你團隊的集體記憶,AI 有能力閱讀並理解這些記憶,然後用現代化的方式繼續往前走。不需要推倒重建,也不需要凍結開發去修舊債——只需要一條平行的新軌道,和耐心地一次移動一個服務。

    想了解新軌道的 CI/CD 具體設計,可以參考上一篇文章:AI 輔助開發 CI/CD 工作流:Jenkins、K8s、ISO 27001 完整設計

  • AI 輔助開發 CI/CD 工作流:Jenkins、K8s、ISO 27001 完整設計

    重點摘要

    • AI 只負責寫 code、提 PR,不碰版本決策和 Production 部署,人類保留最終控制權
    • 透過 Git tag 觸發 Jenkins,Staging 全自動部署、Production 手動 helm 執行,兩階段驗證才上線
    • 敏感資訊三層隔離:.gitignore → K8s Secrets → etcd 加密,密碼永遠不進 repo
    • 補齊 RBAC、Audit Log、鏡像簽名、Secrets Scan 四大安全缺口,達到 ISO 27001 合規

    AI 輔助開發越來越普遍,但大多數團隊面臨同一個問題:AI 寫的 code 要怎麼安全地上線? 誰決定部署時機?密碼怎麼管?如果 AI 出錯了,有什麼防護網?

    本篇文章完整說明 ONEEC OMS 系統實際採用的 AI 協作工作流設計,包含完整的 User Story、Jenkins Pipeline 架構、三環境部署策略,以及通過安全審查後補齊的 RBAC、Audit Log、鏡像簽名等安全強化配置。

    核心設計理念:人類掌控節奏,AI 加速執行

    這套工作流的核心原則只有一句話:AI 是高效能的執行者,不是決策者。具體體現在以下四點:

    • AI 負責:寫 code、建 Dockerfile、提 PR、提供 Jenkins script 和 Helm chart
    • 用戶負責:code review、創建 git tag(決定版本和部署時機)、手動 helm 部署到 Production
    • 運維負責:管理 K8s Secrets、設定 Jenkins credentials、維護集群
    • 敏感資訊:密碼、API Key、SSL 憑證永遠不進入 Git repo

    完整 User Story:從需求到上線的 10 個步驟

    以下用一個真實場景說明整個流程:場景:優化訂單 API 的查詢效能

    Step 1:AI 開發(feature branch)

    AI 從 dev 分支切出 feature branch,完成開發後推送 PR:

    # AI 執行
    git checkout dev && git pull origin dev
    git checkout -b feature/order-api-optimize
    
    # 編寫程式碼...
    
    # 本地驗證
    docker-compose up -d
    curl http://localhost:8080/api/orders?status=pending
    # ✅ 回傳正確,效能提升 30%
    
    # 提交並推送
    git add . && git commit -m "feat(order-api): optimize query performance"
    git push origin feature/order-api-optimize
    # 建立 PR → dev

    Step 2:用戶 Code Review & Merge

    用戶在 GitHub UI 審查 PR:確認邏輯正確、有測試覆蓋、無敏感資訊後 approve 並 merge 到 dev。此時沒有任何自動化觸發,代碼靜靜等待部署決策。

    Step 3:用戶創建 Staging Tag → Jenkins 自動觸發

    用戶決定要部署到測試環境時,創建一個 staging-v* tag:

    # 用戶執行
    git tag staging-v1.0.1
    git push origin staging-v1.0.1
    
    # GitHub Webhook → Jenkins 自動執行:
    # ├─ Secrets 掃描(gitleaks)
    # ├─ docker build(所有 pods)
    # ├─ cosign 簽名鏡像
    # ├─ docker push to registry
    # ├─ helm deploy to Staging K8s(使用 values-staging.yaml)
    # └─ 通知用戶:Staging v1.0.1 is live

    Step 4:用戶在 Staging 驗證

    kubectl get pods -n staging
    curl https://staging-api.example.com/api/orders?status=pending
    # ✅ 功能正常,效能優化生效
    # ✅ 錯誤率 0%
    # ✅ 回應時間 < 100ms

    Step 5:用戶創建 Production Tag → Jenkins 構建正式鏡像

    # 用戶執行(確認 Staging 無誤後)
    git tag v1.0.1
    git push origin v1.0.1
    
    # Jenkins 執行:
    # ├─ Secrets 掃描
    # ├─ docker build(所有 pods,tag 改為 v1.0.1)
    # ├─ cosign 簽名鏡像
    # ├─ docker push to registry
    # ├─ 生成 Helm values(不含敏感資訊)
    # └─ 通知用戶:Images ready, run helm command

    Step 6:用戶手動部署到 Production

    Production 部署是整個流程中唯一純手動的步驟,這是刻意設計的——確保每一次正式上線都有人類判斷:

    # 用戶在本機執行
    helm upgrade --install order-api \
      /path/to/your/prod-configs/order-api/values-prod.yaml \
      --set image.tag=v1.0.1 \
      -n production
    
    # K8s 自動從 Secrets 注入密碼、API Key
    # Kyverno 自動驗證鏡像簽名(未簽名直接拒絕)
    # Deployment 完成 ✅

    Step 7:監控確認上線成功

    kubectl get pods -n production
    curl https://api.example.com/api/orders?status=pending
    # ✅ 正式環境驗證通過,上線成功

    三個部署環境的定義與分工

    環境 用途 部署方式 配置來源 觸發者
    Dev 本地開發驗證 docker-compose up .env.dev AI(開發時)
    Staging 測試環境(K8s) Jenkins 自動部署 values-staging.yaml(在 repo) 用戶(tag 觸發)
    Production 正式環境(K8s) 手動 helm 部署 values-prod.yaml(用戶維護)+ K8s Secrets 用戶(手動執行)

    Jenkins Pipeline 完整架構

    Jenkins Pipeline 由 GitHub Webhook(tag push)觸發,整個流程分為 6 個 Stage:

    Stage 0:Secrets 掃描(安全門控)

    這是整個 Pipeline 的第一道防線,也是最重要的安全門控。使用 gitleaks 掃描 repo 中是否含有密碼、API Key 等敏感資訊,發現即中止構建並通知安全告警

    stage('Secrets Scan') {
        steps {
            sh '''
                gitleaks detect \
                  --source . \
                  --config .gitleaks.toml \
                  --exit-code 1 \
                  --report-format json \
                  --report-path gitleaks-report.json
            '''
        }
        post {
            failure {
                sh 'sh scripts/notify-security-alert.sh ${TAG_NAME} gitleaks-report.json'
                error('❌ Secrets 掃描發現敏感資訊,構建中止!')
            }
        }
    }

    Stage 1:Tag 偵測(決定部署目標)

    根據 tag 名稱判斷本次構建的部署目標:

    stage('Detect Tag') {
        steps {
            script {
                if (env.TAG_NAME =~ /^staging-v.*/) {
                    env.DEPLOYMENT_ENV = 'staging'
                } else if (env.TAG_NAME =~ /^v.*/) {
                    env.DEPLOYMENT_ENV = 'production'
                } else {
                    error("❌ 未知 tag 格式: ${env.TAG_NAME}")
                }
            }
        }
    }

    Stage 2:Build Images

    構建所有 Pod 的 Docker 鏡像。鏡像本身不含任何配置、密碼、API Key,這是配置與代碼分離的核心原則:

    #!/bin/bash
    # scripts/build-docker.sh
    TAG=$1
    
    docker build -t registry.example.com/order-api:${TAG} ./simpleec-api
    docker build -t registry.example.com/user-app:${TAG} ./user-app
    docker build -t registry.example.com/channel-job:${TAG} ./simpleec-channel-job
    # ... 所有 pods

    Stage 3:Sign Images(供應鏈安全)

    使用 cosign 為每個鏡像簽名,確保 Production 只能部署來自 Jenkins 的受信任鏡像:

    stage('Sign Images') {
        steps {
            withCredentials([file(credentialsId: 'cosign-private-key', variable: 'COSIGN_KEY')]) {
                sh 'sh scripts/sign-docker.sh ${TAG_NAME} ${COSIGN_KEY}'
            }
        }
    }
    
    # scripts/sign-docker.sh
    for IMAGE in "${IMAGES[@]}"; do
        cosign sign --key "${COSIGN_KEY}" \
          --tlog-upload=false \
          "${IMAGE}"
    done

    Stage 4:Push Images

    推送到 Docker Registry。Registry 啟用 Immutable Tags,同一個 tag 無法被覆蓋,確保版本不可篡改:

    stage('Push Images') {
        steps {
            withCredentials([usernamePassword(
                credentialsId: 'docker-registry-creds',
                usernameVariable: 'REGISTRY_USER',
                passwordVariable: 'REGISTRY_PASS'
            )]) {
                sh 'sh scripts/push-docker.sh ${TAG_NAME}'
            }
        }
    }

    Stage 5a(Staging):自動部署到 Staging K8s

    stage('Deploy to Staging') {
        when { expression { env.DEPLOYMENT_ENV == 'staging' } }
        steps {
            withCredentials([file(credentialsId: 'kubeconfig-staging', variable: 'KUBECONFIG')]) {
                sh '''
                    helm upgrade --install order-api ./k8s/helm/order-api \
                      --values ./k8s/helm/order-api/values-staging.yaml \
                      --set image.tag=${TAG_NAME} \
                      -n staging
                '''
            }
        }
    }

    Stage 5b(Production):生成 Helm Values,通知用戶手動部署

    對於 Production tag,Jenkins 不自動部署,而是生成配置檔並通知用戶手動執行:

    stage('Generate Helm Values') {
        when { expression { env.DEPLOYMENT_ENV == 'production' } }
        steps {
            sh 'sh scripts/generate-helm-values.sh ${TAG_NAME}'
            // 生成 values-v${TAG_NAME}.yaml(不含敏感資訊)
            // 通知用戶:Images ready, run helm command
        }
    }

    Helm 配置隔離:敏感資訊三層防護

    配置分為三層,層層隔離:

    第一層:values-staging.yaml(在 repo,測試配置)

    # 主機名用占位符,從 Jenkins 環境變數注入,不硬編碼內網地址
    env:
      DATABASE_HOST: "${POSTGRES_STAGING_HOST}"
      DATABASE_NAME: simpleec_test
      REDIS_HOST: "${REDIS_STAGING_HOST}"
      API_LOG_LEVEL: DEBUG

    第二層:values-prod.yaml(用戶本機維護,不進 repo)

    # 用戶的私密文件,只在本機
    env:
      DATABASE_HOST: postgres-prod.example.com
      API_LOG_LEVEL: WARN
      # ⚠️ 資料庫密碼不在這裡!從 K8s Secrets 注入
    
    envFrom:
      - secretRef:
          name: database-prod-creds  # K8s Secret(運維管理)
      - secretRef:
          name: api-keys-prod        # K8s Secret(運維管理)

    第三層:K8s Secrets + etcd 加密

    # 運維在 Production K8s 上創建
    kubectl create secret generic database-prod-creds \
      --from-literal=username=prod_user \
      --from-literal=password=<secure-password> \
      -n production
    
    # K8s 預設 Secrets 以 base64 存在 etcd(並非加密!)
    # 必須啟用 encryption at rest
    # /etc/kubernetes/encryption-config.yaml
    apiVersion: apiserver.config.k8s.io/v1
    kind: EncryptionConfiguration
    resources:
      - resources: ["secrets"]
        providers:
          - aescbc:
              keys:
                - name: key1
                  secret: <base64-encoded-32-byte-key>

    安全強化:補齊四大缺口

    原始設計經過安全審查後,發現四個必須在投產前補足的缺口:

    缺口一:K8s RBAC 未定義

    三個角色各有最小權限(文件放在 k8s/rbac/):

    角色 允許操作 明確禁止
    Jenkins SA(staging) update/patch Deployments, get Pods 讀取任何 Secrets
    用戶(production) helm 部署相關資源 讀取業務 Secrets(DB 密碼、API Key)
    運維(production) Secrets 完整管理權
    # 驗證 Jenkins SA 無法讀取 Secrets(應輸出 no)
    kubectl auth can-i get secrets \
      --as=system:serviceaccount:staging:jenkins-deployer \
      -n staging

    缺口二:K8s Audit Log 未配置

    ISO 27001 A.12.4.1 要求所有敏感操作都要有日誌。以下 Audit Policy 至少記錄 Secrets 訪問和 Deployment 變更:

    # /etc/kubernetes/audit-policy.yaml
    apiVersion: audit.k8s.io/v1
    kind: Policy
    rules:
      - level: Metadata
        resources:
          - group: ""
            resources: ["secrets"]  # 所有 Secrets 訪問都記錄
    
      - level: Request
        verbs: ["create", "update", "delete", "patch"]
        resources:
          - group: "apps"
            resources: ["deployments"]
    
      - level: None
        users: ["system:kube-proxy"]
        verbs: ["watch", "list"]

    缺口三:鏡像簽名驗證(Kyverno 準入控制)

    確保集群只能部署來自 Jenkins 簽名的鏡像,防止鏡像替換攻擊:

    apiVersion: kyverno.io/v1
    kind: ClusterPolicy
    metadata:
      name: verify-image-signatures
    spec:
      validationFailureAction: Enforce  # 未簽名鏡像直接拒絕
      rules:
        - name: check-image-signature
          match:
            any:
              - resources:
                  kinds: ["Pod"]
                  namespaces: ["staging", "production"]
          verifyImages:
            - imageReferences:
                - "registry.example.com/*"
              attestors:
                - count: 1
                  entries:
                    - keys:
                        publicKeys: |-
                          -----BEGIN PUBLIC KEY-----
                          # cosign.pub 內容
                          -----END PUBLIC KEY-----

    缺口四:GitHub Branch Protection 口頭約定 → 技術強制

    分支 Required Reviews CI 必須通過 Push 限制
    main 2 人 approve ✅ jenkins-build + secrets-scan 僅 team-lead
    staging 1 人 approve ✅ jenkins-build + secrets-scan 僅 team-lead
    dev 1 人 approve 必須透過 PR(AI 不能直接 push)

    Git 分支策略與 Tag 命名規範

    整個工作流的分支拓撲如下:

    main                     # Production 對應,受嚴格保護
     └─ tag: v1.0.0, v1.0.1  # 觸發 Jenkins 構建 Production 鏡像
    
    staging                  # 測試環境,中度保護
     └─ tag: staging-v1.0.0  # 觸發 Jenkins 自動部署到 Staging K8s
    
    dev                      # 開發積累,AI 透過 PR 提交
     └─ 來源:feature/* 合入
    
    feature/*                # AI 的工作分支(每個功能一個)
     ├─ feature/user-auth
     ├─ feature/order-api
     └─ feature/channel-job-momo

    敏感資訊完整隔離架構

    存放位置 可以存什麼 絕對不能存什麼 管理者
    Git Repository 代碼、Dockerfile、values-staging.yaml、Helm chart 模板 密碼、API Key、SSL 憑證、values-prod.yaml AI + 用戶
    Docker Registry 不含配置的乾淨鏡像(cosign 簽名) 任何敏感資訊 Jenkins(push)
    K8s Secrets(etcd 加密) database-prod-creds、api-keys-prod、SSL 憑證 運維
    Jenkins Credentials GitHub token、Registry credentials、cosign key、kubeconfig 運維

    回滾策略

    Staging 環境回滾

    # 快速回滾到上一個版本
    helm rollback order-api 0 -n staging
    
    # 或指定版本
    helm upgrade order-api ./k8s/helm/order-api \
      --values ./k8s/helm/order-api/values-staging.yaml \
      --set image.tag=staging-v1.0.0 \
      -n staging

    Production 環境回滾

    # 查看部署歷史
    helm history order-api -n production
    
    # 回滾到上一個版本
    helm rollback order-api 0 -n production
    
    # 所有 tag 在 Git 可追溯
    git log --oneline --all | grep "v1.0"

    投產前安全檢查清單

    在正式上線前,以下所有項目必須確認通過:

    代碼倉庫安全

    • ✅ .gitignore 包含 .env, .env.dev, **/values-prod.yaml
    • ✅ repo 根目錄存在 .gitleaks.toml 配置文件
    • ✅ pre-commit hook 已安裝
    • ✅ git log –all — ‘*.env’ 確認歷史中無敏感文件

    Jenkins Pipeline

    • ✅ 第一個 Stage 為 Secrets Scan(gitleaks)
    • ✅ Sign Images Stage 已配置(cosign)
    • ✅ Push Images 使用 Jenkins Credentials(非明文)
    • ✅ GitHub Webhook Secret 已配置(Jenkins + GitHub 雙端)

    K8s 訪問控制

    • ✅ k8s/rbac/ 三個 RBAC 文件已 apply
    • ✅ Jenkins SA 驗證:kubectl auth can-i get secrets … → no
    • ✅ Kyverno 已安裝,鏡像簽名驗證策略已 apply
    • ✅ etcd encryption at rest 已啟用(運維確認)

    審計和監控

    • ✅ K8s Audit Log 已配置(audit-policy.yaml)
    • ✅ Audit Log 保留策略 ≥ 90 天
    • ✅ 告警規則已配置(部署失敗、Secrets 掃描失敗)

    總結:這套工作流解決了什麼問題?

    AI 輔助開發的核心挑戰不是技術,而是信任邊界:誰能做什麼?誰為每個決定負責?這套工作流的答案很清楚:

    • AI 的邊界:寫 code、提 PR、建 Docker image — 技術執行層
    • 用戶的邊界:review 代碼、創建 tag、手動部署 Production — 決策層
    • 運維的邊界:管理 Secrets、維護集群、配置 credentials — 基礎設施層
    • 自動化的邊界:Jenkins 在 tag 觸發後執行既定腳本 — 不越界,不決策

    這種分層設計讓 AI 協作既高效又安全,每一個部署都有完整的審計軌跡,每一個敏感操作都需要人類授權。

  • Kafka 事件驅動架構:打造高可用訂單處理系統

    商業價值:事件驅動架構讓系統能「處理速度提升 10 倍」,從 4-8 小時縮短到 25-35 分鐘。詳見 導讀篇的 ROI 計算

    前言:為什麼需要事件驅動?

    想像一個場景:使用者在後台點擊「同步蝦皮訂單」。

    同步處理的問題:

    • 蝦皮 API 回應慢 → 使用者等待 30 秒以上
    • API 超時 → 整個請求失敗
    • 大量請求 → 伺服器資源耗盡

    解決方案:非同步事件驅動

    使用者請求 背景處理
    │ │
    ▼ │
    ┌─────────┐ 發送訊息 ┌─────────┐
    │ Web API │ ──────────────► │ Kafka │
    └─────────┘ └────┬────┘
    │ │
    ▼ ▼
    回應成功 ┌───────────────┐
    (立即返回) │ Consumer Job │
    │ 慢慢處理… │
    └───────────────┘
    效果:使用者立即收到「已排程」回應,實際同步在背景執行。

    架構設計

    Topic 設計:每個通路獨立

    Topic 名稱 用途 Consumer
    oms-action-shopee 蝦皮相關動作 Shopee Consumer
    oms-action-momo Momo 相關動作 Momo Consumer
    oms-action-yahoo Yahoo 相關動作 Yahoo Consumer
    oms-action-pchome PChome 相關動作 PChome Consumer
    為什麼要分開?

    • 蝦皮 API 壞了,不影響 Momo 訂單處理
    • 可以針對不同平台調整 Consumer 數量
    • 方便監控各平台的處理狀況

    Producer:發送訊息

    @Service
    public class ActionProducer {

    private final KafkaTemplate<String, String> kafkaTemplate;

    /**
    * 發送動作到對應的通路 Topic
    */

    public void sendAction(ChannelType channel, ActionMessage message) {
    String topic = “oms-action-“ + channel.getCode();
    String payload = JsonUtil.toJson(message);

    kafkaTemplate.send(topic, message.getMerchantId(), payload)
    .addCallback(
    result -> log.info(“發送成功: {}”, topic),
    error -> log.error(“發送失敗: {}”, error.getMessage())
    );
    }
    }

    訊息格式設計

    {
    “header”: {
    “messageId”: “uuid-xxxx-xxxx”,
    “timestamp”: “2024-03-18T10:30:00Z”,
    “traceId”: “trace-xxxx”
    },
    “body”: {
    “merchantId”: “M001”,
    “actionType”: “SYNC_ORDERS”,
    “parameters”: {
    “startDate”: “2024-03-17”,
    “endDate”: “2024-03-18”
    }
    }
    }

    Consumer:處理訊息

    @Component
    public class ActionConsumer {

    @Autowired
    private ChannelFactory channelFactory;

    @KafkaListener(topics = “oms-action-shopee”)
    public void consumeShopee(String message) {
    processAction(ChannelType.SHOPEE, message);
    }

    @KafkaListener(topics = “oms-action-momo”)
    public void consumeMomo(String message) {
    processAction(ChannelType.MOMO, message);
    }

    private void processAction(ChannelType channel, String message) {
    try {
    // 1. 解析訊息
    ActionMessage action = JsonUtil.fromJson(message);

    // 2. 取得對應的通路處理器
    ChannelAction handler = channelFactory.getAction(channel);

    // 3. 執行動作
    ActionResult result = handler.execute(action);

    // 4. 回寫結果
    saveResult(action, result);

    } catch (Exception e) {
    // 5. 錯誤處理
    handleError(message, e);
    }
    }
    }


    錯誤處理策略

    錯誤類型 處理方式 範例
    暫時性錯誤 重試 3 次 API 超時、網路問題
    永久性錯誤 記錄並跳過 資料格式錯誤
    未知錯誤 進入 Dead Letter Queue 系統異常
    @Bean
    public DefaultErrorHandler errorHandler() {
    // 設定重試策略
    BackOff backOff = new ExponentialBackOff(1000L, 2.0);
    backOff.setMaxElapsedTime(30000L); // 最多重試 30 秒

    return new DefaultErrorHandler(
    (record, exception) -> {
    // 重試失敗後,送到 Dead Letter Queue
    sendToDeadLetterQueue(record, exception);
    },
    backOff
    );
    }


    監控與告警

    監控指標 正常值 告警條件
    Consumer Lag < 1000 > 5000 持續 5 分鐘
    處理時間 < 5 秒 > 30 秒
    錯誤率 < 1% > 5%
    Dead Letter 數量 0 > 10

    效能調校

    # application.yml
    spring:
    kafka:
    consumer:
    # 每次拉取的最大筆數
    max-poll-records: 100

    # 拉取間隔
    fetch-min-size: 1
    fetch-max-wait: 500ms

    producer:
    # 批次發送設定
    batch-size: 16384
    buffer-memory: 33554432

    # 壓縮
    compression-type: lz4


    總結

    設計 效果
    非同步處理 使用者不用等待 API 回應
    Topic 分離 通路故障隔離
    重試機制 暫時性錯誤自動恢復
    Dead Letter Queue 問題訊息不遺失
    監控告警 問題即時發現

    為什麼不用其他方案?

    方案 優點 缺點 結論
    同步處理 簡單、好除錯 使用者要等、效能差 小流量可用
    Redis Queue 輕量、快速 持久化弱、無法分區 簡單場景可用
    RabbitMQ 功能豐富、可靠 吞吐量不如 Kafka 適合複雜路由
    Kafka 高吞吐、持久化、分區 學習曲線、維運成本 大流量首選

    實戰踩坑

    坑 1:Consumer Lag 暴增

    雙 11 當天 Consumer Lag 飆到 50,000+,訂單處理延遲 2 小時。原因:單一 Consumer 處理太慢。解法:增加 Consumer 數量到 Partition 數量,同時優化處理邏輯(批次處理)。

    坑 2:訊息重複消費

    Consumer 處理到一半掛掉,重啟後同一筆訂單被處理兩次,導致重複出貨。解法:加入冪等性檢查(用訂單 ID 去重)。

    坑 3:Topic 沒分開

    最初所有平台共用一個 Topic,蝦皮 API 壞了堵住整條 Queue,Momo 訂單也跟著延遲。後來拆成每個平台獨立 Topic,故障隔離。


    系列導航

    ◀ 上一篇
    工廠模式
    📚 返回目錄 下一篇 ▶
    多租戶認證
  • 多通路電商系統架構:用工廠模式整合 17 個平台

    商業價值:這篇介紹的工廠模式讓「新增平台從 2-3 個月縮短到 2-3 週」,直接影響 導讀篇提到的 80% 擴展成本降低

    前言:當你要同時對接 17 個電商平台

    在多通路電商系統中,我們需要整合多個平台:

    平台類型 範例 特性
    綜合電商 蝦皮、Momo、Yahoo、PChome 訂單量大、API 複雜
    開店平台 Shopify、Shopline、91APP 彈性高、客製化多
    國際平台 樂天、Coupang、Amazon 多語系、跨境物流
    問題:每個平台的 API 格式、認證方式、資料結構都不同。如果用 if-else 判斷,程式碼會變成災難。

    解決方案:工廠模式 + 策略模式

    核心思想:定義統一介面,每個平台各自實作。新增平台時,只需要新增一個實作類別。

    Step 1:定義統一介面

    所有平台都必須實作這個介面:

    /**
    * 電商平台動作介面
    * 所有平台整合都必須實作這個介面
    */

    public interface ChannelAction {

    // 取得平台設定(API URL、版本等)
    ChannelSetting getSetting();

    // 取得平台授權 Token
    TokenResult getAccessToken(Merchant merchant);

    // 驗證必要資料是否齊全
    boolean validateRequired(ActionRequest request);

    // 執行實際動作(同步訂單、更新庫存等)
    ActionResult execute(ActionRequest request);
    }

    Step 2:每個平台各自實作

    蝦皮實作範例:

    @Component
    public class ShopeeAction implements ChannelAction {

    @Override
    public ChannelSetting getSetting() {
    return ChannelSetting.builder()
    .apiUrl(“https://partner.shopeemobile.com”)
    .version(“v2”)
    .authType(AuthType.OAUTH2)
    .build();
    }

    @Override
    public TokenResult getAccessToken(Merchant merchant) {
    // 蝦皮使用 OAuth2 + 簽章驗證
    String signature = generateSignature(merchant);
    return callShopeeAuthAPI(merchant, signature);
    }

    @Override
    public ActionResult execute(ActionRequest request) {
    // 呼叫蝦皮 API 執行動作
    return callShopeeAPI(request);
    }
    }

    Momo 實作範例:

    @Component
    public class MomoAction implements ChannelAction {

    @Override
    public ChannelSetting getSetting() {
    return ChannelSetting.builder()
    .apiUrl(“https://api.momo.com.tw”)
    .version(“v1”)
    .authType(AuthType.API_KEY)
    .build();
    }

    @Override
    public TokenResult getAccessToken(Merchant merchant) {
    // Momo 使用 API Key 驗證
    return TokenResult.of(merchant.getMomoApiKey());
    }

    @Override
    public ActionResult execute(ActionRequest request) {
    // 呼叫 Momo API 執行動作
    return callMomoAPI(request);
    }
    }

    Step 3:工廠類別統一管理

    @Component
    public class ChannelFactory {

    private final Map<ChannelType, ChannelAction> actionMap;

    // Spring 自動注入所有 ChannelAction 實作
    public ChannelFactory(List<ChannelAction> actions) {
    this.actionMap = actions.stream()
    .collect(Collectors.toMap(
    action -> action.getSetting().getChannelType(),
    action -> action
    ));
    }

    /**
    * 根據通路類型取得對應的實作
    */

    public ChannelAction getAction(ChannelType channelType) {
    ChannelAction action = actionMap.get(channelType);
    if (action == null) {
    throw new UnsupportedChannelException(
    “不支援的通路: “ + channelType
    );
    }
    return action;
    }
    }


    使用方式

    業務邏輯層只需要這樣呼叫:

    @Service
    public class OrderSyncService {

    @Autowired
    private ChannelFactory channelFactory;

    public SyncResult syncOrders(ChannelType channel, Merchant merchant) {
    // 1. 取得對應的通路實作
    ChannelAction action = channelFactory.getAction(channel);

    // 2. 取得授權 Token
    TokenResult token = action.getAccessToken(merchant);

    // 3. 執行同步
    ActionRequest request = ActionRequest.builder()
    .merchant(merchant)
    .token(token)
    .actionType(ActionType.SYNC_ORDERS)
    .build();

    return action.execute(request);
    }
    }

    優點:不管是蝦皮、Momo、Yahoo 還是其他平台,呼叫方式完全一樣。新增平台時,業務邏輯層完全不用改。

    新增平台有多簡單?

    假設要新增 Coupang 韓國平台:

    @Component
    public class CoupangAction implements ChannelAction {

    @Override
    public ChannelSetting getSetting() {
    return ChannelSetting.builder()
    .apiUrl(“https://api-gateway.coupang.com”)
    .version(“v2”)
    .authType(AuthType.HMAC)
    .build();
    }

    // … 實作其他方法
    }

    步驟 工作內容 影響範圍
    1 建立 CoupangAction 類別 只有新檔案
    2 實作 ChannelAction 介面 只有新檔案
    3 加上 @Component 註解 只有新檔案
    4 完成!Spring 自動註冊 零修改現有程式

    設計模式總結

    模式 用途 在這裡的應用
    策略模式 定義演算法家族,讓它們可互換 每個平台是一個策略
    工廠模式 封裝物件建立邏輯 根據通路類型取得實作
    依賴注入 解耦合 Spring 自動管理
    效益:

    • 新增通路:從 2-3 個月縮短到 2-3 週
    • 維護成本:改一個平台不影響其他平台
    • 測試:每個平台可以獨立單元測試

    為什麼不用其他方案?

    方案 優點 缺點 結論
    if-else 判斷 簡單直接 每次加平台要改核心程式碼 小規模可用,超過 3 個平台就很痛苦
    Switch Case 比 if-else 清楚 還是要改核心程式碼 同上
    反射 + 設定檔 完全不改程式碼 除錯困難、IDE 無法追蹤 過度設計,維護成本高
    工廠 + 策略 新增只加檔案、Spring 自動註冊 需要理解設計模式 中大型系統的最佳平衡

    實戰踩坑

    坑 1:平台 API 變更沒通知

    蝦皮某次 API 升級,回傳欄位名稱從 order_sn 改成 ordersn。因為每個平台有獨立的 Action 類別,我們只需要改 ShopeeAction,其他 16 個平台完全不受影響。如果用 if-else,改錯一行就全部爆炸。

    坑 2:忘記加 @Component

    新人寫好 CoupangAction 卻沒加 @Component,Spring 沒註冊到 Factory。呼叫時直接噴 UnsupportedChannelException。後來在程式碼審查加入檢查項:「確認新 Action 有 @Component」。

    坑 3:介面設計太死

    最初 ChannelAction 只有 execute() 一個方法。後來發現有些平台需要 OAuth 刷新 Token、有些需要 Webhook 處理。介面改了三次才穩定。教訓:先做 3-5 個平台再抽象,別一開始就過度設計


    系列導航

    ◀ 上一篇
    導讀篇
    📚 返回目錄 下一篇 ▶
    Kafka 事件驅動