Skip to content

CT vs Terraform

FeatureCTTerraform
ScopeKubernetes manifests onlyAny cloud resource (AWS, GCP, Azure, K8s, …)
LanguageTypeScriptHCL (HashiCorp Configuration Language)
OutputStatic YAML / JSONAPI calls via providers (real provisioning)
StateStateless — no state fileStateful — terraform.tfstate required
Apply modelct template → YAML → kubectl / ArgoCDterraform apply — provisions directly
IDE supportFull IntelliSense via generated .d.tsHCL extension (limited vs TypeScript)
Drift detectionGitOps tool responsibilityBuilt-in (terraform plan)
Learning curveKnow TypeScript + K8s YAML? You’re done.HCL syntax + provider schemas + state mechanics
Modules / reuseES module imports from Git URLsTerraform modules from registry / Git

Like Pulumi, Terraform operates at a different layer than CT:

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

Terraform can manage Kubernetes resources via the kubernetes and helm providers, but it’s designed for infrastructure, not for templating application manifests at scale.

CT
Terraform
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,
});
resource "kubernetes_deployment" "api" {
metadata {
name = "api"
namespace = "production"
}
spec {
replicas = 3
selector {
match_labels = {
app = "api"
}
}
template {
metadata {
labels = {
app = "api"
}
}
spec {
container {
name = "api"
image = "myapp:1.0"
port {
container_port = 8080
}
}
}
}
}
}
resource "kubernetes_service" "api" {
metadata {
name = "api"
namespace = "production"
}
spec {
selector = {
app = "api"
}
port {
port = 80
target_port = 8080
}
}
}

Concise factory helpers vs verbose HCL resource blocks. The provider translates HCL to Kubernetes API calls and tracks state. CT just outputs YAML.

Terraform requires a state file (terraform.tfstate) that maps every managed resource to its real-world counterpart. This means:

  • You need a state backend (S3, Terraform Cloud, local file, Consul, …).
  • Concurrent runs need state locking to prevent corruption.
  • Losing state means losing track of what Terraform manages — manual imports required.
  • terraform plan shows a diff against state, not against the cluster directly.
  • Sensitive values can end up in state.

CT is stateless. It generates YAML. The only “state” is your Git repo and whatever your GitOps tool tracks. Nothing to lock, nothing to corrupt, nothing to back up.

CT
Terraform
deployment({
name: "api",
replicas: Values.replicas,
image: Values.image,
port: 8080,
});
if (Values.hpa?.enabled) {
hpa({
name: "api",
minReplicas: Values.replicas,
maxReplicas: Values.hpa.max,
});
}
resource "kubernetes_deployment" "api" {
count = var.enable_api ? 1 : 0
metadata {
name = "api"
}
spec {
replicas = var.replicas
}
}
resource "kubernetes_horizontal_pod_autoscaler" "api" {
count = var.enable_hpa ? 1 : 0
metadata {
name = "api"
}
spec {
min_replicas = var.replicas
max_replicas = var.max_replicas
scale_target_ref {
api_version = "apps/v1"
kind = "Deployment"
name = kubernetes_deployment.api[0].metadata[0].name
}
}
}

count and for_each are the primary conditional mechanisms in HCL. Referencing conditional resources requires index syntax ([0]). CT uses real if, real variables, real functions.

CT
Terraform
import { createApp }
from "github.com/my-org/k8s-helpers@v1.0/app";
createApp({
name: "api",
image: Values.image,
replicas: Values.replicas,
});
module "api" {
source = "git::https://github.com/my-org/tf-modules.git//k8s-app?ref=v1.0"
name = "api"
image = var.api_image
replicas = var.replicas
}

Standard ES imports. Pin to a tag. No special module structure, no variable/output declarations.

  • You need to provision cloud infrastructure (VPCs, RDS, Route53, IAM) — CT doesn’t do this.
  • Your org standardizes on Terraform and the kubernetes provider is good enough for your K8s needs.
  • You need terraform plan for change preview including infrastructure dependencies.
  • You manage resources across multiple providers in a single configuration.
  • You only need Kubernetes manifests and already have a GitOps pipeline.
  • You want stateless generation — no tfstate, no backend, no locking headaches.
  • You want real TypeScript instead of HCL for logic, loops, and composition.
  • You want concise manifests — CT factories vs verbose HCL resource blocks.
  • You want generated YAML you can review in a PR before it reaches the cluster.

CT and Terraform are natural complements:

Terraform → provisions infrastructure (VPC, EKS, RDS, DNS)
CT → generates Kubernetes manifests for apps
ArgoCD → syncs CT-generated manifests to the EKS cluster

Terraform manages the platform, CT manages the workloads running on it. No overlap, no conflict.