CDK8s が GA したので次は CDK8s+ でもっと楽したい

この記事は AWS CDK Advent Calendar 2021 の 21 日目と AWS Containers Advent Calendar 2021 の11日目のクロスポスト記事です。

このブログでは、2021/10 に一般利用可能になった CDK8s の L2 Constract ライブラリである CDK8s+ について入門していきます。「CDK8s+ を通してCDK8s に興味をもって欲しい!触ってみて欲しい!」をゴールにします。

はじめに / CDK8s とは

CDK8s (CDK for Kubernetes) は、CDK の設計思想を、Kubernetes の世界にも適用できないかと開発された、オープンソースのツールキットです。Kubernetes 上のリソースを、使い慣れたプログラミング言語を使ってソースコードとして実装し、そこからマニフェスト YAML ファイルを生成できます。その YAML ファイルを Kubernetes のコントロールプレーンに適用できます。

YAML でリソースを定義していく上での Pros、Cons は一例ですが、以下のようではないでしょうか。

  • Pros
    • 静的に定義が書かれていて読みやすい
    • インデントを使いデータの階層構造を定義できる
    • 配列・スカラー・ハッシュ等表現力豊富な書き方ができる
  • Cons
    • 必要な定義全てを宣言する必要があり、効率的に書けない
    • 規模が増えていくとボイラープレート用意したりで、手作業が辛い
    • ユニットテストが難しい。メンテナンスが辛い

YAML で書くことを否定しているわけではないですよ!Pros で書いたように YAML で書いた方が遥かに便利で簡単なケースもたくさんあります。ただ、普段慣れしたんだプログラミング言語で使い慣れた IDE のツールや機能を使って実装できるのは非常にありがたいと思う瞬間が確かにあります。

最終的にはピュアなマニフェスト YAML を生成しますので、プログラミング言語による実装はしますが、宣言型の適切な状態アプローチを損なうことはありません。また、生成した YAML は EKS や k8s on EC2 にかぎらず、オンプレミス等の任意の Kubernetes クラスターで使用できます。

CDK8s は、オープンソースとして開発されています。CNCF プロジェクトの一つで、現在は Sandbox として扱われています。

2021 年 10 月 12 日に一般利用可能 (v1.0) となり、2021 年 12 月 31 日現在すでに v2.0.16 がリリースされていますw

サポートされている言語は以下の通りです。

  • TypeScript
  • Python
  • Java
  • Go(デベロッパープレビュー)

例えば、TypeScript で以下のようなコードから

import { Construct } from 'constructs';
import { App, Chart } from 'cdk8s';
import { KubeDeployment, KubeService, IntOrString  } from './imports/k8s';

export class HelloKube extends Chart {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    const label = { app: 'my-nginx' };

    new KubeService(this, 'service', {
      spec: {
        type: 'LoadBalancer',
        ports: [ { port: 8080, targetPort: IntOrString.fromNumber(80) } ],
        selector: label
      }
    });

    new KubeDeployment(this, 'deployment', {
      spec: {
        replicas: 3,
        selector: {
          matchLabels: label
        },
        template: {
          metadata: { labels: label },
          spec: {
            containers: [
              {
                name: 'my-nginx',
                image: 'nginx',
                ports: [ { containerPort: 80 } ]
              }
            ]
          }
        }
      }
    });
  }
}

const app = new App();
new HelloKube(app, 'my-nginx');
app.synth();

こちらの Deployment と Service の マニフェスト Yaml を生成できます。

apiVersion: v1
kind: Service
metadata:
  name: my-nginx-service-c8df35b9
spec:
  ports:
    - port: 8080
      targetPort: 80
  selector:
    app: my-nginx
  type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx-deployment-c85286ef
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-nginx
  template:
    metadata:
      labels:
        app: my-nginx
    spec:
      containers:
        - image: nginx
          name: my-nginx
          ports:
            - containerPort: 80

1年くらい前に撮ったやつですが、CDK8s で Yaml を生成して K8s クラスターにデプロイするまでの以下のデモ動画や、その他詳細や Walkthrough については、GA 時ブログや、プレビュー時に喋ったこちらのスライドをぜひご覧ください。

CDK8s+ ことはじめ

さて本題です。上記で例示したコード見て気づいた方もいると思いますが、Yaml で書いていた時と比べて、実はそこまで効率的ではないんですよね。YAML 上のリソースやプロパティをプログラミング言語で 1:1 で対応したものを実装しているに過ぎません。CDK を触っている方なら予測がつくと思いますが、CDK8s(cdk8s-core) 自体は CDK でいう L1 Constracts を提供しています。L1 Constracts でゴリゴリ書いていくのもいいですが、CDK といえばやはり L2 Constracts の存在ですよね。そこで CDK8s+ です。CDK8s では CDK8s+ が L2 Constracts にあたります。

CDK8s+ をどんどん使っていって、Intent ベースな実装でマニフェスト YAML を効率的に生成したいところです。

ただ、CDK8s 自体は一般利用可能ですが、CDK8s+ はまだベータ版です。今後一般利用可能になることに期待しています。

例えば、先ほど生成したマニフェストを今度は CDK8s+ を使って実装すると、以下のように直感的にシンプルな表現で書けます。行数的にもかなり減らせていますし、spec を記述する必要がないので、非常にフラットな構造で実装できるのも個人的には嬉しいです。

