【入門編】Pulumi Dynamic Providersでカスタムリソースを自作する:API連携とプロビジョニングの自動化 – インフラ構成管理(IaC)活用バイブル

こんにちは!クラウドインフラ・SREの世界へようこそ。
日々のインフラ運用で、「お、このSaaS、APIはあるのにTerraformもPulumiも公式プロバイダがないぞ…どうしよう」と絶望した経験はありませんか?

「仕方ない、シェルスクリプトで力技の自動化(ラッパー)を書くか…」と思ったあなた、ちょっと待ってください。その場しのぎのスクリプトは、インフラストラクチャの「冪等性(べきとうせい)」を破壊し、いつか夜中の障害対応という悪夢を連れてやってきます。

今回は、そんな絶望を希望に変える、Pulumiの秘技「Dynamic Providers(ダイナミックプロバイダー)」の世界へご案内します。これをマスターすれば、あなたの手元にある任意のAPIや社内システムを、完全にIaCのライフサイクル(Create, Read, Update, Delete)に組み込めるようになります。

今日の目標は、初心者の方でも迷わず理解できるように、優しく、かつ現場で即戦力となる知見を交えてハンズオン形式で解説することです。これをマスターすれば、毎日の作業が劇的に楽になりますよ。さあ、一緒に深淵を覗いてみましょう!

—

1. Pulumi Dynamic Providersとは何か?

通常のPulumiプロバイダ(AWSやKubernetesなど)は、Go言語などで書かれた巨大なバイナリであり、クラウドベンダーのAPIを叩きます。
一方、Dynamic Providerは、「TypeScript/JavaScript」や「Python」といった使い慣れた言語で、自分自身のカスタムリソースの挙動(作成・読み込み・更新・削除)を直接コードで定義できる機能です。

ざっくり言うと、こういうことです:
> 「独自のAPIクライアントを書いて、それをPulumiのステート管理(状態管理)の枠組みに無理なく参加させる仕組み」

これにより、既存のプロバイダが存在しないニッチなSaaSや、社内のレガシーなプロビジョニング基盤をも、`pulumi up` や `pulumi destroy` の美しいうねりの中に完全に統合できるのです。

—

2. 今回のハンズオンのゴール

今回は分かりやすさを優先し、外部の実際のSaaSではなく、「ローカルのJSONファイルや簡易的なHTTPサーバー(あるいはモックAPI)」を対象に見立てて、カスタムリソースを構築してみましょう。

テーマは 「社内チャット通知システムへの登録リソース」 です。
Pulumiから「ユーザー名」を渡して登録し、名前を変更すれば更新され、リソースを消せばAPI経由で登録抹消される――このライフサイクルを実装します。

—

3. 基礎セットアップ:プロジェクトの初期化

まずは環境を整えましょう。Node.js (TypeScript) を使って進めます。適当なディレクトリを作成し、Pulumiプロジェクトを初期化してください。

mkdir pulumi-dynamic-sample
cd pulumi-dynamic-sample
pulumi new aws-typescript –name pulumi-dynamic-sample –stack dev –yes

(※今回はAWSのインフラは使いませんが、TypeScriptのプロジェクト構造を手軽に作るためにaws-typescriptテンプレートを流用します。不要なコードは後で削ります)

必要な依存関係を確認しつつ、作業ディレクトリを整えましょう。

—

4. ライフサイクルを完全実装する!Dynamic Providerのコード

ここが今回のメインディッシュです。
Pulumiの `pulumi.dynamic.ResourceProvider` インターフェースを実装するクラスを作成します。

プロジェクト内に `notifierProvider.ts` というファイルを作成し、以下のコードを記述してください。

`notifierProvider.ts`

import as pulumi from “@pulumi/pulumi”;

// 1. リソース作成時に受け取る入力プロパティの定義
interface NotifierArgs {
message: string;
recipient: string;
}

// 2. Pulumiが管理するステート(出力を含む)の定義
interface NotifierState extends NotifierArgs {
// API側から払い出されたユニークなIDなどを想定
registrationId: string;
}

