Appearance
Azure Pipeline — Kubeconfig 隔離最佳實踐
在 Azure Pipeline self-hosted runner 上,以
Agent.TempDirectory+KUBECONFIG環境變數取代~/.kube/config,避免多 job 並行時的 race condition 與憑證殘留。
概述
Azure DevOps Pipeline 搭配 self-hosted runner 部署 K8s 時,常見的做法是將 kubeconfig decode 後寫入 ~/.kube/config。問題在於 ~ 指向 runner host 的真實 home 目錄,屬於全域共用路徑:多條 pipeline 同時執行時,後寫入的 config 會覆蓋前一個(race condition);job 結束後憑證檔案仍殘留在 host(安全疑慮);下一個 job 可能誤用上一次留下的 cluster config(環境污染)。
解決方式是使用 $(Agent.TempDirectory) 下的唯一路徑,透過 KUBECONFIG 環境變數讓 kubectl 讀取,並在 job 結束時(含失敗情境)強制清理。此方案不修改任何 host 全域設定,天然隔離多個並行 job。
核心內容
Agent.TempDirectory 的隔離特性
Azure Pipeline 內建變數,指向 agent 為本次 job 配置的臨時目錄(例如 /home/azureuser/agent/_temp)。重要特性:
- 每個 job 有獨立路徑,天然隔離
- Job 結束後 agent 會自動清理目錄內容
- 不影響 host 全域的
~/.kube/config
以 $(Build.BuildId) 為檔名後綴可進一步確保唯一性,防止同一 agent 上兩個 job 在極端情況下撞到同一路徑。
四個關鍵技術細節
chmod 600:kubectl 在部分環境下若 kubeconfig 權限為 644(其他用戶可讀)會拒絕讀取或發出警告,設為 600 確保只有當前用戶可讀。
##vso[task.setvariable]:Azure Pipeline 的 shell 變數無法跨 step 存活。需用此指令將值寫入 pipeline 變數,後續 step 以 $(KUBECONFIG_PATH) 引用。
env: KUBECONFIG: $(KUBECONFIG_PATH):只在該 step 的 process 中有效,不影響 host 或其他 step。這是讓不同 job 使用不同 cluster config 的核心手段。
condition: always():確保即使 deploy step 失敗,cleanup step 仍然執行,避免憑證殘留。
與舊寫法的對比
舊寫法(~/.kube/config) | 新寫法(temp path) | |
|---|---|---|
| 路徑 | Host 全域共用 | Job 專屬臨時路徑 |
| 並行安全 | ❌ 會互相覆蓋 | ✅ 以 BuildId 隔離 |
| 憑證殘留 | ❌ 執行後仍存在 | ✅ condition: always() 清理 |
| 多 cluster 支援 | ❌ 需手動切換 context | ✅ 每個 job 各自指定 |
| Host 環境污染 | ❌ 修改全域設定 | ✅ 不影響 host |
關鍵要點
~/.kube/config在 self-hosted runner 是全域共用路徑,多 pipeline 並行時必然衝突$(Agent.TempDirectory)/kubeconfig-$(Build.BuildId)提供天然的 job 層級隔離env:層級的KUBECONFIG變數只在該 step 有效,不污染環境condition: always()確保 cleanup 在任何情況(成功/失敗/取消)下都執行- Microsoft-hosted agent 每次都是乾淨環境,舊寫法也可接受;但統一採用本方案是更好的規範
實際應用
適用情境:
- 長期常駐的 self-hosted runner(最需要隔離)
- 多個 pipeline 共用同一台 runner
- 同一台 runner 需要連接不同 K8s cluster
若使用 Microsoft-hosted agent 或每次 job 起一台全新 VM,~/.kube/config 的寫法也可接受,但仍建議採用本方案作為統一規範,避免日後切換到 self-hosted 時需要重構。
部署設定參考
以下為完整的 Azure Pipeline YAML 範例,供日後查詢與複製使用。
完整 Pipeline YAML
yaml
- stage: Deploy
displayName: Deploy
dependsOn: Build
jobs:
- job: DeployNorway
displayName: Deploy Norway
pool:
name: Default
steps:
- task: KubectlInstaller@0
inputs:
kubectlVersion: 1.24.0
displayName: "Install kubectl"
- task: AzureKeyVault@2
displayName: "Azure Key Vault: Get Secrets"
inputs:
azureSubscription: $(keyvaultServiceConnection)
KeyVaultName: "bj0600"
SecretsFilter: "*"
RunAsPreJob: false
# ✅ 寫入 temp 路徑,並透過 vso 指令傳遞給後續 steps
- script: |
KUBECONFIG_PATH="$(Agent.TempDirectory)/kubeconfig-$(Build.BuildId)"
echo -n $(K8S-CONFIG-BASE64) | base64 -d > "$KUBECONFIG_PATH"
chmod 600 "$KUBECONFIG_PATH"
echo "##vso[task.setvariable variable=KUBECONFIG_PATH]$KUBECONFIG_PATH"
displayName: "Decode kubeconfig to temp"
# --- Deploy for norway-dev branch ---
- ${{ if eq(variables['Build.SourceBranchName'], 'norway-dev') }}:
- script: |
kubectl create secret generic norway-dev-env \
--from-env-file=norway_server/.env.development \
-n default -o yaml --dry-run=client | kubectl apply -f -
sed -e "s|AZURE_IMAGE|$(containerRegistry)/$(serverImageName):$(tag)|g" \
norway_server/k8s/dev/server.yaml | kubectl apply -f -
sed -e "s|AZURE_IMAGE|$(containerRegistry)/$(serverImageName):$(tag)|g" \
norway_server/k8s/dev/syncer.yaml | kubectl apply -f -
displayName: "Deploy norway-dev to k8s"
env:
KUBECONFIG: $(KUBECONFIG_PATH) # ✅ 明確指定,不依賴預設路徑
- script: |
sed -e "s|AZURE_IMAGE|$(containerRegistry)/$(webImageName):$(tag)|g" \
norway_server/k8s/dev/web.yaml | kubectl apply -f -
displayName: "Deploy norway-dev web to k8s"
env:
KUBECONFIG: $(KUBECONFIG_PATH)
# --- Deploy for norway branch ---
- ${{ if eq(variables['Build.SourceBranchName'], 'norway') }}:
- script: |
kubectl create secret generic norway-env \
--from-env-file=norway_server/.env.production \
-n default -o yaml --dry-run=client | kubectl apply -f -
sed -e "s|AZURE_IMAGE|$(containerRegistry)/$(serverImageName):$(tag)|g" \
norway_server/k8s/prd/server.yaml | kubectl apply -f -
sed -e "s|AZURE_IMAGE|$(containerRegistry)/$(serverImageName):$(tag)|g" \
norway_server/k8s/prd/syncer.yaml | kubectl apply -f -
displayName: "Deploy norway-prd to k8s"
env:
KUBECONFIG: $(KUBECONFIG_PATH)
- script: |
sed -e "s|AZURE_IMAGE|$(containerRegistry)/$(webImageName):$(tag)|g" \
norway_server/k8s/prd/web.yaml | kubectl apply -f -
displayName: "Deploy norway-prd web to k8s"
env:
KUBECONFIG: $(KUBECONFIG_PATH)
# ✅ 無論成功或失敗都清理憑證
- script: |
rm -f "$(KUBECONFIG_PATH)"
echo "kubeconfig cleaned up."
displayName: "Cleanup kubeconfig"
condition: always()Decode 步驟說明
bash
# kubeconfig 以 Base64 形式儲存在 Azure Key Vault,取出後解碼
KUBECONFIG_PATH="$(Agent.TempDirectory)/kubeconfig-$(Build.BuildId)"
echo -n $(K8S-CONFIG-BASE64) | base64 -d > "$KUBECONFIG_PATH"
chmod 600 "$KUBECONFIG_PATH"
echo "##vso[task.setvariable variable=KUBECONFIG_PATH]$KUBECONFIG_PATH"
echo -n去掉末尾換行符,避免 base64 解碼出錯。$(K8S-CONFIG-BASE64)來自 AzureKeyVault task 取回的 Secret。
相關概念
- Azure DevOps Pipeline CI/CD 設定指南 — Azure Pipeline 完整部署情境參考
- Kubernetes RBAC 帳號管理 — 生成限權 kubeconfig 的方法
- 驗證 Base64 Kubeconfig 有效性 — 部署前驗證 kubeconfig 是否有效
- 維運 SOP:憑證與帳密更新 — 更新 Azure Keyvault 中的 Base64 kubeconfig
- Azure Pipelines Self-Hosted Agent — Ubuntu 安裝與服務設定 — Self-Hosted Agent 在 Ubuntu 上的安裝與 systemd 常駐服務 SOP