iT邦幫忙

2026 iThome 鐵人賽

0
AI Engineering

Backend 工程師的 Azure GenAI 實戰系列 第 35

Day 35:Azure OpenAI 升級 Foundry:kind 改一個字,沒動過的 role assignment 跟著變寬

  • 分享至 

  • xImage
  •  

這個系列 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 那顆資源今天的處境

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 overviewms.date 2026-08-14,查核 2026-09

lab 的 create-openai.sh 建的就是 --kind OpenAI,服役中的那顆帳號今天仍是這個 kind,Day 4 到 Day 34 全部文章的 runtime 都指著它。所以這不是別人的問題。

但「該不該升」對這個系列不是顯然要升。架構文推薦 Foundry resource 作為「most AI development scenarios」的起點,同一頁也留了反面條件(Microsoft Foundry architecturems.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 Foundryms.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;一組列舉只蓋住其中一支。第三個角色只是旁證:證明兄弟路徑真的存在。

所以觀察得到的擴權需要兩個輸入,缺一不可

  • (a) 資源的能力面擴大——12 → 65,Foundry 的產品決定;
  • (b) 某條既有指派用的是萬用字元寫法——當初挑角色時挑到的那一種。

角色語法只決定哪一條沒被改過的指派會跟著擴,它不創造那個擴張。兩個都在,有效權限就變寬了;而這個變化不出現在任何 diff 裡——指派沒動、role definition 沒動,動的是資源上的一個屬性。IaC 的 plan 會告訴你 kindOpenAIAIServices,不會告訴你誰因此多了 53 條能力面可以叫。

「面」和「權限路徑」不是同一個量——properties.endpoints 數的是資源宣告的 API 面,RBAC 比對的是 data action 路徑;本次在權限層只實測了其中一條(Language analyze-text)。

這跟系列前面兩篇同族但不同:Day 24 是「沒寫不等於預設」,Day 19 是「簽章證明不了的才是難題」,這篇是**「沒改的東西語意變了」**。

順帶第二根刺,同一份 role definition 讀得到:Cognitive Services UseractionsMicrosoft.CognitiveServices/accounts/listkeys/action。它本來就能列 API key,而升級後那把 key(逐位元組沒變的那把)打開的是更大的面。Cognitive Services OpenAI User 沒有 listkeys

到這裡都是結構事實,任何人重跑 az role definition list 就能複驗。但「那條 Language 路徑對萬用字元角色會不會真的通」是預測,不是結論——它要在 data plane 上量。

四、實測:三個 principal、一條路徑、相差一秒

實驗的形狀是有控制臂的:一顆 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 的角色從頭到尾沒被改過。

一行 kind 的 patch 讓誰的權限變寬了。上方:kind: OpenAI 的資源宣告 12 條 endpoint、全部 openai.azure.com;經過深藍節點「az cognitiveservices account update --kind AIServices --allow-project-management true,同一顆資源、一個屬性的 patch」之後,變成 kind: AIServices、65 條 endpoint——原 12 條原樣保留,加上 Language、Speech、Vision、Content Safety、Translator、Foundry API。下方兩個角色節點:粉底的 P「Cognitive Services User,dataActions = Microsoft.CognitiveServices/*,一條萬用字元蓋整個 provider」與淺藍的 Q「Cognitive Services OpenAI User,dataActions = 16 條、全在 accounts/OpenAI/ 底下,本系列 Day 10/20/24 用的就是這條」,各以一條標注「指派沒動、role definition 沒動」的邊指向升級後才存在的路徑「POST /language/:analyze-text」。路徑分出兩個結果:P 是 200、正常語言偵測結果;Q 是 401 PermissionDenied,服務端指名缺 accounts/Language/analyze-text/action。

一件事要照實寫:開跑前預先登錄的結果表把「被拒」寫成 403,實際回的是 401。腳本因此把這組結果判為「落在預登錄表之外」,只記原始狀態碼,事後不補表。所以能寫的是「P 成功、Q 被拒、服務端指名缺的 data action」,不能寫「符合預測」——預測的那格是 (200, 403),量到的是 (200, 401)。對下一次同型實驗的教訓是:格子要按語意(成功/被拒/其他)寫,不要按單一狀態碼寫。

https://ithelp.ithome.com.tw/upload/images/20260904/20168288YnPC21yrwF.png
忍喵:這個 401 不是攻擊面,是提醒面——它告訴你萬用字元角色那一邊會安靜地拿到 200。你帳號上有幾條指派是 Cognitive Services User?不是猜,az role assignment list --scope <account id> --include-inherited 跑一次。

五、回得去,但不是 undo

官方說升級可逆,前置條件是先刪掉 projects、connections、非 Azure OpenAI 的 model deployments。本次在有一個 project 的狀態下試回退:

ERROR: (RollbackToOpenAIKindNotAllowedWithActiveProjects) Rollback to kind OpenAI is not allowed when there are active projects.

不是泛用的 BadRequest,是專屬錯誤碼——平台預期你會撞上。刪掉 project 後回退成功,kindproperties.endpoint 都還原,endpoints 條目數回到 12。

但回退後的資源不是原狀。Azure/azure-rest-api-specs#38678(2025-11-10 開,查核 2026-09-01 仍 open)回報:AIServicesOpenAI 回退後 allowProjectManagement 仍留在 true。本次複驗到:

時點 kind allowProjectManagement
建立 OpenAI False
升級後 AIServices True
回退後 OpenAI True

結果是一顆 kind: OpenAIallowProjectManagement: true 的帳號——建立路徑產不出這個狀態。「回得去」與「回到原樣」是兩件事;而且回退的代價跟你用 Foundry 用得多深成正比:project 開得越多、connection 接得越多,回頭路越窄。

https://ithelp.ithome.com.tw/upload/images/20260904/201682886EdTsxsQ0P.png
忍喵:「可逆」在 production 的意思是「我試過回來,而且回來之後長得跟去之前一樣」。這裡第一句成立、第二句不成立——所以它是「可回退」,不要在變更單上寫「可逆」。

還有一項刻意沒量:既有 model deployment 升級後是否原樣存活。驗它要在 ephemeral 資源上開 deployment,而那吃的是服役帳號同一個 subscription × region 的 TPM 池——為了一篇文章去碰 34 篇文章的 runtime 額度,不划算。本次全程未開任何 deployment、未呼叫任何 LLM,腳本對 deployment create 這類指令直接拒絕執行。這一格空著,照實寫。

六、所以該不該升,升之前查什麼

先講這個系列的判斷:服役中的那顆 Azure OpenAI 帳號暫不升級。 三個理由都來自上面的量測:

  1. lab 落在官方自己寫的「standalone might be sufficient」那句話裡——它需要的 API 面在 12 條之內,多出來的 53 條沒有一條是它要叫的。
  2. portal 不支援是入口的事。lab 的 infra 全走 CLI(Day 26),deployment 管理走 CLI,這條路今天沒有被關。
  3. 回退回不到原狀(#38678 複驗到),而「既有 deployment 是否存活」本次沒有量——兩個未定之數疊在服役資源上,代價是 34 篇文章的前提。

觸發升級的條件也寫清楚:需要 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 usageDetailsmetric=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 次,這一段跟著改。

這天的誠實邊界

  • 一次觀測。 一顆資源、japaneast、三個角色、一條路徑,2026-09-01。不宣告「升級是安全的」,也不宣告「升級一定會擴權」。
  • Q 臂證明的範圍只有一格。 它證明的是「Cognitive Services OpenAI User 這個角色,對 Language analyze-text 這一條 action,在升級後仍被拒」。不能推出「所有非萬用字元指派都安全」——一個自訂角色若逐條列了升級後才可用的 action,它一樣會通,本次沒有測這種情形。
  • 401 不是 403。 觀測落在預登錄表之外,照實記,不補表。
  • previousKind 是「這條讀取路徑上未觀測到」,不是「不存在」。
  • DNS 那 5 分 25 秒是單次觀測,不是 SLA,不外推。
  • #38678 是他人回報的缺陷,本次複驗到——只寫複驗到,不推論修復時程。
  • 既有 deployment 升級後是否存活:未觀測。 Agent Service、Foundry evaluation、hosted agent:零實測。portal 路徑:沒走。
  • 半衰期短。 本篇引用的 Foundry 文件 ms.date 集中在 2026-05 到 2026-08,ms.update-cycle 90 天。全部是 dated observation。
  • 實際花費未確立;查到的是「0 筆紀錄」。 兩者不可互換——0 筆不能讀成 0 元或免費。這個訂閱沒有逐日心跳列,證不了管線已處理到那一天。上一節。

小結

回到題目:kind 改一個字,誰的權限變寬了。答案在角色與資源的交叉上:資源的面從 12 條長到 65 條,而本次量到的是「萬用字元那條指派跟著長,被測的那條列舉指派沒有」——一角色、一條 action、一次觀測,不是關於整個列舉類別的結論。這件事不會出現在任何 diff 裡。你帳號上每一條指派各自蓋到什麼,得自己跑 az role assignment list 加上讀 role definition 才知道。

這是 Bonus 通道的第五篇。Day 30 說 Foundry lifecycle「連底都沒有」,這篇補了 resource lifecycle 這一格,而且說清楚只補到這裡——Agent Service 與 evaluation 的底,還是沒有。

用到的 Azure 服務

  • Azure OpenAI in Microsoft Foundry(ephemeral 帳號 kind: OpenAIAIServicesOpenAI,建立到刪除存活 10 分 03 秒,已 delete + purge;服役帳號全程只被讀取)
  • Microsoft Foundry Tools — Language(analyze-text,Entra 驗證,成功呼叫 2 次)
  • Microsoft Entra ID(三個 ephemeral app registration,已刪除)

本篇的雲端花費:未確立。帳單查核結果是「2026-09-01 該用量日無計費紀錄」(2026-09-03 對帳,T+26h10m,四次查詢皆 0 筆)——這是查不到列,不能解讀為 0 元或免費。ephemeral 資源已全數刪除並 purge,subscription 內無殘留。


本文由作者規劃與撰寫,AI(Claude)協助草稿整理與程式碼驗證;技術內容與觀點由作者確認並負責。


上一篇
Day 34:hybrid、vector、semantic ranker 怎麼選——先問答案被哪條腿弄丟,再問哪個模式排得準
系列文
Backend 工程師的 Azure GenAI 實戰35
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言