// 3. Dynamic Provider の本体クラス
class NotifierProvider implements pulumi.dynamic.ResourceProvider {

// 【Create】リソースが新しく作成される時に呼ばれる
async create(inputs: NotifierArgs): M {
console.log(`[Create] 外部APIへ登録中… recipient: ${inputs.recipient}`);

// TODO: ここで実際のAPI(fetch等)を叩く
// 今回はモックとして、ランダムなIDを生成して返す
const registrationId = `reg-${Math.random().toString(36.substring(2, 9))}`;

// 成功したら、外部IDと実際に適用されたステートを返す
return {
id: registrationId,
outs: {
…inputs,
registrationId,
},
};
}

// 【Read】現在の実態(リモートの状態)を読み込む時に呼ばれる(ドリフト検出などに使われる)
async read(id: string, props: NotifierState): Promise {
console.log(`[Read] 外部APIから状態を取得中… id: ${id}`);
// 通常はAPIを叩いて実態が存在するか確認し、propsを返す
return {
id,
props,
};
}

// 【Update】プロパティが変更された時に呼ばれる
async update(id: string, olds: NotifierState, news: NotifierArgs): Promise {
console.log(`[Update] 外部APIを更新中… id: ${id}, old recipient: ${olds.recipient} -> new: ${news.recipient}`);

// TODO: APIの更新処理を記述

return {
outs: {
…news,
registrationId: id,
},
};
}

// 【Delete】リソースが削除される時に呼ばれる
async delete(id: string, props: NotifierState): Promise {
console.log(`[Delete] 外部APIから削除中… id: ${id}, recipient: ${props.recipient}`);
// TODO: APIの削除処理を記述
}
}

// 4. ユーザーが扱いやすいようにラップしたカスタムリソースクラス
export class NotifierResource extends pulumi.dynamic.Resource {
public readonly registrationId!: pulumi.Output;
public readonly message!: pulumi.Output;
public readonly recipient!: pulumi.Output;

constructor(name: string, args: NotifierArgs, opts?: pulumi.CustomResourceOptions) {
// 親クラスに Provider のインスタンスと入力を渡す
super(new NotifierProvider(), name, { …args, registrationId: undefined }, opts);
}
}

> SREの知見:冪等性とエラーハンドリング
> 各メソッド(`create`, `update`, `delete` 等)の中身を実装する際、ネットワークエラーやAPI側のレートリミット(429 Too Many Requests)に直面します。実運用では、指数バックオフ(Exponential Backoff)を用いたリトライ機構を必ずここに組み込んでください。Pulumiの実行が途中で失敗しても、ステートの整合性が崩れないように設計するのがプロの技です。

—

5. HelloWorld:カスタムリソースを動かしてみよう!

それでは、先ほど作成したカスタムリソースを `index.ts` から呼び出して、実際に `pulumi up` を実行してみましょう。

`index.ts` を以下のように書き換えます(既存のAWSコードは削除して構いません)。

`index.ts`

import as pulumi from “@pulumi/pulumi”;
import { NotifierResource } from “./notifierProvider”;

// 自作したカスタムリソースを宣言
const myNotifier = new NotifierResource(“my-first-custom-resource”, {
recipient: “sre-team@example.com”,
message: “Hello from Pulumi Dynamic Provider!”,
});

// 出力定義
export const resourceId = myNotifier.registrationId;
export const resourceRecipient = myNotifier.recipient;

さあ、魔法の呪文を唱えましょう。

pulumi up

実行結果はどうなったでしょうか?
コンソールログに `[Create] 外部APIへ登録中…` が出力され、無事にカスタムリソースがPulumiの管理下に置かれたはずです。

変更(Update)のテスト

次に、`index.ts` の `recipient` の値を変更してみましょう。

const myNotifier = new NotifierResource(“my-first-custom-resource”, {
recipient: “platform-sre-team@example.com”, // 変更!
message: “Hello from Pulumi Dynamic Provider!”,
});

再度 `pulumi up` を実行してください。
今度は `[Update] 外部APIを更新中…` が走り、差分(Diff)だけが美しく適用されます。シェルスクリプトではこうはいきません。これがIaCの醍醐味です。

削除(Delete)のテスト

役目が終わったら、お片付けです。

pulumi destroy

`[Delete] 外部APIから削除中…` がログに出力され、リソースが綺麗に消え去ります。完璧ですね!

—

まとめ

お疲れ様でした!今回は Pulumi Dynamic Providers の基本と、ライフサイクル(Create, Read, Update, Delete)の完全な実装方法をハンズオン形式で解説しました。

  • 公式プロバイダがないAPIも、TypeScript/Pythonで自作できる
  • `pulumi.dynamic.Resource` を継承することで、既存のクラウドインフラと同等にステート管理や差分検出の恩恵を受けられる
  • 泥臭いシェルスクリプトの自動化から卒業し、堅牢な冪等性を担保できる

これをマスターしたあなたなら、社内のどんなレガシーシステムやマイナーなSaaSであっても、恐れることなくIaCの宇宙へ統合できるはずです。毎日のインフラ運用の景色が、今日から少し変わるのを感じていただけたでしょうか?

あなたのSREライフが、よりエレガントで快適なものになりますように。それではまた別の深淵でお会いしましょう!

タイトルとURLをコピーしました