1. 程式人生 > 實用技巧 >pulumi - 基礎設施程式碼化

pulumi - 基礎設施程式碼化

pulumi - 基礎設施程式碼化

本文不是一篇 pulumi 入門文件!文章主要內容是我對 pulumi 的一些思考,以及使用 pulumi 遇到的各種問題+解決方法。

pulumi 和 terraform 一樣,都是自動化管理基礎設施的工具,但是它解決了 terraform 配置的一個痛點:配置語法太過簡單,導致配置繁瑣,而且還要額外學習一門 DSL - hcl。

terraform 雖然應用廣泛,但是它預設使用的 HCL 語言太簡單,表現力不夠強。
這導致在更復雜的場景下,我們無法更自動化地進行基礎設施配置,而需要更復雜的步驟:

  1. 藉助 Python 等其他語言先生成出 HCL 配置
  2. 通過 terraform 命令列進行 plan 與 apply
  3. 通過 Python 程式碼解析 terraform.tfstat,獲取 apply 結果,再進行進一步操作。

這顯然是一個很麻煩的過程。其中最主要的原因,是 terraform 只做到了「基礎設施即配置」,而「配置」過於簡單。

這種情況下,就需要用到真正的「基礎設施即程式碼」工具 - Pulumi 了。它的優勢如下:

  1. pulumi 是目前最流行的 真-IaaS 工具(另一個是剛出爐沒多久的 terraform-cdk),對各語言的支援最為成熟。
  2. 相容 terraform 的所有 provider,只是需要自行使用 pulumi-tf-provider-boilerplate
    重新打包,有些麻煩。
    1. pulumi 官方的 provider 幾乎全都是封裝的 terraform provider,包括 aws/azure/alicloud,目前只發現 kubernetes 是原生的(獨苗啊)。
  3. 狀態管理和 secrets 管理有如下幾種選擇:
    1. 使用 app.pulumi.com(預設):免費版提供 stack 歷史管理,可以看到所有的歷史記錄。另外還提供一個資源關係的視覺化面板。總之很方便,但是多人合作就需要收費。
    2. 本地檔案儲存:pulumi login file:///app/data
    3. 雲端物件儲存,目前貌似只支援 aws-s3/gcp/azure 三種。
    4. gitlab 13 支援 Terraform HTTP State 協議
      ,等這個 pr 合併,pulumi 也能以 gitlab 為 backend 了。
    5. 使用 pulumi 企業版(自建服務):比 app.pulumi.com 提供更多的特性,但是顯然是收費的。。

上述工具支援通過 Python/TypeScript 等語言來描述配置。好處有:

  1. 批量建立資源,動態生成資源引數。
    1. 比如批量建立一批名稱類似的 ECS 伺服器/VPC交換機。如果使用 terraform,你需要編寫 module 來實現配置的複用,然後使用 hcl 的特殊語法來動態拼接出資源名稱,因為語法限制,這種 HCL 能實現的功能也很有限。
    2. 而使用 pulumi,Python/TypeScript 這類通用的程式語言,能滿足你的一切需求,而且作為一個開發人員/DevOps,你應該對它們相當熟悉。
  2. 更方便測試:可以使用各程式語言中流行的測試框架來測試 pulumi 配置!
  3. 使用程式碼編寫 Kubernetes 配置,no-yaml
    1. yaml 也存在和 HCL 一樣的問題,配置太死板,導致我們現在需要通過 helm/kustomize + python 來生成 yaml ...

使用建議

  1. 建議檢視對應的 terraform provider 文件:pulumi 的 provider 基本都是封裝的 terraform 版本,而且文件是自動生成的,比(簡)較(直)難(一)看(坨)懂(shi),examples 也少。
  2. stack: pulumi 官方提供了兩種 stack 用法:「單體」和「微-stack」
    1. 單體: one stack hold them all,通過 stack 引數來控制步驟。stack 用來區分環境 dev/pro 等。
    2. 微-stack: 每一個 stack 是一個步驟,所有 stack 組成一個完整的專案。
    3. 實際使用中,我發現「微-stack」模式需要使用到 pulumi 的 inter-stack dependencies,報一堆的錯,而且不夠靈活。因此目前更推薦「單體」模式。

我們最近使用 pulumi 完全重寫了以前用 terraform 編寫的雲上配置,簡化了很多繁瑣的配置,也降低了我們 Python 運維程式碼和 terraform 之間的互動難度。
另外我們還充分利用上了 Python 的型別檢查和語法檢查,很多錯誤 IDE 都能直接給出提示,強化了配置的一致性和可維護性。

