cdk8s Documentation

Define Kubernetes native apps and abstractions using object-oriented programming

4,809 stars JavaScript JavaScript 1 file ยท ~5,899 tokens
RAW Doc

File: README.md

Cloud Development Kit for Kubernetes

cdk8s is an open-source software development framework for defining Kubernetes applications and reusable abstractions using familiar
programming languages and rich object-oriented APIs. cdk8s apps synthesize into standard Kubernetes
manifests which can be applied to any Kubernetes cluster.

cdk8s is a Cloud Native Computing Foundation Sandbox Project, built with โค๏ธ at AWS. We encourage you to try it out, leave feedback, and jump in to help!

Contents:

Repositories

This project consists of multiple packages, maintained and released via the following repositories:

  • cdk8s - Core library. For historical reasons note that the cdk8s package is maintained in the cdk8s-team/cdk8s-core repository.
  • cdk8s-cli - Command-Line interface.
  • cdk8s-plus - High-Level constructs for Kubernetes core.

The current repository acts as an umbrella repository for cross module concerns, as well as the deployment of cdk8s.io

Overview

cdk8s apps are programs written in one of the supported programming
languages. They are structured as a tree of
constructs.

The root of the tree is an App construct. Within an app, users define any
number of charts (classes that extend the Chart class). Each chart is
synthesized into a separate Kubernetes manifest file. Charts are, in turn,
composed of any number of constructs, and eventually from resources, which
represent any Kubernetes resource, such as Pod, Service, Deployment,
ReplicaSet, etc.

cdk8s apps only define Kubernetes applications, they don't actually apply
them to the cluster. When an app is executed, it synthesizes all the charts
defined within the app into the dist directory, and then those charts can be
applied to any Kubernetes cluster using kubectl apply -f dist/chart.k8s.yaml or a GitOps tool like Flux.

cdk8s is based on the design concepts and technologies behind the AWS
Cloud Development Kit
, and can interoperate with
AWS CDK constructs to define cloud-native applications that include both
Kubernetes resources and other CDK constructs as first class citizens.

Read our blog or watch our CNCF webinar to learn more and see a live demo of cdk8s in action.

Getting Started

See the Getting Started guide in
cdk8s Documentation.

Help & Feedback

Interacting with the community and the development team is a great way to
contribute to the project. Please consider the following venues (in order):

Documentation

See cdk8s Documentation.

Examples

See our Examples Directory.

Roadmap

See our roadmap for details about our plans for the project.

Community

See Awesome cdk8s.

If you're a cdk8s user please consider adding your name to the ADOPTERS file.

Contributing

The cdk8s project adheres to the CNCF Code of
Conduct
.

We welcome community contributions and pull requests. See our contribution
guide
for more information on how to report issues, set up a
development environment and submit code.

CDK8s.io website

See Docs Directory.

License

This project is distributed under the Apache License, Version 2.0.


File: docs/basics/api-object.md

ApiObject

An ApiObject is a construct that represents an entry in a Kubernetes manifest (level 0).
In most cases, you won't use ApiObject directly but rather use classes that
are imported through cdk8s import and which extend this base class.

