cdk8s をもっと使いこなす - kintone AI チームの活用 Tips

この記事は kintone の生成 AI チームで連載中の kintone AIリレーブログ 2026 の 6 本目の記事です。 リレーブログでは、生成 AI チームのメンバーが AI トピックに限らずさまざまなことについて発信していきます。


こんにちは!

kintone 生成 AI チームの 386jp です。

前回の記事「cdk8s を使ってみた! - TypeScript で Kubernetes を管理する実践 Tips」では、 cdk8s を導入した背景と実感したメリットを紹介しました。

今回は、より実践的な内容として、私たちのチームが cdk8s を使う上で工夫しているパターンを詳しく紹介します。

目次:

前回のおさらい

前回の記事では、 cdk8s の概要と、 TypeScript で Kubernetes マニフェストを管理するメリットを紹介しました。

今回は、私たちのチームが実際にどのような工夫をしているか、具体的な実装パターンを紹介します。

kintone AI チームでの活用

私たちのチームでは、複数環境への対応、再利用可能なコンポーネント化を意識して、以下のような構成を採用しています。

まずはディレクトリ構成から、全体像を見てみましょう:

our-awesome-cdk8s-project
├── config # 設定ファイルの保存場所
├── resources # CRD の TypeScript 定義を配置
└── src
    ├── apps # k8s マニフェストの内容を記述
    ├── core # k8s マニフェストの内容を記述
    ├── config # 設定ファイルの parse など
    ├── resources # 再利用できそうなコンポーネントをまとめたもの
    ├── test
    └── util

それでは、各ディレクトリの役割と使い方を詳しく見ていきます。

core と apps によるコンポーネント管理

チームでは、 Kubernetes (EKS) クラスタを走らせるのに必要不可欠なコンポーネントは core 、その上で実行する各種アプリケーションは apps と定義し、分離して管理しています。

core には、 EKS でワークロードを実行する node の設定を行っている Karpenter や、 Argo CD の設定、メトリクス関連などのスタックを入れています。 apps としては、これまで Lambda 上で動かしていた AI Service を Kubernetes に移行して配置しています(移行の経緯については「kintone AI でも Kubernetes はじめました」をご覧ください)。今後の開発でここのスタックは増えていく予定です。

ディレクトリ構成としては以下のようになっています:

our-awesome-cdk8s-project
└── src
    ├── apps
    │   ├── ai-service
    │   │   └── index.ts
    │   └── index.ts
    └── core
        ├── app-of-apps
        │   ├── const.ts
        │   └── index.ts
        ├── karpenter
        │   ├── index.ts
        │   └── nodepool
        │       └── general-purpose.ts
        ├── opentelemetry-collector
        │   ├── const.ts
        │   └── index.ts
        └── index.ts

各 core, apps に配置しているスタックは、それぞれが独立した Argo CD Application として EKS 上にデプロイされています。 App-of-apps パターンを採用し、 core/app-of-apps スタック内で、 src/{core, apps}/index.ts に import されたスタックに対して Argo CD Application のマニフェストを動的に生成しています。この際、 core, apps に分離されているため、 argocd.argoproj.io/sync-wave アノテーションを活用することで、 sync 順序を制御しています。

config ディレクトリ: 設定ファイルを簡単に管理する

私たちのチームでは、cdk8s 上の設定を簡単に修正できるように、運用時に柔軟に調整したい値や、 CI/CD や AWS CDK からの値のやり取りのために、 yaml ファイルに設定値の一部を記載する方針を取りました。リポジトリトップに config というディレクトリを用意し、そこに設定用の yaml ファイルを保存。そしてその yaml ファイルを読み込み、 zod の parse をかける処理を src/config 配下で行っています。

具体的なディレクトリ構成は以下のようになっています:

our-awesome-cdk8s-project
├── config
│   ├── apps
│   │   └── ai-service
│   │       ├── common.yaml
│   │       ├── dev.yaml
│   │       ├── staging.yaml
│   │       └── prod.yaml
│   ├── core
│   │   ├── app-of-apps
│   │   │   ├── common.yaml
│   │   │   └── dev.yaml
│   │   ├── karpenter
│   │   │   ├── dev.yaml
│   │   │   ├── staging.yaml
│   │   │   └── prod.yaml
│   │   └── opentelemetry-collector
│   │       ├── common.yaml
│   │       └── prod.yaml
│   ├── global
│   │   ├── common.yaml
│   │   ├── dev.yaml
│   │   ├── staging.yaml
│   │   └── prod.yaml
│   └── versions
│       └── common.yaml
└── src
    ├── config
    │   └── types.ts
    └── main.ts

