Pulumiカスタムプロバイダ開発の深淵:Bridge技術とSchema駆動による社内レガシー・独自APIの完全IaC化
インフラストラクチャをコード(IaC)として管理する時代において、AWSやKubernetesといったメジャーなリソースの宣言的管理はもはやコモディティである。真のエンジニアリングの戦場は、「組織固有のレガシーシステム」「社内独自REST API」「非公開のクラウド基幹」を、いかにしてモダンなIaCパイプラインに統合するか、という点にある。
TerraformのHCLに絶望し、TypeScriptやGo、Pythonの型システムと強力なエコシステムを手に入れた我々にとって、Pulumiはその終着点に見える。だが、公式プロバイダが存在しない社内システムに直面した時、多くのエンジニアはシェルスクリプトや場当たり的なAnsibleプレイブックという名の「技術的負債の墓場」へと逃げ帰る。
断言しよう。その必要はない。
本稿では、既存のTerraformプロバイダを流用するPulumi Bridge(tfbridge)技術と、JSON Schemaを起点にゼロからプロバイダを爆誕させるPulumi Schema駆動開発の極意を、低レイヤのアーキテクチャ解釈とともに叩き込む。
—
1. 内部アーキテクチャの理解:Pulumiプロバイダはどう動いているのか
カスタムプロバイダのコードを書く前に、PulumiのRPC(gRPC)ベースのプロトコルスタックを脳裏に焼き付けなければならない。
[Pulumi Engine] –(gRPC)—> [Provider Plugin (Go)] –(HTTP/REST)—> [Target API / Legacy System]
Pulumiエンジンとプロバイダは完全に疎結合であり、独立したプロセスとして動く。通信はすべてgRPCで行われる。プロバイダの本体は、指定されたスキーマに従って以下のライフサイクルメソッドを実装するバイナリ(通常はGo製)に他ならない。
- `Configure`: 認証情報やエンドポイントの初期化
- `Check / Diff`: ユーザーが書いたコードのDesired Stateと、実際のCurrent Stateの差分計算
- `Create / Update / Delete`: 実際のAPIを叩いてリソースを収束させる実体化処理
このプリミティブな仕組みを理解していれば、「自社製APIを叩くプロバイダを書く」という行為が、単なるgRPCサーバーの実装に過ぎないことが見えてくる。
—
2. アプローチA:Terraform Bridge(`tfbridge`)による高速マイグレーション
もしあなたの組織が、過去に無理やり書いた社内用のTerraformプロバイダ(あるいはGoで書かれたTerraform Plugin SDKベースのコード)を持っているなら、ゼロからプロバイダを書くのは愚行である。
Pulumiの `pulumi-tfbridge` を使えば、既存のTerraformプロバイダを数時間でPulumiネイティブのプロバイダに変換(ブリッジ)できる。
ブリッジプロジェクトの骨格
典型的なブリッジプロバイダのGoプロジェクト構造は以下のようになる。
pulumi-resource-internalapi/
├── cmd/
│ └── pulumi-resource-internalapi/
│ └── main.go # エントリポイント
├── provider/
│ ├── provider.go # tfbridgeのラッパー設定
│ └── resources.go # リソースのマッピング定義
└── sdk/ # 自動生成される多言語SDK
`resources.go` における型マッピングとオーバーライドの極意
`tfbridge` の真価は、TerraformのスキーマをPulumiの強力な型システムに自動変換しつつ、必要に応じてエッジケースを握り潰せる点にある。
// provider/resources.go
package provider
import (
“unicode”
“github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge”
shimv2 “github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/sdk-v2”
“github.com/your-org/terraform-provider-internalapi/internalapi” // 既存のTerraformプロバイダ
“github.com/pulumi/pulumi/sdk/v3/go/common/tokens”
)
// パッケージ名とモジュール名の定義
const (
mainPkg = “internalapi”
mainMod = “index”
)
func member(mod string, mem string) tokens.ModuleMember {
return tokens.ModuleMember(mainPkg + “:” + mod + “:” + mem)
}
func resourceType(mod string, res string) tokens.Type {
return tokens.Type(member(mod, res))
}
// ProviderはTerraformプロバイダをPulumi向けにラップして返す
func Provider() tfbridge.ProviderInfo {
// 既存のTerraformプロバイダのShimを取得
p := shimv2.NewProvider(internalapi.Provider())
prov := tfbridge.ProviderInfo{
P: p,
Name: “internalapi”,
DisplayName: “Internal Legacy API”,
Publisher: “YourCompany”,
LogoURL: “https://raw.githubusercontent.com/…/logo.png”,
Description: “A Pulumi provider for proprietary internal legacy systems.”,
Keywords: []s{“pulumi”, “internalapi”, “bridge”},
License: “Apache-2.0”,
Homepage: “https://example.com”,
Repository: “https://github.com/your-org/pulumi-internalapi”,
Config: map[string]tfbridge.SchemaInfo{
// APIトークンなどのグローバル設定
“api_token”: {
Secret: tfbridge.True(), // Pulumiのシークレット管理に強制的に乗せる
},
},
Resources: map[string]tfbridge.ResourceInfo{
“internalapi_server”: {
Tok: resourceType(mainMod, “Server”),
Fields: map[string]tfbridge.SchemaInfo{
// レガシーなsnake_caseをTypeScript側で自然なcamelCaseに強制変換
“ip_address”: {
Name: “ipAddress”,
},
},
},
},
DataSources: map[string]tfbridge.DataSourceInfo{
“internalapi_server”: {
Tok: resourceType(mainMod, “getServer”),
},
},
JavaScript: &tfbridge.JavaScriptInfo{
PackageName: “@your-org/internalapi”,
Dependencies: map[string]string{
“@pulumi/pulumi”: “^3.0.0”,
},
},
Golang: &tfbridge.GolangInfo{
ImportBasePath: “github.com/your-org/pulumi-internalapi/sdk/go/internalapi”,
GenerateResourceContainerTypes: true,
},
}
return prov
}
このブリッジコードをコンパイルし、ビルドスクリプトを走らせるだけで、TypeScript、Python、Go、C#の完全なSDKが爆誕する。
レガシーなTerraformプロバイダを捨て去るのではなく、Pulumiのエコシステムへと「橋渡し」するこの手法は、移行コストを劇的にゼロへと近づける。
—
3. アプローチB:Pulumi Schema駆動によるゼロからの完全カスタムプロバイダ開発
Terraformの遺産すらなく、完全に新規の社内REST APIを直接叩くプロバイダを作りたい場合、あるいはTerraformの制約(単一プロセスの限界など)から脱却したい場合は、Pulumi Schema(Pulumi Package Schema)をファーストに据えたネイティブプロバイダ開発を行う。
1. スキーマ定義(`schema.json`)の設計
Pulumiプロバイダの心臓部は、型安全なインターフェース定義である `schema.json` である。これがあらゆる言語のSDKの源泉となる。
{
“name”: “enterprise-mesh”,
“version”: “0.1.0”,
“displayName”: “Enterprise Service Mesh Internal API”,
“config”: {
“properties”: {
“endpoint”: {
“type”: “string”,
“description”: “Base URL of the internal mesh control plane.”
}
},
“required”: [“endpoint”]
},
“resources”: {
“enterprise-mesh:index:RouteRule”: {
“properties”: {
“serviceName”: {
“type”: “string”,
“description”: “Target microservice name.”
},
“weight”: {
“type”: “integer”,
“description”: “Traffic weight percentage.”
},
“routes”: {
“type”: “array”,
“items”: {
“type”: “string”
}
}
},
“required”: [“serviceName”, “weight”, “routes”]
}
}
}
2. Goによるプロバイダ本体の実装
プロバイダの実装では、PulumiのGo SDK(`github.com/pulumi/pulumi/sdk/v3/go/pulumi/provider`)を利用し、gRPCサーバーとしての振る舞いを記述する。
package main
import (
“context”
“fmt”
“net/http”
“os”
p “github.com/pulumi/pulumi/sdk/v3/go/pulumi/provider”
“github.com/pulumi/pulumi/sdk/v3/go/pulumi”
)
// プロバイダのステート構造体
type MeshProvider struct {
endpoint string
client http.Client
}
// RouteRuleリソースの実装
type RouteRule struct{}
// 実際のライフサイクルメソッド群をここにバインドしていく
// (Create, Read, Update, Deleteの各ハンドラーを実装)
func main() {
// プラグインサーバーとして起動
p.Main(“enterprise-mesh”, os.Args[1:], &p.Provider{
// スキーマやハンドラーの紐付けを定義
})
}
このアプローチの強みは、Terraformのプロバイダースペック(SDK v2 / Plugin Framework)という「余計な中間層」を排除し、自社APIの仕様に100%最適化された高速かつ軽量なプロバイダを構築できる点にある。
—
4. エキスパート向け知見:パフォーマンスチューニングとメモリ最適化ハック
最後に、大規模なインフラストラクチャ(10万リソース超)をPulumiのカスタムプロバイダで管理する際に直面する、地獄のようなボトルネックを回避するための実践的ハックを伝授する。
1. gRPCストリーミングとコネクションプーリングの徹底
カスタムプロバイダと社内APIの間で、リソースごとにHTTPコネクションを張るような愚行をしてはならない。
`Provider.Configure` の段階でコネクションプーリングを実装し、`net/http.Transport` のチューニングを施すこと。
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 time.Second,
}
client := &http.Client{Transport: transport}
2. 冪等性と楽観的ロック(Optimistic Locking)の強制
社内レガシーAPIは、多くの場合、並行リクエストに対する排他制御が脆弱である。
Pulumiの並列実行(Parallelism)機能により、同一リソースへの同時 `Create` や `Update` が飛んだ際にAPIがクラッシュすることがある。プロバイダ側の `Diff` および `Update` メソッドにおいて、リビジョン番号やETagを用いた楽観的ロック機構を必ず実装せよ。
3. メモリリークの検知とプロファイリング(pprofの導入)
プロバイダは長期稼働するプロセスではないが、数万リソースを扱う巨大な `pulumi up` の実行中、Goのガベージコレクションが追いつかずに OOM Killer に屠られるケースが多発する。
カスタムプロバイダのエントリポイントに必ず `net/http/pprof` を組み込み、デバッグ時にはメモリプロファイルを採取できるようにしておけ。
import _ “net/http/pprof”
go func() {
log.Println(http.ListenAndServe(“localhost:6060”, nil))
}()
—
結びにかえて
IaCツールにおける真の勝利とは、「用意されたプロバイダの枠内でやりくりすること」ではなく、「自社のビジネスロジックやレガシーシステムを、コードの表現力の高みへと引きずり込むこと」にある。
PulumiのBridge技術とSchema駆動開発は、そのための最も鋭利な刃物だ。
もはや「APIがあるから手動で叩く」「シェルスクリプトでラップする」という言い訳は通用しない。すべてのインフラストラクチャを、厳密な型と宣言的パイプラインの支配下に置くのだ。プログラマとしての矜持を持ち、自社インフラの深淵をコードで完全に掌握せよ。