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

Externalized Configuration

ตอนนี้คุณ package แต่ละ service เป็น image ที่ไม่เปลี่ยนแปลงและเลื่อน image ตัวนั้นเป๊ะ ๆ จาก development ผ่าน staging ไปยัง production แต่ service ต้องการการตั้งค่าที่ต่างกันในแต่ละที่เหล่านั้น: database URL ที่ต่างกัน, ที่อยู่ message-broker ที่ต่างกัน, credential ที่ต่างกัน และ feature flag ที่เปิดใน staging และปิดใน production

ถ้าฝังค่าเหล่านั้นเข้าไปใน image คุณก็จะไม่มี artifact ก้อนเดียวอีกต่อไป แต่มี image หนึ่งตัวต่อหนึ่ง environment แต่ละตัว build จาก code ชุดเดียวกันแต่ compile ค่าต่างกันเข้าไป แปลว่า image ที่คุณทดสอบใน staging ไม่ใช่ image ที่รันจริงใน production ที่แย่กว่านั้นคือถ้าฝัง database password เข้าไปด้วย secret ของคุณจะไปนอนอยู่ใน image layer ใน registry ให้ใครก็ตามที่ pull ได้เปิดอ่าน

สิ่งที่คุณต้องการคือ image ที่ build แล้วก้อนเดียวรันได้ทุก environment โดยไม่ต้องแก้ ของที่ทดสอบจะได้เป็นของเดียวกับที่ส่งมอบเป๊ะ ๆ นั่นแปลว่าค่าที่ผูกกับ environment อย่าง endpoint, credential และ feature flag อยู่ใน image ไม่ได้ ต้องมาจากภายนอกและส่งเข้ามาตอนที่ service เริ่มรันในแต่ละที่ ส่วนค่าที่อ่อนไหวอย่าง password และ token ต้องจัดการในฐานะ secret ไม่ใช่ plain text ที่นอนอยู่ใน image หรือใน repository

ดังนั้นแรงที่ขัดแย้งกันคือ: artifact หนึ่งชิ้นที่ไม่เปลี่ยนแปลงและเป็นกลางต่อสภาพแวดล้อม แต่มีการตั้งค่าที่ถูกต้อง เฉพาะสภาพแวดล้อม และปลอดภัยต่อ secret ตอน runtime

คำตอบคือใช้ externalized configuration ให้ image บรรจุแค่ code กับค่าเริ่มต้นที่สมเหตุสมผล ส่วนค่าที่ผูกกับ environment ทั้งหมด inject เข้ามาตอน runtime ตัว service จะอ่าน configuration ตอนเริ่มทำงาน และถ้าเป็นบางแหล่งก็อ่านระหว่างรันได้ด้วย แทนที่จะอ่านจากค่าคงที่ที่ compile ติดมากับ build

มีกลไกการ inject ที่พบบ่อยสามแบบ ซึ่งมักใช้ร่วมกัน:

  • Environment variable — platform ตั้งค่าเช่น database URL เข้าไปในสภาพแวดล้อมของ service ตอน launch ง่าย ใช้ได้ทั่วไป และเป็นรูปแบบที่พบบ่อยที่สุด
  • Configuration server — service ดึงการตั้งค่าจาก config service ส่วนกลางตอนเริ่มทำงาน ซึ่งเก็บ configuration ของทั้งกองทัพไว้ในที่เดียวที่ถูกควบคุมและสามารถ push การอัปเดตได้
  • Secrets store — ค่าที่อ่อนไหวอยู่ใน secrets manager เฉพาะและถูก mount หรือดึงตอน runtime ไม่เคยถูกเขียนเข้าไปใน image หรือ source control

image ตัวเดียวกัน เริ่มใน staging ก็หยิบค่าของ staging มา เริ่มใน production ก็หยิบค่าของ production มา artifact เหมือนกัน มีเพียง configuration ที่ inject เข้ามาเท่านั้นที่ต่างกัน

flowchart LR
  subgraph Sources[Config sources per environment]
    Env[Environment variables]
    CfgSrv[Config server]
    Sec[Secrets store]
  end
  Img[Immutable image: code + defaults] --> Svc[Service at startup]
  Env -->|inject| Svc
  CfgSrv -->|fetch| Svc
  Sec -->|mount / fetch| Svc
  Svc --> Run[Running service, configured for this environment]
image ที่ไม่เปลี่ยนแปลงตัวเดียว; environment variable, config server และ secrets store inject ค่าต่อสภาพแวดล้อมตอนเริ่มทำงาน

image ไม่พ่วงค่าของ environment ใดมาด้วยเลย การ deploy ในแต่ละ environment จะเป็นตัว inject ค่าเข้าไป ค่าธรรมดามาจาก environment variable ส่วน database password อ้างอิงจาก secret จะได้ไม่โผล่ทั้งใน manifest และใน image

# Production deployment — same image as staging, different injected values.
env:
- name: SERVICE_ENV
value: production
- name: ORDER_DB_URL
value: postgres://orders-db.prod.internal:5432/orders
- name: BROKER_URL
value: kafka://broker.prod.internal:9092
- name: FEATURE_NEW_CHECKOUT
value: "false"
- name: ORDER_DB_PASSWORD
valueFrom:
secretRef:
name: order-db-credentials
key: password

