- Concept Development
- Implementation Resources
- Existing Library / Software
- Implementation Process
- Knowledge from Lecture
- Installation
- Usage
- Job Assignment
- References
在現今網站與 App 的開發流程中,許多團隊往往只專注於程式撰寫與功能測試,便直接部署上線,尤其現在使用者開始習慣讓 AI 協助生成程式碼,卻忽略背後潛藏的安全風險。等到作品上線後,發生了安全問題,才意識到「程式碼安全漏洞太多」,造成資料外洩或服務中斷等損失。為了避免這種「事後補救」的情況,我們設計了一條 DevSecOps CI/CD Pipeline,將程式檢查、安全掃描、Image 建置、部署與上線整合成一套自動化流程。讓開發者在開發初期時就將「安全」納入考量。透過 GitHub Actions workflow,系統可以在 Pull Request、Main Build、Staging 與 Production 等不同階段進行層層把關,讓開發者在專案上線後,安心給每個人使用。
我們專案的目標是建立一套可以運行在自建 Kubernetes 叢集上的 DevSecOps CI/CD Pipeline, 讓使用者只要跟著我們步驟設定,就能把自己的專案接上安全的 CI/CD 流程。
| Repo | 用途 | 連結 |
|---|---|---|
DevSecOps_CI-CD_Pipeline |
放你的專案原始碼,並執行 CI/CD workflow,例如程式檢查、安全掃描、Image 建置與部署流程 | 範例 |
infra |
負責建立與管理 Kubernetes 及 CI/CD Pipeline 所需的工具,例如 Terraform modules、自訂 Runner Image、BuildKit Dockerfile、Harbor 憑證與 Kubernetes manifests | 範例 |
youtube-music-bot-gitops |
記錄 Kubernetes staging / production 目前需要部署哪個版本,例如 image digest、Deployment、Service 與環境設定 | 範例 |
我們設計的 DevSecOps CI/CD Pipeline 是以 bs10081/youtube-music-bot 的專案作為示範案例,透過這個專案展示如何在開發、測試與部署過程中導入安全檢測與自動化流程。
| VM | 角色 | 建議記憶體 | 建議 CPU | 建議磁碟 |
|---|---|---|---|---|
| VM1 | Control Plane | 16GB | 4 cores | 50GB |
| VM2 | CI Worker | 32GB+ | 8 cores | 200GB |
| VM3 | Production Worker | 32GB | 4 cores | 50GB |
VM2 需要最多資源,因為每個 Kata MicroVM 約需要 4-8GB 記憶體,多個平行 job 會等比例增加記憶體需求。
Job 實際運行的位置
這個 pipeline 的 CI job 不會跑在 GitHub 的雲端伺服器上,而是跑在你自己 K8s 叢集裡指定的 CI Worker node(在我們的環境裡是 VM2 / node2)。
運作方式
GitHub 收到 push / PR 事件
↓
ARC(Actions Runner Controller)監聽到有新 job
↓
ARC 在 K8s 叢集裡動態建立一個 EphemeralRunner Pod
↓
這個 Pod 被排程到貼有 dedicated=ci-security 標籤的 node(VM2)
↓
Pod 用 runtimeClassName: kata,所以實際上是在
Kata MicroVM 裡執行(獨立的 Linux kernel,跟 VM2 host 完全隔離)
↓
CI job 執行完成
↓
Pod 自動銷毀,不留任何狀態
為什麼 job 會固定跑在 VM2
ARC 的 RunnerScaleSet 設定了 nodeSelector:
nodeSelector:
dedicated: ci-security只有貼上 dedicated=ci-security 這個 label 的 node,才會被 K8s 排程器選中來跑 Runner Pod。在我們的叢集裡,只有 VM2 有這個 label,所以所有 CI job 都會跑在 VM2 上,不會跑到 VM1(control plane)或 VM3(production)。
| 軟體 | 版本 | 安裝位置 |
|---|---|---|
| Ubuntu | 24.04 LTS | 三台 VM |
| Kubernetes | v1.30 | 三台 VM |
| containerd | v2.x | 三台 VM |
| Kata Containers | 3.2.0 | VM2 |
| Docker | 29.x | VM2(build/push runner image 用) |
| Helm | v3.x | VM1 |
| Terraform | 1.15.x | VM1 |
| 服務 | 用途 | 費用 |
|---|---|---|
| GitHub | Repo、Actions、GitHub App | 免費 |
| FOSSA | License 掃描 | 免費方案 |
| Harbor | 自架,不需外部帳號 | 免費 |
CI 靜態分析只在 PR 階段執行,push main 後不會重複跑。
| 工具 | 版本 | 用途 |
|---|---|---|
| Kubernetes | v1.30 | 自建叢集 |
| containerd | v2.2.x | K8s CRI runtime |
| Calico | v3.27 | CNI 網路插件 |
| Kata Containers | 3.2.0 | MicroVM 隔離 CI 環境 |
| local-path-provisioner | latest | K8s 本地儲存 StorageClass |
| 工具 | 版本 | 用途 |
|---|---|---|
| GitHub Actions | - | CI/CD workflow 觸發與執行 |
| ARC(Actions Runner Controller) | 0.14.2 | 在 K8s 動態建立 Runner Pod |
| BuildKit | v0.30.0 | 高效能 Container Image 建置 |
| Harbor | v2.x | 私有 Container Registry |
| Argo CD | latest | GitOps 部署引擎 |
| Kustomize | - | Staging / Production overlay 管理 |
| Terraform | 1.15.x | Infrastructure as Code |
| Helm | v3.x | K8s 應用程式套件管理 |
| 工具 | 用途 |
|---|---|
| Biome | TypeScript / JavaScript Lint + Format |
| tsc | TypeScript 型別檢查 |
| Semgrep | 規則式靜態安全掃描 |
| CodeQL | 深度跨函式資料流分析(SAST) |
| Hadolint | Dockerfile 最佳實踐檢查 |
| Syft | 產生 SBOM(軟體元件清單) |
| Grype | CVE 漏洞掃描 |
| FOSSA | License 合規與供應鏈掃描 |
| Cosign | Container Image 數位簽章與驗證 |
| OWASP ZAP | 動態安全測試(DAST) |
| Kyverno | K8s admission control 政策 |
| CodeRabbit | AI Code Review(SaaS) |
| Snyk / Dependency Review | Dependency 漏洞與 License 風險 |
harbor.jlsa.local:30443/ci/arc-runner:v0.4.0
- 將
kubectl、buildctl、syft、grype、git、curl、node、npm、tsc、python3等工具全部預先裝在 image 裡。 - 讓 CI job 在執行時不需要每次下載,減少外網依賴、執行速度更穩定。
用 kubeadm 在三台 Ubuntu 24.04 VM 上建立 Kubernetes v1.30 叢集。
三台 VM 的共同基礎設定:
每台 VM 都需要做以下準備,確保 K8s 可以正常運作:
- 永久關閉 Swap,因為 K8s scheduler 不支援 Swap 開啟的環境
- 載入
overlay和br_netfilter核心模組,這是 containerd 和 K8s 網路的必要條件 - 設定 sysctl 網路參數,開啟 IP forwarding 和 bridge netfilter,讓 Pod 之間可以互相溝通
- 安裝 containerd 作為 CRI runtime,並設定
SystemdCgroup = true,讓 cgroup 管理跟 systemd 一致,避免資源競爭 - 安裝 kubeadm、kubelet、kubectl,版本鎖定在 v1.30
VM1 初始化 Control Plane:
在 VM1 上用 kubeadm init 初始化叢集,指定 pod network CIDR 為 192.168.0.0/16(Calico 預設範圍)。初始化完成後設定 kubeconfig,讓 kubectl 可以操作叢集。
接著安裝 Calico CNI 作為網路插件。這裡遇到了 Tigera Operator 的 CRD 載入問題,需要等待 CRD 完全建立後再 apply custom-resources.yaml,解決了 Calico 無法正常啟動的問題。
VM2 和 VM3 加入叢集:
在 VM1 用 kubeadm token create --print-join-command 產生 join 指令,分別在 VM2 和 VM3 執行,讓兩台 worker node 加入叢集。
貼上 Node Label,定義 workload 分流:
為了確保 CI job 只跑在 VM2、App 只跑在 VM3,用 label 來控制 Pod 的排程:
# VM2:CI 安全工廠
kubectl label nodes node2 node-role.kubernetes.io/ci-worker=true
kubectl label nodes node2 dedicated=ci-security
# VM3:生產與儲存節點
kubectl label nodes node3 node-role.kubernetes.io/production-worker=true
kubectl label nodes node3 dedicated=production-storage之後 ARC RunnerScaleSet 的 nodeSelector: {dedicated: ci-security} 和 Harbor 的 nodeSelector: {dedicated: production-storage} 會根據這些 label 自動選擇正確的 node。
Kata Containers 是這個專案安全隔離的核心。它讓每個 CI job 跑在獨立的 MicroVM 裡,有自己的 Linux kernel,跟 VM2 的 host 完全隔離。即使 CI job 裡跑了惡意套件或 ZAP 動態攻擊,也打不出這個 MicroVM 的邊界。
安裝 Kata Containers 靜態包:
從官方 GitHub releases 下載 Kata Containers 3.2.0 的 amd64 靜態包,解壓縮到系統根目錄,binary 會自動放進 /opt/kata/bin/。接著建立 symlink,讓 containerd 可以在預設路徑找到 kata 的 shim:
/usr/local/bin/containerd-shim-kata-v2 → /opt/kata/bin/containerd-shim-kata-v2
設定 containerd 對接 Kata:
在 /etc/containerd/conf.d/kata.toml 加入 Kata 的 runtime 設定,這是 containerd v2(config version 3)的正確格式,放在 conf.d/ 資料夾會被 containerd 自動載入,不需要修改主設定檔。
這裡遇到的主要問題是 containerd 的 config_path 預設是空字串,導致自訂的 certs.d/hosts.toml 完全被忽略,containerd 根本不知道要去哪裡找 registry 的憑證設定。修正方式是把 config_path 指向正確的資料夾:
/etc/containerd/config.toml
config_path = '' → config_path = '/etc/containerd/certs.d'
設定 containerd 信任 Harbor(自簽憑證):
Harbor 安裝時使用自簽憑證,containerd 預設不信任它,拉 image 時會報 x509: certificate signed by unknown authority 的錯誤。解決方式是在 certs.d 裡為 Harbor 建立專屬的 hosts.toml,設定 skip_verify = true,讓 containerd 信任這個 registry 而不驗證憑證。
在 K8s 建立 RuntimeClass:
RuntimeClass 是 K8s 的機制,讓你替不同的 Pod 指定不同的 container runtime。建立 kata RuntimeClass 之後,只要在 Pod spec 加上 runtimeClassName: kata,K8s 就會自動用 Kata Containers 來跑這個 Pod,而不是預設的 runc。ARC 的 RunnerScaleSet 就是靠這個機制,讓每個 CI job 自動跑在 Kata MicroVM 裡。
為 VM2 貼上 Node Label:
為了確保 ARC Runner Pod 只排程到 VM2,而不會跑到 VM3 的生產環境上,需要幫 VM2 貼上專屬的 label。ARC RunnerScaleSet 的 nodeSelector 就是靠這個 label 來選擇目標 node:
dedicated=ci-security → ARC Runner Pod 只落在這個 node 上
kata=true → 標記這個 node 有 Kata runtime 可用
所有平台元件都用 Terraform 管理,存放在 infra repo 的 terraform/ 資料夾,不手動執行 helm install 或 kubectl apply。這樣做的好處是所有設定都有版本控制,可以追蹤每次變更,且可以重複執行,每次結果一致。
Terraform module 設計:
每個平台元件都是獨立的 module,職責清楚:
modules/
├── namespaces/ 建立所有 K8s namespace,確保 namespace 存在才能建其他資源
├── harbor/ 用 Helm 安裝 Harbor,並建立 ci project 和 arc-runner robot account
├── arc/ 安裝 ARC Controller 和 RunnerScaleSet,綁定 Kata RuntimeClass
├── secrets/ 建立 harbor-registry-secret,讓 Pod 可以從 Harbor 拉 image
├── kyverno/ 安裝 Kyverno,建立 image 簽章驗證政策
├── runtimeclass/ 建立 kata RuntimeClass
└── argocd_apps/ 建立 Argo CD Application,管理 staging / production 部署
敏感資料管理:
所有密碼、token、private key 都存在 terraform.tfvars 裡,這個檔案只存在 VM1 本地,加進 .gitignore 確保不會 commit 到 GitHub。terraform.tfvars 和 terraform.tfstate 都存著私鑰,絕對不能 commit 到 repo。
ARC 需要跟 GitHub 認證身份,才能接收 CI job。這裡選擇用 GitHub App 而不是 PAT,原因是 GitHub App 有細粒度的權限控制,而且 token 會自動輪換,不會綁定個人帳號。
GitHub App 設定重點:
- Webhook 關閉:ARC 使用 Long-Poll(主動輪詢)方式連線 GitHub,不需要 GitHub 主動打進來,所以不需要 Webhook,VM 也不需要公開 IP
- 只給最小必要權限:Actions、Administration、Checks、Contents、Pull requests
ARC 的工作方式:
ARC 裡有一個 ScaleSetListener Pod,它用 HTTPS long-poll 一直跟 GitHub 保持連線,等待有沒有新的 CI job 進來。當 GitHub 發出 job 訊號,ScaleSetListener 通知 ARC Controller,Controller 立刻在 K8s 建立一個 EphemeralRunner Pod,指定 runtimeClassName: kata,讓 job 跑在 Kata MicroVM 裡。job 完成後,Pod 自動銷毀,不留任何狀態。
Harbor 安裝在 VM3:
用 Terraform 的 Helm provider 把 Harbor 安裝到 harbor namespace,透過 nodeSelector: {dedicated: production-storage} 確保所有 Harbor Pod 都排到 VM3 上。Harbor 使用 NodePort 30443 對外提供服務,資料存在 VM3 的本地磁碟。
Harbor 用來存放專案所有的 image:
- App image(每次 main build 產出的)
- ARC runner image(自訂的 CI 工具 image)
- BuildKit image(用於 image build)
- ZAP image(用於 DAST)
- Cosign signature artifact(image 的數位簽章)
自訂 Runner Image:
預設的 ghcr.io/actions/actions-runner:latest 裡面沒有任何 CI 工具,每次 job 都要臨時下載 kubectl、syft、grype、cosign 等工具,速度慢又依賴外網。自訂 Runner Image 把所有工具預先打包進去:
harbor.jlsa.local:30443/ci/arc-runner:v0.4.0
內建:kubectl、buildctl、syft、grype、cosign
jq、yq、git、curl、node、npm、tsc、python3
在 VM2 上 build 完成後 push 到 Harbor,ARC 建立 Runner Pod 時直接從 Harbor 拉這個 image,不需要再下載任何工具。
PR 階段(六個 job 平行執行):
每次開 PR 或更新 PR branch,以下六個 job 會同時啟動,互不等待:
| 檔案 | 觸發時機 | 說明 |
|---|---|---|
ci.yml |
PR only | Biome、tsc、Semgrep、Build、Hadolint,只在 PR 跑,merge 後不重複 |
codeql.yml |
PR | SAST 深度分析,結果上傳 GitHub Security tab |
dependency-review.yml |
PR | Dependency 漏洞與 License 風險 |
fossa.yml |
PR | License / 供應鏈掃描,目前 analyze-only bootstrap |
pr-dast.yml |
PR | 建立臨時 preview namespace,部署 image,跑 ZAP,清除 |
| CodeRabbit | PR | AI Code Review,SaaS,不跑在 K8s,只給建議不擋 PR |
Main 階段(merge 後觸發):
| 檔案 | 觸發時機 | 說明 |
|---|---|---|
main-build.yml |
push main | BuildKit build、SBOM、CVE scan、Cosign 簽章、Main Build DAST |
cd-staging.yml |
main-build 成功後 | 更新 GitOps staging digest、Argo CD sync、Staging DAST |
cd-production.yml |
staging DAST 通過 + 人工 approval | promote staging digest 到 production、Argo CD sync |
GitOps repo 用 Kustomize 管理兩個環境的 overlay:
apps/youtube-music-bot/
├── base/ 環境中立的共用資源(Deployment、Service)
└── overlays/
├── staging/ staging 專屬設定(namespace、NodePort、image digest)
└── production/ production 專屬設定(namespace、NodePort、image digest)
base 裡定義通用的 K8s 資源,overlay 只描述跟 base 不同的部分。每次 CI 更新 staging 或 production 的 image digest,Argo CD 就會自動偵測並 sync,讓 cluster 的實際狀態追上 GitOps repo 定義的期望狀態。不需要手動執行任何 kubectl apply,所有部署都有 git commit 記錄,可以完整追蹤和回滾。
目前兩個 Argo CD Application 狀態:
youtube-music-bot-staging Synced Healthy
youtube-music-bot-production Synced Healthy
一套結構化的方法,用來規劃、設計、開發、測試、部署與維護軟體。核心精神是:透過明確的流程,確保軟體品質、降低風險、控制成本,並交付符合使用者需求的產品。
傳統 DevOps 強調快速交付跟持續迭代,安全性檢測往往放在最後才檢查。但隨著供應鏈攻擊、雲端環境、容器化越普遍風險越高,所以有了 DevSecOps 的概念,把安全左移 ( Shift Left Security ) 嵌入在 DevOps 每一階段。
- Dev(Development):規劃、寫程式、建置與測試程式。
- Sec(Security):在每個階段加入安全檢查與防護,例如程式碼分析、漏洞掃描。
- Ops(Operations):部署、監控與維護系統,確保穩定運行並持續修補安全問題。
把安全測試提前到開發流程的最早期。越早發現問題,修復成本越低。每個 PR 都自動跑 SAST、Dependency scan、License scan、DAST。
CI/CD 是一種現代開發時常見的概念流程,透過自動化的方式,可以讓碼程式碼從提交到上線的每個環節,一直自動整合、自動建置、自動測試、自動交付與部屬,大大降低人力成本與維持品質。
- CI ( Continuous Integration ) 持續整合: 開發者如果有將新程式碼 push 到分支或 main 上,會自動開始 build 環境和 test,確保功能與環境沒有問題,才會進行 merge。
- CD ( Continuous Delivery/Deployment ) 持續交付/部署
- CD (Continuous Delivery) 持續交付: 整合完後的版本,要真的確定此版本可以被部屬,會先再次進行測試 (包含: 整合測試、端對端測試)。接著將版本部署到 staging 環境 (模擬正式生產環境),做最後的驗證。最後經由人工手動部屬到生產 (production) 環境。
- CD (Continuous Deployment) 持續部屬: 和自動交付一樣,會先進行部屬前的測試。以自動的方式,將通過測試的版本持續部屬到生產環境中。
SAST 是一種在不執行程式碼的情況下,直接對靜態的原始碼、位元組碼或二進位檔進行掃描,逐行比對並分析程式碼的結構,找出潛在的安全弱點與漏洞,揪出開發人員在撰寫程式時留下的錯誤。也被稱為一種靜態的白箱測試。
用來檢查專案中第三方套件、開源元件、相依套件的工具,透過建立 SBOM 來避免供應鏈攻擊、防範已知漏洞。
列出 Container Image 裡所有套件、版本、License,方便追蹤供應鏈風險。使用 Syft 產生 SBOM,再用 Grype 掃描 CVE。
在系統實際運行的情況下,透過模擬外部攻擊 ( 可能像是: SQL Injection、XSS ),來攔截、分析與紀錄你的系統哪裡有漏洞。常見可檢查的問題包含 SQL Injection、XSS、HTTP Header 設定不安全、Cookie 安全屬性缺失、錯誤訊息洩漏等。
不依賴單一工具,而是多層防護:
PR Gate
↓
Main Build Gate
↓
Image Supply Chain Gate(SBOM + CVE + Cosign)
↓
Main Build DAST
↓
Staging DAST Gate
↓
Production Approval
↓
Production Promotion
↓
Kyverno Admission Policy
任何一層發現問題就阻止往下走。
以 Git repository 作為系統狀態的唯一真實來源(Single Source of Truth)。Argo CD 持續監控 GitOps repo,有任何變更就自動同步到 K8s,所有部署都有 git 記錄可以追蹤和回滾。
基礎設施不做修改,只做替換。每次部署都是全新的 Pod,使用新的 image digest,不是在現有 Pod 上做更改。
不預設任何元件是可信任的:
- 每個 CI job 都在獨立 MicroVM 裡執行,完成後銷毀
- Production 只接受 staging 已驗證過的 digest
- Kyverno 驗證每個 Pod 的 Cosign 簽章(待修改逐步收緊中)
注意:本專題建立在自建 Kubernetes 叢集上,叢集的詳細建置步驟(kubeadm、Calico 安裝等)請參考 Kubernetes 官方文件。以下說明假設 K8s 叢集已正常運作。
在開始之前,你需要準備:
- 三台 Ubuntu 24.04 VM,建議規格:
VM 角色 記憶體 CPU 磁碟 VM1 Control Plane 16GB+ 4 cores 50GB VM2 CI Worker 32GB+ 8 cores 200GB VM3 Production Worker 32GB 4 cores 200GB - 三台 VM 在同一內網,可以互相連線
- VM2 已安裝 Kata Containers,RuntimeClass
kata已建立 - VM2 已設定 containerd 信任 Harbor(
certs.d/hosts.toml) - Helm 和 Terraform 已安裝在 VM1
- 三台 VM 的 K8s 叢集已建立,且
kubectl get nodes三台都是Ready
確認 node label:
kubectl get nodes -L dedicated
# node2 → dedicated=ci-security
# node3 → dedicated=production-storage方式 A:一鍵 Script 自動安裝(建議)
把以下三個 script 分別在對應的 VM 上執行:VM1:
sudo bash <(curl --ipv4 -fsSL https://raw.githubusercontent.com/Julie08080808/infra/main/scripts/setup-vm1.sh)VM2:
sudo bash <(curl --ipv4 -fsSL https://raw.githubusercontent.com/Julie08080808/infra/main/scripts/setup-vm2.sh)VM3:
sudo bash <(curl --ipv4 -fsSL https://raw.githubusercontent.com/Julie08080808/infra/main/scripts/setup-vm3.sh)VM1 跑完後,畫面最後會印出 kubeadm join 指令,複製後分別在 VM2 和 VM3 執行加入叢集。
方式 B:手動安裝
**三台 VM 共同步驟:**# 關閉 Swap
sudo swapoff -a
sudo sed -i '/swap/d' /etc/fstab
# 載入核心模組
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
sudo modprobe overlay && sudo modprobe br_netfilter
# 設定網路參數
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sudo sysctl --system
# 安裝 containerd
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg containerd
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml
sudo systemctl restart containerd && sudo systemctl enable containerd
# 安裝 K8s 工具
sudo mkdir -p /etc/apt/keyrings
curl -4 -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key \
| sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-archive-keyring.gpg
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-archive-keyring.gpg] \
https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /" \
| sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectlVM1:初始化 Control Plane
sudo kubeadm init --pod-network-cidr=192.168.0.0/16 --node-name=vm1-control-plane
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
# 安裝 Calico CNI
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/tigera-operator.yaml
sleep 30
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/custom-resources.yamlVM2:額外安裝 Kata Containers
wget https://github.com/kata-containers/kata-containers/releases/download/3.2.0/kata-static-3.2.0-amd64.tar.xz
sudo tar -xJf kata-static-3.2.0-amd64.tar.xz -C /
sudo ln -sf /opt/kata/bin/containerd-shim-kata-v2 /usr/local/bin/containerd-shim-kata-v2
sudo mkdir -p /etc/containerd/conf.d
sudo tee /etc/containerd/conf.d/kata.toml << 'EOF'
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.kata]
runtime_type = 'io.containerd.kata.v2'
privileged_without_host_devices = true
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.kata.options]
ConfigPath = '/opt/kata/share/defaults/kata-containers/configuration.toml'
EOF
sudo mkdir -p /etc/containerd/certs.d
sudo sed -i "s|config_path = ''|config_path = '/etc/containerd/certs.d'|g" \
/etc/containerd/config.toml
sudo systemctl restart containerdVM2 和 VM3:加入叢集
# 在 VM1 取得 join 指令
kubeadm token create --print-join-command
# 在 VM2 和 VM3 分別執行輸出的指令
sudo kubeadm join <VM1_IP>:6443 --token <token> --discovery-token-ca-cert-hash sha256:<hash>VM1:貼上 Node Label
kubectl label nodes node2 kata=true dedicated=ci-security
kubectl label nodes node2 node-role.kubernetes.io/ci-worker=true
kubectl label nodes node3 dedicated=production-storage
kubectl label nodes node3 node-role.kubernetes.io/production-worker=true# App repo
https://github.com/Julie08080808/youtube-music-bot
# GitOps repo
https://github.com/Julie08080808/youtube-music-bot-gitops
# Infra repo
https://github.com/Julie08080808/infra在執行 Terraform 之前,先把三個 repo 裡所有跟你有關的字串換掉。
| 變數 | 說明 | 你的值 |
|---|---|---|
YOUR_GITHUB_OWNER |
你的 GitHub 帳號 | 例如 john-doe |
YOUR_APP_NAME |
你的 App 名稱,會用在 namespace、image、Argo CD | 例如 my-web-app |
YOUR_GITOPS_REPO |
你的 GitOps repo 名稱 | 例如 my-web-app-gitops |
YOUR_VM3_IP |
VM3 的 IP | 例如 192.168.1.100 |
YOUR_HARBOR_HOSTNAME |
Harbor 的 hostname | 例如 harbor.example.com |
YOUR_APP_PORT |
App 監聽的 port | 例如 8080 |
YOUR_ARC_RUNNER_NAME |
ARC RunnerScaleSet 名稱,workflow 裡 runs-on 要對應這個 |
例如 my-arc-runner |
YOUR_ARC_RUNNER_NAME很重要:workflow 裡每個 job 都有runs-on: arc-runner-set,這個值必須跟 Terraform 建立的 RunnerScaleSet 名稱一致,否則 job 會一直卡在Waiting for a runner。
替換順序很重要:
youtube-music-bot-gitops包含youtube-music-bot,所以一定要先換 GitOps repo 名稱,再換 App 名稱,否則 GitOps repo 名稱會被錯誤替換。
# 第一步:先換 GitOps repo 名稱(包含 App 名稱,要先換)
find terraform/ -name "*.tf" -exec sed -i 's/youtube-music-bot-gitops/YOUR_GITOPS_REPO/g' {} \;
# 第二步:再換 App 名稱
find terraform/ -name "*.tf" -exec sed -i 's/youtube-music-bot/YOUR_APP_NAME/g' {} \;
# 第三步:換 GitHub 帳號
# 這個字串出現在 main.tf、argocd_apps/variables.tf、variables.tf 共三個地方
find terraform/ -name "*.tf" -exec sed -i 's/Julie08080808/YOUR_GITHUB_OWNER/g' {} \;
# 第四步:換 IP
find terraform/ -name "*.tf" -exec sed -i 's/10\.32\.20\.51/YOUR_VM3_IP/g' {} \;
# 第五步:換 Harbor hostname
find terraform/ -name "*.tf" -exec sed -i 's/harbor\.jlsa\.local/YOUR_HARBOR_HOSTNAME/g' {} \;
# 第六步:換 ARC RunnerScaleSet 名稱
find terraform/ -name "*.tf" -exec sed -i 's/arc-runner-set/YOUR_ARC_RUNNER_NAME/g' {} \;驗證是否替換成功:
grep -r "youtube-music-bot-gitops" terraform/ && echo "❌ 還有沒換到的 GitOps repo 名稱" || echo "✅ GitOps repo 名稱替換完成"
grep -r "youtube-music-bot" terraform/ && echo "❌ 還有沒換到的 App 名稱" || echo "✅ App 名稱替換完成"
grep -r "Julie08080808" terraform/ && echo "❌ 還有沒換到的 GitHub 帳號" || echo "✅ GitHub 帳號替換完成"
grep -r "10\.32\.20\.51" terraform/ && echo "❌ 還有沒換到的 IP" || echo "✅ IP 替換完成"
grep -r "arc-runner-set" terraform/ && echo "❌ 還有沒換到的 ARC 名稱" || echo "✅ ARC 名稱替換完成"# 第一步:先換 GitOps repo 名稱(yaml 裡如果有引用到自己的 repo 名稱)
find . -name "*.yaml" -exec sed -i 's/youtube-music-bot-gitops/YOUR_GITOPS_REPO/g' {} \;
# 第二步:再換 App 名稱
find . -name "*.yaml" -exec sed -i 's/youtube-music-bot/YOUR_APP_NAME/g' {} \;
# 第三步:換 IP
find . -name "*.yaml" -exec sed -i 's/10\.32\.20\.51/YOUR_VM3_IP/g' {} \;
# 第四步:換 Harbor hostname
find . -name "*.yaml" -exec sed -i 's/harbor\.jlsa\.local/YOUR_HARBOR_HOSTNAME/g' {} \;
# 第五步:換 GitHub 帳號
find . -name "*.yaml" -exec sed -i 's/Julie08080808/YOUR_GITHUB_OWNER/g' {} \;
# 資料夾改名(一定要在 yaml 替換完之後做)
mv apps/youtube-music-bot apps/YOUR_APP_NAME驗證是否替換成功:
grep -r "youtube-music-bot" . --include="*.yaml" && echo "❌ 還有沒換到的 App 名稱" || echo "✅ App 名稱替換完成"
grep -r "10\.32\.20\.51" . --include="*.yaml" && echo "❌ 還有沒換到的 IP" || echo "✅ IP 替換完成"
grep -r "Julie08080808" . --include="*.yaml" && echo "❌ 還有沒換到的 GitHub 帳號" || echo "✅ GitHub 帳號替換完成"
ls apps/ && echo "↑ 確認資料夾名稱正確"# 第一步:先換 GitOps repo 名稱(workflow 裡有引用 GitOps repo URL)
find .github/workflows/ -name "*.yml" -exec sed -i 's/youtube-music-bot-gitops/YOUR_GITOPS_REPO/g' {} \;
# 第二步:再換 App 名稱
find .github/workflows/ -name "*.yml" -exec sed -i 's/youtube-music-bot/YOUR_APP_NAME/g' {} \;
# 第三步:換 GitHub 帳號
find .github/workflows/ -name "*.yml" -exec sed -i 's/Julie08080808/YOUR_GITHUB_OWNER/g' {} \;
# 第四步:換 IP
find .github/workflows/ -name "*.yml" -exec sed -i 's/10\.32\.20\.51/YOUR_VM3_IP/g' {} \;
# 第五步:換 Harbor hostname
find .github/workflows/ -name "*.yml" -exec sed -i 's/harbor\.jlsa\.local/YOUR_HARBOR_HOSTNAME/g' {} \;
# 第六步:換 ARC Runner 名稱(runs-on 要對應你的 RunnerScaleSet 名稱)
find .github/workflows/ -name "*.yml" -exec sed -i 's/runs-on: arc-runner-set/runs-on: YOUR_ARC_RUNNER_NAME/g' {} \;
# 如果你的 App port 不是 3000
find .github/workflows/ -name "*.yml" -exec sed -i 's/APP_PORT="3000"/APP_PORT="YOUR_APP_PORT"/g' {} \;驗證是否替換成功:
grep -r "youtube-music-bot" .github/workflows/ && echo "❌ 還有沒換到的 App 名稱" || echo "✅ App 名稱替換完成"
grep -r "youtube-music-bot-gitops" .github/workflows/ && echo "❌ 還有沒換到的 GitOps repo 名稱" || echo "✅ GitOps repo 名稱替換完成"
grep -r "Julie08080808" .github/workflows/ && echo "❌ 還有沒換到的 GitHub 帳號" || echo "✅ GitHub 帳號替換完成"
grep -r "10\.32\.20\.51" .github/workflows/ && echo "❌ 還有沒換到的 IP" || echo "✅ IP 替換完成"
grep -r "runs-on: arc-runner-set" .github/workflows/ && echo "❌ 還有沒換到的 runs-on" || echo "✅ runs-on 替換完成"全部做完後,用這個清單確認一遍:
infra repo
✅ GitOps repo 名稱替換完成
✅ App 名稱替換完成
✅ GitHub 帳號替換完成
✅ IP 替換完成
✅ Harbor hostname 替換完成
✅ ARC RunnerScaleSet 名稱替換完成
GitOps repo
✅ GitOps repo 名稱替換完成
✅ App 名稱替換完成(yaml 內容 + 資料夾名稱)
✅ IP 替換完成
✅ Harbor hostname 替換完成
✅ GitHub 帳號替換完成
App repo
✅ GitOps repo 名稱替換完成
✅ App 名稱替換完成
✅ GitHub 帳號替換完成
✅ IP 替換完成
✅ Harbor hostname 替換完成
✅ runs-on 對應到你的 ARC RunnerScaleSet 名稱
✅ App port 替換完成(如果不是 3000)
所有項目都確認後,才繼續 Step 3 ~!
- 去
https://github.com/settings/apps/new - 填寫 App 名稱,Homepage URL 填你的 repo URL
- Webhook → Active:取消勾選
- 設定 Repository permissions:
項目 設定值 Actions Read & write Administration Read & write Checks Read & write Contents Read Metadata Read Pull requests Read & write - 點 Create GitHub App,記下 App ID
- 點 Generate a private key → 下載
.pem - 點 Install App → 選你的 repo → 記下網址列的 Installation ID
cd ~/infra/terraform
cp terraform.tfvars.example terraform.tfvars
nano terraform.tfvars填入以下內容:
harbor_admin_password = "你設定的 Harbor 密碼"
github_token = "ghp_xxx..."
github_owner = "YOUR_GITHUB_OWNER"
github_app_id = "Step 3 取得的 App ID"
github_app_installation_id = "Step 3 取得的 Installation ID"
harbor_url = "https://YOUR_VM3_IP:30443"
harbor_runner_username = "robot$arc-runner"
harbor_runner_password = "稍後從 Harbor 介面取得"
gitops_repo_token = "可寫入 GitOps repo 的 GitHub PAT"
github_app_private_key = <<EOT
-----BEGIN RSA PRIVATE KEY-----
貼上 .pem 檔案內容
-----END RSA PRIVATE KEY-----
EOT
terraform.tfvars絕對不能 commit 到 repo。
cd ~/infra/terraform
terraform init
terraform fmt -recursive
terraform validate
terraform plan
terraform apply完成後 Harbor robot account 的 secret 會自動建立,去 Harbor 介面取得後填回 terraform.tfvars 的 harbor_runner_password,再執行一次 terraform apply。
設定 Docker 信任 Harbor
echo '{"insecure-registries":["YOUR_VM3_IP:30443"]}' \
| sudo tee /etc/docker/daemon.json
sudo systemctl restart docker設定 containerd 信任 Harbor
sudo mkdir -p /etc/containerd/certs.d/YOUR_VM3_IP:30443sudo tee /etc/containerd/certs.d/YOUR_VM3_IP:30443/hosts.toml << 'EOF'
server = "https://YOUR_VM3_IP:30443"
[host."https://YOUR_VM3_IP:30443"]
capabilities = ["pull", "resolve", "push"]
skip_verify = true
EOF
sudo systemctl restart containerd登入 Harbor
docker login YOUR_VM3_IP:30443Build 並 Push
cd infra
docker build \
-t YOUR_VM3_IP:30443/ci/arc-runner:latest \
-f runner-images/arc-runner/Dockerfile \
runner-images/arc-runner/
docker push YOUR_VM3_IP:30443/ci/arc-runner:latest
在 App repo 的 Settings → Secrets and variables → Actions 建立:
| Secret 名稱 | 說明 | 取得方式 |
|---|---|---|
K8S_KUBECONFIG_B64 |
kubeconfig base64 | cat ~/.kube/config | base64 -w0(VM1 執行) |
HARBOR_USERNAME |
Harbor Robot Account 帳號 | Harbor 介面 → robot$arc-runner |
HARBOR_PASSWORD |
Harbor Robot Account 密碼 | Harbor 介面 |
HARBOR_CA_CRT |
Harbor CA 憑證 | kubectl get secret harbor-tls -n harbor -o jsonpath='{.data.ca\.crt}' |
COSIGN_PRIVATE_KEY |
Cosign 私鑰 | cosign generate-key-pair |
COSIGN_PUBLIC_KEY |
Cosign 公鑰 | 同上 |
COSIGN_PASSWORD |
Cosign 私鑰密碼 | 自訂 |
GITOPS_PAT |
操作 GitOps repo 的 PAT | GitHub Settings → Tokens |
FOSSA_API_KEY |
FOSSA License 掃描 | app.fossa.com → Integrations → API Tokens |
確認三台 node 都 Ready
kubectl get nodes -o wide
kubectl get nodes -L dedicated確認 ARC Runner
kubectl get pods -n arc-runners -o wide確認 ARC Runner 連上 GitHub
kubectl get AutoscalingRunnerSet -n arc-runners確認 Harbor secrets
kubectl get secret harbor-registry-secret -n arc-runners
kubectl get secret harbor-registry-secret -n <<staging-youtube-music-bot>>
kubectl get secret harbor-registry-secret -n <<production-youtube-music-bot>>確認 Argo CD Applications
kubectl get applications -n argocd預期:
# YOUR_APP_NAME-staging Synced Healthy
# YOUR_APP_NAME-production Synced Healthy確認服務
curl http://YOUR_VM3_IP:31081 # Staging
curl http://YOUR_VM3_IP:31080 # Production每次開發新功能的標準流程:
# 1. 從最新的 main 建立 feature branch
git checkout main && git pull
git checkout -b feature/你的功能名稱
# 2. 開發、commit
git add .
git commit -m "feat: 描述你做了什麼"
git push -u origin feature/你的功能名稱開 Pull Request 到 main 後,以下六個 check 會自動平行執行:
✓ CI(Biome、tsc、Semgrep、Hadolint)
✓ CodeQL SAST
✓ Dependency Review / Snyk
✓ FOSSA License 掃描
✓ PR DAST(preview 環境 + ZAP)
✓ CodeRabbit AI review
所有 check 通過後,merge PR,pipeline 會自動繼續:
merge main
↓ 自動
main-build(Build image + SBOM + CVE scan + Cosign)
↓ 自動
cd-staging(部署到 staging + ZAP DAST)
↓ DAST 通過後等待人工 approval
cd-production(部署到 production)
Production 需要:
- Staging DAST 通過(FAIL = 0、WARN = 0)
- 人工 Approval(可人工或自動)
Staging DAST 通過後,GitHub 會寄 Email 通知有 pending approval。
手動 approve 的步驟:
- 去
https://github.com/YOUR_GITHUB_OWNER/YOUR_APP_NAME/actions - 點進正在等待的
cd-productionworkflow - 點 Review deployments
- 勾選
production環境 - 填寫備註(可選)
- 點 Approve and deploy
GitHub Actions 頁面:
https://github.com/YOUR_GITHUB_OWNER/YOUR_APP_NAME/actions
每次 push 或 PR 都會在這裡看到執行紀錄。
下載安全報告(Artifacts):
進入任一 workflow run,右側 Artifacts 區塊可以下載:
| Artifact | 說明 |
|---|---|
biome-report |
Lint + Format 問題清單 |
tsc-report |
TypeScript 型別錯誤 |
zap-report |
OWASP ZAP 安全掃描報告(HTML + JSON) |
sbom-*.spdx.json |
軟體元件清單 |
grype-*.json |
CVE 漏洞掃描結果 |
Harbor(查看 image):
https://YOUR_VM3_IP:30443
帳號:admin
每次 main-build 成功,新的 image 會出現在 ci/YOUR_APP_NAME 這個 project 下。
Argo CD(查看部署狀態):
kubectl get applications -n argocd
kubectl describe application YOUR_APP_NAME-staging -n argocd如果 production 部署後發現問題,可以用 git revert 快速回滾:
# 在 GitOps repo 裡找到出問題的 commit
git log -- apps/YOUR_APP_NAME/overlays/production/kustomization.yaml
# 回滾
git revert <bad-commit>
git push origin mainArgo CD 偵測到變更後會自動 sync,回到前一個版本。
回滾後確認:
kubectl get pods -n production-YOUR_APP_NAME -o wide
curl http://YOUR_VM3_IP:31080所有 workflow 都支援手動觸發,不一定要等 push 或 PR:
- 去 GitHub Actions 頁面
- 左側選擇要觸發的 workflow
- 點右側 Run workflow
- 選擇分支(通常是
main) - 點綠色 Run workflow 常見需要手動觸發的情況:
- 想重新跑 main-build 但沒有新 commit
- 想單獨測試某個 workflow
- Pipeline 中途失敗想從頭重跑
| 學號 | 名字 | 工作內容 |
|---|---|---|
| 112213061 | 陳章銓 | CI/CD pipeline 的實作、Kubernetes 的環境建置、readme 撰寫、上台報告 |
| 112213065 | 張詠筑 | CI/CD pipeline 的實作、readme 撰寫、上台報告 |
- MoLi 的 server
- Reg 的專案範例
- BT、蓬萊人偶、Reg 的建議
- Kubernetes Documentation
- Kata Containers Documentation
- Actions Runner Controller
- Harbor Documentation
- Argo CD Documentation
- Kyverno Documentation
- Terraform Documentation
- Calico Documentation
- BuildKit Documentation