TypeScriptでManifestを生成するGeneratorのアーキテクチャ

TypeScriptでManifestを生成するGeneratorのアーキテクチャ #

アーキテクチャが解決すること #

そもそも Generator そのものが解決することは manifest をドキュメントの乖離を防ぎ、YAMLの記法のぶれなどを防ぐことです。 アーキテクチャが解決しなければいけないことは、具体的には次のようなことが挙げられます。

  1. マニフェスト自体のスケーラビリティを確保する
  2. 実際に運用する際に必要最小限の変更だけで Manifest を更新できる ≒ 宣言的な変更で済むようにする
  3. マイクロサービス単位で設定の変更ができる(CPU/MEM/replicas など)
  4. 管理しているマイクロサービス全体のリソース量、変更時の増減が把握できる
  5. Manifest ファイルの命名規則、出力先のディレクトリ・ファイルツリーなどを意識しなくても良い
  6. Generator 自体の保守性を高める

これらを表現するためのアーキテクチャはStatic Site GeneratorやYeoman、Cookiecutter、Rails Scaffoldなどたくさん事例があります。 これらの基本的な骨格をKubernetesのManifest Generatorとして応用し次のようなアーキテクチャが設計しました。

Manifest生成のためのアーキテクチャ

それぞれの役割を紹介します。

名称役割
User Configバージョン変更など最小限の変更を与えるファイル
Kubernetes TypeDefinitionTypeScriptの型定義
MicroService Templateマイクロサービスの種類に応じたテンプレート
DefinitionNamespace名やPort番号、Gatewayの Host 名などの不動値の定義
ResourceParameterMicroService Templateを Kubernetes のリソースコンポーネント単位で結合する
FactoryResourceをどのファイル名でどのグループで出力するか定義する
WriterFactory から与えられた情報から Kubernetes の Manifest や、CPU Requests などのレポートを生成する

具体的な実装例 #

実装サンプルを以下のリポジトリに用意しました。nodejspnpmを利用したサンプルとなっています。 Docker Swarmを利用すればArgo Rollouts + Istioがデプロイできるところまで確認しています。

NamePATH
User Configconfig/*.json
Kubernetes TypeDefinitionsrc/k8s/*
MicroService Templatesrc/templates/*
Definitionsrc/definitions/*
Factorysrc/factory/*/index.ts
Resourcesrc/factory/*/resource.ts
Writersrc/writer/*

依存関係はsverweij/dependency-cruiserのカスタムルールによってテストしています。

Writerが出力するファイルは以下の通り。

特徴的なこと #

ConfigMapの更新した後にPodを再起動する #

例えばDeploymentがConfigMapの設定によって動作を変化させるような場合、ConfigMapだけを更新してもロールアウトは発生しません。

Deploymentのロールアウトは、DeploymentのPodテンプレート(この場合.spec.template)が変更された場合にのみトリガーされます。例えばテンプレートのラベルもしくはコンテナーイメージが更新された場合です。Deploymentのスケールのような更新では、ロールアウトはトリガーされません。

引用: Deploymentの更新

これを対処するには例えばConfigMapのContentHashを計算して、 それをPod TemplateのAnnotationに付与することでConfig MapとDeploymentの関係性を作れます。

import { createHash } from "crypto";

export const createContentHash = (text: string): string => {
  const hash = createHash("md4");
  hash.update(text);
  return hash.digest("hex");
};
kind: Deployment
spec:
  template:
    metadata:
      annotations:
        # Content Hashの値は依存するConfigMapに対して計算する。
        dependency.config-map.content-hash: bf84e528eaedf3fd7c3c438361627800

これのApplyの順番はArgo CDのSync Waveを利用すると簡単に制御できます。

Reportを作成するスクリプトはNonNull Assertionを許可する #

TypeScriptで書いてると厳密に型定義を守ることが開発の安全に繋がりますが、Writerの種類によっては2つの理由でこれを許容します。

  1. 細かく定義するコストが高い
  2. レポートとして自動生成するようなパラメーターは"そもそも必須"であるため、マニフェスト生成時にエラーになってくれたほうが良い

前者は消極的な理由ですが、後者は先程紹介した実装内でExceptionを発生させると同じ意味合いを持っています。 つまり、obj.a?.b?.cで参照するよりもobj.a!.b!.c!で参照すると、型チェックしてthrow new Errorをする手間が省ける算段です。 もしくは、生成されたレポートがおかしな状態になるのでレビューで簡単に防ぐことができます。

Manifest生成はどう使うのがよいか? #

リポジトリ運用について #

namespace単位で管理するのが楽でしょう。ただし、機密情報がある場合はsecretだけまとめたリポジトリを別途切るのは必要です。 namespace内は基本的に競合する.metadata.nameを作ることはできません、加えて仮に同じ名前にしても管理が複雑になります。

ツールについて #

ここで紹介したのは、愚直にKubernetesなどが提供しているOpenAPI Schemaから型定義を生成したものを利用した例でした。 KubernetesのドキュメントにはREST API経由でKubernetes APIをCallするClient Libraryとしていくつか紹介されています。

これを純粋にREST APIのClientとして使うだけでなく、Manifestを生成するために役立てることも可能でしょう。 YAMLで書くには複雑になりすぎた場合に、チームで使い慣れた言語で記述する選択肢も用意されているので一考する価値はあるでしょう。

Generatorを辞めたくなったら #

YAMLだけ残して後の実装はさっぱり捨ててしまいましょう。YAMLだけあればKubernetesにデプロイは可能ですから。