【Pulumi】Pulumi ~ 基本編 / Component ~

■ はじめに

Pulumi の Component (コンポーネント)って概念を勉強する

目次

【1】Component (コンポーネント)
【2】利点
 1)階層化で表示され、見やすくなる
 2)作成・削除の指定が楽になる
【3】使用例
【4】作成に当たって
 1)ComponentResourceクラス
【5】サンプル
 例1:Hello World
 例2:AWS
 例3:Outputとして登録する
 例4:Kubernetes

【1】Component (コンポーネント)

https://www.pulumi.com/docs/intro/concepts/resources/components/

A component resource is a logical grouping of resources.
[訳] コンポーネント リソース は、リソースの論理的なグルーピングです
 => 複数スタックにまたがる設定をまとめる

【2】利点

* 一言でいえば、「管理しやすいから」
 => どう管理しやすいかっていうと、、、
~~~~~
1)階層化で表示され、見やすくなる
2)作成・削除の指定が楽になる
~~~~~

Pulumi ~ 基本編 / CLI
https://dk521123.hatenablog.com/entry/2021/10/25/215508

1)階層化で表示され、見やすくなる

* pulumi preview/up などで階層化で表示され、見やすくなる

$ pulumi stack --show-urns

    TYPE                                 NAME
    pulumi:pulumi:Stack                  quickstart-dev
    │  URN: urn:pulumi:dev::quickstart::pulumi:pulumi:Stack::quickstart-dev
    ├─ demo:DemoComponent                hello-world
    │  │  URN: urn:pulumi:dev::quickstart::yyyyy
    │  ├─ kubernetes:apps/v1:Deployment  nginx
    │  │     URN: urn:pulumi:dev::quickstart::xxxxx
    │  └─ kubernetes:core/v1:Service     nginx
    │        URN: urn:pulumi:dev::quickstart::demo:zzzzzzz
    └─ pulumi:providers:kubernetes       default_3_17_0
          URN: urn:pulumi:dev::quickstart::xxxxxxx

2)作成・削除の指定が楽になる

* pulumi up/destroy --target で作成・削除の指定が楽になる
* 以下の関連記事の「pulumi up」「pulumi destroy」を参照のこと

https://dk521123.hatenablog.com/entry/2021/10/25/215508

#  コマンド例「pulumi up --target {URN}」
pulumi up --target urn:pulumi:dev::quickstart::yyyyy

【3】使用例

例1:Glue workflow に必要な 以下をデプロイ処理を纏める

[1] スクリプトをS3にあげる
[2] Glue workflow作成
[3] Glue Job作成
[4] 紐づく Glue trigger作成

【4】作成に当たって

* pulumi.ComponentResource を継承して使う

https://www.pulumi.com/docs/reference/pkg/nodejs/pulumi/pulumi/#ComponentResource

* 親子関係を設定しておくといい
 => 「例4:Kubernetes」の「{ parent: this }」を参考に、、、

1)ComponentResourceクラス

https://www.pulumi.com/docs/reference/pkg/nodejs/pulumi/pulumi/#ComponentResource

new ComponentResource(
  // type : the fully qualified type token
  type: string,
  // name : The unique name of the resource
  name: string,
  // args : Information passed to [initialize] method.
  args: Inputs,
  // opts : A bag of options that control this resource's behavior.
  opts: ComponentResourceOptions,
  // remote : True if this is a remote component resource.
  remote: boolean
)

【5】サンプル

例1:Hello World

./components/demoComponent.ts

import * as pulumi from "@pulumi/pulumi";

// Inherit pulumi.ComponentResource
export class DemoComponent extends pulumi.ComponentResource {
  readonly arg1: string;

  // Register this component with name demo:DemoComponent
  constructor(
    name: string,
    {
       arg1,
    }: DemoArgs,
    opts?: pulumi.ComponentResourceOptions
  ) {
    super("demo:DemoComponent", name, {}, opts);

    // 以降に処理を書く
    this.arg1 = arg1;
    pulumi.log.info(`arg1 = ${arg1}`)

    // Register that we are done constructing the component
    this.registerOutputs();
  }
}

