อาการ
Error: DATABASE_URL is not defined
at config.ts:12
แต่ใน Deployment มีตั้งไว้:
env:
- name: DATABASE_URL
value: "postgresql://..."
หรือใน ConfigMap:
data:
DATABASE_URL: "postgresql://..."
ทำไมถึงไม่มา?
วิธีดู env ที่ Pod รับจริง
อันดับแรก — ตรวจที่ pod ตัวจริง ไม่ใช่ที่ manifest
kubectl exec -it myapp-abc123 -- env
หรือ:
kubectl exec -it myapp-abc123 -- env | grep DATABASE
ถ้าไม่เห็น DATABASE_URL — env ไม่ได้ถูก inject
ถ้าเห็น แต่ค่าผิด — ConfigMap/Secret ผิด
สาเหตุที่เป็นไปได้
1. envFrom ชี้ไป ConfigMap/Secret ที่ไม่มี
envFrom:
- configMapRef:
name: myapp-config # ← ชื่อนี้มีจริงมั้ย
ตรวจ:
kubectl get configmap -n production
kubectl get secret -n production
ถ้าไม่มี — apply ก่อน หรือเช็ค namespace
2. ConfigMap อยู่คนละ namespace กับ Pod
ConfigMap ของ namespace "default" — Pod ใน namespace "production" เรียกไม่ได้
ตรวจ:
# Pod อยู่ namespace ไหน
kubectl get pod myapp-abc123 -o jsonpath='{.metadata.namespace}'
# ConfigMap อยู่ namespace ไหน
kubectl get configmap myapp-config -A
ทั้งคู่ต้องอยู่ namespace เดียวกัน — ถ้าไม่ใช่ ก็ apply ConfigMap ใน namespace เดียวกับ Pod
3. Key ใน ConfigMap ไม่ตรง
# ConfigMap
data:
DATABASE_URL: "..." # ← key เป็น DATABASE_URL
# Deployment
env:
- name: DATABASE_URL
valueFrom:
configMapKeyRef:
name: myapp-config
key: database_url # ← lowercase ไม่ตรง
ตรวจ:
kubectl get configmap myapp-config -o yaml | grep -A 10 data:
4. Pod ยังไม่ได้ rolling update หลังเปลี่ยน ConfigMap
K8s ไม่ auto-restart pod เมื่อ ConfigMap เปลี่ยน
# ดูว่า pod เกิดเมื่อไหร่
kubectl get pod myapp-abc123 -o jsonpath='{.metadata.creationTimestamp}'
# ดู ConfigMap update เมื่อไหร่
kubectl get configmap myapp-config -o jsonpath='{.metadata.creationTimestamp}'
ถ้า ConfigMap ใหม่กว่า Pod — Pod ยังใช้ค่าเก่า
แก้:
kubectl rollout restart deployment myapp -n production
ป้องกันถาวร — ใช้ Reloader (อ่าน ConfigMap Production)
5. Order ของ env ใน manifest
env ที่กำหนดทีหลังจะ override อันก่อน:
envFrom:
- configMapRef:
name: myapp-config # มี LOG_LEVEL=info
env:
- name: LOG_LEVEL
value: "debug" # อันนี้ทับ
ลำดับใน K8s:
envFrom(ทำตามลำดับใน list)env(overrideenvFrom)
ใช้กฎนี้ override ค่าตัวเดียวจาก ConfigMap
6. Secret ใช้ stringData vs data ผิด
# ผิด — base64 ใส่ผิด
apiVersion: v1
kind: Secret
data:
DATABASE_URL: "postgresql://..." # ← ต้อง base64 encode
# ถูก
data:
DATABASE_URL: "cG9zdGdyZXNxbDovLy4uLg=="
# หรือใช้ stringData (ระบบ encode ให้)
stringData:
DATABASE_URL: "postgresql://..."
ตรวจ:
kubectl get secret myapp-secret -o yaml
decode ดูค่า:
kubectl get secret myapp-secret -o jsonpath='{.data.DATABASE_URL}' | base64 -d
7. Application read env ผิด
ตรวจที่ app code:
// ผิด — ไม่มี value default
const url = process.env.DATABSE_URL // typo!
// ผิด — Next.js ฝั่ง browser ต้อง prefix
const apiKey = process.env.API_KEY // browser อ่านไม่ได้
// ถูก
const url = process.env.DATABASE_URL
const apiKey = process.env.NEXT_PUBLIC_API_KEY
โดยเฉพาะ Next.js — env ที่ใช้ฝั่ง client ต้อง prefix NEXT_PUBLIC_
8. Container ใน pod คนละตัว
Pod มีหลาย container — env ของ A ไม่อยู่ใน B
ตรวจ:
kubectl exec -it myapp-abc123 -c app -- env
kubectl exec -it myapp-abc123 -c sidecar -- env
ถ้า env ต้องอยู่ทั้ง 2 — ใส่ที่ระดับ pod (spec.containers[].env) ของแต่ละ container
K8s ไม่มี "shared env" ระหว่าง container ใน pod
9. Init container vs main container
Init container กับ main container เห็น env แยกกัน
spec:
initContainers:
- name: setup
env: # init เท่านั้น
- name: SETUP_KEY
value: "..."
containers:
- name: app
env: # main เท่านั้น
- name: APP_KEY
value: "..."
SETUP_KEY ไม่อยู่ใน main container
10. value ว่าง vs ไม่ inject
env:
- name: API_KEY
value: "" # มี env ค่าว่าง
vs ไม่มี env key เลย — env ไม่ถูก inject
ใน app:
process.env.API_KEY === "" // case 1
process.env.API_KEY === undefined // case 2
ถ้า code เช็ค if (!process.env.API_KEY) — ทั้งคู่ผ่าน แต่บาง code เช็ค === undefined เท่านั้น
วิธี Debug ที่แนะนำ
Step 1: ดู env ทั้งหมดใน pod
kubectl exec myapp-abc123 -- env | sort
ถ้าไม่มี shell — kubectl exec รัน command ตรง:
kubectl exec myapp-abc123 -- printenv DATABASE_URL
Step 2: ดู spec ของ pod ที่กำลังรัน
kubectl get pod myapp-abc123 -o yaml | grep -A 50 "containers:"
อ่าน env: และ envFrom: — ตรงกับที่ manifest มั้ย
ถ้าไม่ตรง — Pod ยังใช้ spec เก่า (ไม่ได้ rolling update)
Step 3: ตรวจ ConfigMap / Secret
kubectl describe configmap myapp-config
kubectl describe secret myapp-secret
ดู Data: section — มี key ที่ต้องการมั้ย ค่าถูกต้องมั้ย
Step 4: ดู Events ของ Pod
kubectl describe pod myapp-abc123 | grep -A 20 Events
อาจเห็น:
Warning Failed Error: secret "myapp-secret" not found
Step 5: ทดสอบด้วย Pod ใหม่
kubectl run test --rm -it --image=busybox \
--env-from-configmap=myapp-config \
--restart=Never -- env
(ใช้กับบาง K8s version) หรือสร้าง manifest ทดสอบ:
apiVersion: v1
kind: Pod
metadata:
name: env-test
spec:
containers:
- name: test
image: busybox
command: ["sh", "-c", "env && sleep 3600"]
envFrom:
- configMapRef:
name: myapp-config
Pattern ที่ดี
Validate ที่ app start
import { z } from 'zod'
const envSchema = z.object({
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
PORT: z.coerce.number().default(3000),
})
const env = envSchema.parse(process.env)
ถ้า env ขาด/format ผิด → app crash ตอน start ทันที — ดีกว่า silent
ใช้ envFrom เป็นหลัก
envFrom:
- configMapRef:
name: myapp-config
- secretRef:
name: myapp-secret
ดี: ไม่ต้องเขียน env ทีละตัวใน Deployment เพิ่ม/ลด config ที่ ConfigMap จุดเดียว
ใส่ default ใน app
const port = parseInt(process.env.PORT ?? '3000', 10)
const logLevel = process.env.LOG_LEVEL ?? 'info'
env ขาดบางตัว — app ยังทำงานต่อได้
Cheat Sheet
# ดู env จริงใน pod
kubectl exec <pod> -- env | sort
# ดู spec ของ pod
kubectl get pod <pod> -o yaml | grep -A 50 containers:
# ตรวจ ConfigMap/Secret
kubectl get configmap <name> -o yaml
kubectl get secret <name> -o yaml
# decode secret
kubectl get secret <name> -o jsonpath='{.data.<KEY>}' | base64 -d
# rolling restart หลังแก้ ConfigMap
kubectl rollout restart deployment <name>
สรุป
90% ของ env ไม่มา = pod ใช้ spec เก่า → rolling restart
อีก 10%:
- ConfigMap key ผิด
- ConfigMap คนละ namespace
- Override order ใน manifest
- App typo
ตรวจที่ kubectl exec <pod> -- env ก่อนเสมอ — ถ้าเห็นใน pod ก็แสดงว่า K8s inject แล้ว ปัญหาอยู่ที่ app
อ่านต่อ: ConfigMap Production — pattern ที่ป้องกันปัญหานี้ตั้งแต่ต้น