[cdk8s] CDK で Kubernetes を定義してみる

ビデオチュートリアルで cdk8s は[シーディーケィツ]と発音されていました。脳内再生する際に参考にしてください。
2020.08.09

Overview

Cloud Development Kit for Kubernetes(または cdk8s)は CDK で Kubernetes リソースの定義を行うためのフレームワークです。cdk8s は 2020 年 5 月にアルファ版がリリースされており、2020 年 8 月現在もアルファテストフェーズです。

cdk8s を使うことで今まで YAML で行なっていたPodServiceDeploymentReplicaSetなどの定義をプログラミング言語で行うことができます。具体的には、開発者は任意の数のチャートを定義し、定義されたチャートのそれぞれが Kubernetes マニフェストファイルに合成されます。

尚、cdk8s は Kubernetes アプリケーションの定義を行うツールで、実際にクラスタに適応はしません。 npm run synthコマンドを実行した際にプロジェクト内で定義されたチャートがdistディレクトリへYAMLファイルとして吐き出されるので、kubectl apply -f dist/<YOUR_FILE_NAME>.k8s.yamlコマンドや Flux のような GitOps ツールを使って、任意の Kubernetes クラスタに適用してください。

本記事では CDK で Kubernetes アプリケーションの定義を作成し、マニフェストファイルへ出力するところまでの手順をご紹介します。

cdk8s が現在対応している言語はPythonまたはTypescriptです。

Installation

参考:Getting Started with TypeScript

インストール

$ npm install -g cdk8s-cli

プロジェクト雛形作成

$ mkdir cdk
$ cd cdk
$ cdk8s init typescript-app
creating a new project from template: typescript-app
...

Transpile

CDK をデプロイする前に Typescript を Javascript へコンパイルする必要があるので watch mode をオンにしておきます。

$ npm run watch

検証環境

本記事で利用した検証環境は以下です。

cdk8s --version
0.26.0

tsc -v
Version 3.7.4

main.ts

プロジェクトルートに生成されるmain.tsにアプリケーション定義を記述します。

雛形で生成されたmain.tsは以下のような構成です。ここに Kubernetes アプリケーション定義を記述します。

main.ts

import { Construct } from 'constructs';
import { Chart, App } from 'cdk8s';

class MyChart extends Chart {
  constructor(scope: Construct, name: string) {
    super(scope, name);

    // define constructs here
  }
}

const app = new App();
new MyChart(app, 'cdk');
app.synth();

尚、この段階でnpm run synthを実行しても、dist/cdk.k8s.yamlには何も生成されません。

$ npm run synth
$ cat dist/cdk.k8s.yaml
<EMPTY>

Kubernetes オブジェクトを定義する

Kubernetes API オブジェクトをチャートに追加します。

cdk8s では Kubernetes API オブジェクトはconstructsとして表現され、cdk8s initを実行した際にプロジェクトディレクトリの imports/k8s.tsファイルへインポートされます。

先ほど生成されたデフォルトのmain.tsServiceDeploymentリソースを追加します。 image は paulbouwer さんのhello-kubernetesを使います。

main.ts

import { Construct } from 'constructs';
import { App, Chart } from 'cdk8s';

// imported constructs
import { Deployment, Service, IntOrString } from './imports/k8s';

export class MyChart extends Chart {
  constructor(scope: Construct, name: string) {
    super(scope, name);

    const label = { app: 'hello-k8s' };

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

    new Deployment(this, 'deployment', {
      spec: {
        replicas: 2,
        selector: {
          matchLabels: label
        },
        template: {
          metadata: { labels: label },
          spec: {
            containers: [
              {
                name: 'hello-kubernetes',
                image: 'paulbouwer/hello-kubernetes:1.7',
                ports: [ { containerPort: 8080 } ]
              }
            ]
          }
        }
      }
    });
  }
}

const app = new App();
new MyChart(app, 'cdk');
app.synth();

この状態でnpm run synthを実行するとdist/ディレクトリ下のcdk.k8s.yamlに以下の内容が出力されます。

cdk.k8s.yaml

