ConfigMap & Secret
หลักการสำคัญ: แยก configuration ออกจาก container image (ตาม 12-factor app) — image เดียวกันควร deploy ได้ทั้ง dev/staging/prod โดยแค่เปลี่ยน config ไม่ใช่ build image ใหม่ ทุกครั้งที่ env เปลี่ยน
ConfigMap — เก็บค่าที่ไม่ลับ
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
LOG_LEVEL: "info"
MAX_CONNECTIONS: "100"
config.yaml: |
feature_flags:
new_ui: true
"ConfigMap คือกระดาษโน้ตแปะตู้เย็น ไม่ใช่ตู้เซฟ" — ใครผ่านไปมาก็อ่านได้ (ไม่ encrypt, RBAC อ่านง่ายกว่า Secret ในหลาย default config) เหมาะกับข้อมูลที่ "เห็นแล้วไม่เป็นไร" เท่านั้น
ConfigMap มีเพดานขนาดที่คนมักไม่รู้จนกว่าจะชน: 1 MiB ต่อ object (ขีดจำกัดของ etcd ที่ผูกมา ทั้งระบบ ไม่ใช่แค่ ConfigMap) ถ้าพยายามยัด config file ใหญ่ๆ (เช่น dataset หรือ config XML หลาย MB) เข้า ConfigMap จะถูก apiserver ปฏิเสธตรงๆ ทีมที่เจอปัญหานี้มักแก้โดยแยกเป็นหลาย ConfigMap เล็กๆ หรือเปลี่ยนไปใช้ volume แบบอื่น (เช่น init container ที่ดึงไฟล์จาก object storage ตอน start) แทนที่จะพยายามยัดทุกอย่างไว้ใน etcd ซึ่งไม่ได้ถูกออกแบบมาเป็น file storage ตั้งแต่แรก
Secret — เก็บค่าที่ลับ (แต่ไม่ได้เข้ารหัสจริงโดย default!)
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
data:
username: YWRtaW4= # base64 ของ "admin"
password: cGFzc3dvcmQxMjM= # base64 ของ "password123"
Secret เก็บค่าเป็น base64 encoding ไม่ใช่ encryption — ใครก็ตามที่มีสิทธิ์
kubectl get secret -o yaml หรือเข้าถึง etcd โดยตรง decode กลับเป็น plaintext ได้ทันที
ด้วย base64 -d ต้องเปิด encryption at rest ของ etcd แยกต่างหาก และจำกัด RBAC การอ่าน
Secret ให้แคบที่สุดเท่าที่จำเป็น ถ้าต้องการความปลอดภัยสูงขึ้นให้พิจารณา external secret
manager (Vault, AWS Secrets Manager + External Secrets Operator)
"base64 คือเขียนด้วยรหัสมอร์ส ไม่ใช่ใส่กุญแจ" — ทุกคนที่รู้ตารางมอร์ส (คือทุกคนในโลก เพราะ
base64 เป็น public algorithm) ถอดได้ในวินาทีเดียว base64 แค่เปลี่ยน "รูปแบบการเขียน" ไม่ได้
"ล็อก" อะไรเลย
แม้จะเปิด encryption at rest ของ etcd แล้ว Secret ที่ mount เข้า Pod ก็ยังอยู่เป็น plaintext
ใน memory/tmpfs ของ Node นั้นอยู่ดี (เพราะ container ต้องอ่านค่าจริงไปใช้งาน) ความปลอดภัยที่
เพิ่มคือป้องกันคนที่เข้าถึง etcd disk โดยตรง ไม่ใช่ป้องกันคนที่เข้าถึง Node/Pod ได้ — นี่คือ
เหตุผลที่ RBAC การ exec เข้า Pod และสิทธิ์อ่าน Secret สำคัญพอๆ กับ encryption at rest เอง
ทีมที่คิดว่า "เปิด encryption at rest แล้วปลอดภัย 100%" มักพลาดมุมนี้ไป เพราะ threat model
ที่แท้จริงต้องครอบทั้ง etcd, Node, และสิทธิ์ของคนที่เข้าถึง cluster ด้วย
วิธี inject เข้า Pod — 2 แบบหลัก
1. Environment Variable
spec:
containers:
- name: app
envFrom:
- configMapRef:
name: app-config
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
ข้อดี: ง่าย, ตรงไปตรงมา ข้อเสีย: ต้อง restart Pod ถึงจะเห็นค่าใหม่ (env var ไม่ live-reload) และ env var อาจรั่วไหล ผ่าน process listing หรือ crash dump ได้ง่ายกว่า
2. Volume Mount
spec:
containers:
- name: app
volumeMounts:
- name: config-volume
mountPath: /etc/config
volumes:
- name: config-volume
configMap:
name: app-config
ข้อดี: ไฟล์ที่ mount จาก ConfigMap จะอัปเดตอัตโนมัติ (ภายในไม่กี่สิบวินาที) โดยไม่ต้อง restart Pod — แอปที่ watch ไฟล์เองได้ (เช่น reload config บน SIGHUP) จะได้ hot-reload ฟรี ข้อเสีย: แอปต้องเขียนโค้ดอ่านไฟล์เอง ซับซ้อนกว่า env var เล็กน้อย
"env var = ติดป้ายตอนเกิด แก้ไม่ได้จนตาย, volume = สมุดบันทึกที่เขียนทับได้เรื่อยๆ" — env var ถูก inject ตอน container start เท่านั้น (เหมือนป้ายชื่อติดตอนเกิด) ส่วนไฟล์ที่ mount จาก ConfigMap ถูกอัปเดตได้ตลอดชีวิต container (เหมือนสมุดบันทึกที่แก้หน้าไหนก็ได้ตลอดเวลา)
กลไกที่ทำให้ volume-mounted ConfigMap "hot-reload" ได้ ใช้เทคนิคที่เรียกว่า atomic symlink
swap — kubelet ไม่ได้เขียนทับไฟล์เดิมตรงๆ แต่สร้างชุดไฟล์ใหม่ไว้ในโฟลเดอร์ซ่อน แล้วสลับ
symlink (..data) ให้ชี้ไปที่ชุดใหม่แบบ atomic operation เดียว เพื่อไม่ให้แอปที่กำลังอ่านไฟล์
อยู่พอดีเจอไฟล์ครึ่งๆ กลางๆ (partial write) — เป็นเหตุผลที่ถ้าแอปเปิดไฟล์ค้างไว้แบบ
open() ครั้งเดียวตอน start (ไม่ได้ open() ใหม่ทุกครั้งที่อ่าน) จะไม่เห็นการเปลี่ยนแปลง
เลย เพราะ file descriptor เดิมยังชี้ไปที่ inode เก่า ต้องเขียนแอปให้ open() ไฟล์ใหม่ทุกครั้ง
หรือ watch fsnotify event ถึงจะได้ hot-reload จริง
immutable ConfigMap/Secret (performance ที่มักถูกมองข้าม)
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-v2
immutable: true
data:
LOG_LEVEL: "info"
ตั้ง immutable: true เมื่อ config ไม่เปลี่ยนหลังสร้าง (เช่น ใช้ pattern สร้าง ConfigMap ใหม่
ทุกครั้งที่ deploy แทนการแก้ของเดิม) ช่วยลดภาระ kube-apiserver เพราะไม่ต้อง watch การเปลี่ยนแปลง
ของ object นั้นอีก — มีประโยชน์มากใน cluster ขนาดใหญ่ที่มี ConfigMap เยอะ
"immutable = ปิดผนึกกล่อง, ไม่แกะแล้วแกะอีก" — บอก Kubernetes ว่า "กล่องนี้ปิดผนึกแล้ว ไม่ต้อง มาคอยเฝ้าดูว่ามันเปลี่ยนไหม" ลดงานของ apiserver ที่ต้อง watch object ทุกตัวตลอดเวลา
Pattern ที่คู่กับ immutable: true เสมอในทีมที่ทำ GitOps คือ ตั้งชื่อ ConfigMap แบบมี hash
ต่อท้าย (เช่น app-config-a1b2c3) ที่คำนวณจาก content ของ config เอง — เครื่องมืออย่าง
Kustomize มี feature configMapGenerator ที่ทำแบบนี้ให้อัตโนมัติ เมื่อ config เปลี่ยน hash
เปลี่ยน ชื่อ ConfigMap เปลี่ยน ทำให้ Deployment ที่ reference ชื่อนั้นต้องถูก apply ใหม่
โดยธรรมชาติ (เพราะชื่อ reference เปลี่ยน) ซึ่งแก้ปัญหาคลาสสิกที่ว่า "แก้ ConfigMap แล้ว
Deployment ไม่ restart ให้เองอัตโนมัติ" (เพราะ env var ไม่ live-reload ตามที่กล่าวไปข้างต้น)
ได้แบบสวยงามโดยไม่ต้องเขียน annotation trigger เองด้วยมือ
Keep configuration out of the container image (12-factor). ConfigMap holds non-sensitive data
(capped at 1 MiB per object — an etcd-wide limit); Secret looks similar but only base64-
encodes values — not encryption — so etcd encryption-at-rest and tight RBAC matter, though even
with encryption at rest, mounted Secrets still sit as plaintext in the Node's memory/tmpfs, so
Node and exec-access RBAC matter just as much. Values can be injected as environment variables
(simple, but fixed at container start — no live reload) or as mounted volumes (auto-updated via
an atomic symlink swap, enabling hot-reload — but only if the app re-opens the file rather than
holding a stale file descriptor). Marking a ConfigMap/Secret immutable: true reduces API server
watch load; pairing it with content-hash-suffixed names (e.g. via Kustomize's
configMapGenerator) elegantly forces a Deployment restart whenever config actually changes.