2022 年 11 月,我們宣佈將 Cloudflare API 轉換到 OpenAPI 結構描述。那時,我們有一個大膽的目標,就是讓 OpenAPI 結構描述成為我們的 SDK 生態系統和參考文件的真實來源。在 2024 年的 Developer Week 期間,我們宣佈可從這些 OpenAPI 結構描述自動產生 SDK 程式庫,以支援這一點。今天,我們很高興地宣佈,現在可以自動產生該生態系統的最新部分——Terraform 提供程式和 API 參考文件。
這意味著,一旦我們的產品中新增新的功能或屬性,並由團隊記錄到文件,您將能看到它在我們的 SDK 生態系統應如何使用,並能立即投入使用。不再存在延誤。不再存在對 API 端點的覆蓋遺漏。
您可以在 https://developers.cloudflare.com/api-next/ 找到新的文件網站,並且可以透過安裝 5.0.0-alpha1 來試用 Terraform 提供程式的發佈候選版本。
為什麼選擇 Terraform?
對於不熟悉 Terraform 的人來說,它是一個用於以程式碼形式管理基礎架構的工具,就像管理應用程式程式碼一樣。我們的許多客戶(無論大小)都依賴於 Terraform,以與技術無關的方式協調其基礎架構。從本質上來說,它是一個內建生命週期管理的 HTTP 用戶端,這意味著它利用我們公開記錄的 API,以理解如何在資源的整個生命週期中進行建立、讀取、更新和刪除操作。
保持 Terraform 更新——舊的方式
在過去,Cloudflare 一直手動維護 Terraform 提供程式,但由於提供程式內部需要自己獨特的操作方式,因此維護和支援的責任落在了少數人的肩上。由於在提供程式中進行單個變更需要大量的認知開銷,服務團隊總是難以跟上變更數量。一個團隊要對提供程式進行變更,至少需要 3 個 pull 請求(如果您要新增對 cf-terraforming 的支援,則為 4 個 pull 請求)。

