先講結果:把 api.destroy(stack="dev", preview_only=True) 丟給 Pulumi 3.276.0 的新 Automation API,回來的是一行 error: --skip-preview and --preview-only cannot be used together。方法簽章裡明明有 preview_only 這個參數,呼叫卻必定失敗。
事情的起點是 3.276.0 的 release notes(2026-11-12)。它只寫了三行:NodeJS、Python、Go 的 generated automation API 新增 destroy、template、insights。字面看起來是補了三個命令,實際把 Python SDK 的 interface 模組拆開來看,規模完全不是三個。這篇把它從 pip 裝起來,用本機 file backend 跑了一輪 up、state 操作與 destroy,記下哪些能用、哪些會撞牆。
github.compulumi/pulumi用程式語言寫基礎設施的 IaC 工具,CLI 與各語言 SDK 同一個 repo
一、這個版本到底改了什麼
Pulumi 的 Automation API 本來是「把 pulumi CLI 包成函式庫」:LocalWorkspace、Stack 那一組高階物件,負責建 stack、跑 up、拿輸出。3.276.0 新增的是另一條路,叫 generated automation API,檔案開頭直接寫著 Code generated by sdk/python/tools/automation; DO NOT EDIT,由 CLI 的命令定義自動產生,一個 CLI 命令對一個 Python 方法。
release notes 列的三項各有一個 PR:#25053 加 destroy、#25061 加 template、#25069 加 insights。不過這個產生器不是這一版才有。我們拿 3.268.0 與 3.276.0 的同一個檔案比對,中間隔了八個版本,所以 13 變 153 不能全算在 3.276.0 頭上,這一版只是把這條線又往前推了三塊。
跨八個版本的差異如下,方法名稱前綴分組是從 3.276.0 的檔案算出來的:
| 命令群組 | 方法數 | 備註 |
|---|---|---|
env_* |
59 | 環境(env)相關命令 |
org_* |
20 | 組織與成員管理 |
config_*、state_* |
各 12 | 本機 backend 可用 |
insights_* |
11 | file backend 實測會報錯 |
deployment_* |
10 | |
up、destroy、refresh |
各 1 | 都硬寫了 --skip-preview --yes |
3.268.0 版本的 API 類別只有 cancel、install、new、org*、version、whoami 這些,連 up 都沒有。要跑 up 一定得走高階的 Stack。

二、測試環境與方法
整輪都在本機做,用 file backend,不需要登入 Pulumi Cloud,也不碰任何雲端資源。
| 項目 | 內容 |
|---|---|
| 作業系統 | macOS 27(arm64) |
| Pulumi | CLI 與 Python SDK 皆為 3.276.0;對照組為 3.268.0 |
| Provider | pulumi-random 4.21.6(RandomPet、RandomId,純本機運算) |
| Runtime | YAML,沒有另外寫 Python 程式 |
| Backend | file:// 本機目錄,passphrase 以環境變數給定 |
| 測試日期 | 2026-11-14 |
專案只有兩個資源,足夠看出 state 的變化:
name: demo
runtime: yaml
resources:
pet:
type: random:RandomPet
properties:
length: 2
id:
type: random:RandomId
properties:
byteLength: 4
outputs:
petName: ${pet.id}
hex: ${id.hex}
腳本用 API(PulumiCommand(root=...)) 建立物件,每個呼叫都包計時與例外捕捉。每項只跑一次,所以耗時只能當量級參考,不是統計結果。