export type DemoArgs = {
  readonly arg1: string;
}

index.ts

import * as pulumi from "@pulumi/pulumi";

// 作成したComponent クラスを import
import { DemoComponent } from "./components/demoComponent";

const demoComponent = new DemoComponent(
  "hello-world",
  {
    arg1: "Hello, World!!",
  },
);

出力結果例:pulumi review

$ pulumi preview
Previewing update (dev)

View Live: https://app.pulumi.com/user/Hello/dev/previews/xxxxx

     Type                   Name         Plan       Info
 +   pulumi:pulumi:Stack    Hello-dev   create     1 message
 +   └─ demo:DemoComponent  hello-world  create     
 
Diagnostics:
  pulumi:pulumi:Stack (Hello-dev):
    arg1 = Hello, World!!

例2:AWS

demoComponent.ts

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

// Inherit pulumi.ComponentResource 
export class DemoComponent extends pulumi.ComponentResource {
  readonly bucketName: string;

  // Register this component with name demo:DemoComponent
  constructor(
    name: string,
    {
       bucketName,
    }: DemoArgs,
    opts?: pulumi.ComponentResourceOptions
  ) {
    super("demo:DemoComponent", name, {}, opts);

    // 以降に処理を書く

    this.bucketName = bucketName;

    // Create a bucket
    const demoBucket = new aws.s3.Bucket(bucketName, {},
      { parent: this } ); // specify resource parent ★重要★

    // Create a property for the bucket name that was created
    this.bucketName = demoBucket.bucket,

    // Register that we are done constructing the component
    this.registerOutputs();
  }
}

export type DemoArgs = {
  readonly bucketName: string;
}

index.ts

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

// 作成したComponent クラスを import
import { DemoComponent } from "./demoComponent";

const demoComponent = new DemoComponent(
  "hello-world",
  {
    bucketName: "your-s3-bucket",
  },
);

例3:Outputとして登録する

* registerOutputs() の引数に登録すればいい
* 「例1:Hello World」を修正する
* 以下の Github が参考になる

https://github.com/pulumi/examples/tree/master/twilio-ts-component

./components/demoComponent.ts

import * as pulumi from "@pulumi/pulumi";

// Inherit pulumi.ComponentResource
export class DemoComponent extends pulumi.ComponentResource {
  readonly arg1: string;
  // ★注目★
  public readonly result: pulumi.Output<string>;

  // Register this component with name demo:DemoComponent
  constructor(
    name: string,
    {
       arg1,
    }: DemoArgs,
    opts?: pulumi.ComponentResourceOptions
  ) {
    super("demo:DemoComponent", name, {}, opts);

    // 以降に処理を書く
    this.arg1 = arg1;
    pulumi.log.info(`arg1 = ${arg1}`)

    // ★注目★
    // Register the result as an output of the component itself.
    this.result = pulumi.interpolate `Result is ${arg1}...`;
    this.registerOutputs({
      result: this.result,
    });
  }
}

export type DemoArgs = {
  readonly arg1: string;
}

index.ts

import * as pulumi from "@pulumi/pulumi";

// 作成したComponent クラスを import
import { DemoComponent } from "./components/demoComponent";

const demoComponent = new DemoComponent(
  "hello-world",
  {
    arg1: "Hello, World!!",
  },
);

// ★注目★
export const result = demoComponent.result;

出力結果

$ pulumi preview

Outputs:
  result: "Result is Hello, World!!..."

例4:Kubernetes

https://dk521123.hatenablog.com/entry/2022/03/07/233752

でやった例を Component で作成してみる

./components/demoComponent.ts

import * as pulumi from "@pulumi/pulumi";
import * as k8s from "@pulumi/kubernetes";

