這個系列 Day 4 建的那顆 Azure OpenAI resource(kind: OpenAI),在 2026-08 被官方標成新 Foundry portal 不支援——要嘛留在 classic portal,要嘛升級。升級是同一顆資源上一個屬性的 patch,聽起來像換個入口。
但實測它把資源宣告的 API 面從 12 條變成 65 條,而一條從頭到尾沒被改過的 role assignment,只因為當初挑的是萬用字元寫法的角色,就從「只能叫 OpenAI」變成「能叫 Language」。這篇把升級的形狀、那個治理副作用、以及回退的條件量給你看——讀完你能判斷自己該不該升、升之前要交叉查哪兩件事。
Day 30 公開承認過:Foundry lifecycle 是這個系列「連底都沒有」的三題之一。這篇補底,而且先講補到哪裡為止——補的是 resource lifecycle 與它的治理副作用,用一顆 2026-09-01 建了又刪的資源量出來的;Agent Service 與 Foundry evaluation 本次仍然零實測,後面只會以「未觀測」出現。
Day 4 寫過一句話:「服務本體沒變,standalone resource 照樣能開」(查核 2026-07)。一個月後這句話要加註。官方的 GA 總覽現在寫的是:
"Standalone Azure OpenAI resources: Not supported in the new Foundry portal. Continue using Foundry (classic) portal, or upgrade to a Foundry project."
— Foundry portal general availability overview,ms.date2026-08-14,查核 2026-09
lab 的 create-openai.sh 建的就是 --kind OpenAI,服役中的那顆帳號今天仍是這個 kind,Day 4 到 Day 34 全部文章的 runtime 都指著它。所以這不是別人的問題。
但「該不該升」對這個系列不是顯然要升。架構文推薦 Foundry resource 作為「most AI development scenarios」的起點,同一頁也留了反面條件(Microsoft Foundry architecture,ms.date 2026-08-21,查核 2026-09):
"If your workload only requires Azure OpenAI completions without agent hosting or evaluation, a standalone Azure OpenAI resource might be sufficient."
lab 剛好落在這句話裡:它只做 completions 與 embeddings,agent 跑在自己的 backend(Day 17–18),evaluation 自建(Day 28)。portal 不支援是入口的事,API 與 CLI 都還在。有判斷可下的地方在這裡。下判斷之前,得先知道升級到底動了什麼。
官方 how-to 只列 portal、Bicep、Terraform、ARM 四個 tab,沒有 CLI(Upgrade Azure OpenAI to Microsoft Foundry,ms.date 2026-08-19,查核 2026-09)。但 CLI 路徑存在,而且走得通——本次就是用它升的(az 2.89.1,2026-09-01):
az cognitiveservices account update \
--subscription "$SUB" --resource-group "$RG" --name "$NAME" \
--kind AIServices --allow-project-management true
一次 update,exit 0。資源名、resource id、managed identity 的 principal id 都沒換——升級前後與回退後,三個時點讀到的是同一個 principal id。文件不推薦的路徑不等於不存在的路徑,這對 Day 26「CLI scripts first」那條軸是好消息。
官方列了一串「保留」:resource name、tags、網路設定、身份設定、API endpoint 與 API key、custom domain、既有狀態。本次量了其中最要緊的兩項,一項成立、一項要加註:
key1 sha256 before == after : true
key2 sha256 before == after : true
endpoint before: https://<acct>.openai.azure.com/
endpoint after : https://<acct>.cognitiveservices.azure.com/
API key 逐位元組相同,這句成立。properties.endpoint 換了 FQDN——舊 URL 沒有消失,它在 properties.endpoints 映射裡以「Azure OpenAI Legacy API - Latest moniker」之名續存;但資源宣告的主要 endpoint 欄位變成另一支主機名。
寫死 <name>.openai.azure.com 的呼叫端不受影響;從 properties.endpoint 讀 base URL 再寫進設定的呼叫端,會拿到另一支主機。回退後這個欄位也跟著換回去,雙向跟隨 kind。
而那支新主機名不是升級完就能用。升級後 2 秒,三支 FQDN 的可達性:
| FQDN | DNS | TLS |
|---|---|---|
<acct>.openai.azure.com |
✅ | ✅ TLSv1.3 |
<acct>.services.ai.azure.com |
✅ | ✅ TLSv1.3 |
<acct>.cognitiveservices.azure.com |
❌ 查無此名 | — |
解析不出來的那一支,正是資源同一時刻宣告的 properties.endpoint。它在升級後約 5 分 25 秒才解析得出來(單次觀測)。升級腳本如果升完立刻拿新 endpoint 做 smoke test,會在這五分鐘裡看到 DNS 失敗——不是權限問題,不是 404,是名字還沒生出來。
還有一個預期落空。設計時打算用唯讀屬性 previousKind: "OpenAI" 當升級過的機器可判定標記(Day 29 那條「會失敗的指令才是檢查」的直接材料)。實際上 az cognitiveservices account show 回的 properties 在三個時點都沒有這個鍵。這只證明「這條讀取路徑上看不到它」,不證明屬性不存在——但靠它寫檢查,本次會是空讀。
前一節漏了最重的一個數字。properties.endpoints 是資源自己宣告「我服務哪些 API」的那張映射,它的條目數:
| 時點 | 條目數 | 主機名分佈 |
|---|---|---|
kind: OpenAI |
12 | 全部 openai.azure.com |
kind: AIServices |
65 | 原 12 條原樣保留 + cognitiveservices.azure.com(多數)+ services.ai.azure.com + Speech、Translator 各自的主機 |
| 回退後 | 12 | 全部 openai.azure.com |
新增的 53 條包括 Language、Text Analytics、Content Safety、FormRecognizer、Computer Vision、Speech 各支、Translator、Foundry API、以及 Cohere 與 Mistral 的 API 面。一行 kind 的 patch,讓同一顆資源宣告的 API 面變成 5 倍多。 這是 Foundry 的產品決定,官方升級文件明講 Foundry resource 是 capability superset;本次把它量成了一個數字。
單獨看,面變大是功能,不是問題。問題出在它跟另一件事交叉。官方升級文件自己給了一張治理對照表(同一頁,ms.date 2026-08-19):
| RBAC 指派 | 升級前 | 升級後 |
|---|---|---|
Cognitive Services User |
OpenAI features | All Foundry features |
Cognitive Services OpenAI User |
OpenAI features | OpenAI features |
| 未套 policy 的模型存取 | OpenAI models | Any Foundry model |
文件的處置是「提醒 IT 管理員檢查 wildcard 指派」。但文件沒講為什麼,而這件事可以直接讀出來——不需要任何雲端資源,az role definition list --name "<role>" 就看得到:
Cognitive Services User dataActions = ["Microsoft.CognitiveServices/*"]
Cognitive Services OpenAI User dataActions = 16 條,每一條都在 accounts/OpenAI/ 底下
Cognitive Services Language Reader 含 accounts/Language/analyze-text/action
accounts/OpenAI/ 與 accounts/Language/ 是同層的兄弟路徑。一條萬用字元蓋住整個 provider;一組列舉只蓋住其中一支。第三個角色只是旁證:證明兄弟路徑真的存在。
所以觀察得到的擴權需要兩個輸入,缺一不可:
角色語法只決定哪一條沒被改過的指派會跟著擴,它不創造那個擴張。兩個都在,有效權限就變寬了;而這個變化不出現在任何 diff 裡——指派沒動、role definition 沒動,動的是資源上的一個屬性。IaC 的 plan 會告訴你 kind 從 OpenAI 變 AIServices,不會告訴你誰因此多了 53 條能力面可以叫。
「面」和「權限路徑」不是同一個量——properties.endpoints 數的是資源宣告的 API 面,RBAC 比對的是 data action 路徑;本次在權限層只實測了其中一條(Language analyze-text)。
這跟系列前面兩篇同族但不同:Day 24 是「沒寫不等於預設」,Day 19 是「簽章證明不了的才是難題」,這篇是**「沒改的東西語意變了」**。
順帶第二根刺,同一份 role definition 讀得到:Cognitive Services User 的 actions 含 Microsoft.CognitiveServices/accounts/listkeys/action。它本來就能列 API key,而升級後那把 key(逐位元組沒變的那把)打開的是更大的面。Cognitive Services OpenAI User 沒有 listkeys。
到這裡都是結構事實,任何人重跑 az role definition list 就能複驗。但「那條 Language 路徑對萬用字元角色會不會真的通」是預測,不是結論——它要在 data plane 上量。
實驗的形狀是有控制臂的:一顆 ephemeral 資源(前綴 + CSPRNG 後綴,名稱不重用,用完 delete + purge),三個 ephemeral Entra app registration,各自在這顆資源的確切 scope 上恰好一個角色:
| 臂 | 角色 | 角色的 dataActions |
|---|---|---|
| P 假說臂 | Cognitive Services User |
一條萬用字元 |
| Q 陰性對照 | Cognitive Services OpenAI User |
16 條,全在 accounts/OpenAI/ |
| R 陽性對照 | Cognitive Services Language Reader |
含 accounts/Language/analyze-text/action |
Q 就是這個系列的 lab 自己:Day 10 給 APIM gateway、Day 20 給 managed identity、Day 24 給 Container App 的,全是 Cognitive Services OpenAI User。R 的用途是證明「Language 路徑在這顆資源、這個區域、這個 audience 下真的能通」——沒有 R,P 通了可能是路徑本來就開放,Q 不通可能是路徑根本不存在。
指派的檢查用 --include-inherited 而不是 --all(後者只是「列出目前訂閱底下的所有指派」),而且比對 role definition id,不比對顯示名稱。
token 分兩個 audience 取:/openai/v1/* 用 https://ai.azure.com/.default,Language 用 https://cognitiveservices.azure.com/.default;每次送出前先比對 token 的 aud 與目標主機,不符就中止——錯 audience 的 401 跟「路徑不可用」的 401 長得一模一樣。
正式觀測之前有兩道閘門。閘門一:P、Q 各自對 GET /openai/v1/models 拿到 200——證明兩臂的指派在 data plane 上已生效(升級後 4 秒,各第一次嘗試就過;沒有重演 Day 20 那次 14 分 44 秒的傳播)。閘門二是 R 對 Language analyze-text 拿到 200,證明路徑本身可用。R 輪詢了 17 次、320 秒,前 16 次全是 DNS 查無此名,就是上一節那五分鐘。
兩道閘門都過了,正式觀測才成立。同一條路徑、同一 payload、相差 1 秒:
| 臂 | 時點(UTC) | 狀態 | 回應 |
|---|---|---|---|
| P(萬用字元) | 22:25:09 | 200 | 正常語言偵測結果 |
| Q(列舉,OpenAI only) | 22:25:10 | 401 | PermissionDenied |
Q 的回應 body(principal id 遮罩):
{"error":{"code":"PermissionDenied","message":"The principal `<Q-sp-object-id>` lacks the required data action `Microsoft.CognitiveServices/accounts/Language/analyze-text/action` to perform `POST /language/:analyze-text` operation."}}
資源自己把缺的那條 data action 逐字講出來。 這比從 role definition 推論強一階——機制不是我推的,是服務端回的。兩臂之間的差別只剩角色;P 的角色從頭到尾沒被改過。

一件事要照實寫:開跑前預先登錄的結果表把「被拒」寫成 403,實際回的是 401。腳本因此把這組結果判為「落在預登錄表之外」,只記原始狀態碼,事後不補表。所以能寫的是「P 成功、Q 被拒、服務端指名缺的 data action」,不能寫「符合預測」——預測的那格是 (200, 403),量到的是 (200, 401)。對下一次同型實驗的教訓是:格子要按語意(成功/被拒/其他)寫,不要按單一狀態碼寫。
忍喵:這個 401 不是攻擊面,是提醒面——它告訴你萬用字元角色那一邊會安靜地拿到 200。你帳號上有幾條指派是Cognitive Services User?不是猜,az role assignment list --scope <account id> --include-inherited跑一次。
官方說升級可逆,前置條件是先刪掉 projects、connections、非 Azure OpenAI 的 model deployments。本次在有一個 project 的狀態下試回退:
ERROR: (RollbackToOpenAIKindNotAllowedWithActiveProjects) Rollback to kind OpenAI is not allowed when there are active projects.
不是泛用的 BadRequest,是專屬錯誤碼——平台預期你會撞上。刪掉 project 後回退成功,kind 與 properties.endpoint 都還原,endpoints 條目數回到 12。
但回退後的資源不是原狀。Azure/azure-rest-api-specs#38678(2025-11-10 開,查核 2026-09-01 仍 open)回報:AIServices → OpenAI 回退後 allowProjectManagement 仍留在 true。本次複驗到:
| 時點 | kind |
allowProjectManagement |
|---|---|---|
| 建立 | OpenAI |
False |
| 升級後 | AIServices |
True |
| 回退後 | OpenAI |
True |
結果是一顆 kind: OpenAI 且 allowProjectManagement: true 的帳號——建立路徑產不出這個狀態。「回得去」與「回到原樣」是兩件事;而且回退的代價跟你用 Foundry 用得多深成正比:project 開得越多、connection 接得越多,回頭路越窄。
忍喵:「可逆」在 production 的意思是「我試過回來,而且回來之後長得跟去之前一樣」。這裡第一句成立、第二句不成立——所以它是「可回退」,不要在變更單上寫「可逆」。
還有一項刻意沒量:既有 model deployment 升級後是否原樣存活。驗它要在 ephemeral 資源上開 deployment,而那吃的是服役帳號同一個 subscription × region 的 TPM 池——為了一篇文章去碰 34 篇文章的 runtime 額度,不划算。本次全程未開任何 deployment、未呼叫任何 LLM,腳本對 deployment create 這類指令直接拒絕執行。這一格空著,照實寫。
先講這個系列的判斷:服役中的那顆 Azure OpenAI 帳號暫不升級。 三個理由都來自上面的量測:
觸發升級的條件也寫清楚:需要 Foundry-only 的能力(Agent Service、Foundry evaluation、非 OpenAI 的模型)那一天。到那天之前,升級只是把能力面撐大而已。
被否決的替代方案:另建一顆 Foundry resource,把 deployment 搬過去。它避開了 in-place 升級的治理副作用(新資源上的指派是你重新給的),但代價是呼叫端要換 endpoint 與 key——這兩樣本次量過:in-place 升級時 endpoint 換 FQDN、key 不變,另建資源則兩樣都是新的。
額度會怎麼分配本篇沒有量(quota 的 scope 按 model 與 deployment type 區分,見 quotas and limits,查核 2026-09),兩種做法的帳單差也沒有量,所以這裡不比較貴不貴。
可執行的判準是這個:你能不能把現有指派清單逐條讀完,並說出每一條為什麼存在。讀得完就逐條審查後 in-place 升級;讀不完,那份清單本身就是「重新來過」的理由——不是因為它比較省,是因為你已經無法對它負責。
真的要升的時候,查的東西有兩組,而且要交叉:
| 查什麼 | 怎麼查 | 為什麼 |
|---|---|---|
全部有效 RBAC 指派,逐條看角色的 dataActions 是否涵蓋升級後才可用的 action |
az role assignment list --scope <account id> --include-inherited,再對每個 role definition 讀 dataActions |
萬用字元寫法優先看,但只掃萬用字元會漏掉逐條列了新 action 的自訂角色 |
| 套在這顆資源上的 Azure Policy | 另一條路:查 policy assignment 與其規則,看它限制的是哪些 resource kind/capability/model | Policy 沒有 dataActions 也沒有 role definition,跟 RBAC 是兩套機制,要分開查 |
| 升級後資源會多出哪些面 | 官方 capability 文件;或在 ephemeral 資源上升一次,數 properties.endpoints |
輸入 (a):這些是它們會變寬到哪裡 |
呼叫端的 base URL 是抄 properties.endpoint 還是寫死主機名 |
grep 設定檔 | 主要 endpoint 欄位會換 FQDN,新主機名有幾分鐘的 DNS 空窗 |
有沒有人靠 listkeys 拿 key |
同一份指派清單 | 萬用字元角色本來就能列 key,而 key 升級後不變 |
「只掃萬用字元角色」是官方文件給的處置,它抓的是最高風險的那一類,但不是充分條件,有兩個漏法。一是沒有 (a) 的清單,你不知道萬用字元蓋到了什麼;反過來只看能力面不看指派,你不知道誰蓋到了。二是逐條列舉的角色不等於安全——本次的 Q 臂只證明「這一個列舉角色對這一條 Language action 被拒」,一個自訂角色若當初就列了升級後才可用的 action,它不含萬用字元也一樣會通。所以判準是「全部有效授權與 policy,逐條對照升級後才可用的能力」,萬用字元只是優先順位第一。
最後一條:先在 ephemeral 資源上升一次。整個流程本次跑完是 12 分 33 秒、54 次 az 呼叫,帳號從建立到刪除存活 10 分 03 秒。你會在自己的區域、自己的 API 版本上拿到自己的 12 → N,而不是我的 65。
資源形狀是「即用即刪」:建 kind: OpenAI 帳號 → 三個 app registration 與指派 → 升級 → 兩道閘門 → 觀測 → 建 project → 回退(失敗)→ 刪 project → 回退 → 刪帳號 → purge。帳號從建立到刪除 10 分 03 秒。
先盤可能產生費用的東西:全程零 deployment、零 LLM 呼叫;真正觸及服務端的是 Language analyze-text 成功 2 次(R 的閘門二、P 的觀測),Q 的 1 次回 401,/openai/v1/models 2 次;另有 19 次探針在 DNS 就失敗,沒碰到服務。
閒置的 S0 帳號本身有沒有固定費用、Entra app registration 是不是免費——本篇未查核,所以不寫。這兩件事常被當成常識講,但常識不是來源,而定價主張在這個系列要標查核年月與官方出處。
帳單對帳(Azure Cost Management usageDetails,metric=ActualCost)跑了四次,時戳與結果照實記:run 結束後 11 分鐘、8 小時 57 分、12 小時 16 分、26 小時 10 分——四次都是 0 筆。
所以這篇能寫的只有兩句:這次 run 的實際花費未確立;2026-09-01 該用量日無計費紀錄。
這句話是關於帳單的陳述,不是關於成本的陳述。它說「查不到列」,不說「這件事免費」——而且中間那條界線在這次是真的分不開:這個訂閱沒有任何逐日固定計費的資源可以當心跳列,所以「0 筆」分不出「已處理且無費用」與「尚未處理」。
能排除的只有兩個較弱的解釋:不是資源刪掉才查不到(同一支查詢在 08-30 撈得到兩個同樣已刪除的 Search 服務),也不是查詢寫錯(對照窗口回得出 Day 34 那筆 9.08 TWD,逐位元組相同)。
「≥24 小時後仍 0 筆就寫『該用量日無計費紀錄』」這個措辭,是在第一次查詢之前就寫下的。看到結果之後把門檻從 24 小時挪到 48 小時,跟看到 401 之後回頭改預登錄表是同一件事——所以照原規則落盤。真有遲到的列出現,取證檔上那張對帳表會續記第 5 次,這一段跟著改。
Cognitive Services OpenAI User 這個角色,對 Language analyze-text 這一條 action,在升級後仍被拒」。不能推出「所有非萬用字元指派都安全」——一個自訂角色若逐條列了升級後才可用的 action,它一樣會通,本次沒有測這種情形。previousKind 是「這條讀取路徑上未觀測到」,不是「不存在」。#38678 是他人回報的缺陷,本次複驗到——只寫複驗到,不推論修復時程。ms.date 集中在 2026-05 到 2026-08,ms.update-cycle 90 天。全部是 dated observation。回到題目:kind 改一個字,誰的權限變寬了。答案在角色與資源的交叉上:資源的面從 12 條長到 65 條,而本次量到的是「萬用字元那條指派跟著長,被測的那條列舉指派沒有」——一角色、一條 action、一次觀測,不是關於整個列舉類別的結論。這件事不會出現在任何 diff 裡。你帳號上每一條指派各自蓋到什麼,得自己跑 az role assignment list 加上讀 role definition 才知道。
這是 Bonus 通道的第五篇。Day 30 說 Foundry lifecycle「連底都沒有」,這篇補了 resource lifecycle 這一格,而且說清楚只補到這裡——Agent Service 與 evaluation 的底,還是沒有。
用到的 Azure 服務:
kind: OpenAI → AIServices → OpenAI,建立到刪除存活 10 分 03 秒,已 delete + purge;服役帳號全程只被讀取)analyze-text,Entra 驗證,成功呼叫 2 次)本篇的雲端花費:未確立。帳單查核結果是「2026-09-01 該用量日無計費紀錄」(2026-09-03 對帳,T+26h10m,四次查詢皆 0 筆)——這是查不到列,不能解讀為 0 元或免費。ephemeral 資源已全數刪除並 purge,subscription 內無殘留。
本文由作者規劃與撰寫,AI(Claude)協助草稿整理與程式碼驗證;技術內容與觀點由作者確認並負責。