By default, when you import API objects from k8s, all class names will all be
prefixed with Kube to differentiate them from the high-level APIs in cdk8s+.
It is possible to customize the prefix by providing a string value to the
--class-prefix flag, or by passing --no-class-prefix. (Note: if no prefix is
used for k8s resources, this may lead to conflicts - see
https://github.com/cdk8s-team/cdk8s/issues/140).

Resource Names

By default, cdk8s will autogenerate a name for every resource when it is instantiated.
The name is created from the path of the construct, and suffixed with a hash of that path.

For example, the following code:

ts
import { App, Chart } from 'cdk8s';
import * as kplus from 'cdk8s-plus-34';

const app = new App();
const chart = new MyChart(app, 'my-chart');

new kplus.Deployment(chart, 'deployment', {
  containers: [{ image: 'nginx' }]
});

app.synth();

generates a Deployment with the name: my-chart-deployment-c8c354dd. Where my-chart is the id of the Chart, deployment is the id of the Deployment, and c8c354dd is the hash suffix.

You can programmatically access the name of any resource in a cdk8s app by accessing the .name property of a resource. For example:

ts
const deployment = new kplus.Deployment(chart, 'deployment', {
  containers: [{ image: 'nginx' }]
});
const deploymentName = deployment.name;

To remove hashes from resource names, you can set the disableResourceNameHashes property to true:

ts
...
const chart = new MyChart(app, 'my-chart', {
    disableResourceNameHashes: true,
});
...

Which would similarly generate a Deployment with the name: my-chart-deployment. Note that disabling hashes introduces the possibility of name clashes. For example, the following code:

ts
// think of this as a custom construct
const c = new Construct(this, 'Construct');

new kplus.Deployment(c, 'Deployment', {
  containers: [{ image: 'image' }]
});

new kplus.Deployment(this, 'Construct-Deployment', {
  containers: [{ image: 'image' }]
})

will produce two deployments with the same name: chart-construct-deployment.
This happens because cdk8s uses a hyphen (-) as the delimiter between elements
in the path, so if the id of your construct also contains a hyphen (-), a collision
can occur. In addition, you can explicitly set a name for every resource by using the metadata.name property:

ts
new kplus.Deployment(c, 'Deployment', {
  metadata: {
    name: 'my-deployment'
  },
  ...
});

File: docs/basics/app.md

App

The App is the entry-point of your cdk8s application, and is the root of the constructs tree.
Every application first starts by creating an instance of an App.

ts
import { App } from 'cdk8s';

const app = new App();

Then, Charts are registered into the app:

ts
new MyChart(app, 'Chart');

And finally, the app is synthesized in order to produce the YAML manifests. Overall, your cdk8s
application will have the following high level structure to it:

ts
import { App } from 'cdk8s';

// create the app entry-point
const app = new App();

// register charts to the app
new MyChart(app, 'Chart');

// synthesize the app
app.synth();

Resolvers

Resolvers are a mechanism to inject custom logic into the cdk8s value resolution process.
It allows to transform any value just before being written to the Kubernetes manifest.

For example, lets create a resolver that adds a prefix to every resource in our application.
To define a custom resolver, create a class that implements the IResolver interface:

ts
import { IResolver, ResolutionContext } from 'cdk8s';

export class AddNamePrefixResolver implements IResolver {

  constructor(private readonly prefix: string) {}

  public resolve(context: ResolutionContext): void {

    // check if we are resolving the name property
    const isNameProperty = context.key.includes('metadata') && context.key.includes('name') && context.key.length === 2;

    // check we haven't already added a prefix. this is important because
    // resolution is recursive, so we need to avoid infinite recursion.
    const isPrefixed = typeof(context.value) === 'string' && context.value.startsWith(this.prefix);

    if (isNameProperty && !isPrefixed) {
      // replace the value with a new one
      context.replaceValue(`${this.prefix}${context.value}`);
    }
  }

}

The context argument contains information about the value that is currently being resolved:

  • obj: ApiObject currently being resolved.
  • key: Array containing the JSON path elements of the keys leading up to the value.
  • value: The original value.

It also contains the replaceValue method you should use to set a replacement value instead of the original.
When you create a cdk8s App, pass the resolver instance to it via the resolvers property:

ts
import { App, Chart } from 'cdk8s'

const app = new App({ resolvers: [new AddNamePrefixResolver('custom-prefix')] });
new Chart(app, 'Chart');

When you run cdk8s synth, your custom logic will be invoked, allowing you to replace the
original value.

When passing multiple resolvers, the first one (by natural order of the array) that invokes replaceValue is considered, and others are ignored.

One common use-case for resolvers is to fetch values from deployed infrastructure.
This allows authoring cdk8s applications that natively leverage managed cloud resources.
To that end, two specific resolvers are available that allow you to integrate cdk8s with other CDK frameworks:


File: docs/basics/chart.md

Chart

The Chart is a container that synthesizes a single Kubernetes manifest.

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

    // add contents here
  }
}

During synthesis, charts collect all the ApiObject nodes (recursively) and
emit a single YAML manifest that includes all these objects.

When a chart is defined, you can specify chart-level namespace and labels.
Those will be applied to all API objects defined within the chart (recursively):

ts
class MyChart extends Chart {
  constructor(scope: Construct, ns: string) {
    super(scope, ns, {
      namespace: 'my-namespace',
      labels: {
        app: 'my-app',
      },
    });

    new ApiObject(this, 'my-object', {
      apiVersion: 'v1',
      kind: 'Foo'
    });
  }
}

Will synthesize into:

yaml
apiVersion: v1
kind: Foo
metadata:
  namespace: my-namespace
  labels:
    app: my-app

File: docs/basics/constructs.md

Constructs

Constructs are the basic building block of cdk8s. They are the instrument
that enables composition and creation of higher-level abstractions through
normal object-oriented classes.

