跳至主要内容

ADR-033: 必要設定缺少即啟動失敗;Feature 一經 provision 即啟用

ADR 編號: 033 狀態: 已接受 (Accepted) 決策日期: 2026-09-04 決策者: Development Team 取代: ADR-030 中「development/test process 明確設定 app.security.jwt.allow-ephemeral-key=true 才可產生臨時 key」的參考實作預設; 持久化資料加密指南 原「空白 root key 不阻止 Spring context 啟動」的延遲失敗政策 被取代: ADR-034 局部取代決策 1 的 localhost 預設豁免,僅限 app.public-base-url(2026-09-12 P1 驗證通過)


摘要​

Feature 一經 provision 即代表部署要使用它,因此:

  1. 該 Feature 的必要設定(無法給出可運作預設值的參數,例如 root key、JWT 金鑰對、API key、 雲端儲存憑證)缺少時,應用在啟動期失敗,不延後到第一次使用才拋錯、也不靜默降級。
  2. 應用不自行產生任何必要參數,也不把值寫回設定檔;可產生的機密(隨機 key、RSA 金鑰對) 由 /env-config 的 generate(...) 於環境設定產出時建立並持久化。dev 不留例外。
  3. 「是否啟用」不再由獨立旗標預設關閉來表達;旗標若保留,預設為 true,只作營運期 opt-out, 且不由 /env-config 逐環境詢問。

背景 (Context)​

參考實作原本對「provision 了但沒設定」採三種不一致的處理:

現況例
延遲失敗(啟動過、首次使用炸)PersistenceEncryption 靜默丟棄空白 root key;almanac API key 只 WARN;S3/Azure/SFTP 憑證到第一次呼叫才失敗
靜默 deny-allexternal-identity auto-link 開啟但清單為空
獨立旗標預設關閉email-otp.enabled、oidc.enabled、api-key.enabled 皆為 false,另由 /env-config 在每個 tier 詢問

同時,JWT 金鑰對在 allow-ephemeral-key=true 時由應用每次啟動自產臨時金鑰,該旗標由 Gradle 在 bootRun 與 test 注入;persistence-encryption root key 也靠 Gradle 注入一把固定值。 這些出口讓「必要參數缺少」在開發流程中不可見,到部署才被發現,且失敗形態是使用者拿到 202 卻永遠收不到驗證碼這類靜默錯誤。

「先裝 feature、再用旗標決定要不要開」是雙重閘門:provision 已是原始碼層的明確意圖,旗標預設 關閉只增加每個環境必填的設定數量,沒有額外安全價值。

考量的方案 (Options Considered)​

方案 A:維持延遲失敗與預設關閉​

零設定可啟動,但錯誤延後到使用期,且每個 feature 多一個環境設定。

方案 B:應用首次啟動自產可產生的機密並寫回設定檔​

減少設定數量,但應用取得設定檔寫入權、與 /env-config 形成兩個產生點,且無法涵蓋憑證這類 不可產生的參數。

方案 C:必要參數啟動期失敗、產生只在 env-config、旗標預設開(採用)​

一致、可預測、單一產生點;代價是本機啟動前必須先跑 /env-config dev。

決策 (Decision)​

1. 必要參數的判準​

「必要」= Feature 已 provision、且該參數沒有可運作的預設值。有預設值的參數(H2 in-memory datasource、localhost URL、storage.type=database)是「有值」而非「缺值」,不在本 ADR 範圍。

例外:ADR-034 的 app.public-base-url 不提供 classpath localhost fallback;非 DEV 環境須明示,缺值即啟動失敗。2026-09-15 修訂僅允許 app.env=DEV 且 URL 缺值時,依本機 port/SSL/context path 提供 localhost 預設(見 ADR-034)。此例外不改變其他必要參數與 localhost URL 的既有判準。

2. 失敗時機統一為啟動期​

  • 靜態必填以 Spring Boot 既有機制表達(@Validated 或 *Properties.validate()),不引入新的 框架 primitive。
  • 條件式必填(依 type、mode 或 enabled 才需要的參數)在對應 @Configuration 建立第一個 bean 時檢查,沿用 MailProperties.validate*() 的既有模式。
  • 框架 PersistenceEncryption.fromBase64 拒絕空白值,且 active key 缺少時建構即失敗。

3. 應用不產生、不寫回​

移除 app.security.jwt.allow-ephemeral-key 與臨時金鑰產生碑;Gradle 不再為 bootRun 注入任何 必要參數。自動化測試以 fixture 提供必要參數(金鑰對由建構在 fork 測試 JVM 前產生、不進 repo), 這是測試對受測系統的輸入,不是應用自產。

4. 旗標改為 opt-out​

email-otp.enabled、oidc.enabled、api-key.enabled 預設 true,移除其 @conf-env 詢問; local-login.enabled 同步移除詢問。維運者需要暫關(mail 供應商故障、IdP 遷移)時,於外部 .properties 覆寫,不需重新 provision。

5. dev 不留例外​

./gradlew bootRun 前先執行 /env-config dev;generate(...) 標記已涵蓋三個 tier,因此開發者 不需手動準備任何可產生的機密。

權衡分析 (Trade-offs)​

我們獲得什麼​

  • 缺設定的失敗在部署啟動期一次列出,不再散落到首次使用。
  • 每個 optional feature 少一個環境設定;/env-config 少四個逐環境問題。
  • 機密只有一個產生點,設定檔所有權邊界清楚。

我們放棄什麼​

  • 零設定本機啟動。
  • 參考 source 模組同時 provision 全部 feature,其 dev 環境需要為每個 feature 提供必要參數,或以 opt-out 關閉沒有外部相依的 feature(如無 IdP 時關 OIDC)。

實作與驗證​

  • PersistenceEncryptionTest 驗證空 ring 與空白值於建構期失敗。
  • 各 server source 的 *ConfigTest 驗證缺參數時 context 啟動失敗、預設不帶旗標時 bean 存在、 enabled=false 時 bean 不存在。
  • 19-env-config.md 記載本機啟動順序與必要參數語意。

相關文檔 (References)​

變更歷史 (Change Log)​

日期變更內容變更者
2026-09-04接受必要設定啟動期失敗、不自產不寫回、旗標預設開的政策Development Team + AI Assistant

文檔維護者: Development Team + AI Assistant 最後審閱: 2026-09-04