// Inherit pulumi.ComponentResource
export class DemoComponent extends pulumi.ComponentResource {
  public readonly ip: pulumi.Output<string>;

  // Register this component with name demo:DemoComponent
  constructor(
    name: string,
    {
       arg1,
    }: DemoArgs,
    opts?: pulumi.ComponentResourceOptions
  ) {
    super("demo:DemoComponent", name, {}, opts);

    // Minikube does not implement services of type `LoadBalancer`; require the user to specify if we're
    // running on minikube, and if so, create only services of type ClusterIP.
    const config = new pulumi.Config();
    const isMinikube = config.requireBoolean("isMinikube");

    const appName = "nginx";
    const appLabels = { app: appName };
    const deployment = new k8s.apps.v1.Deployment(appName, {
        spec: {
            selector: { matchLabels: appLabels },
            replicas: 1,
            template: {
                metadata: { labels: appLabels },
                spec: { containers: [{ name: appName, image: "nginx" }] }
            }
        }
    }, { parent: this }); // ★親子関係を指定

    // Allocate an IP to the Deployment.
    const frontend = new k8s.core.v1.Service(appName, {
        metadata: { labels: deployment.spec.template.metadata.labels },
        spec: {
            type: isMinikube ? "ClusterIP" : "LoadBalancer",
            ports: [{ port: 80, targetPort: 80, protocol: "TCP" }],
            selector: appLabels
        }
    }, { parent: this }); // ★親子関係を指定

    // When "done", this will print the public IP.
    this.ip = isMinikube
        ? pulumi.output(frontend.spec.clusterIP)
        : pulumi.output(frontend.status.loadBalancer.apply(
              (lb) => lb.ingress[0].ip || lb.ingress[0].hostname
          ));
     
    // Register the result as an output of the component itself.
    this.registerOutputs({
      ip: this.ip,
    });
  }
}

export type DemoArgs = {
  readonly arg1: string;
}

index.ts

import * as pulumi from "@pulumi/pulumi";

// 作成したComponent クラスを import
import { DemoComponent } from "./components/demoComponent";

const demoComponent = new DemoComponent(
  "hello-world",
  {
    arg1: "Hello, World!!",
  },
);

export const ip = demoComponent.ip;

出力結果

Updating (dev):
     Type                                 Name            Status
     pulumi:pulumi:Stack                  quickstart-dev
     ├─ demo:DemoComponent                hello-world     created << 親
     │  ├─ kubernetes:apps/v1:Deployment  nginx           created << 子
     │  └─ kubernetes:core/v1:Service     nginx           created << 子

参考文献

https://tech.guitarrapc.com/entry/2019/12/02/011359
https://qiita.com/suin/items/4cf7e97ae902b2d16417
https://tech.guitarrapc.com/entry/2019/12/05/000000
公式サイト
https://www.pulumi.com/registry/packages/aws/how-to-guides/s3-folder-component/#s3-folder-pulumi-component
https://www.pulumi.com/blog/resource-methods-for-pulumi-packages/#component-implementation
https://www.pulumi.com/docs/intro/concepts/resources/components/

関連記事

Pulumi ~ 基礎知識編 ~
https://dk521123.hatenablog.com/entry/2021/10/23/025230
Pulumi ~ 基本編 / Logging ~
https://dk521123.hatenablog.com/entry/2022/03/04/111618
Pulumi ~ AWS S3 / KMS のデプロイ ~
https://dk521123.hatenablog.com/entry/2022/03/03/095415
Pulumi ~ AWS Glue のデプロイ ~
https://dk521123.hatenablog.com/entry/2022/03/02/122037
Pulumi ~ 入門編 / Hello World in Local/k8s
https://dk521123.hatenablog.com/entry/2022/03/07/233752
Pulumi ~ 入門編 / Hello World in AWS
https://dk521123.hatenablog.com/entry/2022/03/11/184041
Pulumi ~ 基本編 / CLI
https://dk521123.hatenablog.com/entry/2021/10/25/215508