Skip to content

CT vs Pulumi

FeatureCTPulumi
ScopeKubernetes manifests onlyAny cloud resource (AWS, GCP, Azure, K8s, …)
LanguageTypeScript (Goja runtime)TypeScript, Python, Go, C#, Java, YAML
OutputStatic YAML / JSONImperative API calls (real provisioning)
StateStateless — output is YAML, no state fileStateful — state stored in Pulumi Cloud / S3 / local
Apply modelct template → YAML → kubectl / ArgoCDpulumi up — provisions directly
IDE supportFull IntelliSense via generated .d.tsFull IntelliSense (native TypeScript SDK)
Drift detectionGitOps tool responsibility (ArgoCD, Flux)Built-in (pulumi refresh)
Learning curveKnow TypeScript + K8s YAML? You’re done.TypeScript + Pulumi resource model + provider SDKs
PackagingGit URL importsnpm / PyPI / NuGet packages

This is the key distinction: CT and Pulumi solve different problems.

┌─────────────────────────────────┐
│ Cloud Infrastructure │ ← Pulumi / Terraform
│ (VPCs, databases, DNS, IAM) │
├─────────────────────────────────┤
│ Kubernetes Manifests │ ← CT
│ (Deployments, Services, CRDs) │
├─────────────────────────────────┤
│ GitOps / Apply │ ← ArgoCD, Flux, kubectl
└─────────────────────────────────┘

Pulumi can also manage Kubernetes resources via its @pulumi/kubernetes provider, but it does so imperatively — it calls the Kubernetes API directly and tracks state. CT generates declarative YAML that feeds into your existing GitOps pipeline.

CT — declarative generation
Pulumi — imperative provisioning
import { deployment, service }
from "github.com/cloudticon/k8s@master";
deployment({
name: "api",
replicas: Values.replicas,
image: Values.image,
port: 8080,
});
service({
name: "api",
port: 80,
targetPort: 8080,
});
import * as k8s from "@pulumi/kubernetes";
const app = new k8s.apps.v1.Deployment("api", {
metadata: { namespace: "production" },
spec: {
replicas: 3,
selector: { matchLabels: { app: "api" } },
template: {
metadata: { labels: { app: "api" } },
spec: {
containers: [{
name: "api",
image: "myapp:1.0",
ports: [{ containerPort: 8080 }],
}],
},
},
},
});
const svc = new k8s.core.v1.Service("api", {
metadata: { namespace: "production" },
spec: {
selector: { app: "api" },
ports: [{ port: 80, targetPort: 8080 }],
},
});

Pulumi runs pulumi up, connects to the Kubernetes API, creates/updates resources, and stores state. CT runs ct template, outputs YAML, and you apply it however you want — kubectl, ArgoCD, Flux.

Pulumi maintains a state file that tracks every resource it manages. This enables drift detection, dependency graphs, and safe destruction. But it also means:

  • You need a state backend (Pulumi Cloud, S3, local file).
  • Concurrent deploys require locking.
  • Importing existing resources requires pulumi import.
  • Deleting the state loses track of managed resources.

CT is stateless. It generates YAML. Nothing to lock, nothing to corrupt, nothing to import. Your GitOps tool handles the state of what’s deployed.

CT — functions and imports
Pulumi — ComponentResource
import { deployment, service }
from "github.com/cloudticon/k8s@master";
function createApp(
name: string, image: string, port: number,
) {
deployment({
name, image, port,
replicas: Values.replicas,
});
service({ name, port: 80, targetPort: port });
}
createApp("api", Values.apiImage, 8080);
createApp("worker", Values.workerImage, 9090);
class MyApp extends pulumi.ComponentResource {
constructor(
name: string,
opts?: pulumi.ComponentResourceOptions,
) {
super("myorg:MyApp", name, {}, opts);
// create child resources
}
}

Plain functions. No resource model to learn. Import from any Git URL.

  • You need to provision cloud infrastructure (VPCs, RDS, CloudFront, IAM) — CT doesn’t do this.
  • You want one tool for both infrastructure and Kubernetes resources.
  • You need built-in drift detection and destroy capabilities.
  • You manage multi-cloud deployments beyond Kubernetes.
  • You only need Kubernetes manifests and already have a GitOps pipeline.
  • You want stateless generation — no state file, no backend, no locking.
  • You want simple TypeScript without Pulumi’s resource model, outputs, and async patterns.
  • You want generated YAML you can review in a PR before it reaches the cluster.

CT and Pulumi complement each other well:

Pulumi → provisions infrastructure (VPC, RDS, EKS cluster)
CT → generates Kubernetes manifests for apps running on that cluster
ArgoCD → syncs CT-generated manifests to the cluster

Pulumi manages the cluster, CT manages what runs on it.