config ディレクトリの内容ですが、 apps と core にはそれぞれのコンポーネントの設定値が入っています。 global にはコンポーネント関係なく、 cluster-wide で使う設定値が記載されています。 EKS のサブネットや、 AWS のアカウント情報などが入っています。 versions には、各マニフェストで使用しているイメージのバージョンや、 Helm Chart のバージョン情報が記載されています。 チームでは、 Renovate を採用してイメージ情報などを更新しており、 Custom Manager を使ってイメージ情報の更新をしています。この Custom Manager を使ったバージョン更新に関しては、このブログリレーシリーズで後日紹介する予定です。

各設定ファイルは、 common.yaml{dev, staging, prod}.yaml の大きく 2 種類のファイルで構成されています。 common.yaml には、全環境で統一して付与している設定値を記載し、 {dev, staging, prod}.yaml では各環境固有の設定値を記載しています。例えば Pod のリソース要求など、環境ごとに柔軟に調整したい一方で、全環境の yaml に個別に記載するのは管理上煩雑になる設定値があります。このような場合、 common.yaml を活用することで、「基本的には共通の値を使い、 prod のみ異なる値を設定する」といった管理が簡単に行えます。

src/config 配下には、 zod の型情報を配置し、 main.ts で各環境の yaml ファイルを読み込み、各 cdk8s Chart / App に props として渡しています。 zod parse を行うことにより、 yaml ファイル上の設定ミスにも早期に気付けるようになります。

yaml ファイルにすることで、 yq をはじめとするツールを使い、簡単にファイル操作ができるため、 CI/CD 上での設定値の変更が行えます。 EKS を運用すると、どうしても AWS スタックの ARN を指定する場面があり、そのときに yaml ファイルだと簡単に ARN 指定のための設定ファイル修正ができ便利です。

resources ディレクトリ: CRD を TypeScript の世界に取り込む

せっかく TypeScript を活用しているので、カスタムリソースを使う際も、型安全に記述したいものです。

cdk8s には、 cdk8s import コマンドがあり、 CRD のマニフェストを読み込ませると、 TypeScript 上の定義として、 .ts ファイルを API group ごとに生成することができます。

ディレクトリ構成は以下のようになっています:

our-awesome-cdk8s-project
└── resources
    └── imports
        ├── argoproj.io.ts
        ├── k8s.ts
        └── karpenter.sh.ts

チームでは、 Helm Chart も使用しているため、 Helm Chart 内に CRD が含まれていることがあります。そこで、一旦マニフェストを 1 つのファイルにまとめて書き出し、 CRD を集めて cdk8s import コマンドで TypeScript 側に取り込むようにしています。

cdk8s import は、同じ API グループの CRD を 1 つの TypeScript ファイルにまとめる仕様になっています。そのため、事前に CRD を 1 つのファイルに集約してからインポートすることで、 API グループごとに適切にファイルが生成されるようにしています。

TypeScript 上の定義の出力先に関しては、 cdk8s.yaml という cdk8s の設定ファイル内に importDirectory というキーでインポート先の path を入力することで設定が可能です。私たちの場合は、 resources/imports を指定しています。

公式ドキュメントには、 cdk8s.yaml に CRD のマニフェストが配置されているパスを書き込んで cdk8s import する方法が紹介されていたりしますが、先程紹介した version 情報を記載した yaml において、 Helm Chart のバージョンを上げたときに、自動的に TypeScript 上の CRD 定義も追尾してほしいので、 cdk8s で生成されたマニフェストに含まれる CRD をすべて動的に取り込む手法を採用しています。

具体的には、以下のように dist フォルダから CRD を集めてインポートしています:

find dist -name '*crd*.yaml' -exec cat {} \; -exec echo '---' \; | cdk8s import --save=false /dev/stdin

/dev/stdin を指定することで標準入力から CRD を取り込み、 --save=false を付与することでインポート元のパスを cdk8s.yaml に保存せず、動的に CRD を取り込めるようにしています。

src/resources ディレクトリ: よく使うマニフェスト定義をまとめる

cdk8s の利点は、 Construct を活用して、よく使うマニフェスト定義をまとめられるところにあると思います。現在は主に、 Pod 間通信をはじめとする Network Policy 関連の設定をまとめており、これにより、マニフェストの設定を書くスピードが大幅に上がっています。

例えば、 Pod 間で通信を行うための Network Policy だと、 Construct としてまとめておくことで、 Network Policy を 2 つ定義することなく、以下の数行さえ書けば Network Policy が適用できてしまいます。

new PodToPodNetworkPolicy(this, "allow-hoge-to-fuga", {
    namespace: "random-namespace",
    namePrefix: "allow-hoge-to-fuga",
    sourceAppNames: ["hoge"],
    targetAppNames: ["fuga"],
    port: 8000,
});

