pulumi - 基礎設施代碼化
本文不是一篇 pulumi 入門文檔!文章主要內容是我對 pulumi 的一些思考,以及使用 pulumi 遇到的各種問題+解決方法。
pulumi 和 terraform 一樣,都是自動化管理基礎設施的工具,但是它解決了 terraform 配置的一個痛點:配置語法太過簡單,導致配置繁瑣,而且還要額外學習一門 DSL - hcl。
terraform 雖然應用廣泛,但是它默認使用的 HCL 語言太簡單,表現力不夠強。
這導致在更復雜的場景下,我們無法更自動化地進行基礎設施配置,而需要更復雜的步驟:
- 藉助 Python 等其他語言先生成出 HCL 配置
- 通過
terraform
命令行進行 plan 與 apply - 通過 Python 代碼解析
terraform.tfstat
,獲取 apply 結果,再進行進一步操作。
這顯然是一個很麻煩的過程。其中最主要的原因,是 terraform 只做到了「基礎設施即配置」,而「配置」過於簡單。
這種情況下,就需要用到真正的「基礎設施即代碼」工具 - Pulumi 了。它的優勢如下:
- pulumi 是目前最流行的 真-IaaS 工具(另一個是剛出爐沒多久的 terraform-cdk),對各語言的支持最爲成熟。
- 兼容 terraform 的所有 provider,只是需要自行使用 pulumi-tf-provider-boilerplate 重新打包,有些麻煩。
- pulumi 官方的 provider 幾乎全都是封裝的 terraform provider,包括 aws/azure/alicloud,目前只發現 kubernetes 是原生的(獨苗啊)。
- 狀態管理和 secrets 管理有如下幾種選擇:
- 使用 app.pulumi.com(默認):免費版提供 stack 歷史管理,可以看到所有的歷史記錄。另外還提供一個資源關係的可視化面板。總之很方便,但是多人合作就需要收費。
- 本地文件存儲:
pulumi login file:///app/data
- 雲端對象存儲,目前貌似只支持 aws-s3/gcp/azure 三種。
- gitlab 13 支持 Terraform HTTP State 協議,等這個 pr 合併,pulumi 也能以 gitlab 爲 backend 了。
- 使用 pulumi 企業版(自建服務):比 app.pulumi.com 提供更多的特性,但是顯然是收費的。。
上述工具支持通過 Python/TypeScript 等語言來描述配置。好處有:
- 批量創建資源,動態生成資源參數。
- 比如批量創建一批名稱類似的 ECS 服務器/VPC交換機。如果使用 terraform,你需要編寫 module 來實現配置的複用,然後使用 hcl 的特殊語法來動態拼接出資源名稱,因爲語法限制,這種 HCL 能實現的功能也很有限。
- 而使用 pulumi,Python/TypeScript 這類通用的編程語言,能滿足你的一切需求,而且作爲一個開發人員/DevOps,你應該對它們相當熟悉。
- 更方便測試:可以使用各編程語言中流行的測試框架來測試 pulumi 配置!
- 使用代碼編寫 Kubernetes 配置,no-yaml
- yaml 也存在和 HCL 一樣的問題,配置太死板,導致我們現在需要通過 helm/kustomize + python 來生成 yaml ...
使用建議
- 建議查看對應的 terraform provider 文檔:pulumi 的 provider 基本都是封裝的 terraform 版本,而且文檔是自動生成的,比(簡)較(直)難(一)看(坨)懂(shi),examples 也少。
- stack: pulumi 官方提供了兩種 stack 用法:「單體」和「微-stack」
- 單體: one stack hold them all,通過 stack 參數來控制步驟。stack 用來區分環境 dev/pro 等。
- 微-stack: 每一個 stack 是一個步驟,所有 stack 組成一個完整的項目。
- 實際使用中,我發現「微-stack」模式需要使用到 pulumi 的 inter-stack dependencies,報一堆的錯,而且不夠靈活。因此目前更推薦「單體」模式。
我們最近使用 pulumi 完全重寫了以前用 terraform 編寫的雲上配置,簡化了很多繁瑣的配置,也降低了我們 Python 運維代碼和 terraform 之間的交互難度。
另外我們還充分利用上了 Python 的類型檢查和語法檢查,很多錯誤 IDE 都能直接給出提示,強化了配置的一致性和可維護性。
體驗上,terraform 只是配置編寫方式,以及狀態管理有些不同。實際上都是通過同樣的 provider 管理雲上資源。
目前我們使用 pulumi/terraform,實現了雲上環境(資源組、VPC專有網絡、k8s集羣、數據庫、賬號權限系統、負載均衡等等)的一鍵搭建與銷燬。
不過由於阿里雲 provider 暫時還:
- 不支持管理 ASM 服務網格、DTS 數據傳輸等資源
- OSS 等產品的部分參數也暫時不支持配置(比如 OSS 不支持配置圖片樣式、ElasticSearch 暫時不支持自動創建 7.x 版本)
- 不支持創建 ElasticSearch 7.x
這些問題,導致我們仍然有部分配置需要手動處理,另外一些耗時長的資源,需要單獨去創建。
因此還不能實現完全的「一鍵」。
常見問題
1. pulumi 的 Output
常見問題
- pulumi 通過資源之間的屬性引用(
Output[str]
)來確定依賴關係,如果你通過自定義的屬性(str
)解耦了資源依賴,會導致資源創建順序錯誤而創建失敗。 Output[str]
是一個異步屬性,類似 Future,不能被用在 pulumi 參數之外的地方!Output[str]
提供兩種方法能直接對Output[str]
進行一些操作:Output.concat("http://", domain, "/", path)
: 此方法將 str 與Output[str]
拼接起來,返回一個新的Output[str]
對象,可用做 pulumi 屬性。domain.apply(lambda it: print(it))
:Output[str]
的apply
方法接收一個函數。在異步獲取到數據後,pulumi 會調用這個函數,把具體的數據作爲參數傳入。- 另外
apply
也會將傳入函數的返回值包裝成Output
類型返回出來。 - 可用於:在獲取到數據後,將數據打印出來/發送到郵箱/調用某個 API 上傳數據等等。
- 另外
Output.all(output1, output2, ...).apply(lambda it: print(it))
可用於將多個output
值,拼接成一個Output
類型,其內部的 raw 值爲一個 tuple 對象(str1, str2, ...)
.- 官方舉例:
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
被中斷,或者對資源做了手動修改,會發生什麼?
- 強行中斷
pulumi up
,會導致資源進入pending
狀態,必須手動修復。- 修復方法:
pulumi stack export
,刪除 pending 資源,再pulumi stack import
- 修復方法:
- 手動刪除了雲上資源,或者修改了一些對資源管理無影響的參數,對
pulumi
沒有影響,它能正確檢測到這種情況。- 可以通過
pulumi refresh
手動從雲上拉取最新的資源狀態。
- 可以通過
- 手動更改了資源之間的關係(比如綁定 EIP 之類的),很可能導致 pulumi 無法正確管理資源之間的依賴。
5. pulumi-kubernetes?
pulumi-kubernetes 是一條龍服務:
- 在 yaml 配置生成這一步,它能結合/替代掉 helm/kustomize,或者你高度自定義的 Python 腳本。
- 在 yaml 部署這一步,它能替代掉 argo-cd 這類 gitops 工具。
- 強大的狀態管理,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. 資源狀態被破壞時,修復起來非常麻煩
在很多情況下,都可能發生資源狀態被破壞的問題:
- 在創建資源 A,因爲參數是已知的,你直接使用了常量而不是 output。這會導致 pulumi 無法識別到依賴關係!從而創建失敗,或者刪除時資源狀態被破壞!
- 有一個 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 自動輪轉。