iT邦幫忙

2026 iThome 鐵人賽

DAY 30
0
Kubernetes

不是背 YAML!30 天從零打造 Kubernetes 微服務:從本機實戰一路到 CKA系列 第 30 篇

Day 30|Final Project:用 GitHub Actions × Argo CD 串起 CI/CD、GitOps 與 k8s Troubleshooting

  • 分享至 

  • xImage
  •  

終於來到最後一天啦!真是辛苦大家了
程式碼也都可從這個 Repo 取得,懶得打就直接 clone 也可以 XD
https://ithelp.ithome.com.tw/upload/images/20261001/20168537sEP2GYJSx5.png
https://github.com/YIFUNLIN/k8s-30days

先來個小小感言:

原先其實沒有想說要比這個的,小弟目前在服替代役期間 生活乏味,
想耍廢但又深怕自己退伍後忘光光 無法銜接上工作
最後決定還是來比個鐵人賽,這樣才會 push 讓自己每天都要有產出才行 XD
大家的觀看是我繼續的動力!非常感謝 🥹
廢話不多說,讓我們趕快進入正題吧!


正文開始:

30 天前,我們可能還只是在問:

Kubernetes 到底是什麼?

但走到今天,我們已經實際碰過:

FastAPI
Redis
PostgreSQL
Docker
kind
Deployment
Service
ConfigMap
Secret
Probe
PVC
StatefulSet
Scheduler
Affinity
Taint
HPA
Job
CronJob
DaemonSet
Calico
NetworkPolicy
RBAC
Helm
Kustomize
Gateway API
CRD
Operator
kubelet
containerd
Static Pod
etcd

所以 Day 30 不打算再塞一個新的 Kubernetes Resource。

今天反而要做更重要的事情:

把前面 29 天學到的東西真正串起來。

最後再完成兩件事:

第一:把現在的 Kubernetes Project 串上真正的 DevOps Workflow。

第二:建立一套 Kubernetes 出問題時,知道應該從哪裡開始查的 Troubleshooting 方法。

而且今天不只細談所有概念。

也會手把手真的帶你完成一個 Final Lab,並附上實際可能遇到的一些問題和如何除錯:

今日 Lab 大致流程如下:

修改 FastAPI
↓
git push
↓
GitHub Actions
↓
Build Docker Image
↓
Push GHCR
↓
更新 Git 裡的 Kubernetes Desired State
↓
Argo CD 發現變更
↓
Sync 到 kind
↓
Kubernetes 自動部署新版本

把前面學過的:

Docker
GitHub Actions
GHCR
Kustomize
Argo CD
Kubernetes

串成一條完整 Pipeline。


實驗開始!

Part 1:我們現在的流程,其實還很手動

目前每次修改 FastAPI,大概會做:

修改程式碼
↓
docker build
↓
kind load
↓
kubectl apply

學 Kubernetes 時這完全沒有問題。

因為我們就是透過這些手動操作,理解 Image、Deployment、Service 到底在做什麼。

但正式開發環境不會希望工程師每次改一行程式,都手動:

Build
Push
Deploy

因此 DevOps 常見的流程會變成:

git commit
↓
Test
↓
Build
↓
Publish
↓
Deploy

這裡就會出現非常重要的概念:

CI/CD

https://ithelp.ithome.com.tw/upload/images/20260928/20168537RKaMwtEKSL.png

https://kucw.io/blog/cicd-intro/

CI,Continuous Integration,持續整合,意思是每當程式碼被提交後,自動進行 Test、Build 等工作。

CD,Continuous Delivery / Deployment,持續交付/持續部署,則負責把已經 Build 完的版本繼續送到實際環境。

我們今天就從 CI 開始。


那 GitHub Actions 是什麼?

https://ithelp.ithome.com.tw/upload/images/20260928/20168537nxHJPQnQ6G.jpg

GitHub Actions 是 GitHub 提供的自動化平台。

例如我們可以設定:

當 main branch 有新的 Commit
↓
GitHub 偵測到後,會自動啟動執行環境
↓
Checkout 程式碼
↓
Build Docker Image
↓
Push 到 Container Registry

負責真正執行 Workflow 的機器叫:

Runner

如果使用 GitHub 提供的 Runner,就是:

GitHub-hosted Runner

它是 GitHub 在雲端準備的一個執行環境。

https://ithelp.ithome.com.tw/upload/images/20260928/201685372D4M7Es1P3.png

https://kucw.io/blog/github-actions-intro/


先理解 Container Registry

以前我們建立:

cka-api:v4

這個 Image 其實只存在自己的 Mac。

所以才需要:

kind load docker-image \
  cka-api:v4 \
  --name cka-lab

把 Image 搬進 kind Node。

但如果 Kubernetes Cluster 在 AWS、GCP、Azure,Cloud 裡面的 Node 根本看不到我們 Mac 裡面的 Image。

真正的流程會變成:

Source Code
↓
Docker Build
↓
放到 Container Registry
↓
Kubernetes Pull Image

Container Registry 可以理解成:

專門存放 Container Image 的遠端倉庫。
https://ithelp.ithome.com.tw/upload/images/20260928/20168537nTkcoC4X81.png

Docker Hub、AWS ECR、Google Artifact Registry 都是 Container Registry。

今天我們直接使用 GitHub 提供的:

GitHub Container Registry

簡稱:

GHCR

網址:

ghcr.io

建立 GitHub Repository

如果專案目前還只存在自己的 Mac,要先把它放到 GitHub。

進入:

k8s-30days

初始化 Git:

git init

Push 前先建立 .gitignore

第一次 Commit 前,先建立:

vim .gitignore

.gitignore 的作用是告訴 Git:

哪些檔案不要加入版本控制。

我們之前做 etcd Restore 時,例如會產生:

etcd-restored/

這是將 backup.db restore 成 etcd 可使用的 data directroy (類似解壓縮到另一個新資料夾的概念),這樣 etcd 才可真的使用。

裡面有:

member/
snap/
wal/

其中 WAL 是:

Write-Ahead Log

屬於 etcd Runtime Data,不是 Source Code,而且檔案可能非常大。

因此可以加入:

# macOS
.DS_Store

# Python
__pycache__/
*.pyc
.venv/
venv/

# Secrets
.env

# etcd restore data
etcd-restored/

# etcd snapshot
backup.db
snapshot.db

尤其:

.env

更不應該 Push,因為裡面可能有 Token、Password、API Key 等敏感資料。


第一次 Push 到 GitHub

建立 Commit:

git add .
git commit -m "initial commit"

把主要 Branch 統一為:

git branch -M main

接著到 GitHub:

GitHub
→ New repository
→ Repository name:k8s-30days
→ Create repository

建立完成後,把本機接到 GitHub:

git remote add origin \
  https://github.com/YIFUNLIN/k8s-30days.git

確認:

git remote -v

第一次 Push:

git push -u origin main

之後通常只需要:

git push

即可。

現在關係已經變成:

Mac
↓
Local Git
↓
GitHub Repository

(Debug) 如果出現 Large File Warning

若有先做上面那步,這邊可跳過

一開始我沒有將 etcd-restored/ 加到 .gitignore,直接 Push 時,就遇到:

warning: File etcd-restored/...wal is 61 MB
GH001: Large files detected

https://ithelp.ithome.com.tw/upload/images/20260928/20168537aK1hFxvW94.png

太大啦,不能推上去

所以我們現在要把它從 Git Tracking 移除,但保留 Mac 上的檔案:

