Skip to content
6 min read·Lesson 8 of 8

cdk8s: Kubernetes Manifests in TypeScript

cdk8s — the CDK for Kubernetes — generates YAML manifests from TypeScript, bringing type safety, loops, and reuse to Kubernetes configuration.

cdk8s (CDK for Kubernetes) applies the CDK construct model to Kubernetes configuration. Instead of writing 300 lines of YAML for a deployment + service + HPA + NetworkPolicy, you write 50 lines of TypeScript and run cdk8s synth to produce the YAML. Same construct library model, same testing approach — just a different target format.

Setup

npm install -g cdk8s-cli
cdk8s init typescript-app

# Or add to an existing project
npm install cdk8s cdk8s-plus-31        # plus-31 = Kubernetes 1.31 API
my-k8s-config/
├── main.ts           # entry point
├── dist/             # generated YAML
├── cdk8s.yaml        # config
└── package.json

A First Chart

// main.ts
import { App, Chart, ApiObjectMetadata } from 'cdk8s'
import * as kplus from 'cdk8s-plus-31'
import { Construct } from 'constructs'

interface WebAppChartProps {
  namespace: string
  image: string
  replicas: number
  tag: string
}

class WebAppChart extends Chart {
  constructor(scope: Construct, id: string, props: WebAppChartProps) {
    super(scope, id, { namespace: props.namespace })

    const labels = { app: 'web', version: props.tag }

    const deployment = new kplus.Deployment(this, 'Deployment', {
      replicas: props.replicas,
      metadata: { labels },
      select: false,
      podMetadata: { labels },
      containers: [{
        name: 'web',
        image: `${props.image}:${props.tag}`,
        ports: [{ number: 8080 }],
        resources: {
          cpu: { request: kplus.Cpu.millis(100), limit: kplus.Cpu.millis(500) },
          memory: {
            request: kplus.Size.mebibytes(128),
            limit: kplus.Size.mebibytes(512),
          },
        },
        readiness: kplus.Probe.fromHttpGet('/healthz', { port: 8080 }),
        liveness: kplus.Probe.fromHttpGet('/healthz', { port: 8080 }),
      }],
    })

    const service = deployment.exposeViaService({
      name: 'web',
      serviceType: kplus.ServiceType.CLUSTER_IP,
      ports: [{ port: 80, targetPort: 8080 }],
    })

    new kplus.Ingress(this, 'Ingress', {
      rules: [{
        host: `web.${props.namespace}.acme.io`,
        backend: kplus.IngressBackend.fromService(service),
      }],
    })

    new kplus.HorizontalPodAutoscaler(this, 'HPA', {
      target: deployment,
      minReplicas: props.replicas,
      maxReplicas: props.replicas * 5,
      metrics: [kplus.Metric.resourceCpu(kplus.MetricTarget.averageUtilization(60))],
    })
  }
}

const app = new App()
new WebAppChart(app, 'web', {
  namespace: 'production',
  image: 'ghcr.io/acme/web',
  replicas: 3,
  tag: process.env.IMAGE_TAG ?? 'latest',
})
app.synth()

Synthesise

cdk8s synth

# Produces: dist/web.k8s.yaml
# One file per Chart; apply with:
kubectl apply -f dist/

