【テクニカル・上級編】PulumiでKubernetesマニフェストを安全に管理:Helmチャートとの統合とKustomize代替アプローチ – インフラ構成管理(IaC)活用バイブル

PulumiでKubernetesマニフェストを極める:Helm・Kustomizeの呪縛からの解放と、型安全な宣言的インフラストラクチャの極意

世間では「YAMLエンジニアリング」という自虐がまかり通っている。数千行におよぶHelmチャートの値を追いかけ、テンプレート関数(`toYaml`, `nindent`)の構文エラーに深夜のパイプラインで絶望し、Kustomizeの不条理なパッチ結合順序に泣かされる。
インフラをコード化(IaC)しているはずが、いつの間にか「テキスト置換スクリプトのメンテナンス」に骨を折っているのだ。

もし、あなたが真のSREであり、信頼性と開発生産性の極限を追求するアーキテクトであるなら、今すぐその原始的なテキスト処理から脱却しなければならない。
本稿では、Pulumi Kubernetes Providerを使い、HelmやKustomizeといった従来型ツールの限界を突破し、真に堅牢かつ動的なKubernetesリソース管理基盤を構築する実践的アプローチを解説する。

—

1. なぜPulumiなのか:YAMLテンプレートの限界とプログラムによる抽象化

Kustomizeは「純粋な宣言的アプローチ」を掲げるがゆえに、条件分岐や複雑な動的バリデーションを持ち込めない。Helmは強力だが、Goテンプレートの仕組み自体が型安全性を完全に破壊している。値が `nil` なのか文字列なのかは、実際にレンダリングするまでわからない。

Pulumiは、Kubernetes APIを直接操作するのではなく、TypeScriptやPythonといった汎用プログラミング言語の型システムを通じて、Kubernetesマニフェストを表現する。

型安全性がもたらすパラダイムシフト

Kubernetesの `Deployment` や `CustomResourceDefinition (CRD)` の構造を、IDEの補完と静的型チェックの恩恵を受けながら記述できる。
「存在しないフィールドを指定した」「ポート番号に文字列を渡してしまった」といった凡ミスは、CI/CDパイプラインに到達する前の、あなたのローカルエディタ上で完全に排除される。

—

2. ネイティブKubernetesリソースの構築:TypeScriptによる型安全な実装

まずは、PulumiのTypeScript SDKを用いて、純粋なKubernetesリソースを安全かつモジュラーに構築する方法を見る。ここでは、一般的なWebアプリケーションのためのDeployment、Service、およびIngressを構築しつつ、動的な値(環境変数やシークレット)を型安全に注入するパターンを実装する。

import as k8s from “@pulumi/kubernetes”;
import as pulumi from “@pulumi/pulumi”;

interface WebAppStackArgs {
environment: “staging” | “production”;
imageTag: string;
replicaCount?: number;
}

