← 返回上一頁
實測 DevOps 雲端管理

Pulumi 3.276 實測:自動產生的 Automation API 不能預覽

先講結果:把 api.destroy(stack="dev", previewonly=True) 丟給 Pulumi 3.276.0 的新 Automation API,回來的是一行 error: --skip-preview and --preview-only cannot be used together。方法…

重點摘要
  • Python SDK 的 pulumi.automation.interface 從 3.268.0 的 13 個方法定義增加到 3.276.0 的 153 個,檔案從約 1,100 行變成約 14,900 行
  • up、destroy、refresh 這幾個方法硬寫了 --skip-preview --yes,所以 preview_only=True 與 strict=True 一呼叫就報參數衝突
  • up(expect_no_changes=True) 因為沒有預覽,發現有變更時資源已經改完才回傳退出碼 7
  • state_get、state_taint、state_rename、state_move 在本機 file backend 上都能直接用,單次耗時 0.15 到 0.26 秒
  • insights_* 這類命令在 file backend 上會直接報錯,需要 Pulumi Cloud
測試環境驗證於 2026-11-14
適用讀者
用 Python 寫 IaC 自動化腳本、想直接呼叫 Pulumi 全部 CLI 命令的工程師
測試版本
Pulumi CLI 與 Python SDK 3.276.0,對照 3.268.0;pulumi-random provider 4.21.6
測試環境
macOS 27(arm64)、Python 3.12、YAML runtime、file:// 本機 backend
本頁目錄

先講結果:把 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,記下哪些能用、哪些會撞牆。

13 → 153interface 模組的方法定義數(3.268.0 → 3.276.0)
0.15~0.26 sstate_get/taint/rename/move 單次耗時
退出碼 7expect_no_changes 偵測到變更時的結果,資源已被改動

github.compulumi/pulumi用程式語言寫基礎設施的 IaC 工具,CLI 與各語言 SDK 同一個 repoApache-2.0

一、這個版本到底改了什麼

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。

3.268.0 的 interface 模組有 13 個方法定義、約 1,100 行,3.276.0 有 153 個、約 14,900 行
圖 1:同一個 interface 模組在兩個版本的規模(本機 pip 安裝後計算)

二、測試環境與方法

整輪都在本機做,用 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=...)) 建立物件,每個呼叫都包計時與例外捕捉。每項只跑一次,所以耗時只能當量級參考,不是統計結果。

Python 腳本透過 API 類別呼叫 pulumi CLI 子行程,CLI 與 random provider 溝通並寫入 file backend,state_move 與 state_rename 只動 state
圖 2:本次實測的呼叫路徑,generated API 本身只是 subprocess 外殼

第一次嘗試就踩到一個小雷: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 如下:

python
$ 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,看它會不會被換掉:

python
$ 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,想確認這道防線有沒有用:

python
$ 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 搬過去:

python
$ 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:

python
$ 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 的歷史與設定一起清掉。唯一的前提是你要接受「直接動手」這件事,下一節說明為什麼。

generated API 各呼叫耗時,version 0.06 秒,state 系列 0.15 到 0.26 秒,up 與 destroy 約 0.6 到 0.7 秒
圖 3:單次呼叫耗時(n=1,含啟動 pulumi 子行程)

六、限制:硬寫的 --skip-preview 與只給 Cloud 的命令

回頭看前面那個 preview_only 錯誤,成因就藏在產生出來的程式碼裡。destroy 方法的實作一開頭就是:

__final = []
__final.append("destroy")
__flags = []
__flags.append("--skip-preview")
__flags.append("--yes")

也就是不管你傳什麼參數,--skip-preview 與 --yes 一定先塞進去,這是自動化情境為了避免互動式確認而做的設計。副作用是簽章裡跟預覽相關的參數都形同虛設。實測的三個例子:

python
$ 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 上回:

python
$ 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 加預覽,等官方把預覽行為處理好再重新評估。

升級到 3.276 前後的檢查清單
  • 用 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 是額外多出來的一條路。

參考資料

分享這篇
X LinkedIn Facebook Hacker News Reddit

這篇對你有幫助嗎?

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料。