import { Construct } from 'constructs';
import { App, Chart } from 'cdk8s';
import * as kplus from 'cdk8s-plus-22';

export class HelloKube extends Chart {
  constructor(scope: Construct, id: string) {
    super(scope, id);
    
    const deployment = new kplus.Deployment(this, 'Deployment', {
      replicas: 3,
      containers: [{
        image: 'nginx',
        port: 80,
      }],
    });

    deployment.exposeViaService({port: 8080, serviceType: kplus.ServiceType.LOAD_BALANCER});
  }
}

const app = new App();
new HelloKube(app, 'my-nginx');
app.synth();

こちらのコードから以下のようなマニフェスト Yaml が生成されます。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx-deployment-c898d1dd
spec:
  replicas: 3
  selector:
    matchLabels:
      cdk8s.deployment: my-nginx-Deployment-c860f938
  template:
    metadata:
      labels:
        cdk8s.deployment: my-nginx-Deployment-c860f938
    spec:
      containers:
        - env: []
          image: nginx
          imagePullPolicy: Always
          name: main
          ports:
            - containerPort: 80
          volumeMounts: []
      volumes: []
---
apiVersion: v1
kind: Service
metadata:
  name: my-nginx-deployment-service-c8f61b73
spec:
  externalIPs: []
  ports:
    - port: 8080
      targetPort: 80
  selector:
    cdk8s.deployment: my-nginx-Deployment-c860f938
  type: LoadBalancer

CDK8s での実装との違いについて少し掘り下げてみていきます。 まずは、CDK8s+ の Kubernetes v1.22 ライブラリを import しています。この辺りのバージョニングについては後述します。

import * as kplus from 'cdk8s-plus-22';

Deployment オブジェクトの実装についてです。CDK8s での実装では マニフェスト Yaml を書いていた時とほぼ同様の階層で書かれており、また Label Selector を使った関連付けを明示的に表現しています。対して CDK8s+ では前述の通り spec がなく、CDK8s の時と比べてフラットな書き方ができます。また Label Selector の指定をしていません。これらは CDK8s+ の Deployment メソッドが自動で生成しています。

// cdk8s-core
new KubeDeployment(this, 'deployment', {
  spec: {
    replicas: 3,
    selector: {
      matchLabels: label
    },
    template: {
      metadata: { labels: label },
      spec: {
        containers: [
          {
            name: 'my-nginx',
            image: 'nginx',
            ports: [ { containerPort: 80 } ]
          }
        ]
      }
    }
  }
});

// cdk8s-plus
const deployment = new kplus.Deployment(this, 'Deployment', {
  replicas: 3,
  containers: [{
    image: 'nginx',
    port: 80,
  }],
});

続いて Service オブジェクトの実装についてです。CDK8s の実装ではシンプルに Service オブジェクトのマニフェスト Yaml を書くのと同様に Label SelectortargetPort によるマッピングを表現しています。対して CDK8s+ ではそもそも Deployment の延長として Intent ベースな実装になっています。作成した deployment オブジェクトを exposeViaService というメソッドで 8080 ポートで公開するという表現方法となっています。

// cdk8s-core
new KubeService(this, 'service', {
  spec: {
    type: 'LoadBalancer',
    ports: [ { port: 8080, targetPort: IntOrString.fromNumber(80) } ],
    selector: label
  }
});

// cdk8s-plus
deployment.exposeViaService({port: 8080, serviceType: kplus.ServiceType.LOAD_BALANCER});

CDK8s+ は CDK8s アプリケーションに import して利用できます。各言語での Getting Started はこちらを参照ください。

CDK8s+ のバージョニングについて

YAML 上のリソースやプロパティをプログラミング言語で 1:1 で実装していく CDK8s と異なり、CDK8s+ を使った実装は、より実装が抽象化され Constracts が自動生成する部分への依存も増えます。また、CDK8s+ の主な目標の 1 つは、Kubernetes のマニフェストを書く上で覚えるべきことを減らし、マニフェストの記述ミスをする余地をほとんど残さないことです。これを実現するために、CDK8s+ は特定のバージョンの Kubernetes をターゲットにしています。

現在CDK8s+ として提供されているKubernetes バージョンは、1.22,1.21,1.20 の 3 バージョンです。そしてcdk8s-plus-20, cdk8s-plus-21, cdk8s-plus-22 のようにバージョンごとのライブラリが用意されていますので運用している Kubernetes クラスターのバージョンに応じたライブラリを利用します。結果、運用している Kubernetes クラスターで利用できない可能性がある機能がマニフェストに入り込むことがなくなります。

さいごに

このブログでは 「CDK8s+ を通してCDK8s に興味をもって欲しい!触ってみて欲しい!」ことをゴールに入門ブログを書きました。このブログを通して、もし興味を持っていただいた方は、ぜひ公式ドキュメントの CDK8s+ の API リファレンスKubernetes オブジェクトでのユースケースもご覧ください。そして Github に公開されているサンプルもぜひお試しください!

CDK8s+ 早く一般利用可能になってほしいですし、もっといろんなメソッドが用意されると幸せこの上ないです。来年も CDK やその周りにあるプロジェクトを一緒にどんどん発展させていきましょう!

Happy Cording!!

p.s. 「Blog を作ることでアウトプットすることを追い込む Blog」なのに、一年以上放置でほんと反省・・・来年は月に一本を目標に・・・!