The output is standard Kubernetes YAML — no runtime dependency on cdk8s. Any tool that handles YAML (kubectl, Argo CD, Flux, Helm's helm template pipeline) can consume it.

Type Safety in Action

The biggest win over raw YAML is compile-time checking:

// This fails at compile time — 'Deploymnet' is not a valid class
new kplus.Deploymnet(this, 'Typo', { ... })

// This fails — replicas must be a number
replicas: "three"

// This fails — CPU limit can't be less than request
resources: {
  cpu: { request: kplus.Cpu.millis(500), limit: kplus.Cpu.millis(100) }  // TypeScript will catch this pattern in strongly typed wrappers
}

Your editor autocompletes kplus.ServiceType. to show valid values. Misspelled resource names are caught before kubectl apply.

Raw API Objects — the Escape Hatch

When cdk8s-plus doesn't have a construct (custom CRDs, rare resources), use the raw ApiObject:

import { ApiObject } from 'cdk8s'

// A CRD instance
new ApiObject(this, 'Certificate', {
  apiVersion: 'cert-manager.io/v1',
  kind: 'Certificate',
  metadata: { name: 'web-tls', namespace: props.namespace },
  spec: {
    secretName: 'web-tls-secret',
    dnsNames: [`web.${props.namespace}.acme.io`],
    issuerRef: { name: 'letsencrypt-prod', kind: 'ClusterIssuer' },
  },
})

// Generated CRD bindings
// cdk8s import generates type-safe wrappers from a CRD URL or cluster:
// cdk8s import cert-manager.io/Certificate@v1 --language typescript

Importing Existing CRDs

# From a URL
cdk8s import https://github.com/cert-manager/cert-manager/releases/download/v1.15.0/cert-manager.crds.yaml

# From a running cluster
cdk8s import k8s --language typescript

# From Helm chart CRDs
helm template cert-manager jetstack/cert-manager --include-crds |   cdk8s import /dev/stdin --language typescript

The generated classes expose the CRD schema as TypeScript interfaces — full type safety for custom resources.

Multi-Tenant Patterns

Where cdk8s shines most: generating near-identical workloads for many tenants or namespaces:

interface Tenant { name: string; namespace: string; resources: ResourceQuota }

const tenants: Tenant[] = JSON.parse(fs.readFileSync('tenants.json', 'utf8'))

const app = new App()
for (const tenant of tenants) {
  new WebAppChart(app, `web-${tenant.name}`, {
    namespace: tenant.namespace,
    image: 'ghcr.io/acme/web',
    replicas: 2,
    tag: process.env.IMAGE_TAG!,
  })
}
app.synth()

Producing 100 correct, consistent manifests for 100 tenants is a loop — no copy-paste drift.

Testing cdk8s

Same assertion API as CDK:

import { Testing } from 'cdk8s'
import { WebAppChart } from '../lib/web-app-chart'

describe('WebAppChart', () => {
  test('deployment has correct resource limits', () => {
    const app = Testing.app()
    const chart = new WebAppChart(app, 'test', {
      namespace: 'test',
      image: 'ghcr.io/acme/web',
      replicas: 2,
      tag: 'latest',
    })
    const results = Testing.synth(chart)

    const deployment = results.find((r: any) => r.kind === 'Deployment')
    expect(deployment).toBeDefined()

    const container = deployment.spec.template.spec.containers[0]
    expect(container.resources.limits.cpu).toBe('500m')
    expect(container.resources.limits.memory).toBe('512Mi')
  })

  test('has an HPA', () => {
    const app = Testing.app()
    const chart = new WebAppChart(app, 'test', { /* ... */ })
    const results = Testing.synth(chart)
    expect(results.some((r: any) => r.kind === 'HorizontalPodAutoscaler')).toBe(true)
  })
})

GitOps Integration

cdk8s fits perfectly into a GitOps pipeline:

# .github/workflows/manifests.yml
on: [push]
jobs:
  synth:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '22', cache: npm }
      - run: npm ci
      - run: npm test
      - run: cdk8s synth
      - name: Commit and push synthesised YAML
        run: |
          git config user.name github-actions
          git config user.email actions@github.com
          git add dist/
          git diff --staged --quiet || git commit -m "chore: synth manifests [skip ci]"
          git push

The dist/ directory contains the synthesised YAML that Argo CD or Flux watches.

Where cdk8s Fits

Use caseTool
Many similar workloads (multi-tenant, per-env)cdk8s
Reusable internal components with type safetycdk8s
Simple single-workload YAMLRaw YAML (simpler)
Community sharing / package distributionHelm (larger ecosystem)
Runtime values and secretsHelm + values.yaml or External Secrets

The TypeScript IaC Stack — Full Picture

  • cdk8s — Kubernetes manifests (TypeScript → YAML)
  • AWS CDK — AWS infrastructure (TypeScript → CloudFormation)
  • Pulumi — multi-cloud (TypeScript → provider APIs)
  • All three share: construct model, TypeScript type system, Jest testing, npm packaging

Skills transfer across all three. A team fluent in CDK picks up cdk8s in a day and Pulumi in a week. The investment in TypeScript for infrastructure pays dividends across the entire cloud-native stack.

Further Reading

Key Takeaways

  • cdk8s synthesises Kubernetes YAML from TypeScript — same construct model as CDK.
  • cdk8s-plus provides opinionated, type-safe abstractions over raw Kubernetes resources.
  • The Chart is the unit of organisation; cdk8s synth produces one YAML file per Chart.
  • Combine with Argo CD or Flux for GitOps: commit the synthesised YAML, let the cluster reconcile.
  • cdk8s is especially powerful for multi-tenant charts that create many similar workloads programmatically.
🎉

Course Complete!

You've finished TypeScript for Cloud: CDK, Pulumi, and IaC. Now put your knowledge to the test with real exam-style practice questions.