今天來介紹如何撰寫 Pulumi 的整合測試程式。整合測試的目標就是要讓 Pulumi 真正的建立雲端資源,並驗證所建立的雲端資源是否正確。
因此理論上是可以透過任意程式語言,使用 CLI 下達 pulumi up
、pulumi stack
等指令進行整合測試。或者也能透過 Pulumi 新推出的 Automation API 撰寫自動執行 Pulumi 的功能,進行整合測試。
不過今天要介紹的是 Pulumi 內建的整合測試工具 Pulumi golang SDK。我想,可能是因為 Pulumi 是使用 Golang 撰寫,因此本身就有測試的需求,所以才專門有 Go 語言的整合測試 SDK 可以使用。其他程式語言就只能透過 Automation API 來做。
那麼 Pulumi golang SDK 的整合測試功能與 Automation API 又差在哪裡呢?
Golang integration SDK 的套件位於 "github.com/pulumi/pulumi/pkg/v3/testing/integration",由套件名就可以知道他是專門做整合測試用的。
主要使用方式就是定義要執行測試的待測專案位置、要測的 config、secret 等資訊、以及驗證部署結果的測試程式碼。
今天一樣是使用之前的 VPC component resource 專案做為待測程式。以下為使用 Golang 撰寫測試的 template。
func TestVpc(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
t.FailNow()
}
test := integration.ProgramTestOptions{
Dir: path.Join(cwd, "../aws-vpc-ts"),
Quick: true,
SkipRefresh: true,
Config: map[string]string{
},
ExtraRuntimeValidation: func(t *testing.T, stack integration.RuntimeValidationStackInfo) {
},
}
integration.ProgramTest(t, &test)
}
在這個 template 中,使用 integration.ProgramTestOptions struct 定義了要測試的所有參數。最後使用 integration.ProgramTest 運行測試。
在 integration.ProgramTestOptions struct 中,填入的設定說明如下:
Dir
: 指定待測程式專案的位置,以下是專案的大略架構,測試程式碼在 vpc_test.go
中,可以藉由下圖的相對關係看出我的待測專案位於 ../aws-vpc-ts
中.
├── aws-vpc-ts
│ ├── vpc.ts
│ ├── index.ts
│ └── package.json
│
└── integration-test
├── go.mod
└── vpc_test.go
Quick
: 根據說明,Quick 等同於 SkipPreview + SkipExportImport + SkipEmptyPreviewUpdate,也就是跳過
pulumi up
的 preview 之類的工作SkipRefresh
: 跳過 refresh 階段,平常在執行 pulumi up
的時候也會跳過 refresh,如果要 refresh 還需要特別使用 pulumi up --refresh
Config
: 在整合測試中,會自動建立一個運行測試的 stack,這個 Config 就是該 Stack 的 config 設定內容ExtraRuntimeValidation
: 最重要的功能,這個參數需要傳一個 callback function。整合測試建立完資源後,就會呼叫這個 function 執行驗證。此外如果有用到 Secret 的話,可以透過 Secrets
參數傳入,也設定要可以使用的 SecretsProvider
。
更詳細的資訊就要參考 integration.ProgramTestOptions
的文件了。
介紹完參數的作用後,就可以開始撰寫整合測試程式了。首先先來設定 Config 的內容,這邊 Config 的資料一定要是設定字串。
Config: map[string]string{
"aws:region": "ap-east-1",
"vpcCidrBlocks": "10.18.0.0/16",
"numOfAzs": "1",
"numOfPublic": "2",
"numOfPrivate": "1",
},
接著就來撰寫 ExtraRuntimeValidation
內,驗證的程式碼。
ExtraRuntimeValidation 中,第二個參數 stack
就是建立好的 stack
。可以透過 stack.Deployment
得到所有建立的資源列表。
只要可以把列表中每個我們想要驗證的資源都找出來進行驗證即可
以下是驗證資源的標準方式,使用迴圈列出所有資源內容,並對我們關注的資源做處理:
// 列出所有資源
for _, res := range stack.Deployment.Resources {
// 如果資源是 VPC
if res.Type == "aws:ec2/vpc:Vpc" {
// 取得 cidrBlock 的內容
cidrBlock := res.Outputs["cidrBlock"].(string)
// 驗證是否為傳入的 Config 的值
assert.Equal(t, cidrBlock, "10.18.0.0/16")
}
}
首先要怎麼知道是 Public Subnet 還是 Private Subnet 呢? AWS 的 Subnet 上並沒有太多的資訊告訴我們這點。
這邊我想到的方式是透過 Subnet 關聯到的 RouteTable 判斷是 Public 還是 Private。如果是 Public Subnet,應該會使用 defaultRouteTable。如果是 Private 則會有自己的 RouteTable。因為有多少個 Subnet 就會建立多少個 RouteTableAssociation,因此這邊就使用 RouteTableAssociation 判斷 Subnet 的數量。
當然也可以透過在 component resource 上定義 Output 標註是 private 或是 public subnet。這樣就比較省事,只要判斷 component resource 的資訊就可以。
// 列出所有資源
var defaultRouteTableId string
routeTableAssociations := make([]apitype.ResourceV3, 0)
for _, res := range stack.Deployment.Resources {
// 如果資源是 VPC
if res.Type == "aws:ec2/vpc:Vpc" {
// 記錄下 default Route Table 的 ID
defaultRouteTableId = res.Outputs["defaultRouteTableId"].(string)
}
// 如果資源是 RouteTableAssociation
if res.Type == "aws:ec2/routeTableAssociation:RouteTableAssociation" {
// 將所有 RouteTableAssociation 蒐集到 array 中
routeTableAssociations = append(routeTableAssociations, res)
}
}
// 透過 routeTableAssociations 統計 subnet 的數量
publicSubnetIds := mapset.NewSet[string]()
privateSubnetIds := mapset.NewSet[string]()
for _, rta := range routeTableAssociations {
if rta.Outputs["routeTableId"].(string) == defaultRouteTableId {
// 如果 routeTableAssociations 上的是 default route table,則視為 public subnet
publicSubnetIds.Add(rt.Outputs["subnetId"].(string))
} else {
// 否則就是 private subnet
privateSubnetIds.Add(rt.Outputs["subnetId"].(string))
}
}
// 是否有 2 個 public subnet
assert.Equal(t, publicSubnetIds.Cardinality(), 2)
// 是否有 1 個 private subnet
assert.Equal(t, privateSubnetIds.Cardinality(), 1)
這個範例中使用了 golang-set 來收集 subnetId 的資料,set 資料結構的好處就是同樣的資料只會出現一次,不用煩惱重複加一樣的 subnetId 導致判斷出錯。
最後一個範例,就是驗證所有 Subnet 是否只用到一個 AZ。因為在 config 中 numOfAz 設定為 1,因此所有 Subnet 都必須建立在同一個 AZ 中。
驗證方式一樣是將所有 Subnet 的 availabilityZoneId 放進 Set 中。最後檢查 Set 內的元素個數即可。
apset.NewSet[string]()
// 列出所有資源
for _, res := range stack.Deployment.Resources {
// 如果資源是 Subnet
if res.Type == "aws:ec2/subnet:Subnet" {
az := res.Outputs["availabilityZoneId"].(string)
azSet.Add(az)
}
}
// 只用到 1 個 AZ
assert.Equal(t, azSet.Cardinality(), 1)
最後,執行整合測試的方式就很簡單,就跟執行一般 Golang 的測試一樣,使用 go test <filename>
執行。
$ go test vpc_test.go
ok command-line-arguments 189.810s
如果驗證都通過,就會輸出 ok。這邊可以看到這麼簡單的整合測試案例,就需要花費 3 分多鐘的執行。與昨天單元測試都是 1 秒內完成的測試比起來,要費時多了。
那如果錯誤的話,就會看到 fail ,並且會輸出錯誤的原因。
$ go test vpc_test.go
vpc_test.go:77 1 does not equal 2
--- FAIL: TestVpc (190.50s)
... stacktrace ...
FAIL
FAIL command-line-arguments 190.954s
FAIL