Deployment Modes
TypeKro supports multiple deployment strategies. Choose based on your workflow and requirements.
Quick Decision Matrix
| Mode | Best For | Requires Kro Controller | CEL Evaluation |
|---|---|---|---|
| Direct | Development, testing, simple deployments | No | At deploy time |
| Kro | Production, runtime dependencies, GitOps | Yes | At runtime (continuous) |
| Alchemy | Production orchestration with Alchemy framework | No | At deploy time |
| Auto | Automatic mode selection based on environment | Depends | Depends |
| YAML Generation | GitOps workflows, CI/CD pipelines | No | N/A |
Note:
automode is reserved for future use. When specified inDeploymentOptions.mode, it will automatically selectdirectorkrobased on whether a Kro controller is detected in the cluster. Currently, usedirectorkroexplicitly.Note: TypeKro deploys through the Alchemy v2 runtime declaratively, providing state management and lifecycle tracking. Call
factory.toAlchemyResources(spec)to emit per-resource declarations, then materialize them inside an AlchemyStack(see Alchemy Integration). The v1alchemyScopefactory option has been removed.
Value Resolution Behavior
The table below shows how each value type is handled across factory modes and operations.
Key: Resolve = substituted with concrete value | CEL = emitted as ${...} expression | Error = throws with guidance
deploy(spec) — deploys resources to the cluster
| Value Type | Direct Mode | Kro Mode |
|---|---|---|
| Literal (compile-time known) | Resolve | Embedded in CR instance |
schema.spec.* (magic proxy) | Resolve (from spec) | Embedded in CR instance |
$field (forced KubernetesRef) | Resolve (from spec) | Embedded in CR instance |
resources.X.status.Y (cross-resource ref) | Resolve (from live cluster, level-by-level) | Kro controller resolves at runtime |
Cel.expr() | Evaluate via angular-expressions at deploy time | Kro controller evaluates at runtime |
Cel.template() | Evaluate via angular-expressions at deploy time | Kro controller evaluates at runtime |
Template literal (`${schema.spec.name}-app`) | Resolve (marker string → spec value) | Kro controller resolves ${schema.spec.name} |
includeWhen / forEach / readyWhen | Evaluated by composition re-execution | Emitted as Kro directives |
toYaml(spec) — generates YAML offline (no cluster access)
| Value Type | Direct Mode | Kro Mode |
|---|---|---|
| Literal (compile-time known) | Resolve | Embedded in CR instance |
schema.spec.* (magic proxy) | Resolve (from spec) | Embedded in CR instance |
$field (forced KubernetesRef) | Error — Kro optional access (.?field) requires Kro | CEL ${resource.data.?field} |
resources.X.status.Y (cross-resource ref) | Error — needs cluster state | CEL ${X.status.Y} |
Cel.expr() | Error — explicit CEL requires Kro or deploy() | CEL ${expression} |
Cel.template() | Error — explicit CEL requires Kro or deploy() | CEL ${template} |
Template literal (`${schema.spec.name}-app`) | Resolve (marker string → spec value) | CEL ${schema.spec.name}-app |
includeWhen / forEach / readyWhen | Evaluated by composition re-execution | Emitted as Kro directives |
resourceGraph.toYaml() — generates Kro ResourceGraphDefinition YAML (no spec)
All references are emitted as CEL expressions for the Kro controller. This is always Kro-mode output regardless of how you later create factories.
Every emitted status expression is checked against both CEL engines before the ResourceGraphDefinition is produced: cel-js, which direct mode evaluates with, and a curated denylist of confirmed cel-go divergences. A form only one engine accepts is reported with the status leaf, the expression, and the dialect that rejects it — as a warning by default, or as a serialization failure under strictCelDiagnostics / TYPEKRO_STRICT_CEL=1. See Dual-Dialect Validation.
Why does direct mode
toYaml()error on CEL/KubernetesRef?Direct mode
toYaml()generates plain Kubernetes manifests. These must be valid YAML thatkubectl applycan process. CEL expressions and cross-resource references have no meaning outside of Kro. If your resource graph uses these features, usedeploy()(which resolves everything at runtime) orfactory('kro')(which generates Kro-managed YAML).
When is Kro Required?
Direct mode deploys resources immediately and evaluates CEL expressions once at deployment time. No additional controllers needed.
Kro mode creates ResourceGraphDefinitions that the Kro controller manages. CEL expressions are evaluated continuously against live cluster state.
| Feature | Direct Mode | Kro Mode |
|---|---|---|
| Resource deployment | ✅ Immediate | ✅ Via Kro controller |
| Cross-resource references | ✅ Resolved at deploy time | ✅ Resolved at runtime |
| Status expressions | ✅ Evaluated once | ✅ Continuously updated |
| Runtime dependencies | ❌ Static values only | ✅ Live cluster state |
| Continuous reconciliation | ❌ No | ✅ Yes |
| Controller required | ❌ No | ✅ Kro controller |
Use Direct mode when:
- Developing and testing locally
- Simple deployments without runtime dependencies
- You don't want to install additional controllers
Use Kro mode when:
- Resources need to reference each other's live state
- You want continuous reconciliation
- Status should update as cluster state changes
Direct Deployment
Deploy resources immediately to any Kubernetes cluster. No additional controllers required.
const factory = webapp.factory('direct', { namespace: 'dev' });
await factory.deploy({ name: 'my-app', image: 'nginx:latest', replicas: 2 });When to use:
- Local development and rapid iteration
- Testing compositions before production
- Simple deployments without runtime dependencies
- Teams not ready to install Kro controller
How it works:
- TypeKro resolves all references at deployment time
- Resources deploy in dependency order
- Waits for readiness (configurable)
- Returns live status from cluster
Status Fields Resolve Independently
Each status field is resolved on its own. A field that cannot be resolved — most often a CEL expression reaching into an optional nested field a controller has not populated yet, such as service.status.loadBalancer.ingress on a fresh LoadBalancer Service — comes back undefined. Its siblings, and sibling subtrees, keep their resolved values, so a composition that is in fact ready still reports ready, failed and phase.
Each failing field is logged with its path and error, and recorded on the returned status object as a diagnostic:
import { getStatusLeafDiagnostics } from 'typekro';
const app = await factory.deploy({ name: 'my-app' });
app.status.ready; // true — resolved normally
app.status.loadBalancerIp; // undefined — this leaf failed
for (const diagnostic of getStatusLeafDiagnostics(app.status)) {
console.log(diagnostic.path, diagnostic.expression, diagnostic.error.message);
}The diagnostics are attached non-enumerably, so they never appear in Object.keys(), JSON, or emitted YAML.
If a status field is expected to be absent for a while, write the expression so both engines return a fallback rather than erroring — Cel.firstWhereHas() and Cel.loadBalancerAddress() do exactly that. See CEL Expressions.
Resolution never writes into the composition's status template, so the same composition resolves correctly on every reconcile and for every instance.
Streaming Control Plane Logs
Enable real-time Kubernetes event streaming during deployment:
const factory = webapp.factory('direct', {
namespace: 'dev',
eventMonitoring: {
enabled: true,
eventTypes: ['Normal', 'Warning', 'Error'],
includeChildResources: true
},
debugLogging: {
enabled: true,
statusPolling: true,
readinessEvaluation: true,
verboseMode: true
},
progressCallback: (event) => {
console.log(`[${event.type}]`, event);
}
});Environment Variables for Debugging
# Set log level (trace, debug, info, warn, error, fatal)
export TYPEKRO_LOG_LEVEL=debug
# Enable debug mode for factory operations
export TYPEKRO_DEBUG=true
# Enable pretty-printed logs for development
export TYPEKRO_LOG_PRETTY=trueKro Deployment
What is Kro?
Kro is a Kubernetes controller that manages ResourceGraphDefinitions - custom resources that define how to create and manage groups of related resources. TypeKro generates these definitions; Kro runs them.
Generate ResourceGraphDefinitions for the Kro controller to manage. Enables runtime dependencies and continuous reconciliation.
const factory = webapp.factory('kro', { namespace: 'prod' });
await factory.deploy({ name: 'my-app', image: 'nginx:latest', replicas: 5 });When to use:
- Production deployments with runtime dependencies
- Resources that reference each other's live state
- Continuous reconciliation requirements
- Advanced CEL expression evaluation
How it works:
- TypeKro generates a ResourceGraphDefinition
- Kro controller creates and manages resources
- CEL expressions evaluate against live cluster state
- Status updates automatically as resources change
Kro Factory with Event Monitoring
const factory = webapp.factory('kro', {
namespace: 'prod',
timeout: 600000,
eventMonitoring: {
enabled: true,
eventTypes: ['Warning', 'Error'],
includeChildResources: true
},
progressCallback: (event) => {
if (event.type === 'kubernetes-event') {
console.log(`K8s Event: ${event.message}`);
}
}
});Runtime Dependencies
Kro excels at runtime dependencies between resources:
import { kubernetesComposition } from 'typekro';
import { Deployment, Service } from 'typekro/simple';
const stack = kubernetesComposition(definition, (spec) => {
const db = Deployment({ id: 'db', name: 'postgres', image: 'postgres:15' });
const dbService = Service({
id: 'dbService',
name: 'postgres-svc',
selector: { app: 'postgres' },
ports: [{ port: 5432 }]
});
const app = Deployment({
id: 'app',
name: spec.name,
image: spec.image,
env: {
// Runtime reference - resolved by Kro against live cluster state
DATABASE_HOST: dbService.status.clusterIP
}
});
return {
ready: db.status.readyReplicas > 0 && app.status.readyReplicas > 0,
dbEndpoint: `${dbService.status.clusterIP}:5432`
};
});Label-Propagation Guard
The runtime bootstrap also installs a cluster-scoped MutatingAdmissionPolicy that stops anything other than the Kro controller from introducing Kro's ownership labels on an object. Without it, an operator that copies the parent CR's label map onto its children feeds those children to Kro's ApplySet pruner, which deletes them on every requeue.
There is no configuration option — the bootstrap reports status.labelPropagationGuard: 'active' | 'unavailable', and TYPEKRO_DISABLE_LABEL_GUARD=1 is the break-glass for a cluster where the policy misbehaves. The policy's group version is discovered from the target cluster at deploy time, never assumed: v1beta1 on Kubernetes 1.34/1.35, v1 from 1.36, and below 1.34 the API is not served, so the guard is skipped and reports unavailable. If discovery cannot reach the cluster at all — an unreachable API server, RBAC, a timeout — the guard is skipped too, but the warning says discovery failed rather than claiming the cluster is too old, and the failure is not cached as an answer. An offline toYaml() render has no cluster to ask and so leaves the guard out unless TYPEKRO_LABEL_GUARD_API_VERSION pins it. To build a graph for a known cluster outside a deployment, probe it and wrap the build: withLabelPropagationGuardCapability(await probeLabelPropagationGuardSupport(kubeConfig), () => …) — the cluster is always carried explicitly, never inherited from an earlier probe. See Runtime Bootstrap.
YAML Generation
Generate deterministic YAML for GitOps workflows. Works with ArgoCD, Flux, or any GitOps tool.
// Generate ResourceGraphDefinition YAML
const rgdYaml = webapp.toYaml();
// Generate instance YAML
const factory = webapp.factory('kro');
const instanceYaml = factory.toYaml({ name: 'prod-app', image: 'nginx:v1.0', replicas: 3 });
// Write to files for GitOps
writeFileSync('k8s/rgd.yaml', rgdYaml);
writeFileSync('k8s/instance.yaml', instanceYaml);When to use:
- Version-controlled infrastructure
- Audit trails and approval workflows
- CI/CD pipeline integration
- Team collaboration with pull requests
Configuration Options
Direct Factory Options
const factory = webapp.factory('direct', {
namespace: 'production',
timeout: 300000, // 5 minute timeout
waitForReady: true, // Wait for resources to be ready
// Event monitoring - stream control plane logs
eventMonitoring: {
enabled: true,
eventTypes: ['Normal', 'Warning', 'Error'],
includeChildResources: true,
deduplicationWindow: 60,
maxEventsPerSecond: 100
},
// Debug logging
debugLogging: {
enabled: true,
statusPolling: true,
readinessEvaluation: true,
verboseMode: false
},
// Progress callback for custom handling
progressCallback: (event) => {
console.log(`[${event.type}]`, event);
}
});Kro Factory Options
const factory = webapp.factory('kro', {
namespace: 'production',
timeout: 600000, // 10 minute timeout for complex graphs
// Event monitoring works with Kro mode too
eventMonitoring: {
enabled: true,
eventTypes: ['Warning', 'Error']
}
});Environment Patterns
Development
const devFactory = webapp.factory('direct', {
namespace: 'dev',
waitForReady: false, // Fast iteration
timeout: 60000,
debugLogging: { enabled: true, verboseMode: true }
});Production
const prodFactory = webapp.factory('kro', {
namespace: 'production',
timeout: 600000,
eventMonitoring: { enabled: true, eventTypes: ['Warning', 'Error'] }
});
// Or generate YAML for GitOps
const yaml = prodFactory.toYaml(prodSpec);Instance Lifecycle
Deployment
Both modes support waitForReady: true which blocks until all resources report ready:
const factory = app.factory('direct', {
namespace: 'production',
waitForReady: true, // Block until ready
timeout: 600000, // 10 minute timeout
});
const instance = await factory.deploy(spec);
// instance.status.ready === true (guaranteed)In direct mode, TypeKro re-executes the composition with live cluster data after deployment to hydrate status fields with real values (not proxy artifacts).
Deletion
Clean up with factory.deleteInstance(name):
await factory.deleteInstance('my-app');Direct mode: Uses graph-based reverse-topological deletion — resources are deleted in the opposite order they were deployed (App before Database, Database before Namespace). PVCs are cleaned up to unblock namespace termination.
KRO mode: Sends a DELETE to the custom resource instance. KRO's finalizer processes child resource cleanup via its applyset. After the instance is gone, TypeKro cleans up the RGD and CRD (only if no other instances share them).
Next Steps
- Getting Started - Deploy your first app
- External References - Cross-composition coordination