Skip to content

CT vs Helm

FeatureCTHelm
LanguageTypeScriptGo templates
IDE supportFull IntelliSense, autocomplete, jump-to-definitionNone (text templates)
Type safetyCompile-time — Zod validation + generated .d.tsRuntime only — silent failures on typos
ValuesPlain JS objects, typed with DeepPartial<T>Flat YAML values.yaml with no schema by default
LogicReal functions, loops, imports{{ if }}, {{ range }}, {{ include }}
ReuseES module imports from any Git URLChart dependencies, subcharts, library charts
PackagingGit repo + tag — no registry neededOCI / ChartMuseum registry
Learning curveKnow TypeScript? You’re done.Go template DSL, sprig, helpers, _helpers.tpl
Outputct template → YAMLhelm template → YAML
CT
Helm
import { ingress } from "github.com/cloudticon/k8s@master";
if (Values.ingress?.enabled) {
ingress({
name: Values.name,
labels: Values.labels,
annotations: Values.ingress.annotations,
tls: Values.ingress.tls,
rules: Values.ingress.hosts.map(h => ({
host: h.host,
paths: h.paths.map(p => ({
path: p.path,
pathType: p.pathType,
service: Values.name,
port: p.port,
})),
})),
});
}
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "myapp.fullname" $ }}
port:
number: {{ .port }}
{{- end }}
{{- end }}
{{- end }}

Real if, real .map(), real variables. No nindent, no toYaml, no include.

CT — values.json + generated types
Helm — values.yaml
// values.json
{
"replicas": 3,
"image": "nginx:1.25",
"host": "app.example.com"
}
// auto-generated by ct types
interface CtValues {
replicas: number;
image: string;
host: string;
}
replicaCount: 3
image:
repository: nginx
tag: "1.25"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
ingress:
enabled: true
hosts:
- host: app.example.com
paths:
- path: /
pathType: Prefix
port: 80

Typo in replicaCount? Helm won’t tell you — it silently uses the default. With CT you get an immediate red squiggly in VS Code. No deploy needed.

CT
Helm
import { createPostgres }
from "github.com/my-org/k8s-db@v2.0.0/postgres";
import { createRedis }
from "github.com/my-org/k8s-cache@v1.3.0/redis";
createPostgres({ name: "db", instances: 2 });
createRedis({ name: "cache", size: "1Gi" });
charts/
postgresql/
Chart.yaml
values.yaml
templates/
deployment.yaml
service.yaml
...
redis/
Chart.yaml
...
Chart.yaml # dependencies block
values.yaml # override nested subchart values

Updating a subchart means helm dependency update, version pinning, repository registration. Breaking changes cascade through values.yaml overrides. CT uses standard ES imports — pin to a tag, update with a one-line version bump. No registry, no Chart.yaml, no dependency update dance.

CT
Helm
function appName(name: string): string {
return `app-${name}`.slice(0, 63);
}
function labels(name: string) {
return {
"app.kubernetes.io/name": appName(name),
"app.kubernetes.io/managed-by": "ct",
};
}
{{- define "myapp.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- define "myapp.labels" -}}
helm.sh/chart: {{ include "myapp.chart" . }}
app.kubernetes.io/name: {{ include "myapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

Standard TypeScript. Testable. Importable. No magic.

  • You consume third-party charts (Bitnami, Prometheus community) — they ship as Helm charts.
  • Your org already has a large Helm-based GitOps pipeline and migration cost is high.
  • You need Helm hooks (pre-install, post-upgrade) for migration jobs.

CT can coexist — render CT manifests into a GitOps repo alongside Helm releases.

  1. Keep existing Helm charts running.
  2. Start new services with ct init.
  3. Extract shared patterns into CT factory packages.
  4. Gradually replace Helm charts with CT when they need changes anyway.