即使完成了 4 個 pull 請求,它也無法保證覆蓋所有可用屬性,這意味著一些小而重要的細節可能會被遺忘並且不會暴露給客戶,從而在其嘗試設定資源時造成挫敗感。
為了解決這個問題,我們的 Terraform 提供程式需要依賴於我們 SDK 生態系統的其他部分已經從中受益的相同 OpenAPI 結構描述。
自動更新 Terraform
Terraform 與我們的 SDK 的區別在於,它管理資源的生命週期。隨之而來的是一系列新的問題,這些問題與已知值以及管理請求和回應負載中的差異相關。我們來比較建立新 DNS 記錄並擷取該記錄的兩種不同方法。
使用我們的 Go SDK:
// Create the new record
record, _ := client.DNS.Records.New(context.TODO(), dns.RecordNewParams{
ZoneID: cloudflare.F("023e105f4ecef8ad9ca31a8372d0c353"),
Record: dns.RecordParam{
Name: cloudflare.String("@"),
Type: cloudflare.String("CNAME"),
Content: cloudflare.String("example.com"),
},
})
// Wasteful fetch, but shows the point
client.DNS.Records.Get(
context.Background(),
record.ID,
dns.RecordGetParams{
ZoneID: cloudflare.String("023e105f4ecef8ad9ca31a8372d0c353"),
},
)
使用 Terraform:
resource "cloudflare_dns_record" "example" {
zone_id = "023e105f4ecef8ad9ca31a8372d0c353"
name = "@"
content = "example.com"
type = "CNAME"
}
從表面上看,Terraform 方法看起來更簡單,事實確實如此。建立新資源和維護變更的複雜性已得到妥善處理。但問題在於,為了讓 Terraform 提供這種抽象和資料保證,在套用時必須知道所有值。這意味著,即使您沒有使用代理值,Terraform 也需要知道這個值需要是什麼,以便將其儲存在狀態檔案中,並在日後管理該屬性。如果在套用時不知道值,則 Terraform 操作員通常會從提供程式處看到以下錯誤。
Error: Provider produced inconsistent result after apply
When applying changes to example_thing.foo, provider "provider[\"registry.terraform.io/example/example\"]"
produced an unexpected new value: .foo: was null, but now cty.StringVal("").
而在使用 SDK 時,如果您不需要某個欄位,只需忽略它即可,永遠無需操心維護已知值。
為我們的 OpenAPI 結構描述解決這個問題並非易事。自引入 Terraform 產生支援以來,我們的結構描述品質提高了一個數量級。現在,我們明確調出所有存在的預設值、基於請求負載的可變回應屬性以及任何伺服器端計算屬性。所有這些意味著與我們 API 互動的所有人都能獲得更好的體驗。
從 terraform-plugin-sdk 跳轉到 terraform-plugin-framework
要構建 Terraform 提供程式並向操作員公開資源或資料來源,您需要兩項主要內容:提供程式伺服器和提供程式。
提供程式伺服器負責公開一個 gRPC 伺服器,Terraform 核心(透過 CLI)在從操作員提供的設定中管理資源或讀取資料來源時,會使用該伺服器來進行通訊。
提供程式負責包裝資源和資料來源、與遠端服務通訊以及管理狀態檔案。為此,您可以依賴 terraform-plugin-sdk(通常稱為 SDKv2)或 terraform-plugin-framework,其中包括 Terraform 提供的所有介面和方法,以便正確管理內部結構。使用哪個外掛程式取決於提供程式的存在時間。SDKv2 存在時間已久,是大多數 Terraform 提供程式使用的版本,但由於年代久遠和復雜性,它必須保留許多未解決的核心問題,以便為依賴它的人提供向後相容性。 terraform-plugin-framework
是新版,雖然缺乏 SDKv2 所具有的豐富功能,但提供了一種更像 Go 的方法來建置提供程式,並解決了 SDKv2 中的許多基礎錯誤。
(若要更深入地比較 SDKv2 和架構,您可以查看我與 Oktopus Deploy 的 John Bristowe 之間的對話。)
Cloudflare Terraform 提供程式的大部分內容都是使用 SDKv2 建構的,但在 2023 年初,我冒險嘗試了多工處理,在我們的提供程式中同時提供兩者。要理解為什麼需要這樣做,我們必須對 SDKv2 有所瞭解。SDKv2 的結構方式並不真正有利於一致且可靠地表示 null 或「未設定」值。您可以使用實驗性的 ResourceData.GetRawConfig 來檢查設定中的值是 set、null 還是 unknown,但實際上並不支援將其寫回為 null。
我們首次發現這個限制是在邊緣規則引擎(規則集)開始引入新服務的時候,這些服務需要支援 API 回應中包含 unset(或 missing)、 true
或 false
狀態的布林值,每個狀態都有自己的原因和目的。雖然這不是 Cloudflare 的常規 API 設計,但它是一種我們本應能夠處理的有效行事方式。但是,如上所述,SDKv2 提供程式不能處理。這是因為,當回應中不存在某個值或未讀入某個值的狀態時,它會獲得一個與 Go 相容的零值作為預設值。表現為寫入狀態為 false 值後,無法再改為 unset 值(反之亦然)。
為了可靠地使用這些布林值的三種狀態,我們擁有的唯一解決方案是遷移到 terraform-plugin-framework
,該架構具有寫回 unset 值的正確實作。
當我們開始在舊的提供程式中使用 terraform-plugin-framework
新增更多功能後,很明顯,這提供了更好的開發人員體驗,因此我們新增了一個限制,以防止未來任何人在繼續使用 SDKv2 時無意中遇到這個問題。
當我們決定自動產生 Terraform 提供程式時,最理想的做法是讓所有資源都基於 terraform-plugin-framework
,並徹底擺脫 SDKv2 中的問題。這確實使遷移複雜化了,因為隨著內部結構的改進,主要元件也發生了變化,例如我們需要熟悉的結構描述和 CRUD 操作。然而,這是一項值得的投資,因為透過這樣做,我們為提供程式奠定了基礎,做好了面向未來的準備,而且減少了因存在缺陷的內部機制而做出的妥協,從而提供更出色的 Terraform 體驗。
以反覆方式查找錯誤
程式碼產生管道常見的一個問題是,除非您有現成的工具來實作您的新產品,否則很難判斷它是否有效或是否值得使用。當然,您也可以產生測試來試驗新產品,但如果管道中存在錯誤,您很可能並不會看到這個錯誤,因為您產生的測試會認為這個錯誤是預期的行為。
我們已有的一個重要回饋迴圈就是現有的驗收測試套件。現有提供程式中的所有資源都進行了回歸測試和功能測試。最棒的是,由於測試套件正在建立和管理真實資源,我們只需查看 HTTP 流量,看看 API 呼叫是否被遠端端點接受,就能輕鬆判斷結果是否是有效的實作。如要移植測試套件,只需複製所有現有的測試,並檢查任何類型斷言的差異(例如清單到單個嵌套清單),然後啟動測試執行以確定資源是否正常運作。
雖然集中式結構描述管道顯著提高了效率,結構描述修復幾乎瞬間即可傳播到整個生態系統,但它無法幫助我們解決最大的障礙,即揭露掩蓋其他錯誤的錯誤。這非常耗時,因為修復 Terraform 中的問題時,有三個地方可能遇到錯誤:
在進行任何 API 呼叫之前,Terraform 都會實施邏輯結構描述驗證,當遇到驗證錯誤時,它將立即停止。
如果任何 API 呼叫失敗,它將在 CRUD 操作處停下,並傳回診斷資訊,然後立即停止。
執行 CRUD 操作後,Terraform 會進行檢查以確保知道所有值。
這意味著,即使我們在第 1 步中發現錯誤,然後修復了該錯誤,也並不能保證或肯定後面不會再有兩個錯誤在等著我們。更有甚者,即使我們在第 2 步中發現了錯誤並進行了修復,也並不能確保我們在下一輪測試中不會在第 1 步發現錯誤。
對此沒有靈丹妙藥,我們的解決方法是注意結構描述行為中的問題模式,並在它進入程式碼產生管道之前,在 OpenAPI 結構描述中套用 CI lint 規則。採用此方法逐步減少第 1 步和第 2 步的錯誤數量,直到基本上只要處理第 3 步中的錯誤類型。
一種更利用重複使用的模型和結構體轉換方法
在 Terraform 提供程式 CRUD 操作中,經常看到如下樣板:
var plan ThingModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
out, err := r.client.UpdateThingModel(ctx, client.ThingModelRequest{
AttrA: plan.AttrA.ValueString(),
AttrB: plan.AttrB.ValueString(),
AttrC: plan.AttrC.ValueString(),
})
if err != nil {
resp.Diagnostics.AddError(
"Error updating project Thing",
"Could not update Thing, unexpected error: "+err.Error(),
)
return
}
result := convertResponseToThingModel(out)
tflog.Info(ctx, "created thing", map[string]interface{}{
"attr_a": result.AttrA.ValueString(),
"attr_b": result.AttrB.ValueString(),
"attr_c": result.AttrC.ValueString(),
})
diags = resp.State.Set(ctx, result)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
總體而言:
我們使用
req.Plan.Get()
擷取建議的更新(稱為計畫)使用新值執行更新 API 呼叫
將 Go 類型的資料轉換為 Terraform 模型 (
convertResponseToThingModel
)透過呼叫
resp.State.Set()
設定狀態
起初,這似乎並沒有太大的問題。然而,當第 3 步將 Go 類型轉換為 Terraform 模型時,很快就會變得繁瑣、容易出錯且複雜,因為所有資源都需要這樣做才能在類型和相關的 Terraform 模型之間切換。
為了避免產生不必要的複雜程式碼,我們的提供程式中的一項改進是,所有 CRUD 方法都使用統一的 apijson.Marshal
、 apijson.Unmarshal
和 apijson.UnmarshalComputed
方法,這些方法透過基於結構體標籤集中轉換和處理邏輯來解決這個問題。
var data *ThingModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
dataBytes, err := apijson.Marshal(data)
if err != nil {
resp.Diagnostics.AddError("failed to serialize http request", err.Error())
return
}
res := new(http.Response)
env := ThingResultEnvelope{*data}
_, err = r.client.Thing.Update(
// ...
)
if err != nil {
resp.Diagnostics.AddError("failed to make http request", err.Error())
return
}
bytes, _ := io.ReadAll(res.Body)
err = apijson.UnmarshalComputed(bytes, &env)
if err != nil {
resp.Diagnostics.AddError("failed to deserialize http request", err.Error())
return
}
data = &env.Result
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
我們不需要產生數百個類型到模型轉換方法的執行個體,而是為 Terraform 模型新增正確的標記,以一致的方式處理資料的封送處理和解封送處理。這只是對程式碼的一個小變更,但從長遠來看,卻使產生的程式碼更具可重複使用性和可讀性。這種方法的另一個好處是,它非常適合修復錯誤,因為一旦您識別出特定類型欄位的錯誤,只要在統一介面中修復該錯誤,就能在您尚未發現的其他地方修復該錯誤。
請等一下,還有更多(文件)!
為了提升我們的 OpenAPI 結構描述使用,我們正在加強 SDK 與新 API 文件網站的整合。它使用了我們過去兩年投資並解決了一些常見使用問題的相同管道。
SDK 感知
如果您使用過我們的 API 文件網站,就會知道我們提供使用 curl 等命令列工具與 API 互動的範例。這是一個很好的起點,但如果您使用的是其中一個 SDK 程式庫,您需要進行一些推斷,設法將其轉換為您想要使用的方法或類型定義。現在我們使用相同的管道來產生 SDK 和文件,我們將會提供您可能使用的所有庫(而不限於 curl)中的範例,從而解決這個問題。

