問題

一份部署層模板(Helm chart+共用 CI+守門檢查)第一次用真 app(nestjs+vue)部署到本機 kind,十個地方卡住。每一個都不是這份模板獨有,照這份模板開出來的專案照抄一樣會踩。十個坑全部在 kind 上重現、修掉、重跑過部署閘門。

十個坑

1. Helm pre-install hook 跑在一般資源之前,Job 讀不到 ConfigMap 與 Service。 migration Job 用 helm.sh/hook: pre-install,pre-upgrade,起來時 app-config ConfigMap 與 db Service 都還不存在 → CreateContainerConfigError 卡到 timeout,--atomic 再把整個 release 拆掉。兩次各 5 分鐘才看出來。 解法:Job 需要的 ConfigMap 與 db 接點各做一份 hook 版副本(hook-weight: "-5",成功後 hook-succeeded 刪除),Job 以 env 覆蓋 DB host 指向副本。教訓:hook 資源只能依賴 hook 資源。

2. k8s 的 runAsNonRoot 要求 image 的 USER 是數字 uid。 nestjs 的 Dockerfile 寫 USER nestjs(名字),kubelet 驗不了「非 root」直接拒起:container has runAsNonRoot and image has non-numeric user。 解法:Dockerfile 骨架改 USER 1001;既有 image 在 values 給 securityContext.runAsUser。用名字寫 USER 的 image 全會踩。

3. kind 節點解析不到 host.docker.internal。 外部 DB 用 ExternalName 指 host.docker.internal 是常見寫法,但 kind 節點本身是 docker 容器,沒有這個名字。 解法:把 host 上的 DB 容器 docker network connect kind,叢集 CoreDNS 會轉給 docker 內建 DNS,用容器名當 ExternalName。備案是 chart 的 ip 模式(無 selector 的 Service+Endpoints)。

4. Helm 4 把 --atomic 改名 --rollback-on-failure。 2026-09 用 brew install helm 裝到的是 4.3(GitHub releases 上 v4.3.0 是 2026-09-09 發的);舊旗標還能用但印 deprecated。官方 Helm 4 概覽寫明 --atomic → --rollback-on-failure、--force → --force-replace,舊旗標保留但會發出 deprecated 警告。CI 用的 alpine/k8s image 還是 Helm 3。 解法:Makefile 以 helm version 判斷用哪個旗標;CI 固定 Helm 3 用 --atomic。順帶:Helm 4 的 --wait 不帶時預設是 hookOnly,要等 Job 完成得加 --wait 和 --wait-for-jobs(helm upgrade 文件;同頁也寫了設了 --rollback-on-failure 時 --wait 會預設成 watcher)。

5. Helm values 是合併不是取代:疊加層想拿掉 key 要設 null。 values-sit.yaml 有 APP_NAME,values-kind.yaml 沒寫不代表拿掉,會被繼承下來,env 契約檢查一直抓到。 解法:APP_NAME: null(Helm 合併時刪 key);契約檢查腳本把 null 視為「刻意移除」,不算注入。

6. kind 占 host 的 80/443/3306,開發棧同時起就撞埠。 make up 起 compose 時 frontend 80、mysql 3306 綁不上;更陰的是 curl localhost/healthz 回 200,其實是 kind 的 ingress 答的。 解法:撞埠一律只挪 host 映射(FRONTEND_PORT=8080 DB_PORT=3307),容器內部埠不動;驗證時先確認打到的是哪一棧。

7. env 契約檢查第一次跑就抓到「注入了 app 不讀的 key」。 模板的 values 注入 APP_NAME/APP_ENV,nestjs 的 .env.example 根本沒有這兩個(它用 NODE_ENV)。這正是「部署層注入的 env 必須是 app 契約的子集」這條規則要抓的:無效配置不會讓 app 壞,但會讓人以為它有效。 解法:以各 app repo 根目錄的 .env.example 為契約,values 的 env key 必須是它的子集。

8. helm rollback <release> 0(上一版)在自動回滾之後指到的是失敗版。 helm rollback 文件寫的是:revision 省略或設成 0,就回滾到「previous release」。實際演練:壞 tag 升級 → --rollback-on-failure 自動回滾成功(revision 4 = Rollback to 2)。接著「手動再回滾一次」用 helm rollback <release> 0,這個「上一版」是 revision 3——正是那個失敗版,於是壞 image 又被部署回去,卡在 ImagePullBackOff。指定 revision 4 才對。 解法:自己從 helm history -o json 取 status 為 superseded/deployed 的倒數第二個 revision。回滾工具的預設值要用「上一個成功版」定義,不是「上一個版本」。

9. ExternalName 指叢集內 Service 必須寫 FQDN。 ExternalName 的作用是讓叢集 DNS 回一筆 CNAME 指向 externalName 的值(Kubernetes 文件:type: ExternalName)。externalName: postgres(裸名)實測 CoreDNS 解不到;第 3 坑那次能過,是因為目標在 docker DNS 裡。叢集內目標要寫 postgres.<ns>.svc.cluster.local。

10. 以檔案掛載的 Secret 是 root 擁有,非 root 的 app 讀不到。 defaultMode: 0400 只有 root 能讀,容器以 uid 1000 跑就 PermissionError。要 pod 層 fsGroup 加 0440,chart 因此多了 podSecurityContext。

附帶的小坑

  • kubectl wait --for=condition=ready pod 在 pod 還沒被排程時回 no matching resources:先 rollout status 等 Deployment,再 wait pod。
  • gitleaks --no-git 會掃 gitignore 掉的檔案:本機掃描抓到未進版控的本機祕密檔,代表守門有在運作,不是洩漏;把該路徑放行並註明原因,CI 的 git 模式不受影響。
  • worktree 裡的 .git 是指標檔,容器內的 gitleaks 讀不到 history:本機用 --no-git,history 交給 CI。

帶走的原則

  • 每個閘門都要反向驗證:故障注入要真的會壞。把 BACKEND_PORT 改成 3999 不算,因為 app 與探測端一起改了;把 migration 指令改成 false 才算。
  • 卡住不要乾等 timeout:另一個終端盯 pod 事件,5 分鐘的 timeout 通常 20 秒就能看出原因。

相關

  • K8s Helm:helm rollback 的基本用法;第 8 坑是「不指定 revision」那條路的陷阱。
  • K8s 網路:ExternalName 的基本介紹;第 3、9 坑是它在 kind 與叢集內目標上的兩種失敗。