Pulumi Dynamic Providersの深淵:既存プロバイダの限界を超えるAPI連携の自動化
こんにちは。テックリードの私だ。
日々のクラウドインフラ構築において、TerraformやPulumiの公式プロバイダは非常に優秀だ。AWS、GCP、Kubernetes、そして主要なSaaSまで、大抵のリソースはワンライナーで宣言的にプロビジョニングできる。
だが、現実のプロジェクトはどうだ?
「社内ニッチな基幹API」「新進気鋭のSaaS」「自社製オンプレミス連携ゲートウェイ」……こうした独自のインフラストラクチャやAPIに直面した瞬間、既存のプロバイダ網はプツリと途切れる。
「じゃあ、シェルスクリプトやカスタムCLIをラップしてCI/CDで無理やり叩くか?」
……待て。そんなことをすれば、「状態の管理(State)」「冪等性(Idempotency)」「変更履歴の追跡」という、IaCが血眼になって勝ち取ってきた全ての恩恵が水泡に帰す。CI/CDパイプラインは汚れ、リソースのドリフト(乖離)検知は不可能になり、夜間障害でPagerDutyが鳴り響くことになる。
この絶望的なギャップを埋める唯一にして最強のカードが、Pulumi Dynamic Providersだ。
今回は、あらゆるAPIをPulumiのファーストクラス市民(宣言的リソース)へと昇華させ、インフラのライフサイクルに完全に組み込むための実装の極意を、ハンズオン形式でコードの深部まで解説しよう。
—
1. 開発スピードを極限まで高める:Pulumi開発者のためのエコシステム
本題に入る前に、プロの現場で「手薬すりながら」使われている開発環境のチューニングについて共有しておく。これなしでは日々のインフラ開発はただの苦行だ。
絶対入れるべきVS Code / IDEプラグイン
- Pulumi (Official): 型補完、ホバーでのリソースプレビュー、スタック情報の可視化に必須。
- Error Lens: エラーや型ミスマッチをコード行の末尾にインライン表示。TypeScript/Pythonでの記述ミスをゼロにする。
- GitLens: 「なぜこのプロパティが追加されたのか」をコードから一瞬でGit履歴を引いて特定する。
チーム開発のための設定共有(`.vscode/settings.json`)
インフラコードの品質を担保するため、チーム全員の環境でフォーマットとリントを強制せよ。
{
“editor.formatOnSave”: true,
“editor.codeActionsOnSave”: {
“source.fixAll.eslint”: “explicit”
},
“typescript.suggest.completeFunctionCalls”: true,
“files.associations”: {
“Pulumi..yaml”: “yaml”
}
}
—
2. Dynamic Providersのアーキテクチャとライフサイクル
Dynamic Providerは、Pulumiのエンジンと、あなたが書くカスタムロジック(TypeScriptやPythonなど)をRPC(gRPC)で接続する仕組みだ。これにより、任意のAPI呼び出しをPulumiのトランザクション(CRUD操作)にマッピングできる。
実装にあたっては、以下の4つのライフサイクルメソッドを完全に理解し、「完全に冪等な状態」を担保するコードを書く必要がある。
1. `create`: リソースが存在しない状態から、APIを叩いて新規作成し、`id` と `outs`(実リソースの状態)を返す。
2. `read`: 現在のクラウド/API側の実状態をフェッチする。ドリフト検知の要。
3. `update`: プロパティに変更があった場合、APIを叩いて差分を適用する。
4. `delete`: リソース破棄時にAPIを叩いてクリーンアップする。
—
3. 実践ハンズオン:社内ドメイン管理APIを制御するカスタムリソース
今回は例として、「社内DNS/ドメイン管理SaaS(架空の社内API)」をPulumiから直接プロビジョニングするカスタムリソースをTypeScriptで実装する。
プロジェクト構成
.
├── package.json
├── tsconfig.json
├── index.ts
└── provider.ts # Dynamic Providerの実装
① Dynamic Providerの実装 (`provider.ts`)
ここが今回の核心だ。APIクライアントのラップと、CRUDの各メソッドを実装する。
import as pulumi from “@pulumi/pulumi”;
import axios from “axios”;
// 1. リソースの入力プロパティ(ユーザーがPulumiコードで指定する値)
export interface DomainRecordArgs {
domain: string;
targetIp: string;
recordType: “A” | “CNAME” | “TXT”;
}
// 2. リソースの出力プロパティ(APIから返却される実ステータス)
export interface DomainRecordState extends DomainRecordArgs {
recordId: string;
}
// 3. 実際のAPIリクエストをハンドリングする Provider クラス
class DomainRecordProvider implements pulumi.dynamic.ResourceProvider {
private apiEndpoint = “https://api.internal.net/v1/domains”;
private apiToken = process.env.INTERNAL_API_TOKEN;
private getHeaders() {
return {
Authorization: `Bearer ${this.apiToken}`,
“Content-Type”: “application/json”,
};
}
// CREATE: リソースの新規作成
async create(inputs: DomainRecordArgs): Promise
try {
const response = await axios.post(
this.apiEndpoint,
{
domain: inputs.domain,
ip: inputs.targetIp,
type: inputs.recordType,
},
{ headers: this.getHeaders() }
);
const recordId = response.data.id;
// Pulumiのステートに保存される出力値を返す
return {
id: recordId,
outs: {
…inputs,
recordId,
},
};
} catch (error: any) {
throw new pulumi.RunError(`Failed to create domain record: ${error.message}`);
}
}
// READ: リソースの現状確認(ドリフト検出に必須)
async read(id: string, props: DomainRecordState): Promise
try {
const response = await axios.get(`${this.apiEndpoint}/${id}`, {
headers: this.getHeaders(),
});
return {
id: id,
outs: {
domain: response.data.domain,
targetIp: response.data.ip,
recordType: response.data.type,
recordId: id,
},
};
} catch (error: any) {
// リソースが既に外部で削除されていた場合等のハンドリング
if (error.response && error.response.status === 404) {
return { id: “” }; // 空IDを返すとPulumiは「削除された」と判定し再作成する
}
throw new pulumi.RunError(`Failed to read domain record: ${error.message}`);
}
}
// UPDATE: 差分の適用(冪等性を意識したPUT/PATCH)
async update(id: string, olds: DomainRecordState, news: DomainRecordArgs): Promise
try {
await axios.put(
`${this.apiEndpoint}/${id}`,
{
domain: news.domain,
ip: news.targetIp,
type: news.recordType,
},
{ headers: this.getHeaders() }
);
return {
outs: {
…news,
recordId: id,
},
};
} catch (error: any) {
throw new pulumi.RunError(`Failed to update domain record: ${error.message}`);
}
}
// DELETE: リソースの削除
async delete(id: string, props: DomainRecordState): Promise
try {
await axios.delete(`${this.apiEndpoint}/${id}`, {
headers: this.getHeaders(),
});
} catch (error: any) {
// 404はすでに消えているとみなして許容する
if (error.response && error.response.status !== 404) {
throw new pulumi.RunError(`Failed to delete domain record: ${error.message}`);
}
}
}
}
// 4. カスタムリソースのラッパークラス(ユーザーが呼び出すコンポーネント)
export class DomainRecord extends pulumi.dynamic.Resource {
public readonly domain!: pulumi.Output
public readonly targetIp!: pulumi.Output
public readonly recordType!: pulumi.Output
public readonly recordId!: pulumi.Output
constructor(name: string, args: DomainRecordArgs, opts?: pulumi.CustomResourceOptions) {
super(new DomainRecordProvider(), name, args, opts);
}
}
② ユーザーコードでの利用 (`index.ts`)
作成したカスタムリソースは、通常のAWSリソース等と全く同じように、宣言的かつ型安全に扱うことができる。
import as pulumi from “@pulumi/pulumi”;
import { DomainRecord } from “./provider”;
// 社内APIを通じてDNSレコードを宣言的にプロビジョニング
const myAppDns = new DomainRecord(“my-app-dns”, {
domain: “app.internal.corp”,
targetIp: “192.168.10.50”,
recordType: “A”,
});
// 出力エクスポート
export const dnsRecordId = myAppDns.recordId;
export const registeredDomain = myAppDns.domain;
—
4. プロの知見:実運用でハマる「アンチパターン」と回避策
Dynamic Providersを本番環境(Production)に投入する際、多くのエンジニアが踏み抜く地雷がある。これを事前に回避せよ。
アンチパターン1: ネットワークタイムアウトとリトライの欠如
社内APIやSaaSは、クラウド大手のマネージドサービスに比べて圧倒的に不安定だ。ネットワークの瞬断やレートリミット(429 Too Many Requests)で容易に死ぬ。
- 対策: `axios` 単体で使わず、必ず `axios-retry` などのライブラリを挟むか、指数バックオフ(Exponential Backoff)を実装したラッパー関数経由でAPIを叩け。
アンチパターン2: センシティブ情報の平文ステート保存
APIトークンや払い出されたシークレットが、PulumiのStateファイルに平文で記録される事故が後を絶たない。
- 対策: 動的プロバイダが返す `outs` にパスワードやシークレットを含める場合、必ず `pulumi.secret()` でラップするか、ステートに残すべきではない機密情報は出力値(outs)に含めない設計にすること。
アンチパターン3: `read` メソッドでの破壊的エラーハンドリング
API側の一時的な障害(500 Internal Server Error)で `read` が例外を吐いた場合、Pulumiは「リソースが消失した」と誤認して勝手に再作成を試みることがある。
- 対策: `read` 内での非意図的なHTTPエラー(5xx系)は、リソース消失(404)と明確に区別し、即座に例外をスローしてデプロイをアボート(中断)させろ。
—
5. おわりに:IaCの領域を拡張せよ
Pulumi Dynamic Providersをマスターしたあなたにもはや「プロバイダが存在しないから自動化できない」という言い訳は通用しない。
社内のレガシーシステム、IoTデバイスのプロビジョニング、SaaSのライセンス自動割り当て――世界中のあらゆるAPIをコードの支配下に置き、インフラストラクチャの境界線を拡張してほしい。
真のSRE、真のインフラエンジニアであれば、無いなら作ればいい。
コードですべてを制御する快感を、存分に味わってくれ。