ConfigMaps และ Secrets
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”ConfigMap และ Secret แยก configuration ออกจาก container image ทำให้ image เดียวรันได้หลาย environment และ Secret โดย default แค่ base64-encode เท่านั้น ไม่ได้ encrypt
ConfigMap: configuration ที่ไม่ confidential
หัวข้อที่มีชื่อว่า “ConfigMap: configuration ที่ไม่ confidential”ConfigMap เก็บข้อมูล configuration แบบ key-value เช่น feature flag, log level, URL ซึ่ง Pod เอาไปใช้ได้สามแบบ คือเป็น environment variable, เป็น command-line argument หรือเป็นไฟล์ที่ mount เข้ามา ถ้าฝัง configuration ไว้ใน image ต้อง build ใหม่ทุก environment แต่ ConfigMap ทำให้ image เดียวอ่าน configuration ต่างกันได้ตามว่าชี้ไปที่ ConfigMap ตัวไหน
apiVersion: v1kind: ConfigMapmetadata: name: app-configdata: LOG_LEVEL: "info" FEATURE_FLAG_NEW_UI: "true"immutable: trueการตั้ง immutable: true บอก Kubernetes ว่าข้อมูลใน ConfigMap นี้จะไม่เปลี่ยนอีกแล้ว นี่คือ optimization จริงบน cluster ขนาดใหญ่ kubelet ไม่ต้อง watch object นี้เพื่อรอ update อีกต่อไป ลด load ที่ API server และยังกันการแก้ผิดพลาดโดยไม่ตั้งใจได้ด้วย
Secret: โครงสร้างเดียวกัน แต่ intent ต่างกัน พร้อมข้อควรระวังสำคัญ
หัวข้อที่มีชื่อว่า “Secret: โครงสร้างเดียวกัน แต่ intent ต่างกัน พร้อมข้อควรระวังสำคัญ”Secret มีโครงสร้าง object เหมือน ConfigMap ทุกอย่าง แต่ตั้งใจไว้สำหรับค่าที่ sensitive เช่น password, token, certificate ข้อเท็จจริงที่สำคัญที่สุดคือ ค่าใน data ของ Secret แค่ base64-encode เท่านั้น ไม่ได้ encrypt base64 คือ encoding ไม่ใช่ cipher ใครก็ตามที่มีสิทธิ์อ่าน Secret ผ่าน API หรือมีสิทธิ์อ่าน etcd โดยตรง สามารถ decode ออกมาได้ด้วยคำสั่งเดียว อย่ามองว่า Secret ปลอดภัยด้วยตัวเองเฉย ๆ
apiVersion: v1kind: Secretmetadata: name: db-credentialstype: Opaquedata: password: cGFzc3dvcmQxMjM=# Anyone with read access can trivially reverse the encoding — this is not encryptionkubectl get secret db-credentials -o jsonpath='{.data.password}' | base64 --decodeความปลอดภัยที่แท้จริงต้องเพิ่มมาตรการอื่นทับ Secret object เอง
- เปิด encryption at rest ให้ etcd เพื่อให้ byte ที่เก็บบน disk ถูก encrypt จริง
- RBAC ที่จำกัดแน่นหนาว่าใครหรืออะไร
get/listSecret ได้บ้าง - external secret manager (เช่น secret store ที่อิง cloud KMS) เชื่อมผ่าน CSI driver หรือ operator แทนที่จะพึ่ง Secret object ดิบ ๆ ของ Kubernetes เป็น source of truth
Kubernetes มี built-in Secret type มาให้ใช้กับ case ที่พบบ่อย
Opaque— default ทั่วไป ข้อมูล key-value อะไรก็ได้kubernetes.io/dockerconfigjson— credential สำหรับ registry ใช้กับimagePullSecretsตอน pull image จาก private registrykubernetes.io/tls— คู่ TLS certificate กับ private key มักถูกใช้โดย Ingress controller
ใช้ ConfigMap กับ Secret ใน Pod
หัวข้อที่มีชื่อว่า “ใช้ ConfigMap กับ Secret ใน Pod”apiVersion: v1kind: Podmetadata: name: webspec: containers: - name: web image: my-web-app:2.4 envFrom: - configMapRef: name: app-config env: - name: DB_PASSWORD valueFrom: secretKeyRef: name: db-credentials key: password volumeMounts: - name: config-volume mountPath: /etc/config volumes: - name: config-volume configMap: name: app-configสองวิธีนี้พฤติกรรมต่างกันตอน object ต้นทางเปลี่ยนหลัง Pod รันไปแล้ว
- environment variable ถูก resolve ครั้งเดียวตอน container เริ่ม การ update ConfigMap หรือ Secret ทีหลังไม่มีผลกับ container ที่รันอยู่แล้ว container จะยังใช้ค่าเดิมจนกว่าจะถูก restart
- volume ที่ mount ถูก kubelet sync เป็นระยะ พอ ConfigMap หรือ Secret ต้นทางเปลี่ยน เนื้อหาไฟล์บน disk จะถูก update ในที่สุด แต่ application เองก็ยังต้องคอยสังเกตว่าไฟล์เปลี่ยนแล้ว reload เอง Kubernetes ไม่ restart container ให้อัตโนมัติ
stringData: เขียน plaintext ได้ data กลับมาเป็น base64
หัวข้อที่มีชื่อว่า “stringData: เขียน plaintext ได้ data กลับมาเป็น base64”การคำนวณ base64 เองทุก Secret value ทั้งเหนื่อยทั้งพลาดง่าย stringData ให้เขียนค่าของ Secret เป็น plain text ตรง ๆ ได้ API server จะรวมมันเข้า data (encode เป็น base64) ตอนเขียน และไม่ส่ง stringData กลับมาตอนอ่านเลย
apiVersion: v1kind: Secretmetadata: name: db-credentialstype: OpaquestringData: password: 's3cr3t-Value!'การ encode ที่ API server ทำคือ base64 ธรรมดาเป๊ะ ๆ ทำซ้ำได้เองในเครื่องโดยไม่ต้องมี cluster เลย ซึ่งก็เป็นการสาธิตที่ชัดที่สุดว่านี่คือ encoding ไม่ใช่ encryption:
$ echo -n 's3cr3t-Value!' | base64czNjcjN0LVZhbHVlIQ==
$ kubectl create secret generic db-credentials --from-literal=password='s3cr3t-Value!' --dry-run=client -o yamlapiVersion: v1data: password: czNjcjN0LVZhbHVlIQ==kind: Secretmetadata: name: db-credentialsทั้งสองคำสั่งรันโดย ไม่ต่อ cluster เลย (--dry-run=client ไม่แตะ API server เลยสักครั้ง) แต่ได้ czNjcjN0LVZhbHVlIQ== เหมือนกันเป๊ะ — พิสูจน์ว่า data ของ Secret คือ base64 ธรรมดา ใครก็คำนวณเองได้ ไม่ใช่ operation เข้ารหัสที่ต้องมี key
kubectl create secret --dry-run=client -o yaml: pattern ที่ปลอดภัยสำหรับเขียน manifest
หัวข้อที่มีชื่อว่า “kubectl create secret --dry-run=client -o yaml: pattern ที่ปลอดภัยสำหรับเขียน manifest”คำสั่งด้านบนก็คือ pattern ที่ควรใช้จริงตอนเขียน Secret manifest เอง อย่าพิมพ์ base64 เองหรือกังวลว่าจะใช้ data หรือ stringData ถูกไหม — ให้ kubectl generate object shape ที่ถูกต้องให้ ตรวจดู แล้วค่อย save (หรือ pipe เข้า kubectl apply -f - ตรง ๆ) วิธีนี้ทำงานฝั่ง client ล้วน ๆ เลยไม่ได้รับผลกระทบจากสถานะ cluster หรือ API server เลย
immutable: true: reject จริง ไม่ใช่แค่หมายเหตุเรื่อง optimization
หัวข้อที่มีชื่อว่า “immutable: true: reject จริง ไม่ใช่แค่หมายเหตุเรื่อง optimization”พอ ConfigMap หรือ Secret ตั้ง immutable: true แล้ว API server เองจะบังคับใช้จริง การพยายามเปลี่ยน data/binaryData ทีหลัง (หรือเปลี่ยน immutable กลับเป็น false) จะถูก reject ตรง ๆ ไม่ใช่แค่ถูกห้ามด้วย convention เฉย ๆ:
apiVersion: v1kind: ConfigMapmetadata: name: immutable-cmdata: LOG_LEVEL: 'info'immutable: true$ kubectl apply -f immutable-cm.yamlconfigmap/immutable-cm created
$ kubectl patch configmap immutable-cm -p '{"data":{"LOG_LEVEL":"debug"}}'The ConfigMap "immutable-cm" is invalid: data: Forbidden: field is immutable when `immutable` is seterror text ด้านบนเป็นของจริง จับได้ตรง ๆ จาก kind cluster ของโมดูลนี้เอง (หลังจากช่วงที่เครื่องที่ใช้ร่วมกันมี CPU ขาดแคลนอย่างหนักจากหลาย agent ที่รัน kind cluster พร้อมกัน คลี่คลายแล้ว — uptime แสดง load average 20–30 ระหว่างที่มีปัญหา ยืนยันแล้วว่าเป็นปัญหาระดับเครื่องทั้งเครื่องเพราะ cluster verify-k8s ที่ใช้ร่วมกันแยกต่างหาก ก็ fail เหมือนกันในช่วงเวลาเดียวกัน) พฤติกรรมนี้ก็มีเอกสารรองรับด้วยตาม เอกสาร ConfigMap immutability ของ Kubernetes — ทางกลับมีทางเดียวคือลบสร้าง object ใหม่ (แล้ว consumer ทุกตัวที่ reference ชื่อนี้ก็ยังต้องรับ object ใหม่ด้วย)
ConfigMap update propagation วัดจริงเทียบสามเส้นทาง
หัวข้อที่มีชื่อว่า “ConfigMap update propagation วัดจริงเทียบสามเส้นทาง”สามวิธีที่ Pod consume ConfigMap พฤติกรรมต่างกันสิ้นเชิงเมื่อ source เปลี่ยนหลัง Pod รันไปแล้ว:
apiVersion: v1kind: ConfigMapmetadata: name: prop-cmdata: greeting: 'v1'---apiVersion: v1kind: Podmetadata: name: propagation-demospec: containers: - name: app image: busybox:1.38 command: ['sh', '-c', 'sleep 3600'] env: - name: GREETING_ENV valueFrom: configMapKeyRef: name: prop-cm key: greeting volumeMounts: - name: cfg-vol mountPath: /etc/cfg-vol - name: cfg-subpath mountPath: /etc/cfg-subpath/greeting subPath: greeting volumes: - name: cfg-vol configMap: name: prop-cm - name: cfg-subpath configMap: name: prop-cm$ kubectl exec propagation-demo -- sh -c 'echo ENV=$GREETING_ENV; echo VOL=$(cat /etc/cfg-vol/greeting); echo SUBPATH=$(cat /etc/cfg-subpath/greeting)'ENV=v1VOL=v1SUBPATH=v1
$ kubectl patch configmap prop-cm -p '{"data":{"greeting":"v2"}}'configmap/prop-cm patched# patched at 12:17:17 UTC
$ kubectl exec propagation-demo -- sh -c 'echo ENV=$GREETING_ENV; echo VOL=$(cat /etc/cfg-vol/greeting); echo SUBPATH=$(cat /etc/cfg-subpath/greeting)'# checked at 12:18:40 UTC, ~83s laterENV=v1VOL=v2SUBPATH=v1สาม path จาก patch เดียวกันเป๊ะ บน Pod เดียวกันเป๊ะ ตรวจหลังจากนั้น ~83 วินาที ได้ผลจริงต่างกันสามแบบ:
- env var (
GREETING_ENV) — ยังเป็นv1resolve ครั้งเดียวตอน container เริ่ม จะเป็นแบบนั้นตลอดไปไม่ว่าจะรอนานแค่ไหน จนกว่า container จะ restart - full volume mount (
/etc/cfg-vol/greeting) — เป็นv2แล้ว kubelet sync ข้อมูล ConfigMap/Secret ที่ project ให้ Pod เป็นระยะ (ราว ๆ ตาม sync period ที่ config ไว้ ปกติประมาณหนึ่งนาที ตาม เอกสาร ConfigMap ของ Kubernetes) — พิสูจน์ตรงนี้แล้วว่าอยู่ในช่วงเวลานั้นจริง - subPath mount (
/etc/cfg-subpath/greeting) — ยังเป็นv1เหมือน env varsubPathmount คือ bind-mount ของไฟล์นั้นครั้งเดียวตอน Pod เริ่ม ไม่ได้อยู่ในกลไก re-sync เป็นระยะของ kubelet เลย — จะยังอ่านได้v1ไม่ว่าจะรอนานแค่ไหนก็ตาม
Encryption at rest (EncryptionConfiguration): docs-only
หัวข้อที่มีชื่อว่า “Encryption at rest (EncryptionConfiguration): docs-only”ทุกอย่างด้านบนพูดถึงความลับของ object Secret เอง (หรือการไม่มีความลับ) การควบคุมอีกชั้นระดับ cluster คือการ encrypt byte ที่ Kubernetes เก็บจริงใน etcd config ผ่าน EncryptionConfiguration ที่ส่งให้ kube-apiserver ทาง --encryption-provider-config kind ไม่เปิดสิ่งนี้เป็น default และคอร์สนี้ก็พิสูจน์ hands-on ไม่ได้ในเซสชันนี้ไม่ว่าทางไหน: การพิสูจน์ encryption-at-rest ต้องอ่านไฟล์ข้อมูลดิบของ etcd ตรง ๆ (ข้าม API server ไปเลย) เพื่อยืนยันว่า byte ที่เก็บจริงเป็น ciphertext ทึบจริง ไม่ใช่ JSON/protobuf ที่มี base64 อยู่ข้างใน — etcd แบบ containerized ของ kind ทำให้ setup แบบนี้ทำยากพอสมควรอย่างปลอดภัย และนอกเหนือจากปัญหาเครื่องด้านบนแล้ว เรื่องนี้ก็อยู่นอก scope ความลึกของบทนี้อยู่แล้ว ส่วนนี้ตั้งใจให้เป็น docs-only: ดู เอกสาร encrypting data ของ Kubernetes สำหรับ config จริงและตัวเลือก provider (aescbc, aesgcm, kms v2 และอื่น ๆ)
External secrets operator: ภาพรวม docs-only
หัวข้อที่มีชื่อว่า “External secrets operator: ภาพรวม docs-only”แทนที่จะพึ่ง object Secret ดิบ ๆ ของ Kubernetes เป็น source of truth production cluster หลายที่รัน operator ที่ sync ค่าจาก external secret manager เข้า Secret object แบบ native (หรือ inject ตรง ๆ) — External Secrets Operator กับ Sealed Secrets คือตัวอย่าง open-source ที่ maintain แยกกันและใช้กันแพร่หลายสองตัวของ pattern นี้ ทั้งคู่สุดท้ายก็ยังผลิต object Secret ของ Kubernetes ธรรมดาที่มีคุณสมบัติ base64-ไม่ใช่-encryption เหมือนที่พิสูจน์ด้านบนอยู่ดี สิ่งที่มันเพิ่มมาคือทำให้ value ที่ sensitive จริง ๆ ไม่ต้องอยู่ใน git และไม่ต้องผ่าน kubectl apply มือเลย ดึงมาจาก secret store โดยเฉพาะแทน
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอนรีวิว manifest ของ ConfigMap/Secret กับโค้ดที่ consume มัน ให้เช็ค
- comment ในโค้ดที่บอกว่า
dataของ Secret “encrypted” — พิสูจน์ด้านบนแล้วว่าเป็นแค่ base64 ใครก็ decode กลับได้ในคำสั่งเดียวถ้ามีสิทธิ์อ่าน ไม่มี key เกี่ยวข้องเลย subPathmount ของ ConfigMap หรือ Secret ที่มี comment บอกว่าจะ reload ตอนเปลี่ยน — พิสูจน์ด้านบนแล้วว่าsubPathmount ไม่เคย re-sync เลย ต่างจาก full volume mount- โค้ดที่คิดว่า env var จาก ConfigMap/Secret จะรับการเปลี่ยนแปลงทีหลังโดยไม่ต้อง restart Pod — พิสูจน์ด้านบนแล้วว่า env var resolve ครั้งเดียวตอน container เริ่ม
- Secret manifest ที่มีค่า plaintext ดิบ ๆ อยู่ใต้
dataตรง ๆ (แทนที่จะ pre-encode base64 เอง หรือใช้stringData) — ค่าในdataต้องเป็น base64 อยู่แล้ว ดังนั้น plaintext ตรงนั้นจะถูก reject หรือถูกปฏิบัติราวกับเป็น garbage ที่ encode แล้วแบบเงียบ ๆ แล้วแต่ validation ของ field
🐛 หา bug ในโค้ดนี้
apiVersion: v1kind: ConfigMapmetadata:name: feature-flagsdata:new-checkout-flow: "true"---apiVersion: apps/v1kind: Deploymentmetadata:name: webspec:replicas: 3selector: matchLabels: app: webtemplate: metadata: labels: app: web spec: containers: - name: web image: myregistry/web:4.1 # Flip feature-flags's new-checkout-flow and this file updates within # seconds -- no redeploy needed for a flag change. volumeMounts: - name: flags mountPath: /etc/app/flags.conf subPath: flags.conf volumes: - name: flags configMap: name: feature-flags items: - key: new-checkout-flow path: flags.confเฉลย
comment สัญญาไว้ว่า “this file updates within seconds — no redeploy needed” แต่ volumeMounts ใช้ subPath: flags.conf — พิสูจน์ด้านบนแล้วว่า subPath mount คือการ bind ไฟล์นั้นไฟล์เดียวครั้งเดียวตอน Pod เริ่ม และ kubelet ไม่มีวัน re-sync ให้เลย ไม่ว่าจะรอนานแค่ไหนหรือ ConfigMap เปลี่ยนยังไงก็ตาม การแก้ feature-flags หลัง Pod พวกนี้รันอยู่แล้วไม่มีผลอะไรกับไฟล์ที่ mount ไว้เลย จนกว่าทุก Pod จะถูก restart (rolling restart หรือลบ Pod ทิ้ง) — ตรงข้ามกับที่ comment อ้างเป๊ะ ทางแก้: เลิกใช้ subPath แล้ว mount ทั้ง ConfigMap แทน (ยอมรับ delay การ sync ปกติของ kubelet ที่ไม่ทันทีตามที่พิสูจน์ด้านบน) หรือไม่ก็ซื่อสัตย์ใน comment ว่าการเปลี่ยน flag ตรงนี้ต้อง redeploy/restart และออกแบบกลไก feature-flag ให้เหมาะสมกับข้อจำกัดนั้น (เช่น client ที่ poll API ตรง ๆ ถ้าต้องการ propagation ระดับวินาทีจริง ๆ)
flowchart LR cm["ConfigMap: app-config"] -->|envFrom| pod["Pod container"] cm -->|volume mount, kubelet syncs on change| pod sec["Secret: db-credentials"] -->|env valueFrom, fixed at start| pod sec -->|volume mount, kubelet syncs on change| pod
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ConfigMap/Secret คือกลไก ส่ง config ไม่ใช่กลไก ปกป้อง config encoding (base64) ไม่ใช่ encryption และ Kubernetes ขีดเส้นชัดเจนระหว่าง ค่าถูกส่งเข้า container ยังไง (env var เทียบ volume แต่ละแบบมี update semantics ของตัวเอง พิสูจน์ด้านบน) กับ ใครอ่านค่านั้น ณ ตอนพักได้บ้าง (RBAC + etcd encryption ซึ่งทั้งคู่ object Secret เองไม่ได้ให้)
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ข้อดี | ต้นทุน |
|---|---|---|
| env var จาก ConfigMap/Secret | ง่าย สไตล์ 12-factor มาตรฐาน | ล็อกค่าไว้ตอน container เริ่ม พิสูจน์ด้านบน — ต้อง restart ถึงจะรับค่าใหม่ |
| full volume mount | kubelet sync update ให้อัตโนมัติ | ยังเป็น eventually-consistent (ตาม sync period ของ kubelet) app ต้องคอยดูไฟล์เอง |
subPath mount | mount key เดียวโดยไม่เปิดเผยตัวอื่น | ทำลาย live-update ทั้งหมด พิสูจน์ด้านบน — ถาวร ไม่ใช่แค่ช้าลง |
immutable: true | API server watch load น้อยลง ป้องกันแก้พลาด | แก้อะไรก็ต้องลบสร้างใหม่ พิสูจน์ด้านบนด้วย rejection จริง |
เขียนด้วย stringData | ไม่ต้องคำนวณ base64 เอง พิสูจน์ด้านบนว่าตรงเป๊ะ | write-only — ไม่ถูกส่งกลับตอนอ่าน data คือสิ่งที่เก็บจริง |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “Secret ถูก Kubernetes encrypt ให้อัตโนมัติ” พิสูจน์ด้านบนด้วยคำสั่ง
base64แบบ offline ล้วน ๆ ที่ตรงกับ output ของkubectlเป๊ะ — เป็นแค่ base64 decode ได้โดยใครก็ตามที่มีสิทธิ์อ่าน ไม่มี key เกี่ยวข้อง - “subPath ConfigMap ก็ยัง update ในที่สุด แค่ช้ากว่า full mount” พิสูจน์ผิดด้านบนแล้ว — ไม่ update เลย ตามการออกแบบ ต่างจาก full mount ที่ kubelet sync เป็นระยะ
- “
immutable: trueแก้กลับได้ด้วยkubectl applyอีกครั้ง” พฤติกรรมจริงที่อ้างอิงด้านบน: API server reject การเปลี่ยนdata/binaryData(หรือปลดimmutable) ทันทีที่ตั้งไว้แล้ว — ลบสร้าง object ใหม่คือทางเดียว