體驗上,terraform 只是配置編寫方式,以及狀態管理有些不同。實際上都是通過同樣的 provider 管理雲上資源。
目前我們使用 pulumi/terraform,實現了雲上環境(資源組、VPC專有網路、k8s叢集、資料庫、賬號許可權系統、負載均衡等等)的一鍵搭建與銷燬。
不過由於阿里雲 provider 暫時還:

  1. 不支援管理 ASM 服務網格、DTS 資料傳輸等資源
  2. OSS 等產品的部分引數也暫時不支援配置(比如 OSS 不支援配置圖片樣式、ElasticSearch 暫時不支援自動建立 7.x 版本)
  3. 不支援建立 ElasticSearch 7.x

這些問題,導致我們仍然有部分配置需要手動處理,另外一些耗時長的資源,需要單獨去建立。
因此還不能實現完全的「一鍵」。

常見問題

1. pulumi 的 Output 常見問題

  1. pulumi 通過資源之間的屬性引用(Output[str])來確定依賴關係,如果你通過自定義的屬性(str)解耦了資源依賴,會導致資源建立順序錯誤而建立失敗。
  2. Output[str] 是一個非同步屬性,類似 Future,不能被用在 pulumi 引數之外的地方!
  3. Output[str] 提供兩種方法能直接對 Output[str] 進行一些操作:
    1. Output.concat("http://", domain, "/", path): 此方法將 str 與 Output[str] 拼接起來,返回一個新的 Output[str] 物件,可用做 pulumi 屬性。
    2. domain.apply(lambda it: print(it)): Output[str]apply 方法接收一個函式。在非同步獲取到資料後,pulumi 會呼叫這個函式,把具體的資料作為引數傳入。
      • 另外 apply 也會將傳入函式的返回值包裝成 Output 型別返回出來。
      • 可用於:在獲取到資料後,將資料打印出來/傳送到郵箱/呼叫某個 API 上傳資料等等。
    3. Output.all(output1, output2, ...).apply(lambda it: print(it)) 可用於將多個 output 值,拼接成一個 Output 型別,其內部的 raw 值為一個 tuple 物件 (str1, str2, ...).
      1. 官方舉例:connection_string = Output.all(sql_server.name, database.name).apply(lambda args: f"Server=tcp:{args[0]}.database.windows.net;initial catalog={args[1]}...")

2. 如果使用多個雲賬號/多個k8s叢集?

預設情況下 pulumi 使用預設的 provider,但是 pulumi 所有的資源都有一個額外的 opts 引數,可用於設定其他 provider。

示例:

from pulumi import get_stack, ResourceOptions, StackReference
from pulumi_alicloud import Provider, oss

# 自定義 provider,key/secret 通過引數設定,而不是從預設的環境變數讀取。
# 可以自定義很多個 providers
provider = pulumi_alicloud.Provider(
   "custom-alicloud-provider",
   region="cn-hangzhou",
   access_key="xxx",
   secret_key="jjj",
)

# 通過 opts,讓 pulumi 使用自定義的 provider(替換掉預設的)
bucket = oss.Bucket(..., opts=ResourceOptions(provider=provider))

3. inter-stack 屬性傳遞

這東西還沒搞透,待研究。

多個 stack 之間要互相傳遞引數,需要通過 pulumi.export 匯出屬性,通過 stack.require_xxx 獲取屬性。

從另一個 stack 讀取屬性的示例:

from pulumi import StackReference

cfg = pulumi.Config()
stack_name = pulumi.get_stack()  # stack 名稱
project = pulumi.get_project()
infra = StackReference(f"ryan4yin/{project}/{stack_name}")

# 這個屬性在上一個 stack 中被 export 出來
vpc_id = infra.require("resources.vpc.id")

4. pulumi up 被中斷,或者對資源做了手動修改,會發生什麼?

  1. 強行中斷 pulumi up,會導致資源進入 pending 狀態,必須手動修復。
    1. 修復方法:pulumi stack export,刪除 pending 資源,再 pulumi stack import
  2. 手動刪除了雲上資源,或者修改了一些對資源管理無影響的引數,對 pulumi 沒有影響,它能正確檢測到這種情況。
    1. 可以通過 pulumi refresh 手動從雲上拉取最新的資源狀態。
  3. 手動更改了資源之間的關係(比如繫結 EIP 之類的),很可能導致 pulumi 無法正確管理資源之間的依賴。