apiVersion: v1
kind: Service
metadata:
  name: cdk-service-5dd5cffa
spec:
  ports:
    - port: 80
      targetPort: 8080
  selector:
    app: hello-k8s
  type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cdk-deployment-e4826fd6
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello-k8s
  template:
    metadata:
      labels:
        app: hello-k8s
    spec:
      containers:
        - image: paulbouwer/hello-kubernetes:1.7
          name: hello-kubernetes
          ports:
            - containerPort: 8080

これでマニフェストファイルが生成できたので、kubectlコマンドを実行してクラスターに反映することができます。

$ kubectl apply -f dist/cdk.k8s.yaml

Constructs

Construct はプログラムで定義されたHelm Chartだと思ってください。

例えば、先ほどの定義にhello world serviceを追加したい場合には、以下のように記述します。

new WebService(this, 'hello-k8s', {
  image: 'paulbouwer/hello-kubernetes:1.7'
});

WebServiceを定義するため、以下のディレクトリとファイルを新規作成し、以下の内容を記述します。

lib/web-service.ts

web-service.ts

import { Construct, Node } from 'constructs';
import { Deployment, Service, IntOrString } from '../imports/k8s';

export interface WebServiceOptions {
  /**
   * The Docker image to use for this service.
   */
  readonly image: string;

  /**
   * Number of replicas.
   *
   * @default 1
   */
  readonly replicas?: number;

  /**
   * External port.
   *
   * @default 80
   */
  readonly port?: number;

  /**
   * Internal port.
   *
   * @default 8080
   */
  readonly containerPort?: number;
}

export class WebService extends Construct {
  constructor(scope: Construct, ns: string, options: WebServiceOptions) {
    super(scope, ns);

    const port = options.port || 80;
    const containerPort = options.containerPort || 8080;
    const label = { app: Node.of(this).uniqueId };
    const replicas = options.replicas ?? 1;

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

    new Deployment(this, 'deployment', {
      spec: {
        replicas,
        selector: {
          matchLabels: label
        },
        template: {
          metadata: { labels: label },
          spec: {
            containers: [
              {
                name: 'app',
                image: options.image,
                ports: [ { containerPort } ]
              }
            ]
          }
        }
      }
    });
  }
}

main.tsも書き換えましょう。

main.ts

import { App, Chart } from 'cdk8s';
import { Construct } from 'constructs';
import { WebService } from './lib/web-service';

export class MyChart extends Chart {
  constructor(scope: Construct, ns: string) {
    super(scope, ns);

    new WebService(this, 'hello', { image: 'paulbouwer/hello-kubernetes:1.7', replicas: 2 });
    new WebService(this, 'ghost', { image: 'ghost', containerPort: 2368 });
  }
}

const app = new App();
new MyChart(app, "cdk");
app.synth();

再びnpm run synthを実行するとcdk.k8s.yamlが以下のように書き換えられます。

cdk.k8s.yaml

apiVersion: v1
kind: Service
metadata:
  name: cdk-cdk-service-2a50c71f
spec:
  ports:
    - port: 80
      targetPort: 8080
  selector:
    app: cdkBD7FE5AF
  type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cdk-cdk-deployment-32246f9b
spec:
  replicas: 2
  selector:
    matchLabels:
      app: cdkBD7FE5AF
  template:
    metadata:
      labels:
        app: cdkBD7FE5AF
    spec:
      containers:
        - image: paulbouwer/hello-kubernetes:1.7
          name: app
          ports:
            - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: cdk-ghost-service-cdbfc1f3
spec:
  ports:
    - port: 80
      targetPort: 2368
  selector:
    app: cdkghostA3D16120
  type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cdk-ghost-deployment-ac6892bc
spec:
  replicas: 1
  selector:
    matchLabels:
      app: cdkghostA3D16120
  template:
    metadata:
      labels:
        app: cdkghostA3D16120
    spec:
      containers:
        - image: ghost
          name: app
          ports:
            - containerPort: 2368

More Examples

本記事では cdk8s の Get Started のチュートリアルをご紹介しました。 他のサンプルはこちらで紹介されていますので参考にしてみてください。

Video Tutorial

References