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 要靈活非常多。

總之它非常靈活,既可以和 helm/kustomize 結合使用,替代掉 argocd/kubectl。
也可以和 argocd/kubectl 使用,替代掉 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

  • pulumi-alicloud: 管理阿里雲資源
  • pulumi-vault: 我這邊用它來快速初始化 vault,創建與管理 vault 的所有配置。

我創建的 provider:

  • ryan4yin/pulumi-proxmox: 目前只用來自動創建 PVE 虛擬機
    • 可以考慮結合 kubespray/kubeadm 快速創建 k8s 集羣

我正打算創建的 provider:

  • ryan4yin/pulumi-libvirt: 快速創建 kvm 虛擬機
    • 可以考慮結合 kubespray/kubeadm 快速創建 k8s 集羣
  • ryan4yin/pulumi-acme
    • 快速創建與 renew acme 證書,然後結合 pulumi-vault 之類的工具實現 k8s 集羣的 tls 自動輪轉。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章