รูปแบบการกำหนดค่าสำหรับสภาพแวดล้อม Dev, Staging และ Prod
รูปแบบการกำหนดค่าสำแวดล้อมที่ช่วยเก็บ URL, คีย์ และฟีเจอร์แฟลกให้นอกโค้ด สำหรับเว็บ, แบ็คเอนด์ และมือถือ ใน dev, staging, และ prod

ทำไมการใส่ค่าคอนฟิกแบบ hardcoded ถึงเป็นปัญหาซ้ำแล้วซ้ำเล่า
การใส่คอนฟิกแบบ hardcoded ดูเหมือนจะใช้ได้ในวันแรก แต่เมื่อคุณต้องการสภาพแวดล้อม staging, API ที่สอง หรือสวิตช์ฟีเจอร์อย่างรวดเร็ว การเปลี่ยนแปลงที่ “เรียบง่าย” กลับกลายเป็นความเสี่ยงระหว่างการปล่อยแพ็กเกจ วิธีแก้ง่าย ๆ คืออย่าใส่ค่าต่าง ๆ ลงในไฟล์ซอร์สโค้ด ให้เก็บไว้ในการตั้งค่าที่คาดเดาได้แทน
สิ่งที่มักก่อปัญหามีดังนี้:
- URL ของ API ฝังอยู่ในแอป (เรียก prod ขณะทดสอบ หรือเรียก dev หลังการปล่อย)
- API keys ถูกคอมมิตลงในรีโป (ข้อมูลรั่ว, ค่าใช้จ่ายไม่คาดคิด, การหมุนคีย์ฉุกเฉิน)
- ฟีเจอร์ท็อกเกิลเขียนเป็นค่าคงที่ (ต้องปล่อยโค้ดเพื่อปิดสิ่งที่เปิดไว้)
- ID ของระบบวิเคราะห์หรือรายงานข้อผิดพลาดถูกใส่ในโค้ด (ข้อมูลไปผิดที่)
“แค่เปลี่ยนสำหรับ prod” สร้างนิสัยแก้ไขนาทีสุดท้าย แก้แล้วมักข้ามการรีวิว การทดสอบ และความสามารถทำซ้ำ คนหนึ่งเปลี่ยน URL อีกคนเปลี่ยนคีย์ และสุดท้ายคุณตอบไม่ได้ว่า: คอนฟิกไหนที่มากับบิลด์นี้
ตัวอย่างสถานการณ์ทั่วไป: คุณสร้างเวอร์ชันมือถือใหม่ต่อกับ staging แล้วมีคนเปลี่ยน URL เป็น prod ก่อนปล่อยจริง วันถัดไป backend เปลี่ยนอีกครั้งและคุณต้องย้อนกลับ ถ้า URL ถูกฝังไว้ การย้อนกลับหมายถึงการอัพเดตแอปอีกครั้ง ผู้ใช้ต้องรอ และตั๋วซัพพอร์ตพอกพูน
เป้าหมายคือโครงสร้างเรียบง่ายที่ใช้ได้ทั้งเว็บ, backend (Go) และมือถือ (Flutter):
- กฎชัดเจนว่าค่าใดควรอยู่ในโค้ด vs คอนฟิก
- ค่าเริ่มต้นที่ปลอดภัยสำหรับ dev, staging, prod
- สวิตช์ฟีเจอร์ที่เปลี่ยนได้โดยไม่ต้องคอมไพล์ใหม่
- การเก็บความลับอยู่นอกฐานโค้ด และสามารถหมุนคีย์ได้
อะไรที่จริง ๆ เปลี่ยนระหว่าง dev, staging, prod
Dev, staging, และ prod ควรรู้สึกเหมือนเป็นแอปตัวเดียวกันที่รันในสามที่ต่างกัน จุดประสงค์คือเปลี่ยนค่า ไม่ใช่พฤติกรรม
สิ่งที่ควรเปลี่ยนคือทุกอย่างที่ผูกกับที่ที่แอปรันหรือผู้ใช้: base URL และ hostname, ข้อมูลรับรอง, การเชื่อมต่อแบบ sandbox vs ของจริง และการควบคุมความปลอดภัย เช่น ระดับการล็อกหรือการตั้งค่าความปลอดภัยที่เข้มงวดขึ้นใน prod
สิ่งที่ควรคงที่คือโลจิกและสัญญาระหว่างส่วนต่าง ๆ เส้นทาง API, รูปแบบคำร้องและคำตอบ, ชื่อฟีเจอร์ และกฎธุรกิจแกนหลักไม่ควรเปลี่ยนตามสภาพแวดล้อม ถ้า staging มีพฤติกรรมต่างไป มันจะไม่เป็นสนามซ้อมที่เชื่อถือได้สำหรับ production
กฎปฏิบัติ: สร้างสภาพแวดล้อมใหม่เมื่อต้องการระบบแยกจริง ๆ (ข้อมูลแยก, การเข้าถึงแยก, ความเสี่ยงแยก) ถ้าคุณแค่ต้องการ endpoint หรือตัวเลขที่ต่างกัน ให้เพิ่มเป็นค่าคอนฟิกแทน
ตัวอย่าง: คุณอยากทดสอบผู้ให้บริการค้นหาใหม่ ถ้าปลอดภัยที่จะเปิดให้กลุ่มเล็ก ๆ ให้ใช้ feature flag ใน staging เดียวกัน แต่ถ้าต้องการฐานข้อมูลแยกและการควบคุมการเข้าถึงเข้มงวด นั่นคือเวลาที่ควรสร้างสภาพแวดล้อมใหม่
โมเดลคอนฟิกที่ใช้ซ้ำได้ทั่วทั้งระบบ
การตั้งค่าที่ดีมีจุดประสงค์เดียว: ทำให้ยากที่จะเผลอปล่อย URL dev, คีย์ทดสอบ, หรือฟีเจอร์ที่ยังไม่เสร็จ
ใช้สามชั้นเดียวกันสำหรับทุกแอป (เว็บ, backend, มือถือ):
- Defaults: ค่าปลอดภัยที่ใช้ได้ในหลายที่
- Environment overrides: ค่าที่เปลี่ยนตาม dev, staging, prod
- Secrets: ค่าที่อ่อนไหวและไม่เคยอยู่ในรีโป
เพื่อหลีกเลี่ยงความสับสน ให้เลือกแหล่งความจริงหนึ่งที่ต่อแอปแล้วยึดตามมัน เช่น backend อ่านจาก environment variables ตอนเริ่ม, เว็บอ่านจากตัวแปรตอน build หรือไฟล์คอนฟิก runtime ขนาดเล็ก, มือถืออ่านจากไฟล์ environment เล็ก ๆ ที่เลือกตอน build ความสม่ำเสมอภายในแต่ละแอปสำคัญกว่าการบังคับใช้กลไกเดียวกันทุกที่
โครงแบบง่ายที่ใช้ซ้ำได้:
- Defaults อยู่ในโค้ดเป็นค่าคงที่ที่ไม่อ่อนไหว (timeouts, page size, retry counts)
- Overrides อยู่ในไฟล์เฉพาะ env หรือตัวแปร environment (API base URL, เปิด/ปิด analytics)
- Secrets อยู่ใน secret store และฉีดเข้าในขั้นตอน deploy/build (JWT secret, database password, third-party API keys)
การตั้งชื่อให้คนเข้าใจ
ตั้งชื่อคอนฟิกให้ชัดเจนตอบสามคำถาม: มันคืออะไร, ใช้กับที่ไหน, และชนิดข้อมูลเป็นอะไร
ข้อเสนอการตั้งชื่อปฏิบัติ:
- เติมคำนำหน้าตามแอป: WEB_, API_, MOBILE_
- ใช้ ALL_CAPS และ underscore
- แยกตามจุดประสงค์: API_BASE_URL, AUTH_JWT_SECRET, FEATURES_NEW_CHECKOUT
- สำหรับ boolean ให้ชัดเจน: FEATURES_SEARCH_ENABLED=true
แบบนี้จะไม่มีใครเดาว่า “BASE_URL” สำหรับ React, Go หรือ Flutter
ขั้นตอนทีละขั้น: คอนฟิกเว็บแอป (React) โดยไม่ใส่ค่าแบบ hardcode
โค้ด React รันบนเบราว์เซอร์ผู้ใช้ ดังนั้นสิ่งที่คุณส่งมาจะอ่านได้ เป้าหมายคือเก็บความลับไว้บนเซิร์ฟเวอร์ และให้เบราว์เซอร์อ่านแค่ค่าปลอดภัย เช่น API base URL, ชื่อแอป หรือฟีเจอร์ท็อกเกิลที่ไม่อ่อนไหว
1) ตัดสินใจว่าค่าไหนเป็น build-time vs runtime
คอนฟิกตอน build ถูกฉีดเมื่อคุณสร้าง bundle เหมาะสำหรับค่าที่เปลี่ยนไม่บ่อยและเปิดเผยได้
คอนฟิก runtime โหลดเมื่อแอปเริ่ม (เช่น จากไฟล์ JSON เล็ก ๆ ที่เสิร์ฟพร้อมแอป หรือ global ที่ฉีดเข้า) เหมาะสำหรับค่าที่คุณอาจเปลี่ยนหลัง deploy เช่น การสลับ API base URL ระหว่างสภาพแวดล้อม
กฎง่าย ๆ: ถ้าการเปลี่ยนค่าไม่ควรต้อง rebuild UI ให้ทำเป็น runtime
2) เก็บ API base URL โดยไม่คอมมิตมัน
เก็บไฟล์ท้องถิ่นสำหรับนักพัฒนา (ไม่คอมมิต) และตั้งค่าจริงใน pipeline ของคุณ
- Local dev: ใช้
.env.local(gitignored) เช่นVITE_API_BASE_URL=http://localhost:8080 - CI/CD: ตั้ง
VITE_API_BASE_URLเป็น environment variable ในงาน build หรือใส่ลงในไฟล์คอนฟิก runtime ที่สร้างตอน deploy
ตัวอย่าง runtime (เสิร์ฟไว้ข้างแอป):
{ "apiBaseUrl": "https://api.staging.example.com", "features": { "newCheckout": false } }
จากนั้นโหลดครั้งเดียวตอนสตาร์ทและเก็บไว้ที่เดียว:
export async function loadConfig() {
const res = await fetch('/config.json', { cache: 'no-store' });
return res.json();
}
3) เปิดเผยเฉพาะค่าปลอดภัยต่อเบราว์เซอร์
ถือว่าตัวแปร env ใน React เป็นสาธารณะ อย่าใส่รหัสผ่าน, private API keys, หรือฐานข้อมูลลงในเว็บแอป
ตัวอย่างที่ปลอดภัย: API base URL, Sentry DSN (public), build version, และฟีเจอร์ท็อกเกิลแบบไม่อ่อนไหว
ขั้นตอนทีละขั้น: คอนฟิกแบ็คเอนด์ (Go) ที่ตรวจสอบได้
การตั้งค่า backend ปลอดภัยขึ้นเมื่อมีการพิมพ์ชนิดข้อมูล, โหลดจาก environment variables, และตรวจสอบก่อนเซิร์ฟเวอร์เริ่มรับทราฟฟิก
เริ่มจากตัดสินใจว่า backend ต้องการค่าอะไรในการรัน และระบุค่านั้นให้ชัด ค่า "ต้องมี" ทั่วไปได้แก่:
APP_ENV(dev, staging, prod)HTTP_ADDR(เช่น:8080)DATABASE_URL(Postgres DSN)PUBLIC_BASE_URL(ใช้สำหรับ callback และลิงก์)API_KEY(สำหรับบริการภายนอก)
จากนั้นโหลดเข้า struct และ fail fast ถ้ามีค่าใดขาดหรือผิดรูปแบบ จะช่วยให้เจอปัญหาในไม่กี่วินาที แทนที่จะหลัง deploy บางส่วน
package config
import (
"errors"
"net/url"
"os"
"strings"
)
type Config struct {
Env string
HTTPAddr string
DatabaseURL string
PublicBaseURL string
APIKey string
}
func Load() (Config, error) {
c := Config{
Env: mustGet("APP_ENV"),
HTTPAddr: getDefault("HTTP_ADDR", ":8080"),
DatabaseURL: mustGet("DATABASE_URL"),
PublicBaseURL: mustGet("PUBLIC_BASE_URL"),
APIKey: mustGet("API_KEY"),
}
return c, c.Validate()
}
func (c Config) Validate() error {
if c.Env != "dev" && c.Env != "staging" && c.Env != "prod" {
return errors.New("APP_ENV must be dev, staging, or prod")
}
if _, err := url.ParseRequestURI(c.PublicBaseURL); err != nil {
return errors.New("PUBLIC_BASE_URL must be a valid URL")
}
if !strings.HasPrefix(c.DatabaseURL, "postgres://") {
return errors.New("DATABASE_URL must start with postgres://")
}
return nil
}
func mustGet(k string) string {
v, ok := os.LookupEnv(k)
if !ok || strings.TrimSpace(v) == "" {
panic("missing env var: " + k)
}
return v
}
func getDefault(k, def string) string {
if v, ok := os.LookupEnv(k); ok && strings.TrimSpace(v) != "" {
return v
}
return def
}
วิธีนี้ช่วยให้ DSN ของฐานข้อมูล, API keys, และ callback URLs อยู่นอกโค้ดและนอก git ในการตั้งค่าแบบโฮสต์ คุณฉีด env vars เหล่านี้ต่อสภาพแวดล้อมเพื่อให้ dev, staging, และ prod ต่างกันโดยไม่ต้องเปลี่ยนโค้ดบรรทัดเดียว
ขั้นตอนทีละขั้น: คอนฟิกมือถือ (Flutter) ที่ยืดหยุ่น
แอป Flutter มักต้องการสองชั้นคอนฟิก: flavors ตอน build (สิ่งที่คุณปล่อย) และการตั้งค่า runtime (สิ่งที่แอปเปลี่ยนได้โดยไม่ต้องปล่อยใหม่) การแยกสองส่วนนี้ช่วยหยุดการแก้ไข URL แบบด่วนที่กลายเป็นการ rebuild ฉุกเฉิน
1) ใช้ flavors เพื่อยืนยันตัวตน ไม่ใช่ endpoints
สร้างสาม flavors: dev, staging, prod Flavors ควรควบคุมสิ่งที่ต้องตายตัวตอน build เช่น ชื่อแอป, bundle id, signing, โปรเจกต์ analytics, และว่ามีเครื่องมือ debug เปิดหรือไม่
จากนั้นส่งค่า default ที่ไม่อ่อนไหวด้วย --dart-define (หรือ CI ของคุณ) เพื่อไม่ให้ฝังค่าลงในโค้ด:
ENV=stagingDEFAULT_API_BASE=https://api-staging.example.comCONFIG_URL=https://config.example.com/mobile.json
ใน Dart อ่านด้วย String.fromEnvironment และสร้าง AppConfig ง่าย ๆ ตอนเริ่ม
2) ใส่ URL และสวิตช์ในคอนฟิกที่ดึงได้
ถ้าคุณไม่อยาก rebuild สำหรับการเปลี่ยน endpoint เล็ก ๆ อย่าถือว่า API base URL เป็นค่าคงที่ ดึงไฟล์คอนฟิกเล็ก ๆ ตอนเปิดแอป (และแคชมัน) Flavor กำหนดเพียงที่ที่จะไปดึงคอนฟิก
การแยกที่ปฏิบัติได้:
- Flavor (build-time): ตัวตนแอป, default config URL, โปรเจกต์รายงานข้อผิดพลาด
- Remote config (runtime): API base URL, ฟีเจอร์แฟลก, เปอร์เซ็นต์ rollout, โหมดบำรุงรักษา
- Secrets: ไม่เคยถูกปล่อยในแอป (ไบนารีมือถือเปิดได้)\
ถ้าคุณย้าย backend, อัปเดต remote config ให้ชี้ไปยัง base URL ใหม่ ผู้ใช้เดิมจะได้รับการอัปเดตเมื่อล็อนใหม่ โดยมี fallback ปลอดภัยเป็นค่าที่แคชไว้ล่าสุด
ฟีเจอร์แฟลกและสวิตช์ที่ไม่กลายเป็นความโกลาหล
ฟีเจอร์แฟลกมีประโยชน์สำหรับการปล่อยทีละน้อย, A/B test, ปุ่มฆ่าฉุกเฉิน, และทดสอบการเปลี่ยนแปลงที่เสี่ยงใน staging แต่ไม่ได้แทนที่การควบคุมด้านความปลอดภัย ถ้าฟลาง์เป็นตัวป้องกันสิ่งที่ต้องรักษาความปลอดภัย มันไม่ใช่ flag แต่เป็นกฎการยืนยันตัวตน
ปฏิบัติต่อทุก flag เหมือน API: ชื่อชัดเจน, เจ้าของ, และวันหมดอายุ
การตั้งชื่อที่บอกเจตนา
ใช้ชื่อที่บอกว่าผลเมื่อ ON คืออะไร และส่วนไหนของผลิตภัณฑ์ได้รับผล ตัวอย่าง:
feature.checkout_new_ui_enabled(ฟีเจอร์สำหรับลูกค้า)ops.payments_kill_switch(สวิตช์ปิดฉุกเฉิน)exp.search_rerank_v2(การทดลอง)release.api_v3_rollout_pct(การปล่อยแบบค่อยเป็นค่อยไป)debug.show_network_logs(การวินิจฉัย)
ชอบ boolean แบบบวก (..._enabled) มากกว่าภาษาปฏิเสธ คง prefix ให้ค้นหาและตรวจสอบได้ง่าย
ค่าเริ่มต้น, กลไกป้องกัน และการล้าง
เริ่มด้วยค่าเริ่มต้นที่ปลอดภัย: ถ้าบริการ flag down แอปควรทำงานแบบเวอร์ชันเสถียร
แพทเทิร์นที่เป็นจริง: ปล่อย endpoint ใหม่ใน backend คงของเดิมไว้ และใช้ release.api_v3_rollout_pct เพื่อย้ายทราฟฟิกทีละน้อย ถ้าข้อผิดพลาดพุ่ง ให้กลับได้โดยไม่ต้อง hotfix
เพื่อป้องกัน flag สะสม ให้ตั้งกฎ:
- ทุก flag มีเจ้าของและวันที่ “ลบภายใน”
- ลบ flag ภายใน 1-2 release หลังปล่อยเต็มรูปแบบ
- บันทึกค่าฟลาง์ใน flow สำคัญเพื่อการดีบัก
- ตรวจสอบ flag เป็นประจำ (เช่น รายเดือน)
ความลับ: ที่เก็บ, การเข้าถึง, และพื้นฐานการหมุน
"ความลับ" คืออะไรที่จะก่อความเสียหายถ้ารั่ว เช่น API tokens, รหัสผ่านฐานข้อมูล, OAuth client secrets, คีย์เซ็นชื่อ (JWT), ความลับ webhook, และใบรับรองส่วนตัว ไม่ใช่ความลับ: API base URLs, หมายเลขบิลด์, ฟีเจอร์แฟลกสาธารณะ, หรือตัวระบุ analytics ที่เป็นสาธารณะ
แยกความลับออกจากการตั้งค่าอื่น ๆ นักพัฒนาควรแก้ไขคอนฟิกที่ปลอดภัยได้อย่างอิสระ ขณะที่ความลับถูกฉีดเฉพาะตอน runtime และเฉพาะที่จำเป็น
ที่ควรเก็บความลับ (ตามสภาพแวดล้อม)
ใน dev ให้เก็บความลับแบบท้องถิ่นและทิ้งได้ ใช้ไฟล์ .env หรือ keychain ของ OS และทำให้รีเซ็ตง่าย ห้ามคอมมิต
ใน staging และ prod ให้เก็บความลับใน secret store เฉพาะ ไม่ควรอยู่ในรีโป, ในแชท, หรือฝังในแอปมือถือ
- เว็บ (React): อย่าใส่ความลับในเบราว์เซอร์ ถ้าคลายเอ็นต์ต้องการโทเค็น ให้ใช้โทเค็นสั้น ๆ ออกโดย backend
- แบ็คเอนด์ (Go): โหลดความลับจาก env vars หรือ secrets manager ตอนเริ่ม และเก็บไว้ในหน่วยความจำเท่าที่จำเป็น
- มือถือ (Flutter): ถือว่าแอปเป็นสาธารณะ ทุก "ความลับ" ในแอปถูกสกัดออกได้ ให้ใช้โทเค็นที่ออกโดย backend และเก็บข้อมูลเซสชันใน secure storage เท่านั้น
การหมุนโดยไม่ทำลาย production
การหมุนล้มเหลวเมื่อคุณสลับคีย์แล้วลืมให้ไคลเอนต์เก่ายังใช้งานได้ วางแผนช่วงซ้อนทับ
- รองรับความลับสองชุดพร้อมกัน (active + previous) ช่วงเวลาสั้น ๆ
- หมุนความลับใหม่ก่อน แล้วสลับ pointer ของ "active"
- ตรวจสอบความล้มเหลวของการยืนยันตัวตน แล้วลบความลับเก่าหลังช่วงเวลาจบ
- บันทึกเวอร์ชันของความลับ (ไม่ใช่ค่าจริง) เพื่อดีบักอย่างปลอดภัย
แนวทางนี้ใช้ได้กับ API keys, webhook secrets และคีย์เซ็นชื่อ เพื่อลดการเกิด outage แบบไม่คาดคิด
ตัวอย่างการปล่อย: เปลี่ยน API URLs โดยไม่ทำให้ผู้ใช้เสียหาย
คุณมี staging API และ production API ใหม่ เป้าหมายคือย้ายทราฟฟิกทีละขั้น พร้อมวิธีรีเวิร์ทเร็วถ้าพบปัญหา สิ่งนี้ง่ายขึ้นเมื่อแอปอ่าน API base URL จากคอนฟิก ไม่ใช่ฝังในโค้ด
ถือ API URL เป็นค่าสั่ง deploy ในทุกที่ ในเว็บ (React) มันมักเป็นค่าตอน build หรือไฟล์คอนฟิก runtime ในมือถือ (Flutter) มักเป็น flavor บวก remote config ใน backend (Go) เป็น env var runtime ส่วนสำคัญคือความสอดคล้อง: โค้ดใช้ชื่อตัวแปรเดียว (เช่น API_BASE_URL) และไม่ฝัง URL ในคอมโพเนนต์หรือบริการ
การปล่อยแบบค่อยเป็นค่อยไปปลอดภัยอาจเป็น:
- ปล่อย prod API และเก็บไว้เป็น "dark" (รับเฉพาะทราฟฟิกภายใน) ขณะที่ staging ยังคงเป็นค่าเริ่มต้น
- เปลี่ยน dependency ของ backend ก่อน (ถ้า backend เรียกบริการอื่น) โดยใช้ env vars และ restart อย่างเร็ว
- ย้ายทราฟฟิกเว็บเป็นบางส่วนน้อย (หรือเฉพาะบัญชีภายใน)
- ปล่อยแอปมือถือที่มีการตั้งค่านี้ แต่คง flag ควบคุมบนเซิร์ฟเวอร์เพื่อเลื่อนการสลับจนกว่าจะพร้อม
- เพิ่มทราฟฟิกทีละน้อยและเตรียมแผน rollback
การยืนยันเป็นเรื่องการจับความผิดพลาดเร็ว ก่อนผู้ใช้จริงเจอการเปลี่ยน ให้ตรวจสอบ health endpoints, กระบวนการ auth, และบัญชีทดสอบหนึ่งบัญชีสามารถทำหนึ่งเส้นทางสำคัญจบได้หรือไม่
เช็คลิสต์ด่วนก่อนปล่อย
บั๊กคอนฟิกส่วนใหญ่เป็นเรื่องพื้น ๆ: ค่าของ staging เหลืออยู่, default ของ flag กลายเป็นค่าที่ผิด, หรือคีย์ API หายจากบางรีเจียน การเช็คอย่างรวดเร็วจับได้ส่วนใหญ่
ก่อน deploy ให้ยืนยันสามเรื่องตรงกับสภาพแวดล้อมเป้าหมาย: endpoints, ความลับ, และ defaults
- Base URLs ชี้ไปยังที่ถูกต้อง (API, auth, CDN, payments) ตรวจสอบเว็บ, แบ็คเอนด์, มือถือแยกกัน
- ไม่มีคีย์ทดสอบใน production และไม่มีคีย์ production ใน dev หรือ staging และยืนยันชื่อตรงกับที่แอปคาดหวัง
- ฟีเจอร์แฟลกมีค่าเริ่มต้นที่ปลอดภัย สิ่งเสี่ยงทั้งหมดควร default เป็นปิดและเปิดด้วยเจตนา
- การตั้งค่า build และ release ตรงกัน (bundle ID/package name, โดเมนที่กำหนดเอง, CORS origins, OAuth redirect URLs)
- การสังเกตการณ์ถูกตั้งค่า (logs, error reporting, tracing) และติดป้ายถูกต้องตามสภาพแวดล้อม
แล้วทำ smoke test รวดเร็ว เลือก flow ผู้ใช้จริงหนึ่งอย่างและรันแบบ end to end โดยใช้การติดตั้งใหม่หรือโปรไฟล์เบราว์เซอร์สะอาดเพื่อไม่พึ่งพา token ที่แคชไว้
- เปิดแอปและยืนยันว่าโหลดแล้วไม่มีข้อผิดพลาดในคอนโซล
- ลงชื่อเข้าใช้และเรียก API หนึ่งครั้งที่ต้องการ auth (โปรไฟล์, การตั้งค่า, หรือรายการข้อมูลง่าย ๆ)
- กระตุ้นความล้มเหลวที่ควบคุมได้หนึ่งอย่าง (อินพุตผิดหรือออฟไลน์) และยืนยันว่าเห็นข้อความมิตร ไม่ใช่หน้าจอว่าง
- ตรวจสอบ logs และระบบรายงานข้อผิดพลาด: ข้อผิดพลาดทดสอบหนึ่งรายการควรปรากฏภายใต้สภาพแวดล้อมที่ถูกต้องภายในไม่กี่นาที
นิสัยที่เป็นประโยชน์: ถือ staging เหมือน production แต่ค่าต่างกัน นั่นหมายถึง schema คอนฟิกเดียวกัน, กฎการตรวจสอบเดียวกัน, และรูปแบบการ deploy เดียวกัน โครงสร้างต้องเหมือน ค่าเท่านั้นที่ต่าง
ความผิดพลาดทั่วไปที่นำไปสู่อาการล่ม
การ outage ส่วนใหญ่ไม่ใช่เรื่องแปลกใหม่ แต่เป็นความผิดพลาดเรียบง่ายที่หลุดผ่านเพราะคอนฟิกกระจายอยู่หลายไฟล์ หลายขั้นตอน build และแดชบอร์ด และไม่มีใครตอบได้ว่า: "ตอนนี้แอปจะใช้ค่าอะไร?" การตั้งค่าที่ดีทำให้คำถามนั้นตอบง่าย
การผสมค่าระหว่าง build-time และ runtime
กับดักทั่วไปคือใส่ค่าที่ควรเป็น runtime ลงใน build-time การฝัง API base URL ลงใน React build หมายความว่าต้อง rebuild สำหรับทุกสภาพแวดล้อม แล้วใครสักคนอาจ deploy artifact ผิดและ production ชี้ไปที่ staging
กฎที่ปลอดภัยกว่า: ฝังเฉพาะค่าที่ไม่เปลี่ยนหลังปล่อยจริง ๆ (เช่น app version) เก็บรายละเอียดสภาพแวดล้อม (API URLs, feature switches, analytics endpoints) เป็น runtime เมื่อเป็นไปได้ และทำให้แหล่งความจริงชัดเจน
การปล่อย endpoints dev หรือคีย์ทดสอบ
สิ่งนี้เกิดเมื่อค่าเริ่มต้นเป็น "มีประโยชน์" แต่ไม่ปลอดภัย แอปมือถืออาจมี default เป็น dev API หากอ่านคอนฟิกไม่เจอ หรือ backend อาจ fallback ไปยังฐานข้อมูลท้องถิ่นถ้า env var หาย นั่นเปลี่ยนความผิดพลาดเล็ก ๆ ให้เป็น outage ทั้งระบบ
สองนิสัยช่วยได้:
- Fail closed: ถ้าค่าที่ต้องมีหาย ให้ crash ตอนเริ่มด้วยข้อความผิดพลาดที่ชัดเจน
- ทำให้ production เป็นสภาพแวดล้อมที่ยากต่อการตั้งค่าผิดที่สุด: ไม่มี defaults สำหรับ dev, ห้ามคีย์ทดสอบ, ไม่มี debug endpoints เปิด
ตัวอย่างจริง: ปล่อยวันศุกร์ตอนกลางคืน แล้ว build production บังเอิญมีคีย์ชำระเงินของ staging ทุกอย่าง "ทำงาน" จนกระทั่งการชำระเงินล้มเหลว การแก้ไม่ใช่ไลบรารีการชำระเงิน แต่เป็นการตรวจสอบที่ปฏิเสธคีย์ไม่ใช่ production
ปล่อยให้ staging ต่างจาก production เกินไป
Staging ที่ไม่ใกล้เคียง production ให้ความมั่นใจผิดพลาด การตั้งค่าฐานข้อมูลต่างกัน, งาน background หายไป, หรือฟีเจอร์แฟลกต่างกันทำให้บั๊กโผล่หลังการปล่อย
เก็บ staging ใกล้เคียงโดยการสะท้อน schema คอนฟิกเดียวกัน, กฎการตรวจสอบเดียวกัน, และรูปแบบการ deploy เดียวกัน โครงสร้างเท่านั้นที่ต่าง ค่าไม่ควรต่าง
ขั้นตอนถัดไป: ทำให้คอนฟิกเป็นเรื่องน่าเบื่อ ทำซ้ำได้ และปลอดภัย
เป้าหมายไม่ใช่เครื่องมือหรู แต่คือความสม่ำเสมอที่น่าเบื่อ: ชื่อเดียวกัน, ชนิดเดียวกัน, กฎเดียวกันใน dev, staging, prod เมื่อคอนฟิกคาดเดาได้ การปล่อยจะไม่รู้สึกเสี่ยง
เริ่มจากเขียนสัญญาคอนฟิก (config contract) ที่ชัดเจนในที่เดียว ย่อแต่เฉพาะเจาะจง: ชื่อคีย์ทุกอัน, ชนิดข้อมูล (string, number, boolean), แหล่งที่อนุญาตให้มา (env var, remote config, build-time), และค่าเริ่มต้น เพิ่มบันทึกสำหรับค่าที่ห้ามตั้งใน client app (เช่น private API keys) ถือสัญญานี้เหมือน API: การเปลี่ยนต้องผ่านการรีวิว
จากนั้นทำให้ความผิดพลาดล้มเหลวเร็วที่สุด เวลาที่ดีที่สุดในการค้นหาว่า API base URL หายคือใน CI ไม่ใช่หลัง deploy เพิ่มการตรวจสอบอัตโนมัติที่โหลดคอนฟิกแบบเดียวกับที่แอปทำ และเช็ก:
- ค่าที่ต้องมีอยู่จริง (ไม่ใช่สตริงว่าง)
- ชนิดถูกต้อง (ไม่มีบั๊กแบบ "true" vs true)
- กฎสำหรับ prod ผ่าน (เช่น บังคับ HTTPS)
- ฟีเจอร์แฟลกมีชื่อที่รู้จัก (ไม่มีพิมพ์ผิด)
- ความลับไม่ถูกคอมมิตลงรีโป
สุดท้าย ทำให้การกู้คืนเมื่อการเปลี่ยนคอนฟิกผิดพลาดเป็นเรื่องง่าย: เก็บสแนปช็อตของสิ่งที่รันอยู่, เปลี่ยนทีละอย่าง, ยืนยันเร็ว, และเตรียมทางย้อนกลับ
ถ้าคุณสร้างและ deploy ด้วยแพลตฟอร์มอย่าง Koder.ai (koder.ai) กฎเดิมยังใช้: ถือค่าสภาพแวดล้อมเป็นอินพุตของการ build และ hosting, เก็บความลับออกจากซอร์สที่ส่งออก, และตรวจสอบคอนฟิกก่อนปล่อย ความสม่ำเสมอนี้คือสิ่งที่ทำให้การ redeploy และ rollback เป็นเรื่องปกติ
เมื่อคอนฟิกถูกเอกสาร, ตรวจสอบ, และย้อนกลับได้ มันจะหยุดเป็นต้นเหตุของ outage และกลายเป็นส่วนปกติของการปล่อย
คำถามที่พบบ่อย
อะไรควรอยู่ใน config แทนที่จะอยู่ในโค้ด?
เก็บค่าที่เปลี่ยนไปตามสภาพแวดล้อมไว้นอกไฟล์ซอร์สโค้ด ใส่ค่าเริ่มต้นที่ไม่ละเอียดอ่อนในโค้ด ใช้การตั้งค่าเฉพาะสภาพแวดล้อมสำหรับ URL และสวิตช์ต่าง ๆ และส่งต่อข้อมูลลับระหว่างขั้นตอนการบิลด์หรือดีพลอย
เมื่อไรที่ฉันต้องมีสภาพแวดล้อมใหม่?
ใช้สภาพแวดล้อม dev, staging และ prod แยกกันเมื่อคุณต้องการแยกข้อมูล สิทธิ์การเข้าถึง หรือความเสี่ยงออกจากกัน หากเปลี่ยนแค่ URL ขีดจำกัด หรือสวิตช์ ให้เพิ่มค่าคอนฟิกแทน
ฉันเก็บข้อมูลลับไว้ในแอป React ได้ไหม?
ไม่ สิ่งใดก็ตามที่ส่งไปยังเบราว์เซอร์ ผู้ใช้สามารถมองเห็นได้ เก็บคีย์ส่วนตัวและรหัสผ่านไว้ที่แบ็กเอนด์ และเปิดเผยเฉพาะค่าที่ปลอดภัย เช่น URL ของ API สาธารณะ หรือ feature flag แบบง่าย
เว็บแอปของฉันควรใช้คอนฟิกช่วงบิลด์หรือขณะรันไทม์?
ใช้ค่าในช่วงบิลด์สำหรับการตั้งค่าที่แทบไม่เปลี่ยนและเปิดเผยได้อย่างปลอดภัย โหลดคอนฟิกขณะรันไทม์สำหรับค่าที่อาจต้องเปลี่ยนหลังดีพลอย เช่น URL ฐานของ API หรือสวิตช์โหมดบำรุงรักษา
แบ็กเอนด์ Go ควรจัดการคอนฟิกอย่างไร?
โหลดตัวแปรสภาพแวดล้อมที่จำเป็นลงใน struct คอนฟิกแบบมีชนิดข้อมูลเมื่อบริการเริ่มทำงาน ตรวจสอบสภาพแวดล้อม URL และสตริงเชื่อมต่อฐานข้อมูลที่อนุญาต แล้วหยุดการเริ่มต้นพร้อมข้อผิดพลาดที่ชัดเจนหากขาดค่าที่จำเป็น
Flutter ควรจัดการ dev, staging และ prod อย่างไร?
ใช้ Flutter flavors สำหรับรายละเอียดเฉพาะของบิลด์ เช่น app ID การลงนาม และโปรเจ็กต์วิเคราะห์ข้อมูล ดึงการตั้งค่าขณะรันไทม์ที่ไม่ละเอียดอ่อน เช่น URL ของ API และ feature flags จากแหล่ง remote config
ฉันจะป้องกันไม่ให้ feature flags สะสมได้อย่างไร?
ตั้งชื่อ flag ตามพฤติกรรมที่มันเปิดใช้ เช่น feature.checkout_new_ui_enabled กำหนดผู้รับผิดชอบและวันที่นำออกให้แต่ละ flag ตั้งค่าเริ่มต้นของการเปลี่ยนแปลงที่มีความเสี่ยงเป็นปิด และนำ flag ออกไม่นานหลังเปิดใช้ครบถ้วน
ฉันควรเก็บ API keys และรหัสผ่านฐานข้อมูลไว้ที่ไหน?
เก็บข้อมูลลับสำหรับ staging และ production ไว้ในที่เก็บข้อมูลลับเฉพาะ แล้วส่งต่อให้เฉพาะบริการที่ต้องใช้ เก็บข้อมูลลับสำหรับการพัฒนาในเครื่องไว้ในไฟล์ภายในเครื่องที่ถูกละเว้นหรือใน OS keychain
ฉันจะหมุนเวียนข้อมูลลับโดยไม่ทำให้ระบบหยุดทำงานได้อย่างไร?
ใช้ข้อมูลรับรองเก่าและใหม่ควบคู่กันในช่วงเวลาซ้อนทับสั้น ๆ ดีพลอยไคลเอนต์หรือบริการด้วยค่าใหม่ สลับทราฟฟิกที่ใช้งานอยู่ เฝ้าดูความล้มเหลวของการยืนยันตัวตน แล้วนำข้อมูลรับรองเก่าออก
ฉันควรตรวจสอบอะไรก่อนปล่อยเวอร์ชัน production?
ตรวจสอบว่าแต่ละ endpoint ชี้ไปยังสภาพแวดล้อมเป้าหมาย มีข้อมูลลับที่จำเป็นครบถ้วน และ flags ที่มีความเสี่ยงตั้งค่าเริ่มต้นเป็นปิด จากนั้นทดสอบขั้นตอนการใช้งานของผู้ใช้หนึ่งรอบแบบครบถ้วนในโปรไฟล์เบราว์เซอร์ที่สะอาดหรือการติดตั้งแอปใหม่ และยืนยันว่าบันทึกใช้ป้ายกำกับสภาพแวดล้อมที่ถูกต้อง