Skip to main content

100 Days of K8S with Typescript

This post will be another attempt to learn Kubernetes, using typescript & cdk8s this time.

Day 1: Workplace initialization & Hello World.

You’ll need node, npm & typescript. I’ve installed those with brew (cuz I’m doing this on Mac OS X):

> brew install node npm typescript
...
> node --version
v18.9.0
> npm --version
8.19.1
> tsc --version
Version 4.8.3

You might also want to install ts-node so it will not be needed to convert typescript to javascript before running it with node:

> npm install -g ts-node
...
> npx ts-node --version
v10.9.1

Once this little people is installed, let’s write our first typescript program, the hello world program:

let message: string = 'Hello, World!';
console.log(message);

Then, convert it with tsc & run it with node:

> tsc app.ts
> ls -l
total 16
-rw-r--r--  1 pmarie  staff  53 25 Sep 21:38 app.js
-rw-r--r--  1 pmarie  staff  61 13 Sep 23:15 app.ts
> cat app.js
var message = 'Hello, World!';
console.log(message);

> node app.js
Hello, World!

Or do it with only one step using ts-node:

> npx ts-node app.ts
Hello, World!

Day 2: Installing cdk8s & creating our first chart

cdk8s is a development framework dedicated to build kubernetes charts using typescript, python or golang. Installing it is as simple as it can be done with any other tool using brew:

> brew install cdk8s
>

Bootstrapping the cdk8s app

Initializing a new application is a bit long, but it will download all nodes modules and build the initial k8s layer to be able to create k8s charts. It can be done with cdk8s init:

> cdk8s init typescript-app
...
 Your cdk8s typescript project is ready!

   cat help         Print this message
 
  Compile:
   npm run compile     Compile typescript code to javascript (or "yarn watch")
   npm run watch       Watch for changes and compile typescript in the background
   npm run build       Compile + synth

  Synthesize:
   npm run synth       Synthesize k8s manifests from charts to dist/ (ready for 'kubectl apply -f')

 Deploy:
   kubectl apply -f dist/

 Upgrades:
   npm run import        Import/update k8s apis (you should check-in this directory)
   npm run upgrade       Upgrade cdk8s modules to latest version
   npm run upgrade:next  Upgrade cdk8s modules to latest "@next" version (last commit)

Building the code & synthetizing the charts is as simple as running npm run compile then cdk8s synth:

> npm run compile && cdk8s synth
...
dist/hello-world-ts.k8s.yaml

Per default, the chart will be empty. Thanksfully, the getting started page gives a basic example with a service & a deployment:

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

    new KubeService(this, 'service', {
      metadata: {
        labels: label,
      },
      spec: {
        type: 'LoadBalancer',
        ports: [ { port: 8001, 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.7',
                ports: [ { containerPort: 8080 } ]
              }
            ]
          }
        }
      }
    });

After building & creating the charts, the hello-world-ts.k8s.yaml file is no longer empty:

apiVersion: v1
kind: Service
metadata:
  labels:
    app: hello-k8s
  name: hello-world-ts-service-c8780799
spec:
  ports:
    - port: 8001
      targetPort: 8080
  selector:
    app: hello-k8s
  type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-world-ts-deployment-c881e597
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

Note: At the time of writing, I’m using a k3s for my tests. The created service (with a loadbalancer) will then spawn a pod on each nodes to bind the port (8001). For each other LoadBalancer services, using another port will be required. Note also that other k8s installation might require more setup to work correctly.

Testing the chart

> kubectl apply -f dist/hello-world-ts.k8s.yaml
service/hello-world-ts-service-c8780799 created
deployment.apps/hello-world-ts-deployment-c881e597 created
> kubectl get services --show-labels --selector='app=hello-k8s'
NAME                              TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE   LABELS
hello-world-ts-service-c8780799   LoadBalancer   10.43.190.35   10.0.0.4      8001:30106/TCP   50m   app=hello-k8s
> kubectl get pods --show-labels --selector='app=hello-k8s'
NAME                                                 READY   STATUS    RESTARTS   AGE   LABELS
hello-world-ts-deployment-c881e597-5c667d695-shtc4   1/1     Running   0          53m   app=hello-k8s,pod-template-hash=5c667d695
hello-world-ts-deployment-c881e597-5c667d695-zhnrm   1/1     Running   0          53m   app=hello-k8s,pod-template-hash=5c667d695
> curl -s http://shuttle:8001/ | pandoc -f html -t plain
[]