If you come from the Kubernetes world, you can think of constructs as
programmatically defined Helm Charts. The nice thing about constructs being
"programmatically defined" is that we can use them to leverage the full power of
object-oriented programming. For example:

  • You can express the abstraction's API using strong-typed data types
  • You can express rich interactions with methods and properties
  • You can create polymorphic programming models through interfaces and base
    classes
  • Share them through regular package managers
  • Test them using our familiar testing tools and techniques
  • Version them
  • ...and do all that stuff that we've been doing with software in the past 20
    years.

File: docs/basics/deps.md

Dependencies

You can declare dependencies between any two cdk8s constructs using the addDependency() method.

ApiObjects

For example, you can force kubernetes to first apply a Namespace before applying the Service in the scope of that namespace:

typescript
const namespace = new k8s.KubeNamespace(chart, 'backend');
const service = new k8s.KubeService(chart, 'Service', { metadata: { namespace: namespace.name }});

// declare the dependency. this is just a syntactic sugar for Node.of(service).addDependency(namespace)
service.addDependency(namespace);

cdk8s will ensure that the Namespace object is placed before the Service object in the resulting manifest:

yaml
apiVersion: v1
kind: Namespace
metadata:
  name: chart-backend-a59d2e47
---
apiVersion: v1
kind: Service
metadata:
  name: chart-service-93d02be7
  namespace: chart-backend-a59d2e47

Charts

You can also specify dependencies between charts, in exactly the same manner. For example, if we have a chart that provisions our namespace, we need that chart to be applied first:

typescript
const namespaceChart = new Chart(app, 'namespace');
const applicationChart = new Chart(app, 'application');

// declare the dependency. this is just a syntactic sugar for Node.of(applicationChart).addDependency(namespaceChart)
applicationChart.addDependency(namespaceChart);

Running cdk8s synth will produce the following dist directory:

console
> cdk8s synth

dist/0000-namespace.k8s.yaml
dist/0001-application.k8s.yaml

Notice that the namespace chart appears first with the 0000 prefix. This will ensure that a subsequent execution of kubectl apply -f dist/ will apply the namespace first, and the application second.

Custom Constructs

The behavior above applies in the same way to custom constructs that you create or use.

typescript
class Database extends Construct {
  constructor(scope: Construct, name: string) {
    super(scope, name);

    new k8s.KubeStatefulSet(this, 'StatefulSet');
    new k8s.KubeConfigMap(this, 'ConfigMap');
  }
}

const app = new App();

const chart = new Chart(app, 'Chart');

const service = new k8s.KubeService(chart, 'Service')
const database = new Database(chart, 'Database');

service.addDependency(database);

Declaring such a dependency will cause each ApiObject in the source construct, to depend on every ApiObject in the target construct.

Note that in the example above, the source construct is actually an ApiObject, which is also ok since it is essentially a construct with a single ApiObject.

Note that if the source of your dependency is a custom construct, it won't have the addDependency syntactic sugar by default, so you'll have to use Node.of().

The resulting manifest will be:

yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: chart-database-statefulset-4627f8e2
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: chart-database-configmap-676f8640
---
apiVersion: v1
kind: Service
metadata:
  name: chart-service-93d02be7

You can see that all ApiObjects of the Database construct, appear before the Service object.

Things just got cool

If you simply declare a dependency between two ApiObjects (or Constructs), that belong to two different Charts, cdk8s will create the chart dependency automatically for you.

typescript
const namespaceChart = new Chart(app, 'namespace');
const applicationChart = new Chart(app, 'application');

const namespace = new k8s.KubeNamespace(namespaceChart, 'namespace');
const deployment = new k8s.KubeDeployment(applicationChart, 'Deployment');

// dependency between ApiObjects, not Charts!
deployment.addDependency(namespace);

Running cdk8s synth will produce the same result as if explicit chart dependencies were declared:

console
> cdk8s synth

dist/0000-namespace.k8s.yaml
dist/0001-application.k8s.yaml

This means you need not be bothered with managing chart dependencies, simply work with the ApiObjects you create, and let cdk8s infer the chart dependencies.


File: docs/basics/escape-hatches.md

Escape Hatches

An "escape hatch" is an intentional leak in the abstraction layer. It allows
users to "escape the abstraction" and reach out to a lower layer.

Similarly, in CDKs, escape hatches are mechanisms that allow users to tweak the
synthesized output when the abstraction they use does not "hold water".

