Basic Patterns
This guide demonstrates fundamental TypeKro patterns through practical examples. We'll cover simple web applications, database integration, and essential concepts like resource graphs, factory functions, cross-resource references, and deployment strategies.
Simple Web Application
Let's start with the basics: a simple web application with type-safe configuration and environment-specific settings.
What You'll Build
- A web application deployment running nginx
- A service to expose the application
- Type-safe configuration with environment variants
Complete Example
import { type } from 'arktype';
import {
toResourceGraph,
simpleDeployment,
simpleService,
Cel
} from 'typekro';
// Define the application interface
const WebAppSpec = type({
name: 'string',
image: 'string',
replicas: 'number',
environment: '"development" | "staging" | "production"'
});
const WebAppStatus = type({
url: 'string',
phase: '"pending" | "running" | "failed"',
readyReplicas: 'number'
});
// Create the resource graph
export const simpleWebApp = toResourceGraph(
{
name: 'simple-webapp',
apiVersion: 'example.com/v1alpha1',
kind: 'WebApp',
spec: WebAppSpec,
status: WebAppStatus,
},
// ResourceBuilder function - defines the Kubernetes resources
(schema) => ({
// Web application deployment
deployment: simpleDeployment({
name: schema.spec.name,
image: schema.spec.image,
replicas: schema.spec.replicas,
ports: [{ containerPort: 80 }],
// Environment-specific resource limits
resources: schema.spec.environment === 'production'
? { cpu: '500m', memory: '1Gi' }
: { cpu: '100m', memory: '256Mi' }
}),
// Service to expose the application
service: simpleService({
name: Cel.expr(schema.spec.name, '-service'),
selector: { app: schema.spec.name },
ports: [{ port: 80, targetPort: 80 }],
// LoadBalancer in production, ClusterIP elsewhere
type: schema.spec.environment === 'production' ? 'LoadBalancer' : 'ClusterIP'
})
}),
// StatusBuilder function - defines how status fields map to resource status
(schema, resources) => ({
url: schema.spec.environment === 'production'
? Cel.expr(
resources.service.status.loadBalancer.ingress,
'.size() > 0 ? "http://" + ',
resources.service.status.loadBalancer.ingress[0].ip,
': "pending"'
)
: Cel.template('http://%s', resources.service.spec.clusterIP),
phase: Cel.expr(
resources.deployment.status.readyReplicas,
'== ',
resources.deployment.spec.replicas,
'? "running" : "pending"'
),
readyReplicas: resources.deployment.status.readyReplicas
})
);
Deployment Options
Option 1: Direct Deployment
Perfect for development and testing:
// deploy-direct.ts
import { simpleWebApp } from './simple-webapp.js';
async function deployDirect() {
const factory = await simpleWebApp.factory('direct', {
namespace: 'development'
});
const instance = await factory.deploy({
name: 'my-webapp',
image: 'nginx:latest',
replicas: 2,
environment: 'development'
});
console.log('✅ Deployed successfully!');
// Check deployment status
const status = await factory.getStatus(instance);
console.log('📊 Status:', status);
}
deployDirect().catch(console.error);
Option 2: Generate YAML
For GitOps workflows:
// generate-yaml.ts
import { writeFileSync } from 'fs';
import { simpleWebApp } from './simple-webapp.js';
// Generate ResourceGraphDefinition for KRO
const rgdYaml = simpleWebApp.toYaml();
writeFileSync('webapp-definition.yaml', rgdYaml);
// Generate production instance
const prodInstanceYaml = simpleWebApp.toYaml({
metadata: { name: 'production-webapp', namespace: 'production' },
spec: {
name: 'production-webapp',
image: 'nginx:1.24',
replicas: 5,
environment: 'production'
}
});
writeFileSync('webapp-production.yaml', prodInstanceYaml);
// Generate development instance
const devInstanceYaml = simpleWebApp.toYaml({
metadata: { name: 'dev-webapp', namespace: 'development' },
spec: {
name: 'dev-webapp',
image: 'nginx:latest',
replicas: 1,
environment: 'development'
}
});
writeFileSync('webapp-development.yaml', devInstanceYaml);
console.log('📄 YAML files generated!');
Deploy with kubectl:
kubectl apply -f webapp-definition.yaml
kubectl apply -f webapp-production.yaml
Key Concepts Demonstrated
1. Type-Safe Configuration
The WebAppSpec
type ensures your configuration is valid:
// ✅ This works
const instance = await factory.deploy({
name: 'my-app',
image: 'nginx:latest',
replicas: 3,
environment: 'production'
});
// ❌ This causes a TypeScript error
const invalidInstance = await factory.deploy({
name: 'my-app',
image: 'nginx:latest',
replicas: '3', // Error: string not assignable to number
environment: 'prod' // Error: not a valid environment
});
2. Environment-Specific Configuration
Resources adapt based on the environment:
// Production gets more resources
resources: schema.spec.environment === 'production'
? { cpu: '500m', memory: '1Gi' }
: { cpu: '100m', memory: '256Mi' }
// Production gets LoadBalancer, others get ClusterIP
type: schema.spec.environment === 'production' ? 'LoadBalancer' : 'ClusterIP'
3. Status Builder
The status builder uses CEL expressions to reflect the actual state of your deployment:
// StatusBuilder function - maps resource status to your custom status
(schema, resources) => ({
url: schema.spec.environment === 'production'
? Cel.expr(
resources.service.status.loadBalancer.ingress,
'.size() > 0 ? "http://" + ',
resources.service.status.loadBalancer.ingress[0].ip,
': "pending"'
)
: Cel.template('http://%s', resources.service.spec.clusterIP),
phase: Cel.expr(
resources.deployment.status.readyReplicas,
'== ',
resources.deployment.spec.replicas,
'? "running" : "pending"'
),
readyReplicas: resources.deployment.status.readyReplicas
})
Database Integration
Now let's build a more complex example with a PostgreSQL database, demonstrating StatefulSets, Services, ConfigMaps, Secrets, and cross-resource references.
What You'll Build
- PostgreSQL StatefulSet with persistent storage
- Headless Service for StatefulSet pod discovery
- LoadBalancer Service for external access
- ConfigMap for database configuration
- Secret for sensitive credentials
- API application that connects to the database
Complete Database Example
import { type } from 'arktype';
import {
toResourceGraph,
simpleStatefulSet,
simpleService,
simpleConfigMap,
simpleSecret,
simpleDeployment,
Cel
} from 'typekro';
// Define the database schema
const DatabaseSpec = type({
name: 'string',
replicas: 'number>=1',
storageSize: 'string',
databaseName: 'string',
username: 'string',
password: 'string',
externalAccess: 'boolean'
});
const DatabaseStatus = type({
ready: 'boolean',
replicas: 'number',
primaryEndpoint: 'string',
externalEndpoint: 'string'
});
// Create the database resource graph
const database = toResourceGraph(
{
name: 'postgres-database',
apiVersion: 'data.example.com/v1',
kind: 'PostgresDatabase',
spec: DatabaseSpec,
status: DatabaseStatus
},
(schema) => ({
// Configuration for the database
config: simpleConfigMap({
name: Cel.template('%s-config', schema.spec.name),
data: {
// Database configuration
POSTGRES_DB: schema.spec.databaseName,
POSTGRES_USER: schema.spec.username,
PGPORT: '5432',
PGDATA: '/var/lib/postgresql/data/pgdata',
// Performance tuning
shared_preload_libraries: 'pg_stat_statements',
max_connections: '200',
shared_buffers: '256MB',
effective_cache_size: '1GB',
work_mem: '4MB'
}
}),
// Secret for sensitive data
credentials: simpleSecret({
name: Cel.template('%s-credentials', schema.spec.name),
data: {
POSTGRES_PASSWORD: schema.spec.password,
// Additional database users
REPLICATION_USER: 'replicator',
REPLICATION_PASSWORD: 'repl-secret-password'
}
}),
// StatefulSet for PostgreSQL with persistent storage
statefulSet: simpleStatefulSet({
name: schema.spec.name,
image: 'postgres:15',
replicas: schema.spec.replicas,
serviceName: Cel.template('%s-headless', schema.spec.name),
ports: [5432],
env: {
// Reference configuration and secrets
POSTGRES_DB: schema.spec.databaseName,
POSTGRES_USER: schema.spec.username,
POSTGRES_PASSWORD: schema.spec.password,
PGDATA: '/var/lib/postgresql/data/pgdata'
}
// Note: volumeClaimTemplates would be added in full StatefulSet specification
}),
// Headless service for StatefulSet pod discovery
headlessService: simpleService({
name: Cel.template('%s-headless', schema.spec.name),
selector: { app: schema.spec.name },
ports: [{ port: 5432, targetPort: 5432, name: 'postgres' }],
clusterIP: 'None' // Makes it headless
}),
// Regular service for database access
service: simpleService({
name: schema.spec.name,
selector: { app: schema.spec.name },
ports: [{ port: 5432, targetPort: 5432, name: 'postgres' }],
type: 'ClusterIP'
}),
// Conditional external service
...(schema.spec.externalAccess && {
externalService: simpleService({
name: Cel.template('%s-external', schema.spec.name),
selector: { app: schema.spec.name },
ports: [{ port: 5432, targetPort: 5432, name: 'postgres' }],
type: 'LoadBalancer'
})
})
}),
(schema, resources) => ({
ready: Cel.expr(
resources.statefulSet.status.readyReplicas, '>=', schema.spec.replicas
),
replicas: resources.statefulSet.status.readyReplicas,
primaryEndpoint: Cel.template(
'%s:5432',
resources.service.spec.clusterIP
),
externalEndpoint: schema.spec.externalAccess
? Cel.expr(
resources.externalService?.status.loadBalancer.ingress,
'.size() > 0 ? ',
resources.externalService?.status.loadBalancer.ingress[0].ip,
' + ":5432" : "pending"'
)
: 'disabled'
})
);
Application Connected to Database
Now let's create an API application that connects to our database:
// Application that connects to the database
const ApiAppSpec = type({
name: 'string',
image: 'string',
replicas: 'number',
databaseName: 'string'
});
const apiApp = toResourceGraph(
{
name: 'api-with-database',
apiVersion: 'apps.example.com/v1',
kind: 'ApiApp',
spec: ApiAppSpec,
status: type({ ready: 'boolean', url: 'string' })
},
(schema) => ({
// API deployment that connects to database
api: simpleDeployment({
name: schema.spec.name,
image: schema.spec.image,
replicas: schema.spec.replicas,
ports: [8080],
env: {
// Database connection configuration
DATABASE_URL: Cel.template(
'postgres://app:password@%s:5432/%s',
schema.spec.databaseName, // References database service
schema.spec.databaseName
),
DATABASE_HOST: schema.spec.databaseName,
DATABASE_PORT: '5432',
DATABASE_NAME: schema.spec.databaseName,
DATABASE_USER: 'app',
// Application configuration
PORT: '8080',
NODE_ENV: 'production'
}
}),
// Service for the API
apiService: simpleService({
name: schema.spec.name,
selector: { app: schema.spec.name },
ports: [{ port: 80, targetPort: 8080 }]
})
}),
(schema, resources) => ({
ready: Cel.expr(resources.api.status.readyReplicas, '> 0'),
url: Cel.template('http://%s', resources.apiService.spec.clusterIP)
})
);
// Deploy the complete system
async function deployDatabaseApp() {
// Deploy database first
const dbFactory = await database.factory('direct', { namespace: 'database' });
await dbFactory.deploy({
name: 'postgres-main',
replicas: 3,
storageSize: '50Gi',
databaseName: 'myapp',
username: 'app',
password: 'secure-password',
externalAccess: true
});
// Deploy API application
const apiFactory = await apiApp.factory('direct', { namespace: 'default' });
await apiFactory.deploy({
name: 'myapp-api',
image: 'myapp/api:v1.0',
replicas: 2,
databaseName: 'postgres-main'
});
}
Key Database Concepts
1. StatefulSet with Persistent Storage
statefulSet: simpleStatefulSet({
name: schema.spec.name,
image: 'postgres:15',
replicas: schema.spec.replicas,
serviceName: Cel.template('%s-headless', schema.spec.name),
ports: [5432]
})
StatefulSets provide:
- Stable network identities for database pods
- Ordered deployment and scaling
- Persistent storage per pod
- Stable hostnames for replication
2. Headless vs Regular Services
// Headless service for StatefulSet internal communication
headlessService: simpleService({
name: Cel.template('%s-headless', schema.spec.name),
selector: { app: schema.spec.name },
clusterIP: 'None' // Makes it headless
}),
// Regular service for application access
service: simpleService({
name: schema.spec.name,
selector: { app: schema.spec.name },
type: 'ClusterIP'
})
3. Configuration Management
// ConfigMap for non-sensitive configuration
config: simpleConfigMap({
name: Cel.template('%s-config', schema.spec.name),
data: {
POSTGRES_DB: schema.spec.databaseName,
POSTGRES_USER: schema.spec.username,
max_connections: '200'
}
}),
// Secret for sensitive data
credentials: simpleSecret({
name: Cel.template('%s-credentials', schema.spec.name),
data: {
POSTGRES_PASSWORD: schema.spec.password
}
})
4. Cross-Resource References
// API deployment references database
api: simpleDeployment({
env: {
DATABASE_URL: Cel.template(
'postgres://app:password@%s:5432/%s',
schema.spec.databaseName, // References database service
schema.spec.databaseName
)
}
})
Common Pattern Extensions
Add Health Checks
const deployment = simpleDeployment({
name: schema.spec.name,
image: schema.spec.image,
replicas: schema.spec.replicas,
ports: [{ containerPort: 80 }],
// Add health checks
livenessProbe: {
httpGet: { path: '/', port: 80 },
initialDelaySeconds: 30,
periodSeconds: 10
},
readinessProbe: {
httpGet: { path: '/', port: 80 },
initialDelaySeconds: 5,
periodSeconds: 5
}
});
Add ConfigMap
import { simpleConfigMap } from 'typekro';
const resources = {
config: simpleConfigMap({
name: Cel.expr(schema.spec.name, '-config'),
data: {
'nginx.conf': `
server {
listen 80;
location / {
return 200 'Hello from ${schema.spec.name}!';
}
}
`
}
}),
deployment: simpleDeployment({
name: schema.spec.name,
image: schema.spec.image,
volumeMounts: [{
name: 'config',
mountPath: '/etc/nginx/conf.d'
}],
volumes: [{
name: 'config',
configMap: { name: 'config.metadata.name' }
}]
})
};
Add Ingress
import { simpleIngress } from 'typekro';
// Only in production
...(schema.spec.environment === 'production' && {
ingress: simpleIngress({
name: Cel.expr(schema.spec.name, '-ingress'),
rules: [{
host: Cel.template('%s.example.com', schema.spec.name),
http: {
paths: [{
path: '/',
pathType: 'Prefix',
backend: {
service: {
name: 'service.metadata.name',
port: { number: 80 }
}
}
}]
}
}]
})
})
Conditional Resources
Create resources only when certain conditions are met:
const resources = {
app: simpleDeployment({ /* ... */ }),
// Only create external service if external access is enabled
...(schema.spec.externalAccess && {
externalService: simpleService({
name: Cel.template('%s-external', schema.spec.name),
type: 'LoadBalancer'
})
}),
// Only create ingress in production
...(schema.spec.environment === 'production' && {
ingress: simpleIngress({
name: Cel.expr(schema.spec.name, '-ingress'),
host: Cel.template('%s.example.com', schema.spec.name)
})
})
};
Testing Your Deployments
Verify Resources
# Check pods
kubectl get pods -l app=my-webapp
# Check service
kubectl get service my-webapp-service
# Check your custom resource (if using KRO)
kubectl get webapp my-webapp -o yaml
Access Your Application
For development (ClusterIP):
kubectl port-forward service/my-webapp-service 8080:80
curl http://localhost:8080
For production (LoadBalancer):
kubectl get service my-webapp-service -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
curl http://<EXTERNAL-IP>
Database Access
For development:
kubectl port-forward service/postgres-main 5432:5432
psql -h localhost -p 5432 -U app -d myapp
For external access:
kubectl get service postgres-main-external -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
psql -h <EXTERNAL-IP> -p 5432 -U app -d myapp
Best Practices
1. Use Secrets for Passwords
Always store sensitive data in Kubernetes Secrets:
credentials: simpleSecret({
name: 'db-credentials',
data: {
POSTGRES_PASSWORD: process.env.DB_PASSWORD // From environment
}
})
2. Configure Resource Limits
Set appropriate resource limits for your workloads:
deployment: simpleDeployment({
name: 'my-app',
image: 'nginx:latest',
resources: {
requests: { cpu: '100m', memory: '256Mi' },
limits: { cpu: '500m', memory: '1Gi' }
}
})
3. Use Descriptive Names
// ✅ Good
const userApiDeployment = simpleDeployment({ name: 'user-api' });
const userApiService = simpleService({ name: 'user-api-service' });
// ❌ Avoid
const d1 = simpleDeployment({ name: 'app' });
const s1 = simpleService({ name: 'svc' });
4. Environment-Specific Configuration
const config = schema.spec.environment === 'production'
? { replicas: 5, resources: { cpu: '500m', memory: '1Gi' } }
: { replicas: 1, resources: { cpu: '100m', memory: '256Mi' } };
const deployment = simpleDeployment({
name: schema.spec.name,
image: schema.spec.image,
replicas: config.replicas,
resources: config.resources
});
5. Validate Configuration Early
async function deployApp(input: unknown) {
const spec = AppSpec(input); // Validate immediately
if (spec instanceof type.errors) {
throw new Error(`Invalid spec: ${spec.summary}`);
}
// Proceed with validated data
const factory = await app.factory('direct', { namespace: 'default' });
return factory.deploy(spec);
}
Next Steps
Now that you understand the basic patterns, try these examples:
- Microservices - Multiple interconnected services
- Multi-Environment - Deploy across environments
- CI/CD - Continuous integration and deployment
- Monitoring - Set up monitoring and observability
Or explore advanced topics:
- Runtime Behavior - Status hydration and cross-references
- CEL Expressions - Add dynamic logic
- Factories - Build custom factory functions
- Deployment Strategies - Learn different deployment methods