Hello world!

  ------- ----------------------------------------------------
  pod:    hello-world-ts-deployment-c881e597-5c667d695-zhnrm
  node:   Linux (5.19.7-200.fc36.x86_64)
  ------- ----------------------------------------------------

Applying the example chart using kubectl apply created a deployment (with 2 replicas) & a service binding on my installation port 8001 on each k8s nodes. Reaching one of the node is correctly serving the demo container.

Day 3: Testing using jest

Today, I’m testing testing. It seems like it is possible to test javascript or typescript stuff with jest. When generating first time our cdk8s app, it created already a basic unit test. Let’s install jest before continuing:

> npm install -y jest

jest is able to record snapshots of the current state, so it can be re-tested later. This can be done thanks to:

> jest --updateSnapshot

I added some basic test to verify the generated service. I’m testing it is a label value, the defined ports and the type . It looks like this:

  test('HelloWorldTsHasService', () => {
    const app = Testing.app();
    const chart = new HelloWorldChart(app, 'test-chart');
    const results = Testing.synth(chart);

    var service = results.find((obj) => {
      return obj.kind === 'Service';
    });

    expect(service.metadata.labels['app']).toBe('hello-k8s');
    expect(service.spec.ports.length).toBe(1);
    expect(service.spec.ports[0].port).toBe(8001);
    expect(service.spec.ports[0].targetPort).toBe(8080);
    expect(service.spec.type).toBe('LoadBalancer');
  });

Running the test looked like this:

> jest
 PASS  ./main.test.ts
  TestHelloWorldTs
    ✓ BasicHelloWorldSnapshot (5 ms)
    ✓ HelloWorldTsHasService (1 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   1 passed, 1 total
Time:        1.192 s
Ran all test suites.

Day 4: Playing with kubernetes/client-node.

Today, I took a look to the javascript kubernetes client, aka kubernetes/client-node.

First, I created a new project and imported the library:

> npm install -y @kubernetes/client-node

Then, I wrote some code:

import _, { CoreV1Api, AppsV1Api, KubeConfig } from '@kubernetes/client-node';

const kc = new KubeConfig();
kc.loadFromDefault();

const k8sApi = kc.makeApiClient(CoreV1Api);
const k8sAppsApi = kc.makeApiClient(AppsV1Api);

async function listDeploymentsForNamespace(namespace: string) {
    const res = await k8sAppsApi.listNamespacedDeployment(namespace);
    res.body.items.forEach((o: _.V1Deployment) => {
        console.log("namespace:" + namespace + " " + o.metadata?.name);
    });
}

async function listNamespacesAndDeployments() {
    const res = await k8sApi.listNamespace();
    res.body.items.forEach((o: _.V1Namespace) => {
        if (typeof o.metadata?.name === 'string') {
            listDeploymentsForNamespace(o.metadata?.name);
        }
    });
}

listNamespacesAndDeployments();

The snippet is quite small, and it took me some time to write it as I really hate async code. My goal was to write a couple of api calls and do stuff with those, and the only thing I succeeded was to list namespaces & deployments in those. At first, the result was messy (async code!), but eventually I made it work. The result of this:

> npx ts-node hello-world.ts
namespace:default hello-world-ts-deployment-c881e597
namespace:cert-manager cert-manager
namespace:cert-manager cert-manager-cainjector
namespace:cert-manager cert-manager-webhook
namespace:kube-system local-path-provisioner
namespace:kube-system coredns
namespace:kube-system traefik
namespace:kube-system metrics-server