Skip to content

CT vs Kustomize

FeatureCTKustomize
LanguageTypeScriptYAML patches + overlays
IDE supportFull IntelliSense, autocomplete, jump-to-definitionYAML schema only
Type safetyCompile-time — generated .d.tsNone — patches fail silently on wrong paths
Values / configTyped JS objects (values.json)configMapGenerator, secretGenerator, vars
LogicReal functions, loops, conditionalsStrategic merge patches, JSON patches
ReuseES module imports from any Git URLbases / components references
CompositionFunction calls + importsDirectory-based overlays
Learning curveKnow TypeScript? You’re done.Patch mechanics, overlay hierarchy, transformer order
Outputct template → YAMLkustomize build → YAML

Kustomize starts from base YAML and patches it. CT generates YAML from code. This is a fundamental difference.

CT
Kustomize
import { deployment, service, hpa }
from "github.com/cloudticon/k8s@master";
const env = Values.env; // "dev" | "staging" | "prod"
deployment({
name: `${env === "dev" ? "dev-" : ""}api`,
namespace: Values.namespace,
replicas: Values.replicas,
image: Values.image,
});
service({
name: "api",
namespace: Values.namespace,
port: 80,
targetPort: 8080,
});
if (env === "prod") {
hpa({
name: "api",
minReplicas: Values.replicas,
maxReplicas: Values.maxReplicas,
});
}
base/
# deployment.yaml, service.yaml, kustomization.yaml
# overlays/
# dev/ → kustomization.yaml, replicas-patch.yaml
# staging/ → kustomization.yaml
# prod/ → kustomization.yaml, hpa.yaml
# overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
- path: replicas-patch.yaml
namePrefix: dev-
namespace: development
---
# overlays/dev/replicas-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 1

One file, real if, real variables. No directory trees, no patch files, no mental model of “what is the base + what patches apply in what order.”

CT
Kustomize
Terminal window
ct template . \
--values values-dev.json \
--namespace dev
ct template . \
--values values-staging.json \
--namespace staging
ct template . \
--values values-prod.json \
--namespace prod
overlays/
# dev/
# kustomization.yaml
# staging/
# kustomization.yaml
# prod/
# kustomization.yaml
#
# Each environment is a directory with its own
# kustomization.yaml that references the base
# and adds patches. Shared changes must be
# replicated or extracted into components.

Same template, different values. No directory duplication.

CT
Kustomize
if (Values.hpa?.enabled) {
hpa({
name: "api",
minReplicas: Values.hpa.min,
maxReplicas: Values.hpa.max,
});
}
components/hpa/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
resources:
- hpa.yaml
---
# overlays/prod/kustomization.yaml
components:
- ../../components/hpa
# Components can only be included or excluded
# per overlay — no runtime conditionals.
CT
Kustomize
const labels = {
"app.kubernetes.io/name": "myapp",
"app.kubernetes.io/managed-by": "ct",
};
deployment({
name: "myapp-api",
labels,
// ...
});
kustomization.yaml
commonLabels:
app: myapp
team: platform
namePrefix: myapp-
namespace: production
# Labels are injected everywhere including
# selectors, which can cause issues with
# immutable fields. namePrefix/nameSuffix
# apply blindly to all resource names.

You decide what gets labeled and how. No transformer surprises.

When a patch doesn’t apply correctly, it fails silently or produces unexpected output. You debug by running kustomize build and diffing the output with expectations. Strategic merge patches can be especially confusing with arrays.

TypeScript errors show at write time in the editor. Runtime errors include stack traces. You can console.log intermediate values. Output is deterministic — same values always produce same YAML.

  • You have existing YAML manifests and want minimal changes (just patches on top).
  • Your team doesn’t know TypeScript but is comfortable with YAML.
  • You use kubectl apply -k in a simple CI pipeline and don’t need more.
  • You consume upstream YAML manifests (e.g. from a vendor) and just need to adjust namespace/labels.

CT can coexist — render CT manifests alongside Kustomize overlays in the same GitOps repo.

  1. Keep existing Kustomize bases running.
  2. Start new services with ct init.
  3. Extract shared patterns into CT factory packages.
  4. When a Kustomize overlay becomes too complex (3+ patch files, nested components), rewrite it as CT.