ข้ามไปยังเนื้อหา

ConfigMaps และ Secrets

ConfigMap และ Secret แยก configuration ออกจาก container image ทำให้ image เดียวรันได้หลาย environment และ Secret โดย default แค่ base64-encode เท่านั้น ไม่ได้ encrypt

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: v1
kind: ConfigMap
metadata:
name: app-config
data:
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: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
data:
password: cGFzc3dvcmQxMjM=
Terminal window
# Anyone with read access can trivially reverse the encoding — this is not encryption
kubectl get secret db-credentials -o jsonpath='{.data.password}' | base64 --decode

ความปลอดภัยที่แท้จริงต้องเพิ่มมาตรการอื่นทับ Secret object เอง

  • เปิด encryption at rest ให้ etcd เพื่อให้ byte ที่เก็บบน disk ถูก encrypt จริง
  • RBAC ที่จำกัดแน่นหนาว่าใครหรืออะไร get/list Secret ได้บ้าง
  • 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 registry
  • kubernetes.io/tls — คู่ TLS certificate กับ private key มักถูกใช้โดย Ingress controller
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
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 ให้อัตโนมัติ

การคำนวณ base64 เองทุก Secret value ทั้งเหนื่อยทั้งพลาดง่าย stringData ให้เขียนค่าของ Secret เป็น plain text ตรง ๆ ได้ API server จะรวมมันเข้า data (encode เป็น base64) ตอนเขียน และไม่ส่ง stringData กลับมาตอนอ่านเลย

k8s/configmaps-and-secrets/secret-stringdata.yaml
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
stringData:
password: 's3cr3t-Value!'

การ encode ที่ API server ทำคือ base64 ธรรมดาเป๊ะ ๆ ทำซ้ำได้เองในเครื่องโดยไม่ต้องมี cluster เลย ซึ่งก็เป็นการสาธิตที่ชัดที่สุดว่านี่คือ encoding ไม่ใช่ encryption:

$ echo -n 's3cr3t-Value!' | base64
czNjcjN0LVZhbHVlIQ==
$ kubectl create secret generic db-credentials --from-literal=password='s3cr3t-Value!' --dry-run=client -o yaml
apiVersion: v1
data:
password: czNjcjN0LVZhbHVlIQ==
kind: Secret
metadata:
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 เลย

พอ ConfigMap หรือ Secret ตั้ง immutable: true แล้ว API server เองจะบังคับใช้จริง การพยายามเปลี่ยน data/binaryData ทีหลัง (หรือเปลี่ยน immutable กลับเป็น false) จะถูก reject ตรง ๆ ไม่ใช่แค่ถูกห้ามด้วย convention เฉย ๆ:

k8s/configmaps-and-secrets/immutable-cm.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: immutable-cm
data:
LOG_LEVEL: 'info'
immutable: true
$ kubectl apply -f immutable-cm.yaml
configmap/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 set

error text ด้านบนเป็นของจริง จับได้ตรง ๆ จาก kind cluster ของโมดูลนี้เอง (หลังจากช่วงที่เครื่องที่ใช้ร่วมกันมี CPU ขาดแคลนอย่างหนักจากหลาย agent ที่รัน kind cluster พร้อมกัน คลี่คลายแล้ว — uptime แสดง load average 20–30 ระหว่างที่มีปัญหา ยืนยันแล้วว่าเป็นปัญหาระดับเครื่องทั้งเครื่องเพราะ cluster verify-k8s ที่ใช้ร่วมกันแยกต่างหาก ก็ fail เหมือนกันในช่วงเวลาเดียวกัน) พฤติกรรมนี้ก็มีเอกสารรองรับด้วยตาม เอกสาร ConfigMap immutability ของ Kubernetes — ทางกลับมีทางเดียวคือลบสร้าง object ใหม่ (แล้ว consumer ทุกตัวที่ reference ชื่อนี้ก็ยังต้องรับ object ใหม่ด้วย)

สามวิธีที่ Pod consume ConfigMap พฤติกรรมต่างกันสิ้นเชิงเมื่อ source เปลี่ยนหลัง Pod รันไปแล้ว:

k8s/configmaps-and-secrets/propagation-demo.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: prop-cm
data:
greeting: 'v1'
---
apiVersion: v1
kind: Pod
metadata:
name: propagation-demo
spec:
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=v1
VOL=v1
SUBPATH=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 later
ENV=v1
VOL=v2
SUBPATH=v1

สาม path จาก patch เดียวกันเป๊ะ บน Pod เดียวกันเป๊ะ ตรวจหลังจากนั้น ~83 วินาที ได้ผลจริงต่างกันสามแบบ:

  • env var (GREETING_ENV) — ยังเป็น v1 resolve ครั้งเดียวตอน 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 var subPath mount คือ bind-mount ของไฟล์นั้นครั้งเดียวตอน Pod เริ่ม ไม่ได้อยู่ในกลไก re-sync เป็นระยะของ kubelet เลย — จะยังอ่านได้ v1 ไม่ว่าจะรอนานแค่ไหนก็ตาม

ทุกอย่างด้านบนพูดถึงความลับของ 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 และอื่น ๆ)

แทนที่จะพึ่ง 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 โดยเฉพาะแทน

ตอนรีวิว manifest ของ ConfigMap/Secret กับโค้ดที่ consume มัน ให้เช็ค

  • comment ในโค้ดที่บอกว่า data ของ Secret “encrypted” — พิสูจน์ด้านบนแล้วว่าเป็นแค่ base64 ใครก็ decode กลับได้ในคำสั่งเดียวถ้ามีสิทธิ์อ่าน ไม่มี key เกี่ยวข้องเลย
  • subPath mount ของ ConfigMap หรือ Secret ที่มี comment บอกว่าจะ reload ตอนเปลี่ยน — พิสูจน์ด้านบนแล้วว่า subPath mount ไม่เคย 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 ในโค้ดนี้

k8s/configmaps-and-secrets/live-reload.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: feature-flags
data:
new-checkout-flow: "true"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
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
ข้อมูล ConfigMap กับ Secret เข้าถึง Pod ผ่าน env var และผ่าน volume mount

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 mountkubelet sync update ให้อัตโนมัติยังเป็น eventually-consistent (ตาม sync period ของ kubelet) app ต้องคอยดูไฟล์เอง
subPath mountmount key เดียวโดยไม่เปิดเผยตัวอื่นทำลาย live-update ทั้งหมด พิสูจน์ด้านบน — ถาวร ไม่ใช่แค่ช้าลง
immutable: trueAPI 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 ใหม่คือทางเดียว
base64 encoding ใน Kubernetes Secret ถือเป็น encryption หรือไม่
วิธีใช้ข้อมูลแบบไหนที่รับ update ของ ConfigMap หรือ Secret ได้โดยไม่ต้อง restart container
ทำไมถึงอยากตั้ง ConfigMap เป็น immutable
Secret type ไหนที่ built-in ไว้ใช้เก็บ credential สำหรับ pull image จาก private registry
ทั้งสองคำสั่งรันโดยไม่ต่อ cluster เลย output ที่เหมือนกันเป๊ะพิสูจน์อะไร
$ echo -n 's3cr3t-Value!' | base64
czNjcjN0LVZhbHVlIQ==

$ kubectl create secret generic db-credentials --from-literal=password='s3cr3t-Value!' --dry-run=client -o yaml
apiVersion: v1
data:
  password: czNjcjN0LVZhbHVlIQ==
error text จริงนี้แปลว่าอะไรสำหรับ ConfigMap ที่เขียน immutable: true ไว้
$ kubectl apply -f immutable-cm.yaml
configmap/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 set