金曜日を台無しにした3,000行のYAMLファイル
6ヶ月前、私たちのCI/CDパイプラインは、たった一つの3,000行あるYAMLファイルによって停止しました。当時、私のチームは3つの異なる環境にわたって約40のマイクロサービスを管理していました。Gitリポジトリは、冗長なマニフェストの墓場のようになっていました。
新しい環境変数が必要になるたびに、何十ものファイル間で手動でコードを同期しなければなりませんでした。あの金曜日、ConfigMap内のわずか2スペースのインデントミスがリンターをすり抜け、ステージング環境のロジックを破壊したのです。私たちは空白エラー’のデバッグだけに4時間を費やしました。
これが「YAML地獄(YAML Hell)」の限界点です。Kubernetesは世界クラスのオーケストレーターですが、YAMLはデータシリアライズ形式であり、プログラミング言語ではありません。ループ、ロジック、意味のある抽象化、>そして強い型付けといった、エンジニアが何十年も信頼してきた基本的なツールが欠けているのです。
なぜ大規模環境でYAMLは壁にぶつかるのか
問題はKubernetesそのものではなく、私たちの「意図」を表現する方法にあります。生のYAMLを書くのは、設計図なしにハンマーだけで摩天楼を建てるようなものです。インフラがスケールするにつれ、3つの大きな痛みが現れます。
- 抽象化の欠如: 10個のサービスが設定の90%を共有していても、結局はほぼ同一の10個のファイルをメンテナンスすることになります。この重複は、設定のドリフト(不整合)を招く原因となります。
- 型安全性の欠如:
cpu: "500"という無効な文字列を記述しても、クラスターがそれを拒否するまでIDEは警告してくれません。巨大なマニフェストの中では、こうした小さなエラーはデプロイされるまで隠れたままです。 - ボイラープレートの疲弊: 標準的なDeploymentやServiceを作成するためだけに、100行もの繰り返しのテキストを書く必要はないはずです。
Helm vs. Kustomize vs. CDK8s:勝者はどのツールか?
私たちはすぐにCDK8sに飛びついたわけではありません。まずは業界標準のツールをテストしました。実際のプロダクションワークフローでの評価は以下の通りです。
1. Helm
Helmは公開パッケージの配布には優れていますが、Goテンプレートに大きく依存しています。YAMLと {{ if .Values.enabled }} が混在すると、デバッグが極めて困難な「スパゲッティコード」が生まれます。ロジックが複雑になると、テンプレートは解読不能になります。
2. Kustomize
Kustomizeはパッチを使用して値を上書きします。これは kubectl ネイティブであるため素晴らしい手法です。しかし、あくまで宣言的なままです。もし20個のマイクロサービスのリストに対してそれぞれServiceを生成したい場合、結局20個の手動エントリを書くことに戻ってしまいます。
3. CDK8s (Cloud Development Kit for Kubernetes)
CDK8sは、TypeScript、Python、またはJavaでリソースを定義できるようにすることで、ゲームのルールを変えます。コードをコンパイルすると、標準的で有効なKubernetes YAMLが出力されます。Kubernetesが期待するマニフェストを出力しながら、クラス、ループ、条件分岐といった機能を利用できます。これはソフトウェアエンジニアリングと運用の架け橋となります。
移行:CDK8sへの転換
1ヶ月のテストを経て、コアサービスをTypeScriptを用いたCDK8sに移行しました。この転換により、私たちは「YAML編集者」から「プラットフォームエンジニア」へと進化しました。静的なデータを管理する代わりに、再利用可能なインフラライブラリを構築し始めたのです。
環境構築
始めるには、CDK8s CLIが必要です。最初のセットアップにはNode.jsをお勧めします。VS CodeでのTypeScriptのオートコンプリートは、深い階層のKubernetesスキーマを扱う際に非常に役立つからです。
# CLIをグローバルにインストール
npm install -g cdk8s-cli
# 新しいプロジェクトディレクトリを初期化
mkdir k8s-infrastructure && cd k8s-infrastructure
cdk8s init typescript-app
コピペをロジックに置き換える
ブロックを複製する代わりに、ロジックを再利用可能なクラスにラップできます。以下は、標準的なウェブサービスの定義を一つのTypeScriptコンストラクトに凝縮した実際の例です。
import { Construct } from 'constructs';
import { App, Chart, ChartProps } from 'cdk8s';
import { KubeDeployment, KubeService, IntOrString } from './imports/k8s';
export class WebService extends Construct {
constructor(scope: Construct, id: string, port: number, image: string) {
super(scope, id);
const label = { app: id };
new KubeService(this, 'service', {
spec: {
type: 'ClusterIP',
ports: [ { port, targetPort: IntOrString.fromNumber(port) } ],
selector: label
}
});
new KubeDeployment(this, 'deployment', {
spec: {
replicas: 2,
selector: { matchLabels: label },
template: {
metadata: { labels: label },
spec: {
containers: [
{ name: 'web', image: image, ports: [ { containerPort: port } ] }
]
}
}
}
});
}
}
サービスのインスタンス化
これで、新しいサービスの追加は一行で済むようになりました。設定オブジェクトをループさせて、複数のデプロイを一瞬で生成できます。
class MyChart extends Chart {
constructor(scope: Construct, id: string, props: ChartProps = { }) {
super(scope, id, props);
const apps = [
{ name: 'auth-api', port: 8080, image: 'myorg/auth:v1.2.4' },
{ name: 'cart-api', port: 8081, image: 'myorg/cart:v2.1.0' },
{ name: 'catalog-api', port: 8082, image: 'myorg/catalog:v1.0.5' }
];
apps.forEach(app => {
new WebService(this, app.name, app.port, app.image);
});
}
}
const app = new App();
new MyChart(app, 'production');
app.synth();
cdk8s synth を実行すると、このスクリプトは完璧にフォーマットされた単一のYAMLファイルを生成します。もしすべてのサービスのレプリカ数を2から5に更新する必要があれば、40個のファイルを編集するのではなく、クラス内の一行を変更するだけで済みます。
結果:コード量は70%削減、信頼性は100%向上
これをプロダクション環境で6ヶ月運用した結果、効果は数値として現れました。インフラコードの総行数を70%以上削減できました。TypeScriptコンパイラが開発中にエラーをキャッチするため、構文エラーは実質的に消失しました。誤ってCPU制限のフィールドに文字列を渡してしまっても、IDEが即座に赤線でエラーを表示してくれます。
技術的に最大の収穫は、ユニットテストでした。現在、私たちはJestを使用してインフラを検証しています。YAMLが生成される前に、すべてのDeploymentに特定のセキュリティコンテキストやリソース制限が含まれていることを、プログラムでアサート(検証)できます。このレベルのバリデーションは、生のYAMLやHelmではほぼ不可能です。
最後に
CDK8sを採用することは、Kubernetesを捨てることではなく、ようやくインフラを「ソフトウェア」として扱い始めることを意味します。もしHelmチャートが解読不能になっていたり、YAMLファイルが数千行に膨れ上がっているなら、切り替えの時期かもしれません。初期設定には少し手間がかかりますが、長期的なメンテナンスコストの削減とデプロイに対する自信の向上は、その投資に見合う価値があります。

