ปัญหาของ ConfigMap แบบ basic
ตัวอย่างที่หลายคนเริ่มต้น:
apiVersion: v1
kind: ConfigMap
metadata:
name: myapp-config
data:
DATABASE_URL: postgresql://user:pass@db:5432/myapp
API_KEY: sk_live_abcdef
LOG_LEVEL: info
ปัญหา:
- Secret อยู่ใน ConfigMap — ใครเข้า cluster ก็เห็น password
- ไม่มี hot reload — แก้ ConfigMap แล้ว pod ยังใช้ค่าเก่าจน restart
- ทุก env ใช้ ConfigMap คนละไฟล์ — drift ง่าย
- ไม่ validate — ค่าผิด pod พังตอน start
Pattern ที่ใช้ใน production
1. แยก Config ออกจาก Secret
ConfigMap = ค่าที่อยู่ใน git ได้ Secret = ค่าที่ห้ามเห็นใน code review
# configmap.yaml — commit ใน git ได้
apiVersion: v1
kind: ConfigMap
metadata:
name: myapp-config
data:
LOG_LEVEL: info
PORT: "3000"
CACHE_TTL: "300"
FEATURE_NEW_CHECKOUT: "false"
---
# secret.yaml — ไม่ commit, gen จาก env หรือ vault
apiVersion: v1
kind: Secret
metadata:
name: myapp-secret
type: Opaque
stringData:
DATABASE_URL: postgresql://...
API_KEY: sk_live_...
JWT_SECRET: ...
ใส่ใน Deployment:
spec:
template:
spec:
containers:
- name: app
envFrom:
- configMapRef:
name: myapp-config
- secretRef:
name: myapp-secret
2. ใช้ External Secrets Operator (ESO)
แทนที่ commit secret ที่ encode base64 (ซึ่งไม่ใช่ encryption) — pull จาก vault จริง:
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: myapp-secret
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: myapp-secret
data:
- secretKey: DATABASE_URL
remoteRef:
key: secret/data/myapp
property: database_url
- secretKey: API_KEY
remoteRef:
key: secret/data/myapp
property: api_key
ESO จะ pull จาก HashiCorp Vault / AWS Secrets Manager / Doppler / 1Password / etc. → สร้าง K8s Secret อัตโนมัติ
ดี:
- Secret ไม่อยู่ใน git
- Rotate ที่ vault — pod ใช้ค่าใหม่อัตโนมัติ
- Audit trail ใน vault
3. Hot Reload — เปลี่ยน config โดยไม่ restart
K8s ไม่ auto-restart pod เมื่อ ConfigMap เปลี่ยน
วิธี A: Mount เป็นไฟล์ — auto sync
spec:
containers:
- name: app
volumeMounts:
- name: config
mountPath: /etc/myapp
readOnly: true
volumes:
- name: config
configMap:
name: myapp-config
ConfigMap mount เป็นไฟล์ — เมื่อ ConfigMap เปลี่ยน K8s sync ไฟล์ภายใน 1-2 นาที (kubelet sync)
ใน app — watch ไฟล์เปลี่ยนแล้ว reload:
import { watchFile } from 'fs'
let config = JSON.parse(fs.readFileSync('/etc/myapp/config.json', 'utf-8'))
watchFile('/etc/myapp/config.json', () => {
config = JSON.parse(fs.readFileSync('/etc/myapp/config.json', 'utf-8'))
console.log('Config reloaded')
})
Limitation: env var (envFrom) ไม่ sync — restart pod ถึงจะเห็น
วิธี B: Restart pod เมื่อ ConfigMap เปลี่ยน (Reloader)
ติดตั้ง stakater/Reloader:
helm install reloader stakater/reloader -n kube-system
ใส่ annotation ใน Deployment:
metadata:
annotations:
reloader.stakater.com/auto: "true"
ทุกครั้งที่ ConfigMap/Secret เปลี่ยน → Reloader restart Deployment ที่อ้างถึง — rolling update ไม่มี downtime
วิธี C: Hash ของ ConfigMap ใน Pod template
ใส่ hash ของ ConfigMap ใน pod template — ทุกครั้ง ConfigMap เปลี่ยน hash เปลี่ยน → trigger rolling update
spec:
template:
metadata:
annotations:
checksum/config: ${CONFIG_HASH}
generate hash ตอน deploy:
CONFIG_HASH=$(kubectl get configmap myapp-config -o yaml | sha256sum | cut -c1-10)
sed -i "s/\${CONFIG_HASH}/$CONFIG_HASH/g" deployment.yaml
kubectl apply -f deployment.yaml
Helm ทำให้อัตโนมัติด้วย:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
4. Multi-environment — Kustomize
แยก ConfigMap ของแต่ละ env แต่ใช้ base ร่วมกัน:
k8s/
├── base/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── configmap.yaml ← default values
│ └── kustomization.yaml
└── overlays/
├── staging/
│ ├── configmap.yaml ← override
│ └── kustomization.yaml
└── production/
├── configmap.yaml ← override
└── kustomization.yaml
base/configmap.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: myapp-config
data:
LOG_LEVEL: info
PORT: "3000"
overlays/production/configmap.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: myapp-config
data:
LOG_LEVEL: warn
CACHE_TTL: "3600"
overlays/production/kustomization.yaml:
resources:
- ../../base
patches:
- path: configmap.yaml
target:
kind: ConfigMap
name: myapp-config
namePrefix: prod-
namespace: production
deploy:
kubectl apply -k overlays/production
5. Validation ก่อน apply
ตรวจ ConfigMap value ก่อน deploy — กัน typo/value ผิด
ใช้ schema validation:
# ใช้ kyverno policy
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: validate-myapp-config
spec:
validationFailureAction: enforce
rules:
- name: log-level-must-be-valid
match:
any:
- resources:
kinds: [ConfigMap]
names: [myapp-config]
validate:
message: "LOG_LEVEL ต้องเป็น debug, info, warn หรือ error"
pattern:
data:
LOG_LEVEL: "debug|info|warn|error"
หรือ validate ใน app เอง:
import { z } from 'zod'
const configSchema = z.object({
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']),
PORT: z.coerce.number().min(1024).max(65535),
CACHE_TTL: z.coerce.number().positive(),
})
const config = configSchema.parse(process.env)
ถ้าค่าผิด — app crash ตอน start แทนที่ silent ใช้ default
6. ConfigMap ที่ใหญ่ — แยกเป็นหลาย key
ConfigMap มี limit 1MiB — ของใหญ่ต้องแบ่ง
ใส่หลายไฟล์ใน ConfigMap เดียว:
apiVersion: v1
kind: ConfigMap
metadata:
name: myapp-templates
data:
email-welcome.html: |
<html>...
email-reset.html: |
<html>...
notification-template.json: |
{"title": ...}
mount เป็น volume → app อ่านเป็นไฟล์
7. Generated ConfigMap จาก ENV file
ใช้ Kustomize generate จาก .env ที่ commit ใน git แยก:
# kustomization.yaml
configMapGenerator:
- name: myapp-config
envs:
- .env.production
.env.production:
LOG_LEVEL=warn
PORT=3000
CACHE_TTL=3600
kubectl apply -k .
Kustomize gen ConfigMap ที่มี hash ใน name → ทุกครั้งเปลี่ยน hash เปลี่ยน → trigger restart
Anti-pattern ที่ควรหลีกเลี่ยง
❌ Hardcode IP / hostname
data:
DB_HOST: "10.0.0.5" # ถ้า IP เปลี่ยน — ทุก app ใช้ค่าผิด
ใช้ Service DNS:
data:
DB_HOST: "postgres.production.svc.cluster.local"
❌ Mix configuration กับ secret
อย่าใส่ password ใน ConfigMap แม้ encode base64 — มันแค่ encode ไม่ใช่ encrypt
❌ ConfigMap "everything"
อย่าใส่ทั้ง app config + nginx config + queue config ใน 1 ConfigMap แยกตาม concern
myapp-config ← app
myapp-nginx-conf ← nginx
myapp-queue-conf ← queue
❌ ลืม versioning
deploy แล้วไม่รู้ใครแก้อะไรเมื่อไหร่ — ทุก ConfigMap ผ่าน git + PR review
ตัวอย่างเต็ม — production-ready
# base/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: myapp-config
labels:
app: myapp
component: config
data:
LOG_LEVEL: info
PORT: "3000"
CACHE_TTL: "300"
FEATURE_FLAGS: "checkout-v2,search-v2"
---
# external-secret.yaml
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: myapp-secret
spec:
refreshInterval: 1h
secretStoreRef:
name: doppler-store
kind: ClusterSecretStore
target:
name: myapp-secret
dataFrom:
- extract:
key: myapp-production
---
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
annotations:
reloader.stakater.com/auto: "true"
spec:
replicas: 3
selector:
matchLabels: { app: myapp }
template:
metadata:
labels: { app: myapp }
spec:
containers:
- name: app
image: ghcr.io/myorg/myapp:1.0.0
envFrom:
- configMapRef: { name: myapp-config }
- secretRef: { name: myapp-secret }
livenessProbe:
httpGet: { path: /health, port: 3000 }
initialDelaySeconds: 10
readinessProbe:
httpGet: { path: /ready, port: 3000 }
initialDelaySeconds: 3
resources:
limits: { cpu: 500m, memory: 512Mi }
requests: { cpu: 100m, memory: 128Mi }
เช็คลิสต์ของดี
- [ ] ConfigMap commit ใน git, secret ไม่
- [ ] ใช้ External Secrets / sealed-secrets / SOPS สำหรับ secret
- [ ] Hot reload (Reloader / file watch / Helm hash)
- [ ] Validate value ที่ app start
- [ ] ใช้ Service DNS แทน hardcode IP
- [ ] แยก ConfigMap ตาม concern ไม่รวมในก้อนเดียว
- [ ] Multi-env ใช้ Kustomize / Helm values
- [ ] Audit trail — ทุกการเปลี่ยน config ผ่าน PR
สรุป
ConfigMap เป็นเรื่อง basic ของ K8s — แต่ทำให้ดี = ลด human error เยอะ
เริ่มจาก: แยก secret ↔ config + Reloader + validation = ครอบคลุม 80% ของ pattern ที่ต้องการ
ขั้นต่อไป: ESO + Kustomize multi-env + audit ผ่าน GitOps (ArgoCD/Flux)
อ่านเพิ่ม: Environment Variables Security — รายละเอียดการจัดการ secret