第一次嘗試就踩到一個小雷:PulumiCommand(root="pulumi-home") 給了相對路徑,後面呼叫又帶 cwd="proj",結果 FileNotFoundError: 'pulumi-home/bin/pulumi'。路徑是在切換到 cwd 之後才被解析的,root 要給絕對路徑。另外 up 沒有 skip_preview 這個參數,前面第一版腳本多帶了它,吃了一個 TypeError,原因在第六節。
三、實測 up 與 state_get、taint
先用 CLI 建好 stack 並跑過一次 up,再用 Python 呼叫。版本確認與 state_get 如下:
$ api.version().stdout v3.276.0 $ api.state_get("urn:pulumi:dev::demo::random:index/randomPet:RandomPet::pet", stack="dev", cwd="proj").stdout Type: random:index/randomPet:RandomPet URN: urn:pulumi:dev::demo::random:index/randomPet:RandomPet::pet ID: crucial-tadpole Inputs: length: 2 Outputs: id : "crucial-tadpole" length : 2 separator : "-"
state_get 的第一個參數是資源的 URN,也可以用 pulumi do show-resources 列出來的短識別碼。呼叫一次 0.15 秒,等於 pulumi state get 的成本,沒有額外負擔。
接著把 pet 標成 tainted,再呼叫 up,看它會不會被換掉:
$ api.state_taint(URN + "pet", stack="dev", cwd="proj").stdout 1 resources tainted $ api.up(stack="dev", cwd="proj", suppress_progress=True).stdout Updating (dev): ++ random:index:RandomPet pet creating replacement (0s) [diff: ~taint] ++ random:index:RandomPet pet created replacement (0.01s) [diff: ~taint] +- random:index:RandomPet pet replacing (0s) [diff: ~taint] +- random:index:RandomPet pet replaced (0.00s) [diff: ~taint] -- random:index:RandomPet pet deleting original (0s) [diff: ~taint] -- random:index:RandomPet pet deleted original (0.00s) [diff: ~taint] pulumi:pulumi:Stack demo-dev Outputs: hex : "2d44866c" ~ petName: "crucial-tadpole" => "humane-elk" Resources: +-1 replaced 2 unchanged
hex 沒動,petName 從 crucial-tadpole 換成 humane-elk,只有被標記的那個資源被重建。這一段跟直接下 CLI 的結果一模一樣,也是 generated API 最好的地方:行為不用再多學一層。
回傳值要注意。這些方法回的是 CommandResult,只有 stdout、stderr、code 三個欄位,沒有像高階 Stack.up() 那樣的 UpResult(有輸出值與摘要物件)。想拿 petName 得自己解析文字,或另外呼叫 stack output --json。
四、state_rename 與 expect_no_changes 的坑
資源改名是 IaC 常見的麻煩,改了程式裡的名稱,Pulumi 會當成「刪舊的、建新的」。新的 state_rename 讓人先把 state 裡的名稱改掉。團隊故意只改 state、不改 Pulumi.yaml,並且帶上 expect_no_changes=True,想確認這道防線有沒有用:
$ api.state_rename(URN + "pet", "mascot", stack="dev", cwd="proj").stdout Resource renamed $ api.up(stack="dev", cwd="proj", suppress_progress=True, expect_no_changes=True) CommandError: code: 7 stdout: Updating (dev): + random:index:RandomPet pet creating (0s) + random:index:RandomPet pet created (0.00s) - random:index:RandomPet mascot deleting (0s) - random:index:RandomPet mascot deleted (0.00s) pulumi:pulumi:Stack demo-dev Outputs: hex : "2d44866c" ~ petName: "humane-elk" => "absolute-burro" Resources: + 1 created - 1 deleted 2 changes. 2 unchanged stderr: error: no changes were expected for update but changes were proposed
這是整輪最值得記住的一段。expect_no_changes 的用意是「有變更就別動」,但輸出清楚顯示 pet 已經建好、mascot 已經刪掉,petName 也換了新值,Pulumi 是先做完才用退出碼 7 告訴你有變更。原因在於 generated API 的 up 一律帶 --skip-preview,沒有預覽這一關,就沒有「發現變更之前先停下來」的機會。
拿 random 資源來試無傷大雅。換成資料庫或負載平衡器,同樣的呼叫就是真的刪掉再建。要用 CI 擋變更,得自己先跑 pulumi preview --expect-no-changes,或用高階 API 的預覽,不能指望這個參數。
五、state_move 與 destroy --remove
再來是把資源在 stack 之間搬動。團隊另外建了一個空的 two stack,把 pet 從 dev 搬過去:
$ api.state_move(URN + "pet", source="dev", dest="two", cwd="proj").stdout Planning to move the following resources from organization/demo/dev to organization/demo/two: - urn:pulumi:dev::demo::random:index/randomPet:RandomPet::pet Successfully moved resources from organization/demo/dev to organization/demo/two
一個呼叫、0.26 秒完成。不過這一步我們只確認了命令回報成功,沒有再回頭列兩個 stack 的資源清單做二次驗證,因為 do_show_resources 這個方法不接受 stack 參數(呼叫時直接 TypeError),要改用 CLI 查。
收尾用 destroy:
$ api.destroy(stack="dev", cwd="proj", remove=True, suppress_progress=True).stdout Destroying (dev): - random:index:RandomPet pet deleting (0s) - random:index:RandomId id deleting (0s) - random:index:RandomPet pet deleted (0.00s) - random:index:RandomId id deleted (0.00s) - pulumi:pulumi:Stack demo-dev deleting (0s) - pulumi:pulumi:Stack demo-dev deleted (0.00s) Resources: - 3 deleted The resources in the stack have been deleted, and the history and configuration removed.
這就是 release notes 說的新增項目,行為正常,remove=True 連 stack 的歷史與設定一起清掉。唯一的前提是你要接受「直接動手」這件事,下一節說明為什麼。

