Zero
Zero
Back

Use cdk8s to Define your Kubernetes Manifest with TypeScript

cdk8s is a command-line tool that enables you to create a Kubernetes manifest using a general purpose programming language. In this post, we'll use cdk8s to deploy the nginx web server to DigitalOcean Kubernetes.

Sam Magura

Sam Magura

An old-fashioned ship's wheel

A Kubernetes manifest is a YAML file that describes the services, deployments, containers, and other resources that make up your Kubernetes cluster. Defining your Kubernetes infrastructure by writing the manifest YAML manually is fine, but it can be slow to learn since, (A) configuring Kubernetes is very complex and (B) your editor won't provide any meaningful type-checking or Intellisense when editing YAML files.

Enter cdk8s , the Cloud Development Kit for Kubernetes. cdk8s addresses the pains of "YAML programming" by allowing you to define your Kubernetes resources in the general purpose programming language of your choice. cdk8s currently supports JavaScript, TypeScript, Python, Java, and Go.

While cdk8s is not directly related to the AWS Cloud Development Kit , it clearly takes inspiration from it. If you are familiar with how constructs work in the AWS CDK, you will feel right at home with cdk8s.

One thing to be aware of before choosing cdk8s is that it is purely a tool for creating Kubernetes manifests, meaning that cdk8s isn't involved at all in deploying your manifest to the Kubernetes cluster.

Secure your secrets conveniently

Zero is a modern secrets manager built with usability at its core. Reliable and secure, it saves time and effort.

Zero dashboard

What We're Building

In this article, we'll walk through deploying an nginx  web server to DigitalOcean Kubernetes . We will use cdk8s to synthesize the Kubernetes manifest and then apply the manifest by running kubectl in a GitHub Actions workflow. The DigitalOcean access token will be stored securely in Zero and fetched at deployment time using the official Zero GitHub Action .

🔗 The full code for this example is available in the zerosecrets/examples  GitHub repository.

Creating the DigitalOcean Kubernetes Cluster

Creating a Kubernetes cluster on DigitalOcean is extremely straightforward. Simply log in to your account, select "Kubernetes" in the menu on the left, and click "Create cluster". You can leave all options at the default values, except for the node plan and node count. For these settings, I recommend selecting the cheapest option to save money. Currently, this means the $12 per month node plan and 1 node. Submit the form and DigitalOcean will begin provisioning your cluster.

While you're waiting, install kubectl  and doctl  which you will need to deploy to the cluster from your local machine.

After installing both, create a new DigitalOcean personal access token by clicking the "API" link at the bottom of the main DigitalOcean navigation. For now, place the access token in a secure location on your computer — we'll move this secret to Zero at a later point. Then run

1
shell
doctl auth init

and paste in the access token. Once that is done, authorize doctl to connect to your cluster by running

1
shell
doctl kubernetes cluster kubeconfig save <CLUSTER_ID>

This command will update your kubeconfig file , which is located at ~/.kube/config.

The cluster ID is a UUID that you can copy from the cluster's page in the DigitalOcean portal. To test that the connection to the cluster is working, you can run kubectl cluster-info:

1
2
3
shell
$ kubectl cluster-info
Kubernetes control plane is running at https://72c9491f-bcf4-461e-b3e6-81f04d8602cf.k8s.ondigitalocean.com
CoreDNS is running at https://72c9491f-bcf4-461e-b3e6-81f04d8602cf.k8s.ondigitalocean.com/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

Bootstrapping the cdk8s Project

Now let's install cdk8s and use it to create our Kubernetes manifest by writing TypeScript code. The main way to use cdk8s is the CLI — you can find the instructions for installing it on the Getting Started  page.

Now, make a new directory and run cdk8s init to set up a new project from a template:

1
2
3
shell
$ mkdir nginx-project
$ cd nginx-project
$ cdk8s init typescript-app

This will create a package.json and a main.ts entrypoint, among other files. main.ts is where we'll write our "constructs", which is a generic cdk8s term that encompassasses things like Kubernetes services and deployments.

The cdk8s Getting Started guide shows the code  to deploy a "Hello world" Docker image to your cluster. We only need to tweak this code slightly to make it deploy the nginx image instead.

Here's the code you should place inside the constructor of MyChart:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
typescript
const label = {app: 'nginx-project'}

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

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

This is the same as the code from the cdk8s docs, with two changes:

  1. The image key was changed from paulbouwer/hello-kubernetes:1.7 to nginx. It's OK to omit the tag since latest will work for our purposes.
  2. The nginx container listens on port 80, so the targetPort and containerPort were both changed from 8080 to 80.

When the code is in place, run

1
shell
npm run build

to synthesize the TypeScript code into a YAML Kubernetes manifest. If you open the generated manifest (at the path dist/nginx-project.k8s.yaml), you'll see a YAML file that is very similar to the TypeScript code we wrote:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx-project-service-c8631c61
spec:
  ports:
    - port: 80
      targetPort: 80
  selector:
    app: nginx-project
  type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-project-deployment-c86313b8
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-project
  template:
    metadata:
      labels:
        app: nginx-project
    spec:
      containers:
        - image: nginx
          name: nginx
          ports:
            - containerPort: 80

