DEV Community

Environment Variable — Config Management

Quản lý config: validate env lúc khởi động, fail fast thay vì sập lúc chạy

Một ứng dụng cần cấu hình khác nhau theo môi trường — URL database, khóa API, cổng, feature flag — và cách chuẩn để truyền chúng vào mà không hard-code là environment variable. Nhưng process.env.Xstring | undefined: nếu một biến bắt buộc bị thiếu hay sai định dạng, ứng dụng không biết cho tới khi chạm tới chỗ dùng nó — có thể là giữa một request quan trọng, hàng giờ sau khi deploy. Kỹ thuật cốt lõi là validate toàn bộ config một lần lúc khởi động và refuse to start nếu sai, biến một lỗi runtime mơ hồ thành một lỗi startup rõ ràng.

Cơ chế hoạt động

process.env chứa các biến do môi trường cung cấp; dotenv nạp file .env vào process.env lúc dev. Thay vì rải process.env.X khắp code, gom vào một module config được validate và ép kiểu một lần:

import { z } from 'zod'

const schema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']),
  PORT: z.coerce.number().default(3000),      // env là string -> ép số
  DATABASE_URL: z.string().url(),              // bắt buộc, đúng định dạng URL
  JWT_SECRET: z.string().min(32),              // bắt buộc, đủ dài
})

export const config = schema.parse(process.env) // throw NGAY lúc khởi động nếu thiếu/sai
Enter fullscreen mode Exit fullscreen mode

schema.parse chạy một lần khi process khởi động: nếu DATABASE_URL thiếu hay JWT_SECRET quá ngắn, nó throw với thông báo rõ trước khi server nhận request nào. Phần còn lại của code import config đã được kiểm và đúng kiểu (số là số, không phải chuỗi).

Vấn đề gặp trong production

Failure mode: thiếu env phát hiện muộn. Không validate lúc khởi động, một biến thiếu chỉ lộ khi code chạm tới nó:

// rải khắp nơi, không kiểm trước
const token = jwt.sign(payload, process.env.JWT_SECRET) // undefined -> token rỗng/lỗi giữa request
await db.connect(process.env.DATABASE_URL)              // undefined -> lỗi mơ hồ khi kết nối
Enter fullscreen mode Exit fullscreen mode

App khởi động "thành công", nhận traffic, rồi vỡ ở request đầu tiên chạm tới biến thiếu — hoặc tệ hơn, chạy với giá trị sai âm thầm (ký JWT với secret undefined). Validate lúc khởi động chuyển lỗi này về thời điểm deploy, nơi nó hiển nhiên và an toàn.

Failure mode: env là string, dùng như số/bool gây bug ngầm. process.env.PORT"3000" (chuỗi), process.env.FEATURE_X"false" (chuỗi "false"truthy!):

if (process.env.FEATURE_X) { ... } // "false" là truthy -> luôn chạy, sai hoàn toàn
const timeout = process.env.TIMEOUT * 1000 // "30" * 1000 may work, nhưng "30s" -> NaN
Enter fullscreen mode Exit fullscreen mode

Ép kiểu tường minh trong schema (coerce.number, parse boolean đúng cách) loại bỏ cả lớp bug này.

Failure mode: rò secret. Khóa API, DB password trong env tuyệt đối không được log hay commit. .env phải trong .gitignore; log config lúc khởi động phải che secret; secret production nên lấy từ secret manager (Vault, AWS Secrets Manager, biến môi trường của platform) chứ không phải file .env commit nhầm. Một secret lọt vào git history hay log là sự cố bảo mật thật.

Failure mode: lẫn build-time config với runtime config. Config nhúng lúc build (ví dụ biến VITE_/NEXT_PUBLIC_ của frontend) bị đóng băng vào bundle — đổi nó phải build lại, và nó công khai trong code client (không bao giờ để secret ở đây). Config runtime (đọc process.env lúc chạy ở server) đổi được không cần build lại. Nhầm hai loại dẫn tới "đổi env mà không có tác dụng" (vì đã nhúng lúc build) hoặc lộ secret ra client.

