iT邦幫忙

2023 iThome 鐵人賽

DAY 7
0
Cloud Native

30 天學習 Pulumi:用各種程式語言控制雲端資源系列 第 7

[Day 07] Pulumi 中的 Input 與 Output 概念 (1)

  • 分享至 

  • xImage
  •  

經過幾天上手 Pulumi 後,我們來看看 Pulumi 的 Input 與 Output 這兩種類型的資料。

撰寫 IaC 的時候,總是離不開對資源的管理。而幾乎每個資源都會需要提供一些參數才能建立,例如建立 VPC 的時候需要給定一個 cidrBlock 或是 IPAM Pool ID。VPC 建立好後,也會產生許多的屬性供我們取用,例如 arn、defaultNetworkAclId、defaultRouteTableId 等資料。

我們要怎麼知道一個資源需要什麼參數才能建立?它提供了什麼樣的屬性跟我們使用呢?

答案是:查詢資源的文件!

例如 AWS Route53 Record 的文件中,Record Resource Properties 區域就會詳細的告訴我們 Input 與 Output 分別有什麼。

  • Input 就是我們要傳遞給那個資源的參數與型別
  • Output 是那個資源提供給我們的屬性與型別

而在 Input 中,如果有標註紅色的 * 符號,就代表這個 Input 參數是必填。例如 AWS Route53 Record 中,必填的參數為 nametypezoneId,除此之外都是選填。

往下拉也能看到 AWS Route53 Record 提供給我們什麼樣的屬性,這邊可以看到只有提供兩個,分別是 fqdnid

你的 Output 是我們 Input

在管理資源,我們很常會將某個資源的 Output 做為另一個資源的 Input 參數。

例如剛剛看到的 AWS Route53 Record 就需要 zoneId,那 zoneId 要去哪裡找呢?
我們可以透過手動建立一個 Route53 DNS Zone,並取得該 zone 的 zoneId,再將 zoneId 填寫到 AWS Route53 Record 中。或是透過 AWS Route53 Zone 建立 zone,並將 zone 的 output 中的 zoneID 做為 Record 的 input。

將 output 傳入 input 的範例:

// 建立 Zone
const zone = new aws.route53.Zone("example", {
  name: "example.com.",
});

const www = new aws.route53.Record("example", {
  // 將 zone 的 output 屬性當作 record 的 input 屬性
  zoneId: zone.zoneId,
  name: "www.example.com.",
  type: "TXT",
  records: [
    "Hello, World"
  ]
});

深入來看 Input 與 Output

其實 Input、Output 的概念也不新奇,將 Output 的結果傳入 Input 也沒什麼了不起。用過其他 IaC 工具的朋友可能也習以為常。

那為啥在 Pulumi 中要特別拿出來講呢?

因為上面我們用的很順手的方式,其實在 Pulumit 中,是不同的物件型別。而在撰寫程式的時候,清楚知道輸入輸出的型別很重要,傳錯型別可能會導致編譯錯誤,或是運行錯誤。

首先我們來仔細觀察一下,TypeScript 中,aws.route53.Zone 參數的型別長怎麼樣:

export interface ZoneArgs {
    comment?: pulumi.Input<string>;
    delegationSetId?: pulumi.Input<string>;
    forceDestroy?: pulumi.Input<boolean>;
    name?: pulumi.Input<string>;
    tags?: pulumi.Input<{
        [key: string]: pulumi.Input<string>;
    }>;
    vpcs?: pulumi.Input<pulumi.Input<inputs.route53.ZoneVpc>[]>;
}

可以注意到 ZoneArgs Interface 中所有的參數型別都是 pulumi.Input,只是這邊透過泛型定義 Input 內的型別。

接著我們再來看一下 aws.route53.Zone 類別提供我們存取的屬性內容:

export declare class Zone extends pulumi.CustomResource {
    readonly arn: pulumi.Output<string>;
    readonly comment: pulumi.Output<string>;
    readonly delegationSetId: pulumi.Output<string | undefined>;
    readonly forceDestroy: pulumi.Output<boolean | undefined>;
    readonly name: pulumi.Output<string>;
    readonly nameServers: pulumi.Output<string[]>;
    readonly primaryNameServer: pulumi.Output<string>;
    readonly tags: pulumi.Output<{
        [key: string]: string;
    } | undefined>;
    readonly tagsAll: pulumi.Output<{
        [key: string]: string;
    }>;
    readonly vpcs: pulumi.Output<outputs.route53.ZoneVpc[] | undefined>;
    readonly zoneId: pulumi.Output<string>;
}

