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
- Overview
- Getting Started
- Help & Feedback
- Documentation
- Examples
- Roadmap
- Community
- Contributing
- CDK8s.io website
- License
Repositories
This project consists of multiple packages, maintained and released via the following repositories:
- cdk8s - Core library. For historical reasons note that the
cdk8spackage is maintained in thecdk8s-team/cdk8s-corerepository. - 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):
- Search open issues
- Stack Overflow: cdk8s
- File a new issue
- Slack: #cdk8s channel in cdk.dev
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:
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:
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:
...
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:
// 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:
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.
import { App } from 'cdk8s';
const app = new App();Then, Charts are registered into the app:
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:
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:
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:
ApiObjectcurrently 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:
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
replaceValueis 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.
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):
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:
apiVersion: v1
kind: Foo
metadata:
namespace: my-namespace
labels:
app: my-appFile: 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:
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:
apiVersion: v1
kind: Namespace
metadata:
name: chart-backend-a59d2e47
---
apiVersion: v1
kind: Service
metadata:
name: chart-service-93d02be7
namespace: chart-backend-a59d2e47Charts
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:
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:
> cdk8s synth
dist/0000-namespace.k8s.yaml
dist/0001-application.k8s.yamlNotice 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.
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
addDependencysyntactic sugar by default, so you'll have to useNode.of().
The resulting manifest will be:
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-93d02be7You 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.
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:
> cdk8s synth
dist/0000-namespace.k8s.yaml
dist/0001-application.k8s.yamlThis 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:
- 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). - 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):
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 extendApiObject, 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:
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.
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:
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:
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 childrenApiObjects 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:
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
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).