私有雲小型 IDP 實戰:用 ArgoCD + Helm 打造 Lean GitOps 開發者平台
前言:小團隊為什麼需要 IDP?
Internal Developer Platform (IDP) 這個詞聽起來很重,總讓人聯想到 Backstage、平台團隊、Plugin 地獄。但對已經有 Kubernetes 經驗的小團隊來說,IDP 的本質很簡單:
讓開發者能自助完成「建環境、部署、Scale」這些日常操作,而不用每次都找 DevOps。
如果你的團隊目前還在用 Docker Compose 管理開發/測試環境,很可能已經遇到這些痛點:
- 「staging 壞了誰動的?」——沒有版本紀錄
- 「我改了 compose 檔,別人怎麼 sync?」——環境漂移
- 「想開個 preview 環境測 PR」——做不到
- 「要 scale 某個服務」——手動改 replicas 還要 ssh
這篇文章要告訴你:用現成開源工具,1-2 人花 1-2 週就能搭出一個「夠用」的小型 IDP,取代 Docker Compose 的痛點,又不會掉進 Backstage 的複雜度陷阱。
一、Docker Compose 的結構性痛點
先診斷問題,才能對症下藥。Docker Compose 的問題不在語法,而在操作模型:
| 痛點 | 根本原因 | 後果 |
|---|---|---|
| 多環境複製難 | 一個 docker-compose.yml 綁死一個環境 |
dev/staging/prod 各一份,維護地獄 |
| 沒有審批機制 | 誰都能 docker-compose up -d |
環境被改壞沒人知道 |
| 無法原子回滾 | 沒有版本概念 | 「剛才是什麼版本?」 |
| Scale 不一致 | 手動改 replicas,沒有 HPA | 流量來了手忙腳亂 |
| 狀態不可觀測 | 沒有內建 metrics | 服務掛了要 ssh 進去 docker ps |
💡 核心問題:Docker Compose 是「命令式操作」,你告訴它「做什麼」。但現代 DevOps 需要「宣告式管理」——你描述「要什麼狀態」,系統自動收斂。
這就是 Kubernetes + GitOps 能解的問題。
二、Lean GitOps IDP:最小可行的平台
什麼是 Lean GitOps IDP?
我把這套方案叫做 Lean GitOps IDP,核心理念是:
- Git 是唯一事實來源 (Single Source of Truth)
- ArgoCD 是部署控制器 + UI
- Helm/Kustomize 是服務定義 DSL
- 沒有自己寫的 API,沒有 Plugin 系統
這套方案有意識地避開了 IDP 最常見的坑:
| 陷阱 | 為什麼要避開 |
|---|---|
| ❌ Backstage | Plugin 地獄、治理成本高、小團隊維護不起 |
| ❌ 自己寫平台 API | 變成「平台產品」,無限膨脹 |
| ❌ 強求 Golden Path | 小團隊反而被模板卡住,不如保持彈性 |
這套方案怎麼解 Compose 的痛點?
| Compose 痛點 | Lean GitOps IDP 怎麼解 |
|---|---|
| 多環境複製 | K8s namespace + Helm values 檔案 |
| 無審批 | Git PR + Code Review |
| 無回滾 | ArgoCD 一鍵 rollback(有完整 revision history) |
| Scale 不一致 | HPA + replica 定義在 Helm |
| 沒狀態可觀測 | Prometheus + Grafana(K8s 生態標配) |
這不是升級工具,是升級操作模型。
三、技術棧選型
核心工具堆疊
| 工具 | 角色 | 為什麼選它 |
|---|---|---|
| ArgoCD | GitOps 控制器 | Git push → 自動 sync 到 K8s,內建 UI、diff、rollback |
| Helm | 服務模板化 | 參數化 YAML,一套 chart 打多環境 |
| Kustomize | 環境差異化 | 如果你討厭模板語法,用 overlay 更直觀 |
| Prometheus + Grafana | 監控 | K8s 標配,Helm 一鍵裝 |
可選工具(後續加)
| 工具 | 角色 | 什麼時候加 |
|---|---|---|
| Portainer / Lens | K8s Dashboard | 給不熟 kubectl 的人用 |
| Tekton / GitHub Actions | CI Pipeline | 需要 build image 時 |
| Crossplane | IaC 管理雲資源 | 需要管 RDS/S3 時 |
四、前置條件
在開始之前,確認你有:
基礎設施
- Kubernetes Cluster:可以是 k3s、RKE2、kubeadm 或任何發行版
- 最低配置:3 node、8GB RAM/node、50GB storage
- 生產建議:HA control plane + 分離 worker node
- kubectl 已配置並能連到 cluster
- Helm v3 已安裝
網路需求
- Cluster 能存取 Git 倉庫(GitHub/GitLab/Gitea)
- 有 Ingress Controller(nginx-ingress / traefik)
- 有 DNS 指向 Ingress(或用 nip.io 測試)
權限
- K8s cluster-admin 權限(安裝階段需要)
- Git repo 的 write 權限
# 確認環境
kubectl version --client
helm version
kubectl get nodes
五、Step-by-Step 實戰
Step 1: 安裝 ArgoCD(5 分鐘)
# 建立 namespace
kubectl create namespace argocd
# 安裝 ArgoCD
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# 等待所有 pod ready
kubectl wait --for=condition=Ready pods --all -n argocd --timeout=300s
# 取得初始密碼
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
💡 記下密碼,預設帳號是
admin。
Step 2: 暴露 ArgoCD UI
方法 A:Port Forward(開發/測試用)
kubectl port-forward svc/argocd-server -n argocd 8080:443
# 然後瀏覽 https://localhost:8080
方法 B:Ingress(生產用)
# argocd-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: argocd-server-ingress
namespace: argocd
annotations:
nginx.ingress.kubernetes.io/ssl-passthrough: "true"
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
spec:
ingressClassName: nginx
rules:
- host: argocd.your-domain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: argocd-server
port:
number: 443
kubectl apply -f argocd-ingress.yaml
Step 3: 安裝 ArgoCD CLI
# macOS
brew install argocd
# Linux
curl -sSL -o argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
chmod +x argocd && sudo mv argocd /usr/local/bin/
# 登入
argocd login localhost:8080 --username admin --password <your-password> --insecure
Step 4: 建立 Git Repo 結構
這是 Lean GitOps IDP 的核心——你的「平台」就是這個 repo:
platform-config/
├── README.md
├── apps/ # ArgoCD Application 定義
│ ├── dev/
│ │ └── my-api.yaml
│ ├── staging/
│ │ └── my-api.yaml
│ └── prod/
│ └── my-api.yaml
├── charts/ # 你的 Helm Charts
│ └── my-api/
│ ├── Chart.yaml
│ ├── values.yaml
│ ├── values-dev.yaml
│ ├── values-staging.yaml
│ ├── values-prod.yaml
│ └── templates/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ └── hpa.yaml
└── scripts/ # 自助腳本
├── create-preview-env.sh
└── Makefile
Step 5: 建立 Helm Chart
charts/my-api/Chart.yaml:
apiVersion: v2
name: my-api
description: My API Service
type: application
version: 0.1.0
appVersion: "1.0.0"
charts/my-api/values.yaml (預設值):
replicaCount: 1
image:
repository: your-registry/my-api
tag: latest
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 8080
ingress:
enabled: false
host: ""
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 10
targetCPUUtilizationPercentage: 80
charts/my-api/values-dev.yaml:
replicaCount: 1
image:
tag: dev-latest
ingress:
enabled: true
host: my-api.dev.your-domain.com
autoscaling:
enabled: false
charts/my-api/values-staging.yaml:
replicaCount: 2
image:
tag: staging-latest
ingress:
enabled: true
host: my-api.staging.your-domain.com
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 5
charts/my-api/values-prod.yaml:
replicaCount: 3
ingress:
enabled: true
host: api.your-domain.com
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 20
resources:
limits:
cpu: 1000m
memory: 512Mi
requests:
cpu: 200m
memory: 256Mi
charts/my-api/templates/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Chart.Name }}
labels:
app: {{ .Chart.Name }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
app: {{ .Chart.Name }}
template:
metadata:
labels:
app: {{ .Chart.Name }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: {{ .Values.service.port }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
livenessProbe:
httpGet:
path: /health
port: {{ .Values.service.port }}
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: {{ .Values.service.port }}
initialDelaySeconds: 5
periodSeconds: 5
Step 6: 建立 ArgoCD Application
apps/dev/my-api.yaml:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-api-dev
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/your-org/platform-config.git
targetRevision: main
path: charts/my-api
helm:
valueFiles:
- values.yaml
- values-dev.yaml
destination:
server: https://kubernetes.default.svc
namespace: dev
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
# 建立 Application
kubectl apply -f apps/dev/my-api.yaml
# 或用 CLI
argocd app create my-api-dev \
--repo https://github.com/your-org/platform-config.git \
--path charts/my-api \
--dest-server https://kubernetes.default.svc \
--dest-namespace dev \
--helm-set-file values=values-dev.yaml \
--sync-policy automated
Step 7: 驗證部署
# 查看 ArgoCD 應用狀態
argocd app list
argocd app get my-api-dev
# 查看 K8s 資源
kubectl get all -n dev
# 觸發 sync(如果沒開 auto sync)
argocd app sync my-api-dev
# 查看 sync 歷史
argocd app history my-api-dev
# 回滾到上一版
argocd app rollback my-api-dev 1
六、自助操作入口
Makefile(最簡單的 CLI 入口)
scripts/Makefile:
.PHONY: help deploy-dev deploy-staging create-preview sync rollback logs
ARGOCD_SERVER ?= localhost:8080
help:
@echo "Available commands:"
@echo " make deploy-dev - Deploy to dev environment"
@echo " make deploy-staging - Deploy to staging (requires PR approval)"
@echo " make create-preview - Create preview environment for PR"
@echo " make sync APP=my-api - Force sync an application"
@echo " make rollback APP=my-api REV=1 - Rollback to revision"
@echo " make logs APP=my-api - View application logs"
deploy-dev:
@echo "Syncing dev environment..."
argocd app sync my-api-dev --server $(ARGOCD_SERVER)
deploy-staging:
@echo "⚠️ Staging deployment requires PR approval"
@echo "Please create a PR to update charts/my-api/values-staging.yaml"
create-preview:
@read -p "Enter PR number: " PR_NUM; \
./create-preview-env.sh $$PR_NUM
sync:
@if [ -z "$(APP)" ]; then echo "Usage: make sync APP=<app-name>"; exit 1; fi
argocd app sync $(APP) --server $(ARGOCD_SERVER)
rollback:
@if [ -z "$(APP)" ] || [ -z "$(REV)" ]; then echo "Usage: make rollback APP=<app-name> REV=<revision>"; exit 1; fi
argocd app rollback $(APP) $(REV) --server $(ARGOCD_SERVER)
logs:
@if [ -z "$(APP)" ]; then echo "Usage: make logs APP=<app-name>"; exit 1; fi
kubectl logs -l app=$(APP) -n dev --tail=100 -f
Preview Environment 腳本
scripts/create-preview-env.sh:
#!/bin/bash
set -e
PR_NUM=$1
if [ -z "$PR_NUM" ]; then
echo "Usage: $0 <pr-number>"
exit 1
fi
NAMESPACE="preview-pr-${PR_NUM}"
APP_NAME="my-api-pr-${PR_NUM}"
echo "Creating preview environment for PR #${PR_NUM}..."
# 建立 ArgoCD Application
cat <<EOF | kubectl apply -f -
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ${APP_NAME}
namespace: argocd
labels:
preview: "true"
pr: "${PR_NUM}"
spec:
project: default
source:
repoURL: https://github.com/your-org/platform-config.git
targetRevision: pr-${PR_NUM}
path: charts/my-api
helm:
valueFiles:
- values.yaml
- values-dev.yaml
parameters:
- name: ingress.host
value: pr-${PR_NUM}.preview.your-domain.com
destination:
server: https://kubernetes.default.svc
namespace: ${NAMESPACE}
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
EOF
echo "✅ Preview environment created!"
echo " URL: https://pr-${PR_NUM}.preview.your-domain.com"
echo " Namespace: ${NAMESPACE}"
echo ""
echo "To delete this preview environment:"
echo " argocd app delete ${APP_NAME}"
七、加入監控(可選但推薦)
安裝 Prometheus + Grafana
# 使用 kube-prometheus-stack(一鍵全裝)
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install monitoring prometheus-community/kube-prometheus-stack \
--namespace monitoring \
--create-namespace \
--set grafana.adminPassword=your-password
建立基本 Dashboard
登入 Grafana 後,匯入這些社群 Dashboard(ID 輸入即可):
| Dashboard ID | 名稱 | 用途 |
|---|---|---|
| 315 | Kubernetes cluster monitoring | 整體 cluster 狀態 |
| 6417 | Kubernetes Pods | Pod 層級 metrics |
| 1860 | Node Exporter Full | Node 硬體資源 |
| 14055 | ArgoCD | ArgoCD 運作狀態 |
八、權限控管(RBAC)
最小權限原則設定
rbac/developer-role.yaml:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: dev
name: developer
rules:
- apiGroups: [""]
resources: ["pods", "pods/log", "services", "configmaps"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"] # 允許 debug
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: developer-binding
namespace: dev
subjects:
- kind: Group
name: developers
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: developer
apiGroup: rbac.authorization.k8s.io
ArgoCD RBAC
argocd-rbac-cm.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-rbac-cm
namespace: argocd
data:
policy.csv: |
# Developers 可以 sync dev 環境,只能看 staging/prod
p, role:developer, applications, get, */*, allow
p, role:developer, applications, sync, */*-dev, allow
p, role:developer, logs, get, */*, allow
# DevOps 可以操作所有環境
p, role:devops, applications, *, */*, allow
p, role:devops, clusters, *, *, allow
# 綁定群組
g, developers, role:developer
g, devops-team, role:devops
policy.default: role:readonly
九、常見問題與解法
Q1: ArgoCD sync 失敗怎麼辦?
# 查看詳細錯誤
argocd app get my-api-dev --show-operation
# 查看 resource diff
argocd app diff my-api-dev
# 強制 sync(小心使用)
argocd app sync my-api-dev --force
Q2: Helm values 沒生效?
# 確認 ArgoCD 讀到的 values
argocd app manifests my-api-dev | head -50
# 本地測試 Helm render
helm template my-api ./charts/my-api -f charts/my-api/values-dev.yaml
Q3: 怎麼處理 Secrets?
方法 A:Sealed Secrets(推薦)
# 安裝 Sealed Secrets controller
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm install sealed-secrets sealed-secrets/sealed-secrets -n kube-system
# 加密 secret
kubeseal --format yaml < my-secret.yaml > my-sealed-secret.yaml
# 把 sealed secret commit 到 Git(安全的)
方法 B:External Secrets Operator
如果用 Vault / AWS Secrets Manager:
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets -n external-secrets --create-namespace
Q4: CI/CD 怎麼整合?
典型流程:
Developer PR → GitHub Actions build image → push to registry →
update image tag in Git → ArgoCD auto sync
github-actions-workflow.yaml:
name: Build and Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push Docker image
run: |
docker build -t ${{ secrets.REGISTRY }}/my-api:${{ github.sha }} .
docker push ${{ secrets.REGISTRY }}/my-api:${{ github.sha }}
- name: Update image tag in Git
run: |
git clone https://github.com/your-org/platform-config.git
cd platform-config
sed -i "s/tag: .*/tag: ${{ github.sha }}/" charts/my-api/values-dev.yaml
git commit -am "chore: update my-api to ${{ github.sha }}"
git push
十、進階:加入自助服務層(方案 B)
當團隊超過 10 人,或新人頻繁加入時,可以加一層更友好的自助入口:
Service Template Repository
建立一個標準的服務模板:
# 用 cookiecutter 或直接 GitHub Template Repo
platform-templates/
├── README.md
├── svc-go-api/ # Go API 模板
│ ├── cookiecutter.json
│ ├── {{cookiecutter.name}}/
│ │ ├── Dockerfile
│ │ ├── main.go
│ │ ├── helm/
│ │ └── .github/workflows/
├── svc-node-api/ # Node.js API 模板
└── svc-python-worker/ # Python Worker 模板
開發者只需要:
# 建立新服務
cookiecutter https://github.com/your-org/platform-templates --directory svc-go-api
# 輸入參數
service_name [my-service]: payment-api
port [8080]: 8080
owner [team-name]: platform-team
# 自動產生完整的服務 + Helm chart + CI/CD
這讓開發者感覺在「點平台」,但實際上只是產 YAML + Git commit。
十一、成本與時間估算
建置成本
| 階段 | 時間 | 人力 | 備註 |
|---|---|---|---|
| 安裝 ArgoCD + 基本設定 | 半天 | 1 人 | 直接 Helm install |
| 建立 Helm Chart 模板 | 1-2 天 | 1 人 | 複製現有服務改 |
| Git Repo 結構 + CI 整合 | 1 天 | 1 人 | GitHub Actions 直接用 |
| 文件 + 團隊培訓 | 1-2 天 | 1 人 | README + 小 workshop |
| 總計 | 1-2 週 | 1-2 人 | MVP 上線 |
維運成本
| 項目 | 成本 |
|---|---|
| 軟體授權 | 免費(全 OSS) |
| 額外硬體 | ArgoCD 約需 1 core + 1GB RAM |
| DevOps 日常維護 | ~2-4 hr/week(處理 edge case) |
與其他方案比較
| 方案 | 建置成本 | 維運成本 | 自助程度 |
|---|---|---|---|
| Lean GitOps IDP(本文) | 低(1-2 週) | 低 | 中(CLI/UI) |
| Backstage | 高(1-3 月) | 高(專人維護) | 高(完整 Portal) |
| 商用 PaaS(Heroku, Render) | 低 | 高(按量計費) | 高 |
| 純手動 | 無 | 高(每次都要 DevOps) | 無 |
十二、迭代路徑建議
根據團隊規模和成熟度,建議的演進路徑:
階段一:Lean GitOps MVP(本文)
├── ArgoCD + Helm + Makefile
├── 驗證 2-3 個服務的 deploy 流程
└── 目標:2 週上線
↓ 如果團隊反饋「還要手改太多 YAML」
階段二:加自助層
├── Service Template Repo
├── cookiecutter / GitHub Template
└── 目標:再 +1 週
↓ 如果需要管理 RDS/S3 等基礎設施
階段三:IaC 擴張
├── Crossplane 或 Terraform + GitOps
├── 治理範圍擴大到雲資源
└── 目標:視複雜度 2-4 週
⚠️ 不要跳級:階段一沒驗證成功就直接跳到階段三,會讓團隊同時承受「學 GitOps」+「學 IaC」的雙重壓力。
結論:夠用就好
對於 ≤20 人、服務數 ≤30 的團隊,這套 Lean GitOps IDP 是最佳投報比的選擇:
| ✅ 優點 | ❌ 不適合 |
|---|---|
| 成本低(全 OSS) | 需要完整 Portal 的大團隊 |
| 心智模型清楚(Git 就是 SSOT) | 開發者完全不碰 YAML |
| 不會把 DevOps 變成平台 PM | 需要多租戶隔離 |
| 1-2 週落地 | 合規要求極高 |
核心心法:
- 先讓 GitOps 跑起來,再談自助入口
- 用 PR 取代審批系統,Git history 就是 audit log
- 不要為了「IDP 完整性」犧牲可用性
如果你的團隊已經有 Kubernetes 經驗,這套方案能讓你在不增加太多複雜度的情況下,解決 Docker Compose 的痛點,同時為未來擴展留好空間。
附錄:指令速查表
| 任務 | 指令 |
|---|---|
| 安裝 ArgoCD | kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml |
| 取得 admin 密碼 | kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d |
| 登入 CLI | argocd login <server> --username admin --password <password> |
| 查看所有 App | argocd app list |
| 同步 App | argocd app sync <app-name> |
| 回滾 | argocd app rollback <app-name> <revision> |
| 查看 diff | argocd app diff <app-name> |
| 刪除 App | argocd app delete <app-name> |
| Helm 本地 render | helm template <name> <chart-path> -f <values-file> |