Cách debug và monitor

Nếu app "khởi động ổn rồi vỡ lúc chạy" với lỗi liên quan config, gần như chắc chưa validate env lúc khởi động — thêm một schema parse ở entry point để fail fast. Khi một feature flag boolean "luôn bật", kiểm tra nó có đang so sánh chuỗi "false" truthy không. Log danh sách biến config đã che secret lúc khởi động để xác nhận môi trường nạp đúng cái gì. Với "đổi env không có tác dụng", kiểm tra đó là build-time hay runtime config. Quét git history và log cho secret rò (công cụ như gitleaks); xoay (rotate) ngay secret nào từng lọt ra. Giữ một .env.example (không có giá trị thật) làm tài liệu các biến cần thiết.

Tradeoff

Validate config lúc khởi động tốn chút công viết schema, đổi lại biến mọi lỗi config (thiếu, sai định dạng, sai kiểu) thành một lỗi startup rõ ràng tại thời điểm deploy thay vì một crash mơ hồ giữa production — gần như luôn đáng. Gom config vào một module đã ép kiểu cũng cho phần còn lại của code dùng giá trị đúng kiểu, an toàn. Quy tắc thực tế: validate toàn bộ env một lần lúc khởi động với một schema (zod hoặc tương đương), fail fast nếu sai; ép kiểu tường minh (số/bool) thay vì dùng chuỗi trực tiếp; không bao giờ log/commit secret, dùng secret manager cho production; phân biệt rõ build-time và runtime config; và giữ .env.example làm tài liệu. Chi phí nhỏ trả trước đổi lấy không có sự cố config âm thầm.

Câu hỏi phỏng vấn

Tại sao nên validate env lúc khởi động, và những bẫy nào khi dùng process.env?

Nên validate env lúc khởi động vì process.env.Xstring | undefined: không kiểm trước thì một biến bắt buộc bị thiếu hay sai định dạng chỉ lộ khi code chạm tới nó — có thể giữa một request quan trọng hàng giờ sau deploy, hoặc tệ hơn chạy âm thầm với giá trị sai (ký JWT với secret undefined). Validate toàn bộ config một lần bằng một schema (zod) ở entry point biến lỗi đó thành một lỗi startup rõ ràng tại thời điểm deploy — fail fast, an toàn. Các bẫy của process.env: env luôn là string nên if (process.env.FLAG) với "false" vẫn truthy (luôn chạy), và phép tính trên chuỗi ra NaN — phải ép kiểu tường minh; rò secret nếu log/commit (.env phải trong .gitignore, dùng secret manager cho production); và lẫn build-time config (nhúng vào bundle, công khai ở client, đổi phải build lại) với runtime config. Điểm ăn điểm: gom config vào một module đã validate và ép kiểu để phần còn lại dùng giá trị đúng kiểu, giữ .env.example làm tài liệu, và rotate ngay secret từng lọt ra.

Hands-on

Viết một config loader thật dùng zod (hoặc tương đương): khai schema cho NODE_ENV, PORT (ép số, có default), DATABASE_URL (url bắt buộc), JWT_SECRET (độ dài tối thiểu), parse process.env một lần ở entry point và export object đã ép kiểu. Xóa một biến bắt buộc và chạy app để thấy nó fail ngay lúc khởi động với thông báo rõ thay vì vỡ lúc chạy. Tạo một feature flag boolean đọc thẳng process.env với giá trị "false" để tái hiện bug truthy, rồi sửa bằng parse boolean trong schema. Thêm .env vào .gitignore, tạo .env.example, và chạy gitleaks trên repo để xác nhận không có secret nào lọt vào history; cuối cùng phân biệt một biến build-time (frontend) với một biến runtime (server) và quan sát đổi biến build-time không có tác dụng tới khi build lại.

Top comments (0)