git rm -r --cached etcd-restored

如果 Repository 才剛建立,可以直接修改上一個 Commit:

git add .gitignore
git commit --amend --no-edit

再:

git push --force-with-lease origin main

最後變成:

Mac
└── etcd-restored/     ← 還存在

Git
└── 不追蹤

GitHub
└── 不保存 Runtime Data

這也是一個很實際的 DevOps 習慣:

Source Code、Config 可以進 Git;Secret、Log、Backup、Runtime Data 通常不應該直接進 Git。


建立第一個 GitHub Actions Workflow: build.yaml

建立:

mkdir -p .github/workflows

記得是 workflows,有 s !!!
不然 github action 會讀不到哦

再建立:

vim .github/workflows/build.yaml

目錄會變成:

.github/
└── workflows/
    └── build.yaml

GitHub 會自動辨識:

.github/workflows/

裡面的 Workflow YAML。

先建立 build.yaml

vim .github/workflows/build.yaml

裡面:

name: Build API

on:
  push:
    branches:
      - main

permissions:
  contents: read
  packages: write

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Login to GHCR
        run: |
          echo "${{ secrets.GITHUB_TOKEN }}" \
          | docker login ghcr.io \
            -u "${{ github.actor }}" \
            --password-stdin

      - name: Build
        run: |
          OWNER=$(
            echo "${GITHUB_REPOSITORY_OWNER}" \
            | tr '[:upper:]' '[:lower:]'
          )

          docker build \
            -t ghcr.io/${OWNER}/cka-api:${GITHUB_SHA} \
            ./app

      - name: Push
        run: |
          OWNER=$(
            echo "${GITHUB_REPOSITORY_OWNER}" \
            | tr '[:upper:]' '[:lower:]'
          )

          docker push \
            ghcr.io/${OWNER}/cka-api:${GITHUB_SHA}

解釋:它是最基本的 GitHub Actions Workflow

這份 YAML 可以把它理解成「告訴 GitHub 什麼時候要做事,以及要做哪些事」。

  • name: Build API 這個 Workflow 顯示在 Actions 頁面的名稱;
  • on.push.branches: main 則代表只要有人 Push 到 main Branch,就觸發這套流程。
  • permissions 是設定這次 Workflow 拿到的 GITHUB_TOKEN 有哪些權限,其中 contents: read 讓它讀取 Repository,packages: write 則讓它可以把 Docker Image Push 到 GHCR。
  • 真正執行工作的地方是 jobs。這裡只有一個叫 build 的 Job,runs-on: ubuntu-latest 代表 GitHub 會準備一台 Ubuntu Runner。
    • 底下的 steps 就是這台 Runner 依序做的事情:先用 actions/checkout 把程式碼抓下來,再登入 GHCR
    • 接著 docker build 建立 Image
    • docker push 上傳。Image Tag 使用 ${GITHUB_SHA},也就是觸發這次 Workflow 的 Git Commit SHA,因此之後可以從 Image 反查到 Source Code。

換句話說,這份 YAML 可以先讀成:

Push main
→ 開 Ubuntu Runner
→ 抓程式碼
→ 登入 GHCR
→ Build Image
→ Push Image 到 hub上

建立好 build.yaml 後,就來 Push 啦:

git add .
git commit -m "add github actions workflow"
git push

接著到:

GitHub Repository
→ Actions

就可以看到 Workflow。

現在:

git push
↓
GitHub Actions
↓
Docker Build
↓
GHCR

已經自動化了。


GITHUB_TOKEN 又是什麼?

這裡我們沒有自己建立 Password。

GitHub Actions 執行 Workflow 時,GitHub 會自動提供:

GITHUB_TOKEN

它是一個暫時性的 Token。

例如:

permissions:
  contents: read
  packages: write

代表:

可以讀 Repository
+
可以寫 GitHub Packages / GHCR

因此 Workflow 才有權限:

docker push ghcr.io/...

為什麼 image version 不要一直用 latest?

假設 Production 現在跑:

cka-api:latest

凌晨出問題。

你第一個問題可能就是:

這到底是哪一版程式?

所以我們改成:

cka-api:<Git SHA>

Git SHA 是 Git Commit 的唯一識別碼。

因此可以一路追:

Running Image
↓
Image Tag
↓
Git SHA
↓
Git Commit
↓
Source Code

這種能力叫:

Traceability

也就是:

可追溯性。


(非必要) Apple Silicon 還有一個問題

我們現在的 kind Cluster 跑在 Apple Silicon Mac。

因此 Node 通常是:

arm64

GitHub-hosted Runner Build 出來的 Image,則可能是:

linux/amd64

所以更完整的做法是建立:

Multi-Architecture Image

讓同一個 Image 同時支援:

linux/amd64
linux/arm64

等等 Final Lab 我們就直接把這件事一起處理掉。


為什麼 GitHub Actions 不能直接 Deploy 到我的 kind?

這是一個非常重要的觀念。

我們的:

kind Cluster

存在 Mac。

GitHub Actions Runner 卻存在:

GitHub Cloud

所以:

GitHub Runner
       │
       │ Internet
       │
       ✕
       │
Mac localhost
       │
       ▼
kind Cluster

它們不是同一台機器。

即使把:

kubeconfig

放進 GitHub Secret,Runner 看到的:

localhost
127.0.0.1

也是 Runner 自己。

不是我們的 Mac。

因此我們不讓 GitHub Actions 直接 Push Deployment 到本機 kind。

這時就輪到:

GitOps

登場。


GitOps 到底是什麼?

https://ithelp.ithome.com.tw/upload/images/20260928/20168537udkaSBngH0.png

https://picluster.ricsanfre.com/docs/argocd/

傳統 Deployment 比較像:

CI
↓
kubectl apply
↓
Cluster

CI 主動把東西 Push 進 Kubernetes。

GitOps 則反過來:

Git Repository
      ▲
      │ Compare
      │
   Argo CD
      │
      ▼
Kubernetes Cluster

Argo CD 是 GitOps Controller。

它直接跑在 Kubernetes 裡,不斷比較:

Git Desired State

與:

Cluster Actual State

如果不同,就:

Sync

讓 Cluster 回到 Git 描述的狀態。

是不是非常熟悉?

因為 Kubernetes 從 Day 1 就一直在做:

Desired State
vs
Actual State

Part 2:Final DevOps Lab

接下來不只停留在概念。

我們真的把目前的:

FastAPI
+
GitHub Actions
+
GHCR
+
Kustomize
+
Argo CD
+
kind

全部串起來。

最終希望完成:

Developer
   │
   │ git push
   ▼
GitHub
   │
   ▼
GitHub Actions
   │
   ├── Build Image
   │
   └── Push GHCR
   │
   ▼
更新 Kustomize newTag
   │
   ▼
Git Repository
   ▲
   │ Watch
   │
Argo CD
   │
   ▼
kind Kubernetes
   │
   ▼
cka-api 新版本

這就是今天真正的 Final Lab。


Step 1:先整理 Kustomize

我們前面已經有:

deploy/
├── base/
│   ├── api.yaml
│   └── kustomization.yaml
│
└── overlays/
    ├── local/
    │   └── kustomization.yaml
    └── production/
        └── kustomization.yaml

今天先讓 Argo CD 管理:

deploy/overlays/local

也就是我們現在的 kind Environment。

修改:

vim deploy/overlays/local/kustomization.yaml

例如:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - ../../base

replicas:
  - name: api
    count: 1