Deploying from Local Development

To deploy the Kubernetes manifest to the cluster running in DigitalOcean, run

1
shell
kubectl apply -f dist/nginx-project.k8s.yaml

I recommend adding this command as a script in your package.json to save yourself some typing.

The first time you run the deployment, it will provision a DigitalOcean load balancer which takes a few minutes. You can monitor the status of this in the DigitalOcean portal. Once the load balancer is up, copy its public IP. Make sure to copy the IP of the load balancer and not the private IP of the Kubernetes node, like I did the first time I tried this.

Viewing the load balancer in the DigitalOcean portal. The load balancer's public IP is marked with a check.

Now simply paste the load balancer IP into your browser's address bar. You should see the default nginx webpage!

If you see this, it means nginx is set up and working correctly

If something didn't work, you can run kubectl describe pods to see the status of your pods.

Automating the Deployment with Zero

Ideally, both our application code and our infrastructure would be deployed from a CI/CD system. Let's automate the semi-manual process described above using GitHub Actions, so that the Kubernetes manifest is applied to the cluster each time a PR is merged into the main branch.

But first, let's move the DigitalOcean access token into Zero so that our workflow can retrieve the token from Zero when it runs. This is easiest step of the whole process!

To create a new Zero project and connect it to GitHub Actions:

  1. Log in to Zero and create a new project. Copy the Zero token to your clipboard.
  2. Go to the settings page of your GitHub repository. Select "Secrets and variables > Actions" in the menu.
  3. Create a new repository secret named ZERO_TOKEN and paste in the Zero token.

The menu item in the GitHub repository settings for adding a secret for use in GitHub Actions

Now, let's add the DigitalOcean personal access token to the Zero project:

  1. Return to Zero and click the "New secret" button.
  2. Select DigitalOcean for the secret type.
  3. Paste the DigitalOcean personal access token into the TOKEN field.

Your access token is now stored securely in Zero!

Writing the GitHub Actions Workflow

To define your GitHub Actions workflow, create a file called .github/workflows/main.yml. Below are the key steps of the workflow. First, we use the Zero GitHub Action  to exchange our Zero token for the DigitalOcean access token. This places the token in an environment variable which we then pass to the doctl setup action. Then, we run cdk8s to synthesize the Kubernetes manifest, and then apply it with kubectl.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
yaml
- name: Retrive the DigitalOcean token from Zero
  uses: zerosecrets/github-actions/token-to-secrets@main
  with:
    zero-token: ${{ secrets.ZERO_TOKEN }}
    apis: 'digitalocean'

- name: Install doctl
  uses: digitalocean/action-doctl@v2
  with:
    token: ${{ env.ZERO_SECRET_TOKEN }}

# TODO Replace the cluster name with your own
- name: Save DigitalOcean kubeconfig with short-lived credentials
  run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 k8s-1-26-3-do-0-nyc3-1682249551462

- name: npm install
  run: npm install

- name: npm run build
  run: npm run build

- name: kubectl apply
  run: kubectl apply -f dist/nginx-project.k8s.yaml

Click here  to view the full workflow file.

Cleaning Up

When you are done, navigate to the cluster in the DigitalOcean portal and click "Destroy".

Wrap-Up

This article demonstrated how to use cdk8s to define your Kubernetes manifest using TypeScript code, instead of writing the manifest YAML directly. The cdk8s approach is beneficial because:

  1. When using cdk8s, you piece together high-level building blocks and let the cdk8s CLI handle the low-level details, and
  2. Your editor can provide vastly better type-checking and Intellisense in TypeScript files than it can in YAML files.

Together, these two factors make Kubernetes development faster and easier to learn. Some teams even report that switching to cdk8s transformed their entire Kubernetes development workflow.

This walkthrough really just showed a proof of concept — the next step would be to deploy a more meaningful application to the Kubernetes cluster. If you are looking for a relatively simple way to expand on the above example, try configuring the nginx container to serve a static website, like a single-page React application. That said, Kubernetes is overkill for serving a static website — you'll get the most out of Kubernetes if you are building a complex distributed system.


Other articles

An abstract shape

Deploying Azure Functions with Pulumi and Zero

In this post, we'll use Pulumi to define our application's Azure infrastructure using clean and declarative TypeScript code.

Desk lamp

Gain Insight into your Users with Twilio Segment and Next.js

Zero brings the greatest value to your team once you integrate with multiple 3rd party APIs. This article adds Segment analytics to our previous DigitalOcean Kubernetes + GitHub Actions project, with both the Segment and DigitalOcean keys fetched using the Zero secrets manager.

Secure your secrets

Zero is a modern secrets manager built with usability at its core. Reliable and secure, it saves time and effort.