使用 cURL 擷取所有區域的範例。

使用 Typescript 程式庫擷取所有區域的範例。

使用 Python 程式庫擷取所有區域的範例。

使用 Go 程式庫擷取所有區域的範例。
透過這一改進,我們還可以記住語言選擇,因此,如果您選擇使用我們的 Typescript 程式庫檢視文件並繼續瀏覽,我們將一直向您展示使用 Typescript 的範例,直到更換到其他語言。
最重要的是,當我們向現有端點引入新屬性或新增 SDK 語言時,此文件網站會自動與管道保持同步。無需再付出巨大努力來使其全部保持最新。
更快速、更高效的轉譯
我們一直難以解決的一個問題是 API 端點的龐大數量以及如何表示它們。截至本文發佈時,我們有 1,330 個端點,對於每個端點,我們有一個請求負載,一個回應負載,以及多個與之關聯的類型。在轉譯這麼多資訊時,我們過去使用的解決方案不得不做出一些權衡,以便讓部分表示能夠正常運作。
API 文件網站的下一次反覆將透過幾種方式解決這個問題:
它作為一個現代 React 應用程式實作,將互動式用戶端體驗與靜態預先轉譯內容結合起來,從而實現快速初始載入和快速導覽。(是的,即使不啟用 JavaScript,它也能正常運作!)。
它會在您導覽時逐步擷取基礎資料。
透過解決這個基本問題,我們還推出了對文件網站和 SDK 生態系統的其他改進計畫,以改善使用者體驗,而無需像過去那樣做出權衡。
權限
在文件網站中,使用者最希望重新實作的功能之一就是 API 端點的最低所需權限。文件網站的早期版本中曾經提供這個功能。然而,大多數使用它的人並不知道,這些值是手動維護的,且經常不正確,導致使用者提交支援工單並感到沮喪。
在 Cloudflare 的身分識別和存取管理系統中,「我需要什麼才能存取這個端點」並不是一個簡單的問題。這樣做的原因是,在請求傳送到控制平面的正常流程中,我們需要兩個不同的系統來回答同一個問題的不同部分,然後將這些資訊結合起來給出完整的答案。由於我們最初無法在 OpenAPI 管道中自動執行此操作,因此我們選擇將其排除在外,而不是提供一個錯誤且無法驗證的值。
直到今天,我們很高興地宣佈,端點權限又回來了!我們建立了一些新的工具,提取這個問題的回答,讓我們可以將其整合到程式碼產生管道中,並讓所有端點自動獲取這些資訊。與程式碼產生平台的其他部分非常類似,它專注於讓服務團隊擁有並維護高品質的結構描述,這些結構描述可以重複使用,並引入增值功能,且無需他們進行任何工作。

無需等待更新
隨著這些公告發佈,我們將不再需要等待更新來進入 SDK 生態系統。透過這些新的改進,我們能夠在團隊記錄新屬性和端點時立即簡化它們的功能。您還在猶豫什麼?立即前來探索 Terraform 提供程式和 API 文件網站吧。