3sky's notes

Minimal blog about IT

CDK8S workflow example

2022-07-31 5 min read 3sky

Welcome

After a long break(again!) - I’m back. Older, more tired, and handsome as always. Newsletter is very time-consuming, and being a consultant is time-consuming. Everything is time-consuming. That’s why I decided to write this post with a bit different approach. Let’s talk about processes. The tech part is still most important, unfortunately, sometimes it’s not the key. So what is CDK?(generated with rytr) CDKs are a set of tools that enable developers to make use of the cloud environment. They provide a set of tools and resources that allow developers to deploy their applications in the cloud. CDKs are used for different purposes such as:

  • Providing the developer with access to data and services in the cloud.
  • Providing an abstraction layer between the developer’s code and the service APIs.
  • Providing a single interface for accessing multiple APIs from different providers.
  • Facilitating the deployment of applications on third-party clouds.

Tools used in this episode

  • cdk8s
  • monodraw(for diagrams)

Intro

Recently I was asked for a new shiny infra-cd-cd-dev system. Developers strongly pushed the idea of real infrastructure as a code. Let’s use cdk8s, or cdktf they said. I was skeptical about that. It doesn’t mean I prefer only raw Kubernetes manifests and sed. This does mean that I prefer appropriate solutions. The client already has a set of Terraform modules, helm charts, pipelines, and even bash scripts somewhere. Making a switch to cd-something in the middle of platform migration, redesigning workflow, and CICD pipelines on top of the working system can be very destructive. Fortunately today we can focus on a hypothetic new system without a time-to-money issue. Let’s begin then.

Flow design

At the beginning of system development, we would like to deliver something. I know that platform is just a moving element that can be changed. However, in many cases, we choose the deployment environment at the beginning of the project. Based on that we would like to be fast. Developers want to deploy as fast as possible. We can help them with simple project management. For example, we have a backend app and a frontend app. We would like to be fast and flexible, very flexible. Take a look at the below structure.

cdk-1
Example project structure

We have three repositories. Let’s assume that the backend team and frontend team are not the same people. This structure contains one big project repo, with submodules, and Makefile. Make can pull submodules, build the app, push it, modify the CDK definitions and run kubectl apply -f. All from a workstation. It’s not a proper GitOps or even DevOps. Instead, we are fast. Also, developers become friendly with Kubernetes and objects as they are responsible for their namespace now. There is no “DevOps” person - only full stacks :) And that’s fine. Until we grow. Then we want to add some CI, at least for unit tests and build tests. And that’s fine. Repos are separated, so we don’t block other developers’ work. What if someone wants to add a deployment part instead CI? That’s fine too. We can add a pipeline to the CDK repo which will be triggered via some event. Strange, but we can do that. We’re flexible. In general, we can split our project into two stages. Rapid growth when we need to be fast, and stable environment when we need GitOps + full CICD. Then we can simply modify the image.

cdk-2
Example full project structure

Summary

As you probably noticed, we can use Terraform or Pulumi, or Helm instead of cdk8s. That is true. The system always can be flexible and easy to customize. This post was supposed to be about cdk8s wasn’t it? So why use it? To be honest I don’t know.

  • Documentation is hm, hard to read. Or requires some time to get familiar with.
  • Speed? Not sure, at the end, it still requires kubectl apply, and you need to add manifest generation time.
  • Maybe it’s easy to use? Yes, if you configure kubectl client earlier, it’s the same as helm.
  • Writing in many languages? That’s true. Unfortunately, if we talk about Kubernetes, developers still need to know how to compose regular manifests. There is no magic.
  • Ah is it shiny? Yes, it’s.

So who should use the tool? Everyone who enjoys the style. It’s just a tool. Ops people will go with Terraform, someone with Pulumi. CDKs are for developers who know exactly what they want to do and prefer TypeScrypt over HCL. Also, it’s a library, not another tool to get familiar with. However, I’m sure that real Ops will go with Terraform, rather than with cdk8s.

Bonus

As additional material, I have some CDK8S examples with namespace and ConfigMap definitions. Resolving that, took me some time. So take it and use it :)

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

// imported constructs
import { KubeDeployment, KubeService, IntOrString, KubeNamespace, KubeConfigMap } from './imports/k8s';


export class MyChart extends Chart {
  //CHECK: NS can be provided as ChartProps param, but you can't create NS then.
  //However if you already have a namespace CharProps is a place for you 
  constructor(scope: Construct, id: string, props: ChartProps = {}) {
    super(scope, id, props);

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


    new KubeConfigMap(this, "config", {
      metadata: {
        namespace: ns,
        name: 'config'
      },
      data: {
        //CHECK: we need to use '' in the key name to allow '.' in the name
        'app.properties': [
          "environment=production",
          "logging=INFO",
          "logs_path=$APP_HOME/logs/"
        ].join('\n'),
      }
    });

    new KubeNamespace(this, "namespace", {
      metadata: {
        labels: label,
        name: ns,
      }
    });
    new KubeService(this, 'service', {
      metadata: { namespace: ns },
      spec: {
        type: 'LoadBalancer',
        ports: [{ port: 80, targetPort: IntOrString.fromNumber(8080) }],
        selector: label
      }
    });

    new KubeDeployment(this, 'deployment', {
      metadata: { namespace: ns },
      spec: {
        replicas: 2,
        selector: {
          matchLabels: label
        },
        template: {
          metadata: { labels: label },
          spec: {
            containers: [
              {
                name: 'hello-kubernetes',
                image: 'paulbouwer/hello-kubernetes:1.7',
                ports: [{ containerPort: 8080 }],
                //CHECK: mapping volumes - brackets with types, are much more annoying than YAML
                volumeMounts: [{ name: 'config', mountPath: "/config", readOnly: true }]
              }
            ],
            volumes: [{
              name: 'config',
              configMap: {
                name: 'app-config',
                items: [{
                  key: 'app.properties',
                  path: 'app.properties'
                }]
              }
            }]
          }
        }
      }
    });
  }
}

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