You may need to use an escape hatch in the following cases:

  1. You are using an imported API object (e.g. KubeDeployment) and there is an
    issue with the schema or a bug in "import" which results in an invalid
    manifest or missing fields (as an example see
    issue #140).
  2. You are using a high-level API (e.g. cdk8s+) which does not expose some
    functionality which exists in the lower-level resources.

Patching API objects directly

The ApiObject class, which is the base of all objects
synthesized into a Kubernetes manifest offers an API for patching the
synthesized output at the wire level using JSON Patch
(RFC-6902):

ts
import { JsonPatch } from 'cdk8s';
apiObject.addJsonPatch(JsonPatch.replace('/foo', 'bar'));
apiObject.addJsonPatch(JsonPatch.add('/foo/bar/0', { bar: 123 }));

During synthesis, patches are applied in-order after the API object synthesized
itself.

All classes generated using the CLI import command extend
ApiObject, and therefore include the addJsonPatch() method.

Patching API objects behind higher-level APIs

The second use case for using escape hatches is when you are working against a
higher-level construct which, for some reason, does not allow you to achieve
what you need.

For example, let's say you are using the Pod class from cdk8s+ and you wish to
set enableServiceLinks to true. This feature is currently not supported in
the cdk8s+ Pod API, so you'll want to patch the underlying KubePod and set
this value.

To do that, you will need to "peak" into the construct tree and find the underlying
API object, so you can apply the patch to it:

ts
import { Pod } from 'cdk8s-plus-34';
import { ApiObject } from 'cdk8s';

const pod = new Pod(...);
const kubePod = ApiObject.of(pod);
kubePod.addJsonPatch(...);

The ApiObject.of() method uses capabilities of the constructs programming
model
to find the "default child" of a construct
(Node.of(c).defaultChild). When a construct is initialized, it can either
explicitly assign the value of Node.of(this).defaultChild = xxx or it can use
the identity Default for one of its child constructs. This will automatically
identify it as the default child.

!!! tip
The ApiObject.of() method recursively searches down the construct tree
through child constructs called Default until it finds a child of type
ApiObject. This means, for example, that ApiObject.of(apiObject) returns
the same object.

There could be situations where a default child is not recorded by a high-level
construct. This still does not mean you are blocked from patching the underlying
API objects. You can still use Node.of(x) to traverse the construct tree to
obtain the child. For example, you can use Node.of(x).findChild(id) to
retrieve any child by its ID.


File: docs/basics/helm.md

Helm Support

You can use the Helm construct in order to include Helm
charts.

!!! note
You will need helm to be installed locally for this feature.
For accessing private helm repositories, you must be authenticated to the repository in a way that the helm pull command recognizes.

The following example adds the
bitnami/redis
Helm chart with sentinel containers enabled:

The Bitnami helm repo needs to be added through: helm repo add bitnami https://charts.bitnami.com/bitnami

For accessing private helm repositories, you must be authenticated to the repository in a way that the helm template
command recognizes.

ts
class MyChart extends cdk8s.Chart {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    const redis = new Helm(this, 'redis', {
      chart: 'bitnami/redis',
      values: {
        sentinel: {
          enabled: true
        }
      }
    });
  }
}

The Helm construct will render the manifest from the specified chart by
executing helm template. If values is specified, these values will override
the default values included with the chart.

The name option can be used to specify the chart's release name.
If not specified, a valid and unique release name will be allocated
based on the construct path.

The Helm construct extends Include and inherits its API. For example, you
can use the apiObjects property to find and interact with included API
objects.

The following example shows how to add an annotation to the Redis master
deployment:

ts
const master = redis.apiObjects.find(o => o.name === 'foo-redis-master');
master.metadata.addAnnotation('my.annotation', 'hey-there');

File: docs/basics/include.md

Include

The Include construct can be used to include an existing manifest in a chart.

The following example will include the Kubernetes Dashboard in MyChart:

ts
import { Include } from 'cdk8s';

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

    const dashboard = new Include(this, 'dashboard', {
      url: 'https://raw.githubusercontent.com/kubernetes/dashboard/v2.4.0/aio/deploy/recommended.yaml',
      // or
      url: 'dashboard.yaml'
    });

    // ...other resources
  }
}

All API objects defined in the included manifest will be added as children
ApiObjects under the Include construct's scope and can be accessed
through the apiObject property:

The following example queries for all the Deployment resources in the
dashboard:

ts
const deploymentApiObject = dashboard.apiObjects.find(c => c.kind === 'Deployment');

NOTE: names of included objects (metadata.name) are preserved. This means that
if you try to include the same manifest twice into the same chart, your manifest
will have duplicate definitions of the same objects.


File: docs/basics/index.md

Basic concepts of cdk8s

This section discusses core concepts in cdk8s.

!!! info

text
The documentation here relates to version `2.x` of the cdk8s toolchain, which is the latest. If you are still using version `1.x`, please refer to the [Migrating from 1.x Guide](../migrating-from-1.x.md).