六、限制:硬寫的 --skip-preview 與只給 Cloud 的命令
回頭看前面那個 preview_only 錯誤,成因就藏在產生出來的程式碼裡。destroy 方法的實作一開頭就是:
__final = []
__final.append("destroy")
__flags = []
__flags.append("--skip-preview")
__flags.append("--yes")
也就是不管你傳什麼參數,--skip-preview 與 --yes 一定先塞進去,這是自動化情境為了避免互動式確認而做的設計。副作用是簽章裡跟預覽相關的參數都形同虛設。實測的三個例子:
$ api.destroy(stack="dev", cwd="proj", preview_only=True) code: 1 stderr: error: --skip-preview and --preview-only cannot be used together $ api.refresh(stack="dev", cwd="proj", preview_only=True) code: 1 stderr: error: --skip-preview and --preview-only cannot be used together $ api.up(stack="dev", cwd="proj", strict=True) code: 1 stderr: error: --strict cannot be used with --skip-preview; strict requires a preview
檔案裡 --skip-preview 一共出現 4 處,--yes 出現 33 處。想預覽的話,目前只能另外走 pulumi preview 或高階 API,generated API 裡找不到出路。這跟 release notes 的措辭有落差:官方只說「新增 destroy」,沒有提醒它沒有預覽這一關。
另一類限制是後端。insights_account_list 在 file backend 上回:
$ api.insights_account_list(cwd="proj") code: 1 stderr: error: resolving cloud context: `pulumi api` requires the Pulumi Cloud backend; run `pulumi login`
template_list 倒是能跑,花了 1.2 秒,回傳 {"templates": []},沒有報錯但清單是空的。env_*、org_*、deployment_* 這幾組我們沒有在 file backend 上測,不下結論。另外找不到的 stack 會給明確的錯誤與退出碼:destroy(stack="nope") 回 error: no stack named 'nope' found,退出碼 6,這種訊息拿來做重試或告警判斷很好用。
七、跟高階 Automation API 怎麼取捨
| 面向 | generated API(pulumi.automation.interface.API) |
高階 LocalWorkspace/Stack |
|---|---|---|
| 涵蓋範圍 | 153 個方法,幾乎對應整個 CLI | 專注在 stack 生命週期 |
| 預覽 | up、destroy、refresh 一律跳過 |
有預覽 API(PreviewResult 型別存在於同一個套件) |
| 回傳值 | CommandResult(stdout/stderr/code) |
UpResult、DestroyResult 等結構化物件 |
| 實測狀態 | 本篇跑過 up、destroy、state_、template_list、insights_ | 本篇沒有實際跑,只確認型別名稱存在 |
說明一下最後一列:高階 API 這次沒有動手測,表格裡它那一欄只根據套件內容與型別名稱,行為細節沒有驗證。
結論是兩種不衝突。需要建立、更新、清理資源的主流程,仍然用高階 API,因為有預覽與結構化結果;那些高階 API 沒包的命令,例如 state_move、state_taint、config_*,用 generated API 直接呼叫會省掉自己拼 subprocess 與參數的工夫。
八、實務建議與結論
這一版的 generated API 現在還適合拿來補高階 API 沒包的命令,不適合拿來當 up/destroy 的主要入口。團隊接下來的做法是:state 操作與設定類命令先在測試 stack 用起來,涉及真實雲端資源的 up、destroy 繼續走高階 API 加預覽,等官方把預覽行為處理好再重新評估。
- 用
pip show pulumi確認 SDK 與 CLI 版本一致 PulumiCommand(root=...)給絕對路徑,避免切換 cwd 後找不到執行檔- 需要預覽的流程不要用
api.up/api.destroy - 不要把
expect_no_changes當成「有變更就不執行」的保護 - 要用
insights_*、env_*前先確認已登入 Pulumi Cloud - 改資源名稱用
state_rename時,同步改程式裡的名稱
只看這次實測,generated API 的方向是對的:行為跟 CLI 一致、呼叫成本低、新命令自動出現。要先弄清楚的是它預設的「自動化模式」把互動確認與預覽一起拿掉了,使用上要比高階 API 更小心,不然 expect_no_changes 這種參數會讓人誤以為多了一層保護。
3.276.0 的 generated API 可以預覽變更嗎?
本次實測不行。`up`、`destroy`、`refresh` 都硬寫了 `--skip-preview`,`preview_only` 與 `strict` 會直接回參數衝突錯誤,預覽請走 `pulumi preview` 或高階 API。
使用 generated API 一定要登入 Pulumi Cloud 嗎?
不用。`up`、`destroy`、`state_*` 在本機 file backend 都能跑;`insights_*` 這類命令在 file backend 上會報錯,需要 Pulumi Cloud。
NodeJS 與 Go 版本也一樣嗎?
release notes 說三個語言都新增了同樣的命令,但本次只實測 Python,其他語言的行為沒有驗證。
舊的 LocalWorkspace 寫法需要改嗎?
不用。高階 API 還在同一個套件裡,這次升級沒有拿掉它;generated API 是額外多出來的一條路。
參考資料
- Pulumi GitHub Releases:v3.276.0(2026-11-12)
- Pulumi PR:#25053 destroy、#25061 template、#25069 insights
- Pulumi Automation API 文件:pulumi.com/docs/iac/using-pulumi/automation-api
這篇對你有幫助嗎?


發佈留言