IaC 宣告不了的部分,以及怎麼用 REST 補完
一份 JSONL 文件可以宣告系統的大部分,但不是全部。有六項設定沒有對應的行類型,或是雖然有、卻只能寫下綁環境的值,所以一次完整的部署是「一份文件」加上「一小串 REST 呼叫」。
這一頁就是那串呼叫。每一項都會說要跑什麼、相對於 plan / apply 該在什麼時候跑,以及最容易踩到的那件事:之後這個設定由哪一邊擁有,因為只要文件也宣告了同一件事,重新 apply 就會直接蓋掉。
清單
| 設定 | 文件為什麼裝不了 | 怎麼補 |
|---|---|---|
require_approval 由誰核准 | 規則引用的是覆核範本的原始 id,而範本與群組屬於覆核模組 | 先建立覆核範本 |
| Callback token | 沒有行類型。密鑰只顯示一次,無法在文件裡來回 | apply 之後鑄造 |
| 帶密鑰的公開讀取權杖 | public_read v1 只支援無密鑰,理由同上 | apply 之後鑄造 |
跨表的 create_record / update_record 動作 | 它的 data 鍵一律對照觸發器自身的資料表解析;只有 materialize_slots 接受 $table:<ref> 目標 | 用 REST 宣告該觸發器 |
| 系統 tag 以外的 tag | 沒有 tag 行類型;command.tag_id 只吃同 scope 的原始 id | 先建立 tag |
移除 client_access 設定 | 這個 kind 沒有 state,所以沒有 absent 可宣告 | 在文件裡停用它 |
1. 先建立覆核範本,再第一次 apply
require_approval 規則如果沒有一個能解析到同公司、可用範本的 template_id,根本存不進去,所以這是前置條件,不是後續補充。請先在 Review 模組建立範本,該模組不在本手冊範圍內;最低限度的說明見核准流程指南。
然後把範本 id 貼進規則那一行:
{"kind":"rule","table":"orders","ref":"big_orders_need_review","spec":{"type":"require_approval","events":["created","updated"],"template_id":"<template-uuid>","when":[{"column":"total","op":"gt","value":100000}]}}擁有權: 規則屬於文件,範本屬於覆核模組。重新 apply 不會動到範本,但會重新套用規則,所以檔案裡那個 id 才是算數的那個。
搬到別的環境: 那個範本 id 在別的環境不存在。請先在目標環境建立範本,apply 之前只改這一個值。這是整個格式裡最不可攜的一行,所以請把核准規則放在你預期會逐環境修改的小文件裡,不要埋在上百行的大 bundle 中間。
2. apply 之後鑄造 callback token
資料表要先存在才會有權杖,所以這是 apply 之後的步驟:
curl -X POST "$BASE_URL/private/module/custom_tables/chatroom/$CHATROOM_ID/tables/$TABLE_ID/callback-tokens" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "name": "訂單系統回呼", "allowed_ops": "create,update", "valid_until": null }'201 只會帶一次 secret。請立刻放進密鑰管理系統;列表路由不會再給你。
擁有權: 完全屬於 REST。沒有行類型就沒有 drift,重新 apply 也撤不掉它。這是雙面的:export 同樣不會輸出它,所以權杖在你的文件裡是看不見的,必須另外記錄。
見 callbackTokens.mint 與外部回呼,包含公開表單用來長出欄位的 form-schema 路由。
3. apply 之後鑄造帶密鑰的公開讀取權杖
IaC 可以宣告無密鑰的公開權杖,也就是 URL 本身就是憑證。如果你要 bearer 密鑰,請用 REST 鑄造:
curl -X POST "$BASE_URL/private/module/custom_tables/chatroom/tables/$TABLE_ID/public-read-tokens" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "name": "夥伴資料流", "view_id": "<view-uuid>", "secretless": false, "visible_columns": ["品項", "單價"], "rpm": 120 }'注意這個路徑沒有 scope id 那一段。檢視必須已經存在,所以先 apply 文件,再從 IaC state 讀出 view id(做法見第 4 步)。
擁有權: 屬於 REST,而 IaC 永遠不會接管它。用同一個 ref 寫一行 public_read 只會再鑄一個另一個權杖,不會把這個接過去。同一個已公開的檢視,請只選一條路。
見 publicRead.mint 與公開讀取。
4. 用 REST 宣告跨表寫入的觸發器
寫入另一張表的 create_record 或 update_record 動作無法寫進文件。Plan 會把每個 data 鍵都對照觸發器自身的資料表解析,所以另一張表的欄位會以 unknown column ref 失敗——或者,當觸發器的資料表剛好有同名 ref 的欄位時,悄悄對應到那個欄位。只有 materialize_slots 有跨表寫法:$table:<ref>。
請把那個觸發器留在文件之外,apply 之後用 REST 加上。先從 IaC state 讀出目標資料表的 id:
# 這份文件管理的每一個資源的 state 列
curl "$BASE_URL/private/module/custom_tables/chatroom/$CHATROOM_ID/tables/iac/state?system=fulfilment" \
-H "Authorization: Bearer $TOKEN"{ "rows": [ { "system": "fulfilment", "resource_type": "table", "ref": "shipments", "resource_id": "<table-uuid>", "table_id": null } ], "total": 1 }然後用觸發器的 PUT 寫入;它的 body 是該表完整的觸發器清單:把受 IaC 管理的觸發器原樣送回,再加上新的那個,其 table_id 設為上面的 resource_id,data 以目標資料表的欄位為鍵。
擁有權: 這個觸發器屬於 REST。Apply 以 merge-upsert 寫入自己的觸發器行,從不抹除它沒有管理的觸發器,所以重新 apply 不會動到它。但 export 會拒絕帶有這種觸發器的資料表(export: internal key '{key}' has no ref mapping (legacy/unmanaged column)),所以在該觸發器存在期間,請用 table_ids= 只匯出其他資料表。
見 iac.state 與 triggers.set。
5. 先建立非系統 tag,再第一次 apply
header 的 system 會自動管理一個 tag,並把文件裡的資料表都掛上去。這個同步只增不減:離開文件或已刪除的資料表仍保有這個 tag。對大多數 bundle 這就夠了,包含 command:省略 tag_id 時它會綁到系統 tag。如果你需要另一個 tag,請用 REST 建立並掛表:
curl -X POST "$BASE_URL/private/module/custom_tables/chatroom/$CHATROOM_ID/table-tags" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "name": "營運", "description": "日常營運相關資料表", "color": "#2563eb" }'
curl -X POST "$BASE_URL/private/module/custom_tables/chatroom/$CHATROOM_ID/table-tags/$TAG_ID/tables/$TABLE_ID" \
-H "Authorization: Bearer $TOKEN"擁有權: 系統 tag 以外的 tag 都屬於 REST。Command 指名了它就會讓文件綁住那個環境,和範本 id 一樣,所以能省略 tag_id 就省略。
見 tags.create 與 tags.assignTable。
6. 結束 client access
client_access 沒有 state 欄位,所以沒有 absent 那一行可以寫。請改成在文件裡停用它:
{"kind":"client_access","table":"orders","spec":{"passphrase_enabled":false}}有保存 hash 時,export 會把 passphrase 寫回 "<REDACTED>",否則不輸出它。在同一個 scope apply 這個 redacted 值會保留既有 hash。沒有 hash 時,"<REDACTED>" 在你換成真實 passphrase 之前都會失敗:若該行在 live 資料表上啟用 passphrase access,失敗發生在 plan;其他情況——例如在新 scope 中由文件建立資料表——則發生在 apply。
文件帶得走、卻搬不動的值
有幾種值在文件裡有位置,但不可攜。把 export 套用到別處之前,請先檢查它們:
- 觸發器憑證匯出為
"__SECRET__";新的 scope 沒有可保留的值,請補上真實值。 - Rule policy 的
link_targetleaf 會原樣複製其target欄位;那是被連結資料表的內部 key,請透過 REST 編輯。 - 私人 view 可以宣告,但 export 只輸出共享 view,所以它們永遠不會出現在 export 裡。
Column ACL key 現在可攜:settings.column_acl 會解析同表邏輯 ref 與沒有歧義的顯示名稱 alias,export 則輸出 symbolic ref。原始 native key 仍只指名自己的 live 欄位。Policy 驗證與型別化參照錯誤見 table line。
順序整理
面對一個全新的環境:
- 為每一條
require_approval規則建立覆核範本,以及任何非系統 tag,然後把這些 id 換進文件,並把 export 中每個 secret 佔位符換成真實值:client passphrase 與 command callback 的"<REDACTED>",以及 triggerwebhook/api_call憑證的"__SECRET__"。新的 scope 沒有可保留的儲存值,所以它們會被拒絕——在 plan(trigger 的代碼是trigger_secret_keep_refused),或者對文件所建立資料表上的 client passphrase 而言,在 apply。 plan、讀差異、apply。- 讀 IaC state 取得你需要的線上 id,並用 REST 加上跨表寫入的觸發器。
- 再
plan一次,應該是乾淨的。第二次 plan 還有變更表示文件裡有東西沒有收斂,怎麼讀那個結果見 plan 與 apply。 - 鑄造 callback token 與需要密鑰的公開讀取權杖,並保存它們的密鑰。 第 1 步與第 3 步是讓文件變成「綁環境」的兩件事。格式裡其他東西都可以原樣搬動,因為授權、洞察系統選擇與身分欄位的儲存格都接受可攜身分代號——只有密鑰例外:export 永遠不會帶出它們,必須由第 1 步補上。