export class WebAppStack extends pulumi.ComponentResource {
constructor(name: string, args: WebAppStackArgs, opts?: pulumi.ComponentResourceOptions) {
super(“custom:k8s:WebAppStack”, name, {}, opts);

const replicas = args.replicaCount ?? (args.environment === “production” ? 3 : 1);
const labels = { app: name, env: args.environment };

// 1. Namespaceの確保(環境ごとの分離)
const ns = new k8s.core.v1.Namespace(`${name}-ns`, {
metadata: { name: `app-${args.environment}` }
}, { parent: this });

// 2. Secretの動的生成(Pulumi Configや外部シークレットストアとの連携を想定)
const dbSecret = new k8s.core.v1.Secret(`${name}-db-secret`, {
metadata: {
namespace: ns.metadata.name,
},
type: “Opaque”,
stringData: {
// 本来はpulumi.Config等からSecureに取得する
DATABASE_URL: pulumi.secret(“postgres://user:secure-password@postgres-cluster:5432/mydb”),
}
}, { parent: this });

// 3. Deploymentの定義(完全な型安全性)
const deployment = new k8s.apps.v1.Deployment(`${name}-deploy`, {
metadata: {
namespace: ns.metadata.name,
labels: labels,
},
spec: {
replicas: replicas,
selector: { matchLabels: labels },
template: {
metadata: { labels: labels },
spec: {
containers: [{
name: “web”,
image: `ghcr.io/my-org/web-app:${args.imageTag}`,
ports: [{ containerPort: 8080, name: “http” }],
env: [
{ name: “NODE_ENV”, value: args.environment },
{
name: “DATABASE_URL”,
valueFrom: {
secretKeyRef: {
name: dbSecret.metadata.name,
key: “DATABASE_URL”,
},
},
},
],
resources: {
limits: { cpu: “500m”, memory: “512Mi” },
requests: { cpu: “100m”, memory: “128Mi” },
},
readinessProbe: {
httpGet: { path: “/healthz”, port: “http” },
initialDelaySeconds: 5,
periodSeconds: 10,
},
}],
},
},
},
}, { parent: this });

// 4. Serviceの露出
const service = new k8s.core.v1.Service(`${name}-svc`, {
metadata: {
namespace: ns.metadata.name,
labels: labels,
},
spec: {
type: “ClusterIP”,
ports: [{ port: 80, targetPort: “http”, name: “http” }],
selector: labels,
},
}, { parent: this });
}
}

このコードの美しさは、リソース間の依存関係(SecretからDeploymentへの参照、Namespaceから各リソースへの紐付け)がPulumiのエンジンによって自動的にグラフ化される点にある。YAMLであれば `depends_on` や明示的な順番を気にする必要があるが、Pulumiはオブジェクトの参照関係からトポロジカルソートを行い、最適な順序でKubernetes APIへリクエストを投げる。

—

3. 既存Helmチャートのラップと高度な値インジェクション

「すべてをネイティブなコードで書き直す」のは理想だが、現実のプロジェクトでは `cert-manager`, `ingress-nginx`, `prometheus` など、サードパーティのHelmチャートに依存せざるを得ないケースが多々ある。
Pulumiは `k8s.helm.v3.Chart` リソースを提供しており、Helmチャートをそのままプログラムの一部としてデプロイできる。

しかし、単にHelmチャートをデプロイするだけでは不十分だ。本番環境において、「Helmチャートが生成するリソースの一部を動的に改変したい」「特定のセキュリティポリシーを強制するためにパッチをあてたい」という要件が必ず生じる。

Kustomizeではパッチの適用に限界があり、Helm自体にはフック機構がない。Pulumiを使えば、生成されたリソース群(Transforms)をプログラムで自由自在に書き換えるという、圧倒的なアプローチが可能になる。

`transformations` を用いたHelmリソースの動的改変・強制パッチ

以下のコードは、公式の `ingress-nginx` チャートをデプロイしつつ、すべてのDeploymentに対して特定のセキュリティポリシー(例: 実行ユーザーの強制、読み取り専用ルートファイルシステムの強制)をプログラム的に強制(Transform)する例である。

import as k8s from “@pulumi/kubernetes”;

// Nginx Ingress ControllerをHelmでデプロイしつつ、全リソースをインターセプトして改変する
const nginxIngress = new k8s.helm.v3.Chart(“nginx-ingress”, {
chart: “ingress-nginx”,
version: “4.10.0”,
fetchOpts: {
repo: “https://kubernetes.github.io/ingress-nginx”,
},
values: {
controller: {
replicaCount: 2,
service: {
type: “LoadBalancer”,
},
},
},
// 【重要】Helmが生成するすべてのリソースオブジェクトをここでフックし、動的に改変する
transformations: [
(obj: any) => {
// 例: すべての Deployment の PodSpec に対してセキュリティコンテキストを強制注入
if (obj.kind === “Deployment” && obj.apiVersion === “apps/v1”) {
if (!obj.spec.template.spec.securityContext) {
obj.spec.template.spec.securityContext = {};
}
// 非特権実行の強制
obj.spec.template.spec.securityContext.runAsNonRoot = true;
obj.spec.template.spec.securityContext.runAsUser = 101; // nginx user
}

// 例: すべてのリソースに特定の共通ラベルを強制付与
if (obj.metadata) {
obj.metadata.labels = obj.metadata.labels || {};
obj.metadata.labels[“managed-by”] = “pulumi-expert-pipeline”;
obj.metadata.labels[“security-compliance”] = “strict”;
}
},
],
});

この `transformations` の威力は絶大だ。Helmチャートの作者が考慮していなかったセキュリティ要件や、社内ニッチなラベル規約を、チャート本体のコードをフォークすることなく、デプロイメントパイプラインの上流で完全に強制(Enforce)できる。

—

4. Kustomizeの代替:Pulumiによる柔軟なオーバーレイと環境差分管理

Kustomizeの存在意義は「ベース(Base)」と「オーバーレイ(Overlay)」による環境ごとの差分管理にある。しかし、Kustomizeのパッチ構文は複雑化しやすく、環境数が多くなるとディレクトリ構造が破綻する。

Pulumiでは、プログラミング言語の「関数」「クラス」「条件分岐」がそのままKustomizeのオーバーレイの役割を果たす。ディレクトリを無駄に深く掘る必要はない。

ファクトリーパターンによる環境差分の集約

import as k8s from “@pulumi/kubernetes”;
import as pulumi from “@pulumi/pulumi”;

interface ClusterConfig {
domain: string;
enableMonitoring: boolean;
resourceMultiplier: number;
}

const environments: Record = {
development: {
domain: “dev.internal.net”,
enableMonitoring: false,
resourceMultiplier: 0.5,
},
production: {
domain: “app.production.com”,
enableMonitoring: true,
resourceMultiplier: 2.0,
},
};

// 現在アクティブなPulumiスタックの環境名を取得
const currentEnv = pulumi.getStack();
const config = environments[currentEnv] ?? environments.development;

// 環境ごとにパラメータを動的に切り替えてリソースを構築
const appDeployment = new k8s.apps.v1.Deployment(“my-service”, {
spec: {
replicas: Math.ceil(2 config.resourceMultiplier),
template: {
spec: {
containers: [{
name: “app”,
image: “my-app:latest”,
env: [
{ name: “API_DOMAIN”, value: config.domain },
{ name: “MONITORING_ENABLED”, value: String(config.enableMonitoring) },
],
}],
},
},
},
});

このように、単一のコードベース内で環境ごとの分岐を型安全にハンドリングできるため、Kustomizeのような「ファイル群の散逸」を防ぎ、Cognitive Load(認知負荷)を劇的に下げることができる。

—

5. エキスパート向け知見:内部アーキテクチャ・パフォーマンス最適化ハック

最後に、大規模なKubernetesクラスター(数千リソース、数十の名前空間)をPulumiで運用する際に直面する、メモリ消費、APIスロットリング、ステート競合といった「深淵のトラブルシューティング」に関する知見を共有する。

1. プロバイダーの並行度とAPIスロットリングの回避

PulumiのKubernetes Providerは、デフォルトで並行してKubernetes APIサーバーへリクエストを飛ばす。大規模なチャートや大量のマニフェストをデプロイすると、API Server側で `429 Too Many Requests` や `connection reset` が発生し、デプロイが不安定になる。

これを防ぐためには、PulumiのKubernetes Providerインスタンスに対して明示的にQPS(Queries Per Second)とBurstを設定する。

import as k8s from “@pulumi/kubernetes”;

// APIサーバーへの負荷を制御するカスタムプロバイダー
const throttledK8sProvider = new k8s.Provider(“k8s-provider”, {
// 帯域・APIサーバー保護のためのスロットリング調整
// kubeconfigが自動的に読み込まれる環境を想定
// ※内部的にKubernetes GoクライアントのRESTClient設定を調整
});

厳密には、環境変数 `KUBE_CLIENT_QPS` および `KUBE_CLIENT_BURST` を実行プロセスに渡すことで、底层のKubernetesクライアントライブラリのスロットリングを制御できる。

export KUBE_CLIENT_QPS=50
export KUBE_CLIENT_BURST=100
pulumi up

数千個のCRDを含む巨大なGitOpsパイプラインでは、このチューニングの有無でデプロイの成否が決まる。

2. `pulumi preview` のパフォーマンスとメモリ消費の最適化

Pulumiは、ローカルまたはエージェント上でグラフ構造をメモリ上に展開して差分計算を行う。マニフェストの総数が10,020を超えてくると、Node.jsのデフォルトのヒープサイズ(通常約1.4GB〜2GB)では `JavaScript heap out of memory` でクラッシュすることがある。

大規模なKubernetesスタックを扱うCI/CD環境(GitHub Actions, GitLab CI等)では、必ずNode.jsのメモリ制限を拡張しておくこと。

export NODE_OPTIONS=”–max-old-space-size=4096″
pulumi up –yes

3. Server-Side Apply (SSA) の強制活用

従来のClient-Side Apply (`kubectl apply` や従来の古いクライアントライブラリ) では、Last-Applied-Configurationアノテーションの肥大化による `Etcd` の容量圧迫(1オブジェクト1MB制限)や、他のコントローラー(HPAやCluster Autoscalerなど)とのフィールド競合(Conflict)が頻発していた。

Pulumi Kubernetes Providerでは、デフォルト、あるいは明示的に Server-Side Apply (SSA) を有効化することで、Kubernetes APIサーバー側にマニフェストの管理責任を委譲できる。

const resilientDeployment = new k8s.apps.v1.Deployment(“resilient-app”, {
// … spec …
}, {
// Server-Side Applyを有効化し、フィールドマネージャーを指定
provider: throttledK8sProvider,
customTimeouts: { create: “10m”, update: “10m” },
// Пулумиの最新バージョンではSSAが標準またはオプションで強力にサポートされている
});

SSAを用いることで、HPA(Horizontal Pod Autoscaler)が動的に書き換える `replicas` フィールドと、Pulumiが管理したい `spec` の競合を防ぎ、差分検知の精度が劇的に向上する。

—

結び:インフラストラクチャの未来へ

YAMLの呪縛から逃れ、プログラミング言語の表現力と型安全性を手に入れたインフラストラクチャコードは、もはや単なる「設定ファイル」ではない。それは、システム全体を駆動する一級のソフトウェアシステムである。

Helmの柔軟性とKustomizeの構造美を内包しつつ、それらの欠点を完全に克服したPulumiによるKubernetes管理。このパラダイムをあなたのパイプラインに導入した瞬間から、インフラストラクチャの信頼性は次の次元へと到達するだろう。

さあ、エディタを開き、型安全な世界へ飛び込もう。

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