プログラムからkubernetesYAMLを生成できるcdk8sを試してみた

8 min read読了の目安(約7200字

cdk8sとは

Kubernetes の厄介なところの一つであるYAMLをプログラムから生成しちゃうやつです。

現在対応している言語は TypeScript, Python, JavaScript があります。
将来的には、Java、.NET、Go に対応するそうです。

できること・できないこと

  • できること
    • YAMLファイルをプログラムから生成できます。
    • クラスターに依存しません。(オンプレ、クラウドどちらもで動く)
    • Kubernetes APIを任意選択できます。
  • できないこと
    • このツールからデプロイはできません。(GitOpsを介してやる必要があります。)

セットアップ

今回はTypeScriptを使ってさくっとやってみます。
とりあえず当たり前にインストールします。

$ npm install -g cdk8s-cli
       or
$ yarn global add cdk8s-cli

helpはこんな感じで表示されます。今回はcdk8s init TYPEを叩いてみましょう。

$ cdk8s -h
cdk8s [コマンド]

コマンド:
  cdk8s import [SPEC]  Imports API objects to your app by generating constructs.                                                                   
  cdk8s init TYPE      Create a new cdk8s project from a template.
  cdk8s synth          Synthesizes Kubernetes manifests for all charts in your app.                                                                     

オプション:
  --version  バージョンを表示                                                     
  --help     ヘルプを表示                                                       

Options can be specified via environment variables with the "CDK8S_" prefix (e.g. "CDK8S_OUTPUT")

TypeScript で開発するときにやっておきたいこと。

今回は TypeScript で作成するので、watch mode をオンにしておきましょう。

$ yarn watch

Kubernetes オブジェクトを定義

Kubernetes API オブジェクトを定義していきます。

cdk8s では kubernetes API オブジェクトはconstructsで表現されます。
そのため、cdk8s init実行時にimports/k8s.tsにインポートされます。

今回は paulbouwer 氏の hello-kubernetes を元に、 Service と Deployment を定義しましょう。
定義先はmain.tsです。

import { Construct } from 'constructs';
import { App, Chart, ChartProps } from 'cdk8s';
import { KubeService, IntOrString, KubeDeployment } from './imports/k8s';
                                        
export class MyChart extends Chart {    
  constructor(scope: Construct, id: string, props: ChartProps = { }) {
    super(scope, id, props);            
                                        
    // define resources here            
    const label = { app: 'hello-k8s' }  
    new KubeService(this, 'service', {  
      spec: {                           
        type: 'LoadBalancer',           
        ports: [ { port: 80, targetPort: IntOrString.fromNumber(8080) } ],
        selector: label                 
      }                                 
    })                                  
                                        
    new KubeDeployment(this, 'deployment', {
      spec: {
        replicas: 2,
        selector: {
          matchLabels: label
        },
        template: {
          metadata: { labels: label },
          spec: {
            containers: [
              {
                name: 'hello-kubernetes',
                image: 'paulbouwer/hello-kubernetes:1.8',
                ports: [ { containerPort: 8080 } ]
              }
            ]
          }
        }
      }
    })
  }
}

出来上がったら以下のyarn synthと叩いてみましょう。
dist 配下にファイルが作成されます。

$ cat dist/hello-world.k8s.yaml 
apiVersion: "v1"
kind: "Service"
metadata:
  name: "hello-world-service-c82ef75b"
spec:
  ports:
    - port: 80
      targetPort: 8080
  selector:
    app: "hello-ks8s"
  type: "LoadBalancer"
---
apiVersion: "apps/v1"
kind: "Deployment"
metadata:
  name: "hello-world-deployment-c83a8861"
spec:
  replicas: 2
  selector:
    matchLabels:
      app: "hello-ks8s"
  template:
    metadata:
      labels:
        app: "hello-ks8s"
    spec:
      containers:
        - image: "paulbouwer/hello-kubernetes:1.8"
          name: "hello-kubernetes"
          ports:
            - containerPort: 8080

Constructs

Helm Chart とほぼ同じです。
今回作ったものは単一のもになりますが、別のサービスを追加したいくなるときもあります。
Service を追加してみましょう。
まずはlibディレクトリを作成し、その中にWebServiceを作っていきましょう。

libディレクトリにファイルを置くことで再利用可能なコンポーネントを作成していきます。

import {IntOrString, KubeDeployment, KubeService} from 'cdk8s-plus-17/lib/imports/k8s' 
import { Construct } from 'constructs' 

export interface WebServiceProps {
  readonly image: string
  readonly replicas?: number
  readonly port?: number
  readonly containerPort?: number 
}

export class WebService extends Construct {
  constructor(scope: Construct, id: string, props: WebServiceProps) {
    super(scope, id) 

    const port = props.port || 80
    const containerPort = props.containerPort || 8080
    const label = { app: 'hello-k8s' }
    const replicas = props.replicas ?? 1

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

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

できたら、main.tsをこのように書き換えましょう。

import { Construct } from 'constructs';
import { App, Chart, ChartProps } from 'cdk8s';
import { WebService } from './lib/WebService';
                                        
export class MyChart extends Chart {    
  constructor(scope: Construct, id: string, props: ChartProps = { }) {
    super(scope, id, props);            
    
    /** 
     * ここを書き換える
     * 今回はhelloとghostを追加してみた
     * ghost: https://hub.docker.com/_/ghost
     */ 
    new WebService(this, 'hello', { image: 'paulbouwer/hello-kubernetes:1.7', replicas: 10 });
    new WebService(this, 'ghost', { image: 'ghost', containerPort: 2368 });
                                        
  }
}

const app = new App();
new MyChart(app, 'hello-world');
app.synth();

cdk8s synthを叩くと以下のように追加されます。

apiVersion: "v1"
kind: "Service"
metadata:
  name: "hello-world-hello-service-c8847821"
spec:
  ports:
    - port: 80
      targetPort: 8080
  selector:
    app: "hello-k8s"
  type: "LoadBalancer"
---
apiVersion: "apps/v1"
kind: "Deployment"
metadata:
  name: "hello-world-hello-deployment-c8f0bd0b"
spec:
  replicas: 10
  selector:
    matchLabels:
      app: "hello-k8s"
  template:
    metadata:
      labels:
        app: "hello-k8s"
    spec:
      containers:
        - image: "paulbouwer/hello-kubernetes:1.7"
          name: "app"
          ports:
            - containerPort: 8080
---
apiVersion: "v1"
kind: "Service"
metadata:
  name: "hello-world-ghost-service-c81e52c6"
spec:
  ports:
    - port: 80
      targetPort: 2368
  selector:
    app: "hello-k8s"
  type: "LoadBalancer"
---
apiVersion: "apps/v1"
kind: "Deployment"
metadata:
  name: "hello-world-ghost-deployment-c8c14ddd"
spec:
  replicas: 1
  selector:
    matchLabels:
      app: "hello-k8s"
  template:
    metadata:
      labels:
        app: "hello-k8s"
    spec:
      containers:
        - image: "ghost"
          name: "app"
          ports:
            - containerPort: 2368

まとめ

kubernetesYAMLの管理は大変ですがこのようにコードで管理できると便利ですね。
良ければいいねよろしくお願います!

参考

https://cdk8s.io/docs/latest/getting-started/
https://aws.amazon.com/jp/blogs/news/introducing-cdk-for-kubernetes/