← กลับ
KubernetesConfigMapBest PracticeProduction

ConfigMap Template ที่ใช้ได้จริงใน Production

เลิก hardcode env / config — pattern ของ ConfigMap ที่ทีมจริงใช้ รวม secret separation, hot reload, multi-env, validation

2026-04-12อ่าน 7 นาทีใหม่

ปัญหาของ 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

ปัญหา:

  1. Secret อยู่ใน ConfigMap — ใครเข้า cluster ก็เห็น password
  2. ไม่มี hot reload — แก้ ConfigMap แล้ว pod ยังใช้ค่าเก่าจน restart
  3. ทุก env ใช้ ConfigMap คนละไฟล์ — drift ง่าย
  4. ไม่ 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

← ดูบทความอื่น