設定為什麼要從 code 裡分離出來
// ❌ 最常見的問題:設定寫死在 code 裡
const db = new Database({
host: 'localhost',
port: 5432,
password: 'mypassword123', // 這個進了 git
});「等等我只是在本地開發」——git 的 blame 不會忘,repo 的歷史不會消失。即使你之後刪掉那行,任何人都能從 git log 裡找回那個密碼。
2024 年的 GitHub Advisory Database 裡,credential exposure 是 secret 洩漏的最常見來源,通常不是被駭,而是工程師無意間 commit 進去的。
12-factor app 的 Config 原則(2011 年提出,2026 年還是對的):
把應用的設定存在環境變數裡。設定 = 在不同部署環境(dev / staging / prod)之間會改變的東西。
什麼算是 Config
判斷方式:這個值在 dev / staging / prod 之間不一樣嗎? 是的話就是 config,不是的話是常數。
Config(環境變數):
DATABASE_URL # 每個環境的 DB 不同
REDIS_URL # dev 跑 local Redis,prod 跑 ElastiCache
JWT_SECRET # 每個環境用不同的 secret
AWS_S3_BUCKET # dev 和 prod 用不同 bucket
LOG_LEVEL # dev 是 debug,prod 是 warn
MAX_UPLOAD_SIZE # 可能依環境調整
常數(hardcode 在 code 裡沒問題):
DEFAULT_PAGE_SIZE = 20
PASSWORD_MIN_LENGTH = 8
SUPPORTED_IMAGE_TYPES = ['jpg', 'png', 'webp']
型別安全的 Config
process.env.DATABASE_URL 的型別是 string | undefined——使用時要到處加 ! 或 ??。更糟的是如果這個值不存在,應用通常不會在啟動時報錯,而是在某個 runtime 路徑走到才爆。
解法:在 startup 時驗證所有必要的 config,型別確定後注入。
TypeScript(Zod):
import { z } from 'zod';
const configSchema = z.object({
NODE_ENV: z.enum(['development', 'staging', 'production']).default('development'),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
JWT_EXPIRES_IN: z.string().default('15m'),
REDIS_URL: z.string().url().optional(),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
AWS_S3_BUCKET: z.string().optional(),
});
export type Config = z.infer<typeof configSchema>;
export function loadConfig(): Config {
const result = configSchema.safeParse(process.env);
if (!result.success) {
console.error('Invalid config:');
console.error(result.error.format());
process.exit(1); // 設定不合法直接停機,不要讓壞設定的 app 跑起來
}
return result.data;
}
// 在 main.ts / bootstrap 最早的地方呼叫
const config = loadConfig();Python(pydantic-settings):
from pydantic_settings import BaseSettings
from pydantic import PostgresDsn, RedisDsn
class Config(BaseSettings):
NODE_ENV: str = "development"
PORT: int = 3000
DATABASE_URL: PostgresDsn # pydantic 自動驗 URL 格式
JWT_SECRET: str
JWT_EXPIRES_IN: str = "15m"
REDIS_URL: RedisDsn | None = None
LOG_LEVEL: str = "info"
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8"
)
config = Config() # 缺少必要欄位時啟動就報錯Spring Boot(@ConfigurationProperties):
# application.yml
app:
jwt:
secret: ${JWT_SECRET} # 從環境變數取,不 hardcode
expires-in: 15m
database:
pool-size: ${DB_POOL_SIZE:10} # 有預設值的環境變數@ConfigurationProperties(prefix = "app.jwt")
@Validated
public record JwtConfig(
@NotBlank String secret,
Duration expiresIn
) {}多環境的 Config 分層
.env 檔案的分層慣例:
.env # 共用預設值(進 git,不放 secret)
.env.local # 本地覆蓋(不進 git)
.env.development # dev 環境
.env.staging # staging 環境(通常不在本地,在 CI/CD 注入)
.env.production # production(絕對不在本地,在 K8s Secret / Vault 注入)
.gitignore 要正確:
.env.local
.env.*.local
.env.staging
.env.production進 git 的 .env 只放安全的預設值:
# .env(進 git)
NODE_ENV=development
PORT=3000
LOG_LEVEL=debug
# 這些不進 git,每個工程師自己在 .env.local 設
# DATABASE_URL=
# JWT_SECRET=
# REDIS_URL=Secret Management:不要存在 git
Secret(JWT_SECRET、DB 密碼、API key)的管理分幾個層次:
Level 1(最基本):.env 檔案本地管理,不進 git
- 問題:工程師之間怎麼傳密碼?Slack DM 或 1Password?新人 onboarding 麻煩。
Level 2:CI/CD 的 Secrets(GitHub Actions Secrets / GitLab CI Variables)
- 在 CI/CD 介面手動設定 secret,Pipeline 以環境變數注入,不存在 repo
Level 3:K8s Secret
# kubectl create secret generic app-secrets
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
stringData:
JWT_SECRET: "your-secret-here"
DATABASE_URL: "postgres://..."# Deployment 注入 secret 作為環境變數
env:
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: app-secrets
key: JWT_SECRETLevel 4(生產級):HashiCorp Vault / AWS Secrets Manager / GCP Secret Manager
- Secret 集中管理、有 audit log、支援 rotation
- 應用啟動時動態拉 secret,不存在 env file 或 K8s Secret(因為 K8s Secret 預設只是 base64 編碼,不是加密)
實務建議:中小型團隊 Level 2–3 夠用;Level 4 在 PCI-DSS / SOC2 合規或 secret rotation 需求時才值得投入。
Config 的 Anti-patterns
// ❌ 在 module 層級直接讀 process.env(啟動時不驗證)
export const DB_URL = process.env.DATABASE_URL; // undefined 也不報錯
// ❌ 環境判斷用 if/else 分散在 code 各處
if (process.env.NODE_ENV === 'production') {
// prod 邏輯
} else {
// dev 邏輯
}
// 這種 if 散在 30 個地方,改 config 結構很麻煩
// ❌ 同一個 secret 用在不同環境
JWT_SECRET=supersecretkey123 // dev 和 prod 用同一個
// 開發環境的 log 洩漏了 JWT,生產環境的 token 也能被 forge各框架的 Config 整合
| 框架 | Config 工具 | Secret 整合 |
|---|---|---|
| Express | dotenv + Zod 自建 | K8s Secret → 環境變數 |
| FastAPI | pydantic-settings(內建支援) | K8s Secret → 環境變數 |
| NestJS | @nestjs/config(ConfigModule + Joi 驗證) | K8s Secret → 環境變數 |
| Spring Boot | application.yml + @ConfigurationProperties | Spring Cloud Config Server / Vault |
| Laravel | config/ 目錄 + env() helper | Laravel Forge / K8s |