images:
  - name: cka-api
    newName: ghcr.io/yifunlin/cka-api
    newTag: bootstrapi
      

告訴 Kustomize「Local 環境要改哪些東西」

這份 YAML 不負責建立一套全新的 Deployment,而是在修改 base。

  • apiVersion 和 kind: Kustomization 是告訴 Kustomize「這是一份 Kustomize 設定」
  • resources: ../../base 則表示先把 deploy/base 裡原本的 Kubernetes Resource 拿進來,再針對 Local 環境做調整。
  • replicas 表示 Local 環境的 api Deployment 只需要 1 個 Pod;
  • images 則是在改 Image。
    • name: cka-api 是要尋找 Base 裡原本使用的 cka-api,找到之後把它改成 ghcr.io/yifunlin/cka-api,再由 newTag 決定實際版本。也就是 Base 可以繼續保持通用設定,而 Local Overlay 只負責描述「這個環境和 Base 有哪些不同」。

所以這份設定真正表達的是:

先使用 Base
↓
Local 環境 replicas 改成 1
↓
Image 改從 GHCR 取得
↓
使用指定的 newTag

目前寫的是 newTag: bootstrapi;如果這只是初始化用的 Placeholder,要注意 GHCR 必須真的有這個 Tag,否則在 GitHub Actions 更新成真正的 Git SHA 之前,Kubernetes 會 Pull 不到它。

這裡非常重要。

假設 Base Deployment 原本:

containers:
  - name: api
    image: cka-api:v4

Kustomize 會找到:

cka-api

並把它換成:

ghcr.io/yifunlin/cka-api:<新的 Git SHA>

所以我們不用每一次 Build 都直接修改:

deployment.yaml

只需要更新:

newTag

Step 2:把 GitHub Actions 升級成完整 CI Workflow: 完整版 build.yaml:把 CI 和 GitOps 接起來

前面的 Workflow 只有:

Build
↓
Push

但 GitOps 還缺一件事情:

Git 裡面的 Kubernetes Desired State 也要知道新的 Image Version。

所以把 build.yaml 的 Workflow 再改成:

name: Build API

on:
  push:
    branches:
      - main
    paths:
      - "app/**"
      - ".github/workflows/build.yaml"

permissions:
  contents: write
  packages: write

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Set image name
        id: vars
        run: |
          OWNER=$(
            echo "${GITHUB_REPOSITORY_OWNER}" \
            | tr '[:upper:]' '[:lower:]'
          )

          echo "image=ghcr.io/${OWNER}/cka-api" \
            >> "$GITHUB_OUTPUT"

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and Push
        uses: docker/build-push-action@v6
        with:
          context: ./app
          platforms: linux/amd64,linux/arm64
          push: true
          tags: |
            ${{ steps.vars.outputs.image }}:${{ github.sha }}

      - name: Update Kustomize Image Tag
        run: |
          sed -i \
            "s/newTag: .*/newTag: ${GITHUB_SHA}/" \
            deploy/overlays/local/kustomization.yaml

      - name: Commit GitOps Change
        run: |
          git config user.name "github-actions[bot]"
          git config user.email \
            "41898282+github-actions[bot]@users.noreply.github.com"

          git add deploy/overlays/local/kustomization.yaml

          git diff --cached --quiet \
            && exit 0

          git commit \
            -m "chore: deploy ${GITHUB_SHA}"

          git push

完整版 build.yaml:把 CI 和 GitOps 接起來

