Ian Chou's Blog

私有雲小型 IDP 實戰:用 ArgoCD + Helm 打造 Lean GitOps 開發者平台

前言:小團隊為什麼需要 IDP?

Internal Developer Platform (IDP) 這個詞聽起來很重,總讓人聯想到 Backstage、平台團隊、Plugin 地獄。但對已經有 Kubernetes 經驗的小團隊來說,IDP 的本質很簡單:

讓開發者能自助完成「建環境、部署、Scale」這些日常操作,而不用每次都找 DevOps。

如果你的團隊目前還在用 Docker Compose 管理開發/測試環境,很可能已經遇到這些痛點:

這篇文章要告訴你:用現成開源工具,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,核心理念是:

  1. Git 是唯一事實來源 (Single Source of Truth)
  2. ArgoCD 是部署控制器 + UI
  3. Helm/Kustomize 是服務定義 DSL
  4. 沒有自己寫的 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 時

四、前置條件

在開始之前,確認你有:

基礎設施

網路需求

權限

# 確認環境
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 週落地 合規要求極高

核心心法

  1. 先讓 GitOps 跑起來,再談自助入口
  2. 用 PR 取代審批系統,Git history 就是 audit log
  3. 不要為了「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>

參考資源