PodToPodNetworkPolicy の実装例を見る

import { Construct } from "constructs";
import { NetworkPolicy, NetworkPolicyPort, Pods } from "cdk8s-plus-33";

export interface PodToPodNetworkPolicyProps {
    namespace: string;
    namePrefix: string;
    sourceAppNames: string[];
    targetAppNames: string[];
    port: number;
}

/**
 * Pod 間通信を許可する NetworkPolicy クラス(Egress と Ingress の両方を作成)
 */
export class PodToPodNetworkPolicy extends Construct {
    public readonly egressPolicy: NetworkPolicy;
    public readonly ingressPolicy: NetworkPolicy;

    constructor(scope: Construct, id: string, props: PodToPodNetworkPolicyProps) {
        super(scope, id);

        const { namespace, namePrefix, sourceAppNames, targetAppNames, port } =
            props;

        // Egress NetworkPolicy: source から target への通信を許可
        this.egressPolicy = new NetworkPolicy(this, "egress", {
            metadata: {
                namespace,
                name: `${namePrefix}-egress`,
            },
            selector: Pods.select(this, "source-pods", {
                expressions: [
                    {
                        key: "app.kubernetes.io/name",
                        operator: "In",
                        values: sourceAppNames,
                    },
                ],
            }),
            egress: {
                rules: [
                    {
                        ports: [NetworkPolicyPort.tcp(port)],
                        peer: Pods.select(this, "target-pods-for-egress", {
                            expressions: [
                                {
                                    key: "app.kubernetes.io/name",
                                    operator: "In",
                                    values: targetAppNames,
                                },
                            ],
                        }),
                    },
                ],
            },
        });

        // Ingress NetworkPolicy: target が source からの通信を受け入れ
        this.ingressPolicy = new NetworkPolicy(this, "ingress", {
            metadata: {
                namespace,
                name: `${namePrefix}-ingress`,
            },
            selector: Pods.select(this, "target-pods-for-ingress", {
                expressions: [
                    {
                        key: "app.kubernetes.io/name",
                        operator: "In",
                        values: targetAppNames,
                    },
                ],
            }),
            ingress: {
                rules: [
                    {
                        ports: [NetworkPolicyPort.tcp(port)],
                        peer: Pods.select(this, "source-pods-for-ingress", {
                            expressions: [
                                {
                                    key: "app.kubernetes.io/name",
                                    operator: "In",
                                    values: sourceAppNames,
                                },
                            ],
                        }),
                    },
                ],
            },
        });
    }
}

例えば、新しい AI Agent をデプロイする際も、 LangChain や Mastra などのフレームワークごとに共通のマニフェストパターンを Construct として定義しておけば、簡単に展開できます。このような使い方も今後検討しています。

まとめ

本記事では、 kintone 生成 AI チームが cdk8s を使う上で工夫している実践パターンを紹介しました。

  • core/apps の分離: インフラコンポーネントとアプリケーションを分けて管理
  • YAML による設定管理: 環境ごとの差異を柔軟に管理
  • CRD の型安全な扱い: TypeScript の型定義として取り込み、型安全に記述
  • 再利用可能な Construct: 共通パターンをまとめて開発速度を向上

cdk8s は、 TypeScript の力を借りて、 Kubernetes マニフェストの管理をより安全で効率的にしてくれるツールです。

次回の記事では、デプロイ戦略について紹介します:

  • GitOps 用リポジトリとの分離
  • マニフェストの書き出し方の工夫
  • 差分管理と CI/CD での運用

AI 機能をより素早く皆様のもとにお届けするため、新たなツールも活用しながら、 AI 機能の開発・運用をスケールさせていく試みを続けています!

We are hiring !!

kintone の AI 機能を支える基盤を、一緒に作りませんか?

この記事で紹介した cdk8s の実践パターンは、私たちが試行錯誤を重ねながらアーキテクチャを改善してきたものです。 技術選定から設計、実装まで、チームで議論しながら進めています。そして構築した基盤を通じて、 kintone の AI 機能でユーザーに最高の価値を届けることに挑戦し続けています。

こんなことに挑戦できます:

  • TypeScript (CDK / cdk8s) によるクラウドインフラ・Kubernetes 基盤の構築
  • AI ワークロードに最適化された EKS 環境の設計・運用
  • 急速に進化する AI 技術に対応した柔軟なアーキテクチャの設計

新しい技術にワクワクする方、チームで議論しながら最適な解を見つけることが好きな方、自分たちの基盤が開発者やユーザーの価値につながることにやりがいを感じる方――そんなあなたのご応募をお待ちしています!

cybozu.co.jp