第二份 GitHub Actions YAML 是前面簡單版本的升級版。最大的不同,是它不再只是「Build Image → Push GHCR」,而是 Build 完之後還會去修改 Kustomize,所以整條 Pipeline 才真正能和 Argo CD 接起來。
你的 paths 限制 Workflow 主要只有在 app/** 或 Workflow 本身變動時才觸發,避免 GitHub Actions 自己修改 kustomization.yaml 後,又再次觸發 Build,形成循環。因為這次還要 Commit 回 Repository,所以 contents 也從 read 改成了 write。

Set image name 這一步先組出 ghcr.io/.../cka-api,並透過 $GITHUB_OUTPUT 保存給後面的 Step 使用。接著 QEMU 和 Docker Buildx 是為了建立 Multi-Architecture Image,因此同時 Build linux/amd64 和 linux/arm64,你的 Apple Silicon kind 才能使用同一份 Image。
Build and Push 則把這個 Image 直接 Push 到 GHCR,而且 Tag 使用 ${{ github.sha }}。

後面才是 GitOps 最重要的部分。sed 會把:

newTag: 舊版本

直接改成這次新的 ${GITHUB_SHA}。接著 GitHub Actions 用 github-actions[bot] 的身份 Commit 這個變更,再 git push 回 GitHub。於是 Git Repository 裡描述的 Desired State 從「舊 Image」變成「新 Image」,Argo CD 才有東西可以偵測。

所以不要把這份 YAML 看成一大堆指令,它本質上只有:

Code 改變
↓
Build 新 Image
↓
Push GHCR
↓
Kustomize newTag 改成新 SHA
↓
Commit 回 Git
↓
等待 Argo CD 接手

這次多做了三件重要的事情。

第一:

Build AMD64 + ARM64

因此我們的 Apple Silicon kind 可以正常 Pull。

第二:

Image Tag = Git SHA

保有 Traceability。

第三,也是 GitOps 最重要的地方:

Build 完 Image
↓
修改 Kustomize newTag
↓
Commit 回 GitHub

因此 Git Repository 裡面的 Desired State 真的跟著版本一起變了。


為什麼 Trigger 要限制 app/**?

我們現在設定:

paths:
  - "app/**"
  - ".github/workflows/build.yaml"

所以主要只有:

Application Source Code

改變時才 Build 新 Image。

GitHub Actions 後面自己修改:

deploy/overlays/local/kustomization.yaml

不需要再次 Build Image。

否則很容易變成:

Workflow 改 Git
↓
Git 又觸發 Workflow
↓
Workflow 又改 Git
↓
...

把 Trigger 範圍縮小,可以讓 Pipeline 的責任更清楚。


Step 3:第一次讓 GitHub Actions Build Image

Push:

git add .
git commit -m "setup gitops pipeline"
git push

到:

GitHub Repository
→ Actions

應該可以看到:

Build API

https://ithelp.ithome.com.tw/upload/images/20260928/20168537QbjYSTYmbW.png
會長得像這樣!有東西在跑 (名稱跟我不同沒關係!.筆者實作時有小修正commit一版)

開始執行。

https://ithelp.ithome.com.tw/upload/images/20260928/201685378yZ21w8mSn.png

成功後,到 GitHub 的:

Packages

https://ithelp.ithome.com.tw/upload/images/20260928/20168537iu1nGYMlG4.png

應該會看到:

cka-api

裡面的版本 Tag 會是一串:

Git SHA

https://ithelp.ithome.com.tw/upload/images/20260928/20168537kb6i1Lmo9O.png

Step 4:讓 kind 可以 Pull GHCR Image

第一次建立 GHCR Package 時,它可能不是公開狀態。

為了讓這個 Lab 簡單一點,可以去 package management setting 把:

cka-api

Container Package 設定為:

Public

https://ithelp.ithome.com.tw/upload/images/20260928/20168537fFPRqDFFpo.png

https://ithelp.ithome.com.tw/upload/images/20260928/20168537mkTCemofV9.png

這樣 kind 就可以直接:

docker pull

而不需要額外建立 Registry Credential。

如果之後正式環境使用 Private Registry,再進一步使用:

imagePullSecrets

即可。


Step 5:安裝 Argo CD

如果 Day 27 已經安裝,可跳過這兩個指令(建立 namespace 和 install ArgoCD)。

先建立 Namespace:

kubectl create namespace argocd

安裝 Argo CD:

kubectl apply \
  -n argocd \
  --server-side \
  --force-conflicts \
  -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

觀察:

kubectl get pods \
  -n argocd \
  -w

https://ithelp.ithome.com.tw/upload/images/20260928/20168537nfGT8GfJoe.png

等主要 Component 逐漸:

Running

代表 Argo CD 已經起來。

而因為 Argo CD Server 預設沒有直接暴露到你的 Mac 外面,所以如果你想開:
https://localhost:8080 去看 Argo CD Web UI,
還是需要 Port Forward。這也是官方提供的存取方式之一。

Port Forward:

kubectl port-forward \
  svc/argocd-server \
  -n argocd \
  8080:443

瀏覽器進:

https://localhost:8080

登入帳號:

admin

Password 就是剛才取得的 Initial Password。

如果你從來沒改過 Argo CD 的 admin 密碼,可以直接從 Kubernetes 裡的初始密碼 Secret 取出來。

執行:

kubectl -n argocd \
  get secret argocd-initial-admin-secret \
  -o jsonpath="{.data.password}" \
  | base64 -d
echo

會直接印出初始密碼。

然後登入:

Username: admin
Password: 剛剛輸出的密碼

就成功進去啦

https://ithelp.ithome.com.tw/upload/images/20260928/20168537pqRcBgbod0.png


Step 6:建立 Argo CD Application: cka-api-app.yaml

現在要告訴 Argo CD:

我要你監控哪一個 Git Repository 的哪一個資料夾?

先建立:

mkdir -p argocd

建立:

vim argocd/cka-api-app.yaml

內容:

apiVersion: argoproj.io/v1alpha1
kind: Application

metadata:
  name: cka-api
  namespace: argocd

spec:
  project: default

  source:
    repoURL: https://github.com/YIFUNLIN/k8s-30days.git
    targetRevision: main
    path: deploy/overlays/local

  destination:
    server: https://kubernetes.default.svc
    namespace: cka-lab

  syncPolicy:
    automated:
      prune: true
      selfHeal: true

    syncOptions:
      - CreateNamespace=true

告訴 Argo CD「Git 在哪裡,要部署去哪裡」

這份 YAML 和前面兩份最大的差別,是它不是 GitHub Actions 設定,也不是 Kustomize 設定,而是 Argo CD 的 Application Resource。

  • apiVersion: argoproj.io/v1alpha1 和 kind: Application 表示這是一個 Argo CD 安裝 CRD 後才認得的 Custom Resource。
  • metadata 裡則把這個 Application 命名成 cka-api,並建立在 argocd Namespace。
  • source 描述的是 Desired State 從哪裡來。這裡告訴 Argo CD 去 YIFUNLIN/k8s-30days Repository,看 main Branch 裡的 deploy/overlays/local。Argo CD 會 Render 這個 Kustomize 目錄,算出最後應該存在的 Deployment、Service 等 Kubernetes Resource。
  • destination 則回答相反的問題:算完之後要部署去哪裡? https://kubernetes.default.svc 看起來很像一般網址,但它其實不是 Internet 網址。它是 Kubernetes Cluster 內部的 Service DNS 名稱。代表 Argo CD 自己所在的這個 Kubernetes Cluster,而 namespace: cka-lab 代表最後把 Application 部署到 cka-lab Namespace。
  • 最後的 syncPolicy 決定同步策略。
  • automated 代表不用每次手動按 Sync;
  • prune: true 表示 Git 已經刪掉的 Resource,也可以從 Cluster 移除;
  • selfHeal: true 則表示如果有人手動把 Cluster 改到和 Git 不同,Argo CD 可以把它修回 Git 描述的狀態。
  • CreateNamespace=true 則是在目標 Namespace 不存在時允許 Argo CD 建立它。

因此整份 Argo CD YAML 其實只是在回答:

我是誰?
cka-api

Desired State 在哪?
GitHub → main → deploy/overlays/local

要部署去哪?
目前這個 Cluster → cka-lab

不同步怎麼辦?
自動 Sync + Self Heal

Automated Sync、Prune、Self Heal 是什麼?

我們設定:

automated:
  prune: true
  selfHeal: true

Automated Sync 代表:

Git 變了,Argo CD 可以自動同步。

Prune 代表:

Git 裡已經刪掉的 Resource,也可以從 Cluster 移除。

Self Heal 則更加符合 GitOps 精神。

假設有人偷偷:

kubectl scale deployment api \
  -n cka-lab \
  --replicas=10

但 Git 寫的是:

replicas: 2

那就是:

Git Desired State = 2
Cluster Actual State = 10

Argo CD 發現 Drift 後,可以把它修回:

2

這就是:

Self Healing

Step 7:讓 Argo CD 正式開始管理 Application

前面我們只是建立了:

argocd/cka-api-app.yaml

這個檔案目前還只是:

Git Repository 裡的一份 YAML 設定檔。

也就是說:

有 YAML 檔案
≠
Kubernetes 裡已經真的有這個 Resource

所以接下來要把它真正送進 Kubernetes。

先確認目前位於專案根目錄:

cd ~/k8s-30days

然後執行:

kubectl apply \
  -f argocd/cka-api-app.yaml

這一步會把 YAML 裡定義的:

kind: Application

建立到:

argocd Namespace

裡。


為什麼 Argo CD 可以認得 Application?

因為我們前面安裝 Argo CD 時,不只是安裝一些 Pod。

Argo CD 也會替 Kubernetes 加入自己的:

CRD

也就是:

Custom Resource Definition

其中就包含:

Application

所以原本 Kubernetes 不認識:

kind: Application

但是安裝 Argo CD 後,Kubernetes API 就開始認得這種 Resource。

整個關係可以理解成:

Argo CD Installation
↓
安裝 Application CRD
↓
Kubernetes 開始認識 kind: Application
↓
kubectl apply cka-api-app.yaml
↓
建立 Application Resource

Application Resource 在描述什麼?

我們的:

argocd/cka-api-app.yaml

其實是在告訴 Argo CD 三件事:

第一:去哪裡找 Desired State?
第二:要監控哪個資料夾?
第三:要部署到哪個 Kubernetes Cluster / Namespace?

例如:

source:
  repoURL: https://github.com/YIFUNLIN/k8s-30days.git
  targetRevision: main
  path: deploy/overlays/local

代表:

Git Repository:
https://github.com/YIFUNLIN/k8s-30days.git

Branch:
main

真正要監控的 Kubernetes 設定:
deploy/overlays/local

而:

destination:
  server: https://kubernetes.default.svc
  namespace: cka-lab

代表:

把這些 Resource 部署到 Argo CD 自己所在的 Kubernetes Cluster,目標 Namespace 是 cka-lab。

所以整體關係是:

GitHub Repository
        │
        │
        ▼
deploy/overlays/local
        │
        │ Desired State
        ▼
      Argo CD
        │
        ▼
   cka-lab Namespace

為什麼是 kubernetes.default.svc?

這個:

https://kubernetes.default.svc

是 Kubernetes Cluster 內部用來存取:

Kubernetes API Server

的 Service DNS。

因為 Argo CD 本身就跑在這個 kind Cluster 裡,所以它可以直接透過:

kubernetes.default.svc

管理:

自己所在的這一個 Cluster。

因此這個 Lab 不需要另外設定一台遠端 Cluster。


執行後確認 Application 有沒有建立成功

先:

kubectl get applications \
  -n argocd

應該會看到:

NAME      SYNC STATUS   HEALTH STATUS
cka-api   ...

https://ithelp.ithome.com.tw/upload/images/20260928/20168537B9z3eUqk1p.png

也可以看更詳細資訊:

kubectl describe application cka-api \
  -n argocd

現在 Kubernetes 裡就真的存在:

Application: cka-api

接著 Argo CD 會開始做什麼?

一旦 Application 建立完成,Argo CD 的:

application-controller

就會開始持續做:

讀 Git
↓
Render Kustomize
↓
取得 Desired State
↓
讀 Kubernetes Cluster
↓
取得 Actual State
↓
比較兩者
↓
如果不同就 Sync

所以它的核心其實還是在做:

Desired State
vs
Actual State

只是這次:

Desired State

不是直接來自:

kubectl apply deployment.yaml

而是來自:

Git Repository

如果有開 Automated Sync

我們前面設定:

syncPolicy:
  automated:
    prune: true
    selfHeal: true

所以 Argo CD 發現 Git 和 Cluster 不一致時,可以自動:

Sync

例如:

Git:
replicas = 1

Cluster:
replicas = 3

如果開:

selfHeal

Argo CD 就會嘗試把 Cluster 修回:

replicas = 1

這就是 GitOps 很重要的概念:

Git 才是 Desired State 的 Source of Truth。


確認 Argo CD 有沒有開始管理 cka-api

查看:

kubectl get applications \
  -n argocd

也可以打開 Argo CD Web UI,看:

Sync Status
Health Status
Git Revision
Resources

如果 Application 顯示:

Synced
Healthy

代表:

Git Desired State
↓
Argo CD
↓
Kubernetes Actual State

目前是一致的。

接著再查看:

kubectl get pods \
  -n cka-lab

以及目前 Deployment 使用的 Image:

kubectl get deployment api \
  -n cka-lab \
  -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'

https://ithelp.ithome.com.tw/upload/images/20260928/201685374U1FmxHeHy.png

如果看到:

ghcr.io/yifunlin/cka-api:<Git SHA>

https://ithelp.ithome.com.tw/upload/images/20260928/20168537uxGix2UFjf.png

代表:

Argo CD 已經開始根據 Git 裡的 Kustomize 設定管理這個 Deployment。

現在流程才真正完成:

Application YAML
↓
kubectl apply
↓
建立 Argo CD Application Resource
↓
Argo CD 開始 Watch Git
↓
同步 deploy/overlays/local
↓
管理 cka-lab

從 Argo CD Web UI 看懂我們剛剛到底建立了什麼

現在你只要在 127.0.0.1:8080 登入後,
就可以成功進來看到這個視覺化後的 Applications 了!

https://ithelp.ithome.com.tw/upload/images/20260928/20168537SfOjwBs2Se.png

接下來我們會先詳細解釋相關原理:

前面執行:

kubectl apply \
  -f argocd/cka-api-app.yaml

之後,我們不只是把一份 YAML 丟進 Kubernetes。

真正發生的是:

cka-api-app.yaml
↓
建立 Argo CD Application
↓
Argo CD 讀取 Git Repository
↓
讀取 deploy/overlays/local
↓
Render Kustomize
↓
取得完整 Kubernetes Desired State
↓
與 Cluster Actual State 比較
↓
Sync
↓
Web UI 建立 Resource Tree

所以現在打開 Argo CD Web UI,就可以看到剛才建立的:

cka-api

Application,以及它底下實際管理的 Kubernetes Resource。

這裡有一個很重要的觀念:

Argo CD 並不是把 Cluster 裡所有 Resource 全部列給你看,而是顯示這個 Application 所管理的 Resource。

我們的 Application 設定:

source:
  repoURL: https://github.com/YIFUNLIN/k8s-30days.git
  targetRevision: main
  path: deploy/overlays/local

所以 Argo CD 會從:

deploy/overlays/local

開始 Render Kustomize。

如果這個 Overlay 最後產生的是:

Service api
Deployment api

Argo CD 的 cka-api Application 就會管理這些 Resource。

因此現在畫面裡看到:

cka-api
   │
   ├── Service api
   │
   └── Deployment api
          │
          ├── ReplicaSet
          ├── ReplicaSet
          ├── ReplicaSet
          └── ReplicaSet
                 │
                 └── Pod

不是 Argo CD 自己額外建立了一套 Kubernetes Resource。

而是:

Argo CD 把 Kubernetes 原本的 Resource 關係視覺化了。


為什麼 Deployment 底下會有很多 ReplicaSet?

這張圖裡一個很容易讓人疑惑的地方,就是:

Deployment api

下面怎麼會有好幾個:

ReplicaSet

例如畫面中可以看到:

api-855d7cff4
api-584697869f
api-98458988
api-7d96c7cfbf

這其實正好是前面學 Deployment 時提過的:

Deployment
↓
ReplicaSet
↓
Pod

當 Deployment 的 Pod Template 發生改變,例如:

Image Tag 改變

Kubernetes 不會直接修改舊 ReplicaSet。

而是建立一個:

新的 ReplicaSet

https://ithelp.ithome.com.tw/upload/images/20260928/20168537SuN3DrsY7U.png

例如:

Revision 1
↓
ReplicaSet A

Revision 2
↓
ReplicaSet B

Revision 3
↓
ReplicaSet C

Revision 4
↓
ReplicaSet D

所以經過幾次:

Image 更新
Rolling Update

之後,就會留下多個 ReplicaSet。

舊 ReplicaSet 通常會被 Scale 到:

0 Pod

但仍然保留一段時間,讓 Deployment 可以:

Rollback

而目前最新的 ReplicaSet 才會真正維持正在執行的 Pod。

所以這張 Argo CD Resource Tree,其實把 Deployment 的關係畫得非常直觀:

Deployment api
       │
       ├── 舊 ReplicaSet rev1
       ├── 舊 ReplicaSet rev2
       ├── 舊 ReplicaSet rev3
       │
       └── Current ReplicaSet rev4
                     │
                     ▼
                    Pod

這也是為什麼 Argo CD UI 很適合拿來理解 Kubernetes Resource 之間的關係。


Healthy 和 Synced 是兩件不同的事情

畫面左上角可以看到兩個非常重要的狀態:

APP HEALTH
Healthy

以及:

SYNC STATUS
Synced

這兩個看起來很像,但代表完全不同的事情。

Synced 回答的是:

Git 裡面描述的 Desired State,和 Cluster 現在的 Actual State 一不一樣?

例如 Git 寫:

replicas: 1

Cluster 也是:

replicas = 1

而 Image 也完全相同,那就是:

Synced

如果有人直接執行:

kubectl scale deployment api \
  -n cka-lab \
  --replicas=3

但是 Git 還是:

replicas: 1

Argo CD 就可能暫時看到:

OutOfSync

因為:

Git Desired State
≠
Cluster Actual State

Healthy 則是在回答另外一件事:

這些 Resource 本身目前運作得健不健康?

例如 Deployment:

replicas = 1

而且:

1 個 Pod Ready

那 Deployment 就可能顯示:

Healthy

所以:

Synced

是在看:

Git vs Cluster

而:

Healthy

是在看:

Resource 本身有沒有正常運作

這兩件事情不能混在一起。

甚至可能出現:

Synced
但
Degraded

代表:

Git 和 Cluster 設定完全一致,但 Application 本身其實跑壞了。

例如 Git 裡本來就寫了一個不存在的 Image:

image: ghcr.io/example/api:v999999

Argo CD 很忠實地把它部署出去,所以:

Desired State = Actual State
→ Synced

但是 Pod:

ImagePullBackOff

因此 Application:

不 Healthy

這個差別非常重要。


上面的 Git Revision 又代表什麼?

畫面中可以看到:

Synced to main (ef10f26)

https://ithelp.ithome.com.tw/upload/images/20260928/20168537Wyi0jMdVKT.png

這表示 Argo CD 目前同步的是:

main Branch

中的某一個 Git Commit。

例如:

ef10f26

就是 Git SHA 的縮寫。

這代表我們現在已經可以從:

Kubernetes Running Resource

一路追到:

Argo CD
↓
Git Revision
↓
Git Commit
↓
Source Code / Manifest

這正是前面提過的:

Traceability

也就是可追溯性。

而畫面中甚至可以看到:

Author:
github-actions[bot]

Comment:
chore: deploy ...

https://ithelp.ithome.com.tw/upload/images/20260928/20168537F4Ntm11L6w.png

這代表目前這次 Deployment 的 Desired State,正是我們前面的:

GitHub Actions

自動修改:

Kustomize newTag

之後 Commit 回 GitHub 的結果。

也就是整條 Chain 已經真的串起來:

GitHub Actions
↓
更新 Kustomize
↓
github-actions[bot] Commit
↓
Argo CD 發現 Git Revision 改變
↓
Sync
↓
Kubernetes

不是只有流程圖而已。


Last Sync 又是在看什麼?

右邊還可以看到:

LAST SYNC
Sync OK

這是在告訴我們:

最近一次 Argo CD 執行 Sync 的結果。

例如:

Sync OK

代表最近一次同步成功。

下面還能看到:

時間
Author
Commit Message
Git Revision

所以當 Production 發生:

「剛剛部署完之後 API 就掛了」

Argo CD UI 可以很快幫你回答:

剛剛到底部署了哪個 Git Commit?

誰產生這次變更?

什麼時間 Sync?

Sync 成功還是失敗?

這也是 GitOps 在 Troubleshooting 時非常有價值的地方。


Resource Tree 怎麼看?

畫面最主要的區域就是:

Application Resource Tree

我們目前可以看到:

cka-api
   │
   ├── svc/api
   │
   └── deploy/api
          │
          ├── rs
          ├── rs
          ├── rs
          └── rs
                │
                └── pod

這些縮寫分別是:

svc
→ Service

deploy
→ Deployment

rs
→ ReplicaSet

pod
→ Pod

也就是把 Kubernetes 原本:

Service

Deployment
↓
ReplicaSet
↓
Pod

的關係直接視覺化。

點擊其中一個 Resource,例如:

Deployment api

https://ithelp.ithome.com.tw/upload/images/20260928/20168537DJnbyaYtwu.png

通常可以進一步查看:

Manifest
Live Manifest
Events
Health
Resource Details

點擊 Pod,也可以進一步查看 Pod 狀態,甚至從 Argo CD 介面進一步觀察 Log。

https://ithelp.ithome.com.tw/upload/images/20260928/20168537nGPDaF0drP.png

所以 Argo CD 不只是:

Deployment Tool

它也可以變成一個很好用的:

Kubernetes Application View

上面的幾個按鈕又是做什麼?

上方可以看到:

DETAILS
DIFF
SYNC
SYNC STATUS
HISTORY AND ROLLBACK
DELETE
REFRESH

其中最值得先理解的是:

DIFF

它會比較:

Git Desired State

和:

Cluster Actual State

到底差在哪裡。

例如有人手動修改 Deployment,DIFF 就能很直觀地看到:

Git:
replicas: 1

Cluster:
replicas: 3

這對 Troubleshooting 非常有用。


SYNC 則代表:

https://ithelp.ithome.com.tw/upload/images/20260928/20168537UYi42F97OQ.png

手動要求 Argo CD 把 Git Desired State 套用到 Cluster。

我們現在已經設定:

syncPolicy:
  automated:

所以通常不需要一直自己按:

SYNC

Git 變動之後,Argo CD 可以自己處理。

但如果沒有開 Automated Sync,就可以從這裡手動 Sync。


HISTORY AND ROLLBACK

https://ithelp.ithome.com.tw/upload/images/20260928/20168537pK9qZF0t4w.png

則可以查看:

過去同步過哪些 Revision

以及在適當情況下回到前面的版本。

這也是 GitOps 的另一個優點:

每次 Deployment
↓
都有 Git Revision
↓
有歷史
↓
可以追蹤

REFRESH

則是要求 Argo CD:

再重新取得 Git 與 Cluster 狀態並比較。

如果剛剛 Push Git,Web UI 看起來還沒更新,就可以:

Refresh

讓狀態重新整理。

至於:

DELETE

就不要把它當成普通的重新整理按鈕亂按。

它是刪除這個 Argo CD Application 的操作,而且依刪除方式與設定不同,可能連被 Application 管理的 Resource 都一起處理,所以實驗時也要看清楚再操作。


所以是哪一步讓這個畫面出現的?

最後再把整件事情串一次。

不是:

kubectl port-forward

讓 Service 和 Deployment 出現在 UI。

Port Forward 只是:

你的 Browser
↓
localhost:8080
↓
argocd-server

讓我們可以開 Argo CD 網頁而已。

真正關鍵的是 Step 7:

kubectl apply \
  -f argocd/cka-api-app.yaml

它建立:

Argo CD Application

然後:

Application
↓
source.repoURL
↓
main
↓
deploy/overlays/local
↓
Kustomize Render
↓
得到 Service + Deployment
↓
Argo CD Sync
↓
Kubernetes

最後 Argo CD 再從 Kubernetes API 取得 Resource 之間的關係,因此 Web UI 才能畫出:

                cka-api
                   │
          ┌────────┴────────┐
          ▼                 ▼
      Service api      Deployment api
                             │
                             ▼
                        ReplicaSet
                             │
                             ▼
                            Pod

所以這張 UI 其實就是前面所有概念的視覺化版本:

Git
↓
Desired State
↓
Argo CD
↓
Kubernetes Resource
↓
Actual State

而


接下來 Step 8 才要做真正的 End-to-End 測試:

修改 main.py
↓
git push
↓
GitHub Actions
↓
Build / Push GHCR
↓
更新 Kustomize newTag
↓
Argo CD 偵測變化
↓
自動 Sync
↓
Deployment Rolling Update

這樣我們才可以驗證:

整條 CI + GitOps Pipeline 真的有串起來。


Step 8:真正測一次完整 CI + GitOps Pipeline

前面我們已經完成:

GitHub Actions
↓
GHCR
↓
Kustomize
↓
Argo CD
↓
kind Kubernetes

而且 Argo CD 的 Application 已經顯示:

Synced
Healthy

現在才要做今天最重要的實驗:

只修改 Application Source Code,後面全部交給 Pipeline 自己完成。

這一次我們不再手動:

docker build
kind load
kubectl apply Deployment

而是看看:

git push

之後,整套系統能不能自己把新版本部署到 Kubernetes。


先確認本機 Git 是最新的

這一步之後會變得很重要。

因為我們的 GitHub Actions 不只是 Build Image,它還會:

修改 Kustomize newTag
↓
git commit
↓
git push 回 GitHub

所以 GitHub Repository 有可能比我們 Mac 上的 Local Repository 更新。
所以當你想要Push 時,就會失敗
https://ithelp.ithome.com.tw/upload/images/20260928/20168537u93d0cLhnq.png
下面的<補充> 會進行詳細說明,為何會突然本地落後於 repo

因此以後在開始修改前,都先做:

git pull --rebase origin main

rebase 在這裡可以簡單理解成:

先把 GitHub 上最新的 Commit 拉回來,再把自己的工作接在最新版本後面。

這樣可以避免等等 Push 時發現:

fetch first

修改 FastAPI

打開:

vim app/main.py

我們原本首頁是:

@app.get('/')
def root():

    visits = redis_client.incr("visits")

    return {
        "message":"Hello from k8s",
        "visits": visits
    }

只需要把:

Hello from k8s

改成:

Hello from GitOps!

變成:

@app.get('/')
def root():

    visits = redis_client.incr("visits")

    return {
        "message":"Hello from GitOps!",
        "visits": visits
    }

其他 Endpoint 不需要刪。

例如之前為了 HPA Load Test 建立的:

@app.get('/work')
def work():
    end_time = time.time() + 0.2
    counter = 0

    while time.time() < end_time:
        counter += 1

    return {
        "iterations": counter
    }

可以繼續保留。

它只有在有人 Request:

/work

時才會故意消耗一些 CPU,用來測試 HPA,平常不會自己一直執行。


Commit 並 Push

接著:

git add app/main.py

建立 Commit:

git commit -m "update api message"

然後:

git push

這時我們只做了一件事:

把新的 Source Code Push 到 GitHub。

接下來不要再:

docker build
kind load
kubectl apply

而是開始讓 Pipeline 自己跑。


<補充> 如果出現 fetch first

有時可能看到:

! [rejected] main -> main (fetch first)

Updates were rejected because the remote contains work
that you do not have locally.

這不一定代表出錯。

在我們現在的架構裡,很可能是因為:

GitHub Actions
↓
Build Image
↓
更新 Kustomize newTag
↓
Commit
↓
Push 回 GitHub

所以:

GitHub main

已經比:

Local main

多了一個 GitHub Actions Bot 的 Commit。

這時執行:

git pull --rebase origin main

它會把:

Remote Commit
+
你的 Local Commit

重新接成一條線:

原本:

        ┌─ GitHub Actions Commit
────────┤
        └─ 你的 Commit


rebase 後:

──────── GitHub Actions Commit
            │
            ▼
        你的 Commit

完成後再:

git push

即可。

如果想先確認 GitHub 到底多了哪些 Commit,可以:

git fetch origin

再:

git log HEAD..origin/main \
  --oneline

如果看到:

chore: deploy <Git SHA>

就代表那正是 GitHub Actions 更新 Kustomize Image Version 產生的 Commit。


Push 之後到底發生了什麼?

這次真正要理解的是後面的整條 Chain。

第一步:

git push

GitHub 發現:

app/**

有改變。

因此觸發:

GitHub Actions

接著 Runner:

Checkout Source Code
↓
Build linux/amd64 + linux/arm64 Image
↓
Push Image 到 GHCR

新 Image 會類似:

ghcr.io/yifunlin/cka-api:<Git SHA>

接下來 GitHub Actions 修改:

deploy/overlays/local/kustomization.yaml

原本可能是:

images:
  - name: cka-api
    newName: ghcr.io/yifunlin/cka-api
    newTag: 舊的-Git-SHA

改成:

images:
  - name: cka-api
    newName: ghcr.io/yifunlin/cka-api
    newTag: 新的-Git-SHA

然後 GitHub Actions 再:

git commit
↓
git push

把新的 Desired State 寫回 GitHub。


Argo CD 接手

現在 Git 裡的:

deploy/overlays/local

改變了。

而我們前面建立的:

Application: cka-api

正好在監控:

GitHub
↓
main
↓
deploy/overlays/local

所以 Argo CD 會比較:

Git Desired State

ghcr.io/yifunlin/cka-api:<新 SHA>

和:

Cluster Actual State

ghcr.io/yifunlin/cka-api:<舊 SHA>

發現:

Desired State
≠
Actual State

於是 Automated Sync 開始作用。


Kubernetes 開始 Rolling Update

Argo CD 更新 Deployment 後,Kubernetes 發現:

Pod Template

裡面的 Image 改了。

例如:

舊:
ghcr.io/yifunlin/cka-api:abc123

新:
ghcr.io/yifunlin/cka-api:def456

Deployment Controller 就會開始:

Rolling Update

也就是:

建立新 Pod
↓
新 Pod Pull 新 Image
↓
新 Pod Ready
↓
移除舊 Pod

因此我們可以直接觀察:

kubectl get pods \
  -n cka-lab \
  -w

可能會看到:

舊 api Pod       Running
新 api Pod       ContainerCreating
新 api Pod       Running
舊 api Pod       Terminating

https://ithelp.ithome.com.tw/upload/images/20260928/20168537n2eQh6c6KV.png

這就是 Rolling Update 真正在發生。


觀察 Deployment 是否更新完成

另外開一個 Terminal:

kubectl rollout status \
  deployment/api \
  -n cka-lab

成功後會看到類似:

deployment "api" successfully rolled out

https://ithelp.ithome.com.tw/upload/images/20260928/20168537OtnACnLlKV.png

接著確認目前 Kubernetes 真正使用的 Image:

kubectl get deployment api \
  -n cka-lab \
  -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'

https://ithelp.ithome.com.tw/upload/images/20260928/20168537I74vsVVMGd.png

應該會看到:

ghcr.io/yifunlin/cka-api:<新的 Git SHA>

也可以查看 Argo CD:

kubectl get applications \
  -n argocd

https://ithelp.ithome.com.tw/upload/images/20260928/201685375PteHpvx2b.png

應該最後再次回到:

Synced
Healthy

整個狀態就變成:

Git newTag
    =
Deployment Image
    =
Pod Image

最後真的打 API 驗證

如果目前 api Service Port 是:

80

先:

kubectl port-forward \
  svc/api \
  -n cka-lab \
  8000:80

再開另一個 Terminal:

curl http://127.0.0.1:8000/

原本可能得到:

{
  "message": "Hello from k8s",
  "visits": 10
}

現在應該變成:

{
  "message": "Hello from GitOps!",
  "visits": 1
}

https://ithelp.ithome.com.tw/upload/images/20260928/20168537JpIN5UZ32r.png

注意:

visits

還會繼續增加。

因為它存在:

Redis

裡,而不是存在 API Pod 本身。

所以即使 API Deployment Rolling Update、Pod 被換掉:

API Pod
舊 → 新

Redis 裡的資料仍然存在。

這也順便再次驗證了我們前面學到的:

Stateless Application
+
External State

概念。


整條 Pipeline 現在真的跑起來了

我們這次真正做的只有:

修改 main.py
↓
git commit
↓
git push

後面全部變成:

Developer
   │
   │ git push
   ▼
GitHub Repository
   │
   ▼
GitHub Actions
   │
   ├── Build Multi-Arch Image
   │
   └── Push GHCR
   │
   ▼
更新 Kustomize newTag
   │
   ▼
Git Commit
   │
   ▼
Argo CD
   │
   │ Compare
   ▼
Desired State ≠ Actual State
   │
   ▼
Sync
   │
   ▼
Deployment
   │
   ▼
Rolling Update
   │
   ▼
New Pod
   │
   ▼
Hello from GitOps!

而我們完全沒有手動:

docker build
kind load
kubectl apply Deployment

這就是今天最重要的成果。


為什麼之後要養成 git pull 的習慣?

以前只有我們自己修改 Repository:

Mac
↓
GitHub

但現在多了一個角色:

GitHub Actions Bot

它也會修改 Repository:

Mac Developer
       │
       ▼
     GitHub
       ▲
       │
GitHub Actions Bot

所以 GitHub 已經不再只是:

被動接收我們的 Push

它自己也可能產生新的 Commit。

因此之後開始工作前,可以養成:

git pull --rebase origin main

的習慣。

確保:

Local main

先跟:

GitHub main

同步。

在我們這個學習 Project 裡,Application Code 與 GitOps Config 放在同一個 Repository,這種情況尤其容易遇到。

真正大型 Production 環境也常進一步把:

Application Repository

與:

GitOps Repository

拆開。

例如:

cka-api
└── Source Code

cka-gitops
└── Kubernetes Manifest

這樣 CI 更新 Deployment Version 時,就不會一直改 Application Source Repository。

但目前 Day 30 的 Lab 使用:

Single Repository

反而比較容易一次看懂:

Code
→ CI
→ Image
→ Git Desired State
→ Argo CD
→ Kubernetes

整條 GitOps Pipeline 是怎麼串起來的。


現在 CI 與 GitOps 的責任非常清楚

GitHub Actions 負責:

Source Code
↓
Build
↓
Container Image
↓
Publish GHCR
↓
更新 Image Version

Argo CD 負責:

Watch Git
↓
Compare Desired / Actual State
↓
Sync
↓
Reconcile

Kubernetes 則負責:

Deployment
↓
Scheduler
↓
kubelet
↓
containerd
↓
Pod

三者不是互相取代。

而是一層接著一層。


Part 3:Final Boss —— Kubernetes 掛了怎麼辦?

前面我們已經把 CI、GitOps 與 Kubernetes 串起來了。最後要練的,其實也是實務上最重要的能力之一:Troubleshooting。

真實世界通常不會直接告訴你「Service Selector 寫錯」或「Image Tag 不存在」,你看到的往往只有一句:

API DOWN

所以排錯最重要的不是看到錯誤就開始亂下指令,而是先回答:

問題到底發生在哪一層?

可以先記住這條主線:

症狀
↓
判斷 Layer
↓
找 Evidence
↓
定位原因
↓
再處理

先從 Pod 狀態開始

只要 kubectl 還能正常使用,我通常會先看:

kubectl get pods -n cka-lab -o wide

kubectl get events \
  -n cka-lab \
  --sort-by=.lastTimestamp

第一步不是急著修,而是先看 Pod 到底是 Pending、ImagePullBackOff、CrashLoopBackOff,還是看起來 Running 卻沒有 Ready。

如果是 Pending,通常代表 Container 還沒真正開始跑,這時應該先查 Scheduling:

kubectl describe pod POD_NAME -n cka-lab

重點看最下面的 Events。像是 CPU / Memory 不足、NodeSelector、Affinity、Taint,甚至 PVC 尚未 Bound,都可能讓 Pod 一直卡在 Pending。


ImagePullBackOff 與 CrashLoopBackOff 要分清楚

如果看到:

ImagePullBackOff

代表 kubelet 連 Image 都還沒成功拉下來。

這時查:

kubectl describe pod POD_NAME -n cka-lab

優先確認 Image Name、Tag、Registry 權限、CPU Architecture,以及 Image 是否真的存在。

如果看到:

CrashLoopBackOff

情況就不同了。

它代表 Container 有成功啟動,但是程式一直 Crash,又被 Kubernetes 重啟。

這時才適合開始看 Log:

kubectl logs POD_NAME -n cka-lab

kubectl logs POD_NAME \
  -n cka-lab \
  --previous

--previous 特別重要,因為目前的 Container 可能已經重新啟動,真正 Crash 前的錯誤反而存在上一個 Container Instance 裡。


Pod Running,不代表服務真的正常

有時候 Pod 顯示:

Running

但可能是:

0/1

這通常代表 Container 還活著,但 Readiness Probe 沒有通過。

這時就要開始檢查:

Probe
ConfigMap
Secret
Environment Variable
Application Log

所以:

Running ≠ Healthy

Kubernetes 不只在意 Process 有沒有活著,也在意它是不是真的可以提供服務。


Pod 正常,就繼續往 Service 與 Network 查

假設 Pod 已經:

Running 1/1

API 還是連不到,就往下一層看:

kubectl get svc -n cka-lab
kubectl get endpointslice -n cka-lab

這裡最常遇到的問題就是:

Service Selector
≠
Pod Label

結果 Service 雖然存在,背後卻根本沒有 Endpoint。

如果 Endpoint 正常,再繼續往:

DNS
NetworkPolicy
Gateway
HTTPRoute

檢查。

也就是從最內層一路往外查:

Pod
↓
Service
↓
EndpointSlice
↓
NetworkPolicy / DNS
↓
Gateway / HTTPRoute
↓
External Client

這樣就不需要一開始看到 API 掛掉,就同時懷疑十幾種東西。


另外兩個常見訊號

如果指令直接看到:

Forbidden

通常就應該先想到:

RBAC

可以利用:

kubectl auth can-i ...

確認目前的 User 或 ServiceAccount 到底有沒有對應權限。

如果看到:

Node NotReady

問題就已經不只是 Application 了,而要繼續往:

Node
↓
kubelet
↓
Container Runtime
↓
CNI

檢查。

最後,如果連:

kubectl get nodes

都無法正常連到 Cluster,那才要把焦點移到 Control Plane,例如 API Server、Static Pod、憑證與 etcd。

所以整套 Troubleshooting 思路其實可以濃縮成:

kubectl 能不能用?
↓
Pod 正常嗎?
↓
Container 正常嗎?
↓
Service 有 Endpoint 嗎?
↓
Network 正常嗎?
↓
External Traffic 正常嗎?

一層一層往下查,比背幾十個 kubectl 指令重要得多。


30 天走到這裡,我們到底學會了什麼?

回頭看 Day 1,我們可能只是知道:

Kubernetes 可以管理 Container

但走到 Day 30,看到:

Deployment
Service
ConfigMap
Secret
PVC
StatefulSet
Scheduler
HPA
NetworkPolicy
RBAC
Helm
Kustomize
Gateway API
CRD
Operator
kubelet
containerd
etcd
Argo CD

已經不再只是彼此獨立的名詞。

我們現在可以把它們真正串起來:

Developer
↓
Git Push
↓
GitHub Actions
↓
Build Image
↓
GHCR
↓
更新 Git Desired State
↓
Argo CD
↓
Kubernetes
↓
Scheduler
↓
kubelet
↓
containerd
↓
Pod

而當中任何一層出問題,我們也開始知道應該從哪裡找 Evidence。

這其實才是這 30 天最重要的成果。

不是背完所有 Kubernetes Resource,而是開始理解:

**一個 Application 從程式碼,到 Container,到 Kubernetes,再到真正提供服務,中間到底發生


上一篇
Day 29|etcd Backup / Restore:如果 Kubernetes 的「記憶」壞掉怎麼辦?
系列文
不是背 YAML!30 天從零打造 Kubernetes 微服務:從本機實戰一路到 CKA 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言