問題
一份部署層模板(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 秒就能看出原因。