service อ่านสิ่งเหล่านี้ตอนเริ่มทำงาน การเลื่อนขึ้น production หมายถึงการ deploy image ตัวเดียวกัน ด้วย manifest นี้แทนที่จะเป็นของ staging password ถูก resolve จาก secrets store ตอน launch และไม่เคยลงไปอยู่ใน image layer

สิ่งที่คุณได้:

  • image เดียว ทุกสภาพแวดล้อม artifact ที่ไม่เปลี่ยนแปลงเพียงชิ้นเดียวถูกเลื่อนจาก staging ไป production โดยไม่ต้องแก้ ดังนั้นสิ่งที่คุณทดสอบจึงเป็นสิ่งที่คุณรันเป๊ะ ๆ — ไม่มีการ build ใหม่ต่อสภาพแวดล้อม
  • secret ปลอดภัยขึ้น credential อยู่ใน secrets store และ inject เข้ามาตอน runtime จึงไม่ตกค้างอยู่ใน image layer และไม่หลุดเข้า source control
  • การตั้งค่าจากส่วนกลางที่เปลี่ยนได้ ด้วย config server configuration ของทั้งกองทัพอยู่ในที่เดียวที่ถูกควบคุม และบางค่าสามารถเปลี่ยนได้โดยไม่ต้อง build ใหม่หรือแม้แต่ redeploy

สิ่งที่คุณต้องเสีย:

  • การจัดการ secret กลายเป็นระบบเต็มตัว คุณต้องดูแล secrets store คุมสิทธิ์เข้าถึง และ rotate credential การ externalize config ไม่ได้ทำให้เรื่อง secret ง่ายขึ้น แค่ทำให้เห็นชัดขึ้นว่าต้องจัดการ
  • มี dependency เพิ่มตอน startup ถ้า service ดึง config จาก config server หรือ secrets store แหล่งนั้นจะกลายเป็น dependency ที่ต้องมีถึงจะเริ่มทำงานได้ จึงต้อง available สูงและมี fallback ที่สมเหตุสมผล
  • การกระจัดกระจายและ drift ของ config การตั้งค่าที่กระจายข้าม env var, config server และ secret สามารถ drift ระหว่างสภาพแวดล้อมได้ คุณต้องมีข้อตกลง, การ validate ตอนเริ่มทำงาน และแหล่งความจริงเดียวต่อแต่ละเรื่อง
  • Service per Container — externalized configuration คือสิ่งที่ทำให้ container image ที่ไม่เปลี่ยนแปลงเป็นกลางต่อสภาพแวดล้อมอย่างแท้จริง
  • Microservice Chassis — chassis มักเป็น component ที่อ่านและ validate configuration ที่ inject เข้ามา
  • Serverless Deployment — function รับการตั้งค่าในแบบเดียวกัน จากค่าที่ inject เข้ามาแทนที่จะเป็นค่าคงที่ที่ฝังไว้
ข้อดีข้อแลกเปลี่ยน
เปลี่ยน config โดยไม่ต้อง rebuild หรือ redeployconfig store เพิ่มเติมที่ต้องดูแล — อีก component ที่ล้มได้
config แตกต่างระหว่าง environment โดยไม่ต้องแก้ codesecret management ซับซ้อนขึ้น — ต้องป้องกัน secret ใน config store
rollback config ได้โดยไม่ rollback codeconfig ที่ผิดทำให้ service พังทั้งหมดพร้อมกัน
audit trail ว่า config เปลี่ยนเมื่อไรและโดยใครต้องมี fallback ถ้า config store ไม่ available ตอน start

Secret ใน Environment Variable ธรรมดา — ส่ง secret ผ่าน env var โดยไม่ encrypt อาการ:

  • secret อยู่ใน docker-compose.yml ที่ commit ใน git
  • process list บน server แสดง secret ใน command line argument
  • ใช้ secret manager จริง ๆ (AWS Secrets Manager, Vault) แทน

Config Drift — config ระหว่าง environment ต่างกันโดยไม่ตั้งใจ อาการ:

  • ทำงานใน staging แต่พังใน production
  • timeout ใน dev ต่างจาก production ทำให้ load test ไม่ reflect ความเป็นจริง

💡 ตัวอย่างจากของจริง

Netflix:

  • ใช้ Archaius สำหรับ dynamic configuration
  • เปลี่ยน config ได้ใน runtime โดยไม่ restart service
  • ใช้สำหรับ feature flag และ A/B testing config

Kubernetes:

  • ConfigMap สำหรับ non-secret config
  • Secret object สำหรับ sensitive data (โดย default เป็นแค่ base64-encoded ไม่ได้ encrypt — ต้องเปิด encryption at rest / ใช้ KMS เองถึงจะ encrypt จริง)
  • environment เปลี่ยน config โดยการ swap ConfigMap — ไม่ต้อง rebuild image
อะไรคือแนวคิดหลักของ externalized configuration?
ทำไมการฝัง database password เข้าไปใน image จึงเป็นปัญหา?
ข้อใดคือกลไกที่พบบ่อยในการ inject configuration ตอน runtime?
ข้อใดคือต้นทุนจริงของการ externalize configuration?