可以發現,所有供我們存取的屬性都是 pulumi.Output 型別。

再回過頭來看上一節建立 zone 與 record 的範例,我們就會發現奇怪的地方了。
在 record 中,我們將 zone.zoneId 這個 pulumi.Output<string> 型別的資料傳入接受 pulumi.Input<string> 的參數中。
另外我們又將純文字 (string)的 nametype 傳給接受 pulumi.Input<string> 之中。

到底這個 pulumi.Input 是什麼型別呢?傳泛型中指定的型別也行、與之相對應的 pulumi.Output 也行?這邊沒有說到,其實 Input 也可以是一個 Promise 型別。

可以從 Input 的定義看到,其實 Input 可以是沒有包裝過的型別、Promise、Output,這也解釋了為什麼我們可以傳遞各種不同的資料給 Input。

/**
 * [Input] is a property input for a resource.  It may be a promptly available T, a promise for one,
 * or the output from a existing Resource.
 */
// Note: we accept an OutputInstance (and not an Output) here to be *more* flexible in terms of
// what an Input is.  OutputInstance has *less* members than Output (because it doesn't lift anything).
export type Input<T> = T | Promise<T> | OutputInstance<T>;

這邊修改一下範例,展示一下如何傳送 Promise 給 Input:

const www = new aws.route53.Record("example", {
  zoneId: zone.zoneId,
  // 傳遞 Promise<string> 至 Input<string> 中
  name: Promise.resolve("www.example.com."),
  type: "TXT",
  records: [
    "Hello, World"
  ]
});

Output

看過了 Input 可以接受不同的型別後,我們來看 Output。
Output 就與 Input 不同了,Output 是真正將值封裝成一個物件,並且提供一些方式來存取他。我們可以從定義看得出來他的不平凡:

export type Output<T> = OutputInstance<T> & Lifted<T>;

但很不幸的,Pulumi 的 Output 並沒有提供任何方法、屬性讓我們直接存取到裡面所封裝的值。例如說我們拿到了一個 Output<string> 型別的值,我們是沒有任何方式可以取得裡面的資料的!

這個設計就造就了在撰寫 Pulumi 程式時的各種問題,因為無法取得 Output 內的值,對於撰寫 iac 程式很不方便。怎麼說呢?請看以下例子:

// 我們從某個 Resource 拿到了 ip address,假設是 200.100.10.20 好了
const myIp: pulumi.Output<string> = eip.publicIp

// 我們想要將 ip 字串與其他字串做一些處理,例如取得該 ip 的網段,並產生 CIDR 格式的網段描述,類似這樣: 200.100.10.0/24
// 產生出來後,我想要將他做為其他資源的 Input

以上的範例中,我們想要對 myIp 內封裝的 string 做處理,並傳送至其他資源中使用,但 Output 中沒有提供任何方式可以拿到 string,這要怎麼辦?

當然在 Pulumi 一定有方法可以處理這個問題,不然這個 IaC 工具就會很難被使用。那要怎麼處理呢?

Pulumi 的 Output 提供了一些函式讓我們可以將 Output 的內容做一些轉換,轉換成另一個 Output。

這個概念很重要:Pulumi 的 Output 提供方式讓我們將 Output 轉換為另一種 Output

我們永遠無法拿到解開的內容,但可以透過轉換的方式將 Output 轉成另一種樣子的 Output。反正 Input 可以接受一個 Output。

為何 Pulumi 要這樣設計?要怎麼將 Output 轉成另一種 Output 呢?我們明天再來介紹 Output 的詳細用法。

後記

今天一直猶豫要繼續重構之前的內容,然後把 stack output 講完。還是該面對 Input 與 Output。還是寫了 Input Output 的內容了,這個概念在 Pulumi 中太重要了,沒有學會的話,很多操作會遇到問題。stack output 就留到 Input Output 之後,在繼續講下去吧。


上一篇
[Day 06] 善用程式語言優勢撰寫 Pulumi IaC
下一篇
[Day 08] Pulumi 中的 Input 與 Output 概念 (2)
系列文
30 天學習 Pulumi:用各種程式語言控制雲端資源30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言