上一篇文章提到 Pulumi 中,所有資源的屬性都會是 Output 型別,並且是不能直接拿到 Output 的內容。如果要將 Output 內容做處理,必須使用 Output 提供的方法來轉換 Output 成為另一種 Output。
這篇文章就來說明 Output 提供了那些方法可以處理 Output。
在 Pulumi 中,最常用來處理 Output 的方式就屬 Apply 了,任何的 Output 都可以執行他的 Apply 方法,並且傳遞一個 lambda 函式,用來處理 Output 的值。
例如以下範例:
const webServerUrl: pulumi.Output<string> =
eip.publicIp.apply(function (publicIp: string) {
return "http://" + publicIp + "/";
});
這這個範例中,我想要使用 Elastic IP 做為 web server 的網址,因此需要在字串前後加點東西。但是 Output 型別不允許我們這麼做,因此我們就可以透過 apply 這個方法來轉換 Output。
apply 接受一個 lambda 函式,參數會是 Output 的值。因此我們就能透過這個 lambda 函式將 public 前後加上 http://
、/
。而這個 lambda 函式的回傳值,最後還會被包裝成 Output 型別。因此我們的 webServerUrl 就會的型別是 Output<string>
。
如果我們要組合多個 Output 的值,要怎麼做呢?
我們可以透過 pulumi.all 將多個 Output 轉換為一個 Output。
例如以下範例就會將 myVpc.id
、myVpc.cidrBlock
組合成一個 Output combineOutput
。可以注意到 combineOutput
的型別為 Output,內裝的是一個陣列,第一個元素為 string,第二個也是 string。
const combineOutput: pulumi.Output<[string, string]> =
pulumi.all([myVpc.id, myVpc.cidrBlock]);
將多個 Output 組合為一個 Output 後,就能透過 apply 來組合資料了。
const vpcInfo: pulumi.Output<string> =
combineOutput.apply(function ([vpcId, vpcCidrBlock]) {
return vpcId + " " + vpcCidrBlock
});
通常來說,使用 pulumi.all
組合多個 Output 成為一個後的目的就是要一起處理這些 Output。因此可以將兩行程式碼改寫為以下:
const vpcInfo: pulumi.Output<string> =
pulumi.all([myVpc.id, myVpc.cidrBlock])
.apply(function ([myVpcId, myVpcCidrBlock]) {
return myVpcId + " " + myVpcCidrBlock
});
之前提到 Input 其實有可能是 3 種型別,純型別(沒有用 Output、Promise 包裝過的型別)、Output、Promise。這樣就會導致我們要處理 Input 的時候會很麻煩。因為我們不清楚使用者會如何使用我們的程式,就需要判斷不同的型別做不同的處理。
回顧一下,前面重構時,類別建構子的參數:
interface CreatePrivateSubnetArgs {
az: string;
cidr: string;
vpcId: pulumi.Input<string>;
natGatewayId: pulumi.Input<string>;
defaultTags: Record<string, string>;
}
可以看到 vpcId 是一個 Input,因此使用者在建立 PrivateSubnbet 物件時,vpcId 可能是由 VPC 資源的 Output 取得,也有可能是手動輸入一個 vpc id 字串。
假設因為某些原因,我們需要將 vpcId 做處理,就得判斷使用者傳入的內容是什麼。這會導致程式的複雜。
在 Pulumi 官方文件中有提到,建議將要處理的 Input 轉換為 Output 進行處理。
這時我們就會用到 pulumi.output
函式了,這個函式是專門將任何的 pulumi.Input 轉換為 pulumi.Output 的函式。
function processVpcId(vpcId: pulumi.Input<string>) {
let _vpcid = pulumi.output(vpcId);
}
轉換為 Output 後,就可以使用 apply
方法對 output 進行處理,不需要擔心他是純型別、Promise 還是 Output 了。這可以大大簡化對 Input 內容處理的複雜度。
但前面有提到 apply
只能將 Output 轉換為 Output,我們將 Input 變成 Output 以後要怎麼再讓他變回 Input,並傳遞給建立資源的參數呢?
這點倒是不用擔心,因為前面提到,Input 這個型別有三種可能:純型別、Promise、Output。因此轉換後的 Output 其實也是 Input 可以接受的三種型別中的其中一種,是可以直接傳遞到建立資源的參數中不會有問題的。
apply
後的結果就是純值,或是誤將 apply
的回傳值傳入不支援處理 Apply 的 function我們來看一個官方提到常見的錯誤。
這邊直接引用官方的程式碼來看一下發生什麼事了:
const bucketPolicy = new aws.s3.BucketPolicy("cloudfront-bucket-policy", {
bucket: contentBucket.bucket,
policy: JSON.stringify({
Version: "2012-10-17",
Statement: [
{
Sid: "CloudfrontAllow",
Effect: "Allow",
Principal: {
AWS: iamArn,
},
Action: "s3:GetObject",
Resource: bucket.arn.apply(arn => arn),
},
],
})
});
這個程式碼是建立一個 S3 的 BucketPolicy,在 Policy 的地方透過 JSON.stringify 將 object 轉換為 string,並傳遞給 policy。
policy 物件中有用到 bucket.arn
,且他有使用 apply 對這個 bucket.arn 做處理。
看起來都很正常,這有什麼問題呢?
問題就出在 JSON.stringify 是沒辦法序列化 bucket.arn.apply(arn => arn)
,因為 apply
還是會回傳 Output,沒有任何方法可以取得 Output 的值,因此 JSON.stringify 也無法取得轉換後的 arn 的值。
那麼要怎麼做才是對的?
這邊使用 JSON.stringify 是為了將整個 policy 變成字串做為 Input,但我們其實可以將 policy 轉為 Output。
我們可以透過 apply
的 lambda function,將 arn 轉換為 policy。
const policy: pulumi.Output<string> =
bucket.arn.apply(function (arn: string) {
return JSON.stringify({
Version: "2012-10-17",
Statement: [
{
Sid: "CloudfrontAllow",
Effect: "Allow",
Principal: {
AWS: iamArn,
},
Action: "s3:GetObject",
Resource: arn,
},
],
})
})
查詢Bucket Policy 的文件可以發現,policy 不只可以接受 string,還可以接受 PolicyDocument。PolicyDocument 定義的所有參數都是 pulumi.Input,因此我們不需要煩惱要去轉換 bucket.arn 的值,可以直接這樣使用:
const policy: aws.iam.PolicyDocument = {
Version: "2012-10-17",
Statement: [
{
Sid: "CloudfrontAllow",
Effect: "Allow",
Principal: {
AWS: iamArn,
},
Action: "s3:GetObject",
Resource: arn,
}
]
};
第二個常見的錯誤就是將 Output 的值做為資源的 Name。資源的名稱是不支援任何 Input、Output、Promise 的,只能是純文字。因此是無法將 output 做為資源的值使用。
例如:
const publicEc2Name = vpc.id.apply(vpcId => "public-subnet-" + vpcId)
const publicSubnet = new aws.ec2.Subnet(publicEc2Name, ...);
如果一定要這樣用的話,可以在 apply
內部建立資源。
const publicEc2Name = vpc.id.apply((vpcId: string) => {
const publicSubnet = new aws.ec2.Subnet("public-subnet-" + vpcId, ...);
})
但並不建議在 apply 內部建立資源,因為 apply 的 lambda 執行是非同步的,所以在 preview 產生 diff 的時候可能會有不準確的狀況發生。建議還是想辦法讓值可以固定,不依賴其他輸出。
Output 的設計理念與非同步程式設計的 Promise 很像,都是一個目前沒有值,之後有值的時候可以透過 apply
去存取值。所以如果比較熟悉以前 JavaScript 操作 Promise 的方法的話,應該會對 Output 的操作不陌生。(會說以前是因為現在都改用 async/await 操作 Promise了)