5. pulumi-kubernetes?

pulumi-kubernetes 是一條龍服務:

  1. 在 yaml 配置生成這一步,它能結合/替代掉 helm/kustomize,或者你高度自定義的 Python 指令碼。
  2. 在 yaml 部署這一步,它能替代掉 argo-cd 這類 gitops 工具。
  3. 強大的狀態管理,argo-cd 也有狀態管理,可以對比看看。

也可以僅通過 kubernetes_pulumi 生成 yaml,再通過 argo-cd 部署,這樣 pulumi_kubernetes 就僅用來簡化 yaml 的編寫,仍然通過 gitops 工具/kubectl 來部署。

使用 pulumi-kubernetes 寫配置,要警惕邏輯和資料的混合程度。
因為 kubernetes 的配置複雜度比較高,如果動態配置比較多,很容易就會寫出難以維護的 python 程式碼來。

渲染 yaml 的示例:

from pulumi import get_stack, ResourceOptions, StackReference
from pulumi_kubernetes import Provider
from pulumi_kubernetes.apps.v1 import Deployment, DeploymentSpecArgs
from pulumi_kubernetes.core.v1 import (
	ContainerArgs,
	ContainerPortArgs,
	EnvVarArgs,
	PodSpecArgs,
	PodTemplateSpecArgs,
	ResourceRequirementsArgs,
	Service,
	ServicePortArgs,
	ServiceSpecArgs,
)
from pulumi_kubernetes.meta.v1 import LabelSelectorArgs, ObjectMetaArgs

provider = Provider(
   "render-yaml",
   render_yaml_to_directory="rendered",
)

deployment = Deployment(
	"redis",
	spec=DeploymentSpecArgs(...),
   opts=ResourceOptions(provider=provider),
)

如示例所示,pulumi-kubernetes 的配置是完全結構化的,比 yaml/helm/kustomize 要靈活非常多。

6. 阿里雲資源 replace 報錯?

部分只能建立刪除,不允許修改的資源,做變更時會報錯:「Resources aleardy exists」,
這類資源,通常都有一個「force」引數,指示是否強制修改——即先刪除再重建。

7. 有些資源屬性無法使用 pulumi 配置?

這得看各雲服務提供商的支援情況。

比如阿里雲很多資源的屬性,pulumi 都無法完全配置,因為 alicloud provider 的功能還不夠全面。

目前我們生產環境,大概 90%+ 的東西,都可以使用 pulumi 實現自動化配置。
而其他 OSS 的高階引數、新出的 ASM 服務網格、kubernetes 的授權管理、ElasticSearch7 等資源,還是需要手動配置。

這個沒辦法,只能等阿里雲提供支援。

8. CI/CD 中如何使 pulumi 將狀態儲存到檔案?

CI/CD 中我們可能會希望 pulumi 將狀態儲存到本地,避免連線 pulumi 中心伺服器。
這一方面能加快速度,另一方面一些臨時狀態我們可能根本不想儲存,可以直接丟棄。

方法:

# 指定狀態檔案路徑
pulumi login file://<file-path>
# 儲存到預設位置: ~/.pulumi/credentials.json
pulumi login --local

# 儲存到遠端 S3 儲存(minio/ceph 或者各類雲物件儲存服務,都相容 aws 的 s3 協議)
pulumi login s3://<bucket-path>

登入完成後,再進行 pulumi up 操作,資料就會直接儲存到你設定的路徑下。

缺點

1. 報錯資訊不直觀

pulumi 和 terraform 都有一個缺點,就是封裝層次太高了。

封裝的層次很高,優點是方便了我們使用,可以使用很統一很簡潔的宣告式語法編寫配置。
而缺點,則是出了 bug,報錯資訊往往不夠直觀,導致問題不好排查。

2. 資源狀態被破壞時,修復起來非常麻煩

在很多情況下,都可能發生資源狀態被破壞的問題:

  1. 在建立資源 A,因為引數是已知的,你直接使用了常量而不是 output。這會導致 pulumi 無法識別到依賴關係!從而建立失敗,或者刪除時資源狀態被破壞!
  2. 有一個 pulumi stack 一次在三臺物理機上建立資源。你白天建立資源晚上刪除資源,但是某一臺物理機晚上會關機。這將導致 pulumi 無法查詢到這臺物理機上的資源狀態,這個 pulumi stack 在晚上就無法使用,它會一直報錯!

常用 Provider

我建立的 provider:

我正打算建立的 provider: